diff --git a/index.html b/index.html index 0657b46..c89ca26 100755 --- a/index.html +++ b/index.html @@ -158,9 +158,7 @@ braeden@sowinski.dev
-
- Braeden Sowinski profile photo -
+ Braeden Sowinski profile photo

whoami

@@ -184,33 +182,31 @@ About
+
+
+ title="LinkedIn" aria-label="LinkedIn"> - LinkedIn - Gitea - + - Email
- diff --git a/public/css/projects.css b/public/css/projects.css index 2e7d27c..86d82b9 100755 --- a/public/css/projects.css +++ b/public/css/projects.css @@ -5,28 +5,10 @@ /* The page header sits on the background; only cards get a surface. */ .projects-hero { - position: relative; - isolation: isolate; margin-bottom: 40px; max-width: 760px; } -/* Soft scrim so the heading stays readable over the ASCII field, without - putting the text in a box. */ -.projects-hero::before { - content: ""; - position: absolute; - inset: -28px -10px -20px -10px; - z-index: -1; - pointer-events: none; - background: radial-gradient( - ellipse at 30% 50%, - rgba(11, 12, 15, 0.92), - rgba(11, 12, 15, 0.6) 45%, - transparent 75% - ); -} - .projects-hero h1, .entry-title { font-size: clamp(1.5rem, 3vw, 2.1rem); diff --git a/public/css/style.css b/public/css/style.css index b319579..c2bc276 100755 --- a/public/css/style.css +++ b/public/css/style.css @@ -2,8 +2,8 @@ :root { --theme: #0b0c0f; - --surface: rgba(20, 22, 27, 0.34); - --surface-raised: rgba(28, 31, 38, 0.46); + --surface: rgba(21, 23, 28, 0.58); + --surface-raised: rgba(28, 31, 37, 0.62); --bar: rgba(255, 255, 255, 0.04); --primary: #ccd0d9; --bright: #f2f4f8; @@ -13,7 +13,7 @@ --border: rgba(255, 255, 255, 0.1); --border-strong: rgba(255, 255, 255, 0.2); --glow: rgba(235, 82, 52, 0.16); - --shadow: 0 20px 54px rgba(0, 0, 0, 0.4); + --shadow: 0 18px 50px rgba(0, 0, 0, 0.34); --selection: rgba(235, 82, 52, 0.3); --ascii-base: #99a2b4; --ascii-accent: #eb5234; @@ -23,9 +23,6 @@ --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; - - /* Fine film grain that sits on every frosted surface. */ - --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)' opacity='0.55'/%3E%3C/svg%3E"); } * { @@ -78,11 +75,7 @@ body::before { inset: 0; z-index: 0; pointer-events: none; - opacity: 0.15; -} - -body[data-page="home"] #ascii-bg { - opacity: 0.34; + opacity: 0.22; } header, @@ -96,27 +89,11 @@ main, .surface { position: relative; - isolation: isolate; background: var(--surface); - backdrop-filter: blur(26px) saturate(150%); - -webkit-backdrop-filter: blur(26px) saturate(150%); + backdrop-filter: blur(18px) saturate(135%); + -webkit-backdrop-filter: blur(18px) saturate(135%); border: 1px solid var(--border); - box-shadow: - var(--shadow), - inset 0 1px 0 rgba(255, 255, 255, 0.07); -} - -.surface::after { - content: ""; - position: absolute; - inset: 0; - z-index: -1; - border-radius: inherit; - pointer-events: none; - background-image: var(--grain); - background-size: 180px 180px; - opacity: 0.22; - mix-blend-mode: soft-light; + box-shadow: var(--shadow); } a, @@ -442,17 +419,25 @@ body.menu-open .overlay * { margin-left: 8px; } +/* Photo and contact row stack in a left column, both pinned to the top, + with the text column beside them. */ .term__body { display: grid; - grid-template-columns: auto 1fr; - gap: 30px; - padding: 32px 30px; - align-items: center; + grid-template-columns: 164px 1fr; + grid-template-areas: + "photo stream" + "contact stream"; + grid-template-rows: auto auto; + align-content: start; + column-gap: 30px; + row-gap: 16px; + padding: 32px 30px 34px; } .term__avatar { - width: 148px; - height: 148px; + grid-area: photo; + width: 164px; + height: 164px; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--border-strong); @@ -462,11 +447,12 @@ body.menu-open .overlay * { } .term__stream { + grid-area: stream; min-width: 0; } .cmd { - margin: 0 0 18px; + margin: 0 0 16px; color: var(--muted); font-size: 13px; } @@ -592,19 +578,21 @@ body.menu-open .overlay * { color: var(--secondary); } -/* The landing page gets labelled contact cards instead of bare icons. */ +/* The landing page gets large icon buttons for contact. */ .hero-social { - gap: 12px; + grid-area: contact; + gap: 10px; flex-wrap: wrap; - margin-top: 28px; + margin-top: 0; + width: 164px; } .hero-social .social-link { display: inline-flex; align-items: center; - gap: 10px; - padding: 10px 16px; - font-size: 13.5px; + justify-content: center; + width: 48px; + height: 48px; color: var(--primary); border: 1px solid var(--border); border-radius: var(--radius); @@ -612,8 +600,8 @@ body.menu-open .overlay * { } .hero-social .social-link svg { - height: 18px; - width: 18px; + height: 20px; + width: 20px; color: var(--muted); transition: color 0.2s var(--ease), @@ -666,7 +654,7 @@ body.menu-open .overlay * { @media only screen and (max-width: 860px) { #menu, - .term__avatar-wrap { + .term__avatar { display: none; } @@ -682,8 +670,15 @@ body.menu-open .overlay * { .term__body { grid-template-columns: 1fr; - gap: 0; - padding: 26px 20px; + grid-template-areas: + "stream" + "contact"; + row-gap: 24px; + padding: 26px 20px 28px; + } + + .hero-social { + width: auto; } .page-shell { @@ -691,7 +686,8 @@ body.menu-open .overlay * { } .hero-social .social-link { - padding: 9px 14px; + width: 44px; + height: 44px; } .footer { diff --git a/public/scripts/ascii-bg.js b/public/scripts/ascii-bg.js index 4d6ba4e..6481159 100644 --- a/public/scripts/ascii-bg.js +++ b/public/scripts/ascii-bg.js @@ -8,62 +8,54 @@ const context = canvas.getContext("2d", { alpha: true }); const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); - const CELL_WIDTH = 13; - const CELL_HEIGHT = 17; - const ASPECT = CELL_WIDTH / CELL_HEIGHT; - const FRAME_MS = 1000 / 24; - - // Ambient dither behind everything. - const DUST = "·.:-=+*"; - const DUST_BASE = 0.05; - const DUST_WAVE = 0.16; - - // Lit torus, shaded the way donut.c does it: a luminance ramp from a dot to a - // solid block, with a z-buffer so the near face hides the far one. - const RAMP = ".,-~:;=!*#$@"; - const R1 = 1; - const R2 = 2; - const K2 = 5; - const THETA_STEPS = 120; - const PHI_STEPS = 360; + const CHARS = "·.:-=+*#"; + const CELL_WIDTH = 14; + const CELL_HEIGHT = 19; + const FRAME_MS = 1000 / 18; + const BASE_DENSITY = 0.09; + const WAVE_DENSITY = 0.24; let columns = 0; let rows = 0; let dither = new Float32Array(0); - let depth = new Float32Array(0); - let shade = new Int8Array(0); - - const thetaCos = new Float32Array(THETA_STEPS); - const thetaSin = new Float32Array(THETA_STEPS); - const phiCos = new Float32Array(PHI_STEPS); - const phiSin = new Float32Array(PHI_STEPS); - - for (let i = 0; i < THETA_STEPS; i += 1) { - const theta = (i / THETA_STEPS) * Math.PI * 2; - thetaCos[i] = Math.cos(theta); - thetaSin[i] = Math.sin(theta); - } - - for (let i = 0; i < PHI_STEPS; i += 1) { - const phi = (i / PHI_STEPS) * Math.PI * 2; - phiCos[i] = Math.cos(phi); - phiSin[i] = Math.sin(phi); - } - - let baseColor = "#8e97a8"; - let accentColor = "#eb5234"; - let dimColor = "#5d6472"; + let baseColor = "rgba(255, 255, 255, 1)"; + let accentColor = "rgba(235, 82, 52, 1)"; let time = 0; - let angleA = 0.6; - let angleB = 0.9; let lastFrame = 0; let rafId = 0; + // Interfering waves, centred on the viewport, give the field slow bright and + // dark swells. Which cells are lit comes from a fixed per-cell dither value, + // so the texture stays even everywhere instead of pooling into blobs, and + // cells brighten as a swell passes rather than flickering at random. + const waveAt = (x, y, t) => { + const radius = Math.sqrt( + ((x - columns / 2) * 0.55) ** 2 + (y - rows / 2) ** 2 + ); + + const a = Math.sin(x * 0.068 + t * 0.55); + const b = Math.sin(y * 0.1 - t * 0.42); + const c = Math.sin((x * 0.4 + y * 0.6) * 0.07 + t * 0.33); + const d = Math.sin(radius * 0.1 - t * 0.6); + + return (a + b + c + d) / 8 + 0.5; + }; + + const buildDither = () => { + dither = new Float32Array(columns * rows); + + for (let row = 0; row < rows; row += 1) { + for (let column = 0; column < columns; column += 1) { + const noise = Math.sin(column * 127.1 + row * 311.7) * 43758.5453; + dither[row * columns + column] = noise - Math.floor(noise); + } + } + }; + const readThemeColors = () => { const styles = getComputedStyle(document.body); baseColor = styles.getPropertyValue("--ascii-base").trim() || baseColor; accentColor = styles.getPropertyValue("--ascii-accent").trim() || accentColor; - dimColor = styles.getPropertyValue("--ascii-dim").trim() || dimColor; }; const resize = () => { @@ -82,131 +74,38 @@ columns = Math.ceil(width / CELL_WIDTH); rows = Math.ceil(height / CELL_HEIGHT); - - const cells = columns * rows; - dither = new Float32Array(cells); - depth = new Float32Array(cells); - shade = new Int8Array(cells); - - for (let row = 0; row < rows; row += 1) { - for (let column = 0; column < columns; column += 1) { - const noise = Math.sin(column * 127.1 + row * 311.7) * 43758.5453; - dither[row * columns + column] = noise - Math.floor(noise); - } - } - }; - - // Slow interfering swells that move brightness through the dust layer. - const swellAt = (x, y, t) => { - const radius = Math.sqrt(((x - columns / 2) * 0.55) ** 2 + (y - rows / 2) ** 2); - - return ( - (Math.sin(x * 0.068 + t * 0.55) + - Math.sin(y * 0.1 - t * 0.42) + - Math.sin((x * 0.4 + y * 0.6) * 0.07 + t * 0.33) + - Math.sin(radius * 0.1 - t * 0.6)) / - 8 + - 0.5 - ); - }; - - const projectTorus = () => { - depth.fill(0); - shade.fill(-1); - - const sinA = Math.sin(angleA); - const cosA = Math.cos(angleA); - const sinB = Math.sin(angleB); - const cosB = Math.cos(angleB); - - const centerX = columns / 2; - const centerY = rows / 2; - // Size the torus against the shorter axis so it always fits on screen. - const span = Math.min(rows, columns * ASPECT) * 0.6; - const K1 = (span * K2) / (R1 + R2); - - for (let i = 0; i < THETA_STEPS; i += 1) { - const ct = thetaCos[i]; - const st = thetaSin[i]; - const circleX = R2 + R1 * ct; - const circleY = R1 * st; - - for (let j = 0; j < PHI_STEPS; j += 1) { - const cp = phiCos[j]; - const sp = phiSin[j]; - - const x = circleX * (cosB * cp + sinA * sinB * sp) - circleY * cosA * sinB; - const y = circleX * (sinB * cp - sinA * cosB * sp) + circleY * cosA * cosB; - const z = K2 + cosA * circleX * sp + circleY * sinA; - const ooz = 1 / z; - - const screenX = Math.round(centerX + K1 * ooz * x); - const screenY = Math.round(centerY - K1 * ooz * y * ASPECT); - - if (screenX < 0 || screenX >= columns || screenY < 0 || screenY >= rows) { - continue; - } - - const luminance = - cp * ct * sinB - - cosA * ct * sp - - sinA * st + - cosB * (cosA * st - ct * sinA * sp); - - if (luminance <= 0) { - continue; - } - - const index = screenY * columns + screenX; - - if (ooz > depth[index]) { - depth[index] = ooz; - shade[index] = Math.min(RAMP.length - 1, Math.floor(luminance * 8)); - } - } - } + buildDither(); }; const draw = () => { + const width = canvas.width; + const height = canvas.height; + context.save(); context.setTransform(1, 0, 0, 1, 0, 0); - context.clearRect(0, 0, canvas.width, canvas.height); + context.clearRect(0, 0, width, height); context.restore(); - projectTorus(); - let currentColor = null; - const setColor = (color) => { - if (color !== currentColor) { - context.fillStyle = color; - currentColor = color; - } - }; for (let row = 0; row < rows; row += 1) { for (let column = 0; column < columns; column += 1) { - const index = row * columns + column; - const lit = shade[index]; - const x = column * CELL_WIDTH; - const y = row * CELL_HEIGHT; + const level = waveAt(column, row, time); - if (lit >= 0) { - const level = lit / (RAMP.length - 1); - setColor(level > 0.72 ? accentColor : baseColor); - context.globalAlpha = 0.22 + level * 0.62; - context.fillText(RAMP[lit], x, y); + if (dither[row * columns + column] > BASE_DENSITY + level * WAVE_DENSITY) { continue; } - const swell = swellAt(column, row, time); + const glyph = CHARS[Math.min(CHARS.length - 1, Math.floor(level * CHARS.length))]; + const color = level > 0.9 ? accentColor : baseColor; - if (dither[index] > DUST_BASE + swell * DUST_WAVE) { - continue; + if (color !== currentColor) { + context.fillStyle = color; + currentColor = color; } - setColor(dimColor); - context.globalAlpha = 0.1 + swell * 0.4; - context.fillText(DUST[Math.min(DUST.length - 1, Math.floor(swell * DUST.length))], x, y); + context.globalAlpha = 0.06 + level * 0.5; + context.fillText(glyph, column * CELL_WIDTH, row * CELL_HEIGHT); } } @@ -221,9 +120,7 @@ } lastFrame = timestamp; - time += 0.016; - angleA += 0.0115; - angleB += 0.0062; + time += 0.021; draw(); };