UNPKG

raindrop-fx

Version:
266 lines (244 loc) 8.2 kB
import { minus, Rect, vec2 } from "@sardinefish/zogra-renderer"; import { RainDrop } from "./raindrop"; import { JitterOption } from "./random"; import { Spawner } from "./spawner"; import { Time } from "./utils"; export interface SimulatorOptions { viewport: Rect; /** * Time range between two raindrop spwan. */ spawnInterval: [number, number]; /** * Random size range when spawn a new raindrop */ spawnSize: [number, number]; /** * Maximal amount of spawned raindrops. * Recommend less than 2000 */ spawnLimit: number; /** * Recommend in range (0..1), other value should be ok. */ slipRate: number; /** * Describe how often a raindrop change its motion state */ motionInterval: [number, number]; /** * Random velociy x relative to velocity y. Recommend in range (0..0.1) */ xShifting: [number, number]; /** * Relative size for collision check. Recommend in range (0.6..1.2) */ colliderSize: number; /** * Mass density of the slipping trail raindrop. * Recommend in range (0.1..0.3) * * A large value cause a raindrop quickly lose its size during slipping. */ trailDropDensity: number; /** * Random size range of slipping trail drop. Recommend in range (0.3..0.5) */ trailDropSize: [number, number]; /** * Random distance range between two slipping trail drop. Recommend in range (20..40) */ trailDistance: [number, number]; /** * Vertical spread of a new spawned slipping trail drop. Recommend in range (0.4..0.6) */ trailSpread: number /** * Spread rate when a new spawned raindrop hit the screen. Recommend in range (0.4..0.7) */ initialSpread: number; /** * Spread shrink rate per seconds. Recommend in range (0.01..0.02) */ shrinkRate: number; /** * Spread rate by velocity Y. Recommend in range (0.2..0.4) * * Raindrop with higher fall speed looks more narrow. */ velocitySpread: number; /** * Mass lose per second. Recommend in range (10..30) */ evaporate: number; /** * Gravity acceleration in pixels/s. Recommend 2400 */ gravity: number; } export class CollisionGrid extends Array<RainDrop> { /**@deprecated */ push(...item :RainDrop[]) { return super.push(...item); } add(raindrop: RainDrop) { const len = super.push(raindrop); raindrop.gridIdx = len - 1; raindrop.grid = this; } delete(raindrop: RainDrop) { this[raindrop.gridIdx as number] = this[this.length - 1]; this[raindrop.gridIdx as number].gridIdx = raindrop.gridIdx; this.length--; raindrop.gridIdx = -1; raindrop.grid = undefined; } } export class RaindropSimulator { options: SimulatorOptions; spawner: Spawner; raindrops: RainDrop[] = []; grid: CollisionGrid[] = []; constructor(options: SimulatorOptions) { this.options = options; this.spawner = new Spawner(this, options); this.resize(); } get gridSize() { return this.options.spawnSize[1] * 0.3 } // max collide distance resize() { const w = Math.ceil(this.options.viewport.size.x / this.gridSize); const h = Math.ceil(this.options.viewport.size.y / this.gridSize); let base = 0; if (this.grid.length < w * h) { base = this.grid.length; this.grid.length = w * h; } for (let i = base; i < this.grid.length; i++) this.grid[i] = new CollisionGrid(); } gridAt(gridX: number, gridY: number) { if (gridX < 0 || gridY < 0) return undefined; const gridWidth = Math.ceil((this.options.viewport.xMax - this.options.viewport.xMin) / this.gridSize); const idx = gridY * gridWidth + gridX; if (idx >= this.grid.length) return undefined; return this.grid[idx]; } gridAtWorldPos(x: number, y: number) { return this.gridAt(...this.worldToGrid(x, y)); } worldToGrid(x: number, y: number): [number, number] { const gridX = Math.floor(x / this.gridSize); const gridY = Math.floor(y / this.gridSize); return [gridX, gridY]; } add(raindrop: RainDrop) { this.raindrops.push(raindrop); let grid = this.gridAtWorldPos(raindrop.pos.x, raindrop.pos.y); if (grid) { grid.add(raindrop); raindrop.gridIdx = grid.length - 1; } } update(time: Time) { if (this.raindrops.length <= this.options.spawnLimit) { for (const newDrop of this.spawner.update(time.dt).trySpawn()) { this.raindrops.push(newDrop); } } this.raindropUpdate(time); this.collisionUpdate(); for (let i = 0; i < this.raindrops.length; i++) { if (this.raindrops[i].destroied) { this.raindrops[i].grid?.delete(this.raindrops[i]); this.raindrops[i] = this.raindrops[this.raindrops.length - 1]; this.raindrops.length--; } } } raindropUpdate(time: Time) { for (let i = 0; i < this.raindrops.length; i++) { const raindrop = this.raindrops[i]; if (raindrop.destroied) continue; raindrop.updateRaindrop(time); if (raindrop.pos.y < -100) raindrop.destroied = true; if (raindrop.destroied) continue; const [gridX, gridY] = this.worldToGrid(raindrop.pos.x, raindrop.pos.y); const grid = this.gridAt(gridX, gridY); if (grid !== raindrop.grid) { raindrop.grid?.delete(raindrop); grid?.add(raindrop); raindrop.grid = grid; } } } collisionUpdate() { for (let i = 0; i < this.raindrops.length; i++) { const raindrop = this.raindrops[i]; if (raindrop.destroied) continue; const [gridX, gridY] = this.worldToGrid(raindrop.pos.x, raindrop.pos.y); for (let x = -1; x <= 1; x++) { for (let y = -1; y <= 1; y++) { const grid = this.gridAt(gridX + x, gridY + y); if (!grid) continue; for (const other of grid) { const isSame = other === raindrop; const isParent = other.parent === raindrop || raindrop.parent === other; const isAdjacent = raindrop.parent && (raindrop.parent === other.parent); if (other.destroied || isParent || isAdjacent || isSame) continue; let dx = raindrop.pos.x - other.pos.x; let dy = raindrop.pos.y - other.pos.y; let distance = Math.sqrt(dx * dx + dy * dy); if (distance - raindrop.mergeDistance - other.mergeDistance < 0) { if (raindrop.mass >= other.mass) { raindrop.merge(other); other.destroied = true; } else { other.merge(raindrop); raindrop.destroied = true; } } } } } } } }