UNPKG

@threlte/extras

Version:

Utilities, abstractions and plugins for your Threlte apps

161 lines (160 loc) 5.43 kB
import { useTask, useThrelte } from '@threlte/core'; import { CanvasTexture } from 'three'; function smoothAverage(current, measurement, smoothing = 0.9) { return measurement * smoothing + current * (1.0 - smoothing); } const easeCircleOut = (x) => Math.sqrt(1 - Math.pow(x - 1, 2)); class TrailTexture { trail = []; canvas; ctx; texture; force = 0; size; maxAge; radius; intensity; ease; minForce; interpolate; smoothing; blend; constructor({ size = 256, maxAge = 750, radius = 0.3, intensity = 0.2, interpolate = 0, smoothing = 0, minForce = 0.3, blend = 'screen', ease = easeCircleOut } = {}) { this.size = size; this.maxAge = maxAge; this.radius = radius; this.intensity = intensity; this.ease = ease; this.interpolate = interpolate; this.smoothing = smoothing; this.minForce = minForce; this.blend = blend; this.canvas = document.createElement('canvas'); this.canvas.width = this.canvas.height = this.size; const ctx = this.canvas.getContext('2d'); if (!ctx) throw new Error('Could not get 2D canvas context'); this.ctx = ctx; this.ctx.fillStyle = 'black'; this.ctx.fillRect(0, 0, this.size, this.size); this.texture = new CanvasTexture(this.canvas); } update(delta) { this.clear(); for (let i = this.trail.length - 1; i >= 0; i--) { this.trail[i].age += delta * 1000; if (this.trail[i].age > this.maxAge) { this.trail.splice(i, 1); } } if (!this.trail.length) this.force = 0; for (const point of this.trail) { this.drawTouch(point); } this.texture.needsUpdate = true; } clear() { this.ctx.globalCompositeOperation = 'source-over'; this.ctx.fillStyle = 'black'; this.ctx.fillRect(0, 0, this.size, this.size); } addTouch(x, y) { const last = this.trail[this.trail.length - 1]; if (last) { const dx = last.x - x; const dy = last.y - y; const dd = dx * dx + dy * dy; const force = Math.max(this.minForce, Math.min(dd * 10000, 1)); this.force = smoothAverage(force, this.force, this.smoothing); if (this.interpolate) { const lines = Math.ceil(dd / Math.pow((this.radius * 0.5) / this.interpolate, 2)); if (lines > 1) { for (let i = 1; i < lines; i++) { this.trail.push({ x: last.x - (dx / lines) * i, y: last.y - (dy / lines) * i, age: 0, force }); } } } } this.trail.push({ x, y, age: 0, force: this.force }); } drawTouch(point) { const pos = { x: point.x * this.size, y: (1 - point.y) * this.size }; let intensity = 1; if (point.age < this.maxAge * 0.3) { intensity = this.ease(point.age / (this.maxAge * 0.3)); } else { intensity = this.ease(1 - (point.age - this.maxAge * 0.3) / (this.maxAge * 0.7)); } intensity *= point.force; this.ctx.globalCompositeOperation = this.blend; const radius = this.size * this.radius * intensity; const grd = this.ctx.createRadialGradient(pos.x, pos.y, Math.max(0, radius * 0.25), pos.x, pos.y, Math.max(0, radius)); grd.addColorStop(0, `rgba(255, 255, 255, ${this.intensity})`); grd.addColorStop(1, `rgba(0, 0, 0, 0.0)`); this.ctx.beginPath(); this.ctx.fillStyle = grd; this.ctx.arc(pos.x, pos.y, Math.max(0, radius), 0, Math.PI * 2); this.ctx.fill(); } } /** * Creates a canvas-based trail texture that responds to pointer movement. * The texture renders a fading trail of points that can be used as a * displacement map, alpha map, or any other texture-driven effect. * * @example * ```svelte * <script> * const { texture, onPointerMove } = useTrailTexture() * </script> * * <T.Mesh onpointermove={onPointerMove}> * <T.PlaneGeometry /> * <T.MeshStandardMaterial displacementMap={texture} /> * </T.Mesh> * ``` */ export const useTrailTexture = (config) => { const { invalidate } = useThrelte(); const trail = new TrailTexture(config?.() ?? {}); $effect(() => { const c = config?.() ?? {}; trail.maxAge = c.maxAge ?? 750; trail.radius = c.radius ?? 0.3; trail.intensity = c.intensity ?? 0.2; trail.interpolate = c.interpolate ?? 0; trail.smoothing = c.smoothing ?? 0; trail.minForce = c.minForce ?? 0.3; trail.blend = c.blend ?? 'screen'; trail.ease = c.ease ?? easeCircleOut; }); useTask((delta) => { trail.update(delta); if (trail.trail.length > 0) { invalidate(); } }, { autoInvalidate: false }); const setTrail = (x, y) => { trail.addTouch(x, y); }; const onPointerMove = (event) => { if (event.uv) { setTrail(event.uv.x, event.uv.y); } }; return { texture: trail.texture, onPointerMove, setTrail }; };