| 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'; |
|
|
| |
| 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() : ''; |
| |
| if (/apple gpu|apple m/.test(gpu)) return 'high'; |
| |
| if (/swiftshader|llvmpipe|mali-4|adreno 3/.test(gpu)) return 'low'; |
| if (/intel(?!.*(iris|uhd|arc))/.test(gpu)) return 'low'; |
| |
| if (/mali-g[567]|adreno [45]|intel (iris|uhd)|geforce (mx|gt)|radeon (rx )?(5[0-4]|vega 8)/.test(gpu)) return 'mid'; |
| |
| 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] }; |
|
|
| |
| 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 } |
| }; |
|
|
| |
| 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; |
|
|
| |
| 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; |
|
|
| |
| let pageVisible = true; |
| |
| |
| |
| 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; |
| }); |
| |
| |
| |
| 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 }); |
|
|
| |
| 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); |
|
|
| |
| 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); |
| } |
| ` |
| }; |
|
|
| |
| |
| |
| |
| 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); |
|
|
| |
| dotMatrixPass.uniforms.uResolution.value.set(size.width, size.height); |
|
|
| |
| let scrollY = 0; |
| let smoothScrollY = 0; |
|
|
| window.addEventListener('scroll', () => { |
| scrollY = window.scrollY; |
| }, { passive: true }); |
|
|
| |
| const clock = new THREE.Clock(); |
| const raycaster = new THREE.Raycaster(); |
| const _forward = new THREE.Vector3(); |
| let resizeTimeout = null; |
|
|
| |
| const tierOrder = ['high', 'mid', 'low']; |
| let fpsFrames = 0; |
| let fpsStartTime = performance.now(); |
| let fpsWatchdogActive = true; |
| const FPS_SAMPLE_WINDOW = 2000; |
| const FPS_THRESHOLD = 30; |
|
|
| function downgradeQuality() { |
| const currentIndex = tierOrder.indexOf(currentTier); |
| if (currentIndex >= tierOrder.length - 1) { |
| |
| fpsWatchdogActive = false; |
| return; |
| } |
| const nextTier = tierOrder[currentIndex + 1]; |
| currentTier = nextTier; |
| quality = { ...qualityPresets[currentTier] }; |
|
|
| |
| const pr = Math.min(window.devicePixelRatio, quality.pixelRatio); |
| renderer.setPixelRatio(pr); |
| renderer.setSize(size.width, size.height); |
| composer.setSize(size.width, size.height); |
|
|
| |
| 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; |
|
|
| |
| fpsFrames = 0; |
| fpsStartTime = performance.now(); |
|
|
| |
| if (currentIndex + 1 >= tierOrder.length - 1) { |
| fpsWatchdogActive = false; |
| } |
|
|
| |
| const tierEl = document.getElementById('settings-tier'); |
| if (tierEl) tierEl.textContent = `GPU Tier: ${currentTier} (auto)`; |
| } |
|
|
| function animate() { |
| |
| if (!pageVisible || !heroOnScreen) return; |
|
|
| const dt = Math.min(clock.getDelta(), 0.05); |
| 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); |
|
|
| |
| smoothScrollY += (scrollY - smoothScrollY) * 0.1; |
| const vh = window.innerHeight; |
| const scrollProgress = Math.min(smoothScrollY / vh, 1.0); |
|
|
| |
| 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.render(); |
|
|
| |
| 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 { |
| |
| fpsWatchdogActive = false; |
| } |
| } |
| } |
| } |
|
|
| renderer.setAnimationLoop(animate); |
|
|
| |
| 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); |
| |
| |
| renderer.domElement.style.width = window.innerWidth + 'px'; |
| renderer.domElement.style.height = window.innerHeight + 'px'; |
| resizeTimeout = setTimeout(handleResize, 200); |
| }, { passive: true }); |
|
|
| |
| |
| if ('requestIdleCallback' in window) { |
| requestIdleCallback(() => composer.render(), { timeout: 1000 }); |
| } else { |
| setTimeout(() => composer.render(), 200); |
| } |