1. Mechanisme — kopieer 1-op-1, geen styling-keuzes
// Mechanisme: cta-shader-orb-kinetic
// OGL fullscreen-quad shader met snel-pulserende noise + chromatic shift.
// uTime via rAF, uMouse drijft distortion. GSAP intro op CTA-text.
// Fallback: CSS conic-gradient pulse als WebGL faalt of reduced-motion.
import gsap from 'https://esm.sh/gsap@3.12.5';
import { Renderer, Program, Mesh, Triangle } from 'https://esm.sh/ogl@1.0.10';
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const root = document.querySelector('.shader-orb-kinetic');
if (root) {
const canvas = root.querySelector('canvas');
const txt = root.querySelectorAll('.kinetic-word');
// Intro
if (!reduce) gsap.from(txt, { yPercent: 110, autoAlpha: 0, duration: 0.8, stagger: 0.04, ease: 'back.out(1.4)' });
let useGL = false;
try {
const renderer = new Renderer({ canvas, dpr: Math.min(window.devicePixelRatio, 2), alpha: true });
const gl = renderer.gl;
useGL = !!gl;
const geom = new Triangle(gl);
const program = new Program(gl, {
vertex: `attribute vec2 position; void main(){ gl_Position = vec4(position,0.0,1.0); }`,
fragment: `precision highp float;
uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
// simplex-ish hash noise
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); float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1)); vec2 u=f*f*(3.0-2.0*f); return mix(a,b,u.x)+(c-a)*u.y*(1.0-u.x)+(d-b)*u.x*u.y; }
void main(){
vec2 uv = (gl_FragCoord.xy - 0.5*uRes) / min(uRes.x,uRes.y);
vec2 m = (uMouse - 0.5*uRes) / min(uRes.x,uRes.y);
float d = length(uv - m*0.4);
float pulse = 0.5 + 0.5*sin(uTime*3.2);
float n = noise(uv*3.0 + uTime*0.9);
float ring = smoothstep(0.55,0.0, abs(d - 0.32 - 0.04*pulse - 0.06*n));
float core = smoothstep(0.45,0.0, d - 0.05*n);
// chromatic shift
float r = smoothstep(0.5,0.0, length(uv - m*0.4 + vec2(0.012*pulse,0.0)) - 0.05*n);
float g = core;
float b = smoothstep(0.5,0.0, length(uv - m*0.4 - vec2(0.012*pulse,0.0)) - 0.05*n);
vec3 cream = vec3(0.957,0.945,0.922);
vec3 orange = vec3(1.0,0.290,0.110);
vec3 ink = vec3(0.039,0.039,0.039);
vec3 col = mix(ink, orange, r*0.9 + ring*0.6);
col = mix(col, cream, g*0.35);
col += vec3(b*0.15, 0.0, 0.0);
col = mix(ink, col, smoothstep(0.0,0.05, core+ring*0.5));
gl_FragColor = vec4(col, 1.0);
}`,
uniforms: { uTime:{value:0}, uRes:{value:[1,1]}, uMouse:{value:[0,0]} }
});
const mesh = new Mesh(gl, { geometry: geom, program });
function resize(){ const r = canvas.getBoundingClientRect(); renderer.setSize(r.width, r.height); program.uniforms.uRes.value = [gl.canvas.width, gl.canvas.height]; }
resize(); addEventListener('resize', resize);
canvas.addEventListener('pointermove', e=>{ const r=canvas.getBoundingClientRect(); program.uniforms.uMouse.value = [(e.clientX-r.left)*renderer.dpr, (r.height-(e.clientY-r.top))*renderer.dpr]; });
let raf; const start = performance.now();
function loop(t){ program.uniforms.uTime.value = (t-start)/1000; renderer.render({ scene: mesh }); raf = requestAnimationFrame(loop); }
if (!reduce) raf = requestAnimationFrame(loop); else renderer.render({ scene: mesh });
} catch(e){ useGL = false; }
if (!useGL) root.classList.add('no-webgl');
}