silphius/app/particles/emitter.js
2024-08-03 16:02:09 -05:00

123 lines
3.5 KiB
JavaScript

import K from 'kefir';
import * as easings from '@/util/easing.js';
import {TAU} from '@/util/math.js';
export default class Emitter {
constructor(ecs) {
this.ecs = ecs;
this.scheduled = [];
}
async configure(entityId, {fields, shape}) {
const entity = this.ecs.get(entityId);
if (shape) {
switch (shape.type) {
case 'circle': {
const r = Math.random() * TAU;
entity.Position.x += Math.cos(r) * shape.payload.radius;
entity.Position.y += Math.sin(r) * shape.payload.radius;
break;
}
case 'filledCircle': {
const r = Math.random() * TAU;
entity.Position.x += Math.cos(r) * Math.random() * shape.payload.radius;
entity.Position.y += Math.sin(r) * Math.random() * shape.payload.radius;
break;
}
case 'filledRect': {
entity.Position.x += Math.random() * shape.payload.width - (shape.payload.width / 2);
entity.Position.y += Math.random() * shape.payload.height - (shape.payload.height / 2);
break;
}
}
}
if (fields) {
for (const {easing = 'linear', path, value} of fields) {
let walk = entity;
const pathCopy = path.slice(0);
const final = pathCopy.pop();
for (const key of pathCopy) {
walk = walk[key];
}
const c = Math.random() * (value.length - 1);
const i = Math.floor(c);
walk[final] = easings[easing](c - i, value[i], value[i + 1] - value[i], 1);
}
}
return entity;
}
emit(particle) {
particle = {
...particle,
entity: {
Position: {},
Sprite: {},
...particle.entity,
},
}
let {count = 1} = particle;
const {entity, frequency = 0, spurt = 1, ttl = 0} = particle;
if (ttl > 0) {
count = Math.floor(ttl / frequency);
}
const specifications = Array(count);
for (let i = 0; i < count * spurt; ++i) {
specifications[i] = entity;
}
const stream = K.stream(async (emitter) => {
const entityIds = await this.ecs.createManyDetached(specifications);
if (0 === frequency) {
this.ecs.attach(entityIds);
for (const entityId of entityIds) {
emitter.emit(this.configure(entityId, particle));
}
emitter.end();
return;
}
const it = entityIds.values();
const batch = new Set();
for (let i = 0; i < spurt; ++i) {
batch.add(it.next().value);
}
this.ecs.attach(batch);
for (const entityId of batch) {
emitter.emit(this.configure(entityId, particle));
}
count -= 1;
if (0 === count) {
emitter.end();
return;
}
let accumulated = 0;
const scheduled = (elapsed) => {
accumulated += elapsed;
while (accumulated > frequency && count > 0) {
const batch = new Set();
for (let i = 0; i < spurt; ++i) {
batch.add(it.next().value);
}
this.ecs.attach(batch);
for (const entityId of batch) {
emitter.emit(this.configure(entityId, particle));
}
accumulated -= frequency;
count -= 1;
}
if (0 === count) {
this.scheduled.splice(this.scheduled.indexOf(scheduled), 1);
emitter.end();
}
};
this.scheduled.push(scheduled);
});
return stream;
}
tick(elapsed) {
for (const ticker of this.scheduled) {
ticker(elapsed);
}
}
}