packages/core/src/features/particles/lib/definition.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import {
2 assertFiniteVector,
3 assertNonnegativeNumber,
4 assertPositiveNumber,
5 cloneParticleRecord,
6 createParticleEffect2D,
7 hashParticleEmitterName,
8 normalizeBounds2D,
9 normalizeEmission,
10 normalizeParameters,
11 normalizeRenderer2D,
12 particleDefinitionError,
13 particleScalarRange,
14 validateAppearance,
15 validateScalarVector,
16 type ParticleEffect2DRecord,
17 type ParticleEmitter2DRecord,
18 type ParticleScalarRange,
19 type ParticleValidationContext,
20 } from './definition-records.js';
21 import { getParticleDescriptorRecord } from './particle.js';
22 import { compileParticleEffect2D } from './compiler.js';
23 import type {
24 PibblParticleAppearance,
25 PibblParticleBounds2D,
26 PibblParticleEffect2D,
27 PibblParticleEffect2DInput,
28 PibblParticleInitial2D,
29 PibblParticleMotion2D,
30 PibblParticleParameterDefinitions,
31 PibblParticleShape2D,
32 PibblParticleValuesOf,
33 } from './types.js';
34
35 interface MutableBounds {
36 min: number[];
37 max: number[];
38 }
39
40 function minimumDisplacement(range: ParticleScalarRange, seconds: number): number {
41 return Math.min(0, range.min * seconds, range.max * seconds);
42 }
43
44 function maximumDisplacement(range: ParticleScalarRange, seconds: number): number {
45 return Math.max(0, range.min * seconds, range.max * seconds);
46 }
47
48 function maximumMagnitude(range: ParticleScalarRange): number {
49 return Math.max(Math.abs(range.min), Math.abs(range.max));
50 }
51
52 function normalizeShape(
53 shape: PibblParticleShape2D,
54 context: ParticleValidationContext,
55 ): { readonly shape: PibblParticleShape2D; readonly bounds: MutableBounds } {
56 if (shape === null || typeof shape !== 'object') {
57 particleDefinitionError({ ...context, property: 'shape' }, 'must be a record', shape);
58 }
59 const module = `shape ${String((shape as { type?: unknown }).type)}`;
60 switch (shape.type) {
61 case 'point': {
62 const position = shape.position ?? [0, 0] as const;
63 assertFiniteVector(position, 2, { ...context, module, property: 'shape.position' });
64 return {
65 shape: Object.freeze({ type: 'point', position: cloneParticleRecord(position) }),
66 bounds: { min: [...position], max: [...position] },
67 };
68 }
69 case 'line':
70 assertFiniteVector(shape.from, 2, { ...context, module, property: 'shape.from' });
71 assertFiniteVector(shape.to, 2, { ...context, module, property: 'shape.to' });
72 return {
73 shape: cloneParticleRecord(shape),
74 bounds: {
75 min: [Math.min(shape.from[0], shape.to[0]), Math.min(shape.from[1], shape.to[1])],
76 max: [Math.max(shape.from[0], shape.to[0]), Math.max(shape.from[1], shape.to[1])],
77 },
78 };
79 case 'rectangle':
80 assertPositiveNumber(shape.width, { ...context, module, property: 'shape.width' });
81 assertPositiveNumber(shape.height, { ...context, module, property: 'shape.height' });
82 if (shape.sample !== 'interior' && shape.sample !== 'edge') {
83 particleDefinitionError(
84 { ...context, module, property: 'shape.sample' },
85 'must be interior or edge',
86 shape.sample,
87 );
88 }
89 return {
90 shape: cloneParticleRecord(shape),
91 bounds: {
92 min: [-shape.width / 2, -shape.height / 2],
93 max: [shape.width / 2, shape.height / 2],
94 },
95 };
96 case 'circle':
97 assertNonnegativeNumber(shape.radius, {
98 ...context, module, property: 'shape.radius',
99 });
100 if (shape.sample !== 'interior' && shape.sample !== 'circumference') {
101 particleDefinitionError(
102 { ...context, module, property: 'shape.sample' },
103 'must be interior or circumference',
104 shape.sample,
105 );
106 }
107 return {
108 shape: cloneParticleRecord(shape),
109 bounds: {
110 min: [-shape.radius, -shape.radius],
111 max: [shape.radius, shape.radius],
112 },
113 };
114 case 'ring':
115 assertNonnegativeNumber(shape.innerRadius, {
116 ...context, module, property: 'shape.innerRadius',
117 });
118 assertNonnegativeNumber(shape.outerRadius, {
119 ...context, module, property: 'shape.outerRadius',
120 });
121 if (shape.innerRadius > shape.outerRadius) {
122 particleDefinitionError(
123 { ...context, module, property: 'shape.outerRadius' },
124 'must be at least shape.innerRadius',
125 shape.outerRadius,
126 RangeError,
127 );
128 }
129 return {
130 shape: cloneParticleRecord(shape),
131 bounds: {
132 min: [-shape.outerRadius, -shape.outerRadius],
133 max: [shape.outerRadius, shape.outerRadius],
134 },
135 };
136 default:
137 particleDefinitionError(
138 { ...context, module, property: 'shape.type' },
139 'uses an unsupported 2D shape',
140 (shape as { type?: unknown }).type,
141 );
142 }
143 }
144
145 function requireRange(
146 value: Parameters<typeof particleScalarRange>[0],
147 parameters: PibblParticleParameterDefinitions,
148 context: ParticleValidationContext,
149 minimum?: number,
150 maximum?: number,
151 ): ParticleScalarRange {
152 const range = particleScalarRange(value, parameters, context);
153 if (minimum !== undefined && range.min < minimum) {
154 particleDefinitionError(context, `must be at least ${minimum}`, range.min, RangeError);
155 }
156 if (maximum !== undefined && range.max > maximum) {
157 particleDefinitionError(context, `must be at most ${maximum}`, range.max, RangeError);
158 }
159 return range;
160 }
161
162 function validateColor(
163 value: PibblParticleInitial2D['color'],
164 parameters: PibblParticleParameterDefinitions,
165 context: ParticleValidationContext,
166 ): void {
167 if (value === undefined) return;
168 if (typeof value === 'string') {
169 if (value.trim().length === 0) {
170 particleDefinitionError(context, 'must be a nonempty color string', value);
171 }
172 return;
173 }
174 const record = getParticleDescriptorRecord(value);
175 if (record?.type === 'parameter') {
176 const definition = parameters[record.name];
177 if (definition === undefined) {
178 particleDefinitionError(context, 'references an unknown parameter', record.name);
179 }
180 if (definition.type !== 'color') {
181 particleDefinitionError(
182 context,
183 `parameter "${record.name}" must have type color; received ${definition.type}`,
184 );
185 }
186 return;
187 }
188 if (record?.type !== 'choice') {
189 particleDefinitionError(context, 'must use particleChoice for variable colors', value);
190 }
191 for (let index = 0; index < record.choices.length; index += 1) {
192 const choice = record.choices[index]!.value;
193 if (typeof choice !== 'string' || choice.trim().length === 0) {
194 particleDefinitionError(
195 { ...context, property: `${context.property}.choices[${index}].value` },
196 'must be a nonempty color string',
197 choice,
198 );
199 }
200 }
201 }
202
203 function normalizeInitial(
204 initial: PibblParticleInitial2D,
205 parameters: PibblParticleParameterDefinitions,
206 context: ParticleValidationContext,
207 ): {
208 readonly initial: PibblParticleInitial2D;
209 readonly lifetime: ParticleScalarRange;
210 readonly velocity: readonly ParticleScalarRange[];
211 readonly maximumSize: number;
212 readonly dynamic: boolean;
213 } {
214 if (initial === null || typeof initial !== 'object') {
215 particleDefinitionError(
216 { ...context, property: 'initial' }, 'must be a record', initial,
217 );
218 }
219 const lifetime = requireRange(
220 initial.lifetimeMilliseconds,
221 parameters,
222 { ...context, module: 'initial', property: 'initial.lifetimeMilliseconds' },
223 );
224 if (lifetime.min <= 0) {
225 particleDefinitionError(
226 { ...context, module: 'initial', property: 'initial.lifetimeMilliseconds' },
227 'must be positive',
228 lifetime.min,
229 RangeError,
230 );
231 }
232 const velocity = initial.velocity === undefined ? Object.freeze([
233 Object.freeze({ min: 0, max: 0, dynamic: false }),
234 Object.freeze({ min: 0, max: 0, dynamic: false }),
235 ]) : validateScalarVector(
236 initial.velocity,
237 2,
238 parameters,
239 { ...context, module: 'initial', property: 'initial.velocity' },
240 );
241 let maximumSize = 1;
242 let sizeDynamic = false;
243 if (initial.size !== undefined) {
244 const sizeDescriptor = getParticleDescriptorRecord(initial.size);
245 const vectorSize = sizeDescriptor?.type === 'parameter' &&
246 parameters[sizeDescriptor.name]?.type === 'vector2';
247 const ranges = Array.isArray(initial.size) || vectorSize ? validateScalarVector(
248 initial.size,
249 2,
250 parameters,
251 { ...context, module: 'initial', property: 'initial.size' },
252 ) : [requireRange(
253 initial.size as Parameters<typeof particleScalarRange>[0],
254 parameters,
255 { ...context, module: 'initial', property: 'initial.size' },
256 0,
257 )];
258 for (const range of ranges) {
259 if (range.min < 0) {
260 particleDefinitionError(
261 { ...context, module: 'initial', property: 'initial.size' },
262 'must be nonnegative',
263 range.min,
264 RangeError,
265 );
266 }
267 maximumSize = Math.max(maximumSize, maximumMagnitude(range));
268 sizeDynamic ||= range.dynamic;
269 }
270 }
271 if (initial.rotationDegrees !== undefined) {
272 requireRange(initial.rotationDegrees, parameters, {
273 ...context, module: 'initial', property: 'initial.rotationDegrees',
274 });
275 }
276 if (initial.angularVelocityDegreesPerSecond !== undefined) {
277 requireRange(initial.angularVelocityDegreesPerSecond, parameters, {
278 ...context, module: 'initial',
279 property: 'initial.angularVelocityDegreesPerSecond',
280 });
281 }
282 validateColor(initial.color, parameters, {
283 ...context, module: 'initial', property: 'initial.color',
284 });
285 if (initial.opacity !== undefined) {
286 requireRange(initial.opacity, parameters, {
287 ...context, module: 'initial', property: 'initial.opacity',
288 }, 0, 1);
289 }
290 return Object.freeze({
291 initial: cloneParticleRecord(initial),
292 lifetime,
293 velocity,
294 maximumSize,
295 dynamic: lifetime.dynamic || velocity.some(range => range.dynamic) || sizeDynamic,
296 });
297 }
298
299 function maximumAppearanceSize(
300 appearance: PibblParticleAppearance | undefined,
301 current: number,
302 ): number {
303 const curve = appearance?.size;
304 if (curve === undefined) return current;
305 let result = current;
306 for (const [, value] of curve.keys) {
307 if (typeof value === 'number') result = Math.max(result, Math.abs(value));
308 else result = Math.max(result, Math.abs(value[0]), Math.abs(value[1]));
309 }
310 return result;
311 }
312
313 function normalizeMotion(
314 motion: readonly PibblParticleMotion2D[] | undefined,
315 parameters: PibblParticleParameterDefinitions,
316 context: ParticleValidationContext,
317 ): { readonly motion: readonly PibblParticleMotion2D[]; readonly ranges: readonly unknown[]; readonly dynamic: boolean } {
318 if (motion === undefined) {
319 return Object.freeze({ motion: Object.freeze([]), ranges: Object.freeze([]), dynamic: false });
320 }
321 if (!Array.isArray(motion)) {
322 particleDefinitionError({ ...context, property: 'motion' }, 'must be an array', motion);
323 }
324 let orbitWriter: number | undefined;
325 let additivePositionWriter: number | undefined;
326 const ranges: unknown[] = [];
327 let dynamic = false;
328 for (let index = 0; index < motion.length; index += 1) {
329 const moduleValue = motion[index]!;
330 if (moduleValue === null || typeof moduleValue !== 'object') {
331 particleDefinitionError(
332 { ...context, module: `motion[${index}]`, property: 'motion' },
333 'must be a record',
334 moduleValue,
335 );
336 }
337 const module = `motion[${index}] ${String((moduleValue as { type?: unknown }).type)}`;
338 if (moduleValue.type === 'orbit') {
339 const earlier = orbitWriter ?? additivePositionWriter;
340 if (earlier !== undefined) {
341 particleDefinitionError(
342 { ...context, module, property: 'position' },
343 `ambiguously writes position already written by motion[${earlier}]`,
344 moduleValue.type,
345 );
346 }
347 orbitWriter = index;
348 } else if (moduleValue.type === 'drift' || moduleValue.type === 'sway') {
349 if (orbitWriter !== undefined) {
350 particleDefinitionError(
351 { ...context, module, property: 'position' },
352 `ambiguously writes position already written by motion[${orbitWriter}]`,
353 moduleValue.type,
354 );
355 }
356 additivePositionWriter ??= index;
357 }
358 switch (moduleValue.type) {
359 case 'acceleration':
360 case 'gravity':
361 case 'drift': {
362 const vector = moduleValue.type === 'drift' ?
363 moduleValue.velocity : moduleValue.value;
364 const valueRanges = validateScalarVector(vector, 2, parameters, {
365 ...context, module,
366 property: moduleValue.type === 'drift' ? 'motion.velocity' : 'motion.value',
367 });
368 ranges.push({ type: moduleValue.type, value: valueRanges });
369 dynamic ||= valueRanges.some(range => range.dynamic);
370 break;
371 }
372 case 'drag': {
373 const amount = requireRange(moduleValue.amount, parameters, {
374 ...context, module, property: 'motion.amount',
375 }, 0);
376 ranges.push({ type: 'drag', amount });
377 dynamic ||= amount.dynamic;
378 break;
379 }
380 case 'radialAcceleration': {
381 assertFiniteVector(moduleValue.center, 2, {
382 ...context, module, property: 'motion.center',
383 });
384 const amount = requireRange(moduleValue.amount, parameters, {
385 ...context, module, property: 'motion.amount',
386 });
387 ranges.push({ type: 'radialAcceleration', center: moduleValue.center, amount });
388 dynamic ||= amount.dynamic;
389 break;
390 }
391 case 'orbit': {
392 assertFiniteVector(moduleValue.center, 2, {
393 ...context, module, property: 'motion.center',
394 });
395 const speed = requireRange(moduleValue.degreesPerSecond, parameters, {
396 ...context, module, property: 'motion.degreesPerSecond',
397 });
398 ranges.push({ type: 'orbit', center: moduleValue.center, speed });
399 dynamic ||= speed.dynamic;
400 break;
401 }
402 case 'sway': {
403 const amplitude = requireRange(moduleValue.amplitude, parameters, {
404 ...context, module, property: 'motion.amplitude',
405 }, 0);
406 const frequency: readonly ParticleScalarRange[] =
407 Array.isArray(moduleValue.frequency) ? moduleValue.frequency.map(
408 (value: number, valueIndex: number) => requireRange(value, parameters, {
409 ...context, module, property: `motion.frequency[${valueIndex}]`,
410 }, 0),
411 ) : [requireRange(moduleValue.frequency, parameters, {
412 ...context, module, property: 'motion.frequency',
413 }, 0)];
414 ranges.push({ type: 'sway', amplitude, frequency });
415 dynamic ||= amplitude.dynamic || frequency.some(range => range.dynamic);
416 break;
417 }
418 case 'angularVelocity': {
419 const speed = requireRange(moduleValue.degreesPerSecond, parameters, {
420 ...context, module, property: 'motion.degreesPerSecond',
421 });
422 ranges.push({ type: 'angularVelocity', speed });
423 dynamic ||= speed.dynamic;
424 break;
425 }
426 default:
427 particleDefinitionError(
428 { ...context, module, property: 'motion.type' },
429 'uses an unsupported 2D motion module',
430 (moduleValue as { type?: unknown }).type,
431 );
432 }
433 }
434 return Object.freeze({
435 motion: cloneParticleRecord(motion),
436 ranges: Object.freeze(ranges.map(range => cloneParticleRecord(range))),
437 dynamic,
438 });
439 }
440
441 function deriveBounds(
442 shape: MutableBounds,
443 lifetimeMilliseconds: number,
444 velocity: readonly ParticleScalarRange[],
445 motionRanges: readonly unknown[],
446 size: number,
447 ): PibblParticleBounds2D {
448 const seconds = lifetimeMilliseconds / 1_000;
449 const bounds: MutableBounds = { min: [...shape.min], max: [...shape.max] };
450 for (let axis = 0; axis < 2; axis += 1) {
451 bounds.min[axis]! += minimumDisplacement(velocity[axis]!, seconds);
452 bounds.max[axis]! += maximumDisplacement(velocity[axis]!, seconds);
453 }
454 for (const untyped of motionRanges) {
455 const motion = untyped as {
456 type: string;
457 value?: readonly ParticleScalarRange[];
458 amount?: ParticleScalarRange;
459 amplitude?: ParticleScalarRange;
460 center?: readonly [number, number];
461 };
462 if (motion.type === 'acceleration' || motion.type === 'gravity') {
463 for (let axis = 0; axis < 2; axis += 1) {
464 const scale = 0.5 * seconds * seconds;
465 bounds.min[axis]! += minimumDisplacement(motion.value![axis]!, scale);
466 bounds.max[axis]! += maximumDisplacement(motion.value![axis]!, scale);
467 }
468 } else if (motion.type === 'drift') {
469 for (let axis = 0; axis < 2; axis += 1) {
470 bounds.min[axis]! += minimumDisplacement(motion.value![axis]!, seconds);
471 bounds.max[axis]! += maximumDisplacement(motion.value![axis]!, seconds);
472 }
473 } else if (motion.type === 'radialAcceleration') {
474 const expansion = 0.5 * maximumMagnitude(motion.amount!) * seconds * seconds;
475 for (let axis = 0; axis < 2; axis += 1) {
476 bounds.min[axis]! -= expansion;
477 bounds.max[axis]! += expansion;
478 }
479 } else if (motion.type === 'sway') {
480 const expansion = maximumMagnitude(motion.amplitude!);
481 for (let axis = 0; axis < 2; axis += 1) {
482 bounds.min[axis]! -= expansion;
483 bounds.max[axis]! += expansion;
484 }
485 } else if (motion.type === 'orbit') {
486 const center = motion.center!;
487 const radius = Math.max(
488 Math.hypot(bounds.min[0]! - center[0], bounds.min[1]! - center[1]),
489 Math.hypot(bounds.min[0]! - center[0], bounds.max[1]! - center[1]),
490 Math.hypot(bounds.max[0]! - center[0], bounds.min[1]! - center[1]),
491 Math.hypot(bounds.max[0]! - center[0], bounds.max[1]! - center[1]),
492 );
493 bounds.min = [center[0] - radius, center[1] - radius];
494 bounds.max = [center[0] + radius, center[1] + radius];
495 }
496 }
497 const rendererExpansion = size / 2;
498 return Object.freeze({
499 min: Object.freeze([
500 bounds.min[0]! - rendererExpansion,
501 bounds.min[1]! - rendererExpansion,
502 ]) as readonly [number, number],
503 max: Object.freeze([
504 bounds.max[0]! + rendererExpansion,
505 bounds.max[1]! + rendererExpansion,
506 ]) as readonly [number, number],
507 });
508 }
509
510 function normalizeEmitter(
511 input: import('./types.js').PibblParticleEmitter2D,
512 parameters: PibblParticleParameterDefinitions,
513 debugName: string | undefined,
514 ): ParticleEmitter2DRecord {
515 if (input === null || typeof input !== 'object') {
516 particleDefinitionError(
517 { debugName, property: 'emitter' }, 'must be a record', input,
518 );
519 }
520 const context = { debugName, emitter: input.name };
521 if (typeof input.name !== 'string' || input.name.trim().length === 0) {
522 particleDefinitionError(
523 { debugName, emitter: String(input.name), property: 'name' },
524 'must be a nonempty string',
525 input.name,
526 );
527 }
528 if (!Number.isSafeInteger(input.capacity) || input.capacity <= 0) {
529 particleDefinitionError(
530 { ...context, property: 'capacity' },
531 'must be a positive finite safe integer',
532 input.capacity,
533 RangeError,
534 );
535 }
536 if (input.space !== undefined && input.space !== 'local') {
537 particleDefinitionError(
538 { ...context, property: 'space' }, 'must be local in Project 1', input.space,
539 );
540 }
541 if (input.overflow !== undefined &&
542 input.overflow !== 'reject-newest' && input.overflow !== 'recycle-oldest') {
543 particleDefinitionError(
544 { ...context, property: 'overflow' }, 'is unsupported', input.overflow,
545 );
546 }
547 const emission = normalizeEmission(input.emission, context);
548 const normalizedShape = normalizeShape(input.shape, context);
549 const normalizedInitial = normalizeInitial(input.initial, parameters, context);
550 const normalizedMotion = normalizeMotion(input.motion, parameters, context);
551 const appearance = validateAppearance(input.appearance, context);
552 const renderer = normalizeRenderer2D(input.renderer, context);
553 const cannotDerive = normalizedInitial.dynamic || normalizedMotion.dynamic ||
554 renderer.type === 'path' || renderer.type === 'sprite';
555 let bounds: PibblParticleBounds2D;
556 let boundsSource: 'explicit' | 'derived';
557 if (input.bounds !== undefined) {
558 bounds = normalizeBounds2D(input.bounds, context);
559 boundsSource = 'explicit';
560 } else if (cannotDerive) {
561 particleDefinitionError(
562 { ...context, property: 'bounds' },
563 'must be explicit and finite for dynamic, parameterized, or renderer-unbounded motion',
564 );
565 } else {
566 bounds = normalizeBounds2D(
567 deriveBounds(
568 normalizedShape.bounds,
569 normalizedInitial.lifetime.max,
570 normalizedInitial.velocity,
571 normalizedMotion.ranges,
572 maximumAppearanceSize(appearance, normalizedInitial.maximumSize),
573 ),
574 context,
575 );
576 boundsSource = 'derived';
577 }
578 return Object.freeze({
579 name: input.name,
580 nameHash: hashParticleEmitterName(input.name),
581 capacity: input.capacity,
582 overflow: input.overflow ?? 'reject-newest',
583 space: 'local',
584 emission,
585 shape: normalizedShape.shape,
586 initial: normalizedInitial.initial,
587 motion: normalizedMotion.motion,
588 ...(appearance === undefined ? {} : { appearance }),
589 renderer,
590 bounds,
591 boundsSource,
592 });
593 }
594
595 /**
596 * Validates and brands a reusable Canvas 2D particle effect while preserving emitter names and
597 * parameter types.
598 *
599 * @param input - Emitter definitions, parameter declarations, and effect configuration.
600 * @returns A validated particle effect preserving emitter names and parameter types. See
601 * {@link PibblParticleEffect2D} , {@link PibblParticleValuesOf} ,
602 * {@link PibblParticleParameterDefinitions} .
603 *
604 * @see {@link PibblParticleEffect2D}
605 * @see {@link PibblParticleValuesOf}
606 * @see {@link PibblParticleParameterDefinitions}
607 * @see {@link PibblParticleEffect2DInput}
608 */
609 export function defineParticleEffect2D<
610 const Input extends PibblParticleEffect2DInput,
611 >(
612 input: Input,
613 ): PibblParticleEffect2D<
614 Input['emitters'][number]['name'],
615 PibblParticleValuesOf<
616 Input extends { readonly parameters: infer Definitions }
617 ? Extract<Definitions, PibblParticleParameterDefinitions>
618 : undefined
619 >
620 > {
621 if (input === null || typeof input !== 'object') {
622 particleDefinitionError({}, 'input must be a record', input);
623 }
624 const debugName = input.debugName;
625 if (debugName !== undefined &&
626 (typeof debugName !== 'string' || debugName.trim().length === 0)) {
627 particleDefinitionError({ property: 'debugName' }, 'must be a nonempty string', debugName);
628 }
629 if (!Array.isArray(input.emitters) || input.emitters.length === 0) {
630 particleDefinitionError({ debugName, property: 'emitters' }, 'must be nonempty', input.emitters);
631 }
632 const parameters = normalizeParameters(input.parameters, debugName);
633 const seenNames = new Set<string>();
634 const emitters: ParticleEmitter2DRecord[] = [];
635 for (const emitter of input.emitters) {
636 if (emitter === null || typeof emitter !== 'object') {
637 particleDefinitionError(
638 { debugName, property: 'emitter' }, 'must be a record', emitter,
639 );
640 }
641 if (seenNames.has(emitter.name)) {
642 particleDefinitionError(
643 { debugName, emitter: emitter.name, property: 'name' },
644 'must be unique; duplicate emitter name',
645 emitter.name,
646 );
647 }
648 seenNames.add(emitter.name);
649 emitters.push(normalizeEmitter(emitter, parameters, debugName));
650 }
651 const record: ParticleEffect2DRecord = Object.freeze({
652 dimension: 2,
653 debugName,
654 parameters,
655 emitters: Object.freeze(emitters),
656 emitterNames: Object.freeze(emitters.map(emitter => emitter.name)),
657 capabilities: Object.freeze({
658 statelessCanvas: true,
659 requiresStateful: Object.freeze([]),
660 }),
661 });
662 const effect = createParticleEffect2D(record) as PibblParticleEffect2D<
663 Input['emitters'][number]['name'],
664 PibblParticleValuesOf<
665 Input extends { readonly parameters: infer Definitions }
666 ? Extract<Definitions, PibblParticleParameterDefinitions>
667 : undefined
668 >
669 >;
670 compileParticleEffect2D(effect);
671 return effect;
672 }
673
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.