PageSourceSearch

https://slytherin.ai/js/three/cursor-field.js

js slytherin.ai collected 2026-10-05 12:38:28 UTC 4,896 bytes, 164 lines download raw bytes

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.