silphius/app/particles/emitter.js

123 lines
3.5 KiB
JavaScript
Raw Normal View History

2024-07-30 09:56:53 -05:00
import K from 'kefir';
2024-08-01 00:39:54 -05:00
import * as easings from '@/util/easing.js';
2024-08-01 14:32:20 -05:00
import {TAU} from '@/util/math.js';
2024-08-01 00:39:54 -05:00
2024-07-30 09:56:53 -05:00
export default class Emitter {
constructor(ecs) {
this.ecs = ecs;
this.scheduled = [];
}
2024-08-03 15:25:41 -05:00
async configure(entityId, {fields, shape}) {
const entity = this.ecs.get(entityId);
2024-07-30 09:56:53 -05:00
if (shape) {
switch (shape.type) {
2024-08-01 14:32:20 -05:00
case 'circle': {
const r = Math.random() * TAU;
2024-08-03 15:25:41 -05:00
entity.Position.x += Math.cos(r) * shape.payload.radius;
entity.Position.y += Math.sin(r) * shape.payload.radius;
2024-08-01 14:32:20 -05:00
break;
}
case 'filledCircle': {
const r = Math.random() * TAU;
2024-08-03 15:25:41 -05:00
entity.Position.x += Math.cos(r) * Math.random() * shape.payload.radius;
entity.Position.y += Math.sin(r) * Math.random() * shape.payload.radius;
2024-08-01 14:32:20 -05:00
break;
}
2024-07-30 09:56:53 -05:00
case 'filledRect': {
2024-08-03 15:25:41 -05:00
entity.Position.x += Math.random() * shape.payload.width - (shape.payload.width / 2);
entity.Position.y += Math.random() * shape.payload.height - (shape.payload.height / 2);
2024-07-30 09:56:53 -05:00
break;
}
}
}
2024-08-01 00:39:54 -05:00
if (fields) {
for (const {easing = 'linear', path, value} of fields) {
2024-08-03 15:25:41 -05:00
let walk = entity;
2024-08-01 00:39:54 -05:00
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);
}
}
2024-08-03 15:25:41 -05:00
return entity;
2024-07-30 09:56:53 -05:00
}
emit(particle) {
particle = {
...particle,
entity: {
Position: {},
Sprite: {},
...particle.entity,
},
}
let {count = 1} = particle;
2024-08-03 16:02:09 -05:00
const {entity, frequency = 0, spurt = 1, ttl = 0} = particle;
if (ttl > 0) {
count = Math.floor(ttl / frequency);
}
2024-08-03 15:25:41 -05:00
const specifications = Array(count);
2024-08-03 16:02:09 -05:00
for (let i = 0; i < count * spurt; ++i) {
2024-08-03 15:25:41 -05:00
specifications[i] = entity;
}
const stream = K.stream(async (emitter) => {
const entityIds = await this.ecs.createManyDetached(specifications);
2024-07-30 09:56:53 -05:00
if (0 === frequency) {
2024-08-03 15:25:41 -05:00
this.ecs.attach(entityIds);
for (const entityId of entityIds) {
emitter.emit(this.configure(entityId, particle));
2024-07-30 09:56:53 -05:00
}
2024-08-03 15:25:41 -05:00
emitter.end();
2024-07-30 09:56:53 -05:00
return;
}
2024-08-03 15:25:41 -05:00
const it = entityIds.values();
2024-08-03 16:02:09 -05:00
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));
}
2024-07-30 09:56:53 -05:00
count -= 1;
if (0 === count) {
2024-08-03 15:25:41 -05:00
emitter.end();
2024-07-30 09:56:53 -05:00
return;
}
let accumulated = 0;
const scheduled = (elapsed) => {
accumulated += elapsed;
while (accumulated > frequency && count > 0) {
2024-08-03 16:02:09 -05:00
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));
}
2024-07-30 09:56:53 -05:00
accumulated -= frequency;
count -= 1;
}
if (0 === count) {
this.scheduled.splice(this.scheduled.indexOf(scheduled), 1);
2024-08-03 15:25:41 -05:00
emitter.end();
2024-07-30 09:56:53 -05:00
}
};
this.scheduled.push(scheduled);
});
return stream;
}
tick(elapsed) {
for (const ticker of this.scheduled) {
ticker(elapsed);
}
}
}