Spaces:
Running
Running
Download src/particles.js from jedisct1/edge-runner: direct link, hf CLI and curl.
- Browser
- Download file 7.54 kB
-
https://huggingface.co/spaces/jedisct1/edge-runner/resolve/main/src/particles.js
- Command line
-
hf download hf://spaces/jedisct1/edge-runner/src/particles.js
-
curl -L -o particles.js https://huggingface.co/spaces/jedisct1/edge-runner/resolve/main/src/particles.js
7.54 kB
| // Additive point sprites for speed streaks, pickups and impacts. One Points | |
| // object with a pool of particles, so effects never touch the garbage collector | |
| // during play. | |
| import * as THREE from 'three'; | |
| import { PALETTE } from './config.js'; | |
| const MAX = 900; | |
| function spriteTexture() { | |
| const c = document.createElement('canvas'); | |
| c.width = 64; | |
| c.height = 64; | |
| const ctx = c.getContext('2d'); | |
| const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32); | |
| g.addColorStop(0, 'rgba(255,255,255,1)'); | |
| g.addColorStop(0.3, 'rgba(255,255,255,0.7)'); | |
| g.addColorStop(1, 'rgba(255,255,255,0)'); | |
| ctx.fillStyle = g; | |
| ctx.fillRect(0, 0, 64, 64); | |
| return new THREE.CanvasTexture(c); | |
| } | |
| export class Particles { | |
| constructor(scene) { | |
| this.positions = new Float32Array(MAX * 3); | |
| this.colors = new Float32Array(MAX * 3); | |
| this.sizes = new Float32Array(MAX); | |
| this.alpha = new Float32Array(MAX); | |
| this.vel = new Float32Array(MAX * 3); | |
| this.life = new Float32Array(MAX); | |
| this.maxLife = new Float32Array(MAX); | |
| this.drag = new Float32Array(MAX); | |
| this.gravity = new Float32Array(MAX); | |
| this.baseSize = new Float32Array(MAX); | |
| this.cursor = 0; | |
| const geo = new THREE.BufferGeometry(); | |
| geo.setAttribute('position', new THREE.BufferAttribute(this.positions, 3)); | |
| geo.setAttribute('aColor', new THREE.BufferAttribute(this.colors, 3)); | |
| geo.setAttribute('aSize', new THREE.BufferAttribute(this.sizes, 1)); | |
| geo.setAttribute('aAlpha', new THREE.BufferAttribute(this.alpha, 1)); | |
| geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, -100), 400); | |
| const mat = new THREE.ShaderMaterial({ | |
| transparent: true, | |
| depthWrite: false, | |
| blending: THREE.AdditiveBlending, | |
| uniforms: { | |
| uTexture: { value: spriteTexture() }, | |
| uPixelRatio: { value: Math.min(devicePixelRatio, 2) }, | |
| }, | |
| vertexShader: ` | |
| attribute vec3 aColor; | |
| attribute float aSize; | |
| attribute float aAlpha; | |
| uniform float uPixelRatio; | |
| varying vec3 vColor; | |
| varying float vAlpha; | |
| void main() { | |
| vColor = aColor; | |
| vAlpha = aAlpha; | |
| vec4 mv = modelViewMatrix * vec4(position, 1.0); | |
| gl_PointSize = aSize * uPixelRatio * (26.0 / max(0.001, -mv.z)); | |
| gl_Position = projectionMatrix * mv; | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform sampler2D uTexture; | |
| varying vec3 vColor; | |
| varying float vAlpha; | |
| void main() { | |
| vec4 tex = texture2D(uTexture, gl_PointCoord); | |
| gl_FragColor = vec4(vColor, tex.a * vAlpha); | |
| } | |
| `, | |
| }); | |
| this.points = new THREE.Points(geo, mat); | |
| this.points.frustumCulled = false; | |
| scene.add(this.points); | |
| this.geo = geo; | |
| this.tmpColor = new THREE.Color(); | |
| } | |
| emit(count, opts) { | |
| const { | |
| position, | |
| spread = { x: 0.2, y: 0.2, z: 0.2 }, | |
| velocity = { x: 0, y: 0, z: 0 }, | |
| velocitySpread = { x: 1, y: 1, z: 1 }, | |
| color = 0xffffff, | |
| colorJitter = 0, | |
| size = 6, | |
| sizeSpread = 0, | |
| life = 0.6, | |
| lifeSpread = 0, | |
| gravity = 0, | |
| drag = 1.5, | |
| } = opts; | |
| for (let i = 0; i < count; i++) { | |
| const idx = this.cursor; | |
| this.cursor = (this.cursor + 1) % MAX; | |
| const i3 = idx * 3; | |
| this.positions[i3] = position.x + (Math.random() - 0.5) * spread.x; | |
| this.positions[i3 + 1] = position.y + (Math.random() - 0.5) * spread.y; | |
| this.positions[i3 + 2] = position.z + (Math.random() - 0.5) * spread.z; | |
| this.vel[i3] = velocity.x + (Math.random() - 0.5) * velocitySpread.x; | |
| this.vel[i3 + 1] = velocity.y + (Math.random() - 0.5) * velocitySpread.y; | |
| this.vel[i3 + 2] = velocity.z + (Math.random() - 0.5) * velocitySpread.z; | |
| this.tmpColor.setHex(color); | |
| const j = 1 + (Math.random() - 0.5) * colorJitter; | |
| this.colors[i3] = THREE.MathUtils.clamp(this.tmpColor.r * j, 0, 4); | |
| this.colors[i3 + 1] = THREE.MathUtils.clamp(this.tmpColor.g * j, 0, 4); | |
| this.colors[i3 + 2] = THREE.MathUtils.clamp(this.tmpColor.b * j, 0, 4); | |
| this.baseSize[idx] = Math.max(0.5, size + (Math.random() - 0.5) * sizeSpread); | |
| this.sizes[idx] = this.baseSize[idx]; | |
| this.maxLife[idx] = life + Math.random() * lifeSpread; | |
| this.life[idx] = this.maxLife[idx]; | |
| this.alpha[idx] = 1; | |
| this.drag[idx] = drag; | |
| this.gravity[idx] = gravity; | |
| } | |
| } | |
| // Trail behind the player, denser and longer at speed. Emitted slightly behind | |
| // so it reads as an exhaust plume rather than a pool of light under the node. | |
| trail(dt, position, speedNorm, boosting) { | |
| const rate = 40 + speedNorm * 190 + (boosting ? 260 : 0); | |
| const count = Math.min(24, Math.round(rate * dt)); | |
| this.emit(count, { | |
| position: { x: position.x, y: position.y, z: position.z - 0.7 }, | |
| spread: { x: 0.34, y: 0.3, z: 0.16 }, | |
| velocity: { x: 0, y: 0.4, z: 7 + speedNorm * 14 }, | |
| velocitySpread: { x: 1.5, y: 1.2, z: 4 }, | |
| color: boosting ? PALETTE.ice : PALETTE.redBright, | |
| colorJitter: 0.4, | |
| size: boosting ? 6 : 4, | |
| sizeSpread: 2.5, | |
| life: 0.35 + speedNorm * 0.25, | |
| drag: 2.6, | |
| }); | |
| } | |
| burst(position, { color = PALETTE.red, count = 46, power = 8 } = {}) { | |
| this.emit(count, { | |
| position, | |
| spread: { x: 0.3, y: 0.3, z: 0.3 }, | |
| velocity: { x: 0, y: 1.5, z: 0 }, | |
| velocitySpread: { x: power, y: power, z: power }, | |
| color, | |
| colorJitter: 0.5, | |
| size: 9, | |
| sizeSpread: 6, | |
| life: 0.7, | |
| lifeSpread: 0.4, | |
| gravity: -12, | |
| drag: 2.2, | |
| }); | |
| } | |
| ring(position, color) { | |
| const count = 34; | |
| for (let i = 0; i < count; i++) { | |
| const a = (i / count) * Math.PI * 2; | |
| this.emit(1, { | |
| position, | |
| spread: { x: 0.05, y: 0.05, z: 0.05 }, | |
| velocity: { x: Math.cos(a) * 6, y: Math.sin(a) * 6, z: 0 }, | |
| velocitySpread: { x: 0.4, y: 0.4, z: 0.4 }, | |
| color, | |
| size: 8, | |
| life: 0.45, | |
| drag: 4, | |
| }); | |
| } | |
| } | |
| // Sparks skidding along the road while landing or boosting. | |
| ground(position, color) { | |
| this.emit(3, { | |
| position, | |
| spread: { x: 0.5, y: 0.05, z: 0.3 }, | |
| velocity: { x: 0, y: 1.4, z: 5 }, | |
| velocitySpread: { x: 2.4, y: 1.6, z: 3 }, | |
| color, | |
| size: 5, | |
| sizeSpread: 3, | |
| life: 0.4, | |
| gravity: -18, | |
| drag: 2, | |
| }); | |
| } | |
| update(dt) { | |
| let live = 0; | |
| for (let i = 0; i < MAX; i++) { | |
| if (this.life[i] <= 0) { | |
| if (this.alpha[i] !== 0) this.alpha[i] = 0; | |
| continue; | |
| } | |
| live++; | |
| this.life[i] -= dt; | |
| const t = Math.max(0, this.life[i] / this.maxLife[i]); | |
| const i3 = i * 3; | |
| const d = Math.max(0, 1 - this.drag[i] * dt); | |
| this.vel[i3] *= d; | |
| this.vel[i3 + 1] = this.vel[i3 + 1] * d + this.gravity[i] * dt; | |
| this.vel[i3 + 2] *= d; | |
| this.positions[i3] += this.vel[i3] * dt; | |
| this.positions[i3 + 1] += this.vel[i3 + 1] * dt; | |
| this.positions[i3 + 2] += this.vel[i3 + 2] * dt; | |
| this.alpha[i] = t * t; | |
| this.sizes[i] = this.baseSize[i] * (0.35 + t * 0.65); | |
| if (this.life[i] <= 0) this.alpha[i] = 0; | |
| } | |
| this.geo.attributes.position.needsUpdate = true; | |
| this.geo.attributes.aColor.needsUpdate = true; | |
| this.geo.attributes.aSize.needsUpdate = true; | |
| this.geo.attributes.aAlpha.needsUpdate = true; | |
| return live; | |
| } | |
| clear() { | |
| this.life.fill(0); | |
| this.alpha.fill(0); | |
| this.geo.attributes.aAlpha.needsUpdate = true; | |
| } | |
| } | |