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>
77 lines
3.2 KiB
TypeScript
77 lines
3.2 KiB
TypeScript
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 };
|
||
}
|