Compare commits

..

131 Commits

Author SHA1 Message Date
a8f012066c removed horizontally-centre-aligned class to be superseded by ImageRow component 2026-05-05 13:16:54 +02:00
165d0b7042 ContentSidebar now uses grid; adjusted sizing 2026-04-27 15:05:31 +02:00
cf125809bd improved responsiveness of ContentSidebar and TableOfContents when resizing window 2026-04-27 14:49:48 +02:00
72347769c3 improved styling of side-bound TableOfContents component 2026-04-26 22:39:07 +02:00
a1effdec8e fixed ContentSidebar sizing and margin of first item 2026-04-26 22:34:02 +02:00
3586d7eece replaced manually-added table of contents with automatic ContentSidebar toc where needed 2026-04-26 22:22:29 +02:00
b24712ef4c banner now controlled by Content and ContentSidebar components 2026-04-26 22:13:15 +02:00
feebf17bd8 removed blur effect from projects page to counteract performance drop on that page 2026-04-26 21:17:36 +02:00
5edd4c7a6d improved interactivity of drawing gallery page 2026-04-26 21:08:16 +02:00
9ff26d3c0b swapped copyparty for drawing gallery link on main page; display latest drawing date there too; added drawing gallery link to header and footer 2026-04-26 12:30:15 +02:00
1cc7323c36 main page can now fetch blog post updated dates 2026-04-26 12:19:43 +02:00
331b8dd95b ImageRow now deprioritises unhovered cards only when container is hovered 2026-04-26 12:15:28 +02:00
1348be3ee9 fixed alt text on feed banner 2026-04-26 10:47:45 +02:00
d746f8ef6f componised ImageRow from /feed 2026-04-26 10:46:56 +02:00
455eb4c291 added outline to drawing gallery on hover 2026-04-26 10:16:27 +02:00
ac123ad0d7 updated LinkRow styling 2026-04-25 12:24:53 +02:00
3a9d4dd2fc moved feed post metadata out of individual posts to be more easily accessible 2026-04-25 12:14:14 +02:00
83c8cdaa34 removed drawings from projects and feed pages 2026-04-24 22:37:28 +02:00
6ab1b392de changed redirect of art/drawings 2026-04-24 22:31:12 +02:00
6578c74b60 added drawings to drawing gallery 2026-04-24 22:30:28 +02:00
353a3a1846 added /drawings 2026-04-24 21:30:33 +02:00
7e78be19da updated gallery description texts for deej0461 2026-04-24 21:02:28 +02:00
dc9907f264 hid 88x31 buttons for the time being 2026-04-24 20:51:25 +02:00
df1a4c8db4 added fancy marker for latest update date in main page link row 2026-04-24 20:48:22 +02:00
db236538a0 added date of latest feed post to main page 2026-04-24 20:33:30 +02:00
f2476dbe8a added alt texts to feed post 2026/0410 2026-04-24 20:09:50 +02:00
97b29f8dce added new project to feed 2026-04-24 20:08:21 +02:00
40d40a187a media player: play button only displayed on playing track 2026-04-20 10:13:30 +02:00
c0901a63aa added track numbers to media player 2026-04-19 21:50:08 +02:00
bdc38cef4e media player now takes custom colour to style itself 2026-04-19 21:41:10 +02:00
b87e334117 added media player to dreamworld project page 2026-04-19 21:10:33 +02:00
c92da6d766 edited styling of media player component 2026-04-19 21:02:39 +02:00
46cd759a5e added music player component 2026-04-18 22:44:50 +02:00
46ba3965e9 created structure and added some entries for music rotation 2026-04-16 09:00:29 +02:00
362b876892 projects status now end-aligned 2026-04-15 17:17:22 +02:00
f9b73851f3 SubtitledImage now respects image width; ImageSubtitle now centers its content 2026-04-15 17:14:43 +02:00
f910b44129 SubtitledImage now using rounded corners 2026-04-15 16:42:04 +02:00
1d9eaa5bdd scroll top button now uses #top instead of custom javascript 2026-04-15 16:36:54 +02:00
78eea262b2 cleaned up some components conditional css class assignment 2026-04-15 16:27:44 +02:00
4434a01341 added death.webp to projektike page 2026-04-15 13:57:51 +02:00
84d6a06156 updated projektike page 2026-04-15 13:51:37 +02:00
aec836fe6a Video component can now fetch content from remote 2026-04-15 13:41:03 +02:00
832f6d2f27 changed reaction images to use specific temp images 2026-04-14 17:55:15 +02:00
143bc5a67d reduced font size 2026-04-14 17:26:16 +02:00
1c6231d47d /now now redirects to /meta/now 2026-04-14 12:27:57 +02:00
f504ba99bf added blog post 2026/0414 2026-04-14 12:26:55 +02:00
21ec4f8aed edited sidebar text blurb on main page 2026-04-14 10:51:42 +02:00
354b37221b added temporary content to now and music-gallery pages; banner2 can now display dateUpdated without other dates set 2026-04-14 10:51:19 +02:00
bbd1b70306 edited text blurb and minor things on main page 2026-04-14 10:33:53 +02:00
67f0f6e8bc removed gitea from main page gallery and added feed links to header and footer 2026-04-14 09:28:43 +02:00
02521366f8 updated links in footer 2026-04-14 08:43:50 +02:00
87fe913035 created now and music-rotation pages; moved privacy page to /other 2026-04-14 08:40:45 +02:00
238deb5756 removed updates page 2026-04-14 08:22:31 +02:00
8b3faec367 fixed some css for feed images 2026-04-14 07:41:46 +02:00
c85f332c2e feed page indicator now more clearly communicates button behaviour when trying to go to a page earlier than first or later than last 2026-04-13 22:29:29 +02:00
37362018fc added feed to main page 2026-04-13 22:26:01 +02:00
aeb8361be8 banners now clickable; added banner to feed page 2026-04-13 22:24:07 +02:00
a88c02922b fancied up feed images 2026-04-13 22:08:04 +02:00
6a9f7f763f added content to feed posts; removed march drawings feed post stub 2026-04-13 21:54:08 +02:00
c30097f781 feed images now clickable 2026-04-13 21:26:15 +02:00
718829bc08 styled feed image gallery 2026-04-13 21:24:59 +02:00
de8223aec8 added file for transparent phone case feed post; added images for that post; added images and imagesAlt attributes to feed posts 2026-04-13 21:17:32 +02:00
02c0753c94 improveed image gallery mobile layout 2026-04-13 19:11:00 +02:00
188363d049 restored feed page 2026-04-13 16:54:50 +02:00
c076be1d5e added bottom margin to main page row entries 2026-04-13 16:30:21 +02:00
df47346a5d added quote reaction and added quote reaction to 2026/0402 blog post 2026-04-13 16:24:53 +02:00
019e94e779 adjusted some text on projects page and positioning of image on main page 2026-04-13 15:10:32 +02:00
5cfd093910 image gallery now stacks vertically on mobile 2026-04-12 12:44:27 +02:00
d13e4516da updated image gallery to use side-to-side view 2026-04-12 12:40:12 +02:00
d5777351e6 added ReactionQuote component 2026-04-12 11:30:01 +02:00
1b41127100 banner2: dates no longer go below the title 2026-04-12 07:19:44 +02:00
4e3e6a455e added descriptions to new images on avh plan project page 2026-04-11 11:49:40 +02:00
ca545ab1d1 adjusted image gallery text shadow and brightness on hover 2026-04-11 10:44:10 +02:00
b0842d9fbd added images for avh plan project page 2026-04-11 10:42:23 +02:00
b447adec0c added filter tags to projects; added zoom-out animation when hovering over a project page 2026-04-10 22:05:26 +02:00
2067516591 added more intense shadow to project page text; added status marker 2026-04-10 21:30:29 +02:00
d6cf112d05 added banner for avhplan 2026-04-09 22:17:53 +02:00
35cfd46e21 added more images to deej0461 2026-04-09 22:04:53 +02:00
8647da389d sorted project assets into new folder structure and added (temporary?) banners for all projects 2026-04-09 21:56:23 +02:00
9bc15030b3 edited style of projects page entries; now fetching banner correctly, respecting both relative and absolute paths 2026-04-09 21:45:37 +02:00
ed12465263 tried some styling for projects page 2026-04-08 22:13:27 +02:00
a774173f0b banner now zooms in on hover 2026-04-08 21:38:13 +02:00
a59179505a added rounded corners to table of contents container 2026-04-08 21:32:07 +02:00
a0320ba7a2 added rounded corners to some main page elements 2026-04-08 21:31:35 +02:00
4f9bd66b2c updated homesick link list look 2026-04-08 18:06:59 +02:00
c014e13b3f removed blog post 2026/0325 and i-made-this tag 2026-04-08 15:39:17 +02:00
d002c97846 main page: moved projects page to upper row; removed art feed 2026-04-08 15:36:28 +02:00
ab903f8873 removed art feed from header/footer 2026-04-08 15:34:25 +02:00
1a8a7523e9 removed art and art feed pages; added blurb to project page, which is half-merged with that of the former art feed 2026-04-08 15:31:03 +02:00
a3d0f657f8 ordered projects chronologically 2026-04-08 15:09:46 +02:00
040862a3e5 styled links in project posts 2026-04-08 15:04:44 +02:00
391debf3b4 removed width-expand effect on hover in homesick link gallery 2026-04-08 14:40:20 +02:00
7c3052b4f0 transferred all project pages to markdown 2026-04-08 14:37:02 +02:00
9a74a6b828 added links to projects pages (not yet styled) 2026-04-08 14:20:58 +02:00
0e08a0e05b moved music posts to markdown files; added links to projects2 entries 2026-04-08 14:19:47 +02:00
98b518a150 added dateIndeterminate parameter to banner2 for miscellaneous dates 2026-04-08 14:01:58 +02:00
9d0a13be30 moved static resources from feed to projects 2026-04-08 13:54:30 +02:00
82c8412144 added description parameter for projects 2026-04-08 13:46:09 +02:00
e936693891 added slugs for project pages 2026-04-08 13:13:59 +02:00
73514f1fbf moved existing feed files to project directory 2026-04-08 13:00:09 +02:00
4a1ba5bdbc moved daisy fm page to markdown 2026-04-08 12:59:26 +02:00
eb99e7fdca removed tags from existing feed posts 2026-04-08 12:45:00 +02:00
9f867b11c5 created projects2 for project page overhaul + merge with art feed 2026-04-08 12:41:54 +02:00
792483232f added lightyears font to feed 2026-04-07 19:31:58 +02:00
224341e3e3 changed image gallery arrows 2026-04-07 18:46:20 +02:00
abdfc505d2 added page indicator 2026-04-07 18:37:54 +02:00
830b5163cb added functional pagination to feed 2026-04-07 17:33:25 +02:00
692d9d8aff laid groundwork to modularise new feed page 2026-04-07 14:58:53 +02:00
2ace713455 added before/after the challenge hints to drawing challenge feed gallery 2026-04-07 08:37:18 +02:00
41084ead1e image gallery improvements: sizing corrected, button padding edited, removed clipping corners from images filling the entire component 2026-04-07 08:35:55 +02:00
0d608a6287 added zone for image gallery hover; added sentence to feed 2026-04-06 21:13:29 +02:00
5c0f1e338c reversed order of drawings on feed 2026-04-06 21:09:56 +02:00
d2a2839d3e added border radius to footer 2026-04-06 20:52:40 +02:00
e88eb805fa added shadow to image gallery desc text for better legibility 2026-04-06 20:36:01 +02:00
70730e166b deleted test page 2026-04-06 20:27:28 +02:00
3d5bb5e096 updated main page 2026-04-06 20:27:09 +02:00
56f7b1c847 added drawings to feed 2026-04-06 20:20:56 +02:00
c385e66162 added /feed page to replace /projects/small and /art/drawings; set up appropriate redirects 2026-04-06 20:08:26 +02:00
6d1a38775d styled new image gallery component; added description and index texts 2026-04-06 19:23:22 +02:00
da62a57bfb added image gallery component; moved blurred background to separate CSS class 2026-04-06 17:53:40 +02:00
256b3d4142 moved water background to separate component 2026-04-06 17:17:01 +02:00
eafe3cbe92 added blog post 2026/0404 2026-04-04 22:32:30 +02:00
f599773c97 updated callout in blog post 2026/0402 2026-04-03 21:54:01 +02:00
d469870619 updated callout in blog post 2026/0317 2026-04-03 21:52:49 +02:00
c82e05e71e updated banner for slightly better mobile experience 2026-04-03 21:47:41 +02:00
d7a3e2efd1 updated about page to remove obsolete information 2026-04-03 21:45:34 +02:00
cb77f341da colour of callout now depends on type 2026-04-03 00:03:05 +02:00
9a55acba5a wrote more for the new blog post 2026-04-02 23:57:12 +02:00
f0726cba4a added callout component 2026-04-02 22:54:42 +02:00
77fa3f8200 added new blog post (unpublished) 2026-04-02 22:20:22 +02:00
682b90cc36 main page sidebar now responds to narrow screen width 2026-04-02 16:58:30 +02:00
175 changed files with 3694 additions and 1783 deletions

View File

@@ -1 +0,0 @@
go fuck yourself

View File

