A particle is a function of its age, not a thing that is stepped (#73)
game-maker / suite (push) Failing after 4m22s
game-maker / suite (push) Failing after 4m22s
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.
This commit is contained in:
1 parent
f83c568999
commit
0f9e1611d1
39 files changed
+2386
-32
No files matched your search
@@ -0,0 +1,104 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user