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:
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">