Files
feuerwerk/src/editor/particles/ParticlePreview.ts
T
root 0f9e1611d1
game-maker / suite (push) Failing after 4m22s
A particle is a function of its age, not a thing that is stepped (#73)
The asset type `particles` is placeable and carries a graph at the same time —
`behavior` was the blueprint. Because a continuous emitter gives birth to particle n
at time n/rate, the whole field is a function of the clock rather than of the frames:
no ring-buffer bookkeeping, exact time jumps, reproducibility as a measurable property.

compile.ts follows compileShader's frame but emits JS closures into a Float64Array
instead of GLSL, and without eval; a node with two consumers is evaluated once.
Drawing is one Pixi ParticleContainer per source, so one draw call.

Collision, wind and braking in flight are out for good — that is the price of the
formula model, not an omission.

The tab count goes from nine to ten, which is exactly the case #84 was built for one
day earlier: at 1280 px the row no longer holds every tab, so guard 14b now asks for
hidden.length > 0 instead of === 0.
2026-09-14 05:44:25 +02:00

105 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Container, Graphics, Texture } from 'pixi.js';
import type { ParticlesAsset } from '../../core/model/Asset';
import { translation } from '../../core/model/matrix';
import type { PropertyBag } from '../../core/model/Properties';
import { defaultsFor } from '../../core/properties/PropertySchema';
import { type Size, Theme, Widget } from '../../ui';
import { ParticleField } from '../../viewport/particles';
import { particleLibrary } from '../../viewport/ParticleLibrary';
const MIN_HEIGHT = 40;
/**
* The asset's own particles, emitted from the middle of the strip by a source with the type's
* default rows. It is the one place a formula graph can be judged: the numbers say nothing. The
* picture is deliberately a plain square – which texture a source throws belongs to the placed
* object, not to the asset the tab is showing.
*/
export class ParticlePreview extends Widget {
private readonly bg = new Graphics();
private readonly content = new Container();
private readonly clip = new Graphics();
private readonly field = new ParticleField();
private asset: ParticlesAsset | null = null;
/** Null: the preview runs on the library clock. A number: it stands at that second. */
private held: number | null = null;
constructor() {
super();
this.minHeight = MIN_HEIGHT;
this.content.addChild(this.field.container);
this.content.mask = this.clip;
this.addChild(this.bg, this.content, this.clip);
}
override measure(): Size {
return { w: Math.max(this.minWidth, 100), h: Math.max(this.minHeight, MIN_HEIGHT) };
}
show(asset: ParticlesAsset | null): void {
this.asset = asset;
this.invalidate();
}
/** Seconds the shown source has been emitting, whether held or running. */
get time(): number {
return this.held ?? Math.max(0, this.field.elapsed(particleLibrary.now()));
}
get holding(): boolean {
return this.held !== null;
}
hold(at: number | null): void {
this.held = at;
this.invalidate();
}
restart(): void {
this.field.restart(particleLibrary.now());
this.invalidate();
}
/** How many particles the preview is drawing – what the checks read. */
get count(): number {
return this.field.count;
}
override redraw(): void {
const c = Theme.colors;
// A ground of its own and a rule along the top: on the graph's own background the strip was
// invisible as a thing, and a particle standing still read as a node that had lost its box.
this.bg.clear().rect(0, 0, this.w, this.h).fill(c.panel);
this.bg.rect(0, 0, this.w, 1).fill(c.border);
this.clip.clear().rect(0, 0, this.w, this.h).fill(0xffffff);
this.field.container.visible = this.asset !== null;
if (!this.asset) return;
const build = particleLibrary.buildFor(this.asset);
const rows = build.compiled?.params ?? [];
const props = previewProps(rows);
const params = rows.map((row) => Number(props[row.key] ?? row.default));
// The source sits in the middle of the strip; a fountain then has room above and below it.
const m = translation(this.w / 2, this.h / 2);
this.field.sync(build.compiled, props, params, m, Texture.WHITE, this.clock());
}
/** Holding at second t means handing the field a clock that reads exactly t since it started. */
private clock(): number {
const now = particleLibrary.now();
if (this.held === null) return now;
return now - this.field.elapsed(now) + this.held;
}
override destroy(): void {
this.field.destroy();
super.destroy();
}
}
/** A source as the asset alone describes it: the type's defaults, the graph's parameters at theirs. */
function previewProps(params: readonly { key: string; default: number }[]): PropertyBag {
const props = defaultsFor('particles');
for (const row of params) props[row.key] = row.default;
return props;
}