{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "waves-three", "title": "Waves Three", "author": "designbycode", "dependencies": [ "three", "clsx", "tailwind-merge" ], "registryDependencies": [], "files": [ { "path": "resources/js/registry/new-york/components/ui/threejs/waves-three.tsx", "content": "import { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\nimport { cn } from '@/lib/utils';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type WaveStyle =\n | 'wireframe' // triangulated mesh (has diagonals — legacy)\n | 'grid' // axis-aligned squares, no diagonals\n | 'dots' // round filled circles at every vertex (shader-based)\n | 'dots-wave' // round dots that scale in size with Z height\n | 'crosses' // small + at every vertex\n | 'diagonal-left' // parallel lines leaning left (\\\\\\)\n | 'diagonal-right' // parallel lines leaning right (///)\n | 'zigzag' // alternating chevron rows\n | 'hexagons' // hexagonal cell grid\n | 'dashes' // dashed horizontal + vertical lines (gaps between cells)\n | 'contour' // topographic iso-lines drawn at fixed Z thresholds\n | 'solid'; // shaded solid surface with lighting\n\nexport interface WavesThreeProps {\n className?: string;\n\n /**\n * Visual style of the wave. See WaveStyle for all options.\n * Default: 'grid'\n */\n style?: WaveStyle;\n\n /**\n * Which lines to draw — applies to 'grid' and 'dashes' styles.\n * - 'both' — horizontal + vertical (default)\n * - 'horizontal' — only lines running left→right\n * - 'vertical' — only lines running top→bottom\n */\n lines?: 'both' | 'horizontal' | 'vertical';\n\n /**\n * CSS/hex color strings blended left→right across the mesh.\n * Minimum 2. Auto-detects dark/light mode if omitted.\n */\n colors?: string[];\n\n /** Camera XYZ position. Default: { x:0, y:0, z:10 } */\n cameraPosition?: { x: number; y: number; z: number };\n\n /** Plane width in world units. Default: 80 */\n planeWidth?: number;\n /** Plane height in world units. Default: 40 */\n planeHeight?: number;\n\n /** Grid columns — higher = denser. Default: 60 */\n segmentsX?: number;\n /** Grid rows. Default: 30 */\n segmentsY?: number;\n\n /** Animation speed multiplier. Default: 1 */\n speed?: number;\n /** Wave peak height. Default: 1.5 */\n amplitude?: number;\n /** Wave spatial density — lower = wider. Default: 0.3 */\n frequency?: number;\n /** Global opacity 0–1. Default: 0.6 */\n opacity?: number;\n /** Pause animation. Default: false */\n paused?: boolean;\n\n /** Mouse influence on wave phase. Default: 2 */\n mouseInfluence?: number;\n /** Mouse influence on mesh tilt. Default: 0.1 */\n mouseRotation?: number;\n\n /**\n * Dot radius in screen pixels — 'dots' and 'dots-wave' styles.\n * Dots are perfectly round via a GLSL discard shader. Default: 3\n */\n dotSize?: number;\n\n /**\n * For 'dots-wave': minimum dot size at wave valleys. Default: 1\n */\n dotSizeMin?: number;\n\n /** Cross arm half-length in world units — 'crosses' style. Default: 0.3 */\n crossSize?: number;\n\n /**\n * Dash fill ratio 0–1 — 'dashes' style.\n * 0.5 = half line, half gap. Default: 0.5\n */\n dashRatio?: number;\n\n /**\n * Number of contour threshold levels — 'contour' style. Default: 6\n */\n contourLevels?: number;\n\n /** High-DPI pixel ratio cap. Default: 2 */\n maxPixelRatio?: number;\n\n /** Called once the renderer and first frame are ready */\n onReady?: () => void;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_LIGHT: string[] = ['#525252', '#525252'];\nconst DEFAULT_DARK: string[] = ['#444444', '#757575'];\n\n// ---------------------------------------------------------------------------\n// Color helpers\n// ---------------------------------------------------------------------------\n\nfunction lerpPalette(t: number, stops: THREE.Color[]): THREE.Color {\n const scaled = Math.max(0, Math.min(1, t)) * (stops.length - 1);\n const lo = Math.floor(scaled);\n const hi = Math.min(lo + 1, stops.length - 1);\n\n return stops[lo].clone().lerp(stops[hi], scaled - lo);\n}\n\nfunction makeColorBuffer(count: number, stops: THREE.Color[]): Float32Array {\n const buf = new Float32Array(count * 3);\n\n for (let i = 0; i < count; i++) {\n const c = lerpPalette(i / Math.max(count - 1, 1), stops);\n buf[i * 3] = c.r;\n buf[i * 3 + 1] = c.g;\n buf[i * 3 + 2] = c.b;\n }\n\n return buf;\n}\n\n// ---------------------------------------------------------------------------\n// Shared vertex-grid builder\n// Returns a flat XY grid of (cols+1)×(rows+1) vertices, Z=0.\n// ---------------------------------------------------------------------------\n\nfunction makeVertexGrid(\n cols: number,\n rows: number,\n w: number,\n h: number,\n): Float32Array {\n const cx = cols + 1;\n const ry = rows + 1;\n const pos = new Float32Array(cx * ry * 3);\n const sx = w / cols;\n const sy = h / rows;\n\n for (let r = 0; r < ry; r++) {\n for (let c = 0; c < cx; c++) {\n const i = (r * cx + c) * 3;\n pos[i] = -w / 2 + c * sx;\n pos[i + 1] = -h / 2 + r * sy;\n pos[i + 2] = 0;\n }\n }\n\n return pos;\n}\n\n// ---------------------------------------------------------------------------\n// Wave Z calculator — used in every style's animation loop\n// ---------------------------------------------------------------------------\n\nfunction calcZ(\n x: number,\n y: number,\n time: number,\n freq: number,\n amp: number,\n mx: number,\n my: number,\n mi: number,\n): number {\n return (\n Math.sin(x * freq + time * 2 + mx * mi) * amp +\n Math.cos(y * freq + time * 1.5 + my * mi)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Geometry builders\n// ---------------------------------------------------------------------------\n\n// GRID — axis-aligned lines only, no diagonals\nfunction buildGrid(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n lines: 'both' | 'horizontal' | 'vertical',\n): { geo: THREE.BufferGeometry; pos: Float32Array } {\n const cx = cols + 1;\n const ry = rows + 1;\n const total = cx * ry;\n const pos = makeVertexGrid(cols, rows, w, h);\n\n const hSegs = lines !== 'vertical' ? ry * cols : 0;\n const vSegs = lines !== 'horizontal' ? cx * rows : 0;\n const idx = new Uint32Array((hSegs + vSegs) * 2);\n let ptr = 0;\n\n if (lines !== 'vertical') {\n for (let r = 0; r < ry; r++) {\n for (let c = 0; c < cols; c++) {\n idx[ptr++] = r * cx + c;\n idx[ptr++] = r * cx + c + 1;\n }\n }\n }\n\n if (lines !== 'horizontal') {\n for (let c = 0; c < cx; c++) {\n for (let r = 0; r < rows; r++) {\n idx[ptr++] = r * cx + c;\n idx[ptr++] = (r + 1) * cx + c;\n }\n }\n }\n\n const geo = new THREE.BufferGeometry();\n geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n geo.setAttribute(\n 'color',\n new THREE.BufferAttribute(makeColorBuffer(total, stops), 3),\n );\n geo.setIndex(new THREE.BufferAttribute(idx, 1));\n\n return { geo, pos };\n}\n\n// DOTS — round circles via ShaderMaterial + gl_PointCoord discard\nfunction buildDots(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n): { geo: THREE.BufferGeometry; pos: Float32Array } {\n const total = (cols + 1) * (rows + 1);\n const pos = makeVertexGrid(cols, rows, w, h);\n const geo = new THREE.BufferGeometry();\n geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n geo.setAttribute(\n 'color',\n new THREE.BufferAttribute(makeColorBuffer(total, stops), 3),\n );\n\n return { geo, pos };\n}\n\n// Round-dot ShaderMaterial — discards fragments outside the circle\nfunction makeRoundDotMaterial(\n size: number,\n opacity: number,\n): THREE.ShaderMaterial {\n return new THREE.ShaderMaterial({\n uniforms: {\n uSize: { value: size },\n uOpacity: { value: opacity },\n },\n vertexShader: /* glsl */ `\n attribute vec3 color;\n varying vec3 vColor;\n uniform float uSize;\n void main() {\n vColor = color;\n vec4 mvPos = modelViewMatrix * vec4(position, 1.0);\n gl_PointSize = uSize;\n gl_Position = projectionMatrix * mvPos;\n }\n `,\n fragmentShader: /* glsl */ `\n varying vec3 vColor;\n uniform float uOpacity;\n void main() {\n // gl_PointCoord is 0..1 across the point sprite\n vec2 uv = gl_PointCoord - vec2(0.5);\n float dist = length(uv);\n if (dist > 0.5) discard; // outside circle → transparent\n // soft anti-alias ring at the edge\n float alpha = 1.0 - smoothstep(0.45, 0.5, dist);\n gl_FragColor = vec4(vColor, alpha * uOpacity);\n }\n `,\n transparent: true,\n depthWrite: false,\n });\n}\n\n// DOTS-WAVE — same as dots but size is modulated by Z in the vertex shader\nfunction makeRoundDotWaveMaterial(\n sizeMin: number,\n sizeMax: number,\n amplitude: number,\n opacity: number,\n): THREE.ShaderMaterial {\n return new THREE.ShaderMaterial({\n uniforms: {\n uSizeMin: { value: sizeMin },\n uSizeMax: { value: sizeMax },\n uAmp: { value: amplitude },\n uOpacity: { value: opacity },\n },\n vertexShader: /* glsl */ `\n attribute vec3 color;\n varying vec3 vColor;\n uniform float uSizeMin;\n uniform float uSizeMax;\n uniform float uAmp;\n void main() {\n vColor = color;\n // Map Z (-amp..+amp) → (sizeMin..sizeMax)\n float t = clamp((position.z + uAmp) / (2.0 * uAmp), 0.0, 1.0);\n gl_PointSize = mix(uSizeMin, uSizeMax, t);\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: /* glsl */ `\n varying vec3 vColor;\n uniform float uOpacity;\n void main() {\n vec2 uv = gl_PointCoord - vec2(0.5);\n float dist = length(uv);\n if (dist > 0.5) discard;\n float alpha = 1.0 - smoothstep(0.45, 0.5, dist);\n gl_FragColor = vec4(vColor, alpha * uOpacity);\n }\n `,\n transparent: true,\n depthWrite: false,\n });\n}\n\n// CROSSES\nfunction buildCrosses(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n armLen: number,\n): { geo: THREE.BufferGeometry; centers: Float32Array; pos: Float32Array } {\n const cx = cols + 1;\n const ry = rows + 1;\n const total = cx * ry;\n const half = armLen / 2;\n const sx = w / cols;\n const sy = h / rows;\n\n const centers = new Float32Array(total * 3);\n const pos = new Float32Array(total * 4 * 3);\n const col = new Float32Array(total * 4 * 3);\n\n for (let r = 0; r < ry; r++) {\n for (let c = 0; c < cx; c++) {\n const vi = r * cx + c;\n const bx = -w / 2 + c * sx;\n const by = -h / 2 + r * sy;\n centers[vi * 3] = bx;\n centers[vi * 3 + 1] = by;\n centers[vi * 3 + 2] = 0;\n const b = vi * 12;\n pos[b] = bx - half;\n pos[b + 1] = by;\n pos[b + 2] = 0;\n pos[b + 3] = bx + half;\n pos[b + 4] = by;\n pos[b + 5] = 0;\n pos[b + 6] = bx;\n pos[b + 7] = by - half;\n pos[b + 8] = 0;\n pos[b + 9] = bx;\n pos[b + 10] = by + half;\n pos[b + 11] = 0;\n const clr = lerpPalette(vi / Math.max(total - 1, 1), stops);\n\n for (let p = 0; p < 4; p++) {\n col[b + p * 3] = clr.r;\n col[b + p * 3 + 1] = clr.g;\n col[b + p * 3 + 2] = clr.b;\n }\n }\n }\n\n const geo = new THREE.BufferGeometry();\n geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n geo.setAttribute('color', new THREE.BufferAttribute(col, 3));\n\n return { geo, centers, pos };\n}\n\n// DIAGONAL-LEFT (\\\\\\) or DIAGONAL-RIGHT (///)\nfunction buildDiagonal(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n dir: 'left' | 'right',\n): { geo: THREE.BufferGeometry; pos: Float32Array } {\n const cx = cols + 1;\n const ry = rows + 1;\n const pos = makeVertexGrid(cols, rows, w, h);\n\n // Each diagonal goes from (r,c) → (r+1,c+1) for right, (r,c+1) → (r+1,c) for left\n const idx = new Uint32Array(cols * rows * 2);\n let ptr = 0;\n\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < cols; c++) {\n if (dir === 'right') {\n idx[ptr++] = r * cx + c;\n idx[ptr++] = (r + 1) * cx + c + 1;\n } else {\n idx[ptr++] = r * cx + c + 1;\n idx[ptr++] = (r + 1) * cx + c;\n }\n }\n }\n\n const geo = new THREE.BufferGeometry();\n geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n geo.setAttribute(\n 'color',\n new THREE.BufferAttribute(makeColorBuffer(cx * ry, stops), 3),\n );\n geo.setIndex(new THREE.BufferAttribute(idx, 1));\n\n return { geo, pos };\n}\n\n// ZIGZAG — alternating row direction creates chevrons\nfunction buildZigzag(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n): { geo: THREE.BufferGeometry; pos: Float32Array } {\n const cx = cols + 1;\n const ry = rows + 1;\n const pos = makeVertexGrid(cols, rows, w, h);\n\n // Per row: connect across the row as a zigzag (top vertices to bottom vertices alternating)\n const segCount = rows * cols * 2; // 2 segments per cell (v-shape)\n const idx = new Uint32Array(segCount * 2);\n let ptr = 0;\n\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < cols; c++) {\n const even = r % 2 === 0;\n\n // Each cell: draw one diagonal and horizontal to form chevron\n if (even) {\n idx[ptr++] = r * cx + c;\n idx[ptr++] = (r + 1) * cx + c + 1;\n idx[ptr++] = r * cx + c + 1;\n idx[ptr++] = (r + 1) * cx + c + 1;\n } else {\n idx[ptr++] = r * cx + c + 1;\n idx[ptr++] = (r + 1) * cx + c;\n idx[ptr++] = r * cx + c;\n idx[ptr++] = (r + 1) * cx + c;\n }\n }\n }\n\n const geo = new THREE.BufferGeometry();\n geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n geo.setAttribute(\n 'color',\n new THREE.BufferAttribute(makeColorBuffer(cx * ry, stops), 3),\n );\n geo.setIndex(new THREE.BufferAttribute(idx, 1));\n\n return { geo, pos };\n}\n\n// HEXAGONS — flat-top hexagonal cells\nfunction buildHexagons(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n): { geo: THREE.BufferGeometry; pos: Float32Array; hexCenters: Float32Array } {\n // Each hexagon = 6 line segments = 12 endpoints (no shared verts → clean vertex colors)\n const hexCols = cols;\n const hexRows = rows;\n const hexCount = hexCols * hexRows;\n const hexR = w / hexCols / 2; // circumradius\n const hexH = hexR * Math.sqrt(3); // flat-top hex height\n\n const pos = new Float32Array(hexCount * 12 * 3); // 6 edges × 2 pts × 3 floats\n const col = new Float32Array(hexCount * 12 * 3);\n const centers = new Float32Array(hexCount * 3);\n\n let hi = 0; // hex index\n\n for (let row = 0; row < hexRows; row++) {\n for (let col2 = 0; col2 < hexCols; col2++) {\n const offset = col2 % 2 === 0 ? 0 : hexH * 0.5;\n const cx2 = -w / 2 + hexR + col2 * hexR * 1.5;\n const cy2 = -h / 2 + hexH * 0.5 + row * hexH + offset;\n\n centers[hi * 3] = cx2;\n centers[hi * 3 + 1] = cy2;\n centers[hi * 3 + 2] = 0;\n\n const t = hi / Math.max(hexCount - 1, 1);\n const clr = lerpPalette(t, stops);\n\n // 6 vertices of flat-top hexagon\n const verts: [number, number][] = [];\n\n for (let k = 0; k < 6; k++) {\n const angle = (Math.PI / 3) * k; // 0°,60°,120°…\n verts.push([\n cx2 + hexR * Math.cos(angle),\n cy2 + hexR * Math.sin(angle),\n ]);\n }\n\n // 6 edges — each as a line segment pair\n for (let k = 0; k < 6; k++) {\n const a = verts[k];\n const b = verts[(k + 1) % 6];\n const base = (hi * 6 + k) * 6; // 2 pts × 3 floats per edge\n pos[base] = a[0];\n pos[base + 1] = a[1];\n pos[base + 2] = 0;\n pos[base + 3] = b[0];\n pos[base + 4] = b[1];\n pos[base + 5] = 0;\n\n for (let p = 0; p < 2; p++) {\n col[base + p * 3] = clr.r;\n col[base + p * 3 + 1] = clr.g;\n col[base + p * 3 + 2] = clr.b;\n }\n }\n\n hi++;\n }\n }\n\n const geo = new THREE.BufferGeometry();\n geo.setAttribute(\n 'position',\n new THREE.BufferAttribute(pos.slice(0, hi * 6 * 6), 3),\n );\n geo.setAttribute(\n 'color',\n new THREE.BufferAttribute(col.slice(0, hi * 6 * 6), 3),\n );\n\n return { geo, pos, hexCenters: centers.slice(0, hi * 3) };\n}\n\n// DASHES — like grid but with a gap in the middle of each segment\nfunction buildDashes(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n lines: 'both' | 'horizontal' | 'vertical',\n dashRatio: number,\n): { geo: THREE.BufferGeometry; pos: Float32Array; basePos: Float32Array } {\n const cx = cols + 1;\n const ry = rows + 1;\n const sx = w / cols;\n const sy = h / rows;\n const half = dashRatio / 2;\n\n // Each dash = 2 endpoints, no shared verts\n const hCount = lines !== 'vertical' ? ry * cols : 0;\n const vCount = lines !== 'horizontal' ? cx * rows : 0;\n const total = (hCount + vCount) * 2;\n\n const pos = new Float32Array(total * 3);\n const basePos = new Float32Array(total * 3); // stored XY, updated Z each frame\n const col = new Float32Array(total * 3);\n\n let p = 0;\n const allVtx = makeVertexGrid(cols, rows, w, h);\n const colorGrid = makeColorBuffer(cx * ry, stops);\n\n const push = (\n x1: number,\n y1: number,\n x2: number,\n y2: number,\n ci: number,\n ) => {\n const clr = ci / Math.max(cx * ry - 1, 1);\n const c = lerpPalette(clr, stops);\n\n for (let k = 0; k < 2; k++) {\n const [px2, py] = k === 0 ? [x1, y1] : [x2, y2];\n pos[p * 3] = px2;\n pos[p * 3 + 1] = py;\n pos[p * 3 + 2] = 0;\n basePos[p * 3] = px2;\n basePos[p * 3 + 1] = py;\n basePos[p * 3 + 2] = 0;\n col[p * 3] = c.r;\n col[p * 3 + 1] = c.g;\n col[p * 3 + 2] = c.b;\n p++;\n }\n };\n\n if (lines !== 'vertical') {\n for (let r = 0; r < ry; r++) {\n for (let c2 = 0; c2 < cols; c2++) {\n const x1 = -w / 2 + c2 * sx;\n const x2 = x1 + sx;\n const y = -h / 2 + r * sy;\n push(x1 + sx * half, y, x2 - sx * half, y, r * cx + c2);\n }\n }\n }\n\n if (lines !== 'horizontal') {\n for (let c2 = 0; c2 < cx; c2++) {\n for (let r = 0; r < rows; r++) {\n const y1 = -h / 2 + r * sy;\n const y2 = y1 + sy;\n const x = -w / 2 + c2 * sx;\n push(x, y1 + sy * half, x, y2 - sy * half, r * cx + c2);\n }\n }\n }\n\n const geo = new THREE.BufferGeometry();\n geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n geo.setAttribute('color', new THREE.BufferAttribute(col, 3));\n\n return { geo, pos, basePos };\n}\n\n// CONTOUR — draws horizontal lines only at fixed Z thresholds (resampled each frame)\n// We build a flat placeholder geo; indices are rebuilt each frame as Z changes.\n// For performance we use a fixed vertex pool and swap positions.\nfunction buildContourPlaceholder(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n levels: number,\n): { geo: THREE.BufferGeometry; vtxGrid: Float32Array } {\n // Max line segments = rows * cols * 4 (at most 4 crossing per cell edge), generous upper bound\n const maxSegs = cols * rows * 4 * 2;\n const pos = new Float32Array(maxSegs * 3);\n const col = new Float32Array(maxSegs * 3);\n const geo = new THREE.BufferGeometry();\n geo.setAttribute(\n 'position',\n new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage),\n );\n geo.setAttribute(\n 'color',\n new THREE.BufferAttribute(col, 3).setUsage(THREE.DynamicDrawUsage),\n );\n geo.setDrawRange(0, 0);\n const vtxGrid = makeVertexGrid(cols, rows, w, h);\n\n return { geo, vtxGrid };\n}\n\n// SOLID — PlaneGeometry + MeshPhongMaterial with lighting\nfunction buildSolid(\n cols: number,\n rows: number,\n w: number,\n h: number,\n stops: THREE.Color[],\n): { geo: THREE.PlaneGeometry; pos: Float32Array } {\n const geo = new THREE.PlaneGeometry(w, h, cols, rows);\n const count = geo.attributes.position.count;\n geo.setAttribute(\n 'color',\n new THREE.BufferAttribute(makeColorBuffer(count, stops), 3),\n );\n const pos = geo.attributes.position.array as Float32Array;\n\n return { geo, pos };\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nconst WavesThree = ({\n className,\n style = 'grid',\n lines = 'both',\n colors,\n cameraPosition = { x: 0, y: 0, z: 10 },\n planeWidth = 80,\n planeHeight = 40,\n segmentsX = 60,\n segmentsY = 30,\n speed = 1,\n amplitude = 1.5,\n frequency = 0.3,\n opacity = 0.6,\n paused = false,\n mouseInfluence = 2,\n mouseRotation = 0.1,\n dotSize = 3,\n dotSizeMin = 1,\n crossSize = 0.3,\n dashRatio = 0.5,\n contourLevels = 6,\n maxPixelRatio = 2,\n onReady,\n}: WavesThreeProps) => {\n const containerRef = useRef(null);\n const [size, setSize] = useState({ width: 0, height: 0 });\n\n // Hot-update refs — no scene restart needed for these\n const mouseRef = useRef({ x: 0, y: 0 });\n const speedRef = useRef(speed);\n const pausedRef = useRef(paused);\n const amplitudeRef = useRef(amplitude);\n const frequencyRef = useRef(frequency);\n const mouseInfluenceRef = useRef(mouseInfluence);\n const mouseRotationRef = useRef(mouseRotation);\n const opacityRef = useRef(opacity);\n\n useEffect(() => {\n speedRef.current = speed;\n }, [speed]);\n useEffect(() => {\n pausedRef.current = paused;\n }, [paused]);\n useEffect(() => {\n amplitudeRef.current = amplitude;\n }, [amplitude]);\n useEffect(() => {\n frequencyRef.current = frequency;\n }, [frequency]);\n useEffect(() => {\n mouseInfluenceRef.current = mouseInfluence;\n }, [mouseInfluence]);\n useEffect(() => {\n mouseRotationRef.current = mouseRotation;\n }, [mouseRotation]);\n useEffect(() => {\n opacityRef.current = opacity;\n }, [opacity]);\n\n // Container size\n useEffect(() => {\n const el = containerRef.current;\n\n if (!el) {\n return;\n }\n\n const ro = new ResizeObserver((entries) => {\n const r = entries[0].contentRect;\n setSize({ width: r.width, height: r.height });\n });\n ro.observe(el);\n\n return () => ro.disconnect();\n }, []);\n\n // Main scene\n useEffect(() => {\n const el = containerRef.current;\n\n if (!el || size.width === 0 || size.height === 0) {\n return;\n }\n\n // Scene & Camera\n const scene = new THREE.Scene();\n const camera = new THREE.PerspectiveCamera(\n 75,\n size.width / size.height,\n 0.1,\n 1000,\n );\n camera.position.set(\n cameraPosition.x,\n cameraPosition.y,\n cameraPosition.z,\n );\n camera.lookAt(0, 0, 0);\n\n // Renderer\n const renderer = new THREE.WebGLRenderer({\n alpha: true,\n antialias: true,\n });\n renderer.setPixelRatio(\n Math.min(window.devicePixelRatio, maxPixelRatio),\n );\n renderer.setSize(size.width, size.height);\n renderer.setClearColor(0x000000, 0);\n el.appendChild(renderer.domElement);\n\n // Colors\n const isDark = document.documentElement.classList.contains('dark');\n const rawColors = colors ?? (isDark ? DEFAULT_DARK : DEFAULT_LIGHT);\n const colorStops = rawColors.map((c) => new THREE.Color(c));\n\n // Per-style setup\n let object3d: THREE.Object3D;\n let geo: THREE.BufferGeometry;\n let mat: THREE.Material;\n let posBuf: Float32Array | null = null;\n let baseBuf: Float32Array | null = null; // for dashes: stores XY reference\n let posAttr: THREE.BufferAttribute | null = null;\n let crossCenters: Float32Array | null = null;\n let hexCentersBuf: Float32Array | null = null;\n let hexPosBuf: Float32Array | null = null;\n let contourVtxGrid: Float32Array | null = null;\n const extraDispose: THREE.Material[] = [];\n let lights: THREE.Light[] = [];\n\n const cols = segmentsX;\n const rows = segmentsY;\n\n if (style === 'wireframe') {\n const g = new THREE.PlaneGeometry(\n planeWidth,\n planeHeight,\n cols,\n rows,\n );\n g.setAttribute(\n 'color',\n new THREE.BufferAttribute(\n makeColorBuffer(g.attributes.position.count, colorStops),\n 3,\n ),\n );\n const m = new THREE.MeshBasicMaterial({\n vertexColors: true,\n wireframe: true,\n transparent: true,\n opacity: opacityRef.current,\n });\n object3d = new THREE.Mesh(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = posAttr.array as Float32Array;\n geo = g;\n mat = m;\n } else if (style === 'grid') {\n const { geo: g, pos } = buildGrid(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n lines,\n );\n const m = new THREE.LineBasicMaterial({\n vertexColors: true,\n transparent: true,\n opacity: opacityRef.current,\n });\n object3d = new THREE.LineSegments(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = pos;\n geo = g;\n mat = m;\n } else if (style === 'dots') {\n const { geo: g, pos } = buildDots(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n );\n const m = makeRoundDotMaterial(dotSize * 2, opacityRef.current);\n object3d = new THREE.Points(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = pos;\n geo = g;\n mat = m;\n } else if (style === 'dots-wave') {\n const { geo: g, pos } = buildDots(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n );\n const m = makeRoundDotWaveMaterial(\n dotSizeMin * 2,\n dotSize * 2,\n amplitude,\n opacityRef.current,\n );\n object3d = new THREE.Points(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = pos;\n geo = g;\n mat = m;\n } else if (style === 'crosses') {\n const {\n geo: g,\n centers,\n pos,\n } = buildCrosses(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n crossSize,\n );\n const m = new THREE.LineBasicMaterial({\n vertexColors: true,\n transparent: true,\n opacity: opacityRef.current,\n });\n object3d = new THREE.LineSegments(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = pos;\n crossCenters = centers;\n geo = g;\n mat = m;\n } else if (style === 'diagonal-left' || style === 'diagonal-right') {\n const dir = style === 'diagonal-left' ? 'left' : 'right';\n const { geo: g, pos } = buildDiagonal(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n dir,\n );\n const m = new THREE.LineBasicMaterial({\n vertexColors: true,\n transparent: true,\n opacity: opacityRef.current,\n });\n object3d = new THREE.LineSegments(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = pos;\n geo = g;\n mat = m;\n } else if (style === 'zigzag') {\n const { geo: g, pos } = buildZigzag(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n );\n const m = new THREE.LineBasicMaterial({\n vertexColors: true,\n transparent: true,\n opacity: opacityRef.current,\n });\n object3d = new THREE.LineSegments(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = pos;\n geo = g;\n mat = m;\n } else if (style === 'hexagons') {\n const {\n geo: g,\n pos,\n hexCenters,\n } = buildHexagons(cols, rows, planeWidth, planeHeight, colorStops);\n const m = new THREE.LineBasicMaterial({\n vertexColors: true,\n transparent: true,\n opacity: opacityRef.current,\n });\n object3d = new THREE.LineSegments(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n hexPosBuf = pos;\n hexCentersBuf = hexCenters;\n geo = g;\n mat = m;\n } else if (style === 'dashes') {\n const {\n geo: g,\n pos,\n basePos,\n } = buildDashes(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n lines,\n dashRatio,\n );\n const m = new THREE.LineBasicMaterial({\n vertexColors: true,\n transparent: true,\n opacity: opacityRef.current,\n });\n object3d = new THREE.LineSegments(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = pos;\n baseBuf = basePos;\n geo = g;\n mat = m;\n } else if (style === 'contour') {\n const { geo: g, vtxGrid } = buildContourPlaceholder(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n contourLevels,\n );\n const m = new THREE.LineBasicMaterial({\n vertexColors: true,\n transparent: true,\n opacity: opacityRef.current,\n });\n object3d = new THREE.LineSegments(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n contourVtxGrid = vtxGrid;\n geo = g;\n mat = m;\n } else {\n // solid\n const { geo: g, pos } = buildSolid(\n cols,\n rows,\n planeWidth,\n planeHeight,\n colorStops,\n );\n const m = new THREE.MeshPhongMaterial({\n vertexColors: true,\n transparent: true,\n opacity: opacityRef.current,\n side: THREE.DoubleSide,\n shininess: 60,\n });\n const keyLight = new THREE.DirectionalLight(0xffffff, 1.2);\n keyLight.position.set(5, 10, 7);\n const fillLight = new THREE.AmbientLight(0xffffff, 0.4);\n scene.add(keyLight, fillLight);\n lights = [keyLight, fillLight];\n object3d = new THREE.Mesh(g, m);\n posAttr = g.attributes.position as THREE.BufferAttribute;\n posBuf = pos;\n geo = g;\n mat = m;\n }\n\n scene.add(object3d);\n\n // Event listeners\n const handleResize = () => {\n camera.aspect = el.clientWidth / el.clientHeight;\n camera.updateProjectionMatrix();\n renderer.setSize(el.clientWidth, el.clientHeight);\n };\n const handleMouse = (e: MouseEvent) => {\n mouseRef.current.x = (e.clientX / window.innerWidth) * 2 - 1;\n mouseRef.current.y = -(e.clientY / window.innerHeight) * 2 + 1;\n };\n window.addEventListener('resize', handleResize);\n window.addEventListener('mousemove', handleMouse);\n\n // Contour iso-line builder (marching squares, edge-interpolated)\n const rebuildContour = (\n vtxGrid: Float32Array,\n zGrid: Float32Array,\n thresholds: number[],\n posAttrC: THREE.BufferAttribute,\n colAttrC: THREE.BufferAttribute,\n ) => {\n const cx2 = cols + 1;\n let ptr = 0;\n const posArr = posAttrC.array as Float32Array;\n const colArr = colAttrC.array as Float32Array;\n\n for (const thresh of thresholds) {\n const t = (thresh - (-amplitude - 1)) / ((amplitude + 1) * 2);\n const clr = lerpPalette(t, colorStops);\n\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < cols; c++) {\n const i00 = r * cx2 + c;\n const i10 = r * cx2 + c + 1;\n const i01 = (r + 1) * cx2 + c;\n const i11 = (r + 1) * cx2 + c + 1;\n\n const z00 = zGrid[i00];\n const z10 = zGrid[i10];\n const z01 = zGrid[i01];\n const z11 = zGrid[i11];\n\n const x00 = vtxGrid[i00 * 3];\n const y00 = vtxGrid[i00 * 3 + 1];\n const x10 = vtxGrid[i10 * 3];\n const y10 = vtxGrid[i10 * 3 + 1];\n const x01 = vtxGrid[i01 * 3];\n const y01 = vtxGrid[i01 * 3 + 1];\n const x11 = vtxGrid[i11 * 3];\n const y11 = vtxGrid[i11 * 3 + 1];\n\n // Collect edge crossing points\n const pts: [number, number, number][] = [];\n\n const cross = (\n zA: number,\n zB: number,\n xA: number,\n yA: number,\n zA2: number,\n xB: number,\n yB: number,\n zB2: number,\n ) => {\n if (zA < thresh !== zB < thresh) {\n const t2 = (thresh - zA) / (zB - zA);\n pts.push([\n xA + (xB - xA) * t2,\n yA + (yB - yA) * t2,\n thresh,\n ]);\n }\n };\n cross(z00, z10, x00, y00, z00, x10, y10, z10); // bottom edge\n cross(z10, z11, x10, y10, z10, x11, y11, z11); // right edge\n cross(z01, z11, x01, y01, z01, x11, y11, z11); // top edge\n cross(z00, z01, x00, y00, z00, x01, y01, z01); // left edge\n\n if (pts.length >= 2 && ptr + 6 <= posArr.length) {\n for (let k = 0; k < 2; k++) {\n posArr[ptr] = pts[k][0];\n posArr[ptr + 1] = pts[k][1];\n posArr[ptr + 2] = pts[k][2];\n colArr[ptr] = clr.r;\n colArr[ptr + 1] = clr.g;\n colArr[ptr + 2] = clr.b;\n ptr += 3;\n }\n }\n }\n }\n }\n\n posAttrC.needsUpdate = true;\n colAttrC.needsUpdate = true;\n (object3d as THREE.LineSegments).geometry.setDrawRange(0, ptr / 3);\n };\n\n // Z grid for contour (shared scratch)\n const zGrid =\n style === 'contour'\n ? new Float32Array((cols + 1) * (rows + 1))\n : null;\n\n // Animation loop\n let rafId: number;\n\n const animate = () => {\n rafId = requestAnimationFrame(animate);\n\n // Sync opacity to all material types\n if ((mat as any).opacity !== undefined) {\n (mat as any).opacity = opacityRef.current;\n }\n\n if ((mat as any).uniforms?.uOpacity) {\n (mat as any).uniforms.uOpacity.value = opacityRef.current;\n }\n\n if (!pausedRef.current) {\n const time = performance.now() * 0.001 * speedRef.current;\n const freq = frequencyRef.current;\n const amp = amplitudeRef.current;\n const mi = mouseInfluenceRef.current;\n const mx = mouseRef.current.x;\n const my = mouseRef.current.y;\n\n if (style === 'crosses' && crossCenters && posAttr && posBuf) {\n const vtxCount = (cols + 1) * (rows + 1);\n\n for (let vi = 0; vi < vtxCount; vi++) {\n const bx = crossCenters[vi * 3];\n const by = crossCenters[vi * 3 + 1];\n const z = calcZ(bx, by, time, freq, amp, mx, my, mi);\n const b = vi * 12;\n posBuf[b + 2] = z;\n posBuf[b + 5] = z;\n posBuf[b + 8] = z;\n posBuf[b + 11] = z;\n }\n\n posAttr.needsUpdate = true;\n } else if (\n style === 'hexagons' &&\n hexCentersBuf &&\n hexPosBuf &&\n posAttr\n ) {\n const hexCount = hexCentersBuf.length / 3;\n\n for (let hi = 0; hi < hexCount; hi++) {\n const bx = hexCentersBuf[hi * 3];\n const by = hexCentersBuf[hi * 3 + 1];\n const z = calcZ(bx, by, time, freq, amp, mx, my, mi);\n // 6 edges × 2 pts = 12 endpoints per hex\n const base = hi * 6 * 6; // 6edges × 6floats\n\n for (let k = 0; k < 12; k++) {\n hexPosBuf[base + k * 3 + 2] = z;\n }\n }\n\n // Sync the slice used in geo\n const posA = geo.attributes\n .position as THREE.BufferAttribute;\n const arr = posA.array as Float32Array;\n arr.set(hexPosBuf.slice(0, arr.length));\n posA.needsUpdate = true;\n } else if (style === 'dashes' && posBuf && baseBuf && posAttr) {\n const total = posBuf.length / 3;\n\n for (let i = 0; i < total; i++) {\n const x = baseBuf[i * 3];\n const y = baseBuf[i * 3 + 1];\n posBuf[i * 3 + 2] = calcZ(\n x,\n y,\n time,\n freq,\n amp,\n mx,\n my,\n mi,\n );\n }\n\n posAttr.needsUpdate = true;\n } else if (\n style === 'contour' &&\n contourVtxGrid &&\n zGrid &&\n posAttr\n ) {\n const vtxCount = (cols + 1) * (rows + 1);\n\n for (let i = 0; i < vtxCount; i++) {\n const x = contourVtxGrid[i * 3];\n const y = contourVtxGrid[i * 3 + 1];\n zGrid[i] = calcZ(x, y, time, freq, amp, mx, my, mi);\n }\n\n const thresholds: number[] = [];\n\n for (let l = 0; l < contourLevels; l++) {\n thresholds.push(\n -amp -\n 1 +\n (l / (contourLevels - 1)) * (amp + 1) * 2,\n );\n }\n\n rebuildContour(\n contourVtxGrid,\n zGrid,\n thresholds,\n geo.attributes.position as THREE.BufferAttribute,\n geo.attributes.color as THREE.BufferAttribute,\n );\n } else if (posBuf && posAttr) {\n // All other styles: simple per-vertex Z update\n const total = posBuf.length / 3;\n\n for (let i = 0; i < total; i++) {\n const x = posBuf[i * 3];\n const y = posBuf[i * 3 + 1];\n posBuf[i * 3 + 2] = calcZ(\n x,\n y,\n time,\n freq,\n amp,\n mx,\n my,\n mi,\n );\n }\n\n posAttr.needsUpdate = true;\n\n // Solid needs normals recomputed for correct lighting\n if (style === 'solid') {\n (geo as THREE.PlaneGeometry).computeVertexNormals();\n }\n }\n\n object3d.rotation.x = my * mouseRotationRef.current;\n object3d.rotation.y = mx * mouseRotationRef.current;\n }\n\n renderer.render(scene, camera);\n };\n\n animate();\n onReady?.();\n\n // Cleanup\n return () => {\n cancelAnimationFrame(rafId);\n window.removeEventListener('resize', handleResize);\n window.removeEventListener('mousemove', handleMouse);\n lights.forEach((l) => scene.remove(l));\n scene.remove(object3d);\n geo.dispose();\n mat.dispose();\n extraDispose.forEach((m2) => m2.dispose());\n renderer.dispose();\n\n if (el.contains(renderer.domElement)) {\n el.removeChild(renderer.domElement);\n }\n };\n }, [\n size.width,\n size.height,\n style,\n colors,\n lines,\n cameraPosition,\n planeWidth,\n planeHeight,\n segmentsX,\n segmentsY,\n dotSize,\n dotSizeMin,\n crossSize,\n dashRatio,\n contourLevels,\n maxPixelRatio,\n onReady,\n ]);\n\n return (\n \n );\n};\n\nexport default WavesThree;\n", "type": "registry:ui" } ], "meta": { "category": "components", "version": "1.0.0" }, "type": "registry:ui" }