babylon.quarks-editor 0.17.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +84 -0
  4. package/dist/core/binding.d.ts +43 -0
  5. package/dist/core/binding.js +82 -0
  6. package/dist/core/collision.d.ts +20 -0
  7. package/dist/core/collision.js +39 -0
  8. package/dist/core/colors.d.ts +31 -0
  9. package/dist/core/colors.js +60 -0
  10. package/dist/core/effectTree.d.ts +25 -0
  11. package/dist/core/effectTree.js +115 -0
  12. package/dist/core/groups.d.ts +12 -0
  13. package/dist/core/groups.js +27 -0
  14. package/dist/core/history.d.ts +25 -0
  15. package/dist/core/history.js +69 -0
  16. package/dist/core/loadEffect.d.ts +8 -0
  17. package/dist/core/loadEffect.js +87 -0
  18. package/dist/core/scrub.d.ts +11 -0
  19. package/dist/core/scrub.js +24 -0
  20. package/dist/core/shapes.d.ts +31 -0
  21. package/dist/core/shapes.js +98 -0
  22. package/dist/core/systems.d.ts +25 -0
  23. package/dist/core/systems.js +76 -0
  24. package/dist/core/textureExport.d.ts +9 -0
  25. package/dist/core/textureExport.js +58 -0
  26. package/dist/core/timeline.d.ts +35 -0
  27. package/dist/core/timeline.js +69 -0
  28. package/dist/core/values.d.ts +31 -0
  29. package/dist/core/values.js +81 -0
  30. package/dist/index.d.ts +15 -0
  31. package/dist/index.js +9 -0
  32. package/dist/react/CurveEditor.d.ts +12 -0
  33. package/dist/react/CurveEditor.js +106 -0
  34. package/dist/react/EffectEditor.d.ts +22 -0
  35. package/dist/react/EffectEditor.js +30 -0
  36. package/dist/react/EffectEditorHost.d.ts +42 -0
  37. package/dist/react/EffectEditorHost.js +247 -0
  38. package/dist/react/GradientEditor.d.ts +7 -0
  39. package/dist/react/GradientEditor.js +72 -0
  40. package/dist/react/ModuleSection.d.ts +9 -0
  41. package/dist/react/ModuleSection.js +24 -0
  42. package/dist/react/PromptDialog.d.ts +14 -0
  43. package/dist/react/PromptDialog.js +45 -0
  44. package/dist/react/SubEmittersModule.d.ts +10 -0
  45. package/dist/react/SubEmittersModule.js +39 -0
  46. package/dist/react/TimelinePanel.d.ts +32 -0
  47. package/dist/react/TimelinePanel.js +225 -0
  48. package/dist/react/TimelineTrackRow.d.ts +23 -0
  49. package/dist/react/TimelineTrackRow.js +107 -0
  50. package/dist/react/ValueField.d.ts +13 -0
  51. package/dist/react/ValueField.js +19 -0
  52. package/dist/react/behaviorModules.d.ts +23 -0
  53. package/dist/react/behaviorModules.js +139 -0
  54. package/dist/react/fields.d.ts +24 -0
  55. package/dist/react/fields.js +30 -0
  56. package/dist/react/index.d.ts +20 -0
  57. package/dist/react/index.js +14 -0
  58. package/dist/react/modules.d.ts +35 -0
  59. package/dist/react/modules.js +439 -0
  60. package/dist/react/theme.d.ts +26 -0
  61. package/dist/react/theme.js +78 -0
  62. package/package.json +63 -0
  63. package/src/core/binding.ts +105 -0
  64. package/src/core/collision.ts +40 -0
  65. package/src/core/colors.ts +84 -0
  66. package/src/core/effectTree.ts +143 -0
  67. package/src/core/groups.ts +29 -0
  68. package/src/core/history.ts +77 -0
  69. package/src/core/loadEffect.ts +102 -0
  70. package/src/core/scrub.ts +28 -0
  71. package/src/core/shapes.ts +133 -0
  72. package/src/core/systems.ts +98 -0
  73. package/src/core/textureExport.ts +63 -0
  74. package/src/core/timeline.ts +94 -0
  75. package/src/core/values.ts +113 -0
  76. package/src/index.ts +32 -0
  77. package/src/react/CurveEditor.tsx +168 -0
  78. package/src/react/EffectEditor.tsx +111 -0
  79. package/src/react/EffectEditorHost.tsx +377 -0
  80. package/src/react/GradientEditor.tsx +127 -0
  81. package/src/react/ModuleSection.tsx +54 -0
  82. package/src/react/PromptDialog.tsx +85 -0
  83. package/src/react/SubEmittersModule.tsx +80 -0
  84. package/src/react/TimelinePanel.tsx +354 -0
  85. package/src/react/TimelineTrackRow.tsx +267 -0
  86. package/src/react/ValueField.tsx +60 -0
  87. package/src/react/behaviorModules.tsx +658 -0
  88. package/src/react/fields.tsx +76 -0
  89. package/src/react/index.ts +43 -0
  90. package/src/react/modules.tsx +1034 -0
  91. package/src/react/theme.ts +85 -0
