diff --git a/app.js b/app.js index cbc33f1..bf0813c 100644 --- a/app.js +++ b/app.js @@ -667,6 +667,158 @@ async function updateHeatmap() { updateHeatmap(); setInterval(updateHeatmap, HEATMAP_REFRESH_MS); +// === TIMMY SIGIL === +// Animated sacred geometry on the platform floor — concentric rings, Star of David, +// Flower of Life petals, and rotating inner hex that glow with the Nexus accent color. + +const SigilShader = { + uniforms: { + uTime: { value: 0.0 }, + uColor: { value: new THREE.Color(NEXUS.colors.accent) }, + uAlpha: { value: 0.72 }, + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform float uTime; + uniform vec3 uColor; + uniform float uAlpha; + varying vec2 vUv; + + // signed-distance circle + float sdCircle(vec2 p, float r) { return length(p) - r; } + + // 0..1 anti-aliased ring + float ring(vec2 p, float r, float width) { + float d = abs(sdCircle(p, r)) - width * 0.5; + return 1.0 - smoothstep(0.0, 0.003, d); + } + + // line (anti-aliased) + float line(vec2 p, vec2 a, vec2 b, float hw) { + vec2 pa = p - a, ba = b - a; + float t = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + return 1.0 - smoothstep(0.0, 0.003, length(pa - ba * t) - hw); + } + + // equilateral triangle outline (star component) + float triangle(vec2 p, float r, float hw) { + float v = 0.0; + for (int i = 0; i < 3; i++) { + float a0 = float(i) * 2.094395; // 120° + float a1 = float(i + 1) * 2.094395; + vec2 va = vec2(cos(a0), sin(a0)) * r; + vec2 vb = vec2(cos(a1), sin(a1)) * r; + v = max(v, line(p, va, vb, hw)); + } + return v; + } + + void main() { + vec2 uv = vUv * 2.0 - 1.0; // [-1, 1] + float dist = length(uv); + + // Clip to circle + if (dist > 0.98) discard; + + float slow = uTime * 0.18; + float med = uTime * 0.42; + + // --- Slowly-rotating coordinate frames --- + float cs1 = cos(slow), ss1 = sin(slow); + float cs2 = cos(-slow * 1.3), ss2 = sin(-slow * 1.3); + vec2 r1 = vec2(uv.x * cs1 - uv.y * ss1, uv.x * ss1 + uv.y * cs1); + vec2 r2 = vec2(uv.x * cs2 - uv.y * ss2, uv.x * ss2 + uv.y * cs2); + + float g = 0.0; + + // --- Concentric rings (static) --- + g += ring(uv, 0.92, 0.012) * 0.8; + g += ring(uv, 0.76, 0.008) * 0.65; + g += ring(uv, 0.56, 0.007) * 0.55; + g += ring(uv, 0.35, 0.006) * 0.50; + g += ring(uv, 0.16, 0.005) * 0.45; + + // --- Star of David: two counter-rotating triangles --- + g += triangle(r1, 0.68, 0.007) * 0.9; + g += triangle(r2, 0.68, 0.007) * 0.9; + + // --- Inner Star of David (smaller, faster counter-rotation) --- + float cs3 = cos(med), ss3 = sin(med); + float cs4 = cos(-med), ss4 = sin(-med); + vec2 r3 = vec2(uv.x * cs3 - uv.y * ss3, uv.x * ss3 + uv.y * cs3); + vec2 r4 = vec2(uv.x * cs4 - uv.y * ss4, uv.x * ss4 + uv.y * cs4); + g += triangle(r3, 0.43, 0.006) * 0.75; + g += triangle(r4, 0.43, 0.006) * 0.75; + + // --- Flower of Life: 7 circles (centre + 6 petals) --- + float flowerR = 0.30; + float petalOffset = 0.30; + g += ring(uv, flowerR, 0.005) * 0.5; + for (int k = 0; k < 6; k++) { + float ang = float(k) * 1.0472 + slow * 0.5; // 60° steps, gentle rotation + vec2 center = vec2(cos(ang), sin(ang)) * petalOffset; + g += ring(uv - center, flowerR, 0.004) * 0.45; + } + + // --- 12 radial spokes --- + for (int k = 0; k < 12; k++) { + float ang = float(k) * 0.5236 + slow; // 30° steps + vec2 dir = vec2(cos(ang), sin(ang)); + vec2 inner = dir * 0.17; + vec2 outer = dir * 0.91; + g += line(uv, inner, outer, 0.0025) * 0.35; + } + + // --- Tiny dots at 12 spoke tips --- + for (int k = 0; k < 12; k++) { + float ang = float(k) * 0.5236 + slow; + vec2 tip = vec2(cos(ang), sin(ang)) * 0.91; + g += (1.0 - smoothstep(0.0, 0.018, length(uv - tip))) * 0.8; + } + + // --- Pulsing bright center dot --- + float pulse = 0.55 + 0.45 * sin(uTime * 2.1); + g += (1.0 - smoothstep(0.0, 0.045, dist)) * pulse * 1.2; + + // --- Edge soft fade --- + g *= 1.0 - smoothstep(0.85, 0.98, dist); + + // --- Breathing overall brightness --- + float breath = 0.82 + 0.18 * sin(uTime * 0.9); + + float alpha = clamp(g * breath, 0.0, 1.0) * uAlpha; + if (alpha < 0.005) discard; + + gl_FragColor = vec4(uColor * (0.7 + g * 0.8), alpha); + } + `, +}; + +const sigilMaterial = new THREE.ShaderMaterial({ + uniforms: SigilShader.uniforms, + vertexShader: SigilShader.vertexShader, + fragmentShader: SigilShader.fragmentShader, + transparent: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + side: THREE.DoubleSide, +}); + +const sigilMesh = new THREE.Mesh( + new THREE.CircleGeometry(4.4, 128), + sigilMaterial +); +sigilMesh.rotation.x = -Math.PI / 2; +sigilMesh.position.y = 0.007; // just above the heatmap layer +sigilMesh.userData.zoomLabel = 'Timmy Sigil'; +scene.add(sigilMesh); + // === MOUSE-DRIVEN ROTATION === let mouseX = 0; let mouseY = 0; @@ -1258,6 +1410,9 @@ function animate() { // Animate procedural clouds cloudMaterial.uniforms.uTime.value = elapsed; + // Animate Timmy sigil — time drives all rotation & pulse in shader + sigilMaterial.uniforms.uTime.value = elapsed; + if (photoMode) { orbitControls.update(); }