stenodojo

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

neu-fx.js (7119B)


      1 (function() {
      2   'use strict';
      3 
      4   var root = document.documentElement;
      5   var rootStyle = root.style;
      6   var tmx = 0.5, tmy = 0.5, smx = 0.5, smy = 0.5;
      7 
      8   // ===== WebGL Background =====
      9   var canvas = document.createElement('canvas');
     10   canvas.style.cssText = 'position:fixed;inset:0;z-index:-1;width:100%;height:100%';
     11   document.body.prepend(canvas);
     12 
     13   var gl = canvas.getContext('webgl', { alpha: false, antialias: false });
     14   if (!gl) { canvas.remove(); return; }
     15 
     16   document.body.style.background = 'none';
     17 
     18   var VERT = 'attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}';
     19   var FRAG = [
     20     'precision mediump float;',
     21     'uniform float t;',
     22     'uniform vec2 r,m;',
     23     'uniform vec3 cb,cl,cd,ca,cm,ct;',
     24     '',
     25     'float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}',
     26     'float noise(vec2 p){',
     27     '  vec2 i=floor(p),f=fract(p);',
     28     '  f=f*f*(3.-2.*f);',
     29     '  return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),',
     30     '             mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);',
     31     '}',
     32     '',
     33     'void main(){',
     34     '  vec2 u=gl_FragCoord.xy/r;',
     35     '  float n=noise(u*3.+t*.08)*.015;',
     36     '  vec2 du=u+n;',
     37     '  vec2 d=(du-vec2(.5,.85))*vec2(1.,r.x/r.y);',
     38     '  float dist=length(d);',
     39     '  vec3 c=mix(cl,cb,smoothstep(.0,.35,dist));',
     40     '  c=mix(c,cd,smoothstep(.35,.65,dist));',
     41     '  vec2 p1=vec2(.25+sin(t*.2)*.2,.7+cos(t*.15)*.2);',
     42     '  vec2 p2=vec2(.75+cos(t*.18)*.15,.3+sin(t*.22)*.15);',
     43     '  vec2 p3=vec2(.5+sin(t*.12)*.25,.5+cos(t*.1)*.2);',
     44     '  c+=ca*exp(-8.*length(u-p1))*.08;',
     45     '  c+=cm*exp(-10.*length(u-p2))*.05;',
     46     '  c+=ct*exp(-7.*length(u-p3))*.04;',
     47     '  c+=ca*exp(-6.*length(u-m))*.05;',
     48     '  float vig=1.-smoothstep(.4,1.2,length(u-.5));',
     49     '  c*=.88+.12*vig;',
     50     '  gl_FragColor=vec4(c,1.);',
     51     '}'
     52   ].join('\n');
     53 
     54   function mk(type, src) {
     55     var s = gl.createShader(type);
     56     gl.shaderSource(s, src);
     57     gl.compileShader(s);
     58     return s;
     59   }
     60 
     61   var prog = gl.createProgram();
     62   gl.attachShader(prog, mk(gl.VERTEX_SHADER, VERT));
     63   gl.attachShader(prog, mk(gl.FRAGMENT_SHADER, FRAG));
     64   gl.linkProgram(prog);
     65   gl.useProgram(prog);
     66 
     67   var buf = gl.createBuffer();
     68   gl.bindBuffer(gl.ARRAY_BUFFER, buf);
     69   gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
     70   var a = gl.getAttribLocation(prog, 'p');
     71   gl.enableVertexAttribArray(a);
     72   gl.vertexAttribPointer(a, 2, gl.FLOAT, false, 0, 0);
     73 
     74   var uf = function(n) { return gl.getUniformLocation(prog, n); };
     75   var uT = uf('t'), uR = uf('r'), uM = uf('m');
     76   var uCB = uf('cb'), uCL = uf('cl'), uCD = uf('cd');
     77   var uCA = uf('ca'), uCM = uf('cm'), uCT = uf('ct');
     78 
     79   var raf;
     80 
     81   function hex(h) {
     82     var n = parseInt(h.replace('#', ''), 16);
     83     return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
     84   }
     85 
     86   function hexToRgb(h) {
     87     return [parseInt(h.slice(1,3),16), parseInt(h.slice(3,5),16), parseInt(h.slice(5,7),16)];
     88   }
     89 
     90   function syncColors() {
     91     var s = getComputedStyle(root);
     92     var g = function(v) { return hex(s.getPropertyValue(v).trim()); };
     93     gl.uniform3fv(uCB, g('--base'));
     94     gl.uniform3fv(uCL, g('--base-light'));
     95     gl.uniform3fv(uCD, g('--base-dark'));
     96     gl.uniform3fv(uCA, g('--accent'));
     97     gl.uniform3fv(uCM, g('--magenta'));
     98     gl.uniform3fv(uCT, g('--teal'));
     99     var rgb = hexToRgb(s.getPropertyValue('--accent').trim());
    100     spot.style.background = 'radial-gradient(circle,rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.07) 0%,transparent 70%)';
    101   }
    102 
    103   function resize() {
    104     var dpr = Math.min(devicePixelRatio, 1.5);
    105     canvas.width = innerWidth * dpr;
    106     canvas.height = innerHeight * dpr;
    107     gl.viewport(0, 0, canvas.width, canvas.height);
    108   }
    109 
    110   addEventListener('resize', resize);
    111   resize();
    112 
    113   new MutationObserver(syncColors).observe(
    114     root, { attributes: true, attributeFilter: ['data-theme'] }
    115   );
    116 
    117   function loop(t) {
    118     smx += (tmx - smx) * 0.03;
    119     smy += (tmy - smy) * 0.03;
    120     gl.uniform1f(uT, t * .001);
    121     gl.uniform2f(uR, canvas.width, canvas.height);
    122     gl.uniform2f(uM, smx, smy);
    123     gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    124     raf = requestAnimationFrame(loop);
    125   }
    126 
    127   // ===== Cursor Spotlight =====
    128   var spot = document.createElement('div');
    129   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';
    130   document.body.appendChild(spot);
    131 
    132   syncColors();
    133   raf = requestAnimationFrame(loop);
    134 
    135   document.addEventListener('visibilitychange', function() {
    136     if (document.hidden) cancelAnimationFrame(raf);
    137     else raf = requestAnimationFrame(loop);
    138   });
    139 
    140   // ===== Unified Mouse Handler =====
    141   var lastTilt = null;
    142   var queued = false;
    143 
    144   document.addEventListener('mousemove', function(e) {
    145     var nx = e.clientX / innerWidth;
    146     var ny = e.clientY / innerHeight;
    147     tmx = nx;
    148     tmy = 1 - ny;
    149 
    150     spot.style.left = e.clientX + 'px';
    151     spot.style.top = e.clientY + 'px';
    152     spot.style.opacity = '1';
    153 
    154     var el = e.target.closest('.stat-card, .mode-card');
    155     if (lastTilt && lastTilt !== el) {
    156       lastTilt.style.removeProperty('--ry');
    157       lastTilt.style.removeProperty('--rx');
    158     }
    159     lastTilt = el;
    160     if (el) {
    161       var rect = el.getBoundingClientRect();
    162       var cx = (e.clientX - rect.left) / rect.width - 0.5;
    163       var cy = (e.clientY - rect.top) / rect.height - 0.5;
    164       el.style.setProperty('--ry', (cx * 5) + 'deg');
    165       el.style.setProperty('--rx', (-cy * 5) + 'deg');
    166     }
    167 
    168     if (queued) return;
    169     queued = true;
    170     requestAnimationFrame(function() {
    171       queued = false;
    172       var lx = (nx - .5) * 2, ly = (ny - .5) * 2;
    173       var v = function(o, b) {
    174         return (-lx*o).toFixed(1) + 'px ' + (-ly*o).toFixed(1) + 'px ' + b + 'px var(--shadow-dark),' +
    175                (lx*o).toFixed(1) + 'px ' + (ly*o).toFixed(1) + 'px ' + b + 'px var(--shadow-light)';
    176       };
    177       rootStyle.setProperty('--neu-raised', v(4, 10));
    178       rootStyle.setProperty('--neu-raised-sm', v(3, 7));
    179       rootStyle.setProperty('--neu-raised-lg', v(6, 14));
    180     });
    181   });
    182 
    183   document.addEventListener('mouseleave', function() {
    184     spot.style.opacity = '0';
    185     if (lastTilt) {
    186       lastTilt.style.removeProperty('--ry');
    187       lastTilt.style.removeProperty('--rx');
    188       lastTilt = null;
    189     }
    190     rootStyle.removeProperty('--neu-raised');
    191     rootStyle.removeProperty('--neu-raised-sm');
    192     rootStyle.removeProperty('--neu-raised-lg');
    193   });
    194 
    195   // ===== Grain Texture =====
    196   var gc = document.createElement('canvas');
    197   gc.width = gc.height = 128;
    198   var gx = gc.getContext('2d');
    199   var id = gx.createImageData(128, 128);
    200   for (var i = 0; i < id.data.length; i += 4) {
    201     id.data[i] = id.data[i+1] = id.data[i+2] = Math.random() * 255 | 0;
    202     id.data[i+3] = 18;
    203   }
    204   gx.putImageData(id, 0, 0);
    205   var ov = document.createElement('div');
    206   ov.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999;background:url(' + gc.toDataURL() + ');mix-blend-mode:overlay;opacity:.5';
    207   document.body.appendChild(ov);
    208 })();