stenodojo

stenodojo
git clone git@git.zachrice.app:repos/stenodojo.git
Log | Files | Refs

commit fbbf974a30107958d9583349f148d9216344f127
parent 79b4510b1c7fb16d036dedbb7941febc66d3fe14
Author: Zach Rice <bynxmusic@gmail.com>
Date:   Thu, 28 May 2026 18:43:01 -0400

Add WebGL neumorphic effects and widen layout for 4K

Introduces dynamic lighting (mouse-tracked shadows, cursor spotlight,
3D card tilt), animated WebGL background with noise and vignette,
glassmorphism navbar, surface gradients with edge highlights, and grain
texture overlay. Bumps max-width from 960px to 1200px across all pages.

Diffstat:
Mstatic/css/style.css | 52+++++++++++++++++++++++++++++++++++++++++-----------
Astatic/js/neu-fx.js | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtemplates/base.html | 1+
3 files changed, 250 insertions(+), 11 deletions(-)

diff --git a/static/css/style.css b/static/css/style.css @@ -55,6 +55,8 @@ --cyan: #7dcfff; --magenta: #bb9af7; --teal: #73daca; + --edge-highlight: rgba(255, 255, 255, 0.05); + --navbar-bg: rgba(36, 40, 59, 0.7); } [data-theme="light"] { @@ -94,6 +96,8 @@ --cyan: #007197; --magenta: #7847bd; --teal: #387068; + --edge-highlight: rgba(255, 255, 255, 0.5); + --navbar-bg: rgba(213, 214, 219, 0.7); } html { @@ -125,10 +129,14 @@ a:hover { color: var(--text-primary); } position: sticky; top: 0; z-index: 100; + background: var(--navbar-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid var(--edge-highlight); } .nav-inner { - max-width: 960px; + max-width: 1200px; margin: 0 auto; padding: 0 1.5rem; display: flex; @@ -210,30 +218,39 @@ a:hover { color: var(--text-primary); } /* ===== MAIN CONTENT ===== */ .main-content { - max-width: 960px; + max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem; } /* ===== CARDS ===== */ .card { - background: var(--base); + background: linear-gradient(145deg, var(--base-light), var(--base)); border-radius: var(--radius-md); padding: 1.5rem; box-shadow: var(--neu-raised); + border-top: 1px solid var(--edge-highlight); + border-left: 1px solid var(--edge-highlight); } .stat-card { - background: var(--base); + background: linear-gradient(145deg, var(--base-light), var(--base)); border-radius: var(--radius-md); padding: 1.25rem; text-align: center; transition: all var(--transition); box-shadow: var(--neu-raised); + border-top: 1px solid var(--edge-highlight); + border-left: 1px solid var(--edge-highlight); + --rx: 0deg; + --ry: 0deg; + --lift: 0px; + transform: rotateX(var(--rx)) rotateY(var(--ry)) translateY(var(--lift)); + will-change: transform; } .stat-card:hover { box-shadow: var(--neu-raised-lg); - transform: translateY(-2px); + --lift: -2px; position: relative; z-index: 1; } @@ -491,6 +508,7 @@ a:hover { color: var(--text-primary); } display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.25rem; + perspective: 800px; } .section-title { @@ -606,6 +624,7 @@ a:hover { color: var(--text-primary); } border-radius: var(--radius-lg); padding: 1rem; box-shadow: var(--neu-pressed-deep); + perspective: 800px; } .mode-card { @@ -614,22 +633,31 @@ a:hover { color: var(--text-primary); } align-items: center; text-align: center; padding: 1.25rem 0.75rem; - background: var(--base); + background: linear-gradient(145deg, var(--base-light), var(--base)); border-radius: var(--radius-md); transition: all var(--transition); text-decoration: none; box-shadow: var(--neu-raised-sm); + border-top: 1px solid var(--edge-highlight); + border-left: 1px solid var(--edge-highlight); + --rx: 0deg; + --ry: 0deg; + --lift: 0px; + transform: rotateX(var(--rx)) rotateY(var(--ry)) translateY(var(--lift)); + will-change: transform; } .mode-card:hover { box-shadow: var(--neu-raised); - transform: translateY(-2px); + --lift: -2px; color: var(--text-primary); position: relative; z-index: 1; } .mode-card:active { box-shadow: var(--neu-pressed); - transform: translateY(0); + --lift: 0px; + --rx: 0deg; + --ry: 0deg; } .mode-card__icon { @@ -772,7 +800,7 @@ a:hover { color: var(--text-primary); } flex-direction: column; align-items: stretch; gap: 1.5rem; - max-width: 1000px; + max-width: 1200px; margin: 0 auto; } @@ -1413,7 +1441,7 @@ a:hover { color: var(--text-primary); } .heatmap-legend__cell { width: 12px; height: 12px; border-radius: 3px; } /* ===== SETTINGS PAGE ===== */ -.settings-page { max-width: 700px; } +.settings-page { } .settings-section { background: var(--base); @@ -1676,13 +1704,15 @@ a:hover { color: var(--text-primary); } } .login-card { - background: var(--base); + background: linear-gradient(145deg, var(--base-light), var(--base)); border-radius: var(--radius-lg); padding: 3rem 2.5rem; max-width: 400px; width: 100%; text-align: center; box-shadow: var(--neu-raised-lg); + border-top: 1px solid var(--edge-highlight); + border-left: 1px solid var(--edge-highlight); } .login-brand { diff --git a/static/js/neu-fx.js b/static/js/neu-fx.js @@ -0,0 +1,208 @@ +(function() { + 'use strict'; + + var root = document.documentElement; + var rootStyle = root.style; + var tmx = 0.5, tmy = 0.5, smx = 0.5, smy = 0.5; + + // ===== WebGL Background ===== + var canvas = document.createElement('canvas'); + canvas.style.cssText = 'position:fixed;inset:0;z-index:-1;width:100%;height:100%'; + document.body.prepend(canvas); + + var gl = canvas.getContext('webgl', { alpha: false, antialias: false }); + if (!gl) { canvas.remove(); return; } + + document.body.style.background = 'none'; + + var VERT = 'attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}'; + var FRAG = [ + 'precision mediump float;', + 'uniform float t;', + 'uniform vec2 r,m;', + 'uniform vec3 cb,cl,cd,ca,cm,ct;', + '', + 'float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}', + 'float noise(vec2 p){', + ' vec2 i=floor(p),f=fract(p);', + ' f=f*f*(3.-2.*f);', + ' return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),', + ' mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);', + '}', + '', + 'void main(){', + ' vec2 u=gl_FragCoord.xy/r;', + ' float n=noise(u*3.+t*.08)*.015;', + ' vec2 du=u+n;', + ' vec2 d=(du-vec2(.5,.85))*vec2(1.,r.x/r.y);', + ' float dist=length(d);', + ' vec3 c=mix(cl,cb,smoothstep(.0,.35,dist));', + ' c=mix(c,cd,smoothstep(.35,.65,dist));', + ' vec2 p1=vec2(.25+sin(t*.2)*.2,.7+cos(t*.15)*.2);', + ' vec2 p2=vec2(.75+cos(t*.18)*.15,.3+sin(t*.22)*.15);', + ' vec2 p3=vec2(.5+sin(t*.12)*.25,.5+cos(t*.1)*.2);', + ' c+=ca*exp(-8.*length(u-p1))*.08;', + ' c+=cm*exp(-10.*length(u-p2))*.05;', + ' c+=ct*exp(-7.*length(u-p3))*.04;', + ' c+=ca*exp(-6.*length(u-m))*.05;', + ' float vig=1.-smoothstep(.4,1.2,length(u-.5));', + ' c*=.88+.12*vig;', + ' gl_FragColor=vec4(c,1.);', + '}' + ].join('\n'); + + function mk(type, src) { + var s = gl.createShader(type); + gl.shaderSource(s, src); + gl.compileShader(s); + return s; + } + + var prog = gl.createProgram(); + gl.attachShader(prog, mk(gl.VERTEX_SHADER, VERT)); + gl.attachShader(prog, mk(gl.FRAGMENT_SHADER, FRAG)); + gl.linkProgram(prog); + gl.useProgram(prog); + + var buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW); + var a = gl.getAttribLocation(prog, 'p'); + gl.enableVertexAttribArray(a); + gl.vertexAttribPointer(a, 2, gl.FLOAT, false, 0, 0); + + var uf = function(n) { return gl.getUniformLocation(prog, n); }; + var uT = uf('t'), uR = uf('r'), uM = uf('m'); + var uCB = uf('cb'), uCL = uf('cl'), uCD = uf('cd'); + var uCA = uf('ca'), uCM = uf('cm'), uCT = uf('ct'); + + var raf; + + function hex(h) { + var n = parseInt(h.replace('#', ''), 16); + return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255]; + } + + function hexToRgb(h) { + return [parseInt(h.slice(1,3),16), parseInt(h.slice(3,5),16), parseInt(h.slice(5,7),16)]; + } + + function syncColors() { + var s = getComputedStyle(root); + var g = function(v) { return hex(s.getPropertyValue(v).trim()); }; + gl.uniform3fv(uCB, g('--base')); + gl.uniform3fv(uCL, g('--base-light')); + gl.uniform3fv(uCD, g('--base-dark')); + gl.uniform3fv(uCA, g('--accent')); + gl.uniform3fv(uCM, g('--magenta')); + gl.uniform3fv(uCT, g('--teal')); + var rgb = hexToRgb(s.getPropertyValue('--accent').trim()); + spot.style.background = 'radial-gradient(circle,rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.07) 0%,transparent 70%)'; + } + + function resize() { + var dpr = Math.min(devicePixelRatio, 1.5); + canvas.width = innerWidth * dpr; + canvas.height = innerHeight * dpr; + gl.viewport(0, 0, canvas.width, canvas.height); + } + + addEventListener('resize', resize); + resize(); + + new MutationObserver(syncColors).observe( + root, { attributes: true, attributeFilter: ['data-theme'] } + ); + + function loop(t) { + smx += (tmx - smx) * 0.03; + smy += (tmy - smy) * 0.03; + gl.uniform1f(uT, t * .001); + gl.uniform2f(uR, canvas.width, canvas.height); + gl.uniform2f(uM, smx, smy); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + raf = requestAnimationFrame(loop); + } + + // ===== Cursor Spotlight ===== + var spot = document.createElement('div'); + spot.style.cssText = 'position:fixed;pointer-events:none;z-index:9998;width:500px;height:500px;border-radius:50%;transform:translate(-50%,-50%);will-change:left,top;opacity:0;transition:opacity 0.4s'; + document.body.appendChild(spot); + + syncColors(); + raf = requestAnimationFrame(loop); + + document.addEventListener('visibilitychange', function() { + if (document.hidden) cancelAnimationFrame(raf); + else raf = requestAnimationFrame(loop); + }); + + // ===== Unified Mouse Handler ===== + var lastTilt = null; + var queued = false; + + document.addEventListener('mousemove', function(e) { + var nx = e.clientX / innerWidth; + var ny = e.clientY / innerHeight; + tmx = nx; + tmy = 1 - ny; + + spot.style.left = e.clientX + 'px'; + spot.style.top = e.clientY + 'px'; + spot.style.opacity = '1'; + + var el = e.target.closest('.stat-card, .mode-card'); + if (lastTilt && lastTilt !== el) { + lastTilt.style.removeProperty('--ry'); + lastTilt.style.removeProperty('--rx'); + } + lastTilt = el; + if (el) { + var rect = el.getBoundingClientRect(); + var cx = (e.clientX - rect.left) / rect.width - 0.5; + var cy = (e.clientY - rect.top) / rect.height - 0.5; + el.style.setProperty('--ry', (cx * 5) + 'deg'); + el.style.setProperty('--rx', (-cy * 5) + 'deg'); + } + + if (queued) return; + queued = true; + requestAnimationFrame(function() { + queued = false; + var lx = (nx - .5) * 2, ly = (ny - .5) * 2; + var v = function(o, b) { + return (-lx*o).toFixed(1) + 'px ' + (-ly*o).toFixed(1) + 'px ' + b + 'px var(--shadow-dark),' + + (lx*o).toFixed(1) + 'px ' + (ly*o).toFixed(1) + 'px ' + b + 'px var(--shadow-light)'; + }; + rootStyle.setProperty('--neu-raised', v(4, 10)); + rootStyle.setProperty('--neu-raised-sm', v(3, 7)); + rootStyle.setProperty('--neu-raised-lg', v(6, 14)); + }); + }); + + document.addEventListener('mouseleave', function() { + spot.style.opacity = '0'; + if (lastTilt) { + lastTilt.style.removeProperty('--ry'); + lastTilt.style.removeProperty('--rx'); + lastTilt = null; + } + rootStyle.removeProperty('--neu-raised'); + rootStyle.removeProperty('--neu-raised-sm'); + rootStyle.removeProperty('--neu-raised-lg'); + }); + + // ===== Grain Texture ===== + var gc = document.createElement('canvas'); + gc.width = gc.height = 128; + var gx = gc.getContext('2d'); + var id = gx.createImageData(128, 128); + for (var i = 0; i < id.data.length; i += 4) { + id.data[i] = id.data[i+1] = id.data[i+2] = Math.random() * 255 | 0; + id.data[i+3] = 18; + } + gx.putImageData(id, 0, 0); + var ov = document.createElement('div'); + ov.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999;background:url(' + gc.toDataURL() + ');mix-blend-mode:overlay;opacity:.5'; + document.body.appendChild(ov); +})(); diff --git a/templates/base.html b/templates/base.html @@ -15,6 +15,7 @@ (function(){var m=document.cookie.match(/(?:^| )theme=([^;]+)/);document.documentElement.setAttribute('data-theme',m?m[1]:'dark')})(); </script> <script src="/static/js/app.js?v={{ cache_bust }}" defer></script> + <script src="/static/js/neu-fx.js?v={{ cache_bust }}" defer></script> </head> <body> <nav class="navbar">