refactor: massively simplify to svelte with web/ directory, prepare for backend

This commit is contained in:
2026-01-04 15:10:35 -06:00
parent af81d8e048
commit 07ea1c093e
89 changed files with 1180 additions and 9659 deletions
+108
View File
@@ -0,0 +1,108 @@
@import "tailwindcss";
@theme {
/* Custom colors */
--color-zinc-850: #1d1d20;
/* Custom font sizes */
--font-size-10xl: 10rem;
/* Drop shadows */
--drop-shadow-extreme: 0 0 50px black;
/* Font families */
--font-inter: "Inter Variable", sans-serif;
--font-hanken: "Hanken Grotesk", sans-serif;
--font-schibsted: "Schibsted Grotesk", sans-serif;
/* Background images */
--background-image-gradient-radial: radial-gradient(
50% 50% at 50% 50%,
var(--tw-gradient-stops)
);
/* Animations */
--animate-bg-fast: fade 0.5s ease-in-out 0.5s forwards;
--animate-bg: fade 2.5s ease-in-out 1.5s forwards;
--animate-fade-in: fade-in 2.5s ease-in-out forwards;
--animate-title: title 3s ease-out forwards;
--animate-fade-left: fade-left 3s ease-in-out forwards;
--animate-fade-right: fade-right 3s ease-in-out forwards;
}
@keyframes fade {
0% {
opacity: 0%;
}
100% {
opacity: 100%;
}
}
@keyframes fade-in {
0% {
opacity: 0%;
}
75% {
opacity: 0%;
}
100% {
opacity: 100%;
}
}
@keyframes fade-left {
0% {
transform: translateX(100%);
opacity: 0%;
}
30% {
transform: translateX(0%);
opacity: 100%;
}
100% {
opacity: 0%;
}
}
@keyframes fade-right {
0% {
transform: translateX(-100%);
opacity: 0%;
}
30% {
transform: translateX(0%);
opacity: 100%;
}
100% {
opacity: 0%;
}
}
@keyframes title {
0% {
line-height: 0%;
letter-spacing: 0.25em;
opacity: 0;
}
25% {
line-height: 0%;
opacity: 0%;
}
80% {
opacity: 100%;
}
100% {
line-height: 100%;
opacity: 100%;
}
}
html,
body {
@apply font-inter overflow-x-hidden text-white;
}
body {
@apply h-full;
}
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
import Dots from "./Dots.svelte";
let {
class: className = "",
backgroundClass = "",
children,
}: {
class?: string;
backgroundClass?: string;
children?: Snippet;
} = $props();
</script>
<div class="pointer-events-none fixed inset-0 -z-20 bg-black"></div>
<Dots class={[backgroundClass]} />
<main class={cn("relative min-h-screen text-zinc-50", className)}>
{#if children}
{@render children()}
{/if}
</main>
+398
View File
@@ -0,0 +1,398 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { ClassValue } from "clsx";
import { onMount, onDestroy } from "svelte";
let {
class: className = "",
scale = 1000,
length = 10,
spacing = 20,
timeScale = 10.25 / 1000,
angleTimeScale = 2.0,
lengthTimeScale = 1.5,
opacity = 0.9,
radius = 3.5,
angleOpacityAmplitude = 0.8,
angleOpacityFloor = 0.1,
randomOpacityMin = 0.5,
randomOpacityMax = 1.0,
dotColor = [200 / 255, 200 / 255, 200 / 255] as [number, number, number],
}: {
class?: ClassValue;
scale?: number;
length?: number;
spacing?: number;
timeScale?: number;
angleTimeScale?: number;
lengthTimeScale?: number;
opacity?: number;
radius?: number;
angleOpacityAmplitude?: number;
angleOpacityFloor?: number;
randomOpacityMin?: number;
randomOpacityMax?: number;
dotColor?: [number, number, number];
} = $props();
let canvas: HTMLCanvasElement;
let cleanupFns: (() => void)[] = [];
function addCleanup(fn: () => void) {
cleanupFns.push(fn);
}
class UniformManager {
private gl: WebGLRenderingContext;
private locations = new Map<string, WebGLUniformLocation>();
constructor(
gl: WebGLRenderingContext,
program: WebGLProgram,
uniforms: string[],
) {
this.gl = gl;
uniforms.forEach((name) => {
const loc = gl.getUniformLocation(program, name);
if (loc) this.locations.set(name, loc);
});
}
set(
name: string,
value: number | [number, number] | [number, number, number],
) {
const loc = this.locations.get(name);
if (!loc) return;
if (Array.isArray(value)) {
if (value.length === 2) {
this.gl.uniform2f(loc, value[0], value[1]);
} else if (value.length === 3) {
this.gl.uniform3f(loc, value[0], value[1], value[2]);
}
} else {
this.gl.uniform1f(loc, value);
}
}
setStatic(values: Record<string, number>) {
Object.entries(values).forEach(([name, value]) => this.set(name, value));
}
}
const vertexShader = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const fragmentShader = `
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_seed;
uniform float u_dpr;
uniform float u_scale;
uniform float u_length;
uniform float u_spacing;
uniform float u_opacity;
uniform float u_radius;
uniform float u_angleTimeScale;
uniform float u_lengthTimeScale;
uniform float u_angleOpacityAmp;
uniform float u_angleOpacityFloor;
uniform float u_randomOpacityMin;
uniform float u_randomOpacityMax;
uniform vec3 u_dotColor;
const float PI = 3.14159265359;
// Simplex 3D noise
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i + u_seed);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ *ns.x + ns.yyyy;
vec4 y = y_ *ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0)*2.0 + 1.0;
vec4 s1 = floor(b1)*2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
}
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise01(vec3 v) {
return (snoise(v) + 1.0) * 0.5;
}
void main() {
vec2 pixelCoord = gl_FragCoord.xy;
float spacing = u_spacing * u_dpr;
float scaleDpr = u_scale * u_dpr;
vec2 gridCoord = floor(pixelCoord / spacing) * spacing;
float minDist = 1000000.0;
float closestRad = 0.0;
float pointOpacity = 0.0;
// Check 9 neighboring grid points
for (float dx = -1.0; dx <= 1.0; dx += 1.0) {
for (float dy = -1.0; dy <= 1.0; dy += 1.0) {
vec2 testGrid = gridCoord + vec2(dx * spacing, dy * spacing);
float rad = (noise01(vec3(testGrid / scaleDpr, u_time * u_angleTimeScale)) - 0.5) * 4.0 * PI;
float len = (noise01(vec3(testGrid / scaleDpr, u_time * u_lengthTimeScale)) + 0.5) * u_length * u_dpr;
vec2 displacedPoint = testGrid + vec2(cos(rad), sin(rad)) * len;
float dist = distance(pixelCoord, displacedPoint);
if (dist < minDist) {
minDist = dist;
closestRad = rad;
pointOpacity = hash(testGrid) * (u_randomOpacityMax - u_randomOpacityMin) + u_randomOpacityMin;
}
}
}
float circle = 1.0 - smoothstep(0.0, u_radius * u_dpr, minDist);
float angleOpacity = (abs(cos(closestRad)) * u_angleOpacityAmp + u_angleOpacityFloor) * pointOpacity * u_opacity;
gl_FragColor = vec4(u_dotColor, circle * angleOpacity);
}
`;
function createShader(
gl: WebGLRenderingContext,
type: number,
source: string,
): WebGLShader | null {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("Shader compile error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(
gl: WebGLRenderingContext,
vertexShader: WebGLShader,
fragmentShader: WebGLShader,
): WebGLProgram | null {
const program = gl.createProgram();
if (!program) return null;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("Program link error:", gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
function initWebGL(canvas: HTMLCanvasElement) {
const gl = canvas.getContext("webgl", {
alpha: true,
premultipliedAlpha: false,
});
if (!gl) {
console.error("WebGL not supported");
return null;
}
const vShader = createShader(gl, gl.VERTEX_SHADER, vertexShader);
const fShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShader);
if (!vShader || !fShader) {
console.error("Shader compilation failed");
return null;
}
const program = createProgram(gl, vShader, fShader);
if (!program) {
console.error("Program linking failed");
return null;
}
return { gl, program };
}
onMount(() => {
const context = initWebGL(canvas);
if (!context) return;
const { gl, program } = context;
// Setup fullscreen quad geometry
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
gl.STATIC_DRAW,
);
const positionLocation = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// Setup uniform manager
const uniforms = new UniformManager(gl, program, [
"u_resolution",
"u_time",
"u_seed",
"u_dpr",
"u_scale",
"u_length",
"u_spacing",
"u_opacity",
"u_radius",
"u_angleTimeScale",
"u_lengthTimeScale",
"u_angleOpacityAmp",
"u_angleOpacityFloor",
"u_randomOpacityMin",
"u_randomOpacityMax",
"u_dotColor",
]);
gl.useProgram(program);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
const dpr = window.devicePixelRatio || 1;
// Set static uniforms
uniforms.setStatic({
u_seed: Math.random() * 1000,
u_dpr: dpr,
u_scale: scale,
u_length: length,
u_spacing: spacing,
u_opacity: opacity,
u_radius: radius,
u_angleTimeScale: angleTimeScale,
u_lengthTimeScale: lengthTimeScale,
u_angleOpacityAmp: angleOpacityAmplitude,
u_angleOpacityFloor: angleOpacityFloor,
u_randomOpacityMin: randomOpacityMin,
u_randomOpacityMax: randomOpacityMax,
});
uniforms.set("u_dotColor", dotColor);
const resizeCanvas = () => {
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight}px`;
gl.viewport(0, 0, canvas.width, canvas.height);
};
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
addCleanup(() => window.removeEventListener("resize", resizeCanvas));
const startTime = Date.now();
let animationId: number;
function render() {
const time = ((Date.now() - startTime) / 1000) * timeScale;
uniforms.set("u_resolution", [canvas.width, canvas.height]);
uniforms.set("u_time", time);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 6);
animationId = requestAnimationFrame(render);
}
render();
addCleanup(() => cancelAnimationFrame(animationId));
addCleanup(() => gl.getExtension("WEBGL_lose_context")?.loseContext());
});
onDestroy(() => {
cleanupFns.forEach((fn) => fn());
});
</script>
<canvas
bind:this={canvas}
class={cn("pointer-events-none fixed inset-0 -z-10", className)}
></canvas>
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import "../app.css";
import "@fontsource-variable/inter";
import "@fontsource/hanken-grotesk/900.css";
import "@fontsource/schibsted-grotesk/400.css";
import "@fontsource/schibsted-grotesk/500.css";
import "@fontsource/schibsted-grotesk/600.css";
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href="/favicon.ico" />
<title>Xevion.dev</title>
<meta
name="description"
content="The personal website of Xevion, a full-stack software developer."
/>
</svelte:head>
{@render children()}
+85
View File
@@ -0,0 +1,85 @@
<script lang="ts">
import AppWrapper from "$lib/components/AppWrapper.svelte";
import IconSimpleIconsGithub from "~icons/simple-icons/github";
import IconSimpleIconsLinkedin from "~icons/simple-icons/linkedin";
import IconSimpleIconsDiscord from "~icons/simple-icons/discord";
import MaterialSymbolsMailRounded from "~icons/material-symbols/mail-rounded";
import MaterialSymbolsVpnKey from "~icons/material-symbols/vpn-key";
// import IconLucideRss from "~icons/lucide/rss";
</script>
<AppWrapper class="overflow-x-hidden font-schibsted">
<!-- Top Navigation Bar -->
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9">
<div class="flex gap-4 items-center">
<!-- <a href="/rss" class="text-zinc-400 hover:text-zinc-200">
<IconLucideRss class="size-5" />
</a> -->
</div>
</div>
<!-- Main Content -->
<div class="flex items-center flex-col">
<div
class="max-w-2xl mx-6 border-b border-zinc-700 divide-y divide-zinc-700"
>
<!-- Name & Occupation -->
<div class="flex flex-col pb-4">
<span class="text-3xl font-bold text-white">Ryan Walters,</span>
<span class="text-2xl font-normal text-zinc-400">
Full-Stack Software Engineer
</span>
</div>
<div class="py-4 text-zinc-200">
<p class="text-[0.95em]">
A fanatical software engineer with expertise and passion for sound,
scalable and high-performance applications. I'm always working on
something new. <br />
Sometimes innovative &mdash; sometimes crazy.
</p>
</div>
<div class="py-3">
<span class="text-zinc-200">Connect with me</span>
<div class="flex gap-x-2 pl-3 pt-3 pb-2">
<a
href="https://github.com/Xevion"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<IconSimpleIconsGithub class="size-4 text-zinc-300" />
<span class="text-sm text-zinc-100">GitHub</span>
</a>
<a
href="https://linkedin.com/in/ryancwalters"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<IconSimpleIconsLinkedin class="size-4 text-zinc-300" />
<span class="text-sm text-zinc-100">LinkedIn</span>
</a>
<button
type="button"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<IconSimpleIconsDiscord class="size-4 text-zinc-300" />
<span class="text-sm text-zinc-100">Discord</span>
</button>
<a
href="mailto:your.email@example.com"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<MaterialSymbolsMailRounded class="size-4.5 text-zinc-300" />
<span class="text-sm text-zinc-100">Email</span>
</a>
<button
type="button"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<MaterialSymbolsVpnKey class="size-4.5 text-zinc-300" />
<span class="text-sm text-zinc-100">PGP Key</span>
</button>
</div>
</div>
</div>
</div>
</AppWrapper>
+21
View File
@@ -0,0 +1,21 @@
import type { PageServerLoad } from "./$types";
interface ProjectLink {
url: string;
title?: string;
}
export interface Project {
id: string;
name: string;
shortDescription: string;
icon?: string;
links: ProjectLink[];
}
export const load: PageServerLoad = async () => {
// TODO: Fetch from Rust backend API
return {
projects: [] as Project[],
};
};
+60
View File
@@ -0,0 +1,60 @@
<script lang="ts">
import AppWrapper from "$lib/components/AppWrapper.svelte";
import { cn } from "$lib/utils";
let { data } = $props();
</script>
<AppWrapper>
<div
class="relative z-10 mx-auto grid grid-cols-1 justify-center gap-y-4 px-4 py-20 align-middle sm:grid-cols-2 md:max-w-200 lg:max-w-300 lg:grid-cols-3 lg:gap-y-9"
>
<div class="mb-3 text-center sm:col-span-2 md:mb-5 lg:col-span-3 lg:mb-7">
<h1
class="pb-3 font-hanken text-4xl text-zinc-200 opacity-100 md:text-5xl"
>
Projects
</h1>
<p class="text-lg text-zinc-400">
created, maintained, or contributed to by me...
</p>
</div>
{#each data.projects as project (project.id)}
{@const links = project.links}
{@const useAnchor = links.length > 0}
{@const href = useAnchor ? links[0].url : undefined}
<div class="max-w-fit">
<svelte:element
this={useAnchor ? "a" : "div"}
{href}
target={useAnchor ? "_blank" : undefined}
rel={useAnchor ? "noreferrer" : undefined}
title={project.name}
class="flex items-center justify-start overflow-hidden rounded bg-black/10 pb-2.5 pl-3 pr-5 pt-1 text-zinc-400 transition-colors hover:bg-zinc-500/10 hover:text-zinc-50"
>
<div class="flex h-full w-14 items-center justify-center pr-5">
<i
class={cn(
project.icon ?? "fa-heart",
"fa-solid text-3xl text-opacity-80 saturate-0",
)}
></i>
</div>
<div class="overflow-hidden">
<span class="text-sm md:text-base lg:text-lg">
{project.name}
</span>
<p
class="truncate text-xs opacity-70 md:text-sm lg:text-base"
title={project.shortDescription}
>
{project.shortDescription}
</p>
</div>
</svelte:element>
</div>
{/each}
</div>
</AppWrapper>
+7
View File
@@ -0,0 +1,7 @@
import { redirect } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async () => {
// TODO: Fetch resume URL from Rust backend API
redirect(302, "https://example.com/resume.pdf");
};