AkurAI Build
Menu

Public repository / fee5b6c2cde89fa6bf8dc51068af7336b56a88b8

AkurAI-Framework / site/frontend/hero3d.js

Latest change edb63febb8db14f87cc49d41b9548f661921d568 - feat(hero): lightweight animated gradient hero background (Stripe-style) by Ólafur Búi Ólafsson

/* hero3d.js — lightweight animated gradient hero background.
 *
 * Mimics the Stripe / whatamesh "mesh gradient" technique (Ashima simplex noise
 * driving colour layers blended over a base), reimplemented as a single full-screen
 * fragment-shader quad — GPU-only, no geometry density, no post-processing. Deep
 * brand palette so light hero text stays readable.
 *
 * Self-hosted Three.js (vendored under /vendor/three, no CDN). Non-blocking: skips
 * silently without WebGL2 (CSS gradient remains), renders a single static frame under
 * prefers-reduced-motion, and pauses when scrolled out of view.
 *
 * Reference: Stripe gradient (Nick Jones) / whatamesh; Ashima Arts simplex noise (MIT).
 */
import * as THREE from '/vendor/three/three.module.js';

const canvas = document.getElementById('hero-bg');
const webgl2 = (() => { try { return !!document.createElement('canvas').getContext('webgl2'); }
  catch { return false; } })();

if (canvas && webgl2) boot();

function boot() {
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const host = canvas.parentElement;

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
  renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));

  const scene = new THREE.Scene();
  const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

  const uniforms = {
    u_time: { value: 0 },
    u_aspect: { value: 1 },
    // deep, muted brand palette — premium, not neon; keeps light text legible
    c0: { value: new THREE.Color(0x070b16) }, // base — deep navy
    c1: { value: new THREE.Color(0x214099) }, // blue   #5b8cff, darkened
    c2: { value: new THREE.Color(0x3b2a86) }, // purple #8a6bff, darkened
    c3: { value: new THREE.Color(0x125446) }, // mint   #7af0d0, darkened
  };

  const material = new THREE.ShaderMaterial({
    uniforms,
    vertexShader: /* glsl */`
      varying vec2 vUv;
      void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }
    `,
    fragmentShader: /* glsl */`
      precision highp float;
      varying vec2 vUv;
      uniform float u_time, u_aspect;
      uniform vec3 c0, c1, c2, c3;

      // Ashima Arts simplex noise 3D (MIT)
      vec4 permute(vec4 x){ return mod(((x*34.0)+1.0)*x, 289.0); }
      vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }
      float snoise(vec3 v){
        const vec2 C = vec2(1.0/6.0, 1.0/3.0);
        const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
        vec3 i  = floor(v + dot(v, C.yyy));
        vec3 x0 = v - i + dot(i, C.xxx);
        vec3 g = step(x0.yzx, x0.xyz);
        vec3 l = 1.0 - g;
        vec3 i1 = min(g.xyz, l.zxy);
        vec3 i2 = max(g.xyz, l.zxy);
        vec3 x1 = x0 - i1 + 1.0 * C.xxx;
        vec3 x2 = x0 - i2 + 2.0 * C.xxx;
        vec3 x3 = x0 - 1.0 + 3.0 * C.xxx;
        i = mod(i, 289.0);
        vec4 p = permute(permute(permute(
            i.z + vec4(0.0, i1.z, i2.z, 1.0))
          + i.y + vec4(0.0, i1.y, i2.y, 1.0))
          + i.x + vec4(0.0, i1.x, i2.x, 1.0));
        float n_ = 1.0/7.0;
        vec3 ns = n_ * D.wyz - D.xzx;
        vec4 j = p - 49.0 * floor(p * ns.z *ns.z);
        vec4 x_ = floor(j * ns.z);
        vec4 y_ = floor(j - 7.0 * x_);
        vec4 x = x_ *ns.x + ns.yyyy;
        vec4 y = y_ *ns.x + ns.yyyy;
        vec4 h = 1.0 - abs(x) - abs(y);
        vec4 b0 = vec4(x.xy, y.xy);
        vec4 b1 = vec4(x.zw, y.zw);
        vec4 s0 = floor(b0)*2.0 + 1.0;
        vec4 s1 = floor(b1)*2.0 + 1.0;
        vec4 sh = -step(h, vec4(0.0));
        vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
        vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
        vec3 p0 = vec3(a0.xy, h.x);
        vec3 p1 = vec3(a0.zw, h.y);
        vec3 p2 = vec3(a1.xy, h.z);
        vec3 p3 = vec3(a1.zw, h.w);
        vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
        p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
        vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
        m = m * m;
        return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
      }

      vec3 blend(vec3 base, vec3 c, float o){ return mix(base, c, clamp(o, 0.0, 1.0)); }

      // one Stripe-style colour layer: simplex → smoothstep → pow → opacity
      float layer(vec2 uv, vec2 freq, float flow, float speed, float seed, float t){
        float n = snoise(vec3(uv.x*freq.x + t*flow, uv.y*freq.y, t*speed + seed));
        return smoothstep(0.0, 1.0, n*0.5 + 0.5);
      }

      void main(){
        vec2 uv = vUv;
        uv.x *= u_aspect;            // keep the noise from stretching wide
        float t = u_time * 0.05;

        vec3 col = c0;
        col = blend(col, c1, pow(layer(uv, vec2(1.8, 2.6),  0.22, 0.16,  0.0, t), 3.0));
        col = blend(col, c2, pow(layer(uv, vec2(2.7, 1.9), -0.18, 0.20, 13.0, t), 3.0));
        col = blend(col, c3, pow(layer(uv, vec2(1.4, 2.3),  0.14, 0.13, 31.0, t), 4.0));

        // gentle top-darken + vignette so hero copy keeps contrast
        col -= pow(vUv.y, 2.2) * 0.05;
        col -= smoothstep(0.45, 1.1, distance(vUv, vec2(0.5))) * 0.06;
        gl_FragColor = vec4(col, 1.0);
      }
    `,
  });

  scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));

  function resize() {
    const w = host.clientWidth || innerWidth;
    const h = host.clientHeight || 520;
    renderer.setSize(w, h, false);
    uniforms.u_aspect.value = w / h;
  }
  resize();
  new ResizeObserver(resize).observe(host);

  const timer = new THREE.Timer();
  let visible = true;
  new IntersectionObserver(([e]) => { visible = e.isIntersecting; },
    { threshold: 0.01 }).observe(host);

  if (reduce) { uniforms.u_time.value = 8.0; renderer.render(scene, camera); return; }

  renderer.setAnimationLoop((ts) => {
    if (!visible) return;
    timer.update(ts);
    uniforms.u_time.value = timer.getElapsed();
    renderer.render(scene, camera);
  });
}