@@ -1,46 +1,43 @@
<script lang="ts">
import { type BannerContent } from './components/banner-content';
let {
title,
date = "", // date posted
dateUpdated = "",
subtitle = "",
banner = "",
bannerAlt = "",
tags = [],
pixelated,
content,
}: {
title: string;
date?: string;
dateUpdated?: string;
subtitle?: string;
banner?: string;
bannerAlt?: string;
tags?: string[];
pixelated?: boolean;
content: BannerContent;
} = $props();
</script>
{#snippet titles({title, subtitle, date}: {title: string, subtitle: string, date: string})}
<h1 class="title">{title}</h1>
{#snippet titles()}
<div class="title-container">
<div class="title-text-container">
{#if subtitle}
<p class="subtitle">[ {subtitle} ]</p>
<h1 class="title">{content.title}</h1>
{#if content.subtitle}
<p class="subtitle">[ {content.subtitle} ]</p>
{/if}
{#if tags.length}
{#if content.tags && content.tags.length > 0}
<div class="tag-container">
{#each tags as tag}
{#each content.tags as tag}
<span class="post-tag">{tag}</span>
{/each}
</div>
{/if}
</div>
{#if date}
{#if content.date || content.dateUpdated || content.dateIndeterminate}
<div class="date-container">
<p class="date">posted :: {date}</p>
{#if dateUpdated}
<p class="date">last updated :: {dateUpdated}</p>
{#if content.dateIndeterminate && content.dateIndeterminate != "undefined"}
<p class="date">:: {content.dateIndeterminate}</p>
{/if}
{#if content.date && content.date != "undefined"}
<p class="date">posted :: {content.date}</p>
{/if}
{#if content.dateUpdated && content.dateUpdated != "undefined"}
<p class="date">last updated :: {content.dateUpdated}</p>
{/if}
</div>
{/if}
@@ -48,14 +45,13 @@
{/snippet}
<div class="container">
{#if banner && banner !== ""}
{#if pixelated}
<img class="banner pixelated-img" src="{banner}" alt="{bannerAlt}">
{:else}
<img class="banner" src="{banner}" alt="{bannerAlt}">
{/if}
{#if content.banner && content.banner !== ""}
<a class="banner-container" href={content.banner}>
<img class="banner {content.pixelated ? "pixelated-img" : ""}" src={content.banner} alt={content.bannerAlt}>
</a>
{/if}
{@render titles({title, subtitle, date})}
{@render titles()}
<hr>
</div>
@@ -64,12 +60,26 @@
width: 100%;
}
.banner {
.banner-container {
max-height: 300px;
height: 300px;
width: 100%;
overflow: hidden;
margin: 0;
display: block;
}
.banner {
width: 100%;
height: 100%;
object-fit: cover;
transition: scale var(--duration-animation) var(--anim-curve);
}
.banner-container:hover .banner {
scale: 1.04;
}
.title-container {
display: flex;
flex-direction: row;
@@ -92,6 +102,8 @@
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 4px;
--color-tag-filters-bg: var(--color-background-highlight-alt);
}
.date-container {
@@ -99,6 +111,8 @@
display: flex;
flex-direction: column;
align-items: flex-end;
align-self: flex-end;
margin-top: 16px;
}
.title {
@@ -141,5 +155,10 @@
font-size: 0.9rem;
line-height: 1.2rem;
}
.banner-container {
height: 180px;
max-height: 180px;
}
}
</style>

View File

@@ -0,0 +1,11 @@
export interface BannerContent {
title: string;
date?: string; // date posted
dateUpdated?: string;
dateIndeterminate?: string; // raw date without an explanation marker next to it
subtitle?: string;
banner?: string;
bannerAlt?: string;
tags?: string[];
pixelated?: boolean;
}

View File

@@ -1,38 +0,0 @@
<script lang="ts">
let {
text,
onClick,
fullWidth,
}: {
text: string;
onClick: () => undefined;
fullWidth?: boolean;
} = $props();
</script>
{#if fullWidth}
<button class="outlined-button outlined-button-fullwidth" onclick={onClick}>{text}</button>
{:else}
<button class="outlined-button" onclick={onClick}>{text}</button>
{/if}
<style>
.outlined-button {
font-family: var(--font-mono);
font-size: var(--font-size-mono);
padding: 8px;
border: var(--border-style) var(--border-dash-size) var(--color-highlight);
color: var(--color-highlight);
font-weight: 700;
cursor: pointer;
transition: background-color var(--duration-animation) var(--anim-curve);
}
.outlined-button:hover {
background-color: var(--color-background-highlight);
}
.outlined-button-fullwidth {
width: 100%;
}
</style>

View File

@@ -0,0 +1,73 @@
<script lang="ts">
// currently available reactions: pointing, joy, quote, warn
let {
reaction,
text,
}: {
reaction: string;
text: string;
} = $props();
function getReactionAlt(r: string): string {
switch (r) {
case "joy":
return "";
case "quote":
return "";
case "pointing":
return "";
case "warn":
return "";
default:
return "reaction image missing";
}
}
</script>
<div class="reaction-container blurred-background">
<div class="reaction-content-container">
<img src="/reactions/{reaction}.webp" alt={getReactionAlt(reaction)}>
<p>{@html text}</p>
</div>
</div>
<style>
.reaction-container {
max-width: 800px;
margin: 32px auto;
overflow: hidden;
}
.reaction-content-container {
display: flex;
flex-direction: row;
align-items: center;
margin: 0 64px;
padding: 16px;
border-radius: var(--border-radius);
border-left: var(--border-style) var(--border-dash-size) var(--color-text);
border-right: var(--border-style) var(--border-dash-size) var(--color-text);
gap: 16px;
}
img {
width: 160px;
}
p {
margin: 0;
}
@media screen and (max-width: 600px) {
.reaction-content-container {
flex-direction: column;
gap: 8px;
}
}
@media screen and (max-width: 450px) {
.reaction-content-container {
margin: 0 32px;
}
}
</style>

View File

@@ -34,18 +34,24 @@
<div class="webring-container">
<div class="webring-row">
<span></span>
<a href="{ringLink}">{highlightEmojiLeft} {ringName} {highlightEmojiRight ?? highlightEmojiLeft}</a>
<span></span>
</div>
<div class="webring-row">
<span></span>
<a href="{prevLink}">{prevSymbol} prev</a>
{#if randLink}
<a href="{randLink}">rand</a>
{/if}
{#if listLink}
<a href="{listLink}">list</a>
{/if}
<a href="{nextLink}">next {nextSymbol}</a>
<span></span>
</div>

View File

@@ -1,34 +1,44 @@
<script lang="ts">
let y: number;
function scrollToTop() {
document.documentElement.scrollTop = 0;
}
</script>
<svelte:window bind:scrollY={y} />
{#if y > 2400}
<button class="scroll-top-button" onclick={scrollToTop}>↑</button>
{/if}
<a
href="#top"
class="
scroll-top-button
blurred-background
{y > 2400 ? "scroll-top-button-visible" : ""}
"
></a>
<style>
.scroll-top-button {
position: fixed;
bottom: 24px;
right: 48px;
font-size: 1.4rem;
font-size: 1.6rem;
font-weight: 700;
cursor: pointer;
border: var(--border-style) var(--border-dash-size) var(--color-highlight);
color: var(--color-highlight);
background-color: var(--color-background-highlight);
backdrop-filter: blur(var(--blur-radius-background));
padding: 12px;
transition: background-color var(--duration-animation) var(--anim-curve);
border-radius: var(--border-radius);
border-top: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
border-bottom: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
color: var(--color-text);
background-color: var(--color-background-highlight-alt);
padding: 14px 22px;
transition: background-color var(--duration-animation) var(--anim-curve),
opacity var(--duration-animation) var(--anim-curve);
z-index: 10;
opacity: 0;
text-decoration: none;
}
.scroll-top-button:hover {
background-color: var(--color-background-highlight-hover);
background-color: var(--color-background-highlight-hover-alt);
}
.scroll-top-button-visible {
opacity: 1;
}
</style>

View File

@@ -21,15 +21,31 @@
} = $props();
</script>
{#snippet subtitledImageContent()}
<a class="
subtitled-img-container
blurred-background
{smaller ? "subtitled-img-reduced-size" : ""}
{alignment == "left"
? "subtitled-img-container-left"
: alignment == "right"
? "subtitled-img-container-right"
: "subtitled-img-container-centred"
}
"
href="{image}"
>
{#if video}
<Video src={image} />
{:else}
{#if pixelated}
<img class="subtitled-img pixelated-img" src="{image}" alt="{altText}">
{:else}
<img class="subtitled-img" src="{image}" alt="{altText}">
{/if}
<img class="
subtitled-img
{pixelated ? "pixelated-img" : ""}
{subtitle ? "subtitled-img-sub" : "subtitled-img-no-sub"}
"
src="{image}"
alt="{altText}"
>
{/if}
{#if subtitle}
@@ -39,40 +55,7 @@
<p class="subtitled-img-text">{subtitle}</p>
</div>
{/if}
{/snippet}
<!-- this structure is ugly as fuck. there must be a better way of doing this -->
{#if alignment && alignment == "left"}
{#if smaller}
<a class="subtitled-img-container subtitled-img-container-left subtitled-img-reduced-size" href="{image}">
{@render subtitledImageContent()}
</a>
{:else}
<a class="subtitled-img-container subtitled-img-container-left" href="{image}">
{@render subtitledImageContent()}
</a>
{/if}
{:else if alignment && alignment == "right"}
{#if smaller}
<a class="subtitled-img-container subtitled-img-container-right subtitled-img-reduced-size" href="{image}">
{@render subtitledImageContent()}
</a>
{:else}
<a class="subtitled-img-container subtitled-img-container-right" href="{image}">
{@render subtitledImageContent()}
</a>
{/if}
{:else}
{#if smaller}
<a class="subtitled-img-container subtitled-img-container-centred subtitled-img-reduced-size" href="{image}">
{@render subtitledImageContent()}
</a>
{:else}
<a class="subtitled-img-container subtitled-img-container-centred" href="{image}">
{@render subtitledImageContent()}
</a>
{/if}
{/if}
</a>
<style>
.subtitled-img-container {
@@ -81,13 +64,13 @@
border: var(--border-dash-size) var(--color-highlight) var(--border-style);
text-decoration: none;
box-sizing: border-box;
backdrop-filter: blur(var(--blur-radius-background));
transition: background-color var(--duration-animation) var(--anim-curve);
border-radius: var(--border-radius);
}
.subtitled-img-container-centred {
width: var(--media-width);
/* width: fit-content; */
max-width: var(--media-width);
width: fit-content;
margin-left: auto;
margin-right: auto;
}
@@ -97,9 +80,21 @@
}
.subtitled-img {
width: 100%;
max-width: 100%;
/* margin: 0; */
width: fit-content;
box-sizing: border-box;
padding: 8px;
overflow: hidden;
}
.subtitled-img-sub {
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.subtitled-img-no-sub {
border-radius: var(--border-radius);
}
hr {

View File

@@ -1,5 +1,12 @@
<script lang="ts">
import {onMount} from 'svelte';
import { onMount } from 'svelte';
let {
type,
}: {
// possible values: none, 'side'
type: string;
} = $props();
interface TocEntry {
text: string;
@@ -63,7 +70,9 @@
{/snippet}
{#if tocEntries.length > 0}
<div class="toc-container">
<div class="{type ? "toc-container-side" : "toc-container"} blurred-background">
<p class="toc-header">on this page</p>
<ul class="toc-list">
{#each tocEntries as entry}
{@render tocEntryLine({ entry })}
@@ -74,21 +83,39 @@
<style>
:global {
body {
--padding-indent-base: 44px;
--padding-level-indent: 24px;
.toc-container, .toc-container-side {
box-sizing: border-box;
padding: 16px 0;
border-radius: var(--border-radius);
}
.toc-container {
--padding-indent-base: 44px;
--padding-level-indent: 24px;
max-width: var(--width-toc);
margin-left: auto;
margin-right: auto;
margin-top: 12px;
box-sizing: border-box;
background-color: var(--color-background-highlight);
padding: 16px 0;
border: var(--border-style) var(--border-dash-size) var(--color-highlight);
backdrop-filter: blur(var(--blur-radius-background));
background-color: var(--color-background-highlight);
}
.toc-container-side {
--padding-indent-base: 20px;
--padding-level-indent: var(--padding-indent-base);
/* width: 300px; */
margin-left: 12px;
margin-right: 12px;
position: sticky;
top: 64px;
border-left: var(--border-style) var(--border-dash-size) var(--color-highlight);
}
.toc-container-side .toc-list a {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
.toc-list {
@@ -96,6 +123,14 @@
margin: 0;
width: 100%;
}
.toc-header {
margin: 0 var(--padding-indent-base);
font-weight: 700;
font-style: italic;
color: var(--color-highlight);
font-family: var(--font-mono);
}
.toc-list a {
width: 100%;

View File

@@ -1,87 +0,0 @@
<script lang="ts">
export interface UpdateEntry {
date: string;
time: string;
content: string;
link?: string;
}
let {
entry,
}: {
entry: UpdateEntry;
} = $props();
</script>
<div class="update-entry">
<div class="update-entry-timestamp-container">
<span>{entry.date}</span>
<span class="update-entry-timestamp-comma">,&nbsp;</span>
<span>{entry.time}</span>
</div>
<span class="update-entry-timestamp-divider">::</span>
<p>
{@html entry.content}
{#if entry.link}
<a class="update-entry-link" href="{entry.link}">»</a>
{/if}
</p>
</div>
<style>
.update-entry * {
margin: 0;
}
.update-entry {
display: flex;
flex-direction: row;
gap: 8px;
margin: 4px 0;
}
.update-entry p {
font-size: 1.0rem;
line-height: 1.3rem;
}
.update-entry-timestamp-container {
display: flex;
flex-direction: row;
}
.update-entry-timestamp-container *, .update-entry-timestamp-divider {
font-family: var(--font-mono);
font-size: 0.8rem;
line-height: 1.3rem;
font-weight: 500;
color: var(--color-highlight);
min-width: fit-content;
}
.update-entry-link {
font-family: var(--font-mono);
font-size: 1.2rem;
color: var(--color-highlight);
text-decoration: none;
line-height: 1.3rem;
}
.update-entry-link:hover {
font-weight: 700;
}
@media screen and (max-width: 550px) {
/* Align timestamp texts vertically */
.update-entry-timestamp-container {
flex-direction: column;
align-items: end;
}
/* Hide separating comma */
.update-entry-timestamp-comma {
display: none;
}
}
</style>

View File

@@ -15,7 +15,8 @@
.image-subtitle-container {
display: flex;
flex-direction: row;
width: var(--media-width);
justify-content: center;
max-width: var(--media-width);
margin: 0 auto;
}

View File

@@ -10,7 +10,7 @@
<div class="entry-container">
{#each posts as post}
<a class="entry" href="{post.key}">
<a class="entry blurred-background-hover" href="{post.key}">
<div class="entry-banner-container">
<img class="entry-banner" src="{post.key}/{post.post.banner}" alt="{post.post.bannerAlt}">
</div>
@@ -19,7 +19,7 @@
<p class="entry-date">
::&nbsp;{post.post.date}
{#if post.post.dateUpdated}
// {post.post.dateUpdated}
//&nbsp;{post.post.dateUpdated}
{/if}
</p>
<p class="entry-description">{post.post.description}</p>
@@ -55,7 +55,6 @@
.entry:hover {
background-color: var(--color-background-highlight-alt);
border-color: var(--color-highlight-alt);
backdrop-filter: blur(var(--blur-radius-background));
}
.entry:hover .entry-banner {
@@ -120,6 +119,8 @@
gap: 4px;
flex-direction: row;
flex-wrap: wrap;
--color-tag-filters-bg: var(--color-background-highlight-alt);
}
@media screen and (max-width: 900px) {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
export interface GalleryEntry {
export interface LinkEntry {
title: string;
subtitle: string;
img: string;
@@ -11,27 +11,25 @@
let {
entries,
}: {
entries: GalleryEntry[];
entries: LinkEntry[];
} = $props();
</script>
<div class="post-list">
<div class="hs-post-list">
{#each entries as entry}
{@render galleryEntry({entry})}
{@render linkEntry({entry})}
{/each}
</div>
{#snippet galleryEntry({entry}: {entry: GalleryEntry})}
<a class="gallery-container" href="{entry.link}">
{#if entry.img && entry.img !== ""}
<img class="gallery-img" src="{entry.img}" alt="{entry.imgAlt}">
{:else}
<div class="gallery-img-placeholder"></div>
{/if}
<div class="gallery-text-container">
<p class="gallery-subtitle">{@html entry.subtitle}</p>
<p class="gallery-title">{entry.title}</p>
<p class="gallery-description">{entry.description}</p>
{#snippet linkEntry({entry}: {entry: LinkEntry})}
<a class="hs-list-container blurred-background-hover" href="{entry.link}">
<div class="hs-list-img-container">
<img class="hs-list-img" src="{entry.img}" alt="{entry.imgAlt}">
</div>
<div class="hs-list-text-container">
<p class="hs-list-subtitle">{@html entry.subtitle}</p>
<p class="hs-list-title">{entry.title}</p>
<p class="hs-list-description">{entry.description}</p>
</div>
</a>
{/snippet}
@@ -42,13 +40,13 @@
--color-laura-darker: #A55051CC;
}
.post-list {
.hs-post-list {
max-width: 1600px;
margin-left: auto;
margin-right: auto;
}
.gallery-container {
.hs-list-container {
box-sizing: content-box;
height: 120px;
display: flex;
@@ -58,23 +56,26 @@
margin: 0;
justify-content: center;
border: var(--border-style) transparent var(--border-dash-size);
border-radius: var(--border-radius);
transition: border-color var(--duration-animation) var(--anim-curve);
}
.gallery-img, .gallery-img-placeholder {
width: 180px;
min-width: 180px;
.hs-list-img-container {
width: 220px;
min-width: 220px;
height: 100%;
overflow: hidden;
transition: border-radius var(--duration-animation) var(--anim-curve);
}
.hs-list-img {
width: 100%;
height: 100%;
margin: 0;
object-fit: cover;
transition: width var(--duration-animation) var(--anim-curve);
transition: scale var(--duration-animation) var(--anim-curve);
}
.gallery-img-placeholder {
background-color: var(--color-highlight-dark);
}
.gallery-text-container {
.hs-list-text-container {
display: grid;
grid-auto-columns: 1fr;
grid-template-rows: 1fr 1fr 0fr;
@@ -86,85 +87,85 @@
grid-template-rows var(--duration-blur) var(--anim-curve);
}
.gallery-title, .gallery-subtitle, .gallery-description {
.hs-list-title, .hs-list-subtitle, .hs-list-description {
margin: 0;
transition: color var(--duration-animation) var(--anim-curve),
opacity var(--duration-animation) var(--anim-curve);
}
.gallery-title {
.hs-list-title {
font-family: var(--font-mono);
font-weight: 700;
}
.gallery-subtitle, .gallery-description {
.hs-list-subtitle, .hs-list-description {
font-size: 1.0rem;
line-height: 1.2rem;
overflow: hidden;
}
.gallery-description {
.hs-list-description {
font-weight: 500;
opacity: 0;
}
.gallery-container:hover {
border-color: var(--color-highlight);
background-color: var(--color-background-highlight);
backdrop-filter: blur(var(--blur-radius-background));
}
.gallery-container:hover p {
color: var(--color-highlight);
.hs-list-container:hover {
border-color: var(--color-highlight-alt);
background-color: var(--color-background-highlight-alt);
}
@media screen and (min-width: 800px) {
.gallery-title {
.hs-list-title {
font-size: 1.4rem;
line-height: 2.0rem;
}
.gallery-container:hover .gallery-img, .gallery-container:hover .gallery-img-placeholder {
width: 260px;
.hs-list-container:hover .hs-list-img {
scale: 1.2;
}
.gallery-container:hover .gallery-text-container {
.hs-list-container:hover .hs-list-img-container {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
.hs-list-container:hover .hs-list-text-container {
grid-template-rows: 0fr 1fr 1fr;
}
.gallery-container:hover .gallery-subtitle {
.hs-list-container:hover .hs-list-subtitle {
opacity: 0;
}
.gallery-container:hover .gallery-description {
.hs-list-container:hover .hs-list-description {
opacity: 1;
}
}
@media screen and (max-width: 800px) {
.gallery-title {
.hs-list-title {
font-size: 1.0rem;
line-height: 1.1rem;
}
.gallery-description {
.hs-list-description {
display: none;
}
.gallery-subtitle {
.hs-list-subtitle {
font-size: 0.8rem;
line-height: 1rem;
/* display: none; */
}
.gallery-container {
.hs-list-container {
height: 72px;
}
.gallery-text-container {
.hs-list-text-container {
padding-left: 8px;
}
.gallery-img, .gallery-img-placeholder {
.hs-list-img-container {
width: 110px;
min-width: 110px;
}

View File

@@ -0,0 +1,175 @@
<script lang="ts">
export interface GalleryImage {
src: string;
alt: string;
desc: string[];
}
let {
images,
}: {
images: GalleryImage[];
} = $props();
let currentIndex: number = $state(0);
function galleryBack() {
if (currentIndex == 0) {
currentIndex = images.length - 1;
} else {
currentIndex -= 1;
}
}
function galleryForward() {
if (currentIndex == images.length - 1) {
currentIndex = 0;
} else {
currentIndex += 1;
}
}
</script>
<!-- TODO on mobile, put buttons between text and image -->
<div class="gallery-zone blurred-background">
<div class="gallery-img-container">
<img class="gallery-img" loading="lazy" src={images[currentIndex].src} alt={images[currentIndex].alt}>
</div>
<div class="gallery-button-container">
<button class="gallery-button" onclick={galleryBack}>&lt;</button>
<div class="gallery-text-container">
<p class="gallery-index">{currentIndex + 1} / {images.length} :: <a href={images[currentIndex].src}>full-size</a></p>
<div class="gallery-desc-container">
{#each images[currentIndex].desc as d}
<p>{@html d}</p>
{/each}
</div>
</div>
<button class="gallery-button" onclick={galleryForward}>&gt;</button>
</div>
</div>
<style>
.gallery-zone {
width: 100%;
display: grid;
grid-template-columns: 5fr 4fr;
margin: 12px auto;
border: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
border-radius: var(--border-radius);
overflow: hidden;
height: var(--media-height);
}
.gallery-img-container {
width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
border-right: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
}
.gallery-img {
object-fit: contain;
margin: 0;
width: 100%;
}
.gallery-text-container {
width: 100%;
height: var(--media-height);
/* overflow: hidden; */
display: flex;
flex-direction: column;
/* justify-content: space-between; */
margin: 0 16px;
overflow: scroll;
box-sizing: border-box;
}
.gallery-index {
font-family: var(--font-mono);
font-weight: 600;
font-size: 1.0rem;
padding-top: 12px;
}
.gallery-text-container p, .gallery-index a {
height: fit-content;
font-size: 1.0rem;
line-height: 1.4rem;
}
.gallery-desc-container {
padding-bottom: 24px;
padding-right: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.gallery-desc-container p {
margin: 0;
}
.gallery-button-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 100%;
}
.gallery-button {
color: var(--color-text);
/* background-color: var(--color-background-highlight-alt); */
height: fit-content;
cursor: pointer;
font-size: 2.4rem;
line-height: 2.4rem;
padding: 8px 12px;
border-top: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
border-bottom: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
transition: background-color var(--duration-animation) var(--anim-curve);
}
.gallery-button:first-child {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
.gallery-button:last-child {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
.gallery-button:hover {
background-color: var(--color-background-highlight-hover-alt);
}
@media screen and (max-width: 700px) {
.gallery-zone {
display: flex;
flex-direction: column;
height: initial;
}
.gallery-text-container {
height: 40vh;
}
.gallery-img-container {
border-right: none;
border-bottom: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
}
.gallery-img {
height: 100%;
}
.gallery-zone > * {
height: 40vh;
}
}
</style>

View File

@@ -1,8 +1,9 @@
<script lang="ts">
export interface GalleryRowEntry {
export interface LinkRowEntry {
title: string;
description: string;
img: string;
latestUpdate?: string;
altText: string;
link: string;
}
@@ -10,19 +11,22 @@
let {
entries,
}: {
entries: GalleryRowEntry[];
entries: LinkRowEntry[];
} = $props();
</script>
<div class="row-container">
{#each entries as entry}
<a class="row-entry" href="{entry.link}">
<a class="row-entry blurred-background-hover" href="{entry.link}">
<div class="row-img-container">
<img class="row-img" src="{entry.img}" alt="{entry.altText}">
</div>
<div class="row-text-container">
<p class="row-title">&gt; {entry.title}</p>
<p class="row-description">{@html entry.description}</p>
{#if entry.latestUpdate}
<p class="row-updated">updated: <span class="row-updated-date">{entry.latestUpdate}</span></p>
{/if}
</div>
</a>
{/each}
@@ -40,6 +44,7 @@
margin: 0;
padding: 8px;
text-decoration: none;
border-radius: var(--border-radius);
transition: background-color var(--duration-animation) var(--anim-curve),
border-color var(--duration-animation) var(--anim-curve),
backdrop-filter var(--duration-blur) var(--anim-curve);
@@ -49,19 +54,24 @@
.row-entry:hover {
background-color: var(--color-background-highlight);
border-color: var(--color-highlight);
backdrop-filter: blur(var(--blur-radius-background));
}
.row-entry:hover .row-img {
scale: 1.2;
}
.row-entry:hover .row-img-container {
border-top-left-radius: calc(var(--border-radius) - 8px);
border-top-right-radius: calc(var(--border-radius) - 8px);
}
.row-img-container {
width: 100%;
height: 160px;
overflow: hidden;
display: flex;
justify-content: center;
transition: border-radius var(--duration-animation) var(--anim-curve);
}
.row-img {
@@ -72,6 +82,7 @@
.row-text-container {
margin-top: 8px;
margin-bottom: 4px;
}
.row-title {
@@ -87,6 +98,37 @@
margin: 0;
}
.row-updated, .row-updated-date {
font-size: 0.9rem;
line-height: 1.2rem;
margin: 0;
font-style: italic;
width: fit-content;
transition: color var(--duration-animation) var(--anim-curve),
background-color var(--duration-animation) var(--anim-curve);
}
.row-updated {
margin-top: 4px;
padding: 2px 8px;
font-weight: 500;
background-color: var(--color-background-highlight);
border-radius: var(--border-radius);
}
.row-entry:hover .row-updated {
color: var(--color-text-dark);
background-color: var(--color-highlight);
}
.row-updated-date {
font-weight: 600;
}
.row-entry:hover .row-updated-date {
color: var(--color-text-dark);
}
@media screen and (max-width: 600px) {
.row-container {
flex-direction: column;

View File

@@ -0,0 +1,4 @@
export interface A11yImage {
src: string;
alt: string;
}

View File

@@ -0,0 +1,75 @@
<script lang="ts">
import { type A11yImage } from "./a11y-img";
let {
images,
// path in which all images lie. this must be the same for all images. if left blank, "./" is used instead
path,
}: {
images: A11yImage[];
path?: string;
} = $props();
</script>
<div class="image-gallery">
{#each images as i}
<a class="image-gallery-link" href="{path ?? "./"}/{i.src}">
<img class="image-gallery-img" loading="lazy" src="{path ?? "./"}/{i.src}" alt={i.alt}>
</a>
{/each}
</div>
<style>
.image-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 4px;
}
.image-gallery-img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
filter: brightness(100%) saturate(80%);
max-height: 320px;
transition: filter var(--duration-animation) var(--anim-curve),
scale var(--duration-animation) var(--anim-curve);
scale: 1.06;
}
.image-gallery-link {
outline: var(--border-style) var(--border-dash-size) transparent;
border-radius: var(--border-radius);
z-index: 10;
transition: outline-color var(--duration-animation) var(--anim-curve);
overflow: hidden;
margin: 0;
}
.image-gallery:hover .image-gallery-img {
filter: brightness(60%) saturate(80%);
}
.image-gallery-link:hover .image-gallery-img {
filter: brightness(100%) saturate(100%);
scale: 1.0;
}
.image-gallery-link:hover {
outline-color: var(--color-highlight);
}
@media screen and (max-width: 800px) {
.image-gallery-link:hover {
outline-color: transparent;
}
.image-gallery-img {
border-radius: 0;
scale: 1.0;
filter: brightness(100%) saturate(100%);
}
}
</style>

306
src/lib/media/player.svelte Normal file
View File

@@ -0,0 +1,306 @@
<script lang="ts">
export interface Track {
title: string;
link: string;
}
let {
tracks,
cover, // album cover (optional, currently unused)
// we're doing this manually instead of letting the computer calculate optimal colours because... it's easier this way. might as well spend the 2 seconds setting two booleans rather than 2 hours trying to make this work halfway decently, lol
customColor,
useDarkText,
useDarkHoverText,
}: {
tracks: Track[];
cover?: string;
customColor: string;
useDarkText?: boolean;
useDarkHoverText?: boolean;
} = $props();
let selectedTrackId: number = $state(0);
let time: number = $state(0);
let duration: number = $state(0);
let paused: boolean = $state(true);
function playTrack(id: number) {
paused = true;
selectedTrackId = id;
time = 0;
paused = false;
}
function format(time: number) {
if (isNaN(time)) return '...';
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
}
function playNextTrack() {
if (selectedTrackId < (tracks.length - 1)) {
playTrack(selectedTrackId + 1)
} else {
selectedTrackId = 0;
time = 0;
paused = true;
}
}
</script>
<div class="media-player" style="
--color-player: {customColor};
--color-player-text: var({useDarkText ? "--color-text-dark" : "--color-text"});
--color-player-text-hover: var({useDarkHoverText ? "--color-text-dark" : "--color-text"});
">
<div class="now-playing">
<audio
src={tracks[selectedTrackId].link}
bind:currentTime={time}
bind:duration
bind:paused
onended={() => {
playNextTrack();
}}
></audio>
<button class="now-playing-button icon"
aria-label={paused ? 'play' : 'pause'}
onclick={() => paused = !paused}
>
{#if paused}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z" />
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
{/if}
</button>
<div>
<p class="now-playing-hint">now playing:</p>
<p class="now-playing-title">{tracks[selectedTrackId].title}</p>
</div>
<p class="now-playing-duration">
{format(time)}&nbsp;//&nbsp;{duration ? format(duration) : '--:--'}
</p>
</div>
<div class="progress-container">
<div class="slider"
onpointerdown={(e: MouseEvent) => {
const div = e.currentTarget as Element;
function seek(e: MouseEvent) {
const { left, width } = div.getBoundingClientRect();
let p = (e.clientX - left) / width;
if (p < 0) p = 0;
if (p > 1) p = 1;
time = p * duration;
}
seek(e);
window.addEventListener('pointermove', seek);
window.addEventListener('pointerup', () => {
window.removeEventListener('pointermove', seek);
}, {
once: true
});
}}
>
<div class="progress-bar" style="--progress: {time / duration}%"></div>
</div>
</div>
<div class="track-list">
{#each tracks as t, index}
<button class="track-play" aria-label="play" onclick={() => {
playTrack(index);
}}>
<svg class="track-icon {index == selectedTrackId ? "track-icon-playing" : ""}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z" />
</svg>
<div class="track-text-container">
<span class="track-title-index">{(index + 1) < 10 ? `0${index + 1}` : index + 1}.</span>
<p class="track-title">{t.title}</p>
</div>
</button>
<a class="track-download-icon" href={t.link} aria-label="download">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
{/each}
</div>
</div>
<style>
.icon {
width: 20px;
height: 20px;
color: var(--color-player-text);
transition: color var(--duration-animation) var(--anim-curve);
}
.icon:hover {
cursor: pointer;
/* color: var(--color-highlight-alt); */
}
.media-player {
--color-player-bg: color-mix(in srgb, var(--color-player) 40%, transparent);
width: 100%;
border-radius: var(--border-radius);
background-color: var(--color-player-bg);
padding: 16px 0 16px;
overflow: hidden;
}
.media-player p, .media-player a {
margin: 0;
color: var(--color-player-text);
}
.now-playing {
display: grid;
grid-template-columns: min-content auto min-content;
gap: 4px;
margin: 0 16px;
}
.now-playing-button {
align-self: center;
width: 40px;
height: 40px;
}
/* not sure why the svg target is required only for the latter here but it won't work without it */
.now-playing-button:hover, .track-download-icon:hover svg {
color: var(--color-player);
}
.now-playing-hint, .now-playing-duration {
font-size: 0.8rem;
line-height: 1.0rem;
}
.now-playing-title {
font-weight: 700;
}
.now-playing-duration {
display: flex;
flex-direction: row;
align-items: end;
font-family: var(--font-mono);
font-weight: 500;
}
.progress-container {
display: flex;
align-items: center;
margin: 4px 12px;
}
.slider {
flex: 1;
height: 8px;
background: var(--color-player-bg);
border-radius: 6px;
overflow: hidden;
cursor: pointer;
margin: 4px 0;
}
.progress-bar {
width: calc(100 * var(--progress));
height: 100%;
background: var(--color-player);
transition: width var(--duration-animation) var(--anim-curve);
}
.track-list {
display: grid;
grid-template-columns: auto min-content;
align-items: center;
}
.track-text-container {
display: flex;
flex-direction: row;
}
.track-title-index {
font-size: 0.8rem;
margin: 0 1px 0 5px;
align-self: flex-end !important;
line-height: 0.8rem;
font-family: var(--font-mono);
}
.track-title {
margin: 0 4px !important;
line-height: 1.0rem;
font-size: 1.0rem;
}
.track-play {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
padding: 4px 0 4px 16px;
height: fit-content;
cursor: pointer;
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
transition: background-color var(--duration-animation) var(--anim-curve);
}
.track-play:hover {
background-color: var(--color-player);
}
.track-title-index, .track-title, .track-icon {
transition: color var(--duration-animation) var(--anim-curve);
}
.track-play:hover .track-title-index, .track-play:hover .track-title, .track-play:hover .track-icon, .track-play:hover .track-icon-playing {
color: var(--color-player-text-hover);
}
.track-play:hover .track-icon {
opacity: 1;
}
.track-icon {
width: 24px;
opacity: 0;
transition: opacity var(--duration-animation) var(--anim-curve),
color var(--duration-animation) var(--anim-curve);
color: var(--color-player-text);
}
.track-icon-playing {
/* color: var(--color-player-text); */
opacity: 1;
}
.track-download-icon {
padding: 0 12px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: color var(--duration-animation) var(--anim-curve);
}
</style>

View File

@@ -1,14 +1,25 @@
<script lang="ts">
let {
src,
thumb,
remote,
}: {
src: string;
thumb?: string;
remote?: boolean;
} = $props();
let space = remote ? "https://files.natconf.dev/cdn/" : "";
</script>
<!-- Muted video element -->
<video controls class="video-block" muted>
<source src={src} type="video/mp4">
Video is broken, sorry!
<video
controls
class="video-block"
muted
preload="none"
poster={thumb ? space + thumb : "/common/video-placeholder.webp"}>
<source src={space + src} type="video/mp4">
Video is broken, sorry!
</video>

View File

@@ -1,33 +1,78 @@
<div class="content">
<div class="side">
<slot name="side-left" />
</div>
<div class="main">
<slot name="main" />
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import type { BannerContent } from "$lib/components/banner-content";
import ScrollTopButton from "$lib/components/scroll-top-button.svelte";
import TableOfContents from "$lib/components/table-of-contents.svelte";
import type { Snippet } from "svelte";
let {
children,
bannerContent,
}: {
children: Snippet,
bannerContent: BannerContent;
} = $props();
</script>
<div class="container">
<Banner2 content={bannerContent}/>
<div class="content">
<div class="content-sidebar-main-container">
{@render children()}
<ScrollTopButton />
</div>
<div class="side">
<TableOfContents type="side" />
</div>
</div>
</div>
<style>
.content {
max-width: 2000px;
margin: 0 auto;
display: flex;
flex-direction: row;
padding: 0 24px;
/* not elegant at all but it works */
:global {
.content-sidebar-main-container > *:nth-child(1) {
margin-top: 0 !important;
}
}
.container {
max-width: var(--page-width);
margin: 0 auto;
padding: 0 24px;
box-sizing: border-box;
}
.content {
max-width: var(--page-width);
width: 100%;
display: grid;
grid-template-columns: auto 300px;
}
.content-sidebar-main-container {
width: 100%;
}
.side {
min-width: 400px;
width: 100%;
margin-top: 32px;
margin-bottom: 8px;
}
@media screen and (max-width: 800px) {
.content {
padding: 0 8px;
flex-direction: column;
width: 100%;
grid-template-rows: auto auto;
grid-template-columns: 1fr;
}
.side {
min-width: 0;
max-width: 100%;
margin-top: 8px;
width: 100%;
order: -1;
}
}
</style>

View File

@@ -1,10 +1,23 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import type { BannerContent } from "$lib/components/banner-content";
import ScrollTopButton from "$lib/components/scroll-top-button.svelte";
import type { Snippet } from "svelte";
let { children } = $props();
let {
children,
bannerContent,
}: {
children: Snippet,
bannerContent?: BannerContent;
} = $props();
</script>
<div class="main-content">
{#if bannerContent}
<Banner2 content={bannerContent} />
{/if}
<ScrollTopButton />
{@render children()}
</div>

View File

@@ -15,8 +15,8 @@
</script>
<footer>
<div class="background">
<hr>
<div class="background blurred-background">
<!-- <hr> -->
<div class="content-container">
<div class="content-box center-box">
<p> 20232026 denizk0461</p>
@@ -26,17 +26,23 @@
<h6>Content</h6>
<a href="/projects">Projects</a>
<a class="link-level-2" href="/projects/projectn5">Homesick</a>
<a href="/feed">Feed</a>
<a href="/blog">Blog</a>
<a href="/art">Art</a>
<a class="link-level-2" href="/art/drawings">Drawings</a>
<a href="/drawings">Drawing Gallery</a>
</div>
<div class="content-box">
<h6>Meta</h6>
<a href="/meta/about">About</a>
<a href="/meta/feeds">Feeds</a>
<a href="/meta/updates">Updates</a>
<a href="/meta/music-rotation">Music Rotation</a>
<a href="/meta/now">Now</a>
<a href="https://code.natconf.dev/denizk0461/pages">Page Source</a>
<a href="/meta/privacy">Privacy & Cookies</a>
</div>
<div class="content-box">
<h6>Other</h6>
<a href="https://files.natconf.dev/">copyparty</a>
<a href="/meta/feeds">Feeds</a>
<a href="https://code.natconf.dev/">Gitea</a>
<a href="/other/privacy">Privacy & Cookies</a>
</div>
</div>
<input
@@ -65,7 +71,8 @@
font-weight: 400;
color: var(--color-text);
text-decoration: none;
font-size: 1rem;
font-size: var(--font-size-mono);
line-height: var(--font-line-height-mono);
margin: 0;
}
@@ -92,8 +99,10 @@
.background {
background-color: var(--color-background-highlight);
backdrop-filter: blur(var(--blur-radius-background));
margin-top: 32px;
border-top: var(--border-style) var(--border-dash-size) var(--color-highlight);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.content-container {
@@ -120,6 +129,7 @@
.commit {
display: inline;
font-size: inherit;
}
@media screen and (max-width: 800px) {

View File

@@ -1,9 +1,10 @@
{#snippet headerLinks()}
<a href="/">Home</a>
<a href="/projects">Projects</a>
<a href="/blog">Blog</a>
<a href="/art">Art</a>
<a href="/meta/about">About</a>
<a href="/">home</a>
<a href="/projects">projects</a>
<a href="/feed">feed</a>
<a href="/blog">blog</a>
<a href="/drawings">drawings</a>
<a href="/meta/about">about</a>
{/snippet}
<div class="header-content">

View File

@@ -0,0 +1,19 @@
<div class="waters"></div>
<style>
.waters {
--color-waters: #242424;
position: fixed;
z-index: -99;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--color-waters);
mask-image: url('/bremen-waters-white.svg');
mask-position: center;
background-position: center;
background-attachment: fixed;
}
</style>

View File

@@ -1,6 +1,7 @@
<script>
import Header from "$lib/viewport/header.svelte";
import Footer from "$lib/viewport/footer.svelte";
import WaterBackground from "$lib/viewport/water-background.svelte";
let { children } = $props();
</script>
@@ -9,7 +10,7 @@
<meta name="robots" content="noai, noimageai">
</svelte:head>
<div class="waters"></div>
<WaterBackground />
<div class="all-content-container">
<Header />
@@ -95,6 +96,7 @@
/* colours */
--color-text: #d0d0d0;
--color-text-secondary: #b0b0b0;
--color-text-tertiary: #808080;
--color-text-img: invert(98%) sepia(1%) saturate(4643%) hue-rotate(297deg) brightness(115%) contrast(76%);
--color-text-dark: #1e1e1e;
@@ -113,8 +115,6 @@
--color-background-highlight-hover: color-mix(in srgb, var(--color-highlight) 60%, transparent);
--color-background-highlight-hover-alt: color-mix(in srgb, var(--color-highlight-alt) 66%, transparent);
--color-background-highlight-hover-dark: color-mix(in srgb, var(--color-highlight-dark) 60%, transparent);
--color-waters: #242424;
--color-link-unvisited: #c2e8ff;
--color-link-visited: #ffd7f0;
@@ -138,10 +138,11 @@
--anim-curve: cubic-bezier(0.22, 1, 0.36, 1);
/* fonts */
--font-line-height: 1.6rem;
--font-line-height: 1.5rem;
--font-line-height-mono: 1.4rem;
--font-sans-serif: 'Bai Jamjuree', 'OpenMoji', sans-serif;
--font-size-sans-serif: 1.1rem;
--font-size-sans-serif: 1.08rem;
--font-mono: 'Kode Mono', 'OpenMoji', monospace;
--font-size-mono: 0.9em;
@@ -158,6 +159,7 @@
/* sizing */
--media-width: 80%;
--media-height: 400px;
--width-toc: 650px;
/* page sizing */
@@ -179,7 +181,6 @@
body {
font-family: var(--font-sans-serif);
font-size: 1.2rem;
color: var(--color-text); /* text colour */
margin: 0;
@@ -193,25 +194,11 @@
/* max-width: 100%; */
}
.waters {
position: fixed;
z-index: -99;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--color-waters);
mask-image: url('/bremen-waters-white.svg');
mask-position: center;
background-position: center;
background-attachment: fixed;
}
p, span, li, pre, a {
color: var(--color-text);
font-size: var(--font-size-sans-serif);
line-height: var(--font-line-height);
margin: 12px 0;
margin: 8px 0;
font-weight: 400;
}
@@ -309,7 +296,7 @@
margin-left: auto;
margin-right: auto;
display: flex;
max-height: 400px;
max-height: var(--media-height);
object-fit: contain;
}
@@ -319,26 +306,23 @@
video {
max-width: var(--media-width);
/* height: var(--media-height); */
margin-top: 12px;
margin-bottom: 12px;
/* padding: 8px;
}
.blurred-background {
backdrop-filter: blur(var(--blur-radius-background));
}
.blurred-background-hover:hover {
backdrop-filter: blur(var(--blur-radius-background));
border: var(--border-dash-size) var(--border-style) var(--color-highlight); */
}
.lightyears-text {
font-family: var(--font-lightyears);
}
.horizontally-centre-aligned {
width: var(--media-width);
display: flex;
justify-content: center;
align-items: center;
margin-left: auto;
margin-right: auto;
}
.inline-img-left {
float: left;
max-width: 24%;
@@ -460,14 +444,77 @@
padding: 4px;
}
.callout-warning {
margin: 12px auto;
max-width: var(--width-toc);
padding: 12px 20px;
box-sizing: border-box;
backdrop-filter: blur(var(--blur-radius-background));
background-color: var(--color-background-highlight-alt);
border: var(--border-dash-size) var(--border-style) var(--color-highlight-alt);
/* TODO this should be in a component! */
.post-tag {
font-family: var(--font-mono);
font-size: 0.8rem;
background-color: var(--color-tag-filters-bg);
margin: 0;
padding: 4px;
border-radius: 8px;
line-height: 1rem;
font-weight: 600;
}
/* container for all tag stuff that only holds the colour */
.tag-filters {
--color-tag-filters: var(--color-highlight);
--color-tag-filters-bg: var(--color-background-highlight);
--color-tag-filters-bg-hover: var(--color-background-highlight-hover);
--color-tag-filters-text: var(--color-text-highlight);
--color-tag-filters-text-active: var(--color-text-dark);
}
.tag-filters-alt {
--color-tag-filters: var(--color-highlight-alt);
--color-tag-filters-bg: var(--color-background-highlight-alt);
--color-tag-filters-bg-hover: var(--color-background-highlight-hover-alt);
--color-tag-filters-text: var(--color-text-highlight-alt);
--color-tag-filters-text-active: var(--color-text);
}
.tag-filter-header {
font-family: var(--font-mono);
font-size: 0.9rem;
/* margin-left: 10px;
margin-right: 10px; */
margin: 12px 10px 4px;
color: var(--color-tag-filters-text);
}
.tag-filter-container {
display: flex;
gap: 8px 12px;
margin: 0 10px 8px;
flex-wrap: wrap;
}
.tag-filter {
width: fit-content;
font-size: 0.9rem;
color: var(--color-text);
padding: 8px;
border-radius: 12px;
cursor: pointer;
transition: background-color var(--duration-animation) var(--anim-curve);
}
.tag-filter-selected {
color: var(--color-tag-filters-text-active);
background-color: var(--color-tag-filters);
}
.tag-filter:hover {
background-color: var(--color-tag-filters-bg-hover);
}
@media screen and (max-width: 600px) {
.tag-filter-container {
gap: 8px;
}
.tag-filter {
font-size: 0.8rem;
}
}
}
</style>

View File

@@ -1,19 +1,21 @@
<script lang="ts">
import Content from "$lib/viewport/content.svelte";
import GalleryRow, { type GalleryRowEntry } from "$lib/lists/gallery-row.svelte";
import LinkRow from "$lib/lists/link-row.svelte";
import { posts as devlogPosts } from "./projects/projectn5/devlog/posts";
import { posts as blogPosts } from "./blog/posts";
import { entries as updateEntries } from "./meta/updates/updates";
import UpdateEntry from "$lib/components/update-entry.svelte";
import IndieButton from "$lib/components/indie-button.svelte";
import { buttons } from "$lib/components/indie-button";
// import IndieButton from "$lib/components/indie-button.svelte";
// import { buttons } from "$lib/components/indie-button";
import { onMount } from "svelte";
import { getLatestPostDate } from "./feed/feed";
import { drawings } from "./drawings/drawings";
let latestDevlogDate = devlogPosts[0].post.date;
let latestBlogDate = blogPosts[0].post.date;
let updateEntriesTrimmed = updateEntries.slice(0, 5);
// this only fetches the updated date if the latest post has been updated
let latestBlogDate = blogPosts[0].post.dateUpdated ?? blogPosts[0].post.date;
let latestDrawingDate = drawings[drawings.length - 1].date;
let latestStatusContent = $state("fetching status...");
let latestStatusTimestamp = $state("?");
@@ -37,54 +39,6 @@
onMount(() => {
getLatestStatus();
})
const galleryTopRow: GalleryRowEntry[] = [
{
title: "Homesick devlog",
description: `My active Godot game project about finding yourself in an unfamiliar future. <i>latest update: ${latestDevlogDate}</i>`,
img: "projects/projectn5/banner2.webp",
altText: "The protagonist Laura standing on a floating platform in the purple test level. Ziplines are all around her and the text 'When this text is spinning, the game is not paused' is frozen in the sky.",
link: "projects/projectn5",
},
{
title: "Blog",
description: `A place where I write about random things. <i>latest post: ${latestBlogDate}</i>`,
img: "blog/robert.webp",
altText: "View at a tram bridge rising and then curving to the left.",
link: "blog",
},
];
const galleryBottomRow: GalleryRowEntry[] = [
{
title: "Projects",
description: "An overview of my more technical projects",
img: "projects/banner.webp",
altText: "An upside-down New 3DS XL lying open on a desk with a small USB-C breakout board attached to it, and a USB-C cable plugged in. The 3DS is glowing to indicate that it is charging.",
link: "projects",
},
{
title: "Art",
description: "My creative side lives here",
img: "art/banner.webp",
altText: "A rainbow-like holographic effect produced by bending a reflective sheet of cardboard.",
link: "art",
},
{
title: "Files",
description: "Find things I've put for download on my Copyparty instance",
img: "main/hypertext.webp",
altText: "Screenshot of Hypertext Unity level. Crates are strewn across the floor, Waluigi is flying in front of the camera, and text such as 'COME AND TRY OUR ALL-NEW BLENDER' and 'omg! it's the brandenbur ger tor!' is displayed.",
link: "https://files.natconf.dev/public/",
},
{
title: "Gitea",
description: "I now also self-host a Gitea instance where I am likely migrating all my projects to",
img: "main/magic.webp",
altText: "A 'magic' command written in Java. The command shuts down the computer when ran.",
link: "https://code.natconf.dev/",
},
];
</script>
<svelte:head>
@@ -94,7 +48,7 @@
<Content>
<h1 class="gradient-title"><i>Moin!</i> ~ welcome to my website :)</h1>
<a href="/blog/2026/0325" class="page-subtitle gradient-title lightyears-text">you can change the world from your bedroom!</a>
<a href="/projects/misc/lightyears-font" class="page-subtitle gradient-title lightyears-text">you can change the world from your bedroom!</a>
<hr>
@@ -102,17 +56,20 @@
<div>
<img class="me-img pixelated-img" src="me.webp" alt="Pixelated mirror selfie of the website creator wearing a green shirt, fitting the website theme. The face is obscured." title="hi!">
<p>Hi! I'm Deniz. Welcome to my website! I keep rewriting this introduction but I'm REALLY bad at this type of stuff.</p>
<p>I made this website because I really don't like modern social media and I wanted a more creative way of expressing myself without giving in to the attention economy or submitting all my data including my soul to some megacorp. That's why you'll find a bunch of stuff here that interests me: programming, gamedev, 3D modelling, electronic music, drawing, electronics and microcontroller programming, Linux and self-hosting, and probably some other stuff too. I am currently developing at least one game and I am also posting random things on my blog, both of which you can find linked above and below.</p>
<p>I listen to A LOT of music (fav artists: <a href="https://acloudyskye.bandcamp.com/">acloudyskye</a>, <a href="https://jaronsteele.bandcamp.com/">Jaron</a>, <a href="https://janeremover.bandcamp.com/">Jane Remover</a>) and I enjoy dabbling around in <a href="https://godotengine.org/">Godot</a> and <a href="https://blender.org/">Blender</a>. I also use <a href="https://fedoraproject.org/">Fedora KDE</a>... btw. Want to know more about me and this website? Firstly, <i>why?</i> But also, <a href="/meta/about">here</a>!</p>
<p>irl I am from 🇩🇪 Northern Germany and studying to become a secondary school teacher.</p>
<p>Hi! I'm Deniz. Welcome to my website! This is my little sanctuary where I post about all my creative projects as well as some of my thoughts.</p>
<p>I made this website because I dislike modern social media and I wanted a more liberating and creative way of expressing myself without giving in to the attention economy by submitting all of my data including my soul to some megacorp. That's why you'll find a mix of all kinds of things that interest me here: programming, gamedev, 3D modelling, electronic music, drawing, electronics and microcontroller programming, Linux and self-hosting, and probably some other stuff too. I am currently developing at least one game and I also post on the <a href="/projects">projects</a> and <a href="/feed">feed</a> pages, depending on which page is more suitable for a given project.</p>
<p>I listen to <b>a lot</b> of music. My favourite artists are <a href="https://acloudyskye.bandcamp.com/">acloudyskye</a>, <a href="https://jaronsteele.bandcamp.com/">Jaron</a>, <a href="https://janeremover.bandcamp.com/">Jane Remover</a>, but you can <a href="/meta/music-rotation">find my current rotation here</a>. I also enjoy dabbling around in cool FOSS tools like <a href="https://godotengine.org/">Godot</a>, <a href="https://blender.org/">Blender</a>, and <a href="https://krita.org/">Krita</a> which I use under <a href="https://fedoraproject.org/">Fedora KDE</a>. If you want to know more about me and this website, <a href="/meta/about">go here</a>!</p>
<p>irl I am from 🇩🇪 Northern Germany and studying to become a secondary school teacher, but between you and me; I may end up doing something else :).</p>
</div>
<div>
<div class="sidebox-container">
<div class="sidebox-container blurred-background">
<h4 class="sidebox-header">heads-up</h4>
<p>This website works best on Firefox and other Gecko-based browsers! All pages <i>should</i> be responsive and work on mobile.</p>
<p>This website is 100% mobile-friendly or at least trying to be!</p>
<p>This website works best on Firefox and other Gecko-based browsers! It is also nearly 100% mobile-friendly! All pages are largely functional without JavaScript but I recommend you enable it (some elements won't work without it)! Links may also change.</p>
<hr>
@@ -132,8 +89,59 @@
<hr>
<GalleryRow entries={galleryTopRow} />
<GalleryRow entries={galleryBottomRow} />
<LinkRow entries={
[
{
title: "Homesick devlog",
description: `my active Godot game project about finding yourself in an unfamiliar future.`,
latestUpdate: latestDevlogDate,
img: "projects/projectn5/banner2.webp",
altText: "The protagonist Laura standing on a floating platform in the purple test level. Ziplines are all around her and the text 'When this text is spinning, the game is not paused' is frozen in the sky.",
link: "projects/projectn5",
},
{
title: "Projects",
description: "<b>[updated]</b> an overview of all my projects!",
img: "projects/banner.webp",
altText: "An upside-down New 3DS XL lying open on a desk with a small USB-C breakout board attached to it, and a USB-C cable plugged in. The 3DS is glowing to indicate that it is charging.",
link: "projects",
},
]} />
<LinkRow entries={
[
{
title: "Creative Feed",
description: `the small things I make find a home here.`,
latestUpdate: getLatestPostDate(),
img: "feed/banner.webp",
altText: "A blue screen with the text 'how do you do art ? 1. face your fears 2. become your heroes'. The 'art' looks to have been edited in. The music artist Porter Robinson is standing in the bottom right corner.",
link: "feed",
},
{
title: "Blog",
description: `a place where I write about random things.`,
latestUpdate: latestBlogDate,
img: "blog/robert.webp",
altText: "View at a tram bridge rising and then curving to the left.",
link: "blog",
},
{
title: "Drawing Gallery",
description: "a collection of my digital and physical drawings.",
latestUpdate: latestDrawingDate,
img: "drawings/banner.webp",
altText: "Several Faber-Castell Polychromos colour pencils lined up with markings next to them in the same colour on a sheet of paper.",
link: "drawings",
},
// {
// title: "Files",
// description: "find things I've put for download on my copyparty instance.",
// img: "main/hypertext.webp",
// altText: "Screenshot of Hypertext Unity level. Crates are strewn across the floor, Waluigi is flying in front of the camera, and text such as 'COME AND TRY OUR ALL-NEW BLENDER' and 'omg! it's the brandenbur ger tor!' is displayed.",
// link: "https://files.natconf.dev/public/",
// },
]} />
<hr>
@@ -166,23 +174,24 @@
<img class="webring-img" usemap="#w95widget" src="/webrings/noai/w95widget.webp" alt="a gray Windows 95 style dialog box titled 'The No AI Webring' with a little icon showing a computer chip in a rubbish bin. beside it are three clickable buttons, labeled Previous, Random... and Next">
</div>
<div class="button-container">
<!-- <div class="button-container">
{#each buttons as button}
<IndieButton button={button} />
{/each}
</div>
<p>to be expanded!</p>
<p class="small-supertext">my own 88x31 button is in the making. ETA: ???</p>
<p class="small-supertext">currently trying to come up with ideas for my own 88x31 button</p> -->
</div>
</Content>
<style>
.split-intro-container {
display: grid;
grid-template-columns: 3fr 1fr;
grid-template-columns: 3fr 30%;
gap: 16px;
}
.button-container {
/* .button-container {
flex: 2;
}
@@ -190,7 +199,7 @@
display: flex;
flex-wrap: wrap;
gap: 8px;
}
} */
.webring {
display: flex;
@@ -204,18 +213,17 @@
}
.me-img {
width: 132px;
min-width: 132px;
width: 10.2rem;
float: left;
margin-right: 12px;
margin: 16px 12px 0 0;
}
.sidebox-container {
padding: 8px 24px;
backdrop-filter: blur(var(--blur-radius-background));
padding: 8px 16px;
border-width: var(--border-dash-size);
border-style: var(--border-style);
border-color: var(--color-highlight-alt);
border-radius: var(--border-radius);
/* transition: border-radius var(--duration-animation) var(--anim-curve); */
/* border-radius: var(--border-radius); */
}
@@ -293,4 +301,11 @@
margin: 4px 0 12px 0;
display: block;
}
@media screen and (max-width: 900px) {
.split-intro-container {
display: flex;
flex-direction: column;
}
}
</style>

View File

@@ -1,39 +0,0 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import GalleryRow, { type GalleryRowEntry } from "$lib/lists/gallery-row.svelte";
const subpages: GalleryRowEntry[] = [
{
title: "Drawing Gallery",
description: "Some cool things I've drawn!",
img: "drawings/banner.webp",
altText: "Several Faber-Castell Polychromos colour pencils lined up with markings next to them in the same colour on a sheet of paper.",
link: "drawings",
},
{
title: "Discography",
description: "Small stories about my past music",
img: "/main/hypertext.webp",
altText: "",
link: "music",
},
];
</script>
<svelte:head>
<title>Art | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="Art"
banner="banner.webp"
bannerAlt="A rainbow-like holographic effect produced by bending a reflective sheet of cardboard."
subtitle="my creative side" />
<p>Here I have collected the products of some of my creative endeavours. Check them out below!</p>
<GalleryRow entries={subpages} />
</Content>

View File

@@ -1,137 +0,0 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import { type Drawing, drawings } from "./drawings";
</script>
<svelte:head>
<title>Drawing Gallery | denizk0461</title>
</svelte:head>
{#snippet drawingGalleryEntry({d}: {d: Drawing})}
<div class="gallery-entry">
<div class="gallery-entry-img-container">
<img src="{d.img}" alt="{d.imgAlt}">
</div>
<div class="gallery-entry-info">
<p class="gallery-entry-title">{d.title} <span>{d.date}</span></p>
{#each d.notes as note}
<p class="gallery-entry-note">{note}</p>
{/each}
<a href="{d.img}">view full-size</a>
</div>
</div>
{/snippet}
<Content>
<Banner2
title="Drawing Gallery"
subtitle=""
banner="banner.webp"
bannerAlt="Several Faber-Castell Polychromos colour pencils lined up with markings next to them in the same colour on a sheet of paper." />
<p>I started drawing at the start of 2026 and this is my page to show off what I make! I've mostly drawn on paper so far (I like the feel and resistance of pens on paper as well as the <a href="/blog/2026/0129">limitations</a> it imposes), but I got into digital art with Krita recently!</p>
<p>Why have I created this page, you may wonder? to pressure myself to draw more</p>
<p>If you're interested, here's a post about me <a href="/blog/2026/0205">drawing every day for 28 days</a> to learn to draw. You may recognise some of the drawings there; I picked out my favourite drawings and added them here!</p>
<div class="drawing-container">
{#each drawings as d}
{@render drawingGalleryEntry({d})}
{/each}
</div>
</Content>
<style>
.drawing-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
}
.gallery-entry {
position: relative;
height: 340px;
}
.gallery-entry-img-container {
overflow: hidden;
}
.gallery-entry-img-container, .gallery-entry-info {
border-radius: 16px;
}
.gallery-entry-img-container, .gallery-entry-info {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: 0;
height: 100%;
width: 100%;
}
.gallery-entry img {
width: 100%;
height: 100%;
object-fit: cover;
transition: scale var(--duration-animation) var(--anim-curve);
}
.gallery-entry:hover img {
scale: 1.2;
}
.gallery-entry:hover .gallery-entry-info {
opacity: 1;
}
.gallery-entry-info {
opacity: 0;
display: flex;
flex-direction: column;
transition: opacity var(--duration-animation) var(--anim-curve);
background-color: var(--color-header-highlight);
padding: 12px 8px;
gap: 4px;
box-sizing: border-box;
justify-content: center;
outline: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
}
.gallery-entry-info * {
margin: 0;
width: fit-content;
}
.gallery-entry-title {
font-family: var(--font-mono);
font-weight: 700;
}
.gallery-entry-title span {
font-size: 0.8rem;
line-height: 0.9rem;
font-weight: 500;
}
.gallery-entry-note, .gallery-entry-info a {
font-size: 1.0rem;
line-height: 1.3rem;
}
@media screen and (max-width: 1000px) {
.drawing-container {
grid-template-columns: 1fr 1fr;
}
}
@media screen and (max-width: 600px) {
.drawing-container {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,5 @@
import { redirect } from "@sveltejs/kit";
export function load() {
redirect(308, '/drawings');
}

View File

@@ -1,90 +0,0 @@
export interface Drawing {
title: string;
date: string;
notes: string[];
img: string;
imgAlt: string;
}
export let drawings: Drawing[] = [
{
title: "Krita #1",
date: "2026-03-10",
notes: [
"ok i changed my mind on digital art. it's awesome.",
"My first drawing using Krita! I went with my usual methods but tried refining some things and adding (hopefully not overly misplaced) shadows too. I ended up really liking the ability to use layers, and colour in digital art just pops so nicely.",
"Initially, I drew the left arm in front of her body but later changed this to avoid drawing the hand.",
],
img: "2026/0310.webp",
imgAlt: "A digital drawing of a girl with long brown hair in a ponytail. She has green eyes and is wearing a cropped shirt with stripes, an orange spaghetti top underneath, and dark trousers. She is holding her hands behind her back.",
},
{
title: "SMILE! :D",
date: "2026-03-04",
notes: [
"This is actually the construction sketch of a drawing I later went over with a fineliner and coloured pencils. However, I kind of prefer the pencil sketch.",
"This was my first attempt at a head-on perspective. I had fun drawing details like the scrunchie, the jeans, and the smile!",
],
img: "/blog/2026/0205/27-1.webp",
imgAlt: "A drawing of a girl with her head tilted towards her right shoulder. She is smiling with her eyes closed and is holding up a victory sign with her left hand. She has her hair in a ponytail and is wearing jeans with shoulder straps, and there is a scrunchie on her left wrist as well.",
},
{
title: "Cyborg Arm",
date: "2026-02-27",
notes: [
"Possibly my favourite sketch from the drawing challenge, because she looks cool, but also because her design deviates from the other characters a bit.",
],
img: "/blog/2026/0205/22.webp",
imgAlt: "A pencil sketch of a girl with a ponytail, crop top, and track pants with a slightly shocked look on her face. She is looking at her right arm, which is a cyborg part.",
},
{
title: "Porter Robinson fanart",
date: "2026-02-26",
notes: [
"I drew the Worlds hand for practice and then decided to draw Po-Uta's head as well. I realised then that learning to draw gave me the ability to draw fanart.",
"I had never considered that possibility before.",
],
img: "/blog/2026/0205/21.webp",
imgAlt: "Two pencil sketches traced over with a black fineliner. The left one is of a hand with a cube in its palm, sketched after the hand on the cover of Porter Robinson's album Worlds. Beneath it is an emoticon used on the same cover. To the right is a manga-style head with green eyes and wavy hair, meant to resemble Porter Robinson's Vocaloid mascot Po-Uta.",
},
{
title: "Emilia",
date: "2026-02-23",
notes: [
"My first character with the new style of drawing eyes I picked up from a manga drawing book!",
"I named her Emilia because she looked like a more nice and caring character.",
],
img: "/blog/2026/0205/18.webp",
imgAlt: "A pencil sketch of a girl holding up a V sign with her left arm. She is wearing a long-sleeve shirt, jeans, and her hair is tied up in a ponytail. She is winking, the other eye is coloured green. Her body is tilted towards the right side of the paper. In the top right corner is a lightly-drawn sketch of the girl's pose.",
},
{
title: "Elizabeth",
date: "2026-02-18",
notes: [
"She's the product of me trying to re-draw the character I drew on day 1 of my drawing challenge, and I was really glad to see that I had actually improved!",
],
img: "/blog/2026/0205/13-2.webp",
imgAlt: "A pencil drawing of a girl looking to the left. She is wearing a cropped loose tee and jeans, while her right hand is hinted to rest on her hip.",
},
{
title: "bread girl",
date: "2026-01-30",
notes: [
"I drew her during a game of Wizard. I initially wanted to make her chew on a whole loaf but I didn't know how to draw that.",
"Wasn't really sure how to convey that her mouth is full either, but in retrospect, I could have exaggerated the bow in her lower eyelids to do so.",
"I like her eyes. Her head could be taller, actually.",
],
img: "2026/breadgirl.webp",
imgAlt: "An anime-style girl chewing on a piece of bread. She wears a ponytail and a sleeveless top.",
},
{
title: "test",
date: "2026-01-29",
notes: [
"A small sketch (only like 4cm wide) that I drew with a ballpoint pen on pink paper. The fact that I was able to sketch this, without any prior practice, plus an intrinsic want to be able to draw made me seriously consider learning to draw.",
"Having learned just a little bit about drawing, I can say now (a month and a half later) that this isn't great, but it served its purpose of making me start to draw!",
],
img: "/blog/2026/0129/girl.webp",
imgAlt: "A small drawing of an anime-style girl's head. She has a ponytail and is looking towards the left with a concentrated gaze.",
},
];

View File

@@ -7,12 +7,12 @@
<title>My Tracks | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="My Tracks"
subtitle=""
banner=""
bannerAlt="" />
<Content bannerContent={{
title: "My Tracks",
subtitle: "",
banner: "",
bannerAlt: "",
}}>
well this is awkward. there's nothing here yet. come back later?

View File

@@ -1,20 +0,0 @@
<script>
let { children } = $props();
</script>
{@render children()}
<style>
:global {
.post-tag {
font-family: var(--font-mono);
font-size: 0.8rem;
background-color: var(--color-background-highlight-alt);
margin: 0;
padding: 4px;
border-radius: 8px;
line-height: 1rem;
font-weight: 600;
}
}
</style>

View File

@@ -8,7 +8,6 @@
function setFilter(tag: BlogPostTag) {
filter = tag;
console.log(filter);
}
function filterPosts(): BlogPostLink[] {
@@ -17,7 +16,6 @@
}
let a: BlogPostLink[] = posts.filter((post) => post.post.tags.includes(filter))
console.log(a);
return a;
}
</script>
@@ -26,69 +24,21 @@
<title>Blog | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="Blog"
banner="robert.webp"
bannerAlt="View at a tram bridge rising and then curving to the left." />
<Content bannerContent={{
title: "Blog",
banner: "robert.webp",
bannerAlt: "View at a tram bridge rising and then curving to the left.",
}}>
<!-- TODO descriptions on filter click -->
<p class="tag-filter-header"># filter posts by tag:</p>
<div class="tag-filter-container">
{#each Object.values(BlogPostTag) as tag}
{#if tag == filter}
<button class="post-tag tag-filter tag-filter-selected" onclick={() => { setFilter(tag) }}>{tag}</button>
{:else}
<button class="post-tag tag-filter" onclick={() => { setFilter(tag) }}>{tag}</button>
{/if}
{/each}
<div class="tag-filters-alt">
<p class="tag-filter-header"># filter posts by tag:</p>
<div class="tag-filter-container">
{#each Object.values(BlogPostTag) as tag}
<button class="post-tag tag-filter {tag == filter ? "tag-filter-selected" : ""}" onclick={() => { setFilter(tag) }}>{tag}</button>
{/each}
</div>
</div>
<BlogGallery posts={filterPosts()} />
</Content>
<style>
.tag-filter-header {
font-family: var(--font-mono);
font-size: 0.9rem;
/* margin-left: 10px;
margin-right: 10px; */
margin: 12px 10px 4px;
color: var(--color-text-highlight-alt);
}
.tag-filter-container {
display: flex;
gap: 8px 12px;
margin: 0 10px 8px;
flex-wrap: wrap;
}
.tag-filter {
width: fit-content;
font-size: 0.9rem;
color: var(--color-text);
padding: 8px;
border-radius: 12px;
cursor: pointer;
transition: background-color var(--duration-animation) var(--anim-curve);
}
.tag-filter-selected {
background-color: var(--color-highlight-alt);
}
.tag-filter:hover {
background-color: var(--color-background-highlight-hover-alt);
}
@media screen and (max-width: 600px) {
.tag-filter-container {
gap: 8px;
}
.tag-filter {
font-size: 0.8rem;
}
}
</style>
</Content>

View File

@@ -1,8 +1,11 @@
<div class="callout-warning">
<p><b>Note</b>: as I'm constantly learning new things, this blog post is sort of outdated now. The information is still correct, but it feels inefficient and leads into a dead-end. I dislike this, as I wanted to write a guide that provides an expandable base. Therefore, I'll likely either update or replace this article and trim it down to focus on the important bits.</p>
<script lang="ts">
import ReactionQuote from "$lib/components/reaction-quote.svelte";
</script>
<p>Expect a Go API.</p>
</div>
<ReactionQuote
reaction="warn"
text="<b>Note</b>: as I'm constantly learning new things, this blog post is sort of outdated now. The information is still correct, but it feels inefficient and leads into a dead-end. I dislike this, as I wanted to write a guide that provides an expandable base. Therefore, I'll likely either update or replace this article and trim it down to focus on the important bits. Expect a Go API."
/>
*Hey!*

View File

@@ -0,0 +1,118 @@
<script lang="ts">
import SubtitledImage from "$lib/components/subtitled-image.svelte";
import ReactionQuote from "$lib/components/reaction-quote.svelte";
</script>
fuck i'm so sick of this shit. i need to incoherently ramble about this
<ReactionQuote
reaction="pointing"
text="hey. if you read the headline and agree with it but don't want to read some mildly infuriating content, skip this post. remember that you're cool and valid, and enjoy the rest of your day. if you are pro-AI, i also recommend you don't read this post. leave this page. in fact, kindly leave my website and reconsider your decisions. if you decide to stand your ground, do not interact with me."
/>
## bluesky is vibe-coded
I was scrolling Bluesky earlier today; something I do maybe once every two weeks when I'm really bored. I only follow a few people (such as [Technology Connections](https://bsky.app/profile/techconnectify.bsky.social), [f4mi](https://bsky.app/profile/f4mi.bsky.social), and [Freya Holmér](https://bsky.app/profile/freya.bsky.social)), so there's never a lot of content on my feed. It's why I sometimes go over to, Orvus forbid, the Discover feed.
What do I see? The Bluesky team is using AI. No, the Bluesky team is *embracing* AI. Actually, the Bluesky CIO Jay Graber is openly admitting to pretty much vibe-coding the website while also working on an AI bot that has, as of a few days ago, been [pre-emptively blocked by over a hundred thousand people](https://bsky.app/profile/bendiskin.bsky.social/post/3miarr2li3k2n). Hear this, Bluesky team: **PEOPLE DO NOT WANT THIS.**
Apparently, this bot is meant to [create custom feeds](https://techcrunch.com/2026/03/28/bluesky-leans-into-ai-with-attie-an-app-for-building-custom-feeds/). It's a task so simple, Jay Graber felt the need to defend it:
<SubtitledImage
image="jay.webp"
altText="Two Bluesky posts from Jay Graber, CIO of Bluesky. The first one reads: 'We hear the concerns about AI. Our goal is to use this technology to give people greater control, not to generate content. Attie uses AI to help you create custom feeds without having to know how to code.' The second one follows up with: 'We'll look into ways to take into account the preferences expressed by people who've blocked @attie.ai. The team has been making progress towards private accounts. There was a lot of good feedback on the tech at ATmosphereConf this week, and we'll have more to share soon.'"
subtitle="&quot;We hear that you dislike what we do. That doesn't mean we'll listen to you.&quot;" />
I find it terrifying how tech CEOs (or CIOs, whatever) continually press on to actively encourage media illiteracy. Building a feed is trivial, not to mention a very personal thing to do what's a 'personal' AI generated feed but someone else's feed? Not counting the waste of our limited land and resources, the overconsumption of our drinking water, the pollution of our air, and everything else associated with running an AI model for a job as trivial (and yet computationally maximally expensive) as this, of course.
As someone who's studying to become a teacher, this really grinds my gears. It's like, opposing education. To give some background on my perspective: the state I live in has a **severe** problem with children scoring low and not being able to acquire the knowledge and skills that are expected of them. Children in primary school barely know how to read anymore. Youths leaving middle school are barely fit for the job market and their apprenticeship applications get rejected because of it, literally leading them into a life of unemployment and poverty right out of the gate. All this while we're suffering from a massive teacher shortage, with some schools either ending school days early or completely cancelling school on some days of the week. Of course, the [gymnasiums](https://de.wikipedia.org/wiki/Gymnasium), where the more fortunate children (not always but often literally from wealthier households) are being taught, struggle less with this, while elementary schools and middle schools for special needs children are hit the hardest. There are a lot of factors at play here, but logically it would make sense to work on fixing them while not introducing new issues, right?
Imagine my fucking frustration when the department of education department decided to provide an *AI chatbot* to all students. What. The. Fuck. They could provide more funding to improve the schools this includes hiring more staff, but also renovating the dilapidated school buildings, some of which are in such poor condition, one of my internship schools had a fence around one of its buildings because there was a danger of *roof tiles falling and striking children*. Instead, they choose to reduce one of the few aspects that school is still good at: enabling children to establish and practice social relationships in a safe environment!
For a while, I tried going into meetings with higher-up people to explain the consequences of this for our children, but these people are so insanely pro-AI, it's like they've been indoctrinated by venture capitalist firms.
To bring this back to the topic at hand: this is one of the reasons why I hate AI integration so much. It discourages trying new skills, failing, learning from your mistakes, allowing yourself to grow. All of this is taken away in lieu of... some averaged-out garbage Silicon Valley is trying to convince you is just as good as the real product.
...when it's not even about the end product!!! I couldn't care less about some of the projects I made in the past what's much more important to me are the experiences and skills I gained from making them!
I may delete my Bluesky account over this. It would be a bit of a shame because it means I'll be further disconnected from some of the people whose works I'm interested in, although realistically I barely use the site anyway. It's like when I deleted Instagram; I feared I'd be disconnected from the people around me, only to realise that I already was and that deleting my account didn't make a difference.
<SubtitledImage
image="scott.webp"
altText="A post by Jay Graber: 'Bluesky is made with AI, the engineers and even some non-engineers use Claude code'. Scott Frerichs responds with: 'God, why are all you tech people like this? And can you please, please fucking stop??'"
subtitle="Scott, du sprichst mir von der Seele." />
## horny men and conservatives are pathetic
<ReactionQuote
reaction="warn"
text="<b>content warning</b>: abuse of women. also, discussion of conservative behaviour. <a href='#protecting-my-server-from-ai-using--ai-'>click here to skip</a>"
/>
In Germany, the name Collien Fernandes has been making its rounds [in the media](https://www.tagesschau.de/inland/gesellschaft/deepfakes-ki-strafrecht-collien-fernandes-christian-ulmen-100.html). It's about Fernandes' ex spreading pornographic material of her on the internet. This has led to a discussion about deepfake pornography on the internet.
And wouldn't you believe it, just this week there were reports on the christian conservative party (CDU), whose members have created a WhatsApp group exclusively made up of men that shared deepfake pornography of their colleagues. The news is highlighting (just to be clear: not showing!) a video of a woman in a bikini, dancing, with the face of a female party colleague superimposed on that woman. This apparently happened back in January, and of course no action had been taken until the incident reached the public. In fact, a politician named Adrian Mohr running for mayor's office in the town of Dörverden in Lower Saxony has directed his peers to ["keep quiet" about the situation](https://www.weser-kurier.de/landkreis-verden/gemeinde-doerverden/doerverden-buergermeisterkandidat-im-fokus-nach-deepfake-affaere-doc858og8il2g1986wvlka) (the article is in German and behind a paywall, but you can circumvent the paywall by disabling JavaScript).
I've said it before, and here it is in writing: I *despise* conservatives. Hypocritical, spineless shits that will do anything to save their own hides while actively harming the lives of everyone except themselves, in every regard: accessibility, public transport, taxes, women's rights... just think of anything and conservatives will try to ruin it.
It really makes me wonder why people like Wiebke Winter are proud and public members of the CDU, a party that clearly shows it views people like her as nothing more than sexual objects to be used and abused.
## protecting my server from AI using... AI?
Since finding out [that an insane amount of bots is trying to steal data off my server at all times](/blog/2026/0214/), I began thinking more about server security. I set up `fail2ban` for instance, which IP-bans connecting clients after a few failed attempts. I also found a tool named [Anubis](https://anubis.techaro.lol/).
It looked really cool. A protective layer against AI bots scraping my website? Made by a queer person? A cute hand-drawn mascot? *Sign me up!*, I thought. I struggled for a long time trying to set it up, as the documentation isn't fantastic, but I found [a blog post](https://huskee.gay/blog/anubis-nginx-docker/) that helped me finally get it up and running.
One thing kept nagging me, however: the developer. Now, I don't want to speak too ill of Xe, it seems to me that they're a good person overall. But browsing their Bluesky profile, their blog, or even just their company's website [techaro.lol](techaro.lol) reveals positivity towards AI that feels extremely hypocritical. How could I take someone's anti-AI scraper tool seriously if their company literally offers consulting on AI?
I've taken down my Anubis instance for now. This doesn't mean I won't ever use it in the future, but right now, the developer's hypocritical use of AI makes me feel too conflicted to be comfortable using Anubis.
Another thing that perplexed me was that [the mascot for Anubis was initially AI generated](https://en.wikipedia.org/wiki/Anubis_(software)#/media/File:Former_Anubis_mascot_pensive.webp). I *hate* when people claim their AI use is justified when they use it as "placeholder art". Gamedevs keep doing that more and more lately and it's infuriating. Speaking of which...
## shovelware in 2026
Lately, whenever I'm looking for new games on Steam, one of the first things I do is scroll down to see whether there's an "AI Generated Content Disclosure". I'm actually quite happy Valve implemented this, as it allows me to very quickly identify games that are not worth my time. Thing is though, I can't decide whether the blubs that these "developers" keep writing are hilarious or pathetic.
Take this blurb by the team behind the title "I Am Jesus Christ" (weird. not linking it):
<ReactionQuote
reaction="quote"
text='"AI tools were used in the production of the voice acting. We are a five-person team that has been working on this project for several years without the budget of a large studio. In our case, this was a practical solution that allowed us to complete the game at the level of quality we were aiming for. All creative decisions and the final shape of the game were made by our team. AI was used as a supporting production tool, not as a replacement for the creative work behind the game."'
/>
This, when dissected, is saying:
- AI was used to replace creative human work
- The developers do not want their use of AI to be viewed as replacing creative human work
- They view their use of AI justified by the fact they are a small team
The way they *instantly* defend their AI usage shows they *clearly* know using generative AI is terrible and frowned upon. They play it off as if they had no choice but to use AI to develop the game! *The quality would have suffered otherwise!*
And using the small size of your team as a justification for not putting effort into your game? It screams cash grab. Get everyone on board, shove out something that can pass as a 'game' as quickly as possible, and then cash in. Fantastic games were produced by small teams, or even solo devs. Do I even need to mention people like Toby Fox?
I wish I could name more people here, but I'm admittedly not as tapped into games as I used to be.
After all this though, you may ask yourself: is the AI generated voice acting of "I Am Jesus Christ" *at least* any good?
No. Of course it isn't. It's shit.
## flash memory is too damn expensive
On perhaps a somewhat lighter note...
With my recent efforts in working on my remote Hetzner server to self-host services such as [Gitea](https://code.natconf.dev), [copyparty](https://files.natconf.dev), and of course this website, I felt I wanted to do a little more. Host some other cool services. It's just that they require more storage and don't really feel at home on a remote server. I wanted a *home* server.
What I had in mind was a small micro PC, the ones businesses keep chucking out, to run things such as Immich, Jellyfin, paperless-ngx, and a local Gitea mirror. They don't use a lot of power, so they'd be perfect. I even found some on Ebay, except that they all came without RAM and storage...
Oh yeah. This is about the flash memory crisis. This stuff has gotten CRAZY EXPENSIVE lately. Why? AI.
For instance, I bought a 1 TB Samsung 970 Evo NVMe SSD at the end of 2020 for 140€. The technology was fairly new at this point, so it was understandably expensive back then. Then, when I built my PC in 2023, I bought a second one for less than 70€. Right now, this very SSD one that's several PCIe generations behind [is going for 220€](https://geizhals.de/samsung-ssd-970-evo-plus-1tb-mz-v7s1t0bw-a1972735.html)! How is that even possible?? Not to mention the RAM situation, where [32 GB kits are being sold for upwards of 400€](https://geizhals.de/corsair-vengeance-rgb-grau-dimm-kit-32gb-cmh32gx5m2b6000z30k-a2866139.html)...
Now, I'm quite fortunate in the fact that I got an old school PC from my job that I can use. In fact, it's what's running my home server applications right now, and quite well too! The CPU is pretty weak (it's some kind of Intel Pentium from a decade ago) but it's got 16 GB of memory and has been performing good enough. I also replaced the hard drive with a cheap (back when I bought it) 480 GB Crucial SATA SSD I originally used to run a custom firmware on my PS3. It was totally overkill for the job, considering the PS3 doesn't even support SATA III, and since my PS3 is kind of on its last legs, I'm using my PC to emulate PS3 games anyway, on the odd day I feel the desire to play those games.
I do worry about the power consumption though, and I would prefer a micro PC with a power-efficient CPU over my current setup. I wish the companies working on AI, or even just the people using these AI products, would reflect on the amounts of power wasted through their actions...

View File

@@ -0,0 +1,47 @@
This post is very reminiscent of [Tom Scott's newsletter](https://www.tomscott.com/newsletter/), where he shares interesting stuff he finds on the internet. I've been browsing around lately, and there are some things I'd like to share now too!
## isopod.cool: Discord alternatives
[link here.](https://isopod.cool/blog/posts/discord2/)
Ada's post on finding alternatives to Discord (and an unfortunate revelation about Matrix not being the end-all-be-all I was hoping to migrate to in a far-away future) sums up the frustration I feel when thinking about the platform and moving away from it. How do you find a messenger that can replace the ubiquitous platform everyone is already using? How do you ensure that this one won't make terrible mistakes and force you to migrate again in a year's time? If anything, I'm glad my move to Signal has worked out well so far, even though I still only have two people on there that I regularly text.
It's an interesting dive into many Discord alternatives with, spoiler alert, no clear recommendation because there just isn't any one platform that would be a perfect fit for the position right now.
Worth reading are also some of the posts linked therein, such as Terence Eden's ["I'm never going back to Matrix"](https://shkspr.mobi/blog/2025/07/im-never-going-back-to-matrix/) for a grim look at what Matrix has become. Speaking of Terence...
## openbenches.org: memorial benches from all over the world
[link here.](https://openbenches.org/)
While browsing fellow standards nerd [Terence Eden](https://shkspr.mobi/)'s website, I stumbled upon their project Open Benches, which collects and archives pictures of memorial benches across the world. It's a cool project with an impressive amount of volunteer work put into it the website has received pictures of more than 40.000 benches, with some contributors having sent in thousands of images on their own!
A majority of the benches collected are in the UK, where Terence lives, but there are entries from all over the globe. I'm planning on contributing too there's a park near me with A LOT of memorial benches that have not been documented yet!
## mayvmusic.com: thirty-one degrees
[link here.](https://mayvmusic.com/thirtyonedegrees)
This is a short post by an artist I recently discovered, Mayv. The message is important for any artist believing they are not progressing: any work you put into your hobbies is progress, you're always improving, even if it doesn't always feel like it. Definitely go read it, but keep in mind that 31 degrees refers to temperature in Fahrenheit!
## eugodr.net: i hate the smartphone
[link here.](https://eugodr.net/p/i-hate-the-smartphone/)
Egor's post on smartphones is one that resonated with me when I read it. There's something quite dystopian about the omnipresence of smartphones in our lives, and this post sums it up quite well.
I do wish we had less of a dependency on phones. For instance, I used to be positive towards consolidating services on our phones, such as payment and tickets. Now I wish this weren't the case: I can and do carry my bank card with me fortunately, but my public transport ticket is on my phone and I cannot change that, meaning I can't use buses/trams/trains without carrying my phone. One time, I rode my bike around the city and didn't bring a phone (not even a music player of any kind!) and it was great; I wish I could do that more but in a more everyday sense.
I dislike the dependency on brain-rotting short-form media. I deleted my Instagram account because I spent too much time being sucked into this rubbish content, on a device I (need to) carry with me everywhere I go, on an app that is designed to waste as much of my time as physically possible. I tell all my friends. They don't see a problem in their overconsumption, though; or at least not enough of a problem to do something about it. While every now and then someone tells me they'd like to share a reel/TikTok/whatever with me but can't because I either deleted my account or never had one to begin with, there's something much more liberating about not being on these platforms that I just can't give up anymore.
Yet, I catch myself staring at this thing much more than I would prefer. At least I reflect my usage, I tell myself.
I recommend reading the article because it's worth re-evaluating the (and I use this term deliberately) relationship we have with these slabs of glass.
## alexwlchan.net: making a PDF that's larger than Germany
[link here.](https://alexwlchan.net/2024/big-pdf/)
I stumbled upon Alex's blog through a post on [creating files in Go if they don't exist yet](https://alexwlchan.net/notes/2025/exclusive-write-in-go/), and while it helped me in what I was trying to do, I found this much more interesting article about creating a PDF larger than my home country. I had to check it out!
It's an in-depth look at the PDF format and how the supposed maximum file size restriction came to be. Definitely interesting if you're into this sort of stuff!

View File

@@ -0,0 +1,15 @@
I've been working a lot on this website lately and I think I finally arrived at a state I'm happy to put online.
The [`/projects`](/projects) page has been updated! Instead of displaying all projects in a stream, they are now linked to their separate pages, which allows for much more detail and visual material on all of my individual projects. You'll find a gallery element on some pages to scroll through images and read blurbs about them! I also added filters for viewing specific project categories. Some project pages have already been updated with more content, but not all. I'll tackle this soon.
I also moved blog post `2026/0325` (the LIGHTYEARS font post) to a separate project page, which you can now find [here](/projects/misc/lightyears-font). This also means the deprecation of the short-lived `i-made-this` tag, as there's a better replacement:
I newly added the [`/feed`](/feed), which is a stream similarly to the old projects page, but it features smaller projects instead. It replaced the small projects page in purpose, though all of the small projects have found a home on [`/projects`](/projects). There's pagination so that your network connection won't be totally overloaded once the feed grows.
The drawing gallery has been deleted in favour of posting sketches on the [`/projects`](/projects) and [`/feed`](/feed) pages, depending on which fits better.
The updates page has also been removed in favour of a little status poster tool I'm working on. It's not quite ready but I'm already using it on the [main page](/) (under "latest status"). It'll be similar to [status.cafe](https://status.cafe) but self-hosted because of course I would do that.
I also added a [`/meta/now`](/meta/now) page as a [long-term update page](https://nownownow.com/about). As [`/now`](/now) is more standard, it also works and redirects to `/meta/now`. There's also [`/meta/music-gallery`](/meta/music-gallery), where I'll soon share and maintain my current music rotation.
On the topic of music: the my-tracks page currently still exists, but without content. It'll probably be relocated (maybe under [`/projects`](/projects)).

View File

@@ -1,7 +1,5 @@
<script>
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import TableOfContents from "$lib/components/table-of-contents.svelte";
import ContentSidebar from "$lib/viewport/content-sidebar.svelte";
export let data;
</script>
@@ -12,18 +10,16 @@
<meta name="DCTERMS.created" content="{data.date}T{data.time}">
</svelte:head>
<Content>
<Banner2
title="{data.title}"
subtitle="{data.subtitle}"
date="{data.date}"
dateUpdated="{data.dateUpdated}"
banner="{data.banner}"
bannerAlt="Banner for blog post '{data.title}'"
tags={data.tags} />
<TableOfContents />
<ContentSidebar bannerContent={{
title: data.title,
subtitle: data.subtitle,
date: data.date,
dateUpdated: data.dateUpdated,
banner: data.banner,
bannerAlt: `Banner for blog post '${data.title}'`,
tags: data.tags,
}}>
<svelte:component this={data.content} />
</Content>
</ContentSidebar>

View File

@@ -30,13 +30,59 @@ export enum BlogPostTag {
NULL = "all", // placeholder when a 'no tag' is needed. if in doubt, do not use this
ART = "art-stuff", // ramblings to do with art
DRAWING = "drawing", // self-explanatory
IMADETHIS = "i-made-this", // stuff i made
// not used because of the new projects page serving this exact purpose
// IMADETHIS = "i-made-this", // stuff i made
META = "natconf-meta", // about the website itself
RANT = "rant", // self-explanatory
TECH_TIP = "tech-tip", // tech guides
}
export const posts: BlogPostLink[] = [
{
key: "2026/0414",
post: {
date: "2026-04-14",
time: "12:24",
banner: "banner.webp",
bannerAlt: "A street sign with the text 'THE OCEAN AT THE END OF THE LANE' and a logo of the city of Portsmouth in the United Kingdom.",
title: "website updates lately",
description: "new and exciting developments on natconf.dev",
tags: [
BlogPostTag.META,
],
}
},
{
key: "2026/0404",
post: {
date: "2026-04-04",
time: "22:30",
banner: "banner.webp",
bannerAlt: "A bench at the end of a short path in a park. The path splits and leads both to the left and the right. The bench is surrounded by trees, bushes, and vibrant green grass.",
title: "cool places on the internet",
description: "There are some cool places on the net and I need to show you some!",
tags: [
// TODO what kind of tag? prob need a new one
],
}
},
{
key: "2026/0402",
post: {
date: "2026-04-02",
time: "23:56",
banner: "banner.webp",
bannerAlt: "A screenshot from the game Animal Crossing: New Horizons showing a speech bubble by the character 'Al', which looks like 'AI'.",
title: "i'm so fucking fed up with ai",
description: "It's time to stop.",
tags: [
BlogPostTag.RANT,
],
}
},
{
key: "2026/0326",
post: {
@@ -51,20 +97,6 @@ export const posts: BlogPostLink[] = [
],
}
},
{
key: "2026/0325",
post: {
date: "2026-03-25",
time: "22:22",
banner: "banner.webp",
bannerAlt: "A sunset captured from an Autobahn exit.",
title: "I made a LIGHTYEARS font",
description: "I feel electric and it's only getting brighter!",
tags: [
BlogPostTag.IMADETHIS,
],
}
},
{
key: "2026/0317",
post: {
@@ -122,7 +154,6 @@ export const posts: BlogPostLink[] = [
tags: [
BlogPostTag.ART,
BlogPostTag.DRAWING,
BlogPostTag.IMADETHIS,
],
}
},
@@ -152,7 +183,6 @@ export const posts: BlogPostLink[] = [
tags: [
BlogPostTag.ART,
BlogPostTag.DRAWING,
BlogPostTag.IMADETHIS,
],
}
},

View File

@@ -0,0 +1,151 @@
<script lang="ts">
import Content from "$lib/viewport/content.svelte";
import Banner2 from "$lib/banner2.svelte";
import { drawings } from "./drawings";
let selectedIndex: number = $state(0);
let drawingsRev = drawings.toReversed();
</script>
<svelte:head>
<title>Drawing Gallery | denizk0461</title>
</svelte:head>
<Content bannerContent={{
title: "Drawing Gallery",
banner: "banner.webp",
bannerAlt: "Several Faber-Castell Polychromos colour pencils lined up with markings next to them in the same colour on a sheet of paper.",
subtitle: "sketches & stuff",
}}>
<div class="selected-container">
<a class="selected-img-link" href={drawingsRev[selectedIndex].src}>
<img
class="selected-img"
src={drawingsRev[selectedIndex].src}
alt={drawingsRev[selectedIndex].alt}
/>
</a>
<div class="selected-text-container">
<div class="selected-text-header">
{#if drawingsRev[selectedIndex].title}
<p class="selected-title">{drawingsRev[selectedIndex].title}</p>
{/if}
<p class="selected-date">from {drawingsRev[selectedIndex].date}</p>
</div>
{#each drawingsRev[selectedIndex].desc as d}
<p>{@html d}</p>
{/each}
</div>
</div>
<hr>
<div class="img-button-container">
{#each drawingsRev as d, index}
<button class="img-button" onclick={() => selectedIndex = index}>
<img src={d.src} alt={d.alt} loading="lazy" />
</button>
{/each}
</div>
</Content>
<style>
.selected-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.selected-img-link {
width: fit-content;
height: 400px;
margin: 0 auto;
overflow: hidden;
}
.selected-img-link:hover .selected-img {
scale: 1.06;
}
.selected-img {
object-fit: contain;
width: 100%;
height: 100%;
max-height: initial;
scale: 1.0;
transition: scale var(--duration-animation) var(--anim-curve);
}
.selected-text-container {
display: flex;
flex-direction: column;
gap: 8px;
overflow: scroll;
padding-right: 12px;
max-height: var(--media-height);
}
.selected-text-header p {
font-weight: 600;
font-style: italic;
}
.selected-title {
font-size: 1.1rem;
}
.selected-date {
font-size: 1.0rem;
}
.selected-text-container * {
margin: 0;
}
.img-button-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
gap: 4px;
}
.img-button {
cursor: pointer;
width: 100%;
overflow: hidden;
border-radius: var(--border-radius);
/* filter: brightness(70%) saturate(60%); */
outline: var(--border-style) var(--border-dash-size) transparent;
transition: outline-color var(--duration-animation) var(--anim-curve);
}
.img-button img {
width: 100%;
height: 200px;
object-fit: cover;
filter: brightness(100%) saturate(80%);
transition: scale var(--duration-animation) var(--anim-curve),
filter var(--duration-animation) var(--anim-curve);
}
.img-button:hover img {
scale: 1.12;
}
.img-button:active img {
scale: 1.08;
}
.img-button-container:hover img {
filter: brightness(70%) saturate(60%);
}
.img-button:hover {
outline-color: var(--color-highlight);
}
.img-button:hover img {
filter: brightness(100%) saturate(100%);
}
</style>

View File

@@ -0,0 +1,171 @@
export interface Drawing {
title?: string;
date: string;
src: string;
alt: string;
desc: string[];
}
export let drawings: Drawing[] = [
{
date: "2026-01-29",
src: "/blog/2026/0129/girl.webp",
alt: "A small drawing of an anime-style girl's head. She has a ponytail and is looking towards the left with a concentrated gaze.",
desc: [
"A small sketch (only like 4cm wide) that I drew with a ballpoint pen on pink paper. The fact that I was able to sketch this, without any prior practice, plus an intrinsic want to be able to draw made me seriously consider learning to draw.",
"Having learned just a little bit about drawing, I can say now (a month and a half later) that this isn't great, but it served its purpose of making me start to draw!",
],
},
{
title: "Bread Girl",
date: "2026-01-30",
src: "2026/breadgirl.webp",
alt: "An anime-style girl chewing on a piece of bread. She wears a ponytail and a sleeveless top.",
desc: [
"I drew her during a game of Wizard. I initially wanted to make her chew on a whole loaf but I didn't know how to draw that.",
"Wasn't really sure how to convey that her mouth is full either, but in retrospect, I could have exaggerated the bow in her lower eyelids to do so.",
"I like her eyes. Her head could be taller, actually.",
],
},
{
title: "Elizabeth",
date: "2026-02-18",
src: "/blog/2026/0205/13-2.webp",
alt: "A pencil drawing of a girl looking to the left. She is wearing a cropped loose tee and jeans, while her right hand is hinted to rest on her hip.",
desc: [
"She's the product of me trying to re-draw the character I drew on day 1 of my drawing challenge, and I was really glad to see that I had actually improved!",
],
},
{
title: "Emilia",
date: "2026-02-23",
src: "/blog/2026/0205/18.webp",
alt: "A pencil sketch of a girl holding up a V sign with her left arm. She is wearing a long-sleeve shirt, jeans, and her hair is tied up in a ponytail. She is winking, the other eye is coloured green. Her body is tilted towards the right side of the paper. In the top right corner is a lightly-drawn sketch of the girl's pose.",
desc: [
"My first character with the new style of drawing eyes I picked up from a manga drawing book!",
"I named her Emilia because she looked like a more nice and caring character.",
],
},
{
title: "Porter Robinson fanart",
date: "2026-02-26",
src: "/blog/2026/0205/21.webp",
alt: "Two pencil sketches traced over with a black fineliner. The left one is of a hand with a cube in its palm, sketched after the hand on the cover of Porter Robinson's album Worlds. Beneath it is an emoticon used on the same cover. To the right is a manga-style head with green eyes and wavy hair, meant to resemble Porter Robinson's Vocaloid mascot Po-Uta.",
desc: [
"I drew the Worlds hand for practice and then decided to draw Po-Uta's head as well. I realised then that learning to draw gave me the ability to draw fanart.",
"I had never considered that possibility before.",
],
},
{
title: "Cyborg Arm",
date: "2026-02-27",
src: "/blog/2026/0205/22.webp",
alt: "A pencil sketch of a girl with a ponytail, crop top, and track pants with a slightly shocked look on her face. She is looking at her right arm, which is a cyborg part.",
desc: [
"Possibly my favourite sketch from the drawing challenge, because she looks cool, but also because her design deviates from the other characters a bit.",
],
},
{
date: "2026-03-04",
src: "/blog/2026/0205/27-1.webp",
alt: "A drawing of a girl with her head tilted towards her right shoulder. She is smiling with her eyes closed and is holding up a victory sign with her left hand. She has her hair in a ponytail and is wearing jeans with shoulder straps, and there is a scrunchie on her left wrist as well.",
desc: [
"This is actually the construction sketch of a drawing I later went over with a fineliner and coloured pencils. However, I kind of prefer the pencil sketch.",
"This was my first attempt at a head-on perspective. I had fun drawing details like the scrunchie, the jeans, and the smile!",
],
},
{
date: "2026-03-10",
src: "2026/0310.webp",
alt: "A digital drawing of a girl with long brown hair in a ponytail. She has green eyes and is wearing a cropped shirt with stripes, an orange spaghetti top underneath, and dark trousers. She is holding her hands behind her back.",
desc: [
"ok i changed my mind on digital art. it's awesome.",
"My first drawing using Krita! I went with my usual methods but tried refining some things and adding (hopefully not overly misplaced) shadows too. I ended up really liking the ability to use layers, and colour in digital art just pops so nicely.",
"Initially, I drew the left arm in front of her body but later changed this to avoid drawing the hand.",
],
},
{
date: "2026-03-18",
src: "2026/0318.webp",
alt: "A smiling character with a ponytail and bangs. Their eyes are closed.",
desc: [
"I drew this character while testing brushes in Krita. It's why the coloured spaces all use a crayon-like brush whereas the shadows are dotted. It doesn't fit together, but hey, I had to learn that at some point, right?",
"I like her expression, it's cute.",
],
},
{
date: "2026-03-28",
src: "2026/0328.webp",
alt: "Different eyes in different colours. The eye in the centre has a four-pronged star for a pupil.",
desc: [
"Some eye drawing practice while I was staying at a friends' house. Wanted to try some shading and overall just making the eyes look more interesting.",
"In retrospect, the eyebrows don't look good, and the eyelashes (on the bottom eye) are way off. I do like the star for the pupil on the middle one though!",
],
},
{
date: "2026-03-29",
src: "2026/0329.webp",
alt: "Different eyes in different colours.",
desc: [
"More eye drawing practice. Note the eyeshadow as well as the shape of the eye in the top left. Eyelashes on the top one are still way off; I think it's both their size as well as their quantity (way too many individual hairs!).",
],
},
{
title: "Lecturer Confused About Gender",
date: "2026-04-10",
src: "2026/0410.webp",
alt: "A sketch of my lecturer. He has short hair, blue eyes, and is wearing glasses. Next to him is a thinking cloud with gender markers and question marks inside.",
desc: [
"This drawing was inspired by my statistics lecturer saying (in good faith): \"there's male and female, but there's also diverse, which is a lot of genders, well I actually don't know how many\".",
],
},
{
title: "Dyed Hair",
date: "2026-04-17",
src: "2026/0417.webp",
alt: "The head of a character facing away. Their hair is black and red.",
desc: [
"I played around with brushes during my statistics class and made this. There are a few things I like about it. I like that I drew it without outlines and it actually looks decent. I also love the hair colour gradient.",
"It doesn't really make sense for the hair to transition from red at the roots to black and then back to red, but it doesn't matter either. It looks cool.",
],
},
{
date: "2026-04-20",
src: "2026/0420.webp",
alt: "Several pencil sketches including Plankton and Patrick from SpongeBob Squarepants as well as other faces. There's also a close-up of a semi-realistic green eye and a box with a plate and a single sphere on it, with the text 'One Pea 5€'.",
desc: [
"I've not given up on drawing using physical paper and pencils! And I've learned to draw Patrick('s head), for better or for worse.",
"This was drawn during a meeting a student group of mine had at university. I had fun drawing Patrick with different expressions! Plankton ended up not so good.",
"I quite like the more realistic-looking eye, especially the front view. The 'website girl' is me deciding on the head to use for sketches I want to put on this website for some text callouts. Haven't gotten around to drawing these sketches yet, but they'll be cool once I do.",
"The 'One Pea 5€' was me anticipating Tomodachi Life: Living the Dream.",
],
},
{
title: "patrick doing not so good",
date: "2026-04-21",
src: "2026/0421.webp",
alt: "Two drawings of Patrick Star's head. The left is neutral, the right looks exhausted.",
desc: [
"Had some more fun drawing Patrick during my shift. The right one is me at work (I work in IT support).",
],
},
{
date: "2026-04-24",
src: "2026/0424_hand.webp",
alt: "A hand with its fingers spread out. The nails are painted green.",
desc: [
"Another statistics class sketch! I'm bad at hands so I wanted to practice. I was a bit worried doing this, as other people in my class could see what's on my tablet, but I want to get over that feeling of people judging my drawings.",
"I played with shading in this one. It went okay. I didn't really know how to shade. The thumb also ended up too thin.",
],
},
{
title: "Blinn-Phong",
date: "2026-04-24",
src: "2026/0424_ball.webp",
alt: "An orange sphere shaded to look three-dimensional.",
desc: [
"This is the dot from a '!' symbol I wrote in my statistics notes. I decided to try and shade it similarly to how I've seen it in game engines and 3D modelling software -- even with a specular highlight!",
"I like how this turned out! From a distance, it really looks kind of 3D, especially compared to all the other notes on the page.",
],
},
];

View File

@@ -0,0 +1,128 @@
<script lang="ts">
import ImageRow from "$lib/media/image-row.svelte";
import ContentSidebar from "$lib/viewport/content-sidebar.svelte";
export let data;
</script>
<svelte:head>
<title>Creative Feed | denizk0461</title>
</svelte:head>
{#snippet pageButtons(currentIndex: number)}
<div class="page-button-container blurred-background">
{#if currentIndex == 1}
<p class="page-button page-button-left page-button-disabled">x</p>
{:else}
<a class="page-button page-button-left page-button-clickable" href="?p={currentIndex - 1}">&lt;</a>
{/if}
{#each { length: data.maxPages }, index}
<a class="page-index" href="?p={index + 1}">{index + 1}</a>
{/each}
{#if currentIndex == data.maxPages}
<p class="page-button page-button-right page-button-disabled">x</p>
{:else}
<a class="page-button page-button-right page-button-clickable" href="?p={currentIndex + 1}">&gt;</a>
{/if}
</div>
{/snippet}
<ContentSidebar bannerContent={{
title: "Creative Feed",
banner: "banner.webp",
bannerAlt: "A blue screen with the text 'how do you do art ? 1. face your fears 2. become your heroes'. The 'art' looks to have been edited in. The music artist Porter Robinson is standing in the bottom right corner.",
subtitle: "minor things I have worked on",
}}>
<p>Welcome to my creative feed! It is heavily inspired by <a href="https://deathsurplus.com/">DeathSurplus' art blog</a> definitely go check out his website!</p>
<p>This page is intended to be a contrasting companion to the <a href="/projects"><code>projects</code> page</a>; I'll use this page for smaller things that don't fit the dedicated-page-format.</p>
{@render pageButtons(data.currentPage)}
{#each data.feedPosts as post}
<h2>{post.metadata.title}</h2>
<p class="subtitle">{post.metadata.subtitle}</p>
<p class="subtitle">{post.metadata.date}</p>
<svelte:component this={post.content} />
{#if post.metadata.images && post.metadata.images.length > 0}
<ImageRow
images={post.metadata.images}
path={post.metadata.id}
/>
{/if}
{/each}
{@render pageButtons(data.currentPage)}
</ContentSidebar>
<style>
.page-button-container {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: center;
border-radius: var(--border-radius);
overflow: hidden;
width: fit-content;
margin: 12px auto;
border-top: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
border-bottom: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
}
.page-button, .page-index {
font-family: var(--font-mono);
padding: 8px 18px;
font-weight: 600;
margin: 0;
transition: background-color var(--duration-animation) var(--anim-curve);
}
.page-button {
color: var(--color-text);
}
.page-button-left {
border-right: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
}
.page-button-right {
border-left: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
}
.page-button-clickable:hover {
cursor: pointer;
}
.page-button-disabled {
color: var(--color-text-tertiary);
cursor: not-allowed;
}
.page-index:link, .page-index:visited {
color: var(--color-text)
}
.page-index:hover, .page-button-clickable:hover {
text-decoration: none;
background-color: var(--color-background-highlight-alt);
}
.subtitle {
font-family: var(--font-mono);
margin: 0;
font-size: 1rem;
line-height: 1.4rem;
font-style: italic;
font-weight: 700;
color: var(--color-highlight-alt);
}
.subtitle::before {
content: "<!-- ";
}
.subtitle::after {
content: " -->";
}
</style>

56
src/routes/feed/+page.ts Normal file
View File

@@ -0,0 +1,56 @@
import { entries, type FeedEntry } from './feed';
interface FeedPost {
path: string;
content: any;
metadata: FeedEntry;
}
let entriesPerPage = 8;
export async function load({ params, url }) {
// Get page index
let pageIndex = Number(url.searchParams.get('p'));
if (pageIndex == 0) {
pageIndex = 1;
}
// TODO check if index exceeds maximum permitted and redirect (to max page?)
let feedPosts: FeedPost[] = [];
let start = (pageIndex - 1) * entriesPerPage;
for (let i = start; i < start + entriesPerPage; i += 1) {
// Stop iterating when end reached
if (i >= entries.length) {
break;
}
let e = entries[i];
let pathArray = e.id.split("/");
let path = `./${pathArray[0]}/${pathArray[1]}`;
// Vite complains if I don't do this even though it's stupid
let page = await import(`./${pathArray[0]}/${pathArray[1]}.md`);
let md = page.metadata;
// let date = e.date;
// prevent SvelteKit from being 'smart' and formatting a YYYY-MM-DD string to a Date object or similar
// if ((date as string).endsWith("0Z")) {
// date = date.split("T")[0];
// }
feedPosts.push({
path,
content: page.default,
metadata: e,
});
}
let currentPage = pageIndex;
let maxPages = Math.ceil(entries.length / entriesPerPage);
return {
currentPage,
maxPages,
feedPosts,
};
}

View File

@@ -0,0 +1,9 @@
I disassembled my Switch because I wanted to check whether the battery was swollen inside. It's had some battery issues (although they seem to have gone away lately). During reassembly, I put too much force on the SD card reader and broke it. Ordered a new part, replaced it, but turns out I broke the board-side connector too. It doesn't grip the ribbon cable like it should. Squishing some cardboard in there to keep the cable connected and level with the board helped. I was worried it would fail, but it's been a couple of days now and it still works flawlessly. Just in time, too; I started playing the new Tomodachi Life and need the storage for all the screenshots and videos I'm capturing.
I also decided to tackle my 2011 3DS again. It broke in 2018: I was playing Super Mario Maker on the couch when it suddenly made a pop sound and turned off. When I disassembled it, I found that not only have I left the 3DS in a dismal state the last time I opened it -- most of the cables weren't even connected, not to mention the missing Y button, the removed battery due to swelling and the broken shoulder buttons --, but the ribbon cable connecting the speakers and top screen backlight had a *tear!* Finally, I knew what the problem may be!
I ordered a replacement cable and tackled the most challenging task imaginable when repairing a 3DS -- threading a cable through the hinge. It's a miracle I didn't destroy another cable in the process. I also replaced the Y button with a 3D printed one I quickly designed in FreeCAD. It works fine enough.
The 3DS works again!! It's been 8 years since it last booted!! I unfortunately don't have its SD card anymore, so the data is gone, but the system works! Truly a miracle.
Only thing left to do now is to fix the [USB-C port on my New 3DS](/projects/electronics/3ds-usb-c/)... it stopped accepting C-to-C connections, although A-to-C still works. Probably just need to resolder or replace the 5.1kΩ resistor.

View File

@@ -0,0 +1,5 @@
Inspired by Nothing phones and cool dbrand skins from way back, I gave my phone a clear back mod! I've grown a bit tired of the white back and wanted to do something cool and unique instead of just buying a new back and sticking that on there. I've been using this phone for 5 years and the glue has been struggling hard to hold onto the back, so I didn't feel bad doing this.
This was arduous and took a lot of time too (5 hours). I used a metal spudger, bamboo toothpicks, and cottons swabs and pads soaked in acetone-free nail polish remover to remove the two paint layers slowly but surely, being careful not to scratch the back too much. It revealed a slightly cloudly but actually decently transparent back that still preserved its slightly pearlescent purple glow!
Only contra is that the back can't hold on anymore because I removed all the glue, but since I'm keeping the phone in a case at all times, this isn't really an issue. Oh and, the exposure to alcohol made the back develop plenty of hairline cracks around the top and bottom edges, but it's still in one piece!

75
src/routes/feed/feed.ts Normal file
View File

@@ -0,0 +1,75 @@
import { type A11yImage } from "$lib/media/a11y-img";
export interface FeedEntry {
id: string;
title: string;
subtitle: string;
date: string;
posted: string;
images: A11yImage[];
}
export let entries: FeedEntry[] = [
{
id: "electronics/nintendo-repairs",
title: "Nintendo Handheld Repairs",
subtitle: "it's been 8 years...",
date: "2026-04-16 2026-04-23",
posted: "2026-04-24",
images: [
{
src: "cardboard.webp",
alt: "A Nintendo Switch with its back removed. The SD card reader connector is obstructed by a pink piece of cardboard.",
},
{
src: "knifetoswitch.webp",
alt: "The SD card reader connector of a Nintendo Switch currently being stabbed with a knife in an attempt at repair.",
},
{
src: "3dsopen.webp",
alt: "A 2011 Nintendo 3DS with its back covers removed on both the top and the bottom half.",
},
{
src: "thread.webp",
alt: "The hinge of a 3DS with some ribbon cables halfway pulled through.",
},
{
src: "threadsuccess.webp",
alt: "The hinge of a 3DS with three ribbon cables successfully pulled through.",
},
{
src: "3dsfunctional.webp",
alt: "A functional 2011 Nintendo 3DS on the home screen, displaying the game 'Zelda: Four Swords Anniversary Edition'",
},
],
},
{
id: "electronics/trans-phone",
title: "Transparent Phone Back",
subtitle: "nowhere to hide!",
date: "2026-04-11 2026-04-12",
posted: "2026-04-12",
images: [
{
src: "scratching.webp",
alt: "The back of a Samsung Galaxy S20 FE lying face-down on a deskmat. Toothpicks, cotton pads, and a metal spudger are lying nearby.",
},
{
src: "halfway.webp",
alt: "A white phone back that's half-scratched off to reveal the clear plastic.",
},
{
src: "clear.webp",
alt: "An entirely clear phone back.",
},
{
src: "final.webp",
alt: "A Samsung Galaxy S20 FE with a clear back inside a clear phone case. The phone has a sticker in the middle of the back that shows a lantern with leaves around it.",
},
],
},
];
export function getLatestPostDate(): string {
return entries[0].posted;
}

View File

@@ -1,8 +1,6 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import TableOfContents from "$lib/components/table-of-contents.svelte";
import LinkList, { type LinkEntry } from "$lib/lists/link-list.svelte";
import ContentSidebar from "$lib/viewport/content-sidebar.svelte";
let favouriteAlbums: LinkEntry[] = [
{
@@ -107,19 +105,18 @@
<title>About | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="About Me & natconf.dev"
banner="/me.webp"
bannerAlt="Mirror picture of me, pixelated beyond recognition"
subtitle="If you'd like to learn more about me and my website"
date="2025-08-10"
dateUpdated="2026-03-26"
pixelated />
<TableOfContents />
<ContentSidebar bannerContent={{
title: "About Me & natconf.dev",
banner: "/me.webp",
bannerAlt: "Mirror picture of me, pixelated beyond recognition",
subtitle: "If you'd like to learn more about me and my website",
date: "2025-08-10",
dateUpdated: "2026-04-03",
pixelated: true,
}}>
<p>Hi there! I'm Deniz (he/him/they). Welcome to my website!</p>
<p>Hi there! I'm Deniz (they/them). Welcome to my website!</p>
<p>If you found this page, you may be interested in further information. And further information you shall get! I wrote a little bit about myself as well as how I made this website down below.</p>
@@ -171,8 +168,6 @@
<p>The <a href="https://ratchetandclank.fandom.com/wiki/Ratchet">rat</a> in the bottom right of the screen is property of <a href="https://insomniac.games/">Insomniac Games</a>. Clicking it will bring you good fortune.</p>
<p>The style of the webring elements is adapted from a template provided by Rainbow Cemetery for the <a href="https://www.rainbowcemetery.com/devring/">Gamedev webring</a>. I adapted it into <a href="https://code.natconf.dev/denizk0461/pages/src/branch/master/src/lib/components/ring.svelte">a Svelte component</a> that allows setting the links, emojis, and arrows more easily.</p>
<h2 id="contact">Contact / Where to find me</h2>
<p>Best to e-mail me if you want to get in touch it's the only way I'm currently reliably available!</p>
@@ -187,7 +182,7 @@
<p>Have a small FAQ. No one ever asked these questions, but you may find the answer you didn't know you needed.</p>
<p class="faq-question">What does '0461' stand for?</p>
<p class="faq-question">What does '0461' in denizk0461 stand for?</p>
<p>It's a reference to Ratchet & Clank; Clank's designation is <a href="https://ratchetandclank.fandom.com/wiki/Clank">XJ-0461</a>. They started using that designation in <i>Ratchet & Clank: A Crack in Time</i> when Orvus is revealed to be Clank's father and he keeps calling him by his 'real' name.</p>
@@ -200,7 +195,7 @@
<p>When I was recently replaying the original Ratchet & Clank (2002) in German, I noticed again that the dialogue translated into German is often longer than the original English dialogue, which makes characters' voice lines run into one another. This doesn't happen in the English original.</p>
<p>This is most noticeable in the cutscene that plays after you acquire the Hologuise on planet Kalebo III, where <a href="https://youtu.be/XIShUN7AUqg?t=3479">the narrator explains how the Hologuise works</a>. The narrations lags WAY behind the visuals, to the point that there's a brief black screen at the end while the narration is still ongoing, and then it just cuts off the narrator entirely.</p>
</Content>
</ContentSidebar>
<style>
.faq-question {

View File

@@ -19,10 +19,10 @@
<title>Feeds | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="Feeds"
subtitle="XML feeds" />
<Content bannerContent={{
title: "Feeds",
subtitle: "XML feeds",
}}>
<p>This is a list of RSS feeds I maintain on this website. You can subscribe to them by adding the link of any feed to an RSS reader of your liking.</p>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import ReactionQuote from "$lib/components/reaction-quote.svelte";
</script>
<svelte:head>
<title>Music Rotation | denizk0461</title>
</svelte:head>
<Content bannerContent={{
title: "Music Rotation",
dateUpdated: "2026-04-14",
}}>
<p>content coming soon.</p>
<img src="/common/gadgetron.webp" alt="The Gadgetron vendor from Ratchet & Clank on PlayStation 2 looking bored.">
<ReactionQuote
reaction="pointing"
text="if the favourite tracks here differ from those I put on my Bandcamp profile, consider the ones here to be correct."
/>
</Content>

View File

@@ -0,0 +1,190 @@
export interface Entry {
title: string;
artist: string;
releaseYear: number;
artwork: string;
description: string[];
favouriteTracks: string[];
}
export let entries: Entry[] = [
{
title: "A Place Where Mountains Hide",
artist: "acloudyskye",
releaseYear: 2019,
artwork: "askye_apwmh",
description: [
"",
],
favouriteTracks: [
"",
]
},
{
title: "Blood Rushing Like Current Through a Powerline",
artist: "acloudyskye",
releaseYear: 2021,
artwork: "askye_brlctap",
description: [
"",
],
favouriteTracks: [
"",
]
},
{
title: "What Do You Want!",
artist: "acloudyskye",
releaseYear: 2022,
artwork: "askye_wdyw",
description: [
"",
],
favouriteTracks: [
"Curses",
"Safety!",
"Overthrower",
"Thief!",
]
},
{
title: "There Must Be Something Here",
artist: "acloudyskye",
releaseYear: 2024,
artwork: "askye_tmbsh",
description: [
"",
],
favouriteTracks: [
"Relay",
"Surface",
"Flares",
"Ditch",
]
},
{
title: "This Won't Be The Last Time",
artist: "acloudyskye",
releaseYear: 2025,
artwork: "askye_twbtlt",
description: [
"",
],
favouriteTracks: [
"Float",
"Innards",
]
},
{
title: "True",
artist: "Avicii",
releaseYear: 2013,
artwork: "avicii_true",
description: [
"",
],
favouriteTracks: [
"Wake Me Up",
"Dear Boy",
]
},
{
title: "it's hard to see color [When You're So Impossibly Far Away*]",
artist: "Jaron",
releaseYear: 2022,
artwork: "jaron/ihtsc",
description: [
"",
],
favouriteTracks: [
"When Everything is Grey",
"de4th\\__* Spirit",
"743⁺Aether*✧ ˳ ¹¹¹} ⁺ . ˳",
"WIRES222",
"NOWNEVER",
"Love [in Every Single Way]",
]
},
{
title: "LIGHTYEARS",
artist: "Jaron",
releaseYear: 2024,
artwork: "jaron/lightyears",
description: [
"",
],
favouriteTracks: [
"LIGHTYEARS",
"SPINNING",
"ICARUS",
"STARS",
]
},
{
title: "",
artist: "",
releaseYear: 202,
artwork: "",
description: [
"",
],
favouriteTracks: [
"",
]
},
{
title: "fishmonger",
artist: "underscores",
releaseYear: 2021,
artwork: "underscores/fishmonger",
description: [
"<i>fishmonger</i> is an album I started listening to about a year after it released, when I had my first job after quitting my computer science program. It joined my life in a time where I didn't really know what to do myself.",
"On one hand, I like the album because it was the start of finding a new home within music where I felt comfortable; finding new artists that I enjoyed listening to. At the same time, <i>fishmonger</i> was there for me in a time where I had to figure myself out and decide where to take my life. It's a journey that's still ongoing, but I've made great progress, and the stability and comfort I got through music like this helped immensely.",
],
favouriteTracks: [
"Kinkos field trip 2006",
"Dry land 2001",
]
},
{
title: "",
artist: "",
releaseYear: 202,
artwork: "",
description: [
"",
],
favouriteTracks: [
"",
]
},
{
title: "The Univa Trilogy",
artist: "TURQUOISEDEATH",
releaseYear: 2025,
artwork: "",
description: [
"The only regret I have with this album is that I didn't start listening to it sooner.",
"I bought <i>The Univa Trilogy</i> on a whim during my abroad semester in England because I liked a few select songs, but due to its length of almost 3 hours, I didn't listen to it until a long train trip three months after my abroad semester ended. On my train trip back, I listened to it again.",
"This album takes you into a world you can just lose yourself in. The long, dreamy tracks create this amazing immersive atmosphere. If you have the time for it, and if you're into drum & bass, I absolutely recommend giving it a try.",
],
favouriteTracks: [
"The Sky Fell",
"Guessabelle",
"Guardian Surface",
]
},
{
title: "Ghostholding",
artist: "venturing",
releaseYear: 202,
artwork: "venturing/ghostholding",
description: [
"",
],
favouriteTracks: [
"Dead forever",
"Sister",
]
},
];

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
</script>
<svelte:head>
<title>Now | denizk0461</title>
</svelte:head>
<Content bannerContent={{
title: "Now",
dateUpdated: "2026-04-14",
}}>
<p>content coming soon.</p>
<img src="/common/gadgetron.webp" alt="The Gadgetron vendor from Ratchet & Clank on PlayStation 2 looking bored.">
</Content>

View File

@@ -1,21 +0,0 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import { entries } from "./updates";
import ChangelogEntry from "$lib/components/update-entry.svelte";
</script>
<svelte:head>
<title>Website Updates | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="Website Updates" />
<div>
{#each entries as entry}
<ChangelogEntry {entry} />
{/each}
</div>
</Content>

View File

@@ -1,86 +0,0 @@
import { type UpdateEntry } from "$lib/components/update-entry.svelte";
export const entries: UpdateEntry[] = [
{
date: "2026-03-26",
time: "20:50",
content: "<b>Important</b>: my website is changing domains.",
link: "/blog/2026/0326",
},
{
date: "2026-03-25",
time: "22:22",
content: "I made a LIGHTYEARS font!",
link: "/blog/2026/0325",
},
{
date: "2026-03-17",
time: "17:10",
content: "a bit uncharacteristic, but I wrote a guide on setting up a SvelteKit app + backend because I found NOTHING of the sort online.",
link: "/blog/2026/0317",
},
{
date: "2026-03-11",
time: "19:21",
content: "new page: my drawings!",
link: "/art/drawings",
},
{
date: "2026-03-07",
time: "14:12",
content: "I fancied up some element animations, especially on the devlog and blog pages! The main page also got some love.",
},
{
date: "2026-03-05",
time: "23:59",
content: "My drawing challenge is complete I learned to draw!",
link: "/blog/2026/0205",
},
{
date: "2026-02-26",
time: "20:21",
content: "There's now a new Small Projects page where I gradually add some of my smaller projects.",
link: "/projects/small",
},
{
date: "2026-02-25",
time: "19:33",
content: "Added cool status markers to the projects page that show whether a project is currently active.",
link: "/projects",
},
{
date: "2026-02-21",
time: "16:25",
content: "My website is now also part of the no AI webring!",
link: "https://baccyflap.com/noai",
},
{
date: "2026-02-16",
time: "18:44",
content: "My website is now part of the bucket webring!",
link: "https://webring.bucketfish.me/",
},
{
date: "2026-02-06",
time: "18:47",
content: "Started a 28-day drawing challenge for myself! Updating the blog post every day with my new drawings.",
link: "/blog/2026/0205",
},
{
date: "2026-02-03",
time: "22:46",
content: "Created a new gallery widget for the main page and added a link to the new Gitea instance.",
},
{
date: "2026-02-03",
time: "15:48",
content: "Now running my own Gitea instance! It now also hosts my website repository.",
link: "https://code.natconf.dev/denizk0461/pages",
},
{
date: "2026-02-02",
time: "19:30",
content: "Updated some texts and moved my contact info to the about page. Also created this changelog!",
link: "/meta/about",
},
];

5
src/routes/now/+page.ts Normal file
View File

@@ -0,0 +1,5 @@
import { redirect } from "@sveltejs/kit";
export function load() {
redirect(308, '/meta/now');
}

View File

@@ -7,13 +7,12 @@
<title>Privacy & Cookies | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="Information on Privacy & Cookies" />
<Content bannerContent={{
title: "Information on Privacy & Cookies",
dateUpdated: "2025-09-10",
}}>
<p>This page uses <b>no cookies</b> as of now. No data will be stored on your device while browsing this website. <b>No trackers</b> are used either <b>no analytics</b>, not even a visit counter of any kind. Not by a third-party, and currently, none I built myself either.</p>
<p>The Godot and Unity projects on the <code>apps.natconf.dev</code> subdomain <i>may</i> cache compiled shaders on your device, I'm not sure. These files would only be used by your GPU to render visual effects for the game they were compiled for.</p>
<p>Last updated: 2025-09-10</p>
</Content>

View File

@@ -1,166 +1,197 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import TableOfContents from "$lib/components/table-of-contents.svelte";
import { type Project, games, hardware, apps, music, getStatusText, getStatusCode } from './projects';
import LinkList from "$lib/lists/link-list.svelte";
import { projects, type Project, ProjectCategory } from "./projects2";
import Content from "$lib/viewport/content.svelte";
import GalleryRow, { type GalleryRowEntry } from "$lib/lists/gallery-row.svelte";
const subpages: GalleryRowEntry[] = [
{
title: "Small Projects",
description: "Showing off the projects that don't get the spotlight",
img: "small/crate.webp",
altText: "A cardboard box filled with electronic components, tools, and screws. They are arranged in 3D printed Gridfinity containers.",
link: "small",
},
{
title: "Discography",
description: "Small stories about my past music",
img: "/main/hypertext.webp",
altText: "",
link: "/art/music",
},
];
let filter = $state(ProjectCategory.NULL);
function setFilter(tag: ProjectCategory) {
filter = tag;
}
function getBannerSrc(p: Project): string {
if (p.banner.startsWith("/")) {
return p.banner;
} else {
return `${p.category}/${p.id}/${p.banner}`;
}
}
function filterProjects(): Project[] {
if (filter == ProjectCategory.NULL) {
return projects;
}
let a: Project[] = projects.filter((project) => project.category === filter)
return a;
}
</script>
<svelte:head>
<title>Projects | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="My Disordered Projects"
banner="/projects/banner.webp"
bannerAlt="An upside-down New 3DS XL lying open on a desk with a small USB-C breakout board attached to it, and a USB-C cable plugged in. The 3DS is glowing to indicate that it is charging."
subtitle="Things I have worked on" />
<Content bannerContent={{
title: "My Disordered Projects",
banner: "banner.webp",
bannerAlt: "An upside-down New 3DS XL lying open on a desk with a small USB-C breakout board attached to it, and a USB-C cable plugged in. The 3DS is glowing to indicate that it is charging.",
subtitle: "things I have worked on",
}}>
<p>Welcome to my projects page! Here I show off all the things I have done. Projects are ordered by general topic, sorted reverse-chronologically, and have a status marker assigned that shows whether they are active or not. have fun browsing~!</p>
<p>Welcome to my 💫new💫 projects page! Here I show off all the things I have done. Projects are ordered reverse-chronologically and have some other neat information displayed. have fun browsing~!</p>
<p>The projects page also has two sister pages that go into detail about specific subgroups of projects:</p>
<div class="tag-filters">
<p class="tag-filter-header"># filter projects by category:</p>
<div class="tag-filter-container">
{#each Object.values(ProjectCategory) as tag}
<button class="post-tag tag-filter {tag == filter ? "tag-filter-selected" : ""}" onclick={() => { setFilter(tag) }}>{tag}</button>
{/each}
</div>
</div>
<GalleryRow entries={subpages} />
<TableOfContents />
<h2 id="games">Games</h2>
{#each games as project}
{@render projectSummary({ project: project })}
{/each}
<h2 id="hardware">Hardware</h2>
{#each hardware as project}
{@render projectSummary({ project: project })}
{/each}
<h2 id="apps">Apps</h2>
{#each apps as project}
{@render projectSummary({ project: project })}
{/each}
<h2 id="music">Music</h2>
{#each music as project}
{@render projectSummary({ project: project })}
{/each}
<div class="project-container">
{#each filterProjects() as p}
{@render project(p)}
{/each}
</div>
</Content>
{#snippet projectSummary({
project
}: {
project: Project;
})}
<h3 id="{project.id}">{project.title}</h3>
{#if project.subtitle}
<p class="project-subtitle">[ {project.subtitle} ]</p>
{#snippet project(p: Project)}
{#if p.directLink}
<a class="project-wrapper" href={p.directLink}>
{@render projectContent(p)}
</a>
{:else}
<a class="project-wrapper" href="{p.category}/{p.id}">
{@render projectContent(p)}
</a>
{/if}
{#if project.banner}
<div class="project-banner-container">
<img class="project-banner" src="{project.banner}" alt="Overview banner for {project.title}">
{/snippet}
{#snippet projectContent(p: Project)}
<div class="project-content-container">
<img class="project-banner" src={getBannerSrc(p)} alt={p.bannerAlt}>
<div class="project-text-container">
<div class="project-text-left">
<p class="project-title">
{#if p.isHighlight}
&#10047;
{/if}
{p.title}
</p>
<p class="project-subtitle">{p.subtitle}</p>
<p class="project-date">{p.date}</p>
</div>
<div class="project-text-right">
<p class="project-status">{p.status}</p>
<p class="project-category">[{p.category}]</p>
</div>
</div>
{/if}
<p class="project-info project-status-c-{getStatusCode(project)}">
{#if project.date}
{project.date} |
{/if}
{getStatusText(project)}
</p>
{#if project.icon}
<img class="project-icon" src="{project.icon}" alt="Icon for {project.title}">
{/if}
{#each project.paragraphs as paragraph}
<p>{@html paragraph}</p>
{/each}
<LinkList entries={project.links} />
</div>
{/snippet}
<style>
.project-subtitle {
font-family: var(--font-mono);
font-size: 1.0rem;
margin-top: 0;
.project-container {
display: flex;
gap: 4px;
margin: 0 auto;
flex-direction: column;
justify-content: stretch;
}
.project-banner-container {
position: relative;
width: 100%;
.project-wrapper {
display: flex;
flex-direction: column;
margin: 0;
/* gap: 4px; */
overflow: hidden;
text-decoration: none;
transition: border-radius var(--duration-animation) var(--anim-curve);
}
.project-content-container {
position: relative;
}
.project-banner {
margin: 0; /* reset left/right margins */
width: 100%;
object-fit: cover;
max-height: 300px;
}
.project-icon {
float: left;
margin: 16px 16px 16px 0;
width: 19%;
}
.project-date-embed {
position: absolute;
left: 0;
bottom: 0;
top: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: brightness(40%) saturate(40%);
transition: filter var(--duration-animation) var(--anim-curve),
scale var(--duration-animation) var(--anim-curve);
scale: 1.05;
}
.project-info {
width: fit-content;
.project-wrapper:hover .project-banner {
filter: brightness(60%) saturate(100%);
scale: 1.0;
}
.project-wrapper:hover {
border-radius: var(--border-radius);
}
.project-text-container {
padding: 16px;
display: flex;
flex-direction: row;
margin-top: 16px;
background-color: color-mix(in srgb, var(--color-status) 6%, transparent);
border: var(--border-style) var(--border-dash-size) var(--color-status);
padding: 2px 8px;
backdrop-filter: blur(var(--blur-radius-background));
justify-content: space-between;
align-items: end;
}
.project-text-container p {
text-shadow: 0 0 6px black, 0 0 9px black;
margin: 0;
}
.project-title {
font-family: var(--font-mono);
font-size: 1.0rem;
font-size: 1.5rem;
line-height: 1.8rem;
font-weight: 700;
color: var(--color-status);
}
/* #region Project Status Colours */
.project-status-c-act {
--color-status: var(--color-highlight);
/* .project-subtitle {
} */
.project-date {
font-style: italic;
font-weight: 600;
}
.project-status-c-ina {
--color-status: #B89751;
.project-text-right {
display: flex;
flex-direction: column;
align-items: end;
}
.project-status-c-aba {
--color-status: #D15555;
.project-status {
font-style: italic;
text-align: end;
}
.project-status-c-fin {
--color-status: #5486D8;
.project-category, .project-status {
font-family: var(--font-mono);
font-weight: 600;
font-size: 1.0rem;
}
.project-status-c-eol {
--color-status: #C353C1;
@media screen and (max-width: 800px) {
.project-title {
font-size: 1.3rem;
line-height: 1.5rem;
}
.project-subtitle, .project-date, .project-category {
font-size: 1.0rem;
line-height: 1.4rem;
}
}
/* #endregion */
</style>

View File

@@ -0,0 +1,59 @@
<script>
import ContentSidebar from "$lib/viewport/content-sidebar.svelte";
export let data;
</script>
<svelte:head>
<title>{data.projectDetails.title} | denizk0461</title>
<meta name="description" content="{data.projectDetails.description}">
<meta name="DCTERMS.created" content="{data.projectDetails.date}T12:00">
</svelte:head>
<ContentSidebar bannerContent={{
title: data.projectDetails.title,
subtitle: data.projectDetails.subtitle,
dateIndeterminate: data.projectDetails.date,
dateUpdated: data.projectDetails.dateUpdated,
banner: data.projectDetails.banner,
bannerAlt: data.projectDetails.bannerAlt,
}}>
{#if data.projectDetails.links.length > 0}
<div class="link-button-container">
{#each data.projectDetails.links as l}
<a class="link-button blurred-background" href={l.link}>{@html l.text}</a>
{/each}
</div>
{/if}
<svelte:component this={data.content} />
</ContentSidebar>
<style>
.link-button-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.link-button {
color: var(--color-text);
padding: 4px 12px;
margin: 0;
font-weight: 600;
border-top: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
border-bottom: var(--border-style) var(--border-dash-size) var(--color-highlight-alt);
border-radius: var(--border-radius);
transition: background-color var(--duration-animation) var(--anim-curve);
}
.link-button:hover {
text-decoration: none;
background-color: var(--color-background-highlight-alt);
}
</style>

View File

@@ -0,0 +1,13 @@
import { projects, type Project } from '../../projects2';
export async function load({ params }) {
const post = await import(`../../${params.category}/${params.id}.md`);
const projectDetails: Project = projects.find((p: Project) => p.category.toString() == params.category.toString() && p.id == params.id)!;
const content = post.default;
return {
content,
projectDetails,
};
}

View File

@@ -0,0 +1,129 @@
<script lang="ts">
import ImageGallery from "$lib/lists/image-gallery.svelte";
import ReactionQuote from "$lib/components/reaction-quote.svelte";
</script>
**AvH-Vertretungsplan** was an app I developed back in 2019 to view the substitution plan of my school more easily, since I disliked the disorganised design of the school's website. The substitution app allowed me to be more quickly informed about my courses, as it notified me about any cancellations and changes in my schedule. It also allowed me to view the canteen offers, even though I'd never eaten at the school's canteen.
The app was initially developed in Java and later converted into Kotlin as part of my Kotlin learning effort. The backend of the app which I needed as the school website required a login to view the substitution plan ran on a Raspberry Pi 3B using a Python script that scraped the website using Selenium and sent out notifications via Firebase. The Raspberry Pi crashed often and the script was not perfect, requiring me to frequently remote into the Pi using VNC, often while I was at school, to edit the script!
<ReactionQuote
reaction="joy"
text="one of my favourite memories regarding this app is the time I first presented the app to my classmates. I had someone from my class help me in presenting; he did the intro, hyping up the crowd, and I followed up by showing and explaining the app. I'm still very grateful he decided to help me with this! Thanks, Leon!"
/>
Since the app was written for native Android but a few of my classmates had iPhones, I had an iOS version in the works, which was developed entirely on a MacOS VM running on my laptop and debugged using an iPhone simulator in Xcode. The app achieved feature parity with the Android version at one point, but since I lacked funding to pay the $99-a-year fee Apple charges developers, it was never published and thus abandoned.
This app was genuinely one of my most fun projects, not only serving me as a handy tool, but also being a great playground for my programming practice, since it taught me to build UIs, use databases, and more. The app had been downloaded by 250 other students at my school, which I was and still am very proud of.
<ImageGallery
images={[
{
src: "html_plan.webp",
alt: "A website showing a table of substitution plan entries. The website is not styled at all, showing only black text on a white background. The page is running on the domain djd4rkn355.github.io, which is my old website.",
desc: [
"In order to start development on the app, I had to find a way to get the substitution plan info from my school's website into a format my app could understand. As the plan was behind a login, I couldn't straight-up scrape the website from mobile, so I used a Raspberry Pi using a webdriver to log in with my account and then scrape the info, then put it into a HTML document that was uploaded online on the predecessor to this very website! This did mean that this information was now publicly accessible, but that didn't become a concern until later.",
"This did mean that I was essentially scraping <i>twice</i> once on the Pi, and once in the app, but it worked so I wasn't concerned back then. Looking back, I should REALLY have used JSON instead.",
],
},
{
src: "app_0.webp",
alt: "A table of substitution plan entries in an Android app with a pink header. The entries are listed in rows.",
desc: [
"Initially, I was unsure whether I could create the kind of app I had in mind, but once my app successfully fetched information online and displayed it for the first time, I knew I could do it.",
],
},
{
src: "app_1.webp",
alt: "The substitution plan app with a card-based design, which visually separates entries.",
desc: [
"I quickly moved to a card-based design, which I stuck with for the entire lifetime of the app.",
],
},
{
src: "plan.webp",
alt: "The substitution plan app on the 'personal' tab greeting the user and listing three entries for courses with information on them.",
desc: [
"This is what the app eventually looked like. I added user-configurable colours (because we could never reach a consensus on which colours the courses should be assigned).",
"The bottom row shows the screens the app later gained, from left to right: the full substitution plan, the personalised plan (where only your own courses are displayed), the information box for supplementary notes, the canteen plan, and settings.",
"I also added an optional greeting at the top of the screen because I thought it would be nice.",
],
},
{
src: "darkmode.webp",
alt: "The substitution plan app in dark mode.",
desc: [
"Of course, a dark mode existed too.",
],
},
{
src: "notifs.webp",
alt: "A screenshot of the notification centre of a Motorola moto g with two notifications from the substitution plan app. One is listing course cancellations, while the other is notifying of a successful push to GitHub.",
desc: [
"The app sent out notifications any time there was information on your own courses. This was accomplished by sending a 'new info available' message to the app, which would then fetch the plan and check whether the user's courses were affected. I used Google's Firebase for this.",
"This, <b>THIS</b> is the sole reason I made this app. I wanted an automatic way of knowing whether my courses were cancelled. I didn't want to manually check the substitution plan every day, especially since the website wasn't mobile-friendly and thus hard to visually parse on a small screen.",
"I, of course, also used the opportunity to implement a dev notification which only I received that notified me if the Raspberry Pi detected changes in the substitution plan and had uploaded them. Sometimes, I would get multiple within a few minutes. When this happened, I knew something was wrong with my script and I had to fix it.",
],
},
{
src: "email.webp",
alt: "An email notifying of a server issue.",
desc: [
"If something REALLY went wrong, the Pi would send an email to me. If this happened, I knew I had to fix the script right away. I did this by logging into the Pi via VNC and fixing the script in an IDE running on the Pi. No, I didn't know what SSH was at the time.",
"Sometimes, I had to fix the script in the middle of my classes, which teachers never minded. I think it's because people didn't bring their laptops/tablets to school back then usually, so they didn't think I was browsing the internet to distract myself or something.",
"Taking care of the server gave me a limited sense of responsibility, which I liked. People depended on me, on my app, for the important but ultimately not critical task of knowing whether they could have a lazy morning.",
],
},
{
src: "diagnostics.webp",
alt: "A diagnostics screen within the substitution plan app listing device and app data as well as the launch and notification count.",
desc: [
"I put in a little diagnostics menu hidden behind a dev code. This proved not only useful, but also very fun.",
],
},
{
src: "intro.webp",
alt: "A setup screen welcoming the user and asking for details on their courses.",
desc: [
"Later on, once more people (and especially ones I didn't even know) started using the app, I built this welcome screen to get the app set up and guide users in inputting their course data. It wasn't extremely intuitive but it was efficient.",
],
},
{
src: "loggedin.webp",
alt: "An unlocked lock with the text 'You have been logged in!' displayed underneath.",
desc: [
"Some months after publishing the app, my computer science teacher approached me and told me the school head caught wind of my app (woah!!) and asked whether I could put in a login screen, as the data should only be accessible to students.",
"I eventually found a way of doing this! The app opened a WebView with the school website's login screen, waited for the user to log in, and then looked for a cookie that notified the website that the user was logged in. The data within the cookie didn't matter as long as it was present, the app could know the user had a valid account. The check was only performed once when setting up the app.",
"By virtue of hosting the data on my publicly-accessible website, the data was still available online without login... but no one minded.",
],
},
{
src: "ios_plan.webp",
alt: "The substitution plan app running on an iOS simulator. It looks almost exactly like the Android version.",
desc: [
"The app also eventually existed on iOS! I developed this exclusively on a MacOS VM and an iOS simulator, as I had no Apple devices available to me. Surprisingly, I managed to recreate my app fully for iOS, but I only ever saw it running on a real device once or twice, as I couldn't get it published on the App Store, largely for financial reasons.",
],
},
{
src: "ios_config.webp",
alt: "The settings screen on the iOS version of the app. It is displayed on an iOS-native modal.",
desc: [
"I made sure to design a UI that felt iOS-native.",
],
},
{
src: "macvm.webp",
alt: "A preferences window on a MacOS VM displaying information on Xcode. It lists 'Mojave 10.14.3 by Techsviewer' as its location.",
desc: [
"I have no idea how I managed to set up this VM. It ran quite poorly on my Surface Laptop, but it worked, and I was determined enough to power through.",
],
},
{
src: "psa.webp",
alt: "The substitution plan app displaying an end-of-year message.",
desc: [
"I also used my admin powers to send messages to users, like this one: 'Happy holidays and a good 2020!'",
],
},
]}
/>

View File

@@ -0,0 +1,5 @@
**Qwark** was an app I used to track relevant information for my time at gymnasium. Initially, it was only meant to track my grades in exams and whatnot, but its capabilities grew to calculate my report card grades and my final grade once I graduated. It was also able to keep simple notes which could be checked off to mark them as done, and it also had a built-in counter I used during lessons to track how often I raised my hand and how often I verbally contributed to classes! A discipline I severely struggled in.
The app was named after Captain Qwark, the clumsy hero figure from the Ratchet & Clank series who never really gets his affairs in order. I suppose this referenced me personally more so than the app, since I was the one who needed help with tracking relevant information a task for which the app proved very helpful!
The app reached EOL when I graduated from gymnasium, since I had no use for it anymore, and I never made it available to anyone else either.

View File

@@ -0,0 +1,3 @@
In the pursuit of finding a simple app on the Play Store that would allow me to put any kind of text on my home screen, I failed, and all I could find were ad-ridden monstrosities, overfilled with features. To fulfil my own requirements, I developed **Text Basic**, which does exactly one thing: put a text on your home screen. Upon user request, the app has grown minimally since, now allowing for text customisation (text font, text size, background style, colours) and a rotation of texts to display.
Surprisingly, the app garnered quite a signficant user base of >2.000 people, many of whom were sending me bug reports, and some even contributing ideas. Many were also frustrated that the text widgets changed... not understanding how the app works. It was taken down from the Play Store at the same time as [WeserPlaner](/projects/apps/weserplaner).

View File

@@ -0,0 +1,5 @@
**WeserPlaner** is an app I developed to more easily view relevant information during my studies at the University of Bremen. It can download the user's timetable from the university's learning platform Stud.IP, and it can download the menus for all canteens managed by the Studierendenwerk Bremen, allowing the user to filter for dietary preferences as well as hiding items containing substances they are allergic against.
In developing this app, I took heavy inspiration from an earlier project of mine, [AvH-Vertretungsplan](/projects/apps/avhplan), which was an app I developed for a school I used to attend, in order to view the substitution plan as well as the canteen offers more easily. Quite similar!
I stopped work on the app in favour of other projects, notably because I had no significant plans for it. The app was taken down from the Google Play Store after I refused to comply with their new developer guidelines, requiring me to publish my home address (what the actual fuck Google?), and after the Studierendenwerk Bremen updated their page around the start of 2025, the app became nonfunctional.

View File

@@ -1,118 +0,0 @@
<script lang="ts">
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import TableOfContents from "$lib/components/table-of-contents.svelte";
</script>
<svelte:head>
<title>Daisy FM Synth | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="Daisy FM Synth"
date="2025-04-04"
banner="/projects/daisyfm/banner.webp"
bannerAlt="Close-up of Daisy, focussed on the effect knobs" />
<img src="/projects/daisyfm/fullview.webp" alt="Top view of the Daisy FM synth">
<p>A friend showed me the <a href="https://electro-smith.com/products/daisy-seed">Daisy Seed</a>, an Arduino-compatible microcontroller made for developing audio equipment. With a little bit of motivation and absolutely no experience in either programming synthesisers or electronics in general, I quickly got my hands on one and started to toy around.</p>
<p>So... <i>how did we get here?</i></p>
<TableOfContents />
<h2 id="creation">How did I make this?</h2>
<h3 id="components">Components</h3>
<p><a href="https://www.reichelt.de/my/2171501">Here's a list of the components</a> I used in this project.</p>
<ul>
<li>21 Cherry MX Low Profile Red switches, of which 15 are used for the playing keys, and 6 are used for the wavetable select (sine, square, saw) 3 for the carrier signal, and another 3 for the modulator signal.</li>
<li>11 rotating potentiometers RK11K113-LIN10K by Alps are used to adjust the volume ADSR curve as well as the effects.</li>
<li>Two CD 4051BE multiplexers are used to connect further inputs, since the analogue inputs on the Daisy did not suffice. One of them connects the majority of the rotating potentiometers to Daisy, the other connects two potentiometers and the 6 wavetable keys. Takumi Ogata has a great guide on how to use these things with Daisy <a href="https://forum.electro-smith.com/t/cd4051-multiplexer-tutorial-is-here/3481">on the Daisy forum</a>!</li>
<li>A toggle switch is used to toggle the flanger on and off.</li>
<li>A 75mm sliding potentiometer is used to adjust the master volume.</li>
<li>An audio jack breakout board provides means to connect the synth to an audio output. This can theoretically be replaced by any kind of analogue connector; this breakout board by Soldered just proved to be the most convenient for both soldering and mounting to the case.</li>
<li>A USB-C breakout board, also by Soldered, is used for power and data transfer (flashing the synth). This isn't necessary, though since Daisy only provides a microUSB connector, I felt this was sorely needed.</li>
<li>Two 5.1 kΩ, 0.25 W resistors are used as pulldown resistors to enable C-to-C functionality on the Daisy, making it compatible to the USB-C standard. Again, not necessary, though quite recommended if you're using a USB-C port.</li>
</ul>
<p>I also bought <a href="https://www.amazon.de/dp/B07WTMGX6C">this 5-pack of 45mm sliding potentiometers</a> from Amazon. They're not of the highest quality and they don't have a covering protecting the resistance strips on the inside from debris, but they do snap to 50%, which I didn't know when I bought them, but turns out to be super convenient when used as a pitch slider!</p>
<h3 id="pcb-assembly">PCB & Assembly</h3>
<h4 id="kicad">KiCad</h4>
<p>I designed the PCB in <a href="https://www.kicad.org/">KiCad</a>. I had no prior experience in designing PCBs, but designing one merely for interactable components was simple, as I didn't have to worry quite so much about electronic interference and similar issues commonly found in original PCB design utilising ICs and other complex components. I don't know much about this... but all of this is to say, learning KiCad for this project wasn't too difficult.</p>
<p>I split the PCB into four layers, separating power, ground, digital signals (switches and toggle), and analogue signals (potentiometers, audio out). I made some exceptions, such as for the waveform buttons, since they cross the playing key traces.</p>
<div class="horizontally-centre-aligned">
<img src="/projects/daisyfm/pcb-sketch.webp" alt="Screenshot of KiCad schematic">
<img src="/projects/daisyfm/pcb-empty.webp" alt="The finished PCB produced from the KiCad schematic">
</div>
<p>The PCB was manufactured by <a href="https://jlcpcb.com/">JLCPCB</a>.</p>
<h4 id="usbc">USB-C</h4>
<p>If using a USB-C breakout board, wire in two 5.1 kΩ pulldown resistors by soldering a resistor between the pin CC1 and ground. Do the same for pin CC2 with a second resistor.</p>
<p>Alternatively, if your USB-C breakout board does not have CC pins exposed, there's a chance you could still connect pulldown resistors to make it compatible. On <a href="https://www.amazon.de/dp/B09FPZDDD9">this breakout board</a> I purchased on Amazon, I discovered that the third pin from the right exposes a CC connection. Wiring a resistor between this pin and ground enabled C-to-C functionality. If you want to test the functionality before soldering, you can hold a resistor between this pin and either the ground connector on the board or the USB port's outer shell, since that one's grounded as well.</p>
<div class="horizontally-centre-aligned">
<img src="/projects/daisyfm/usbc-breakout-small.webp" alt="Close-up of the USB-C breakout board with the CC pin marked">
<img src="/projects/daisyfm/hand.webp" alt="The tiny USB-C breakout board compared to my hand">
</div>
<p>Do keep in mind that this type of connector does not have mounting holes and requires either a more sophisticated mounting mechanism in the chassis, hot glue, or both. Also, soldering to this pin is insanely finicky, since it is VERY small, so I strongly recommend a breakout board such as <a href="https://www.reichelt.de/entwicklerboards-usb-typ-c-adapterboard-buchse-debo-usb-c-f-p376522.html">this one by Soldered</a> that exposes the CC pins.</p>
<p>Actually, I later found out that this breakout board does expose CC solder pads on the back, where an SMD resistor such as <a href="https://www.reichelt.de/smd-widerstand-0402-5-1-kohm-63-mw-1--rnd-0402-1-5-1k-p182941.html">this one</a> can be placed. Since this is a 1mm long SMD component, however, and since I don't have a rework station or anything of the sort, it's safe to say I have not tried this yet.</p>
<p>Now, how do you connect the breakout to Daisy? Daisy does expose USB pins, but they require a little bit of setup. There's another way though, one that's simpler, if perhaps stupid: look for an old microUSB cable (one that carries data) and chop it up, then solder the wires of the end with the microUSB connector to the breakout board. The microUSB end can then be plugged straight into Daisy. By convention, the wiring is as follows:</p>
<ul>
<li>the red wire is power and goes to V or VUSB</li>
<li>the black wire is ground and goes to G or GND</li>
<li>the green wire is data+ and goes to D+</li>
<li>the white wire is data- and goes to D-</li>
</ul>
<p>I used an angled microUSB cable for my chassis.</p>
<h3 id="3d-print">Case & 3D Printing</h3>
<p>All components were printed using a <a href="https://eu.store.bambulab.com/collections/3d-printer/products/a1-mini?variant=49311552176476">Bambu Lab A1 mini printer</a> as well as Bambu Lab PLA Metal in the colours <a href="https://eu.store.bambulab.com/products/pla-metal?variant=46797850902876">Iron Gray</a> and <a href="https://eu.store.bambulab.com/products/pla-metal?variant=46797851099484">Iridium Gold</a>. These materials do not contain metal particles and are very easy to print.</p>
<p>The components were designed using <a href="https://www.autodesk.com/eu/products/fusion-360/overview">Autodesk Fusion</a>. Honestly, this program annoys me to no end sometimes, but it does prove to be quite useful. You can <a href="https://www.autodesk.com/education/edu-software/overview">get it for free</a> if you're a student, educator, or school IT admin, but they really make it difficult to find this page, I'm telling ya.</p>
<h3 id="programming">Programming</h3>
<p>The synth runs on a script written in the Arduino IDE using the <a href="https://electro-smith.github.io/DaisySP/index.html">DaisySP library</a>. Documentation was sparse... but it worked out. Finding the website wasn't actually all too easy, I found, though it proved to be my best resource.</p>
<p>The synth essentially works by holding an array of 15 carrier oscillators and another 15 modulator oscillators. Each oscillator only plays when the volume is greater than 0, meaning that nothing is processed until a key is pressed. However, the load of processing 30 oscillators plus effects, if they are active, when pressing all the keys is too much and will cause Daisy to freeze and crash. This is not an issue during regular use, though.</p>
<p>Find the script on <a href="https://codeberg.org/denizk0461/daisy-fm-synth">my Codeberg page</a>!</p>
<h2 id="features">What can it do?</h2>
<h3 id="osc">Oscillators</h3>
<p>This synth offers 3 waveforms that can be played via the carrier oscillator: sine, square, saw. This oscillator can be modified using frequency modulation by using the modulator oscillator, which also offers sine, square, and saw waveforms. The modulator's pitch deviation from the carrier oscillator can be set, as well as its wet/dry mix.</p>
<h3 id="fx">Effects</h3>
<p>The synth has a flanger effect that can be enabled via a switch and tweaked using frequency, depth, and delay parameters. Distortion and sample reduction are also available.</p>
<h2 id="improvements">What could I have done better?</h2>
<p>There are a few things I would add or do differently, if I was to create a second one of these:</p>
<p>The case has too few screw standoffs, which would result in the PCB flexing when buttons were pushed. I remedied this by gluing in some standoffs that lack screw holes, but it would of course be more ideal to model these into the actual case body. On that note, I would also add more screw holes in the PCB wherever possible, as the PCB didn't feature many holes, and the ones it did have were quite concentrated on the left side of the board.</p>
<p>Some visual feedback would also be nice; a power LED could be nice. A display could have been even better, perhaps a small OLED display like <a href="https://www.reichelt.de/entwicklerboards-display-oled-0-96-128x64-pixel-blau-debo-oled5-0-96-p384685.html">this one</a>. It could have given feedback on individual parameters, such as a percentage on effects when the user turns a knob.</p>
</Content>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import SubtitledImage from "$lib/components/subtitled-image.svelte";
import ImageGallery from "$lib/lists/image-gallery.svelte";
</script>
<SubtitledImage
image="showcase.mp4"
subtitle="it charges via USB-C! also do you like my A Hat in Time theme?"
video
/>
I modded my New 3DS XL (SNES Edition) to give it a USB-C port to charge!
<ImageGallery
images={[
{
src: "finished.webp",
alt: "A back view at a New Nintendo 3DS XL with a USB-C port added between the charging port and the right shoulder buttons.",
desc: ["the USB-C port in all its glory"],
},
{
src: "hole.webp",
alt: "At the top is a view at a USB-C-shaped hole cut into the back half of a New 3DS XL. At the bottom is a look at the same hole from the inside of the case. There are rough cutouts where the stylus slides in.",
desc: [
"a closer look at the holes I cut, and how they affect the stylus slot",
],
},
]}
/>
I used a small USB-C breakout I had lying around that is wired straight into the charging pads of the original charging port, which is left completely intact. The breakout board also has a 5.1kΩ resistor between ground and one of the CC pins (which I had to manually find because it's unlabelled) to allow for using C-to-C cables.
What I wrecked in turn was the wrist strap loop, which I completely cut out to create the hole for the port. The stylus port also got cut down to make space, but my stylus is kind of broken and doesn't stay put when I put it into the system, so I didn't really care about that.
It works well! The hole isn't the prettiest but it was pretty simple to pull off, and extremely cheap as well. In turn I got a 3DS that I can charge using any USB-C cable and I no longer need to lug around the proprietary 3DS charger!

View File

@@ -0,0 +1,116 @@
<script lang="ts">
import TableOfContents from "$lib/components/table-of-contents.svelte";
import ImageRow from "$lib/media/image-row.svelte";
</script>
![Top view of the Daisy FM synth](fullview.webp)
A friend showed me the [Daisy Seed](https://electro-smith.com/products/daisy-seed), an Arduino-compatible microcontroller made for developing audio equipment. With a little bit of motivation and absolutely no experience in either programming synthesisers or electronics in general, I quickly got my hands on one and started to toy around.
So... *how did we get here?*
<TableOfContents />
## How did I make this?
### Components
[Here's a list of the components](https://www.reichelt.de/my/2171501) I used in this project.
- 21 Cherry MX Low Profile Red switches, of which 15 are used for the playing keys, and 6 are used for the wavetable select (sine, square, saw) 3 for the carrier signal, and another 3 for the modulator signal.
- 11 rotating potentiometers RK11K113-LIN10K by Alps are used to adjust the volume ADSR curve as well as the effects.
- Two CD 4051BE multiplexers are used to connect further inputs, since the analogue inputs on the Daisy did not suffice. One of them connects the majority of the rotating potentiometers to Daisy, the other connects two potentiometers and the 6 wavetable keys. Takumi Ogata has a great guide on how to use these things with Daisy [on the Daisy forum](https://forum.electro-smith.com/t/cd4051-multiplexer-tutorial-is-here/3481)!
- A toggle switch is used to toggle the flanger on and off.
- A 75mm sliding potentiometer is used to adjust the master volume.
- An audio jack breakout board provides means to connect the synth to an audio output. This can theoretically be replaced by any kind of analogue connector; this breakout board by Soldered just proved to be the most convenient for both soldering and mounting to the case.
- A USB-C breakout board, also by Soldered, is used for power and data transfer (flashing the synth). This isn't necessary, though since Daisy only provides a microUSB connector, I felt this was sorely needed.
- Two 5.1 kΩ, 0.25 W resistors are used as pulldown resistors to enable C-to-C functionality on the Daisy, making it compatible to the USB-C standard. Again, not necessary, though quite recommended if you're using a USB-C port.
I also bought [this 5-pack of 45mm sliding potentiometers](https://www.amazon.de/dp/B07WTMGX6C) from Amazon. They're not of the highest quality and they don't have a covering protecting the resistance strips on the inside from debris, but they do snap to 50%, which I didn't know when I bought them, but turns out to be super convenient when used as a pitch slider!
### PCB & Assembly
#### KiCad
I designed the PCB in [KiCad](https://www.kicad.org/). I had no prior experience in designing PCBs, but designing one merely for interactable components was simple, as I didn't have to worry quite so much about electronic interference and similar issues commonly found in original PCB design utilising ICs and other complex components. I don't know much about this... but all of this is to say, learning KiCad for this project wasn't too difficult.
I split the PCB into four layers, separating power, ground, digital signals (switches and toggle), and analogue signals (potentiometers, audio out). I made some exceptions, such as for the waveform buttons, since they cross the playing key traces.
<ImageRow
images={[
{
src: "pcb-sketch.webp",
alt: "Screenshot of KiCad schematic",
},
{
src: "pcb-empty.webp",
alt: "The finished PCB produced from the KiCad schematic",
},
]}
/>
The PCB was manufactured by [JLCPCB](https://jlcpcb.com/).
#### USB-C
If using a USB-C breakout board, wire in two 5.1 kΩ pulldown resistors by soldering a resistor between the pin CC1 and ground. Do the same for pin CC2 with a second resistor.
Alternatively, if your USB-C breakout board does not have CC pins exposed, there's a chance you could still connect pulldown resistors to make it compatible. On [this breakout board](https://www.amazon.de/dp/B09FPZDDD9) I purchased on Amazon, I discovered that the third pin from the right exposes a CC connection. Wiring a resistor between this pin and ground enabled C-to-C functionality. If you want to test the functionality before soldering, you can hold a resistor between this pin and either the ground connector on the board or the USB port's outer shell, since that one's grounded as well.
<ImageRow
images={[
{
src: "usbc-breakout-small.webp",
alt: "Close-up of the USB-C breakout board with the CC pin marked",
},
{
src: "hand.webp",
alt: "The tiny USB-C breakout board compared to my hand",
},
]}
/>
Do keep in mind that this type of connector does not have mounting holes and requires either a more sophisticated mounting mechanism in the chassis, hot glue, or both. Also, soldering to this pin is insanely finicky, since it is VERY small, so I strongly recommend a breakout board such as [this one by Soldered](https://www.reichelt.de/entwicklerboards-usb-typ-c-adapterboard-buchse-debo-usb-c-f-p376522.html) that exposes the CC pins.
Actually, I later found out that this breakout board does expose CC solder pads on the back, where an SMD resistor such as [this one](https://www.reichelt.de/smd-widerstand-0402-5-1-kohm-63-mw-1--rnd-0402-1-5-1k-p182941.html) can be placed. Since this is a 1mm long SMD component, however, and since I don't have a rework station or anything of the sort, it's safe to say I have not tried this yet.
Now, how do you connect the breakout to Daisy? Daisy does expose USB pins, but they require a little bit of setup. There's another way though, one that's simpler, if perhaps stupid: look for an old microUSB cable (one that carries data) and chop it up, then solder the wires of the end with the microUSB connector to the breakout board. The microUSB end can then be plugged straight into Daisy. By convention, the wiring is as follows:
- the red wire is power and goes to V or VUSB
- the black wire is ground and goes to G or GND
- the green wire is data+ and goes to D+
- the white wire is data- and goes to D-
I used an angled microUSB cable for my chassis.
### Case & 3D Printing
All components were printed using a [Bambu Lab A1 mini printer](https://eu.store.bambulab.com/collections/3d-printer/products/a1-mini?variant=49311552176476) as well as Bambu Lab PLA Metal in the colours [Iron Gray](https://eu.store.bambulab.com/products/pla-metal?variant=46797850902876) and [Iridium Gold](https://eu.store.bambulab.com/products/pla-metal?variant=46797851099484). These materials do not contain metal particles and are very easy to print.
The components were designed using [Autodesk Fusion](https://www.autodesk.com/eu/products/fusion-360/overview). Honestly, this program annoys me to no end sometimes, but it does prove to be quite useful. You can [get it for free](https://www.autodesk.com/education/edu-software/overview) if you're a student, educator, or school IT admin, but they really make it difficult to find this page, I'm telling ya.
### Programming
The synth runs on a script written in the Arduino IDE using the [DaisySP library](https://electro-smith.github.io/DaisySP/index.html). Documentation was sparse... but it worked out. Finding the website wasn't actually all too easy, I found, though it proved to be my best resource.
The synth essentially works by holding an array of 15 carrier oscillators and another 15 modulator oscillators. Each oscillator only plays when the volume is greater than 0, meaning that nothing is processed until a key is pressed. However, the load of processing 30 oscillators plus effects, if they are active, when pressing all the keys is too much and will cause Daisy to freeze and crash. This is not an issue during regular use, though.
Find the script on [my Codeberg page](https://codeberg.org/denizk0461/daisy-fm-synth)!
## What can it do?
### Oscillators
This synth offers 3 waveforms that can be played via the carrier oscillator: sine, square, saw. This oscillator can be modified using frequency modulation by using the modulator oscillator, which also offers sine, square, and saw waveforms. The modulator's pitch deviation from the carrier oscillator can be set, as well as its wet/dry mix.
### Effects
The synth has a flanger effect that can be enabled via a switch and tweaked using frequency, depth, and delay parameters. Distortion and sample reduction are also available.
## What could I have done better?
There are a few things I would add or do differently, if I was to create a second one of these:
The case has too few screw standoffs, which would result in the PCB flexing when buttons were pushed. I remedied this by gluing in some standoffs that lack screw holes, but it would of course be more ideal to model these into the actual case body. On that note, I would also add more screw holes in the PCB wherever possible, as the PCB didn't feature many holes, and the ones it did have were quite concentrated on the left side of the board.
Some visual feedback would also be nice; a power LED could be nice. A display could have been even better, perhaps a small OLED display like [this one](https://www.reichelt.de/entwicklerboards-display-oled-0-96-128x64-pixel-blau-debo-oled5-0-96-p384685.html). It could have given feedback on individual parameters, such as a percentage on effects when the user turns a knob.

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import SubtitledImage from "$lib/components/subtitled-image.svelte";
import ImageGallery from "$lib/lists/image-gallery.svelte";
</script>
<SubtitledImage
image="finished.webp"
altText="A golden 3D printed shell with a slider on its left, two LEDs recessed, and four black buttons on its right. The buttons have symbols of speakers, monitors, and headphones printed on them. Three screws at the top are visible. A USB-C cable is plugged into the back of it."
subtitle="a handful of device for controlling a handful of other devices"
alignment="left"
smaller
/>
This little device was inspired by one a friend of mine built his own version of: a [deej](https://github.com/omriharel/deej) volume slider panel. This thing allows you to control different applications with individual, *physical*, sliders. Super cool thing.
Except I didn't need all these sliders, really. A single slider would be cool, I thought. You know what I really wanted? Buttons to control the audio *source*, because I switch between speakers and headphones constantly, and that's at least 3 clicks every time I want to switch. So I built a device based on deej, but with some expansions.
I only used few components: a HID-enabled Arduino-compatible Pro Micro with USB-C controls the whole thing. Hooked up to it are four Cherry switches and a Soldering slider I had lying around from my [Daisy project](/projects/daisy), and I added two LEDs for good measure. It's all packaged into a 3D-printed enclosure I designed myself. The slider is screwed in tightly, and so is the top of the case; the key switches are clipped in from the top so they don't fall out; the Arduino and the LEDs are just hot-glued in. For extra flair, the four output buttons are marked with symbols for my outputs: two monitors, a pair of loudspeakers, and a pair of headphones. In the final device, they're arranged so that my two most frequently-used buttons are at the bottom for easier reach.
Software-wise, I set this up with the original deej software to control main volume. For the audio, I used a program called [SoundSwitch](https://soundswitch.aaflalo.me/). The program listened to key presses for the `F21-F24` keys, which the Arduino triggers when the output keys are pressed. The red LED lights up when a key is pressed; the white LED has no assigned function. This worked pretty well, but this is no longer the setup I use, since I switched to Fedora Linux, as I needed to adapt/change the software for the new OS!
<ImageGallery
images={[
{
src: "prototype.webp",
alt: "An Arduino Pro Micro with a white LED attached to it. The LED is glowing.",
desc: [
"I initially tested whether I could get the Arduino to light up an LED. I wanted to include LEDs because I thought I could later integrate them in a cool way, but the device ended up being so simple, there wasn't really a need for much visual feedback.",
],
},
{
src: "soldering.webp",
alt: "An Arduino with keyboard switches, LEDs, and a slider wired into it. The components are connected with relatively long cables.",
desc: [
"It may look like a prototype, but these are the production-ready innards of the machine! Trying to put this into the 3D printed shell without breaking any of the frail soldering joints was a pain, especially as the key switches really weren't meant for soldering and thus barely hold on to the wires.",
],
},
{
src: "printing.webp",
alt: "A Bambu Lab A1 mini 3D printer in the middle of printing casing parts using a golden filament. The printer head has two googly eyes attached.",
desc: [
"googly-eyed printer hard at work.",
"I'm using Bambu Lab PLA Metal 'Iridium Gold Metallic (13400)' for the body here.",
],
},
{
src: "assembly.webp",
alt: "An Arduino set into a 3D printed case with a slider, two LEDs, and four key switches soldered to it using wires. The components are spread out and hanging out the top of the case.",
desc: [
"A visual guide to the (mental) pain I had to endure assembling this thing. Creating a simple PCB would have been MUCH better, but it would have meant designing one in KiCad, paying for at least 5 of them at a supplier, waiting for them to ship, etc. ...",
"Safe to say, I wasn't willing to do any of that. Quick and cheap was my preferred method here.",
],
},
]}
/>
When pressing a keyboard's volume button, Windows raises or lowers volume in increments of 2. Fedora does 5. I found this much handier, so I stopped reaching for the slider and just defaulted to using my keyboard. This meant I didn't bother setting up the slider in Fedora. The buttons work, though, but they needed some adjustment. I think (and I might be wrong??) that Linux doesn't support function keys past F12, so I changed the Arduino script so the buttons trigger `Shift + F9-F12`. Instead of a separate program (which kept asking to be updated...), I now use KDE's built-in Shortcuts that trigger a script. The script is one line: `pactl set-default-sink [sink-name]`. The sink name is hard-coded into the file because, as extensive testing proved, Shortcuts does not allow arguments when entering a command. I currently only have two files set up: one for the primary monitor, one for the headphones.
I much prefer the setup now because it doesn't rely on third-party software anymore.
This thing is, no exaggeration, one of the handiest things I have ever built, because I use it quite literally ***every single day***. I often switch between my monitor's speakers and my headphones, and being able to do that with the press of a single button is *unbelievably* handy. I don't even think about it anymore, I just reach for the buttons whenever I switch. It's a part of my routine now and I wouldn't want to miss it.

View File

@@ -0,0 +1,3 @@
**Magician** is a clone of the card game *Wizard*. I've made it primarily to play with friends, but it's also a test for programming client-server multiplayer games. The clients are programmed in Godot, the server in Python, and they communicate via TCP/IP websockets. The game works with 3 to 6 players.
As the corresponding server is not currently online, the game is unplayable, but you can view the game below.

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import SubtitledImage from "$lib/components/subtitled-image.svelte";
import Video from "$lib/video.svelte";
</script>
Two friends of mine and I worked together on a local multiplayer PvP game where you can throw magical artifacts at one another. It was developed using the Godot Engine.
The game, under the working title **Projektike**, featured artifacts such as fireballs, ice and tree generators, lightning attractors, black and white holes, an Interplanetary Death Ray (later raining rocks), poison clouds, wind pushers, and a couple more:
<Video
src="projects/games/projektike/powerups.mp4"
thumb="projects/games/projektike/powerups.webp"
remote
/>
Similarly to games such as Smash Bros., it also allowed for character customisation and profile creation so that individual players could keep their gained rewards (which were meant to be implemented in the form of in-game cosmetics) and show them off without needing to configure them on each game load. We created a small array of visually distinct maps too.
The project is currently on hold as of May 2025.
<SubtitledImage
image="death.webp"
altText="A wizard looking forward on a bright red background."
subtitle="We had this loading screen where the wizard character's head would spin while the level was being prepared. If the load ever failed though, the screen would turn bright red and the wizard would stop rotating, instead looking straight at the player. I forgot I implemented this and frightened the hell out of myself a few weeks later."
/>

View File

@@ -0,0 +1,5 @@
**Swords & Stuff** (working title) was an RPG inspired by Dragon Quest IX. Traverse the world with a party of up to 4 characters, each with their individual equiment, skills, and vocations, and fight monsters to gain experience!
This game was my first shot at 2D Unity development, which had proven quite fun, but also a little cumbersome at times. Unfortunately, I will not continue the project in its current form, since even though I don't expect to earn any money from my games, let alone publish them on a commercial platform, Unity's TOS changes in mid-2023 made me reconsider my decision to use the platform and I hence abandoned the project. I used this opportunity to learn to use Godot, which I will use for my future projects.
A working, but very early-in-development version of the game is still up on this website. It features a test overworld in which you can move the cast of four characters: Player1, fraxiom64, proudrat, and Grampa Simpson. The enemies, trolldads, chase you and take you into a battle scene that has a functioning queue, but no damage and health mechanics. You can flee battles. Don't open the inventory you cannot escape it.

View File

@@ -0,0 +1,3 @@
**Totally Accurate Dating Simulator** is, as the name suggests, a series of particularly realistic dating simulator experiences. Dive into a world of romance as you meet your matches, curious and disappointing. Who will you meet? Will it be someone your type? That's subjective, and the computer will not even slightly attempt to match you with someone suitable. Prepare yourself for matches that are rarely described with "intriguing" and much more frequently with "funny." Discover which of the 28 endings will determine your fate, and lower your expectations. Significantly.
One of these days, I'll make a TADS 3 with 3D characters and an actual matchmaking mechanic. One day...

View File

@@ -12,9 +12,9 @@ The font exclusively uses characters from the Noto font family. Many of the Noto
To create the font, I used [FontForge](https://fontforge.org). Finding this tool was both a blessing and a curse, as it was exactly what I needed, but it kept. crashing. all. the. time. I tried both the AppImage as well as the release on `dnf` and both had the same issues. I managed to make it work, but it took a lot of patience. Eventually I figured out that importing Noto Maths gave me a 3-8 second window before the editor crashed. The project file would forget the imported font, but if I had copied any glyphs it would keep those.
## Download & use
## Usage
[Download the font here](https://files.natconf.dev/public/lightyears.woff2). It's in the web-optimised `woff2` format and has most characters stripped to minimise its file size it's less than 20 kilobytes in size! Uppercase and lowercase letters are the same.
The font is in the web-optimised `woff2` format and has most characters stripped to minimise its file size it's less than 20 kilobytes in size! Uppercase and lowercase letters are the same.
For use on your website, put the font into your resources/static/similar folder and then add this block of code to your CSS file:

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import Player from "$lib/media/player.svelte";
</script>
**A New Beginning** is an EP I wrote back in 2018 in an effort to change up my production style. Originally, this EP was released as *A New Beginning (3-Track)*, featuring A New Beginning as *Nowy Początek* and *Farewell* in two different versions, one being an instrumental titled *Trzymajcie Się*, and the other one being a bootleg of Kelly Clarkson's *Behind These Hazel Eyes* called *Behind These Hazel Eyes (D4rkn355 'Farewell' Bootleg)*! For copyright reasons, the bootleg never made it onto streaming services.
This EP, to me, represents the start of a deviation in tone, production quality, and musical style from my previous works. While it was only a start, I am quite proud of the works I produced.
<Player
tracks={[
{
title: "A New Beginning",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/01%20F%C3%A6ls%20-%20A%20New%20Beginning.flac",
},
{
title: "Hope",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/02%20F%C3%A6ls%20-%20Hope.flac",
},
{
title: "Farewell",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/03%20F%C3%A6ls%20-%20Farewell.flac",
},
{
title: "Behind These Hazel Eyes (D4rkn355 'Farewell' Bootleg)",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/04%20Kelly%20Clarkson%20-%20Behind%20These%20Hazel%20Eyes%20%28D4rkn355%20%27Farewell%27%20Bootleg%29.flac",
},
]}
cover=""
customColor="#A26D5F"
useDarkHoverText="false"
/>

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import Player from "$lib/media/player.svelte";
</script>
**Dreamworld** is my first album. I always wanted to write a cohesive piece of media, and **Dreamworld** was my first stab at this task. Originally, I had planned to write an album in the month of July 2020, but that fell flat entirely, instead starting out with the production of *Monophobia* a track name inspired by deadmau5' track of the same title and working on the album until September of the year thereafter.
I had started the track *Dreamworld* back in 2019, only adapting it to the project as I thought it would fit thematically. Coincidentally, *Flawed Romance* shares the same chord progression, which genuinely only happened because I accidentally wrote the same progression twice.
The cover for **Dreamworld** is an edited picture of the Wallanlagen in Bremen, featuring a tree that fell and broke a year later. As such, this scene does not exist in quite the same way it did back then anymore.
I own a vinyl record of this album, produced as a one-off by [beevinyl](https://beevinyl.com/). It features the [A New Beginning EP](../anewbeginning) as bonus tracks.
<Player
tracks={[
{
title: "Monophobia",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/01%20F%C3%A6ls%20-%20Monophobia.flac",
},
{
title: "Snow in March",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/02%20F%C3%A6ls%20-%20Snow%20in%20March.flac",
},
{
title: "Flawed Romance",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/03%20F%C3%A6ls%20-%20Flawed%20Romance.flac",
},
{
title: "Identity Crisis",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/04%20F%C3%A6ls%20-%20Identity%20Crisis.flac",
},
{
title: "Dreamworld",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/05%20F%C3%A6ls%20-%20Dreamworld.flac",
},
{
title: "I.M.W.W.H. (I Miss What We Had)",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/06%20F%C3%A6ls%20-%20I.M.W.W.H..flac",
},
{
title: "In Another Time",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/07%20F%C3%A6ls%20-%20In%20Another%20Time.flac",
},
]}
cover=""
customColor="#B796A7"
useDarkHoverText="true"
/>

View File

@@ -0,0 +1,3 @@
These two accounts house demos, work-in-progress versions, and old music tracks I wrote.
I used to experiment a lot! You'll find electronic house, progressive house, hardstyle, melodic stuff, dubstep, joke songs, and even a few remixes. Most of it is terrible, but I like looking back at it.

View File

@@ -2,11 +2,11 @@
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import { posts, type DevlogPostLink } from "./devlog/posts";
import Gallery, { type GalleryEntry } from "$lib/lists/gallery.svelte";
import HomesickLinkList, { type LinkEntry } from "$lib/lists/homesick-link-list.svelte";
let entries: GalleryEntry[] = posts.map(mapEntries);
let entries: LinkEntry[] = posts.map(mapEntries);
function mapEntries(entry: DevlogPostLink, index: number): GalleryEntry {
function mapEntries(entry: DevlogPostLink, index: number): LinkEntry {
return {
title: `${entry.post.title}`,
subtitle: `#${(posts.length - index).toString().padStart(2, '0')} // ${entry.post.date}`,
@@ -32,11 +32,11 @@
<title>Homesick | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="Homesick"
banner="/projects/projectn5/banner2.webp"
bannerAlt="The protagonist Laura standing on a floating platform in the purple test level. Ziplines are all around her and the text 'When this text is spinning, the game is not paused' is frozen in the sky." />
<Content bannerContent={{
title: "Homesick",
banner: "/projects/projectn5/banner2.webp",
bannerAlt: "The protagonist Laura standing on a floating platform in the purple test level. Ziplines are all around her and the text 'When this text is spinning, the game is not paused' is frozen in the sky.",
}}>
<p>I am currently working on a game under the working title <b>Homesick</b> (fka Project N5)! I'm aiming for it to be an action-adventure jump-and-run game inspired by games such as Ratchet & Clank. Development started on <b>2023-09-16</b> and rebooted on <b>2025-05-16</b>.</p>
@@ -44,5 +44,5 @@
<p>Development log entries in reverse chronological order (newest to oldest).</p>
<Gallery {entries} />
<HomesickLinkList {entries} />
</Content>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import Video from "$lib/video.svelte";
import ImageRow from "$lib/media/image-row.svelte";
</script>
My progress in October 2023. Updates are shown in chronological order.
@@ -40,10 +41,18 @@ We now have health points! As it is currently set up, the health bar supports up
The health bar even aligns properly for 5 and 6 max health points, reducing the max points displayed per line from 4 to 3!
<div class="horizontally-centre-aligned">
<img src="2023-10-07_01.webp" alt="Five health points in the shape of plusses">
<img src="2023-10-07_02.webp" alt="Six health points in the shape of plusses">
</div>
<ImageRow
images={[
{
src: "2023-10-07_01.webp",
alt: "Five health points in the shape of plusses",
},
{
src: "2023-10-07_02.webp",
alt: "Six health points in the shape of plusses",
},
]}
/>
## What Are the Controls??
@@ -71,11 +80,22 @@ The Quick Select menu opens when the E key (keyboard) or the North face button (
In order to get started with the 3D models I'll need to create for the game, I attempted to begin the process of modelling the weapon of the protagonist! It's supposed to become a battle axe, though I have not yet finalised whether I'll keep with the idea.
<div class="horizontally-centre-aligned width-restricted">
<img src="2023-10-22_02.webp" alt="Top view of the battle axe with a flat blade">
<img src="2023-10-22_04.webp" alt="Top view of the battle axe with a thickened blade">
<img src="2023-10-22_05.webp" alt="Side view of the battle axe">
</div>
<ImageRow
images={[
{
src: "2023-10-22_02.webp",
alt: "Top view of the battle axe with a flat blade",
},
{
src: "2023-10-22_04.webp",
alt: "Top view of the battle axe with a thickened blade",
},
{
src: "2023-10-22_05.webp",
alt: "Side view of the battle axe",
},
]}
/>
## Hot, Fresh Quality

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import Video from "$lib/video.svelte";
import ImageRow from "$lib/media/image-row.svelte";
</script>
My progress in November 2023. Updates are shown in chronological order.
@@ -10,11 +11,22 @@ Lots of progress on the 3D models! I modelled the first weapon for the game, the
The earliest version was based on an 8-sided cylinder. After some feedback from friends, I remade the weapon, using a 16-sided cylinder, and also adding more details to the weapon overall. More attention went into the grip, which now resembled a weapon grip more so than a stick.
<div class="horizontally-centre-aligned">
<img src="2023-11-05_00.webp" alt="N5 Blaster v1 made from an 8-sided cylinder">
<img src="2023-11-03_01.webp" alt="N5 Blaster v2 made from a 16-sided blaster and with additional lights on the grip">
<img src="2023-11-05_03.webp" alt="N5 Blaster v3 with additional lights on the body">
</div>
<ImageRow
images={[
{
src: "2023-11-05_00.webp",
alt: "N5 Blaster v1 made from an 8-sided cylinder",
},
{
src: "2023-11-03_01.webp",
alt: "N5 Blaster v2 made from a 16-sided blaster and with additional lights on the grip",
},
{
src: "2023-11-05_03.webp",
alt: "N5 Blaster v3 with additional lights on the body",
},
]}
/>
Here's an overview of the first model.
@@ -32,11 +44,22 @@ In my first attempt at creating this icon, I took a picture of the wireframed ic
I also continued work on the battle axe, giving it more character. It's still not close to being finished, but it's now a bit less of a rough draft.
<div class="horizontally-centre-aligned">
<img src="2023-11-01_01.webp" alt="Battle axe with a hollowed-out blade">
<img src="2023-11-01_06.webp" alt="Battle axe with a stabilising pattern in the blade">
<img src="2023-11-01_09.webp" alt="Battle axe with two blades">
</div>
<ImageRow
images={[
{
src: "2023-11-01_01.webp",
alt: "Battle axe with a hollowed-out blade",
},
{
src: "2023-11-01_06.webp",
alt: "Battle axe with a stabilising pattern in the blade",
},
{
src: "2023-11-01_09.webp",
alt: "Battle axe with two blades",
},
]}
/>
I will admit though that I'm unsure whether I'll actually stick with the battle axe as the protagonist's main melee weapon.
@@ -46,10 +69,18 @@ Another idea, though more as an unlockable extra, is Derek the crowbar.
I also worked on the upgrade for the N5 Blaster, the N5 Cannon. Progress on that one has been a bit slow, since I have yet to figure out what kind of weapon I want the upgraded version to be.
<div class="horizontally-centre-aligned">
<img src="2023-11-12_04.webp" alt="N5 Cannon body">
<img src="2023-11-12_02.webp" alt="N5 Cannon Body next to the N5 Blaster">
</div>
<ImageRow
images={[
{
src: "2023-11-12_04.webp",
alt: "N5 Cannon body",
},
{
src: "2023-11-12_02.webp",
alt: "N5 Cannon Body next to the N5 Blaster",
},
]}
/>
And, as a bonus, here's the discarded, very-early-WIP draft I created for a rifle-type weapon. I don't think this type of weapon fits the type of game I'm making.
@@ -63,10 +94,18 @@ And a draft of a rocket launcher with 9 barrels! This is heavily inspired by the
I begun modelling my protagonist! I didn't progress far, as I currently lack a vision for where I really want my character to go in detail. I have slight ideas inspirations are, for example, [Merc & Green from Ratchet: Gladiator](https://hero.fandom.com/wiki/Merc_and_Green), and [Denholm Reynholm](denholm.webp). I quite liked the idea of having glowing tubes on the character's back; I got the inspiration from a Blender tutorial that was randomly recommended to me one morning.
<div class="horizontally-centre-aligned">
<img src="2023-11-11_05.webp" alt="Tubes for the back of the protagonist">
<img src="2023-11-12_01.webp" alt="Glowing tubes attached to a torso block">
</div>
<ImageRow
images={[
{
src: "2023-11-11_05.webp",
alt: "Tubes for the back of the protagonist",
},
{
src: "2023-11-12_01.webp",
alt: "Glowing tubes attached to a torso block",
},
]}
/>
i love

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import Video from "$lib/video.svelte";
import ImageRow from "$lib/media/image-row.svelte";
</script>
This is my final update before the next semester begins! I'm actually quite sad about this. For the past few days, I've been really motivated to work on my game. The free time that I had after I finished my last few submissions for university gave me enough room to spend significant time developing. But now that the next semester is about to begin, I fear that university will rob me of a significant amount of my time, and that I will not be able to spend the rest of my time productively.
@@ -65,10 +66,18 @@ Something visual to intersperse the dry code explanations:
This is an image for a weapon I've come up recently! It's supposed to be a slow-firing but strong blaster, contrasting the rapid-firing but weaker N5 Blaster (which recently changed to an automatic firing mode). The Venom originated from sketches like this, by the way:
<div class="horizontally-centre-aligned">
<img alt="A REALLY crude sketch of a blaster." src="dual_venom_sketch.webp">
<img alt="A crude sketch of a blaster resembling the 3D model shown above." src="venom_sketch.webp">
</div>
<ImageRow
images={[
{
src: "dual_venom_sketch.webp",
alt: "A REALLY crude sketch of a blaster.",
},
{
src: "venom_sketch.webp",
alt: "A crude sketch of a blaster resembling the 3D model shown above.",
},
]}
/>
The bolt visible in the first sketch actually makes me consider using [the model I showed off recently](/projects/projectn5/devlog/2024/0323/#a-new-weapon-is-in-the-works-) as the v2 for the Venom (which I would call Antidote by the way, in reference to [this song](https://youtu.be/fbafd6UV3w4)).

View File

@@ -1,3 +1,7 @@
<script lang="ts">
import ImageRow from "$lib/media/image-row.svelte";
</script>
I'M BACK!!!!
For real this time!! I've been busy with university, then I started working on other projects including 3D prints, electronics, a different game, then got busy with university again, and now I FINALLY started work on Project N5 again. And, as I promised in [my last update](/projects/projectn5/devlog/2024/0713), it would not take me another 3 months to get back to development in fact, I undercut that deadline by a whole 24 hours!
@@ -26,10 +30,18 @@ There's also a blue line shooting out of the gun in the picture above. That's a
The N5 Blaster received an overhauled icon. The soon-to-be-implemented N5 Bomb Launcher also received its own icon!
<div class="horizontally-centre-aligned">
<img src="n5-blaster-icon.webp" alt="N5 Blaster logo made from an icosphere and two handles">
<img src="n5-bomb-launcher-icon.webp" alt="N5 Bomb Launcher logo made from an icosphere and waves">
</div>
<ImageRow
images={[
{
src: "n5-blaster-icon.webp",
alt: "N5 Blaster logo made from an icosphere and two handles",
},
{
src: "n5-bomb-launcher-icon.webp",
alt: "N5 Bomb Launcher logo made from an icosphere and waves",
},
]}
/>
I changed the icosphere by tracing its 3D counterpart (the ball inside the N5 Blaster's glass tube) from a different point of view. It wasn't symmetrical before it is now.

View File

@@ -1,3 +1,7 @@
<script lang="ts">
import ImageRow from "$lib/media/image-row.svelte";
</script>
I have lots progress to share!!
First things first: Laura is, unlike I promised before, not yet finished. However, I have made *so much* progress in the past few weeks that I just wanted to get out already. Here's the current iteration of Laura:
@@ -36,17 +40,34 @@ Modelling hair has proven... challenging. What style to go with? What modelling
The first try (seen above) was using the technique from the SELS video selecting faces from the character's head, duplicating them, separating them into their own mesh, changing the scale, adding a solidify modifier, and then adding faces. This... worked, but I didn't like the results. And I tried quite a few styles.
<div class="horizontally-centre-aligned">
<img src="laura-hair-flat-1.webp" alt="White hair with middle split">
<img src="laura-hair-flat-2.webp" alt="White hair with spiky bangs">
<img src="laura-hair-flat-3.webp" alt="White hair with spiky ends">
</div>
<div class="horizontally-centre-aligned">
<img src="laura-hair-flat-4.webp" alt="Brown hair with middle split, flowing behind the character's ears">
<img src="laura-hair-flat-5.webp" alt="Brown hair with middle split and divided at the ears">
<img src="laura-hair-flat-6.webp" alt="Brown hair with big bangs">
</div>
<ImageRow
images={[
{
src: "laura-hair-flat-1.webp",
alt: "White hair with middle split",
},
{
src: "laura-hair-flat-2.webp",
alt: "White hair with spiky bangs",
},
{
src: "laura-hair-flat-3.webp",
alt: "White hair with spiky ends",
},
{
src: "laura-hair-flat-4.webp",
alt: "Brown hair with middle split, flowing behind the character's ears",
},
{
src: "laura-hair-flat-5.webp",
alt: "Brown hair with middle split and divided at the ears",
},
{
src: "laura-hair-flat-6.webp",
alt: "Brown hair with big bangs",
},
]}
/>
It always looked too flat, too shapeless, too boring, wrong cuts. It just didn't work.
@@ -54,19 +75,35 @@ Next up: a technique shown in these two videos: [Blender: How to Make HAIR, Full
Essentially, you create a curve and a circle, use the circle's shape as a profile for the curve, then change the circle's shape as well as the position and scale of the curve's vertices to create individual hair strands. Shown well in the two videos linked above, this can look pretty amazing! Only one issue: I'm creating a **game** character, and this technique is quite expensive, as it creates a lot of polygons for all the individual hair strands and the detail that goes into them. To mitigate this, I lowered the resolution of the profiling and used only a few curves to create an entire head's worth of hair. This looked a little like this:
<div class="horizontally-centre-aligned">
<img src="laura-hair-curves.webp" alt="Toon shaded view at the character with brown hair">
<img src="laura-hair-curves-2.webp" alt="Side view at the character without special shading">
</div>
<ImageRow
images={[
{
src: "laura-hair-curves.webp",
alt: "Toon shaded view at the character with brown hair",
},
{
src: "laura-hair-curves-2.webp",
alt: "Side view at the character without special shading",
},
]}
/>
This hair mesh originally (left picture) consisted of three parts: two curves at the front (left/right) and one in the back. This... was okay, but scaling the curves made the hair look weird. Thinner strands, especially when there's only a few of them, made them look more like dreads, and scaling up the vertices to large scales, as seen in the front near the top of the head, makes the hair look as if it's ballooning. Getting the shape right was a mess too: using only a single curve in the back meant that I had exactly one curve to cover quite literally half the head, and making sure that this singular strand of hair covered the head stretching from one ear to another was a pain. I tried using five curves (right picture), so that I have three in the back, but it didn't improve anything.
I then went *back* to the first method of scaling up faces from the head, with more knowledge and several tries behind me, and you know what? It actually kind of worked out.
<div class="horizontally-centre-aligned">
<img src="laura-hair-flat-new-2.webp" alt="Front view at the character with a green headband and bangs">
<img src="laura-hair-flat-new-3.webp" alt="Front view at the character with a green headband and middle-split hair">
</div>
<ImageRow
images={[
{
src: "laura-hair-flat-new-2.webp",
alt: "Front view at the character with a green headband and bangs",
},
{
src: "laura-hair-flat-new-3.webp",
alt: "Front view at the character with a green headband and middle-split hair",
},
]}
/>
The right picture is the current iteration of Laura's hair. I added a head band because I thought it looked nice, though that detail is not final.
@@ -98,10 +135,18 @@ Influenced while I was researching a tangential topic on a Blender forum, I read
To illustrate my point (get it?), here are some pictures. Left is in T-pose, right is in A-pose:
<div class="horizontally-centre-aligned">
<img src="deform-1.webp" alt="Diagram showing a t-posing character, with the text 'large angle, thus more deformation'">
<img src="deform-2.webp" alt="Diagram showing an a-posing character, with the text 'smaller angle, thus less deformation'">
</div>
<ImageRow
images={[
{
src: "deform-1.webp",
alt: "Diagram showing a t-posing character, with the text 'large angle, thus more deformation'",
},
{
src: "deform-2.webp",
alt: "Diagram showing an a-posing character, with the text 'smaller angle, thus less deformation'",
},
]}
/>
However, after watching [this video on bind poses](https://youtu.be/FXfc4Gyw6I0) by Doodley, it seems that... it doesn't really matter. Whether you use the T-pose, the A-pose, the lovingly-called hug-pose, or anything else really depends on what you plan to do with your character. Since Laura will mostly wield guns and keep her arms fairly low for most of the game, I decided to change Laura's modelling pose to an A-pose, with her arms pointed 30 degrees downward.

View File

@@ -1,13 +1,26 @@
<script lang="ts">
import ImageRow from "$lib/media/image-row.svelte";
</script>
While I've been busy working on another game with friends lately, I've managed to almost completely finish Laura. Here's what I've achieved!
## Visual Personality Adjustment
As promised before, I've worked on Laura's head a bit more. Her full face shield has been replaced with a face mask / respirator covering only the bottom half of her face. Also, I finally got the hair into a state I'm actually happy with. Here's a comparison:
<div class="horizontally-centre-aligned">
<img src="../../2024/1222/laura-hair-flat-new-3.webp" alt="The old protagonist's head with green clothing and full-face mask">
<img src="laura-head-new.webp" alt="The new protagonist Laura's head with red clothing, a half-face mask. The character now has a brown left eye, a mechanical right eye, and eyebrows, as well as bangs">
</div>
<ImageRow
images={[
{
src: "../../2024/1222/laura-hair-flat-new-3.webp",
alt: "The old protagonist's head with green clothing and full-face mask",
},
{
src: "laura-head-new.webp",
alt: "The new protagonist Laura's head with red clothing, a half-face mask. The character now has a brown left eye, a mechanical right eye, and eyebrows, as well as bangs",
},
]}
path="."
/>
The eyes took some work to get right, but I'm pretty happy with the current result. They're not proper eyeballs, but instead they're embedded into the head, which visually isn't significant because the flat shading would hide these details anyway. She has a brown left eye with a small sparkle, as well as a right eye replacement. This implies that Laura sustained further damage to the right side of her body, which necessitated replacement of her eye in addition to her right arm.
@@ -56,17 +69,34 @@ I've been thinking of adding an option (perhaps as a cheat code) to change her o
## Some Funny Pictures
<div class="horizontally-centre-aligned">
<img src="ok.webp" alt="An OK hand">
<img src="dance.webp" alt="Laura flailing her arms">
<img src="naruto.webp" alt="Laura naturo-running">
</div>
<div class="horizontally-centre-aligned">
<img src="shock.webp" alt="Laura's face looking shocked">
<img src="reprehension.webp" alt="Laura's face looking astonished">
<img src="disgust.webp" alt="Laura's face looking disgusted">
</div>
<ImageRow
images={[
{
src: "ok.webp",
alt: "An OK hand",
},
{
src: "dance.webp",
alt: "Laura flailing her arms",
},
{
src: "naruto.webp",
alt: "Laura naturo-running",
},
{
src: "shock.webp",
alt: "Laura's face looking shocked",
},
{
src: "reprehension.webp",
alt: "Laura's face looking astonished",
},
{
src: "disgust.webp",
alt: "Laura's face looking disgusted",
},
]}
/>
## The Future of this Devlog

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import Video from "$lib/video.svelte";
import ImageRow from "$lib/media/image-row.svelte";
</script>
I've been making a lot of progress in a lot of different areas, so I won't be able to elaborate on every little detail, but I'll focus on more major things. Excited to share what I've been working on!
@@ -84,12 +85,26 @@ I also fixed a long-running bug where the `DirectionalLight3D` of this preview w
### Please Appreciate These Wonderful Temporary Weapon Icons
<div class="horizontally-centre-aligned">
<img src="104-icon.webp" alt="An icon for a rocket launcher that looks like a sperm">
<img src="106-icon.webp" alt="A primitive flame as an icon for a flame thrower">
<img src="107-icon.webp" alt="A hand-drawn crosshair serving as an icon for a rifle">
<img src="108-icon.webp" alt="The words 'VENOM' as an icon for the weapon of that name">
</div>
<ImageRow
images={[
{
src: "104-icon.webp",
alt: "An icon for a rocket launcher that looks like a sperm",
},
{
src: "106-icon.webp",
alt: "A primitive flame as an icon for a flame thrower",
},
{
src: "107-icon.webp",
alt: "A hand-drawn crosshair serving as an icon for a rifle",
},
{
src: "108-icon.webp",
alt: "The words 'VENOM' as an icon for the weapon of that name",
},
]}
/>
## Grand Code Overhaul

View File

@@ -1,7 +1,5 @@
<script>
import Banner2 from "$lib/banner2.svelte";
import Content from "$lib/viewport/content.svelte";
import TableOfContents from "$lib/components/table-of-contents.svelte";
import ContentSidebar from "$lib/viewport/content-sidebar.svelte";
export let data;
</script>
@@ -12,18 +10,14 @@
<meta name="DCTERMS.created" content="{data.date}">
</svelte:head>
<Content>
<Banner2
title="{data.title}"
subtitle="Homesick Devlog"
date="{data.date}"
banner="preview.webp"
bannerAlt="{data.bannerAlt}"
/>
<TableOfContents />
<ContentSidebar bannerContent={{
title: data.title,
subtitle: "Homesick Devlog",
date: data.date,
banner: "preview.webp",
bannerAlt: data.bannerAlt,
}}>
<svelte:component this={data.content} />
</Content>
</ContentSidebar>

View File

@@ -1,368 +0,0 @@
export interface Project {
id: string;
isActive: boolean; // whether the project is currently active (true) or a past project (false)
banner: string;
icon: string;
date?: string;
title: string;
subtitle: string;
paragraphs: string[];
links: Link[];
status: ProjectStatus;
};
export enum ProjectStatus {
ACTIVE,
INACTIVE,
ABANDONED,
FINISHED,
EOL, // end of life
}
export interface Link {
text: string;
link: string;
}
export function getStatusText(project: Project): String {
switch (project.status) {
case ProjectStatus.ACTIVE:
return "active";
case ProjectStatus.INACTIVE:
return "inactive";
case ProjectStatus.ABANDONED:
return "abandoned";
case ProjectStatus.FINISHED:
return "finished";
case ProjectStatus.EOL:
return "end-of-life";
}
}
/**
* Returns static codes that can be used to reference same-name CSS classes
* without relying on display text.
*/
export function getStatusCode(project: Project): String {
switch (project.status) {
case ProjectStatus.ACTIVE:
return "act";
case ProjectStatus.INACTIVE:
return "ina";
case ProjectStatus.ABANDONED:
return "aba";
case ProjectStatus.FINISHED:
return "fin";
case ProjectStatus.EOL:
return "eol";
}
}
export const games: Project[] = [
{
id: "projectn5",
isActive: true,
banner: "/projects/projectn5/banner2.webp",
icon: "",
date: "September 2023 now",
title: "Homesick",
subtitle: "",
paragraphs: [
"I'm currently working on a game developed using Godot, entitled Homesick! It's aiming to be an action-adventure 3D jump & run heavily inspired by games such as <a href='https://en.wikipedia.org/wiki/Ratchet_%26_Clank'>Ratchet & Clank</a>.",
"I maintain a development log, feel free to check it out if you're curious! Or play some of the old builds available for download below.",
],
links: [
{
text: "View the <b>development log</b>",
link: "/projects/projectn5",
},
{
text: "Play an <b>old web build</b> (developed until 2025-05-16)",
link: "https://apps.natconf.dev/projectn5",
},
{
text: "Download the <b>old Windows builds</b>",
link: "https://files.natconf.dev/public/projectn5",
},
],
status: ProjectStatus.ACTIVE,
},
{
id: "magician",
isActive: false,
banner: "/projects/magician/banner.webp",
icon: "",
date: "July 2025",
title: "Magician",
subtitle: "Online Multiplayer Card Game",
paragraphs: [
"<b>Magician</b> is a clone of the card game <i>Wizard</i>. I've made it primarily to play with friends, but it's also a test for programming client-server multiplayer games. The clients are programmed in Godot, the server in Python, and they communicate via TCP/IP websockets. The game works with 3 to 6 players.",
"As the corresponding server is not currently online, the game is unplayable, but you can view the game below.",
],
links: [
{
text: "View the latest <b>Magician</b> build",
link: "https://apps.natconf.dev/magician",
},
],
status: ProjectStatus.ABANDONED,
},
{
id: "projektike",
isActive: false,
banner: "/projects/projektike/banner.webp",
icon: "",
date: "August 2024 May 2025",
title: "Projektike",
subtitle: "PvP Game",
paragraphs: [
"Two friends of mine and I worked together on a multiplayer game where you can throw magical artifacts at one another. It was developed using the Godot Engine. The game featured multiple character classes utilising artifacts such as fire, ice, black/white holes, wind, and teleports, and also had a money collecting mechanic. Player profile saving and character customisation were available, and there was an array of maps available to play on. Up to four players were supported.",
"The project is currently on hold.",
],
links: [
],
status: ProjectStatus.ABANDONED,
},
{
id: "swordsnstuff",
isActive: false,
banner: "/projects/swordsnstuff/banner.webp",
icon: "",
date: "August 2023",
title: "Swords & Stuff",
subtitle: "Unity 2D RPG",
paragraphs: [
"<b>Swords & Stuff</b> (working title) was an RPG inspired by Dragon Quest IX. Traverse the world with a party of up to 4 characters, each with their individual equiment, skills, and vocations, and fight monsters to gain experience!",
"This game was my first shot at 2D Unity development, which had proven quite fun, but also a little cumbersome at times. Unfortunately, I will not continue the project in its current form, since even though I don't expect to earn any money from my games, let alone publish them on a commercial platform, Unity's TOS changes in mid-2023 made me reconsider my decision to use the platform and I hence abandoned the project. I used this opportunity to learn to use Godot, which I will use for my future projects.",
"A working, but very early-in-development version of the game is still up on this website. It features a test overworld in which you can move the cast of four characters: Player1, fraxiom64, proudrat, and Grampa Simpson. The enemies, trolldads, chase you and take you into a battle scene that has a functioning queue, but no damage and health mechanics. You can flee battles. Don't open the inventory you cannot escape it.",
],
links: [
{
text: "Play <b>Swords & Stuff</b>",
link: "https://apps.natconf.dev/swordsnstuff",
},
],
status: ProjectStatus.ABANDONED,
},
{
id: "tads",
isActive: false,
banner: "/projects/tads/banner.webp",
icon: "/projects/tads/icon.webp",
date: "August 2023",
title: "Totally Accurate Dating Simulator",
subtitle: "HTML Text Adventure",
paragraphs: [
"<b>Totally Accurate Dating Simulator</b> is, as the name suggests, a series of particularly realistic dating simulator experiences. Dive into a world of romance as you meet your matches, curious and disappointing. Who will you meet? Will it be someone your type? That's subjective, and the computer will not even slightly attempt to match you with someone suitable. Prepare yourself for matches that are rarely described with \"intriguing\" and much more frequently with \"funny.\" Discover which of the 28 endings will determine your fate, and lower your expectations. Significantly.",
"One of these days, I'll make a TADS 3 with 3D characters and an actual matchmaking mechanic. One day...",
],
links: [
{
text: "Play <b>TADS 1</b>",
link: "https://apps.natconf.dev/tads/1",
},
{
text: "Play <b>TADS 2</b>",
link: "https://apps.natconf.dev/tads/2",
},
],
status: ProjectStatus.FINISHED,
},
];
export const hardware: Project[] = [
{
id: "daisyfm",
isActive: false,
banner: "/projects/daisyfm/banner.webp",
icon: "",
date: "July September 2024",
title: "Daisy",
subtitle: "Electro-Smith Daisy-based FM synth",
paragraphs: [
"One day, I was lying in bed, lacking motivation, telling a friend: \"I don't know what to do right now, I don't really have a project currently.\" His suggestion sent me down a two-month long road to create my own synthesiser.",
],
links: [
{
text: "Read more",
link: "/projects/daisyfm",
},
{
text: "Get the <b>PCB and STL files</b>",
link: "https://files.natconf.dev/public/daisyfm/",
},
{
text: "View the code files on <b>Codeberg</b>",
link: "https://codeberg.org/denizk0461/daisy-fm-synth",
},
],
status: ProjectStatus.FINISHED,
},
];
export const apps: Project[] = [
{
id: "weserplaner",
isActive: false,
banner: "/projects/weserplaner/banner.webp",
icon: "/projects/weserplaner/icon.webp",
date: "April 2023 January 2024",
title: "WeserPlaner",
subtitle: "University Timetable & Canteen Info App",
paragraphs: [
"<b>WeserPlaner</b> is an app I developed to more easily view relevant information during my studies at the University of Bremen. It can download the user's timetable from the university's learning platform Stud.IP, and it can download the menus for all canteens managed by the Studierendenwerk Bremen, allowing the user to filter for dietary preferences as well as hiding items containing substances they are allergic against.",
"In developing this app, I took heavy inspiration from an earlier project of mine, <b>AvH-Vertretungsplan</b>, which was an app I developed for a school I used to attend, in order to view the substitution plan as well as the canteen offers more easily. Quite similar!",
"I stopped work on the app in favour of other projects, notably because I had no significant plans for it. The app was taken down from the Google Play Store after I refused to comply with their new developer guidelines, requiring me to publish my home address (what the actual fuck Google?), and after the Studierendenwerk Bremen updated their page around the start of 2025, the app became nonfunctional.",
],
links: [
{
text: "View on <b>Codeberg</b>",
link: "https://codeberg.org/denizk0461/weserplaner/",
},
{
text: "Link to the <b>Google Play</b> store page (outdated)",
link: "https://play.google.com/store/apps/details?id=com.denizk0461.weserplaner",
},
],
status: ProjectStatus.EOL,
},
{
id: "textbasic",
isActive: false,
banner: "",
icon: "/projects/textbasic/icon.webp",
date: "May November 2023",
title: "Text Basic",
subtitle: "Extremely Basic Text Widget App",
paragraphs: [
"In the pursuit of finding a simple app on the Play Store that would allow me to put any kind of text on my home screen, I failed, and all I could find were ad-ridden monstrosities, overfilled with features. To fulfil my own requirements, I developed <b>Text Basic</b>, which does exactly one thing: put a text on your home screen. Upon user request, the app has grown minimally since, now allowing for text customisation (text font, text size, background style, colours) and a rotation of texts to display.",
"Surprisingly, the app garnered quite a signficant user base of >2.000 people, many of whom were sending me bug reports, and some even contributing ideas. Many were also frustrated that the text widgets changed... not understanding how the app works. It was taken down from the Play Store at the same time as <a href='#weserplaner'>WeserPlaner</a>.",
],
links: [
{
text: "View on <b>Codeberg</b>",
link: "https://codeberg.org/denizk0461/text-basic/",
},
{
text: "Link to the <b>Google Play</b> store page (outdated)",
link: "https://play.google.com/store/apps/details?id=com.denizk0461.textbasic",
},
],
status: ProjectStatus.EOL,
},
{
id: "qwark",
isActive: false,
banner: "",
icon: "/projects/qwark/icon.webp",
date: "June 2019 March 2020",
title: "Qwark Grade Log",
subtitle: "Grade Logging App",
paragraphs: [
"<b>Qwark</b> was an app I used to track relevant information for my time at gymnasium. Initially, it was only meant to track my grades in exams and whatnot, but its capabilities grew to calculate my report card grades and my final grade once I graduated. It was also able to keep simple notes which could be checked off to mark them as done, and it also had a built-in counter I used during lessons to track how often I raised my hand and how often I verbally contributed to classes! A discipline I severely struggled in.",
"The app was named after Captain Qwark, the clumsy hero figure from the Ratchet & Clank series who never really gets his affairs in order. I suppose this referenced me personally more so than the app, since I was the one who needed help with tracking relevant information a task for which the app proved very helpful!",
"The app reached EOL when I graduated from gymnasium, since I had no use for it anymore, and I never made it available to anyone else either.",
],
links: [
{
text: "View the Android app source code on <b>GitHub</b>",
link: "https://github.com/denizk0461/qwark",
},
],
status: ProjectStatus.EOL,
},
{
id: "avhplan",
isActive: false,
banner: "",
date: "April 2019 March 2020",
icon: "/projects/avhplan/icon.webp",
title: "AvH-Vertretungsplan",
subtitle: "Substitution Plan App",
paragraphs: [
"<b>AvH-Vertretungsplan</b> was an app I developed back in 2019 to view the substitution plan of my school more easily, since I disliked the disorganised design of the school's website. The substitution app allowed me to be more quickly informed about my courses, as it notified me about any cancellations and changes in my schedule. It also allowed me to view the canteen offers, even though I'd never eaten at the school's canteen.",
"The app was initially developed in Java and later converted into Kotlin as part of my Kotlin learning effort. The backend of the app, since the school website required a login to view the substitution plan, ran on a Raspberry Pi 3B using a Python script that scraped the website using Selenium and sent out notifications via Firebase. The Raspberry Pi crashed frequently and the script was not perfect, requiring me to frequently remote into the Pi using VNC, often while I was at school, to edit the script!",
"Since the app was written for native Android but a few of my classmates had iPhones, I had an iOS version in the works, which was developed entirely on a MacOS VM running on my laptop and debugged using an iPhone simulator in Xcode. The app achieved feature parity with the Android version at one point, but since I lacked funding to pay the $99-a-year fee Apple charges developers, it was never published and thus abandoned.",
"This app was genuinely one of my most fun projects, not only serving me as a handy tool, but also being a great playground for my programming practice, since it taught me to build UIs, use databases, and more. The app had been downloaded by 250 other students at my school, which I was and still am very proud of.",
],
links: [
{
text: "View the Android app source code on <b>GitHub</b>",
link: "https://github.com/denizk0461/avh-substitution-plan",
},
{
text: "View the iOS app source code on <b>GitHub</b>",
link: "https://github.com/denizk0461/avh-plan-ios",
},
],
status: ProjectStatus.EOL,
},
];
export const music: Project[] = [
{
id: "dreamworld",
isActive: false,
banner: "/projects/dreamworld/banner.webp",
icon: "/projects/dreamworld/icon.webp",
date: "July 2019 September 2021",
title: "Dreamworld",
subtitle: "My First Album",
paragraphs: [
"<b>Dreamworld</b> is my first album. I always wanted to write a cohesive piece of media, and <b>Dreamworld</b> was my first stab at this task. Originally, I had planned to write an album in the month of July 2020, but that fell flat entirely, instead starting out with the production of <i>Monophobia</i> a track name inspired by deadmau5' track of the same title and working on the album until September of the year thereafter.",
"I had started the track <i>Dreamworld</i> back in 2019, only adapting it to the project as I thought it would fit thematically. Coincidentally, <i>Flawed Romance</i> shares the same chord progression, which genuinely only happened because I accidentally wrote the same progression twice.",
"The cover for <b>Dreamworld</b> is an edited picture of the Wallanlagen in Bremen, featuring a tree that fell and broke a year later. As such, this scene does not exist in quite the same way it did back then anymore.",
"I own a vinyl record of this album, produced as a one-off by <a href='https://beevinyl.com/'>beevinyl</a>.",
],
links: [
{
text: "Listen & download on my <b>copyparty</b> instance",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/",
},
],
status: ProjectStatus.FINISHED,
},
{
id: "anewbeginning",
isActive: false,
banner: "",
icon: "/projects/anewbeginning/icon.webp",
date: "May August 2018",
title: "A New Beginning",
subtitle: "",
paragraphs: [
"<b>A New Beginning</b> is an EP I wrote back in 2018 in an effort to change up my production style. Originally, this EP was released as <i>A New Beginning (3-Track)</i>, featuring A New Beginning as <i>Nowy Początek</i> and <i>Farewell</i> in two different versions, one being an instrumental titled <i>Trzymajcie Się</i>, and the other one being a bootleg of Kelly Clarkson's <i>Behind These Hazel Eyes</i> called <i>Behind These Hazel Eyes (D4rkn355 'Farewell' Bootleg)</i>! For copyright reasons, the bootleg never made it onto streaming services.",
"This EP, to me, represents the start of a deviation in tone, production quality, and musical style from my previous works. While it was only a start, I am quite proud of the works I produced.",
],
links: [
{
text: "Listen & download on my <b>copyparty</b> instance",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/",
},
],
status: ProjectStatus.FINISHED,
},
{
id: "soundcloud",
isActive: false,
banner: "",
icon: "/projects/soundcloud/icon.webp",
title: "Soundcloud",
subtitle: "Demo Dump & Archive",
paragraphs: [
"These two accounts house demos, work-in-progress versions, and old music tracks I wrote.",
"I used to experiment a lot! You'll find electronic house, progressive house, hardstyle, melodic stuff, dubstep, joke songs, and even a few remixes. Most of it is terrible, but I like looking back at it.",
],
links: [
{
text: "Listen to the demo dump on <b>Soundcloud</b>",
link: "https://soundcloud.com/denizk0461",
},
{
text: "Listen to the archive on <b>Soundcloud</b>",
link: "https://soundcloud.com/djd4rkn355",
},
],
status: ProjectStatus.INACTIVE,
},
];

View File

@@ -0,0 +1,387 @@
export interface Project {
category: ProjectCategory;
id: string;
directLink?: string; // for linking somewhere directly instead of a project page
banner: string;
bannerAlt: string;
title: string;
subtitle: string;
description: string;
isOngoing: boolean; // whether the project is currently active (true) or a past project (false)
date: string;
dateUpdated?: string;
status: ProjectStatus;
links: Link[]; // may pass an empty array
isHighlight?: boolean;
};
export interface Link {
text: string;
link: string;
}
export enum ProjectCategory {
NULL = "all",
GAMES = "games",
ELECTRONICS = "electronics",
MUSIC = "music",
APPS = "apps",
MISC = "misc",
}
export enum ProjectStatus {
ACTIVE = "active",
INACTIVE = "inactive",
ABANDONED = "abandoned",
FINISHED = "finished",
EOL = "end-of-life", // end of life
}
/**
* Returns static codes that can be used to reference same-name CSS classes
* without relying on display text.
*/
export function getStatusCode(project: Project): String {
switch (project.status) {
case ProjectStatus.ACTIVE:
return "act";
case ProjectStatus.INACTIVE:
return "ina";
case ProjectStatus.ABANDONED:
return "aba";
case ProjectStatus.FINISHED:
return "fin";
case ProjectStatus.EOL:
return "eol";
}
}
export const projects: Project[] = [
// highlighted
{
category: ProjectCategory.GAMES,
id: "projectn5",
directLink: "/projects/projectn5",
banner: "/projects/projectn5/banner2.webp",
bannerAlt: "",
title: "Homesick",
subtitle: "my main video game project",
description: "",
isOngoing: true,
date: "September 2023 now",
status: ProjectStatus.ACTIVE,
links: [
// {
// text: "development log",
// link: "/projects/projectn5",
// },
// {
// text: "old web build (2025-05-16)",
// link: "https://apps.natconf.dev/projectn5",
// },
// {
// text: "old Windows builds",
// link: "https://files.natconf.dev/public/projectn5",
// },
],
isHighlight: true,
},
// 2026
{
category: ProjectCategory.MISC,
id: "lightyears-font",
banner: "banner.webp",
bannerAlt: "A rainbow-like holographic effect produced by bending a reflective sheet of cardboard.",
title: "LIGHTYEARS font",
subtitle: "stylised font",
description: "",
isOngoing: false,
date: "March 2026",
status: ProjectStatus.FINISHED,
links: [
{
text: "font download",
link: "https://files.natconf.dev/public/lightyears.woff2",
}
],
},
// 2025
{
category: ProjectCategory.GAMES,
id: "magician",
banner: "banner.webp",
bannerAlt: "",
title: "Magician",
subtitle: "Online Multiplayer Card Game",
description: "",
isOngoing: false,
date: "July 2025",
status: ProjectStatus.ABANDONED,
links: [
{
text: "view game",
link: "https://apps.natconf.dev/magician",
},
],
},
{
category: ProjectCategory.GAMES,
id: "projektike",
banner: "banner.webp",
bannerAlt: "",
title: "Projektike",
subtitle: "Local PvP Game",
description: "",
isOngoing: false,
date: "August 2024 May 2025",
status: ProjectStatus.ABANDONED,
links: [
],
},
// 2024
{
category: ProjectCategory.ELECTRONICS,
id: "3ds-usb-c",
banner: "finished.webp",
bannerAlt: "",
title: "3DS USB-C mod",
subtitle: "DIY charging port mod",
description: "",
isOngoing: false,
date: "October 2024",
status: ProjectStatus.FINISHED,
links: [
],
},
{
category: ProjectCategory.ELECTRONICS,
id: "daisyfm",
banner: "banner.webp",
bannerAlt: "Close-up of Daisy, focussed on the effect knobs",
title: "Daisy FM Synth",
subtitle: "Electro-Smith Daisy-based FM synth",
description: "",
isOngoing: false,
date: "July September 2024",
status: ProjectStatus.FINISHED,
links: [
{
text: "PCB & STL files",
link: "https://files.natconf.dev/public/daisyfm/",
},
{
text: "source code",
link: "https://codeberg.org/denizk0461/daisy-fm-synth",
},
],
},
{
category: ProjectCategory.ELECTRONICS,
id: "deej0461",
banner: "finished.webp",
bannerAlt: "",
title: "deej0461",
subtitle: "PC companion audio source controller",
description: "",
isOngoing: false,
date: "August 2024",
status: ProjectStatus.FINISHED,
links: [
],
},
{
category: ProjectCategory.APPS,
id: "weserplaner",
banner: "banner.webp",
bannerAlt: "",
title: "WeserPlaner",
subtitle: "University Timetable & Canteen Info App",
description: "",
isOngoing: false,
date: "April 2023 January 2024",
status: ProjectStatus.EOL,
links: [
{
text: "source code",
link: "https://codeberg.org/denizk0461/weserplaner/",
},
{
text: "link to the former <b>Google Play</b> store page",
link: "https://play.google.com/store/apps/details?id=com.denizk0461.weserplaner",
},
],
},
// 2023
{
category: ProjectCategory.APPS,
id: "textbasic",
banner: "icon.webp",
bannerAlt: "",
title: "Text Basic",
subtitle: "Extremely Basic Text Widget App",
description: "",
isOngoing: false,
date: "May November 2023",
status: ProjectStatus.EOL,
links: [
{
text: "source code",
link: "https://codeberg.org/denizk0461/text-basic/",
},
{
text: "former Google Play store page",
link: "https://play.google.com/store/apps/details?id=com.denizk0461.textbasic",
},
],
},
{
category: ProjectCategory.GAMES,
id: "swordsnstuff",
banner: "banner.webp",
bannerAlt: "",
title: "Swords & Stuff",
subtitle: "Unity 2D RPG",
description: "",
isOngoing: false,
date: "August 2023",
status: ProjectStatus.ABANDONED,
links: [
{
text: "play game",
link: "https://apps.natconf.dev/swordsnstuff",
},
],
},
{
category: ProjectCategory.GAMES,
id: "tads",
banner: "banner.webp",
bannerAlt: "",
title: "Totally Accurate Dating Simulator",
subtitle: "HTML Text Adventure",
description: "",
isOngoing: false,
date: "August 2023",
status: ProjectStatus.FINISHED,
links: [
{
text: "play TADS 1",
link: "https://apps.natconf.dev/tads/1",
},
{
text: "play TADS 2",
link: "https://apps.natconf.dev/tads/2",
},
],
},
// 2021
{
category: ProjectCategory.MUSIC,
id: "dreamworld",
banner: "banner.webp",
bannerAlt: "",
title: "Dreamworld",
subtitle: "My First Album",
description: "",
isOngoing: false,
date: "July 2019 September 2021",
status: ProjectStatus.FINISHED,
links: [
{
text: "listen & download",
link: "https://files.natconf.dev/public/my_tracks/Dreamworld/",
},
],
},
// 2020
{
category: ProjectCategory.APPS,
id: "qwark",
banner: "icon.webp",
bannerAlt: "",
title: "Qwark Grade Log",
subtitle: "Grade Logging App",
description: "",
isOngoing: false,
date: "June 2019 March 2020",
status: ProjectStatus.EOL,
links: [
{
text: "android source code",
link: "https://github.com/denizk0461/qwark",
},
],
},
{
category: ProjectCategory.APPS,
id: "avhplan",
banner: "graphic.webp",
bannerAlt: "",
title: "AvH-Vertretungsplan",
subtitle: "Substitution Plan App",
description: "",
isOngoing: false,
date: "April 2019 March 2020",
status: ProjectStatus.EOL,
links: [
{
text: "android source code",
link: "https://github.com/denizk0461/avh-substitution-plan",
},
{
text: "iOS source code",
link: "https://github.com/denizk0461/avh-plan-ios",
},
],
},
// 2018
{
category: ProjectCategory.MUSIC,
id: "anewbeginning",
banner: "cover.webp",
bannerAlt: "",
title: "A New Beginning",
subtitle: "Coming-of-age EP",
description: "",
isOngoing: false,
date: "May August 2018",
status: ProjectStatus.FINISHED,
links: [
{
text: "listen & download",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/",
},
],
},
// no specific date
{
category: ProjectCategory.MUSIC,
id: "soundcloud",
banner: "icon2.webp",
bannerAlt: "",
title: "Soundcloud",
subtitle: "Demo Dump & Archive",
description: "",
isOngoing: false,
date: "",
status: ProjectStatus.INACTIVE,
links: [
{
text: "visit demo dump",
link: "https://soundcloud.com/denizk0461",
},
{
text: "visit archive",
link: "https://soundcloud.com/djd4rkn355",
},
],
},
];

View File

@@ -1,109 +0,0 @@
<script>
import Banner2 from "$lib/banner2.svelte";
import SubtitledImage from "$lib/components/subtitled-image.svelte";
import TableOfContents from "$lib/components/table-of-contents.svelte";
import Content from "$lib/viewport/content.svelte";
</script>
<svelte:head>
<title>Small Projects | denizk0461</title>
</svelte:head>
<Content>
<Banner2
title="My Small Projects"
subtitle="the ones that don't get the spotlight"
banner="crate.webp"
bannerAlt="A cardboard box filled with electronic components, tools, and screws. They are arranged in 3D printed Gridfinity containers." />
<p>Not all of my projects are big, month-long endeavours. Some of them are short and sweet. Sometimes, they're even more rewarding than the bigger ones, because you end up with a finished 'thing' much quicker! And because I like my small projects just as much as my bigger ones, I figured it would be nice to give them a space on my website as well.</p>
<p>There's more to come here! This page is very new and I will add things here gradually.</p>
<TableOfContents />
<h2>3DS USB-C mod</h2>
<p class="subtitle">DIY charging port mod</p>
<p class="subtitle">October 2024</p>
<SubtitledImage
image="3ds-usb-c/showcase.mp4"
subtitle="it charges via USB-C! also do you like my A Hat in Time theme?"
video />
<p>I modded my New 3DS XL (SNES Edition) to give it a USB-C port to charge!</p>
<SubtitledImage
image="3ds-usb-c/finished.webp"
altText="A back view at a New Nintendo 3DS XL with a USB-C port added between the charging port and the right shoulder buttons."
subtitle="the USB-C port in all its glory"
alignment="right" />
<p>I used a small USB-C breakout I had lying around that is wired straight into the charging pads of the original charging port, which is left completely intact. The breakout board also has a 5.1kΩ resistor between ground and one of the CC pins (which I had to manually find because it's unlabelled) to allow for using C-to-C cables.</p>
<SubtitledImage
image="3ds-usb-c/hole.webp"
altText="At the top is a view at a USB-C-shaped hole cut into the back half of a New 3DS XL. At the bottom is a look at the same hole from the inside of the case. There are rough cutouts where the stylus slides in."
subtitle="a closer look at the holes I cut, and how they affect the stylus slot"
alignment="left" />
<p>What I wrecked in turn was the wrist strap loop, which I completely cut out to create the hole for the port. The stylus port also got cut down to make space, but my stylus is kind of broken and doesn't stay put when I put it into the system, so I didn't really care about that.</p>
<p>It works well! The hole isn't the prettiest but it was pretty simple to pull off, and extremely cheap as well. In turn I got a 3DS that I can charge using any USB-C cable and I no longer need to lug around the proprietary 3DS charger!</p>
<h2>deej0461</h2>
<p class="subtitle">PC companion audio source controller</p>
<p class="subtitle">August 2024</p>
<SubtitledImage
image="deej0461/finished.webp"
altText="A golden 3D printed shell with a slider on its left, two LEDs recessed, and four black buttons on its right. The buttons have symbols of speakers, monitors, and headphones printed on them. Three screws at the top are visible. A USB-C cable is plugged into the back of it."
subtitle="a handful of device for controlling a handful of other devices" />
<p>This little device was inspired by one a friend of mine built his own version of: a <a href="https://github.com/omriharel/deej">deej</a> volume slider panel. This thing allows you to control different applications with individual, <i>physical</i>, sliders. Super cool thing.</p>
<SubtitledImage
image="deej0461/printing.webp"
altText="A Bambu Lab A1 mini 3D printer in the middle of printing casing parts using a golden filament. The printer head has two googly eyes attached."
subtitle="googly-eyed printer hard at work"
alignment="left" />
<p>Except I didn't need all these sliders, really. A single slider would be cool, I thought. You know what I really wanted? Buttons to control the audio <i>source</i>, because I switch between speakers and headphones constantly, and that's at least 3 clicks every time I want to switch. So I built a device based on deej, but with some expansions.</p>
<p>I only used few components: a HID-enabled Arduino-compatible Pro Micro with USB-C controls the whole thing. Hooked up to it are four Cherry switches and a Soldering slider I had lying around from my <a href="/projects/daisy">Daisy project</a>, and I added two LEDs for good measure. It's all packaged into a 3D-printed enclosure I designed myself. The slider is screwed in tightly, and so is the top of the case; the key switches are clipped in from the top so they don't fall out; the Arduino and the LEDs are just hot-glued in. For extra flair, the four output buttons are marked with symbols for my outputs: two monitors, a pair of loudspeakers, and a pair of headphones. In the final device, they're arranged so that my two most frequently-used buttons are at the bottom for easier reach.</p>
<p>Software-wise, I set this up with the original deej software to control main volume. For the audio, I used a program called <a href="https://soundswitch.aaflalo.me/">SoundSwitch</a>. The program listened to key presses for the <code>F21-F24</code> keys, which the Arduino triggers when the output keys are pressed. The red LED lights up when a key is pressed; the white LED has no assigned function. This worked pretty well, but this is no longer the setup I use, since I switched to Fedora Linux, as I needed to adapt/change the software for the new OS!</p>
<SubtitledImage
image="deej0461/soldering.webp"
altText="An Arduino set into a 3D printed case with a slider, two LEDs, and four key switches soldered to it using wires. The components are spread out and hanging out the top of the case."
subtitle="no PCB? no problem"
alignment="right" />
<p>When pressing a keyboard's volume button, Windows raises or lowers volume in increments of 2. Fedora does 5. I found this much handier, so I stopped reaching for the slider and just defaulted to using my keyboard. This meant I didn't bother setting up the slider in Fedora. The buttons work, though, but they needed some adjustment. I think (and I might be wrong??) that Linux doesn't support function keys past F12, so I changed the Arduino script so the buttons trigger <code>Shift + F9-F12</code>. Instead of a separate program (which kept asking to be updated...), I now use KDE's built-in Shortcuts that trigger a script. The script is one line: <code>pactl set-default-sink [sink-name]</code>. The sink name is hard-coded into the file because, as extensive testing proved, Shortcuts does not allow arguments when entering a command. I currently only have two files set up: one for the primary monitor, one for the headphones.</p>
<p>I much prefer the setup now because it doesn't rely on third-party software anymore.</p>
<p>This thing is, no exaggeration, one of the handiest things I have ever built, because I use it quite literally <i><b>every single day</b></i>. I often switch between my monitor's speakers and my headphones, and being able to do that with the press of a single button is <i>unbelievably</i> handy. I don't even think about it anymore, I just reach for the buttons whenever I switch. It's a part of my routine now and I wouldn't want to miss it.</p>
</Content>
<style>
.subtitle {
font-family: var(--font-mono);
margin: 0;
font-size: 1.0rem;
line-height: 1.4rem;
font-style: italic;
font-weight: 700;
color: var(--color-highlight-alt);
}
.subtitle::before {
content: '<!-- ';
}
.subtitle::after {
content: ' -->';
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 932 KiB

After

Width:  |  Height:  |  Size: 932 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Some files were not shown because too many files have changed in this diff Show More