1// âââââââââââââââââââââââââââââââââââââââââââââ 2// Slytherin · Cursor field 3// Sparse gold motes that trail the pointer 4// Soft additive particles, mix-blend-multiply for cream bg 5// âââââââââââââââââââââââââââââââââââââââââââââ 6import * as THREE from 'three'; 7 8const VERTEX = /* glsl */` 9 attribute float aSize; 10 attribute float aOffset; 11 uniform float uTime; 12 uniform float uPixelRatio; 13 varying float vAlpha; 14 15 void main() { 16 vec3 pos = position; 17 float t = uTime * 0.6 + aOffset * 6.28; 18 pos.x += sin(t) * 0.04; 19 pos.y += cos(t * 1.13) * 0.04; 20 vec4 mv = modelViewMatrix * vec4(pos, 1.0); 21 gl_Position = projectionMatrix * mv; 22 gl_PointSize = aSize * uPixelRatio * (200.0 / -mv.z); 23 vAlpha = 0.5 + 0.5 * sin(t * 0.7); 24 } 25`; 26 27const FRAGMENT = /* glsl */` 28 precision mediump float; 29 varying float vAlpha; 30 uniform vec3 uColor; 31 void main() { 32 vec2 c = gl_PointCoord - 0.5; 33 float d = length(c); 34 float a = smoothstep(0.5, 0.0, d); 35 a = pow(a, 2.0); 36 gl_FragColor = vec4(uColor, a * vAlpha * 0.55); 37 } 38`; 39 40export function initCursorField(canvas) { 41 if (!canvas) return null; 42 const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 43 if (reduce) return null; 44 45 const renderer = new THREE.WebGLRenderer({ 46 canvas, 47 antialias: true, 48 alpha: true, 49 powerPreference: 'high-performance', 50 }); 51 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.6)); 52 renderer.setClearColor(0x000000, 0); 53 54 const scene = new THREE.Scene(); 55 const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100); 56 camera.position.z = 3; 57 58 // Mote follower group 59 const COUNT = 28; 60 const positions = new Float32Array(COUNT * 3); 61 const sizes = new Float32Array(COUNT); 62 const offsets = new Float32Array(COUNT); 63 const trail = []; // history of mouse positions 64 65 for (let i = 0; i < COUNT; i++) { 66 positions[i * 3] = 0; 67 positions[i * 3 + 1] = 0; 68 positions[i * 3 + 2] = 0; 69 sizes[i] = 4 + Math.random() * 8; 70 offsets[i] = Math.random(); 71 trail.push({ x: 0, y: 0 }); 72 } 73 74 const geo = new THREE.BufferGeometry(); 75 geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 76 geo.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1)); 77 geo.setAttribute('aOffset', new THREE.BufferAttribute(offsets, 1)); 78 79 const uniforms = { 80 uTime: { value: 0 }, 81 uPixelRatio: { value: renderer.getPixelRatio() }, 82 uColor: { value: new THREE.Color('#C8965A') }, 83 }; 84 85 const mat = new THREE.ShaderMaterial({ 86 uniforms, 87 vertexShader: VERTEX, 88 fragmentShader: FRAGMENT, 89 transparent: true, 90 depthWrite: false, 91 blending: THREE.AdditiveBlending, 92 }); 93 94 const points = new THREE.Points(geo, mat); 95 scene.add(points); 96 97 // Reveal canvas now that WebGL is ready 98 canvas.classList.add('is-active'); 99 100 // Resize 101 const resize = () => { 102 const w = window.innerWidth; 103 const h = window.innerHeight; 104 renderer.setSize(w, h, false); 105 camera.aspect = w / h; 106 camera.updateProjectionMatrix(); 107 }; 108 resize(); 109 window.addEventListener('resize', resize); 110 111 // Mouse â world coords (z = 0 plane) 112 const mouse = new THREE.Vector2(0, 0); 113 const ndc = new THREE.Vector2(0, 0); 114 const target = new THREE.Vector3(0, 0, 0); 115 const raycaster = new THREE.Raycaster(); 116 const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); 117 118 let active = false; 119 window.addEventListener('mousemove', (e) => { 120 active = true; 121 ndc.x = (e.clientX / window.innerWidth) * 2 - 1; 122 ndc.y = -(e.clientY / window.innerHeight) * 2 + 1; 123 }); 124 window.addEventListener('mouseleave', () => { active = false; }); 125 126 // Trail update 127 let lastT = 0; 128 const clock = new THREE.Clock(); 129 130 function tick() { 131 const t = clock.getElapsedTime(); 132 uniforms.uTime.value = t; 133 134 // Project mouse to world 135 raycaster.setFromCamera(ndc, camera); 136 const hit = new THREE.Vector3(); 137 raycaster.ray.intersectPlane(plane, hit); 138 if (hit) target.copy(hit); 139 140 // Push to trail every 16ms 141 if (t - lastT > 0.016) { 142 lastT = t; 143 trail.unshift({ x: target.x, y: target.y }); 144 if (trail.length > COUNT) trail.length = COUNT; 145 } 146 147 // Animate motes toward trail with damping 148 for (let i = 0; i < COUNT; i++) { 149 const tr = trail[i] || trail[trail.length - 1]; 150 const px = positions[i * 3]; 151 const py = positions[i * 3 + 1]; 152 const damp = 0.08 + (i / COUNT) * 0.04; 153 positions[i * 3] = px + (tr.x - px) * damp; 154 positions[i * 3 + 1] = py + (tr.y - py) * damp; 155 } 156 geo.attributes.position.needsUpdate = true; 157 158 if (active) renderer.render(scene, camera); 159 requestAnimationFrame(tick); 160 } 161 tick(); 162 163 return { renderer }; 164}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.