@@ -0,0 +1,133 @@
1
+ import {
2
+ CircleEmitter,
3
+ ConeEmitter,
4
+ DonutEmitter,
5
+ GridEmitter,
6
+ HemisphereEmitter,
7
+ MeshSurfaceEmitter,
8
+ PointEmitter,
9
+ RectangleEmitter,
10
+ SphereEmitter,
11
+ } from 'babylon.quarks';
12
+ import type {EmitterShape} from 'babylon.quarks';
13
+ import {EmitterMode} from 'babylon.quarks';
14
+ import type {Mesh} from '@babylonjs/core/Meshes/mesh';
15
+
16
+ export type ShapeType = 'point' | 'sphere' | 'hemisphere' | 'cone' | 'circle' | 'donut' | 'rectangle' | 'grid' | 'mesh_surface';
17
+
18
+ export const SHAPE_TYPES: Array<{value: ShapeType; label: string}> = [
19
+ {value: 'cone', label: 'Cone'},
20
+ {value: 'sphere', label: 'Sphere'},
21
+ {value: 'hemisphere', label: 'Hemisphere'},
22
+ {value: 'circle', label: 'Circle'},
23
+ {value: 'donut', label: 'Donut'},
24
+ {value: 'rectangle', label: 'Rectangle'},
25
+ {value: 'grid', label: 'Grid'},
26
+ {value: 'mesh_surface', label: 'Mesh surface'},
27
+ {value: 'point', label: 'Point'},
28
+ ];
29
+
30
+ export interface ShapeParams {
31
+ radius: number;
32
+ arc: number;
33
+ thickness: number;
34
+ angle: number;
35
+ donutRadius: number;
36
+ width: number;
37
+ height: number;
38
+ mode: EmitterMode;
39
+ spread: number;
40
+ column: number;
41
+ row: number;
42
+ mesh?: Mesh;
43
+ }
44
+
45
+ export const DEFAULT_SHAPE_PARAMS: ShapeParams = {
46
+ radius: 0.3,
47
+ arc: Math.PI * 2,
48
+ thickness: 1,
49
+ angle: 0.5,
50
+ donutRadius: 0.1,
51
+ width: 1,
52
+ height: 1,
53
+ mode: EmitterMode.Random,
54
+ spread: 0,
55
+ column: 10,
56
+ row: 10,
57
+ mesh: undefined,
58
+ };
59
+
60
+ export function getShapeType(shape: EmitterShape): ShapeType {
61
+ const type = (shape as {type?: string}).type ?? 'point';
62
+ return (SHAPE_TYPES.some((s) => s.value === type) ? type : 'point') as ShapeType;
63
+ }
64
+
65
+ export function readShapeParams(shape: EmitterShape): ShapeParams {
66
+ const anyShape = shape as unknown as {[key: string]: unknown};
67
+ const num = (key: string, fallback: number) => (typeof anyShape[key] === 'number' ? (anyShape[key] as number) : fallback);
68
+ return {
69
+ radius: num('radius', DEFAULT_SHAPE_PARAMS.radius),
70
+ mode: num('mode', DEFAULT_SHAPE_PARAMS.mode) as EmitterMode,
71
+ spread: num('spread', DEFAULT_SHAPE_PARAMS.spread),
72
+ arc: num('arc', DEFAULT_SHAPE_PARAMS.arc),
73
+ thickness: num('thickness', DEFAULT_SHAPE_PARAMS.thickness),
74
+ angle: num('angle', DEFAULT_SHAPE_PARAMS.angle),
75
+ donutRadius: num('donutRadius', DEFAULT_SHAPE_PARAMS.donutRadius),
76
+ width: num('width', DEFAULT_SHAPE_PARAMS.width),
77
+ height: num('height', DEFAULT_SHAPE_PARAMS.height),
78
+ column: num('column', DEFAULT_SHAPE_PARAMS.column),
79
+ row: num('row', DEFAULT_SHAPE_PARAMS.row),
80
+ mesh: anyShape.mesh as Mesh | undefined,
81
+ };
82
+ }
83
+
84
+ /** Which parameters are relevant per shape type (drives the UI). `mesh_surface`'s mesh
85
+ * reference isn't a NumberField, so it's handled separately by the Shape module. */
86
+ export const SHAPE_PARAM_KEYS: {[K in ShapeType]: Array<keyof ShapeParams>} = {
87
+ point: [],
88
+ sphere: ['radius', 'arc', 'thickness'],
89
+ hemisphere: ['radius', 'arc', 'thickness'],
90
+ cone: ['radius', 'arc', 'thickness', 'angle'],
91
+ circle: ['radius', 'arc', 'thickness'],
92
+ donut: ['radius', 'arc', 'thickness', 'donutRadius'],
93
+ rectangle: ['width', 'height'],
94
+ grid: ['width', 'height', 'column', 'row'],
95
+ mesh_surface: [],
96
+ };
97
+
98
+ export function createShape(type: ShapeType, params: ShapeParams): EmitterShape {
99
+ switch (type) {
100
+ case 'point':
101
+ return new PointEmitter();
102
+ case 'sphere':
103
+ return new SphereEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
104
+ case 'hemisphere':
105
+ return new HemisphereEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
106
+ case 'cone':
107
+ return new ConeEmitter({
108
+ radius: params.radius,
109
+ arc: params.arc,
110
+ thickness: params.thickness,
111
+ angle: params.angle,
112
+ mode: params.mode,
113
+ spread: params.spread,
114
+ });
115
+ case 'circle':
116
+ return new CircleEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
117
+ case 'donut':
118
+ return new DonutEmitter({
119
+ radius: params.radius,
120
+ arc: params.arc,
121
+ thickness: params.thickness,
122
+ donutRadius: params.donutRadius,
123
+ mode: params.mode,
124
+ spread: params.spread,
125
+ });
126
+ case 'rectangle':
127
+ return new RectangleEmitter({width: params.width, height: params.height, mode: params.mode, spread: params.spread});
128
+ case 'grid':
129
+ return new GridEmitter({width: params.width, height: params.height, column: params.column, row: params.row});
130
+ case 'mesh_surface':
131
+ return new MeshSurfaceEmitter(params.mesh);
132
+ }
133
+ }
@@ -0,0 +1,98 @@
1
+ import {
2
+ ConeEmitter,
3
+ ConstantColor,
4
+ ConstantValue,
5
+ IntervalValue,
6
+ ParticleSystem,
7
+ RenderMode,
8
+ Vector4,
9
+ } from 'babylon.quarks';
10
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
11
+ import type {EffectBinding} from './binding';
12
+
13
+ export interface ChildSystemOptions {
14
+ name?: string;
15
+ /** One-shot burst (sub-emitter target) instead of a looping stream. */
16
+ oneShot?: boolean;
17
+ /** Attach point override — defaults to `parent.emitter`. Lets a new system land as a
18
+ * sibling under a group node instead of nesting under an existing system. */
19
+ parentNode?: TransformNode;
20
+ }
21
+
22
+ /**
23
+ * Creates a child system sharing the parent's scene/texture/blending, parented to the
24
+ * parent emitter (or `options.parentNode` when given) — the Unity pattern where an effect is
25
+ * a hierarchy of systems and groups.
26
+ */
27
+ export function createChildSystem(parent: ParticleSystem, options: ChildSystemOptions = {}): ParticleSystem {
28
+ const oneShot = options.oneShot ?? false;
29
+ const child = new ParticleSystem({
30
+ scene: parent.emitter.getScene(),
31
+ duration: oneShot ? 1 : parent.duration,
32
+ looping: !oneShot,
33
+ startLife: new IntervalValue(0.3, oneShot ? 0.6 : 1.2),
34
+ startSpeed: new IntervalValue(1, 3),
35
+ startSize: new IntervalValue(0.05, 0.2),
36
+ startColor: new ConstantColor(new Vector4(1, 0.9, 0.6, 1)),
37
+ emissionOverTime: new ConstantValue(oneShot ? 0 : 30),
38
+ emissionBursts: oneShot ? [{time: 0, count: new ConstantValue(8), cycle: 1, interval: 0.01, probability: 1}] : [],
39
+ shape: new ConeEmitter({radius: 0.1, angle: Math.PI / 3}),
40
+ renderMode: RenderMode.BillBoard,
41
+ texture: parent.texture ?? undefined,
42
+ transparent: true,
43
+ blendMode: parent.blending,
44
+ worldSpace: true,
45
+ });
46
+ child.emitter.name = options.name ?? (oneShot ? 'sub-emitter' : 'child-system');
47
+ if (oneShot) {
48
+ child.onlyUsedByOther = true;
49
+ }
50
+ child.emitter.parent = options.parentNode ?? parent.emitter;
51
+ return child;
52
+ }
53
+
54
+ /**
55
+ * Strips EmitSubParticleSystem behaviors that point at any of the removed systems, so
56
+ * deleting a sub-emitter target never leaves a dangling reference (which crashes the next
57
+ * update with `reading 'duration'` on the disposed emitter).
58
+ */
59
+ export function unlinkSubEmitterReferences(systems: ParticleSystem[], removed: ParticleSystem[]): void {
60
+ const removedEmitters = new Set(removed.map((system) => system.emitter));
61
+ for (const system of systems) {
62
+ const behaviors = system.behaviors as Array<{type?: string; subParticleSystem?: unknown}>;
63
+ for (let i = behaviors.length - 1; i >= 0; i--) {
64
+ const behavior = behaviors[i];
65
+ if (behavior.type === 'EmitSubParticleSystem' && removedEmitters.has(behavior.subParticleSystem as never)) {
66
+ behaviors.splice(i, 1);
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Disposes the given systems, first unlinking any sub-emitter behaviors that target them. */
73
+ export function removeSystems(binding: EffectBinding, targets: ParticleSystem[], onSelect: (system: ParticleSystem) => void): void {
74
+ // binding.system is "removable" too as long as some other top-level system can take over
75
+ // as the new main — being main vs. sub is an internal bookkeeping detail, not something
76
+ // that should make one arbitrary track un-deletable in the UI.
77
+ if (targets.includes(binding.system)) {
78
+ const replacement = binding.subSystems.find((s) => !targets.includes(s) && !s.onlyUsedByOther);
79
+ if (replacement) {
80
+ binding.promoteToMain(replacement);
81
+ }
82
+ }
83
+ const removable = targets.filter((system) => binding.subSystems.includes(system));
84
+ if (removable.length === 0) {
85
+ return;
86
+ }
87
+ binding.apply(() => {
88
+ unlinkSubEmitterReferences([binding.system, ...binding.subSystems], removable);
89
+ for (const system of removable) {
90
+ system._renderer?.deleteSystem(system);
91
+ system.dispose();
92
+ }
93
+ });
94
+ for (const system of removable) {
95
+ binding.removeSubSystem(system);
96
+ }
97
+ onSelect(binding.system);
98
+ }
@@ -0,0 +1,63 @@
1
+ import type {Texture} from '@babylonjs/core/Materials/Textures/texture';
2
+
3
+ const bakedUrls = new WeakMap<Texture, string>();
4
+
5
+ /**
6
+ * Babylon's glTF loader gives embedded (base64-in-buffer) images a placeholder `data:` url
7
+ * (e.g. `data:leave.glb#image0`) that only works as an in-memory cache key paired with the
8
+ * pixel buffer it loaded alongside — it has no real payload, so a fresh Scene/Engine (e.g.
9
+ * after "Open in editor" navigates to a new page) can't refetch or reconstruct it from that
10
+ * string. Real data URIs always carry a comma-delimited payload; this placeholder never does.
11
+ */
12
+ function isPortableUrl(url: string | null | undefined): boolean {
13
+ if (!url) {
14
+ return false;
15
+ }
16
+ return !url.startsWith('data:') || url.includes(',');
17
+ }
18
+
19
+ /**
20
+ * Ensures a texture's `.url` can be reloaded in a different Scene/Engine, baking its live
21
+ * GPU pixels into a real base64 PNG data URI when it currently only carries Babylon's
22
+ * internal placeholder url (see isPortableUrl). No-ops for textures that already have a
23
+ * fetchable or real `data:` url. Call before serializing a scene that will be reloaded
24
+ * elsewhere — e.g. the demo viewer's "Open in editor" handoff, which navigates to a new page.
25
+ */
26
+ export async function ensurePortableTextureUrl(texture: Texture): Promise<void> {
27
+ if (isPortableUrl(texture.url)) {
28
+ return;
29
+ }
30
+ const cached = bakedUrls.get(texture);
31
+ if (cached) {
32
+ texture.url = cached;
33
+ return;
34
+ }
35
+ const dataUrl = await bakeTextureToDataUrl(texture);
36
+ if (dataUrl) {
37
+ bakedUrls.set(texture, dataUrl);
38
+ texture.url = dataUrl;
39
+ }
40
+ }
41
+
42
+ async function bakeTextureToDataUrl(texture: Texture): Promise<string | undefined> {
43
+ const size = texture.getSize();
44
+ if (!size.width || !size.height) {
45
+ return undefined;
46
+ }
47
+ const pixels = await texture.readPixels();
48
+ if (!(pixels instanceof Uint8Array || pixels instanceof Uint8ClampedArray)) {
49
+ return undefined;
50
+ }
51
+ const canvas = document.createElement('canvas');
52
+ canvas.width = size.width;
53
+ canvas.height = size.height;
54
+ const ctx = canvas.getContext('2d');
55
+ if (!ctx) {
56
+ return undefined;
57
+ }
58
+ // Copy (rather than wrap) so the result is backed by a plain ArrayBuffer — ImageData
59
+ // rejects the SharedArrayBuffer-compatible view type `readPixels` may hand back.
60
+ const bytes = new Uint8ClampedArray(pixels);
61
+ ctx.putImageData(new ImageData(bytes, size.width, size.height), 0, 0);
62
+ return canvas.toDataURL('image/png');
63
+ }
@@ -0,0 +1,94 @@
1
+ import type {ParticleSystem} from 'babylon.quarks';
2
+ import {ConstantValue, IntervalValue} from 'babylon.quarks';
3
+ import type {FunctionValueGenerator, ValueGenerator} from 'babylon.quarks';
4
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
5
+ import type {EffectBinding} from './binding';
6
+ import type {EffectTreeNode} from './effectTree';
7
+
8
+ const MIN_TIMELINE_SPAN = 4;
9
+
10
+ export type TimelineRow =
11
+ | {kind: 'group'; node: TransformNode; name: string; depth: number}
12
+ | {
13
+ kind: 'track';
14
+ node: TransformNode;
15
+ system: ParticleSystem;
16
+ name: string;
17
+ depth: number;
18
+ isRoot: boolean;
19
+ removable: boolean;
20
+ startOffset: number;
21
+ startOffsetVariable: boolean;
22
+ duration: number;
23
+ looping: boolean;
24
+ };
25
+
26
+ /**
27
+ * Reads a representative start-delay value for display without invoking the generator's
28
+ * stateful `genValue` (IntervalValue caches its sampled slot on first call and returns NaN
29
+ * on a later call with a fresh memory array, so it can't be safely re-read on every render).
30
+ */
31
+ export function computeStartOffset(gen: ValueGenerator | FunctionValueGenerator): {value: number; startOffsetVariable: boolean} {
32
+ if (gen instanceof ConstantValue) {
33
+ return {value: gen.value, startOffsetVariable: false};
34
+ }
35
+ if (gen instanceof IntervalValue) {
36
+ return {value: Math.min(gen.a, gen.b), startOffsetVariable: false};
37
+ }
38
+ return {value: 0, startOffsetVariable: true};
39
+ }
40
+
41
+ /**
42
+ * Whether a track can be removed. Sub-systems always can; the designated "main" system can
43
+ * too as long as another top-level system remains to take over that role — being main vs.
44
+ * sub is an internal bookkeeping detail (see EffectBinding.promoteToMain), not something that
45
+ * should make one arbitrary track permanently un-deletable in the UI.
46
+ */
47
+ function isRemovable(binding: EffectBinding, system: ParticleSystem): boolean {
48
+ if (binding.subSystems.includes(system)) {
49
+ return true;
50
+ }
51
+ if (system === binding.system) {
52
+ return binding.subSystems.some((s) => !s.onlyUsedByOther);
53
+ }
54
+ return false;
55
+ }
56
+
57
+ /** Flattens the effect's group/emitter tree into display rows, preserving tree-walk order. */
58
+ export function computeTimelineRows(binding: EffectBinding): TimelineRow[] {
59
+ const rows: TimelineRow[] = [];
60
+ const walk = (node: EffectTreeNode) => {
61
+ if (node.system) {
62
+ const {value, startOffsetVariable} = computeStartOffset(node.system.startDelay);
63
+ rows.push({
64
+ kind: 'track',
65
+ node: node.node,
66
+ system: node.system,
67
+ name: node.name,
68
+ depth: node.depth,
69
+ isRoot: node.node === binding.root,
70
+ removable: isRemovable(binding, node.system),
71
+ startOffset: value,
72
+ startOffsetVariable,
73
+ duration: node.system.duration,
74
+ looping: node.system.looping,
75
+ });
76
+ } else {
77
+ rows.push({kind: 'group', node: node.node, name: node.name, depth: node.depth});
78
+ }
79
+ node.children.forEach(walk);
80
+ };
81
+ walk(binding.getTree());
82
+ return rows;
83
+ }
84
+
85
+ /** Overall visible timeline span: the furthest track extent, floored at a sane minimum. */
86
+ export function computeTimelineSpan(rows: TimelineRow[]): number {
87
+ let span = 0;
88
+ for (const row of rows) {
89
+ if (row.kind === 'track') {
90
+ span = Math.max(span, row.startOffset + row.duration);
91
+ }
92
+ }
93
+ return Math.max(span, MIN_TIMELINE_SPAN);
94
+ }
@@ -0,0 +1,113 @@
1
+ import {Bezier, ConstantValue, IntervalValue, PiecewiseBezier} from 'babylon.quarks';
2
+ import type {FunctionValueGenerator, ValueGenerator} from 'babylon.quarks';
3
+
4
+ export type ScalarGenerator = ValueGenerator | FunctionValueGenerator;
5
+ export type ScalarMode = 'constant' | 'random' | 'curve';
6
+
7
+ /** Editable snapshot of a scalar value generator. */
8
+ export interface ScalarValueState {
9
+ mode: ScalarMode;
10
+ /** Constant value (mode: constant). */
11
+ value: number;
12
+ /** Random range (mode: random). */
13
+ min: number;
14
+ max: number;
15
+ /** Cubic bezier control values over normalized lifetime (mode: curve). */
16
+ curve: [number, number, number, number];
17
+ }
18
+
19
+ export function readScalar(generator: ScalarGenerator | undefined): ScalarValueState {
20
+ const state: ScalarValueState = {mode: 'constant', value: 1, min: 0.5, max: 1.5, curve: [0, 1, 1, 0]};
21
+ if (generator instanceof ConstantValue) {
22
+ state.mode = 'constant';
23
+ state.value = generator.value;
24
+ state.min = generator.value * 0.5;
25
+ state.max = generator.value * 1.5;
26
+ } else if (generator instanceof IntervalValue) {
27
+ state.mode = 'random';
28
+ state.min = generator.a;
29
+ state.max = generator.b;
30
+ state.value = (generator.a + generator.b) / 2;
31
+ } else if (generator instanceof PiecewiseBezier) {
32
+ state.mode = 'curve';
33
+ const first = generator.functions[0]?.[0];
34
+ if (first) {
35
+ state.curve = [first.p[0], first.p[1], first.p[2], first.p[3]];
36
+ state.value = first.p[0];
37
+ }
38
+ }
39
+ return state;
40
+ }
41
+
42
+ export function buildScalar(state: ScalarValueState): ScalarGenerator {
43
+ switch (state.mode) {
44
+ case 'constant':
45
+ return new ConstantValue(state.value);
46
+ case 'random':
47
+ return new IntervalValue(Math.min(state.min, state.max), Math.max(state.min, state.max));
48
+ case 'curve':
49
+ return new PiecewiseBezier([[new Bezier(...state.curve), 0]]);
50
+ }
51
+ }
52
+
53
+ /** One segment of a piecewise bezier curve: start x (0..1) + four control values. */
54
+ export interface CurvePiece {
55
+ start: number;
56
+ p: [number, number, number, number];
57
+ }
58
+
59
+ /** Reads pieces from a PiecewiseBezier (or synthesizes one segment for other generators). */
60
+ export function readPieces(generator: ScalarGenerator | undefined): CurvePiece[] {
61
+ if (generator instanceof PiecewiseBezier && generator.functions.length > 0) {
62
+ return generator.functions.map(([bezier, start]) => ({
63
+ start,
64
+ p: [bezier.p[0], bezier.p[1], bezier.p[2], bezier.p[3]] as [number, number, number, number],
65
+ }));
66
+ }
67
+ const fallback = readScalar(generator);
68
+ return [{start: 0, p: fallback.curve}];
69
+ }
70
+
71
+ export function buildCurve(pieces: CurvePiece[]): PiecewiseBezier {
72
+ const sorted = [...pieces].sort((a, b) => a.start - b.start);
73
+ return new PiecewiseBezier(sorted.map((piece) => [new Bezier(...piece.p), piece.start] as [Bezier, number]));
74
+ }
75
+
76
+ /** Splits the piece containing x into two exact halves using Bezier.split. */
77
+ export function splitPieces(pieces: CurvePiece[], x: number): CurvePiece[] {
78
+ const sorted = [...pieces].sort((a, b) => a.start - b.start);
79
+ for (let i = 0; i < sorted.length; i++) {
80
+ const end = i + 1 < sorted.length ? sorted[i + 1].start : 1;
81
+ if (x > sorted[i].start + 0.02 && x < end - 0.02) {
82
+ const t = (x - sorted[i].start) / (end - sorted[i].start);
83
+ const {left, right} = new Bezier(...sorted[i].p).split(t);
84
+ sorted.splice(
85
+ i,
86
+ 1,
87
+ {start: sorted[i].start, p: [left.p[0], left.p[1], left.p[2], left.p[3]]},
88
+ {start: x, p: [right.p[0], right.p[1], right.p[2], right.p[3]]}
89
+ );
90
+ return sorted;
91
+ }
92
+ }
93
+ return sorted;
94
+ }
95
+
96
+ /** Removes the boundary at pieces[index].start by merging with the previous piece. */
97
+ export function mergePieces(pieces: CurvePiece[], index: number): CurvePiece[] {
98
+ if (index <= 0 || index >= pieces.length) {
99
+ return pieces;
100
+ }
101
+ const sorted = [...pieces].sort((a, b) => a.start - b.start);
102
+ const a = sorted[index - 1];
103
+ const b = sorted[index];
104
+ sorted.splice(index - 1, 2, {start: a.start, p: [a.p[0], a.p[1], b.p[2], b.p[3]]});
105
+ return sorted;
106
+ }
107
+
108
+ /** Samples a cubic bezier defined by four control values at normalized t. */
109
+ export function sampleCurve(curve: [number, number, number, number], t: number): number {
110
+ const [p0, p1, p2, p3] = curve;
111
+ const u = 1 - t;
112
+ return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
113
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ export {EffectBinding} from './core/binding';
2
+ export type {EditorListener} from './core/binding';
3
+ export {EffectHistory} from './core/history';
4
+ export {createChildSystem} from './core/systems';
5
+ export {buildEffectTree, collectSystems, serializeEffectTree, serializeEffectForest} from './core/effectTree';
6
+ export type {EffectTreeNode} from './core/effectTree';
7
+ export type {ChildSystemOptions} from './core/systems';
8
+ export {readScalar, buildScalar, sampleCurve, readPieces, buildCurve, splitPieces, mergePieces} from './core/values';
9
+ export type {ScalarGenerator, ScalarMode, ScalarValueState, CurvePiece} from './core/values';
10
+ export {
11
+ DEFAULT_GRADIENT_STOPS,
12
+ buildGradient,
13
+ buildColorOverLife,
14
+ rgbaToVector4,
15
+ stopToCss,
16
+ stopsToCssGradient,
17
+ hexToRgb,
18
+ rgbToHex,
19
+ findBehavior,
20
+ } from './core/colors';
21
+ export type {GradientStop} from './core/colors';
22
+ export {
23
+ SHAPE_TYPES,
24
+ SHAPE_PARAM_KEYS,
25
+ DEFAULT_SHAPE_PARAMS,
26
+ getShapeType,
27
+ readShapeParams,
28
+ createShape,
29
+ } from './core/shapes';
30
+ export type {ShapeType, ShapeParams} from './core/shapes';
31
+ export {GroundPlaneResolver, ensureGroundResolver} from './core/collision';
32
+ export {ensurePortableTextureUrl} from './core/textureExport';