"use client"; import * as React from "react"; export interface ContourFieldProps { /** primary duotone colour (hex) */ colorA?: string; /** secondary duotone colour (hex) */ colorB?: string; /** field zoom — bigger = finer detail */ scale?: number; /** animation speed */ speed?: number; /** domain-warp strength */ warp?: number; /** number of contour bands */ lines?: number; /** line thickness (0..1) */ thickness?: number; /** overall opacity */ intensity?: number; className?: string; style?: React.CSSProperties; } const FRAG = `#version 300 es precision highp float; out vec4 frag; uniform vec2 uRes; uniform float uTime; uniform vec3 uColA; uniform vec3 uColB; uniform float uScale, uSpeed, uWarp, uLines, uThick, uIntensity; float hash(vec2 p){ p=fract(p*vec2(123.34,345.45)); p+=dot(p,p+34.345); return fract(p.x*p.y); } 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.-2.*f); return mix(mix(a,b,u.x),mix(c,d,u.x),u.y); } float fbm(vec2 p){ float v=0.,a=0.5; for(int i=0;i<6;i++){ v+=a*noise(p); p=p*2.0+vec2(1.7,9.2); a*=0.5; } return v; } void main(){ vec2 uv=(gl_FragCoord.xy-0.5*uRes)/uRes.y; float t=uTime*uSpeed; vec2 p=uv*uScale; vec2 q=vec2(fbm(p+vec2(0.0,0.3*t)), fbm(p+vec2(5.2,1.3)-vec2(0.0,0.22*t))); float f=fbm(p+uWarp*q+0.1*t); float bands=f*uLines; float edge=abs(fract(bands)-0.5)*2.0; float line=smoothstep(uThick+0.05, uThick, edge); float glow=smoothstep(uThick+0.35, uThick, edge)*0.25; vec3 col=mix(uColA,uColB,smoothstep(0.2,0.8,f)); float a=(line+glow)*uIntensity; frag=vec4(col, clamp(a,0.0,1.0)); }`; const VERT = `#version 300 es void main(){ vec2 v[3]=vec2[3](vec2(-1,-1),vec2(3,-1),vec2(-1,3)); gl_Position=vec4(v[gl_VertexID],0,1); }`; function hexToRgb(hex: string): [number, number, number] { const h = hex.replace("#", ""); const n = h.length === 3 ? h.split("").map((c) => c + c).join("") : h; return [parseInt(n.slice(0, 2), 16) / 255, parseInt(n.slice(2, 4), 16) / 255, parseInt(n.slice(4, 6), 16) / 255]; } export function ContourField({ colorA = "#c2f53b", colorB = "#ff2e43", scale = 2.4, speed = 0.5, warp = 1.4, lines = 7, thickness = 0.06, intensity = 1, className, style, }: ContourFieldProps) { const ref = React.useRef(null); const props = React.useRef({ colorA, colorB, scale, speed, warp, lines, thickness, intensity }); props.current = { colorA, colorB, scale, speed, warp, lines, thickness, intensity }; React.useEffect(() => { const canvas = ref.current; if (!canvas) return; const gl = canvas.getContext("webgl2", { alpha: true, antialias: true, premultipliedAlpha: false }); if (!gl) return; const compile = (type: number, src: string) => { const s = gl.createShader(type)!; gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s) || "shader error"); return s; }; const prog = gl.createProgram()!; gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT)); gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG)); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog) || "link error"); gl.useProgram(prog); const U = (n: string) => gl.getUniformLocation(prog, n); const uRes = U("uRes"), uTime = U("uTime"), uColA = U("uColA"), uColB = U("uColB"); const uScale = U("uScale"), uSpeed = U("uSpeed"), uWarp = U("uWarp"), uLines = U("uLines"), uThick = U("uThick"), uInt = U("uIntensity"); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); const resize = () => { const dpr = Math.min(window.devicePixelRatio || 1, 2); const w = Math.max(1, Math.floor(canvas.clientWidth * dpr)); const h = Math.max(1, Math.floor(canvas.clientHeight * dpr)); if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; } gl.viewport(0, 0, canvas.width, canvas.height); }; const ro = new ResizeObserver(resize); ro.observe(canvas); resize(); let raf = 0; const start = performance.now(); const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const draw = (now: number) => { const p = props.current; resize(); gl.uniform2f(uRes, canvas.width, canvas.height); gl.uniform1f(uTime, reduce ? 0 : (now - start) / 1000); gl.uniform3fv(uColA, hexToRgb(p.colorA)); gl.uniform3fv(uColB, hexToRgb(p.colorB)); gl.uniform1f(uScale, p.scale); gl.uniform1f(uSpeed, p.speed); gl.uniform1f(uWarp, p.warp); gl.uniform1f(uLines, p.lines); gl.uniform1f(uThick, p.thickness); gl.uniform1f(uInt, p.intensity); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, 3); raf = reduce ? 0 : requestAnimationFrame(draw); }; raf = requestAnimationFrame(draw); return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension("WEBGL_lose_context")?.loseContext(); }; }, []); return ; }