Time is an argument, so speed scales it and a prewarm offsets it (#87)
game-maker / suite (push) Successful in 9m48s
game-maker / suite (push) Successful in 9m48s
Speed and Prewarm are not state and not a second loop: both are arithmetic on the clock argument the field already passes to every particle formula. fieldTime = elapsed * timeScale + prewarmOffset, a pure rule next to liveRange, called at the one place ParticleField.sync forms its time, so preview and world inherit it together. Prewarm offsets by one lifetime because that is the smallest offset which fills a continuous emitter; a burst is dead at time >= lifetime, so the same offset would skip it entirely and it is suppressed there. The tab's slider counts wall-clock seconds, so its span divides by the scale. A missing row falls back to 1 / off, so old project files need no migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fbb2d157f0
commit
8ae20d044f
8 files changed
+191
-10
No files matched your search
@@ -14,7 +14,7 @@ import {
|
||||
type ParticleOut,
|
||||
particlesFingerprint,
|
||||
} from '../../src/core/particles/compile';
|
||||
import { birthTime, liveCount, liveRange } from '../../src/core/particles/emission';
|
||||
import { birthTime, fieldTime, liveCount, liveRange, prewarmOffset } from '../../src/core/particles/emission';
|
||||
import {
|
||||
hashId,
|
||||
mixColor,
|
||||
@@ -27,11 +27,14 @@ import {
|
||||
import {
|
||||
BURST_COUNT_KEY,
|
||||
EMITTING_KEY,
|
||||
isValidParticlePropertyName,
|
||||
LIFETIME_KEY,
|
||||
MAX_PARTICLES_KEY,
|
||||
PARTICLE_PROPERTY,
|
||||
particleParamDefs,
|
||||
PREWARM_KEY,
|
||||
RATE_KEY,
|
||||
TIME_SCALE_KEY,
|
||||
} from '../../src/core/properties/particles';
|
||||
import { schemaFor, schemaForAsset } from '../../src/core/properties/PropertySchema';
|
||||
import { paletteEntries } from '../../src/editor/graph/paletteEntries';
|
||||
@@ -272,6 +275,8 @@ describe('when particles are alive', () => {
|
||||
rate: 10,
|
||||
count: 20,
|
||||
lifetime: 1,
|
||||
timeScale: 1,
|
||||
prewarm: false,
|
||||
max: 500,
|
||||
};
|
||||
|
||||
@@ -305,6 +310,36 @@ describe('when particles are alive', () => {
|
||||
expect(liveRange({ ...burst, max: 8 }, 0.5)).toEqual({ from: 0, to: 7 });
|
||||
});
|
||||
|
||||
it('runs the source clock at its own speed, which is all a time scale is', () => {
|
||||
const fast = { ...settings, timeScale: 2 };
|
||||
expect(fieldTime(fast, 1)).toBe(2);
|
||||
// Twice the speed is the same picture half the time earlier, particle for particle.
|
||||
expect(liveRange(fast, fieldTime(fast, 1))).toEqual(liveRange(settings, fieldTime(settings, 2)));
|
||||
// A scale of zero holds the second it starts on rather than emptying the field.
|
||||
expect(fieldTime({ ...settings, timeScale: 0 }, 99)).toBe(0);
|
||||
});
|
||||
|
||||
it('starts a prewarmed source one lifetime in, where the field is already full', () => {
|
||||
const warm = { ...settings, prewarm: true };
|
||||
expect(fieldTime(warm, 0)).toBe(settings.lifetime);
|
||||
// Its very first frame carries a whole lifetime of particles; a cold one carries the first.
|
||||
expect(liveCount(liveRange(warm, fieldTime(warm, 0)))).toBe(10);
|
||||
expect(liveCount(liveRange(settings, fieldTime(settings, 0)))).toBe(1);
|
||||
// And it is the steady state, not a spike: a second later there are as many.
|
||||
expect(liveCount(liveRange(warm, fieldTime(warm, 1)))).toBe(10);
|
||||
});
|
||||
|
||||
it('leaves a burst alone, which one lifetime in would be over', () => {
|
||||
const burst = { ...settings, mode: 'burst' as const, prewarm: true };
|
||||
expect(prewarmOffset(burst)).toBe(0);
|
||||
expect(liveCount(liveRange(burst, fieldTime(burst, 0)))).toBe(20);
|
||||
});
|
||||
|
||||
it('counts the prewarm in the seconds a lifetime is in, not in the world clock', () => {
|
||||
// Scaled first, then offset: at twice the speed one second in is the third second of the field.
|
||||
expect(fieldTime({ ...settings, prewarm: true, timeScale: 2 }, 1)).toBe(3);
|
||||
});
|
||||
|
||||
it('emits nothing without a rate, a lifetime or a future', () => {
|
||||
expect(liveCount(liveRange({ ...settings, rate: 0 }, 2))).toBe(0);
|
||||
expect(liveCount(liveRange({ ...settings, lifetime: 0 }, 2))).toBe(0);
|
||||
@@ -325,8 +360,19 @@ describe('a particle colour', () => {
|
||||
describe('the source rows', () => {
|
||||
it('name every setting the emission rule reads', () => {
|
||||
const keys = schemaFor('particles').map((d) => d.key);
|
||||
for (const key of [RATE_KEY, BURST_COUNT_KEY, LIFETIME_KEY, MAX_PARTICLES_KEY, EMITTING_KEY])
|
||||
for (const key of [
|
||||
RATE_KEY,
|
||||
BURST_COUNT_KEY,
|
||||
LIFETIME_KEY,
|
||||
MAX_PARTICLES_KEY,
|
||||
EMITTING_KEY,
|
||||
TIME_SCALE_KEY,
|
||||
PREWARM_KEY,
|
||||
])
|
||||
expect(keys).toContain(key);
|
||||
// A graph parameter may not take one of those names, or the inspector would show it twice.
|
||||
expect(isValidParticlePropertyName(TIME_SCALE_KEY)).toBe(false);
|
||||
expect(isValidParticlePropertyName(PREWARM_KEY)).toBe(false);
|
||||
});
|
||||
|
||||
it('has one Output input per drawn value', () => {
|
||||
|
||||
Reference in new issue
Block a user