Download landing.js from webml-community/gemma-4-webgpu-kernels: direct link, hf CLI and curl.
- Browser
- Download file 28.6 kB
-
https://huggingface.co/spaces/webml-community/gemma-4-webgpu-kernels/resolve/main/landing.js
- Command line
-
hf download hf://spaces/webml-community/gemma-4-webgpu-kernels/landing.js
-
curl -L -o landing.js https://huggingface.co/spaces/webml-community/gemma-4-webgpu-kernels/resolve/main/landing.js
28.6 kB
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; | |
| import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; | |
| import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; | |
| import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'; | |
| import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'; | |
| // ============ DEVICE DETECTION ============ | |
| const isMobile = /Android|iPhone|iPad|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) | |
| || (navigator.maxTouchPoints > 1 && window.innerWidth < 1024); | |
| const gpuTier = (() => { | |
| const gl = document.createElement('canvas').getContext('webgl'); | |
| if (!gl) return 'low'; | |
| const ext = gl.getExtension('WEBGL_debug_renderer_info'); | |
| const gpu = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL).toLowerCase() : ''; | |
| // Apple GPU = Apple Silicon (M1/M2/M3/M4) — these are high-tier | |
| if (/apple gpu|apple m/.test(gpu)) return 'high'; | |
| // Low-tier: old Intel integrated, software renderers, old mobile chips | |
| if (/swiftshader|llvmpipe|mali-4|adreno 3/.test(gpu)) return 'low'; | |
| if (/intel(?!.*(iris|uhd|arc))/.test(gpu)) return 'low'; | |
| // Mid-tier: mid-range mobile, Intel Iris/UHD, older discrete | |
| if (/mali-g[567]|adreno [45]|intel (iris|uhd)|geforce (mx|gt)|radeon (rx )?(5[0-4]|vega 8)/.test(gpu)) return 'mid'; | |
| // Everything else (RTX, RX 6000+, Arc, etc.) = high | |
| return 'high'; | |
| })(); | |
| const qualityPresets = { | |
| low: { pixelRatio: 1.0, marchSteps: 48, aoSteps: 2, dotSize: 6.0, dotGap: 3.0, scanlines: 0.55, bloomEnabled: false }, | |
| mid: { pixelRatio: 1.25, marchSteps: 64, aoSteps: 3, dotSize: 5.0, dotGap: 2.5, scanlines: 0.75, bloomEnabled: true }, | |
| high: { pixelRatio: 1.5, marchSteps: 80, aoSteps: 3, dotSize: 5.0, dotGap: 2.5, scanlines: 0.75, bloomEnabled: true }, | |
| }; | |
| let currentTier = isMobile ? 'low' : gpuTier; | |
| let quality = { ...qualityPresets[currentTier] }; | |
| // ============ SETTINGS ============ | |
| const settings = { | |
| dither: { enabled: true, dotSize: quality.dotSize, dotGap: quality.dotGap, brightness: 0.85, contrast: 0.60, threshold: 0.03, dotColor: [1.0, 1.0, 1.0], bgColor: [0.00784, 0.00784, 0.01176] }, | |
| crosshatch: { enabled: false, intensity: 0.95, angle: 0.4363 }, | |
| bloom: { enabled: quality.bloomEnabled, intensity: 0.55, size: 1.50 }, | |
| crt: { enabled: true, curvature: 0.0, scanlines: quality.scanlines, vignette: 2.00, chroma: 0.0 }, | |
| scene: { gooeyness: 1.20, speed: 0.85 } | |
| }; | |
| // ============ MOUSE TRACKING ============ | |
| const mouse = new THREE.Vector2(0, 0); | |
| let mouseInScene = false; | |
| let mousePressed = false; | |
| let mouseSphereRadius = 0.0; | |
| const mouseSphereTargetRadius = 0.55; | |
| const mouseSphereClickRadius = 0.95; | |
| const mouseWorld = new THREE.Vector3(0, 0, 0); | |
| const mouseWorldTarget = new THREE.Vector3(0, 0, 0); | |
| const mouseDamping = 0.15; | |
| // ============ THREE.JS SETUP ============ | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x020203); | |
| const crtFrame = document.getElementById('crt-frame'); | |
| const getSize = () => { | |
| return { width: window.innerWidth, height: window.innerHeight }; | |
| }; | |
| let size = getSize(); | |
| const camera = new THREE.PerspectiveCamera(60, size.width / size.height, 0.1, 100); | |
| camera.position.set(0, 0, 5); | |
| const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' }); | |
| renderer.setSize(size.width, size.height); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, quality.pixelRatio)); | |
| crtFrame.appendChild(renderer.domElement); | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| controls.enableZoom = false; | |
| controls.enablePan = false; | |
| controls.enableRotate = false; | |
| controls.enabled = false; // prevent OrbitControls from capturing scroll/pointer events | |
| // ============ INPUT EVENTS ============ | |
| let pageVisible = true; | |
| // Render only while the hero canvas is actually on screen. When the chat view scrolls into | |
| // place the background pauses itself (below), freeing the GPU for the WebGPU model so decode | |
| // throughput isn't dragged down by the WebGL background. | |
| let heroOnScreen = true; | |
| const onPointerMove = (e) => { | |
| mouseInScene = true; | |
| const x = e.clientX ?? (e.touches && e.touches[0] ? e.touches[0].clientX : 0); | |
| const y = e.clientY ?? (e.touches && e.touches[0] ? e.touches[0].clientY : 0); | |
| mouse.x = (x / window.innerWidth) * 2 - 1; | |
| mouse.y = -(y / window.innerHeight) * 2 + 1; | |
| }; | |
| document.addEventListener('mousemove', onPointerMove, { passive: true }); | |
| document.addEventListener('touchmove', onPointerMove, { passive: true }); | |
| document.addEventListener('mouseenter', () => { mouseInScene = true; }, { passive: true }); | |
| document.addEventListener('mouseleave', () => { mouseInScene = false; }, { passive: true }); | |
| document.addEventListener('touchstart', (e) => { mouseInScene = true; mousePressed = true; onPointerMove(e); }, { passive: true }); | |
| document.addEventListener('touchend', () => { mousePressed = false; mouseInScene = false; }, { passive: true }); | |
| document.addEventListener('visibilitychange', () => { | |
| pageVisible = !document.hidden; | |
| if (document.hidden) mouseInScene = false; | |
| }); | |
| // Pause the WebGL background as soon as the chat view dominates the screen, so it never competes | |
| // with the WebGPU model for the GPU. Resume when scrolling back to the landing. Falls back to the | |
| // hero canvas's own visibility if there is no #chat section on the page. | |
| const chatSection = document.getElementById('chat'); | |
| if (chatSection) { | |
| new IntersectionObserver(([entry]) => { heroOnScreen = entry.intersectionRatio < 0.4; }, { threshold: [0, 0.4, 1] }).observe(chatSection); | |
| } else { | |
| new IntersectionObserver(([entry]) => { heroOnScreen = entry.isIntersecting; }, { threshold: 0 }).observe(crtFrame); | |
| } | |
| document.addEventListener('mousedown', () => { mousePressed = true; }, { passive: true }); | |
| document.addEventListener('mouseup', () => { mousePressed = false; }, { passive: true }); | |
| // ============ RAYMARCHING QUAD ============ | |
| const quadGeometry = new THREE.PlaneGeometry(2, 2); | |
| const quadMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| uTime: { value: 0 }, | |
| uResolution: { value: new THREE.Vector2(size.width, size.height) }, | |
| uCameraPos: { value: camera.position.clone() }, | |
| uCameraTarget: { value: new THREE.Vector3(0, 0, 0) }, | |
| uPixelRatio: { value: Math.min(window.devicePixelRatio, 1.5) }, | |
| uGooeyness: { value: settings.scene.gooeyness }, | |
| uSpeed: { value: settings.scene.speed }, | |
| uMouseSpherePos: { value: new THREE.Vector3(0, 0, 0) }, | |
| uMouseSphereRadius: { value: 0.0 }, | |
| }, | |
| vertexShader: ` | |
| varying vec2 vUv; | |
| void main() { | |
| vUv = uv; | |
| gl_Position = vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| precision highp float; | |
| #define MARCH_STEPS ${quality.marchSteps} | |
| #define AO_STEPS ${quality.aoSteps} | |
| uniform float uTime; | |
| uniform vec2 uResolution; | |
| uniform vec3 uCameraPos; | |
| uniform vec3 uCameraTarget; | |
| uniform float uPixelRatio; | |
| uniform float uGooeyness; | |
| uniform float uSpeed; | |
| uniform vec3 uMouseSpherePos; | |
| uniform float uMouseSphereRadius; | |
| varying vec2 vUv; | |
| float smin(float a, float b, float k) { | |
| float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); | |
| return mix(b, a, h) - k * h * (1.0 - h); | |
| } | |
| float sdSphere(vec3 p, vec3 center, float radius) { | |
| return length(p - center) - radius; | |
| } | |
| float sceneCompound(vec3 p, float t, float k) { | |
| // Two large primary blobs | |
| float angle1 = t * 0.5; | |
| float angle2 = t * 0.5 + 3.14159; | |
| vec3 c1 = vec3( | |
| cos(angle1) * 2.4 + sin(t * 0.25) * 0.3, | |
| sin(angle1 * 0.6) * 0.8 + cos(t * 0.4) * 0.2, | |
| sin(angle1 * 0.35) * 0.6 | |
| ); | |
| vec3 c2 = vec3( | |
| cos(angle2) * 2.4 + sin(t * 0.3) * 0.3, | |
| sin(angle2 * 0.6) * 0.8 - cos(t * 0.35) * 0.2, | |
| sin(angle2 * 0.35) * 0.6 | |
| ); | |
| float s1 = sdSphere(p, c1, 1.2 + 0.07 * sin(t * 2.5)); | |
| float s2 = sdSphere(p, c2, 1.05 + 0.07 * cos(t * 2.0)); | |
| // Medium satellites | |
| vec3 c3 = c1 + vec3(sin(t * 1.8) * 0.9, cos(t * 2.2) * 0.9, sin(t * 1.5) * 0.6); | |
| vec3 c4 = c2 + vec3(-cos(t * 1.5) * 0.8, sin(t * 1.9) * 0.8, -cos(t * 1.7) * 0.5); | |
| float s3 = sdSphere(p, c3, 0.55); | |
| float s4 = sdSphere(p, c4, 0.5); | |
| // Free-floating blobs | |
| vec3 c5 = vec3(sin(t * 0.7) * 3.0, cos(t * 0.55) * 1.2, cos(t * 0.45) * 0.7); | |
| vec3 c6 = vec3(-cos(t * 0.65) * 2.8, sin(t * 0.75) * 1.0, sin(t * 0.5) * 0.8); | |
| float s5 = sdSphere(p, c5, 0.6); | |
| float s6 = sdSphere(p, c6, 0.55); | |
| float d = smin(s1, s2, k); | |
| d = smin(d, s3, k * 0.7); | |
| d = smin(d, s4, k * 0.7); | |
| d = smin(d, s5, k * 0.8); | |
| d = smin(d, s6, k * 0.8); | |
| return d; | |
| } | |
| float sceneSDF(vec3 p) { | |
| float t = uTime * uSpeed; | |
| float k = uGooeyness; | |
| float d = sceneCompound(p, t, k); | |
| if (uMouseSphereRadius > 0.001) { | |
| float ms = sdSphere(p, uMouseSpherePos, uMouseSphereRadius); | |
| d = smin(d, ms, k * 0.8); | |
| } | |
| return d; | |
| } | |
| vec3 calcNormal(vec3 p) { | |
| const float eps = 0.001; | |
| vec2 h = vec2(eps, 0.0); | |
| return normalize(vec3( | |
| sceneSDF(p + h.xyy) - sceneSDF(p - h.xyy), | |
| sceneSDF(p + h.yxy) - sceneSDF(p - h.yxy), | |
| sceneSDF(p + h.yyx) - sceneSDF(p - h.yyx) | |
| )); | |
| } | |
| float calcAO(vec3 pos, vec3 nor) { | |
| float occ = 0.0; | |
| float sca = 1.0; | |
| for (int i = 0; i < AO_STEPS; i++) { | |
| float h = 0.02 + 0.15 * float(i); | |
| float d = sceneSDF(pos + h * nor); | |
| occ += (h - d) * sca; | |
| sca *= 0.9; | |
| } | |
| return clamp(1.0 - 3.0 * occ, 0.0, 1.0); | |
| } | |
| float fresnel(vec3 viewDir, vec3 normal, float power) { | |
| return pow(1.0 - max(dot(viewDir, normal), 0.0), power); | |
| } | |
| // Fake directional shadow via downward AO probe | |
| float cheapShadow(vec3 pos, vec3 lightDir) { | |
| float d1 = sceneSDF(pos + lightDir * 0.15); | |
| float d2 = sceneSDF(pos + lightDir * 0.4); | |
| float d3 = sceneSDF(pos + lightDir * 0.8); | |
| return clamp(0.3 + 0.7 * smoothstep(0.0, 0.3, min(min(d1, d2), d3)), 0.0, 1.0); | |
| } | |
| mat3 setCamera(vec3 ro, vec3 ta, float cr) { | |
| vec3 cw = normalize(ta - ro); | |
| vec3 cp = vec3(sin(cr), cos(cr), 0.0); | |
| vec3 cu = normalize(cross(cw, cp)); | |
| vec3 cv = normalize(cross(cu, cw)); | |
| return mat3(cu, cv, cw); | |
| } | |
| void main() { | |
| vec2 fragCoord = vUv * uResolution; | |
| vec2 uv = (2.0 * fragCoord - uResolution) / uResolution.y; | |
| vec3 ro = uCameraPos; | |
| vec3 ta = uCameraTarget; | |
| mat3 ca = setCamera(ro, ta, 0.0); | |
| vec3 rd = ca * normalize(vec3(uv, 1.8)); | |
| float t = 0.0; | |
| float d; | |
| vec3 p; | |
| bool hit = false; | |
| for (int i = 0; i < MARCH_STEPS; i++) { | |
| p = ro + rd * t; | |
| d = sceneSDF(p); | |
| if (d < 0.002) { hit = true; break; } | |
| t += d * 0.9; | |
| if (t > 15.0) break; | |
| } | |
| vec3 col = vec3(0.02, 0.02, 0.04); | |
| col += vec3(0.03, 0.01, 0.06) * (1.0 - uv.y * 0.5); | |
| if (hit) { | |
| vec3 nor = calcNormal(p); | |
| vec3 viewDir = normalize(ro - p); | |
| vec3 lightPos1 = vec3(3.0, 4.0, 5.0); | |
| vec3 lightPos2 = vec3(-4.0, 2.0, -3.0); | |
| vec3 lightDir1 = normalize(lightPos1 - p); | |
| vec3 lightDir2 = normalize(lightPos2 - p); | |
| float diff1 = max(dot(nor, lightDir1), 0.0); | |
| float diff2 = max(dot(nor, lightDir2), 0.0); | |
| vec3 halfDir1 = normalize(lightDir1 + viewDir); | |
| vec3 halfDir2 = normalize(lightDir2 + viewDir); | |
| float spec1 = pow(max(dot(nor, halfDir1), 0.0), 64.0); | |
| float spec2 = pow(max(dot(nor, halfDir2), 0.0), 32.0); | |
| float sha1 = cheapShadow(p + nor * 0.01, lightDir1); | |
| float sha2 = cheapShadow(p + nor * 0.01, lightDir2); | |
| float ao = calcAO(p, nor); | |
| float fres = fresnel(viewDir, nor, 3.0); | |
| float sss = max(0.0, dot(viewDir, -lightDir1)) * 0.3; | |
| vec3 baseColor1 = vec3(0.8, 0.15, 0.3); | |
| vec3 baseColor2 = vec3(0.15, 0.4, 0.9); | |
| vec3 baseColor3 = vec3(0.95, 0.5, 0.1); | |
| float colorMix = sin(p.x * 1.5 + uTime * 0.5) * 0.5 + 0.5; | |
| float colorMix2 = cos(p.y * 2.0 - uTime * 0.3) * 0.5 + 0.5; | |
| vec3 baseColor = mix(baseColor1, baseColor2, colorMix); | |
| baseColor = mix(baseColor, baseColor3, colorMix2 * 0.3); | |
| vec3 diffuse = baseColor * (diff1 * sha1 * vec3(1.0, 0.95, 0.9) * 0.8 + diff2 * sha2 * vec3(0.4, 0.5, 0.9) * 0.4); | |
| vec3 specular = vec3(1.0, 0.95, 0.9) * spec1 * sha1 * 0.7 + vec3(0.5, 0.6, 1.0) * spec2 * sha2 * 0.3; | |
| vec3 ambient = baseColor * vec3(0.08, 0.06, 0.12) * ao; | |
| vec3 rim = mix(vec3(0.4, 0.6, 1.0), vec3(1.0, 0.4, 0.6), colorMix) * fres * 0.6; | |
| vec3 subsurface = baseColor * sss * vec3(1.0, 0.3, 0.2); | |
| col = ambient + diffuse + specular + rim + subsurface; | |
| float iridescence = fres * 0.4; | |
| vec3 iriColor = vec3( | |
| sin(dot(nor, vec3(1.0, 0.0, 0.0)) * 6.0 + uTime) * 0.5 + 0.5, | |
| sin(dot(nor, vec3(0.0, 1.0, 0.0)) * 6.0 + uTime * 1.3) * 0.5 + 0.5, | |
| sin(dot(nor, vec3(0.0, 0.0, 1.0)) * 6.0 + uTime * 0.7) * 0.5 + 0.5 | |
| ); | |
| col += iriColor * iridescence; | |
| vec3 ref = reflect(-viewDir, nor); | |
| float envRefl = smoothstep(-0.2, 1.0, ref.y) * 0.15; | |
| col += vec3(0.3, 0.4, 0.8) * envRefl * fres; | |
| } | |
| col = col / (col + vec3(1.0)); | |
| col = pow(col, vec3(1.0 / 2.2)); | |
| float vig = 1.0 - 0.3 * dot(uv * 0.5, uv * 0.5); | |
| col *= vig; | |
| gl_FragColor = vec4(col, 1.0); | |
| } | |
| `, | |
| depthWrite: false, | |
| depthTest: false | |
| }); | |
| const quad = new THREE.Mesh(quadGeometry, quadMaterial); | |
| quad.name = 'raymarchQuad'; | |
| quad.frustumCulled = false; | |
| const quadScene = new THREE.Scene(); | |
| const quadCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); | |
| quadScene.add(quad); | |
| // ============ DOT MATRIX SHADER ============ | |
| const DotMatrixShader = { | |
| uniforms: { | |
| tDiffuse: { value: null }, | |
| uResolution: { value: new THREE.Vector2(size.width, size.height) }, | |
| uDotSize: { value: 5.0 }, | |
| uDotGap: { value: 2.5 }, | |
| uBrightness: { value: 0.85 }, | |
| uContrast: { value: 0.60 }, | |
| uThreshold: { value: 0.03 }, | |
| uDotColor: { value: new THREE.Vector3(1.0, 1.0, 1.0) }, | |
| uBgColor: { value: new THREE.Vector3(0.00784, 0.00784, 0.01176) }, | |
| uCrossEnabled: { value: 0.0 }, | |
| uCrossIntensity: { value: 0.95 }, | |
| uCrossAngle: { value: 0.4363 }, | |
| uBloomEnabled: { value: 1.0 }, | |
| uBloomIntensity: { value: 0.55 }, | |
| uBloomSize: { value: 1.50 }, | |
| uCrtEnabled: { value: 1.0 }, | |
| uCrtCurvature: { value: 0.0 }, | |
| uCrtScanlines: { value: 0.75 }, | |
| uCrtVignette: { value: 2.00 }, | |
| uCrtChroma: { value: 0.0 }, | |
| uDitherEnabled: { value: 1.0 }, | |
| uTime: { value: 0 } | |
| }, | |
| vertexShader: ` | |
| varying vec2 vUv; | |
| void main() { | |
| vUv = uv; | |
| gl_Position = vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| precision highp float; | |
| uniform sampler2D tDiffuse; | |
| uniform vec2 uResolution; | |
| uniform float uDotSize; | |
| uniform float uDotGap; | |
| uniform float uBrightness; | |
| uniform float uContrast; | |
| uniform float uThreshold; | |
| uniform vec3 uDotColor; | |
| uniform vec3 uBgColor; | |
| uniform float uCrossEnabled; | |
| uniform float uCrossIntensity; | |
| uniform float uCrossAngle; | |
| uniform float uBloomEnabled; | |
| uniform float uBloomIntensity; | |
| uniform float uBloomSize; | |
| uniform float uCrtEnabled; | |
| uniform float uCrtCurvature; | |
| uniform float uCrtScanlines; | |
| uniform float uCrtVignette; | |
| uniform float uCrtChroma; | |
| uniform float uDitherEnabled; | |
| uniform float uTime; | |
| varying vec2 vUv; | |
| vec2 crtDistort(vec2 uv, float k) { | |
| vec2 cc = uv - 0.5; | |
| float r2 = dot(cc, cc); | |
| float f = 1.0 + r2 * k * 0.01; | |
| return cc * f + 0.5; | |
| } | |
| void main() { | |
| vec2 uv = vUv; | |
| if (uCrtEnabled > 0.5) { | |
| uv = crtDistort(uv, uCrtCurvature); | |
| if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { | |
| gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); | |
| return; | |
| } | |
| } | |
| vec3 col; | |
| if (uCrtEnabled > 0.5 && uCrtChroma > 0.01) { | |
| vec2 dir = (uv - 0.5) * uCrtChroma * 0.002; | |
| col.r = texture2D(tDiffuse, uv + dir).r; | |
| col.g = texture2D(tDiffuse, uv).g; | |
| col.b = texture2D(tDiffuse, uv - dir).b; | |
| } else { | |
| col = texture2D(tDiffuse, uv).rgb; | |
| } | |
| if (uDitherEnabled < 0.5) { | |
| if (uCrtEnabled > 0.5 && uCrtScanlines > 0.001) { | |
| float scanline = sin(uv.y * uResolution.y * 0.8) * 0.5 + 0.5; | |
| col *= 1.0 - uCrtScanlines * (1.0 - scanline); | |
| } | |
| if (uCrtEnabled > 0.5 && uCrtVignette > 0.001) { | |
| vec2 vig = uv * (1.0 - uv); | |
| float vigMask = pow(vig.x * vig.y * 16.0, uCrtVignette * 0.3); | |
| col *= vigMask; | |
| } | |
| gl_FragColor = vec4(col, 1.0); | |
| return; | |
| } | |
| vec2 pixelCoord = uv * uResolution; | |
| float spacing = uDotSize + uDotGap; | |
| vec2 cell = floor(pixelCoord / spacing); | |
| vec2 cellCenter = (cell + 0.5) * spacing; | |
| vec2 sampleUV = cellCenter / uResolution; | |
| vec3 cellCol; | |
| if (uCrtEnabled > 0.5 && uCrtChroma > 0.01) { | |
| vec2 dir = (sampleUV - 0.5) * uCrtChroma * 0.002; | |
| cellCol.r = texture2D(tDiffuse, sampleUV + dir).r; | |
| cellCol.g = texture2D(tDiffuse, sampleUV).g; | |
| cellCol.b = texture2D(tDiffuse, sampleUV - dir).b; | |
| } else { | |
| cellCol = texture2D(tDiffuse, sampleUV).rgb; | |
| } | |
| float lum = dot(cellCol, vec3(0.299, 0.587, 0.114)); | |
| lum *= uBrightness; | |
| lum = (lum - 0.5) * (1.0 / uContrast) + 0.5; | |
| lum = clamp(lum, 0.0, 1.0); | |
| vec3 bgColor = uBgColor; | |
| if (lum < uThreshold) { | |
| vec3 result = bgColor; | |
| if (uCrtEnabled > 0.5 && uCrtScanlines > 0.001) { | |
| float scanline = sin(uv.y * uResolution.y * 0.8) * 0.5 + 0.5; | |
| result *= 1.0 - uCrtScanlines * (1.0 - scanline) * 0.3; | |
| } | |
| if (uCrtEnabled > 0.5 && uCrtVignette > 0.001) { | |
| vec2 vig = uv * (1.0 - uv); | |
| float vigMask = pow(vig.x * vig.y * 16.0, uCrtVignette * 0.3); | |
| result *= vigMask; | |
| } | |
| gl_FragColor = vec4(result, 1.0); | |
| return; | |
| } | |
| float maxRadius = uDotSize * 0.5; | |
| float minRadius = 0.4; | |
| float lumCurve = pow(lum, uContrast); | |
| float dotRadius = mix(minRadius, maxRadius, lumCurve); | |
| vec2 d = pixelCoord - cellCenter; | |
| vec2 absD = abs(d); | |
| float cornerRadius = mix(0.8, 0.2, lumCurve); | |
| float squareness = smoothstep(0.15, 0.7, lumCurve); | |
| float circleDist = length(d); | |
| float circleEdge = 1.0 - smoothstep(dotRadius - 0.5, dotRadius + 0.5, circleDist); | |
| vec2 qd = absD - vec2(dotRadius - cornerRadius); | |
| float rsDist = length(max(qd, 0.0)) + min(max(qd.x, qd.y), 0.0) - cornerRadius; | |
| float squareEdge = 1.0 - smoothstep(-0.5, 0.5, rsDist); | |
| float dotMask = mix(circleEdge, squareEdge, squareness); | |
| vec3 brightColor = uDotColor; | |
| vec3 midColor = uDotColor * 0.7; | |
| vec3 dimColor = uDotColor * 0.25; | |
| vec3 colNorm = cellCol / (max(max(cellCol.r, cellCol.g), cellCol.b) + 0.001); | |
| vec3 dotColor; | |
| if (lum > 0.65) { | |
| dotColor = mix(brightColor, brightColor * colNorm * 1.5, 0.2); | |
| dotColor *= 1.0 + (lum - 0.65) * 1.5; | |
| } else if (lum > 0.25) { | |
| float mt = (lum - 0.25) / 0.4; | |
| dotColor = mix(midColor, brightColor, mt); | |
| dotColor = mix(dotColor, dotColor * colNorm * 1.2, 0.15); | |
| } else { | |
| float st = lum / 0.25; | |
| dotColor = mix(dimColor * 0.5, dimColor, st); | |
| } | |
| float shadowCrosshatch = 0.0; | |
| if (uCrossEnabled > 0.5 && lum < 0.35) { | |
| float crossSpacing = spacing * 0.7; | |
| float ca = cos(uCrossAngle); | |
| float sa = sin(uCrossAngle); | |
| vec2 rotP = vec2(ca * pixelCoord.x - sa * pixelCoord.y, sa * pixelCoord.x + ca * pixelCoord.y); | |
| vec2 crossCell = floor(rotP / crossSpacing); | |
| vec2 crossCenter = (crossCell + 0.5) * crossSpacing; | |
| float crossDist = length(rotP - crossCenter); | |
| float shadowIntensity = smoothstep(0.35, 0.05, lum); | |
| float crossRadius = mix(0.3, maxRadius * 0.35, shadowIntensity); | |
| shadowCrosshatch = 1.0 - smoothstep(crossRadius - 0.4, crossRadius + 0.4, crossDist); | |
| shadowCrosshatch *= shadowIntensity * uCrossIntensity; | |
| } | |
| float bloomMask = 0.0; | |
| if (uBloomEnabled > 0.5 && lum > 0.6) { | |
| float bloomRadius = dotRadius * uBloomSize; | |
| float bloomDist = length(d); | |
| bloomMask = (1.0 - smoothstep(bloomRadius - 1.0, bloomRadius + 1.0, bloomDist)); | |
| bloomMask *= smoothstep(0.6, 1.0, lum) * uBloomIntensity * 0.5; | |
| } | |
| vec3 result = bgColor; | |
| vec3 crossColor = dimColor * 0.4; | |
| result = mix(result, crossColor, shadowCrosshatch); | |
| result += brightColor * bloomMask; | |
| result = mix(result, dotColor, dotMask); | |
| float innerMask = 0.0; | |
| if (squareness > 0.5) { | |
| vec2 qd2 = absD - vec2(dotRadius * 0.85 - cornerRadius); | |
| float rsDist2 = length(max(qd2, 0.0)) + min(max(qd2.x, qd2.y), 0.0) - cornerRadius; | |
| innerMask = 1.0 - smoothstep(-0.5, 0.5, rsDist2); | |
| } else { | |
| innerMask = 1.0 - smoothstep(dotRadius * 0.7 - 0.5, dotRadius * 0.7 + 0.5, circleDist); | |
| } | |
| float phosphorEdge = max(dotMask - innerMask, 0.0); | |
| result += brightColor * phosphorEdge * 0.15 * lum; | |
| if (uCrtEnabled > 0.5 && uCrtScanlines > 0.001) { | |
| float scanline = sin(uv.y * uResolution.y * 0.8) * 0.5 + 0.5; | |
| result *= 1.0 - uCrtScanlines * (1.0 - scanline); | |
| } | |
| if (uCrtEnabled > 0.5 && uCrtVignette > 0.001) { | |
| vec2 vig = uv * (1.0 - uv); | |
| float vigMask = pow(vig.x * vig.y * 16.0, uCrtVignette * 0.3); | |
| result *= vigMask; | |
| } | |
| if (uCrtEnabled > 0.5) { | |
| result = result; | |
| } | |
| gl_FragColor = vec4(result, 1.0); | |
| } | |
| ` | |
| }; | |
| // ============ POST PROCESSING ============ | |
| // Use a standard RenderPass for the quad scene — the raymarching resolution | |
| // is controlled by uResolution on the quad material (set to half-res), | |
| // while the composer and dot-matrix shader run at full native resolution. | |
| const composer = new EffectComposer(renderer); | |
| composer.setSize(size.width, size.height); | |
| const renderPass = new RenderPass(quadScene, quadCamera); | |
| composer.addPass(renderPass); | |
| const dotMatrixPass = new ShaderPass(DotMatrixShader); | |
| composer.addPass(dotMatrixPass); | |
| const outputPass = new OutputPass(); | |
| composer.addPass(outputPass); | |
| // Dot matrix runs at full resolution for crisp patterns | |
| dotMatrixPass.uniforms.uResolution.value.set(size.width, size.height); | |
| // ============ SCROLL PARALLAX ============ | |
| let scrollY = 0; | |
| let smoothScrollY = 0; | |
| window.addEventListener('scroll', () => { | |
| scrollY = window.scrollY; | |
| }, { passive: true }); | |
| // ============ ANIMATION LOOP ============ | |
| const clock = new THREE.Clock(); | |
| const raycaster = new THREE.Raycaster(); | |
| const _forward = new THREE.Vector3(); | |
| let resizeTimeout = null; | |
| // ============ FPS WATCHDOG — auto-downgrade if <30fps ============ | |
| const tierOrder = ['high', 'mid', 'low']; | |
| let fpsFrames = 0; | |
| let fpsStartTime = performance.now(); | |
| let fpsWatchdogActive = true; | |
| const FPS_SAMPLE_WINDOW = 2000; // 2 seconds | |
| const FPS_THRESHOLD = 30; | |
| function downgradeQuality() { | |
| const currentIndex = tierOrder.indexOf(currentTier); | |
| if (currentIndex >= tierOrder.length - 1) { | |
| // Already at lowest tier | |
| fpsWatchdogActive = false; | |
| return; | |
| } | |
| const nextTier = tierOrder[currentIndex + 1]; | |
| currentTier = nextTier; | |
| quality = { ...qualityPresets[currentTier] }; | |
| // Apply new quality — update pixel ratio | |
| const pr = Math.min(window.devicePixelRatio, quality.pixelRatio); | |
| renderer.setPixelRatio(pr); | |
| renderer.setSize(size.width, size.height); | |
| composer.setSize(size.width, size.height); | |
| // Update dot matrix uniforms | |
| dotMatrixPass.uniforms.uDotSize.value = quality.dotSize; | |
| dotMatrixPass.uniforms.uDotGap.value = quality.dotGap; | |
| dotMatrixPass.uniforms.uCrtScanlines.value = quality.scanlines; | |
| dotMatrixPass.uniforms.uBloomEnabled.value = quality.bloomEnabled ? 1.0 : 0.0; | |
| // Reset watchdog for another sample window | |
| fpsFrames = 0; | |
| fpsStartTime = performance.now(); | |
| // If we hit the lowest tier, stop watching | |
| if (currentIndex + 1 >= tierOrder.length - 1) { | |
| fpsWatchdogActive = false; | |
| } | |
| // Update tier display | |
| const tierEl = document.getElementById('settings-tier'); | |
| if (tierEl) tierEl.textContent = `GPU Tier: ${currentTier} (auto)`; | |
| } | |
| function animate() { | |
| // Skip rendering when the tab is hidden or the hero has scrolled off screen (see heroOnScreen). | |
| if (!pageVisible || !heroOnScreen) return; | |
| const dt = Math.min(clock.getDelta(), 0.05); // Clamp delta to avoid huge jumps | |
| const elapsed = clock.elapsedTime; | |
| controls.update(); | |
| const targetR = mouseInScene ? (mousePressed ? mouseSphereClickRadius : mouseSphereTargetRadius) : 0.0; | |
| const fadeSpeed = mouseInScene ? (mousePressed ? 10.0 : 6.0) : 3.0; | |
| const step = Math.min(1.0, fadeSpeed * dt); | |
| mouseSphereRadius += (targetR - mouseSphereRadius) * step; | |
| if (mouseSphereRadius < 0.005 && !mouseInScene) mouseSphereRadius = 0.0; | |
| raycaster.setFromCamera(mouse, camera); | |
| const rayDir = raycaster.ray.direction; | |
| const rayOrigin = raycaster.ray.origin; | |
| _forward.subVectors(controls.target, camera.position).normalize(); | |
| const dist = camera.position.distanceTo(controls.target); | |
| const t = dist / rayDir.dot(_forward); | |
| mouseWorldTarget.copy(rayOrigin).addScaledVector(rayDir, t); | |
| mouseWorld.lerp(mouseWorldTarget, mouseDamping); | |
| // Smooth scroll interpolation for parallax | |
| smoothScrollY += (scrollY - smoothScrollY) * 0.1; | |
| const vh = window.innerHeight; | |
| const scrollProgress = Math.min(smoothScrollY / vh, 1.0); | |
| // Parallax: shift camera subtly as user scrolls (3D depth effect) | |
| const baseZ = 5; | |
| const baseCamY = 0; | |
| camera.position.y = baseCamY + scrollProgress * 1.5; | |
| camera.position.z = baseZ + scrollProgress * 0.8; | |
| controls.target.y = scrollProgress * 0.8; | |
| quadMaterial.uniforms.uMouseSpherePos.value.copy(mouseWorld); | |
| quadMaterial.uniforms.uMouseSphereRadius.value = mouseSphereRadius; | |
| quadMaterial.uniforms.uTime.value = elapsed; | |
| quadMaterial.uniforms.uCameraPos.value.copy(camera.position); | |
| quadMaterial.uniforms.uCameraTarget.value.copy(controls.target); | |
| dotMatrixPass.uniforms.uTime.value = elapsed; | |
| // Composer renders the quad scene via RenderPass, then applies dot matrix + CRT | |
| composer.render(); | |
| // FPS watchdog — sample for 2s windows, downgrade if below threshold | |
| if (fpsWatchdogActive) { | |
| fpsFrames++; | |
| const elapsed_ms = performance.now() - fpsStartTime; | |
| if (elapsed_ms >= FPS_SAMPLE_WINDOW) { | |
| const avgFps = (fpsFrames / elapsed_ms) * 1000; | |
| if (avgFps < FPS_THRESHOLD) { | |
| downgradeQuality(); | |
| } else { | |
| // Performance is fine, stop watching | |
| fpsWatchdogActive = false; | |
| } | |
| } | |
| } | |
| } | |
| renderer.setAnimationLoop(animate); | |
| // ============ RESIZE (debounced — render loop never pauses) ============ | |
| function handleResize() { | |
| size = getSize(); | |
| camera.aspect = size.width / size.height; | |
| camera.updateProjectionMatrix(); | |
| const pr = Math.min(window.devicePixelRatio, quality.pixelRatio); | |
| renderer.setPixelRatio(pr); | |
| renderer.setSize(size.width, size.height); | |
| quadMaterial.uniforms.uResolution.value.set(size.width, size.height); | |
| quadMaterial.uniforms.uPixelRatio.value = pr; | |
| dotMatrixPass.uniforms.uResolution.value.set(size.width, size.height); | |
| composer.setSize(size.width, size.height); | |
| } | |
| window.addEventListener('resize', () => { | |
| clearTimeout(resizeTimeout); | |
| // Immediately update the canvas CSS size so it fills the window (no black gaps) | |
| // but defer the expensive framebuffer reallocation | |
| renderer.domElement.style.width = window.innerWidth + 'px'; | |
| renderer.domElement.style.height = window.innerHeight + 'px'; | |
| resizeTimeout = setTimeout(handleResize, 200); | |
| }, { passive: true }); | |
| // ============ DEFERRED INIT — pre-warm the shader after first paint ============ | |
| // Force one render so the first real frame doesn't stutter on shader compile. | |
| if ('requestIdleCallback' in window) { | |
| requestIdleCallback(() => composer.render(), { timeout: 1000 }); | |
| } else { | |
| setTimeout(() => composer.render(), 200); // Safari fallback | |
| } |