Download frontend-react/src/components/GridScan.jsx from Aarav13/AuthriX: direct link, hf CLI and curl.
- Browser
- Download file 27.6 kB
-
https://huggingface.co/spaces/Aarav13/AuthriX/resolve/main/frontend-react/src/components/GridScan.jsx
- Command line
-
hf download hf://spaces/Aarav13/AuthriX/frontend-react/src/components/GridScan.jsx
-
curl -L -o GridScan.jsx https://huggingface.co/spaces/Aarav13/AuthriX/resolve/main/frontend-react/src/components/GridScan.jsx
27.6 kB
| import * as faceapi from 'face-api.js'; | |
| import { | |
| BloomEffect, | |
| ChromaticAberrationEffect, | |
| EffectComposer, | |
| EffectPass, | |
| RenderPass, | |
| } from 'postprocessing'; | |
| import { useEffect, useRef, useState } from 'react'; | |
| import * as THREE from 'three'; | |
| import './GridScan.css'; | |
| const vert = `varying vec2 vUv; | |
| void main(){ | |
| vUv = uv; | |
| gl_Position = vec4(position.xy, 0.0, 1.0); | |
| }`; | |
| const frag = `precision highp float; | |
| uniform vec3 iResolution; | |
| uniform float iTime; | |
| uniform vec2 uSkew; | |
| uniform float uTilt; | |
| uniform float uYaw; | |
| uniform float uLineThickness; | |
| uniform vec3 uLinesColor; | |
| uniform vec3 uScanColor; | |
| uniform float uGridScale; | |
| uniform float uLineStyle; | |
| uniform float uLineJitter; | |
| uniform float uScanOpacity; | |
| uniform float uScanDirection; | |
| uniform float uNoise; | |
| uniform float uBloomOpacity; | |
| uniform float uScanGlow; | |
| uniform float uScanSoftness; | |
| uniform float uPhaseTaper; | |
| uniform float uScanDuration; | |
| uniform float uScanDelay; | |
| varying vec2 vUv; | |
| uniform float uScanStarts[8]; | |
| uniform float uScanCount; | |
| const int MAX_SCANS = 8; | |
| float smoother01(float a, float b, float x){ | |
| float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0); | |
| return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); | |
| } | |
| void mainImage(out vec4 fragColor, in vec2 fragCoord){ | |
| vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y; | |
| vec3 ro = vec3(0.0); | |
| vec3 rd = normalize(vec3(p, 2.0)); | |
| float cR = cos(uTilt), sR = sin(uTilt); | |
| rd.xy = mat2(cR, -sR, sR, cR) * rd.xy; | |
| float cY = cos(uYaw), sY = sin(uYaw); | |
| rd.xz = mat2(cY, -sY, sY, cY) * rd.xz; | |
| vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7)); | |
| rd.xy += skew * rd.z; | |
| vec3 color = vec3(0.0); | |
| float minT = 1e20; | |
| float gridScale = max(1e-5, uGridScale); | |
| float fadeStrength = 2.0; | |
| vec2 gridUV = vec2(0.0); | |
| float hitIsY = 1.0; | |
| for (int i = 0; i < 4; i++){ | |
| float isY = float(i < 2); | |
| float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY); | |
| float num = pos - (isY * ro.y + (1.0 - isY) * ro.x); | |
| float den = isY * rd.y + (1.0 - isY) * rd.x; | |
| float t = num / den; | |
| vec3 h = ro + rd * t; | |
| float depthBoost = smoothstep(0.0, 3.0, h.z); | |
| h.xy += skew * 0.15 * depthBoost; | |
| bool use = t > 0.0 && t < minT; | |
| gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV; | |
| minT = use ? t : minT; | |
| hitIsY = use ? isY : hitIsY; | |
| } | |
| vec3 hit = ro + rd * minT; | |
| float dist = length(hit - ro); | |
| float jitterAmt = clamp(uLineJitter, 0.0, 1.0); | |
| if (jitterAmt > 0.0) { | |
| vec2 j = vec2( | |
| sin(gridUV.y * 2.7 + iTime * 1.8), | |
| cos(gridUV.x * 2.3 - iTime * 1.6) | |
| ) * (0.15 * jitterAmt); | |
| gridUV += j; | |
| } | |
| float fx = fract(gridUV.x); | |
| float fy = fract(gridUV.y); | |
| float ax = min(fx, 1.0 - fx); | |
| float ay = min(fy, 1.0 - fy); | |
| float wx = fwidth(gridUV.x); | |
| float wy = fwidth(gridUV.y); | |
| float halfPx = max(0.0, uLineThickness) * 0.5; | |
| float tx = halfPx * wx; | |
| float ty = halfPx * wy; | |
| float aax = wx; | |
| float aay = wy; | |
| float lineX = 1.0 - smoothstep(tx, tx + aax, ax); | |
| float lineY = 1.0 - smoothstep(ty, ty + aay, ay); | |
| if (uLineStyle > 0.5) { | |
| float dashRepeat = 4.0; | |
| float dashDuty = 0.5; | |
| float vy = fract(gridUV.y * dashRepeat); | |
| float vx = fract(gridUV.x * dashRepeat); | |
| float dashMaskY = step(vy, dashDuty); | |
| float dashMaskX = step(vx, dashDuty); | |
| if (uLineStyle < 1.5) { | |
| lineX *= dashMaskY; | |
| lineY *= dashMaskX; | |
| } else { | |
| float dotRepeat = 6.0; | |
| float dotWidth = 0.18; | |
| float cy = abs(fract(gridUV.y * dotRepeat) - 0.5); | |
| float cx = abs(fract(gridUV.x * dotRepeat) - 0.5); | |
| float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy); | |
| float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx); | |
| lineX *= dotMaskY; | |
| lineY *= dotMaskX; | |
| } | |
| } | |
| float primaryMask = max(lineX, lineY); | |
| vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale; | |
| if (jitterAmt > 0.0) { | |
| vec2 j2 = vec2( | |
| cos(gridUV2.y * 2.1 - iTime * 1.4), | |
| sin(gridUV2.x * 2.5 + iTime * 1.7) | |
| ) * (0.15 * jitterAmt); | |
| gridUV2 += j2; | |
| } | |
| float fx2 = fract(gridUV2.x); | |
| float fy2 = fract(gridUV2.y); | |
| float ax2 = min(fx2, 1.0 - fx2); | |
| float ay2 = min(fy2, 1.0 - fy2); | |
| float wx2 = fwidth(gridUV2.x); | |
| float wy2 = fwidth(gridUV2.y); | |
| float tx2 = halfPx * wx2; | |
| float ty2 = halfPx * wy2; | |
| float aax2 = wx2; | |
| float aay2 = wy2; | |
| float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2); | |
| float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2); | |
| if (uLineStyle > 0.5) { | |
| float dashRepeat2 = 4.0; | |
| float dashDuty2 = 0.5; | |
| float vy2m = fract(gridUV2.y * dashRepeat2); | |
| float vx2m = fract(gridUV2.x * dashRepeat2); | |
| float dashMaskY2 = step(vy2m, dashDuty2); | |
| float dashMaskX2 = step(vx2m, dashDuty2); | |
| if (uLineStyle < 1.5) { | |
| lineX2 *= dashMaskY2; | |
| lineY2 *= dashMaskX2; | |
| } else { | |
| float dotRepeat2 = 6.0; | |
| float dotWidth2 = 0.18; | |
| float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5); | |
| float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5); | |
| float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2); | |
| float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2); | |
| lineX2 *= dotMaskY2; | |
| lineY2 *= dotMaskX2; | |
| } | |
| } | |
| float altMask = max(lineX2, lineY2); | |
| float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5)); | |
| float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2)); | |
| float edgeDist = mix(edgeDistY, edgeDistX, hitIsY); | |
| float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist); | |
| altMask *= edgeGate; | |
| float lineMask = max(primaryMask, altMask); | |
| float fade = exp(-dist * fadeStrength); | |
| float dur = max(0.05, uScanDuration); | |
| float del = max(0.0, uScanDelay); | |
| float scanZMax = 2.0; | |
| float widthScale = max(0.1, uScanGlow); | |
| float sigma = max(0.001, 0.18 * widthScale * uScanSoftness); | |
| float sigmaA = sigma * 2.0; | |
| float combinedPulse = 0.0; | |
| float combinedAura = 0.0; | |
| float cycle = dur + del; | |
| float tCycle = mod(iTime, cycle); | |
| float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0); | |
| float phase = scanPhase; | |
| if (uScanDirection > 0.5 && uScanDirection < 1.5) { | |
| phase = 1.0 - phase; | |
| } else if (uScanDirection > 1.5) { | |
| float t2 = mod(max(0.0, iTime - del), 2.0 * dur); | |
| phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur); | |
| } | |
| float scanZ = phase * scanZMax; | |
| float dz = abs(hit.z - scanZ); | |
| float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma)); | |
| float taper = clamp(uPhaseTaper, 0.0, 0.49); | |
| float headW = taper; | |
| float tailW = taper; | |
| float headFade = smoother01(0.0, headW, phase); | |
| float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase); | |
| float phaseWindow = headFade * tailFade; | |
| float pulseBase = lineBand * phaseWindow; | |
| combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0); | |
| float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA)); | |
| combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0); | |
| for (int i = 0; i < MAX_SCANS; i++) { | |
| if (float(i) >= uScanCount) break; | |
| float tActiveI = iTime - uScanStarts[i]; | |
| float phaseI = clamp(tActiveI / dur, 0.0, 1.0); | |
| if (uScanDirection > 0.5 && uScanDirection < 1.5) { | |
| phaseI = 1.0 - phaseI; | |
| } else if (uScanDirection > 1.5) { | |
| phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0); | |
| } | |
| float scanZI = phaseI * scanZMax; | |
| float dzI = abs(hit.z - scanZI); | |
| float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma)); | |
| float headFadeI = smoother01(0.0, headW, phaseI); | |
| float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI); | |
| float phaseWindowI = headFadeI * tailFadeI; | |
| combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0); | |
| float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA)); | |
| combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0); | |
| } | |
| float lineVis = lineMask; | |
| vec3 gridCol = uLinesColor * lineVis * fade; | |
| vec3 scanCol = uScanColor * combinedPulse; | |
| vec3 scanAura = uScanColor * combinedAura; | |
| color = gridCol + scanCol + scanAura; | |
| float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123); | |
| color += (n - 0.5) * uNoise; | |
| color = clamp(color, 0.0, 1.0); | |
| float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0); | |
| float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax); | |
| float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay); | |
| float halo = max(gx, gy) * fade; | |
| alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0)); | |
| fragColor = vec4(color, alpha); | |
| } | |
| void main(){ | |
| vec4 c; | |
| mainImage(c, vUv * iResolution.xy); | |
| gl_FragColor = c; | |
| }`; | |
| // ── Helpers ────────────────────────────────────────────────────────────────── | |
| function srgbColor(hex) { | |
| return new THREE.Color(hex).convertSRGBToLinear(); | |
| } | |
| function smoothDampVec2(current, target, currentVelocity, smoothTime, maxSpeed, deltaTime) { | |
| const out = current.clone(); | |
| smoothTime = Math.max(0.0001, smoothTime); | |
| const omega = 2 / smoothTime; | |
| const x = omega * deltaTime; | |
| const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x); | |
| let change = current.clone().sub(target); | |
| const originalTo = target.clone(); | |
| const maxChange = maxSpeed * smoothTime; | |
| if (change.length() > maxChange) change.setLength(maxChange); | |
| target = current.clone().sub(change); | |
| const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime); | |
| currentVelocity.sub(temp.clone().multiplyScalar(omega)); | |
| currentVelocity.multiplyScalar(exp); | |
| out.copy(target.clone().add(change.add(temp).multiplyScalar(exp))); | |
| const origMinusCurrent = originalTo.clone().sub(current); | |
| const outMinusOrig = out.clone().sub(originalTo); | |
| if (origMinusCurrent.dot(outMinusOrig) > 0) { | |
| out.copy(originalTo); | |
| currentVelocity.set(0, 0); | |
| } | |
| return out; | |
| } | |
| function smoothDampFloat(current, target, velRef, smoothTime, maxSpeed, deltaTime) { | |
| smoothTime = Math.max(0.0001, smoothTime); | |
| const omega = 2 / smoothTime; | |
| const x = omega * deltaTime; | |
| const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x); | |
| let change = current - target; | |
| const originalTo = target; | |
| const maxChange = maxSpeed * smoothTime; | |
| change = Math.sign(change) * Math.min(Math.abs(change), maxChange); | |
| target = current - change; | |
| const temp = (velRef.v + omega * change) * deltaTime; | |
| velRef.v = (velRef.v - omega * temp) * exp; | |
| let out = target + (change + temp) * exp; | |
| if ((originalTo - current) * (out - originalTo) > 0) { | |
| out = originalTo; | |
| velRef.v = 0; | |
| } | |
| return { value: out, v: velRef.v }; | |
| } | |
| function medianPush(buf, v, maxLen) { | |
| buf.push(v); | |
| if (buf.length > maxLen) buf.shift(); | |
| } | |
| function median(buf) { | |
| if (buf.length === 0) return 0; | |
| const a = [...buf].sort((x, y) => x - y); | |
| const mid = Math.floor(a.length / 2); | |
| return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5; | |
| } | |
| function centroid(points) { | |
| let x = 0, y = 0; | |
| const n = points.length || 1; | |
| for (const p of points) { x += p.x; y += p.y; } | |
| return { x: x / n, y: y / n }; | |
| } | |
| function dist2(a, b) { | |
| return Math.hypot(a.x - b.x, a.y - b.y); | |
| } | |
| // ── Component ───────────────────────────────────────────────────────────────── | |
| const MAX_SCANS = 8; | |
| export const GridScan = ({ | |
| enableWebcam = false, | |
| showPreview = false, | |
| modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights', | |
| sensitivity = 0.55, | |
| lineThickness = 1, | |
| linesColor = '#2F293A', | |
| scanColor = '#FF9FFC', | |
| scanOpacity = 0.4, | |
| gridScale = 0.1, | |
| lineStyle = 'solid', | |
| lineJitter = 0.1, | |
| scanDirection = 'pingpong', | |
| enablePost = true, | |
| bloomIntensity = 0, | |
| bloomThreshold = 0, | |
| bloomSmoothing = 0, | |
| chromaticAberration = 0.002, | |
| noiseIntensity = 0.01, | |
| scanGlow = 0.5, | |
| scanSoftness = 2, | |
| scanPhaseTaper = 0.9, | |
| scanDuration = 2.0, | |
| scanDelay = 2.0, | |
| enableGyro = false, | |
| scanOnClick = false, | |
| snapBackDelay = 250, | |
| className, | |
| style, | |
| }) => { | |
| const containerRef = useRef(null); | |
| const videoRef = useRef(null); | |
| const rendererRef = useRef(null); | |
| const materialRef = useRef(null); | |
| const composerRef = useRef(null); | |
| const bloomRef = useRef(null); | |
| const chromaRef = useRef(null); | |
| const rafRef = useRef(null); | |
| const [modelsReady, setModelsReady] = useState(false); | |
| const [uiFaceActive, setUiFaceActive] = useState(false); | |
| const lookTarget = useRef(new THREE.Vector2(0, 0)); | |
| const tiltTarget = useRef(0); | |
| const yawTarget = useRef(0); | |
| const lookCurrent = useRef(new THREE.Vector2(0, 0)); | |
| const lookVel = useRef(new THREE.Vector2(0, 0)); | |
| const tiltCurrent = useRef(0); | |
| const tiltVel = useRef(0); | |
| const yawCurrent = useRef(0); | |
| const yawVel = useRef(0); | |
| const scanStartsRef = useRef([]); | |
| const pushScan = (t) => { | |
| const arr = scanStartsRef.current.slice(); | |
| if (arr.length >= MAX_SCANS) arr.shift(); | |
| arr.push(t); | |
| scanStartsRef.current = arr; | |
| if (materialRef.current) { | |
| const u = materialRef.current.uniforms; | |
| const buf = new Array(MAX_SCANS).fill(0); | |
| for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i]; | |
| u.uScanStarts.value = buf; | |
| u.uScanCount.value = arr.length; | |
| } | |
| }; | |
| const bufX = useRef([]); | |
| const bufY = useRef([]); | |
| const bufT = useRef([]); | |
| const bufYaw = useRef([]); | |
| const s = THREE.MathUtils.clamp(sensitivity, 0, 1); | |
| const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s); | |
| const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s); | |
| const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s); | |
| const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s); | |
| const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s); | |
| const maxSpeed = Infinity; | |
| const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s); | |
| // Mouse interaction | |
| useEffect(() => { | |
| const el = containerRef.current; | |
| if (!el) return; | |
| let leaveTimer = null; | |
| const onMove = (e) => { | |
| if (uiFaceActive) return; | |
| if (leaveTimer) { clearTimeout(leaveTimer); leaveTimer = null; } | |
| const rect = el.getBoundingClientRect(); | |
| const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1; | |
| const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1); | |
| lookTarget.current.set(nx, ny); | |
| }; | |
| const onClick = async () => { | |
| const nowSec = performance.now() / 1000; | |
| if (scanOnClick) pushScan(nowSec); | |
| if (enableGyro && typeof window !== 'undefined' && window.DeviceOrientationEvent && DeviceOrientationEvent.requestPermission) { | |
| try { await DeviceOrientationEvent.requestPermission(); } catch { /* noop */ } | |
| } | |
| }; | |
| const onLeave = () => { | |
| if (uiFaceActive) return; | |
| if (leaveTimer) clearTimeout(leaveTimer); | |
| leaveTimer = window.setTimeout(() => { | |
| lookTarget.current.set(0, 0); | |
| tiltTarget.current = 0; | |
| yawTarget.current = 0; | |
| }, Math.max(0, snapBackDelay || 0)); | |
| }; | |
| el.addEventListener('mousemove', onMove); | |
| el.addEventListener('mouseenter', () => { if (leaveTimer) { clearTimeout(leaveTimer); leaveTimer = null; } }); | |
| if (scanOnClick) el.addEventListener('click', onClick); | |
| el.addEventListener('mouseleave', onLeave); | |
| return () => { | |
| el.removeEventListener('mousemove', onMove); | |
| el.removeEventListener('mouseleave', onLeave); | |
| if (scanOnClick) el.removeEventListener('click', onClick); | |
| if (leaveTimer) clearTimeout(leaveTimer); | |
| }; | |
| }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]); | |
| // Three.js renderer setup | |
| useEffect(() => { | |
| const container = containerRef.current; | |
| if (!container) return; | |
| const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| rendererRef.current = renderer; | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| renderer.outputColorSpace = THREE.SRGBColorSpace; | |
| renderer.toneMapping = THREE.NoToneMapping; | |
| renderer.autoClear = false; | |
| renderer.setClearColor(0x000000, 0); | |
| container.appendChild(renderer.domElement); | |
| const uniforms = { | |
| iResolution: { value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio()) }, | |
| iTime: { value: 0 }, | |
| uSkew: { value: new THREE.Vector2(0, 0) }, | |
| uTilt: { value: 0 }, | |
| uYaw: { value: 0 }, | |
| uLineThickness: { value: lineThickness }, | |
| uLinesColor: { value: srgbColor(linesColor) }, | |
| uScanColor: { value: srgbColor(scanColor) }, | |
| uGridScale: { value: gridScale }, | |
| uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 }, | |
| uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) }, | |
| uScanOpacity: { value: scanOpacity }, | |
| uNoise: { value: noiseIntensity }, | |
| uBloomOpacity: { value: bloomIntensity }, | |
| uScanGlow: { value: scanGlow }, | |
| uScanSoftness: { value: scanSoftness }, | |
| uPhaseTaper: { value: scanPhaseTaper }, | |
| uScanDuration: { value: scanDuration }, | |
| uScanDelay: { value: scanDelay }, | |
| uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 }, | |
| uScanStarts: { value: new Array(MAX_SCANS).fill(0) }, | |
| uScanCount: { value: 0 }, | |
| }; | |
| const material = new THREE.ShaderMaterial({ | |
| uniforms, | |
| vertexShader: vert, | |
| fragmentShader: frag, | |
| transparent: true, | |
| depthWrite: false, | |
| depthTest: false, | |
| }); | |
| materialRef.current = material; | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); | |
| const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); | |
| scene.add(quad); | |
| let composer = null; | |
| if (enablePost) { | |
| composer = new EffectComposer(renderer); | |
| composerRef.current = composer; | |
| composer.addPass(new RenderPass(scene, camera)); | |
| const bloom = new BloomEffect({ intensity: 1.0, luminanceThreshold: bloomThreshold, luminanceSmoothing: bloomSmoothing }); | |
| bloom.blendMode.opacity.value = Math.max(0, bloomIntensity); | |
| bloomRef.current = bloom; | |
| const chroma = new ChromaticAberrationEffect({ | |
| offset: new THREE.Vector2(chromaticAberration, chromaticAberration), | |
| radialModulation: true, | |
| modulationOffset: 0.0, | |
| }); | |
| chromaRef.current = chroma; | |
| const effectPass = new EffectPass(camera, bloom, chroma); | |
| effectPass.renderToScreen = true; | |
| composer.addPass(effectPass); | |
| } | |
| const onResize = () => { | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio()); | |
| if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight); | |
| }; | |
| window.addEventListener('resize', onResize); | |
| let last = performance.now(); | |
| const tick = () => { | |
| const now = performance.now(); | |
| const dt = Math.max(0, Math.min(0.1, (now - last) / 1000)); | |
| last = now; | |
| lookCurrent.current.copy( | |
| smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt) | |
| ); | |
| const tiltSm = smoothDampFloat(tiltCurrent.current, tiltTarget.current, { v: tiltVel.current }, smoothTime, maxSpeed, dt); | |
| tiltCurrent.current = tiltSm.value; tiltVel.current = tiltSm.v; | |
| const yawSm = smoothDampFloat(yawCurrent.current, yawTarget.current, { v: yawVel.current }, smoothTime, maxSpeed, dt); | |
| yawCurrent.current = yawSm.value; yawVel.current = yawSm.v; | |
| const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale); | |
| material.uniforms.uSkew.value.set(skew.x, skew.y); | |
| material.uniforms.uTilt.value = tiltCurrent.current * tiltScale; | |
| material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6); | |
| material.uniforms.iTime.value = now / 1000; | |
| renderer.clear(true, true, true); | |
| if (composerRef.current) composerRef.current.render(dt); | |
| else renderer.render(scene, camera); | |
| rafRef.current = requestAnimationFrame(tick); | |
| }; | |
| rafRef.current = requestAnimationFrame(tick); | |
| return () => { | |
| if (rafRef.current) cancelAnimationFrame(rafRef.current); | |
| window.removeEventListener('resize', onResize); | |
| material.dispose(); | |
| quad.geometry.dispose(); | |
| if (composerRef.current) { composerRef.current.dispose(); composerRef.current = null; } | |
| renderer.dispose(); | |
| renderer.forceContextLoss(); | |
| if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement); | |
| }; | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [sensitivity, lineThickness, linesColor, scanColor, scanOpacity, gridScale, lineStyle, lineJitter, | |
| scanDirection, enablePost, noiseIntensity, bloomIntensity, scanGlow, scanSoftness, scanPhaseTaper, | |
| scanDuration, scanDelay, bloomThreshold, bloomSmoothing, chromaticAberration]); | |
| // Prop updates without full remount | |
| useEffect(() => { | |
| const m = materialRef.current; | |
| if (!m) return; | |
| const u = m.uniforms; | |
| u.uLineThickness.value = lineThickness; | |
| u.uLinesColor.value.copy(srgbColor(linesColor)); | |
| u.uScanColor.value.copy(srgbColor(scanColor)); | |
| u.uGridScale.value = gridScale; | |
| u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0; | |
| u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0)); | |
| u.uBloomOpacity.value = Math.max(0, bloomIntensity); | |
| u.uNoise.value = Math.max(0, noiseIntensity); | |
| u.uScanGlow.value = scanGlow; | |
| u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity)); | |
| u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0; | |
| u.uScanSoftness.value = scanSoftness; | |
| u.uPhaseTaper.value = scanPhaseTaper; | |
| u.uScanDuration.value = Math.max(0.05, scanDuration); | |
| u.uScanDelay.value = Math.max(0.0, scanDelay); | |
| if (bloomRef.current) { | |
| bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity); | |
| bloomRef.current.luminanceMaterial.threshold = bloomThreshold; | |
| bloomRef.current.luminanceMaterial.smoothing = bloomSmoothing; | |
| } | |
| if (chromaRef.current) chromaRef.current.offset.set(chromaticAberration, chromaticAberration); | |
| }, [lineThickness, linesColor, scanColor, gridScale, lineStyle, lineJitter, bloomIntensity, | |
| bloomThreshold, bloomSmoothing, chromaticAberration, noiseIntensity, scanGlow, scanOpacity, | |
| scanDirection, scanSoftness, scanPhaseTaper, scanDuration, scanDelay]); | |
| // Gyro | |
| useEffect(() => { | |
| if (!enableGyro) return; | |
| const handler = (e) => { | |
| if (uiFaceActive) return; | |
| const gamma = e.gamma ?? 0; | |
| const beta = e.beta ?? 0; | |
| lookTarget.current.set(THREE.MathUtils.clamp(gamma / 45, -1, 1), THREE.MathUtils.clamp(-beta / 30, -1, 1)); | |
| tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4; | |
| }; | |
| window.addEventListener('deviceorientation', handler); | |
| return () => window.removeEventListener('deviceorientation', handler); | |
| }, [enableGyro, uiFaceActive]); | |
| // Face-api models | |
| useEffect(() => { | |
| let canceled = false; | |
| const load = async () => { | |
| try { | |
| await Promise.all([ | |
| faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath), | |
| faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath), | |
| ]); | |
| if (!canceled) setModelsReady(true); | |
| } catch { if (!canceled) setModelsReady(false); } | |
| }; | |
| load(); | |
| return () => { canceled = true; }; | |
| }, [modelsPath]); | |
| // Webcam face tracking | |
| useEffect(() => { | |
| let stop = false; | |
| let lastDetect = 0; | |
| const video = videoRef.current; | |
| const start = async () => { | |
| if (!enableWebcam || !modelsReady || !video) return; | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }); | |
| video.srcObject = stream; | |
| await video.play(); | |
| } catch { return; } | |
| const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 }); | |
| const detect = async (ts) => { | |
| if (stop) return; | |
| if (ts - lastDetect >= 33) { | |
| lastDetect = ts; | |
| try { | |
| const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true); | |
| if (res?.detection) { | |
| const { box } = res.detection; | |
| const vw = video.videoWidth || 1, vh = video.videoHeight || 1; | |
| const nx = (box.x + box.width * 0.5) / vw * 2 - 1; | |
| const ny = (box.y + box.height * 0.5) / vh * 2 - 1; | |
| medianPush(bufX.current, nx, 5); medianPush(bufY.current, ny, 5); | |
| const look = new THREE.Vector2(Math.tanh(median(bufX.current)), Math.tanh(median(bufY.current))); | |
| const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh)); | |
| lookTarget.current.copy(look.multiplyScalar(1 + depthResponse * (faceSize - 0.25))); | |
| const lc = centroid(res.landmarks.getLeftEye()); | |
| const rc = centroid(res.landmarks.getRightEye()); | |
| medianPush(bufT.current, Math.atan2(rc.y - lc.y, rc.x - lc.x), 5); | |
| tiltTarget.current = median(bufT.current); | |
| const nose = res.landmarks.getNose(); | |
| const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)]; | |
| const jaw = res.landmarks.getJawOutline(); | |
| const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6; | |
| const yawSignal = Math.tanh(THREE.MathUtils.clamp((dist2(tip, jaw[13] || jaw[14]) - dist2(tip, jaw[3] || jaw[2])) / (eyeDist * 1.6), -1, 1)); | |
| medianPush(bufYaw.current, yawSignal, 5); | |
| yawTarget.current = median(bufYaw.current); | |
| setUiFaceActive(true); | |
| } else { setUiFaceActive(false); } | |
| } catch { setUiFaceActive(false); } | |
| } | |
| if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) { | |
| video.requestVideoFrameCallback(() => detect(performance.now())); | |
| } else { requestAnimationFrame(detect); } | |
| }; | |
| requestAnimationFrame(detect); | |
| }; | |
| start(); | |
| return () => { | |
| stop = true; | |
| if (video) { | |
| const stream = video.srcObject; | |
| if (stream) stream.getTracks().forEach(t => t.stop()); | |
| video.pause(); | |
| video.srcObject = null; | |
| } | |
| }; | |
| }, [enableWebcam, modelsReady, depthResponse]); | |
| return ( | |
| <div ref={containerRef} className={`gridscan${className ? ` ${className}` : ''}`} style={style}> | |
| {showPreview && ( | |
| <div className="gridscan__preview"> | |
| <video ref={videoRef} muted playsInline autoPlay className="gridscan__video" /> | |
| <div className="gridscan__badge"> | |
| {enableWebcam | |
| ? modelsReady | |
| ? uiFaceActive ? 'Face: tracking' : 'Face: searching' | |
| : 'Loading models' | |
| : 'Webcam disabled'} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default GridScan; | |