Files
feuerwerk/src/core/particles/emission.ts
T
adminandClaude Fable 5.1 cc6db5f350
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m46s
Three stores, three importers and three pickers that differ only in their type are one of each (#85)
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:38:54 +02:00

77 lines
3.2 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 type { EmitMode } from '../properties/particles';
/** What the source's own rows say about how many particles there are and for how long. */
export interface EmitterSettings {
mode: EmitMode;
/** Continuous: particles per second. */
rate: number;
/** Burst: how many go out at once. */
count: number;
/** Seconds one particle lives. */
lifetime: number;
/** How fast the source's own clock runs against the world's: 2 is twice the speed. */
timeScale: number;
/** Start a lifetime in, so the field is already full at second 0. */
prewarm: boolean;
/** The most that may be alive at once; the oldest are the ones dropped. */
max: number;
}
/** Which particles are alive, as a range of indices – `to < from` is none. Both ends inclusive. */
export interface LiveRange {
from: number;
to: number;
}
export const NO_PARTICLES: LiveRange = { from: 0, to: -1 };
/**
* When particle `index` was born, in seconds since the source started. A continuous source emits on
* a fixed grid – particle n is born at n/rate – which is what makes the whole field a function of
* time rather than of how many frames have gone by. A burst is every particle at once.
*/
export function birthTime(settings: EmitterSettings, index: number): number {
if (settings.mode === 'burst') return 0;
return settings.rate > 0 ? index / settings.rate : 0;
}
/**
* The clock the formulas are handed, from the seconds the source has been alive: a factor and a
* summand on that one argument rather than any state, which is what lets a prewarmed field be shown
* at its first frame without simulating the way there.
*/
export function fieldTime(settings: EmitterSettings, elapsed: number): number {
return elapsed * settings.timeScale + prewarmOffset(settings);
}
/**
* A lifetime, which is the smallest offset that makes a continuous field full: at that second the
* oldest particle alive is exactly the one about to die. A burst has nothing left by then, so the
* offset would hide it altogether – prewarm means nothing there.
*/
export function prewarmOffset(settings: EmitterSettings): number {
return settings.prewarm && settings.mode === 'continuous' ? Math.max(0, settings.lifetime) : 0;
}
/**
* The particles alive at `time`. `emitUntil` is how far the source was still emitting: switched off,
* it stops taking in new ones while the ones in flight live out their lives.
*/
export function liveRange(settings: EmitterSettings, time: number, emitUntil = time): LiveRange {
const { lifetime, max } = settings;
if (lifetime <= 0 || max < 1 || time < 0) return NO_PARTICLES;
if (settings.mode === 'burst') {
const count = Math.min(Math.floor(settings.count), Math.floor(max));
if (count < 1 || time >= lifetime || emitUntil < 0) return NO_PARTICLES;
return { from: 0, to: count - 1 };
}
const rate = settings.rate;
if (rate <= 0) return NO_PARTICLES;
const newest = Math.floor(Math.min(time, emitUntil) * rate);
if (newest < 0) return NO_PARTICLES;
// Born no earlier than one lifetime ago: `n/rate > time - lifetime`.
const oldest = Math.max(0, Math.floor((time - lifetime) * rate) + 1);
if (oldest > newest) return NO_PARTICLES;
return { from: Math.max(oldest, newest - Math.floor(max) + 1), to: newest };
}