babylon.quarks-editor 0.27.0 → 0.28.0

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 (138) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +31 -4
  3. package/dist/core/assetTree.d.ts +5 -0
  4. package/dist/core/assets.d.ts +7 -0
  5. package/dist/core/assets.js +11 -7
  6. package/dist/core/binding.d.ts +9 -0
  7. package/dist/core/binding.js +8 -0
  8. package/dist/core/collision.d.ts +4 -0
  9. package/dist/core/collision.js +4 -0
  10. package/dist/core/colors.d.ts +16 -0
  11. package/dist/core/colors.js +8 -0
  12. package/dist/core/defaultEffect.d.ts +3 -2
  13. package/dist/core/defaultEffect.js +1 -1
  14. package/dist/core/editorSession.d.ts +229 -0
  15. package/dist/core/editorSession.js +983 -0
  16. package/dist/core/effectTree.d.ts +5 -0
  17. package/dist/core/galleryPlayback.js +2 -1
  18. package/dist/core/history.d.ts +6 -0
  19. package/dist/core/history.js +4 -0
  20. package/dist/core/indexedDbProjectStore.d.ts +13 -0
  21. package/dist/core/indexedDbProjectStore.js +13 -0
  22. package/dist/core/material.d.ts +12 -2
  23. package/dist/core/playback.d.ts +6 -0
  24. package/dist/core/projectStore.d.ts +26 -0
  25. package/dist/core/projectStore.js +1 -0
  26. package/dist/core/sceneEntries.d.ts +3 -0
  27. package/dist/core/scrub.d.ts +1 -1
  28. package/dist/core/scrub.js +2 -2
  29. package/dist/core/shapes.d.ts +24 -0
  30. package/dist/core/shapes.js +5 -0
  31. package/dist/core/systems.d.ts +2 -0
  32. package/dist/core/systems.js +3 -1
  33. package/dist/core/timeline.d.ts +23 -3
  34. package/dist/core/values.d.ts +21 -4
  35. package/dist/core/values.js +5 -0
  36. package/dist/index.d.ts +5 -1
  37. package/dist/react/AssetsPanel.d.ts +7 -0
  38. package/dist/react/ChoiceDialog.d.ts +7 -0
  39. package/dist/react/ContextMenu.d.ts +10 -1
  40. package/dist/react/EditorWorkspace.d.ts +7 -0
  41. package/dist/react/EditorWorkspace.js +6 -3
  42. package/dist/react/EffectEditor.d.ts +8 -2
  43. package/dist/react/EffectEditorHost.d.ts +32 -28
  44. package/dist/react/EffectEditorHost.js +241 -1665
  45. package/dist/react/EffectTreeView.d.ts +3 -0
  46. package/dist/react/MessageDialog.d.ts +5 -0
  47. package/dist/react/ModuleSection.d.ts +10 -0
  48. package/dist/react/ModuleSection.js +4 -1
  49. package/dist/react/ProjectsPage.d.ts +6 -0
  50. package/dist/react/ProjectsPage.js +27 -11
  51. package/dist/react/PromptDialog.d.ts +7 -0
  52. package/dist/react/ScenePanel.d.ts +7 -0
  53. package/dist/react/ScenePanel.js +7 -5
  54. package/dist/react/TextureSelect.d.ts +5 -5
  55. package/dist/react/TimelinePanel.d.ts +13 -0
  56. package/dist/react/TimelinePanel.js +10 -6
  57. package/dist/react/TimelineTrackRow.d.ts +10 -0
  58. package/dist/react/TransportBar.d.ts +9 -0
  59. package/dist/react/ValueField.d.ts +32 -5
  60. package/dist/react/ValueField.js +20 -9
  61. package/dist/react/behaviorModules.d.ts +16 -5
  62. package/dist/react/behaviorModules.js +16 -1
  63. package/dist/react/fields.d.ts +8 -0
  64. package/dist/react/fields.js +3 -0
  65. package/dist/react/host/BottomDocks.d.ts +36 -0
  66. package/dist/react/host/BottomDocks.js +63 -0
  67. package/dist/react/host/HostDialogs.d.ts +50 -0
  68. package/dist/react/host/HostDialogs.js +113 -0
  69. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  70. package/dist/react/host/InspectorSidebar.js +59 -0
  71. package/dist/react/host/SceneSidebar.d.ts +53 -0
  72. package/dist/react/host/SceneSidebar.js +62 -0
  73. package/dist/react/host/ViewportPane.d.ts +56 -0
  74. package/dist/react/host/ViewportPane.js +91 -0
  75. package/dist/react/host/hostHooks.d.ts +44 -0
  76. package/dist/react/host/hostHooks.js +164 -0
  77. package/dist/react/host/hostStyles.d.ts +21 -0
  78. package/dist/react/host/hostStyles.js +57 -0
  79. package/dist/react/index.d.ts +3 -1
  80. package/dist/react/modules.d.ts +17 -6
  81. package/dist/react/modules.js +38 -20
  82. package/dist/react/theme.d.ts +15 -0
  83. package/dist/react/theme.js +15 -0
  84. package/dist/session.d.ts +9 -0
  85. package/dist/session.js +8 -0
  86. package/package.json +17 -11
  87. package/src/core/assetTree.ts +14 -1
  88. package/src/core/assets.ts +25 -10
  89. package/src/core/binding.ts +9 -0
  90. package/src/core/collision.ts +4 -0
  91. package/src/core/colors.ts +21 -1
  92. package/src/core/defaultEffect.ts +4 -3
  93. package/src/core/editorSession.ts +1299 -0
  94. package/src/core/effectTree.ts +5 -0
  95. package/src/core/galleryPlayback.ts +2 -1
  96. package/src/core/history.ts +6 -0
  97. package/src/core/indexedDbProjectStore.ts +13 -0
  98. package/src/core/loadEffect.ts +1 -1
  99. package/src/core/material.ts +14 -4
  100. package/src/core/playback.ts +6 -0
  101. package/src/core/projectStore.ts +26 -0
  102. package/src/core/sceneEntries.ts +3 -0
  103. package/src/core/scrub.ts +2 -2
  104. package/src/core/shapes.ts +28 -1
  105. package/src/core/systems.ts +9 -2
  106. package/src/core/timeline.ts +40 -15
  107. package/src/core/values.ts +20 -4
  108. package/src/index.ts +5 -1
  109. package/src/react/AssetsPanel.tsx +7 -0
  110. package/src/react/ChoiceDialog.tsx +7 -0
  111. package/src/react/ContextMenu.tsx +10 -1
  112. package/src/react/EditorWorkspace.tsx +11 -5
  113. package/src/react/EffectEditor.tsx +7 -1
  114. package/src/react/EffectEditorHost.tsx +406 -2340
  115. package/src/react/EffectTreeView.tsx +3 -0
  116. package/src/react/MessageDialog.tsx +5 -0
  117. package/src/react/ModuleSection.tsx +11 -0
  118. package/src/react/ProjectsPage.tsx +38 -11
  119. package/src/react/PromptDialog.tsx +7 -0
  120. package/src/react/ScenePanel.tsx +14 -5
  121. package/src/react/TextureSelect.tsx +6 -6
  122. package/src/react/TimelinePanel.tsx +23 -6
  123. package/src/react/TimelineTrackRow.tsx +10 -0
  124. package/src/react/TransportBar.tsx +9 -0
  125. package/src/react/ValueField.tsx +58 -14
  126. package/src/react/behaviorModules.tsx +34 -32
  127. package/src/react/fields.tsx +12 -1
  128. package/src/react/host/BottomDocks.tsx +178 -0
  129. package/src/react/host/HostDialogs.tsx +228 -0
  130. package/src/react/host/InspectorSidebar.tsx +147 -0
  131. package/src/react/host/SceneSidebar.tsx +227 -0
  132. package/src/react/host/ViewportPane.tsx +300 -0
  133. package/src/react/host/hostHooks.ts +198 -0
  134. package/src/react/host/hostStyles.ts +65 -0
  135. package/src/react/index.ts +3 -1
  136. package/src/react/modules.tsx +70 -44
  137. package/src/react/theme.ts +15 -0
  138. package/src/session.ts +17 -0
@@ -1,9 +1,12 @@
1
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import React from 'react';
3
+ /** Props of {@link EffectTreeView}. */
3
4
  export interface EffectTreeViewProps {
5
+ /** The effect's root. */
4
6
  root: TransformNode;
5
7
  /** Node shown in the inspector (and moved by the gizmo). */
6
8
  selectedNode: TransformNode | null;
9
+ /** Selects a group or emitter. */
7
10
  onSelect: (node: TransformNode) => void;
8
11
  /** Show the root itself as the first row (default: only its descendants). */
9
12
  includeRoot?: boolean;
@@ -1,8 +1,13 @@
1
1
  import React from 'react';
2
+ /** Props of {@link MessageDialog}. */
2
3
  export interface MessageDialogProps {
4
+ /** The dialog's title. */
3
5
  title: string;
6
+ /** The message; line breaks are kept. */
4
7
  message: string;
8
+ /** The button's text. Default `OK`. */
5
9
  confirmLabel?: string;
10
+ /** Closes the dialog: the button, Enter, Escape or the backdrop. */
6
11
  onClose: () => void;
7
12
  }
8
13
  /** Modal for info and error messages — same chrome as PromptDialog, no text input. */
@@ -1,5 +1,15 @@
1
1
  import React from 'react';
2
+ import type { EffectBinding } from '../core/binding.js';
2
3
  /** Unity-style module foldout with an optional enable checkbox. */
4
+ /** Props of every inspector module: the effect binding it edits, through `binding.apply`. */
5
+ export interface ModuleProps {
6
+ /** The focused effect; the module edits `binding.system`. */
7
+ binding: EffectBinding;
8
+ }
9
+ /**
10
+ * An inspector module: a foldout with a title, a hint, and an enable checkbox when `onToggle` is
11
+ * given. Unity's Particle System inspector modules look the same way.
12
+ */
3
13
  export declare function ModuleSection(props: {
4
14
  title: string;
5
15
  hint?: string;
@@ -5,7 +5,10 @@ import { iconStyle } from './icons.js';
5
5
  import { getInspectorHint } from './inspectorHints.js';
6
6
  import { InspectorInfoIcon } from './InspectorInfoIcon.js';
7
7
  import { theme } from './theme.js';
8
- /** Unity-style module foldout with an optional enable checkbox. */
8
+ /**
9
+ * An inspector module: a foldout with a title, a hint, and an enable checkbox when `onToggle` is
10
+ * given. Unity's Particle System inspector modules look the same way.
11
+ */
9
12
  export function ModuleSection(props) {
10
13
  const [open, setOpen] = useState(props.defaultOpen ?? true);
11
14
  const toggleable = props.onToggle !== undefined;
@@ -1,12 +1,18 @@
1
1
  import React from 'react';
2
2
  import { type ProjectMeta, type ProjectStore } from '../core/projectStore.js';
3
+ /** Props of {@link ProjectsPage}. */
3
4
  export interface ProjectsPageProps {
5
+ /** Where the projects live. */
4
6
  store: ProjectStore;
7
+ /** Opens a project in the editor. */
5
8
  onOpen: (project: ProjectMeta) => void;
9
+ /** The page's heading. */
6
10
  title?: string;
7
11
  /** Optional link back to the page that hosts the editor (e.g. a gallery), shown above the title. */
8
12
  backLink?: {
13
+ /** Where the link goes. */
9
14
  href: string;
15
+ /** The link's text. */
10
16
  label: string;
11
17
  };
12
18
  }
@@ -29,6 +29,15 @@ function StorageBar(props) {
29
29
  background: theme.accent,
30
30
  } }) }), _jsx("div", { style: { fontSize: 11.5, color: theme.textDim, lineHeight: 1.5 }, children: "Projects are saved in this browser only (IndexedDB). Clearing site data removes them, and Safari deletes data of sites not opened for 7 days \u2014 export projects you care about." })] }));
31
31
  }
32
+ async function listProjects(store) {
33
+ try {
34
+ const [list, storage] = await Promise.all([store.listProjects(), store.estimate()]);
35
+ return { list, storage };
36
+ }
37
+ catch (err) {
38
+ return { error: err instanceof Error ? err.message : String(err) };
39
+ }
40
+ }
32
41
  /** Start page: the projects saved in the store, plus storage usage. */
33
42
  export function ProjectsPage(props) {
34
43
  const { store, onOpen, title = 'Effect editor', backLink } = props;
@@ -37,20 +46,27 @@ export function ProjectsPage(props) {
37
46
  const [dialog, setDialog] = useState(null);
38
47
  const [menu, setMenu] = useState(null);
39
48
  const [error, setError] = useState(null);
40
- const reload = useCallback(async () => {
41
- try {
42
- const [list, storage] = await Promise.all([store.listProjects(), store.estimate()]);
43
- setProjects(list);
44
- setEstimate(storage);
45
- }
46
- catch (err) {
47
- setError(err instanceof Error ? err.message : String(err));
49
+ const show = useCallback((listing) => {
50
+ if ('error' in listing) {
51
+ setError(listing.error);
48
52
  setProjects([]);
49
53
  }
50
- }, [store]);
54
+ else {
55
+ setProjects(listing.list);
56
+ setEstimate(listing.storage);
57
+ }
58
+ }, []);
59
+ const reload = useCallback(async () => show(await listProjects(store)), [store, show]);
51
60
  useEffect(() => {
52
- void reload();
53
- }, [reload]);
61
+ let cancelled = false;
62
+ void listProjects(store).then((listing) => {
63
+ if (!cancelled)
64
+ show(listing);
65
+ });
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [store, show]);
54
70
  const run = (action) => {
55
71
  setDialog(null);
56
72
  action()
@@ -1,12 +1,19 @@
1
1
  import React from 'react';
2
+ /** Props of {@link PromptDialog}. */
2
3
  export interface PromptDialogProps {
4
+ /** What to enter. */
3
5
  title: string;
6
+ /** The text the field starts with, selected. */
4
7
  defaultValue?: string;
8
+ /** Hint shown in the empty field. */
5
9
  placeholder?: string;
10
+ /** The confirm button's text. Default `OK`. */
6
11
  confirmLabel?: string;
7
12
  /** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
8
13
  allowEmpty?: boolean;
14
+ /** Receives the entered text, trimmed. */
9
15
  onSubmit: (value: string) => void;
16
+ /** Escape, the backdrop, Cancel, or an empty submit when empty is not allowed. */
10
17
  onCancel: () => void;
11
18
  }
12
19
  /** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
@@ -1,20 +1,27 @@
1
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import React from 'react';
3
3
  import type { SceneEntry } from '../core/sceneEntries.js';
4
+ /** Props of {@link ScenePanel}. */
4
5
  export interface ScenePanelProps {
6
+ /** The effects in the scene, in list order. */
5
7
  entries: SceneEntry[];
6
8
  /** Effect shown in the inspector and timeline. */
7
9
  focusedRoot: TransformNode | null;
8
10
  /** Visible entries — they render and play with the stage transport. */
9
11
  visibleRootIds: ReadonlySet<number>;
12
+ /** Focuses an effect in the inspector and timeline. */
10
13
  onFocus: (entry: SceneEntry) => void;
14
+ /** Shows or hides an effect. */
11
15
  onToggleVisible: (entry: SceneEntry) => void;
12
16
  /** Root of the entry shown alone, or null. */
13
17
  soloRootId: number | null;
18
+ /** Shows an effect alone, or ends solo. */
14
19
  onToggleSolo: (entry: SceneEntry) => void;
20
+ /** Removes an effect from the scene. */
15
21
  onDelete: (entry: SceneEntry) => void;
16
22
  /** "Save as new asset…" in the row's right-click menu. */
17
23
  onSaveAsAsset?: (entry: SceneEntry) => void;
24
+ /** Adds a new default effect. */
18
25
  onCreateDefault: () => void;
19
26
  /** Node shown in the inspector; highlighted in the expanded effect trees. */
20
27
  selectedNode?: TransformNode | null;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDownIcon, ChevronRightIcon, EyeIcon, EyeSlashIcon, SparklesIcon, TrashIcon, ViewfinderCircleIcon, } from '@heroicons/react/24/solid';
3
- import React, { useEffect, useState } from 'react';
3
+ import React, { useState } from 'react';
4
4
  import { buildEffectTree } from '../core/effectTree.js';
5
5
  import { ContextMenu } from './ContextMenu.js';
6
6
  import { EffectTreeView } from './EffectTreeView.js';
@@ -83,11 +83,13 @@ export function ScenePanel(props) {
83
83
  const [menu, setMenu] = useState(null);
84
84
  const [expanded, setExpanded] = useState(() => new Set());
85
85
  // The focused effect opens its tree, so its sub-emitters are one click away.
86
- useEffect(() => {
87
- if (focusedRoot) {
88
- setExpanded((prev) => (prev.has(focusedRoot.uniqueId) ? prev : new Set(prev).add(focusedRoot.uniqueId)));
86
+ const [expandedFocus, setExpandedFocus] = useState(null);
87
+ if (focusedRoot !== expandedFocus) {
88
+ setExpandedFocus(focusedRoot);
89
+ if (focusedRoot && !expanded.has(focusedRoot.uniqueId)) {
90
+ setExpanded(new Set(expanded).add(focusedRoot.uniqueId));
89
91
  }
90
- }, [focusedRoot]);
92
+ }
91
93
  const toggleExpanded = (entry) => setExpanded((prev) => {
92
94
  const next = new Set(prev);
93
95
  if (next.has(entry.root.uniqueId)) {
@@ -1,4 +1,4 @@
1
- import type { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture.js';
1
+ import type { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
2
2
  import type { TextureOption } from './modules.js';
3
3
  /** The url a texture-like object was loaded from, or '' for none. */
4
4
  export declare function textureUrlOf(texture: unknown): string;
@@ -11,10 +11,10 @@ export declare function TextureSelect(props: {
11
11
  /** The slot's current texture (anything with a `url`), or null for none. */
12
12
  value: unknown;
13
13
  textureOptions?: TextureOption[];
14
- /** Turns a URL or `data:` URI into the host's texture object; null when it cannot. */
15
- loadTexture: (url: string) => unknown;
16
- /** The host's texture object, assumed to be a Babylon texture, or null for none. */
17
- onChange: (texture: BaseTexture | null) => void;
14
+ /** Turns a URL or `data:` URI into a texture; null or undefined when it cannot. */
15
+ loadTexture: (url: string) => Texture | null | undefined;
16
+ /** The picked texture, or null for none. */
17
+ onChange: (texture: Texture | null) => void;
18
18
  /** Label of the "none" entry. Default "(none)". */
19
19
  noneLabel?: string;
20
20
  /** Shows a thumbnail of the current texture under the select. Default false. */
@@ -5,18 +5,31 @@ import React from 'react';
5
5
  import type { EffectBinding } from '../core/binding.js';
6
6
  import { type PlaybackState } from '../core/playback.js';
7
7
  export type { PlaybackState } from '../core/playback.js';
8
+ /** Props of {@link TimelinePanel}. */
8
9
  export interface TimelinePanelProps {
10
+ /** The focused effect, whose systems are the tracks. */
9
11
  binding: EffectBinding;
12
+ /** The node shown in the inspector; its track is highlighted. */
10
13
  selectedNode: TransformNode | null;
14
+ /** Selects a track's or group's node. */
11
15
  onSelectNode: (node: TransformNode | null) => void;
16
+ /** The preview scene, redrawn after a scrub. */
12
17
  scene: Scene;
18
+ /** The renderer the effect draws through, refreshed after a scrub. */
13
19
  renderer: BatchedRenderer;
20
+ /** Playback state shared with the host's render loop. */
14
21
  playbackRef: React.MutableRefObject<PlaybackState>;
22
+ /** The playhead line, moved every frame without a re-render. */
15
23
  playheadRef: React.RefObject<HTMLDivElement | null>;
24
+ /** The time and particle counter in the header, written by the host's render loop. */
16
25
  counterRef: React.RefObject<HTMLSpanElement | null>;
26
+ /** The stage is playing. */
17
27
  stagePlaying: boolean;
28
+ /** Plays or pauses the stage. */
18
29
  setStagePlaying: (playing: boolean) => void;
30
+ /** Playback speed, 1 for real time. */
19
31
  speed: number;
32
+ /** Sets the playback speed. */
20
33
  setSpeed: (speed: number) => void;
21
34
  /** False when the focused effect is hidden — it has nothing to play. */
22
35
  playbackEnabled?: boolean;
@@ -38,10 +38,13 @@ export function TimelinePanel(props) {
38
38
  const bodyRef = useRef(null);
39
39
  const dragRef = useRef({ raf: null, pendingTime: null });
40
40
  const focusRootId = binding.root.uniqueId;
41
- React.useEffect(() => {
41
+ // Another effect starts with every system shown and nothing selected.
42
+ const [shownBinding, setShownBinding] = useState(binding);
43
+ if (binding !== shownBinding) {
44
+ setShownBinding(binding);
42
45
  setHiddenSystems(new Set());
43
46
  setGroupSelection(new Set());
44
- }, [binding]);
47
+ }
45
48
  const toggleGroupSelect = (node) => {
46
49
  setGroupSelection((prev) => {
47
50
  const next = new Set(prev);
@@ -145,18 +148,19 @@ export function TimelinePanel(props) {
145
148
  renderer,
146
149
  ]);
147
150
  const visibleRows = useMemo(() => {
151
+ const visible = [];
148
152
  let hideUntilDepth = null;
149
- const visible = rows.filter((row) => {
153
+ for (const row of rows) {
150
154
  if (hideUntilDepth !== null) {
151
155
  if (row.depth > hideUntilDepth)
152
- return false;
156
+ continue;
153
157
  hideUntilDepth = null;
154
158
  }
155
159
  if (row.kind === 'group' && collapsedGroups.has(row.node)) {
156
160
  hideUntilDepth = row.depth;
157
161
  }
158
- return true;
159
- });
162
+ visible.push(row);
163
+ }
160
164
  return visible;
161
165
  }, [rows, collapsedGroups]);
162
166
  const toggleCollapse = (node) => {
@@ -2,21 +2,31 @@ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import React from 'react';
3
3
  import type { EffectBinding } from '../core/binding.js';
4
4
  import type { TimelineRow } from '../core/timeline.js';
5
+ /** Props of {@link TimelineTrackRow}. */
5
6
  export interface TimelineTrackRowProps {
7
+ /** The group or track the row shows. */
6
8
  row: TimelineRow;
9
+ /** The focused effect, edited by the row's actions. */
7
10
  binding: EffectBinding;
11
+ /** The row's node is the one in the inspector. */
8
12
  selected: boolean;
13
+ /** Selects the row's node. */
9
14
  onSelectNode: (node: TransformNode | null) => void;
15
+ /** The ruler's scale, pixels per second. */
10
16
  pxPerSecond: number;
11
17
  /** Matches the ruler's left gutter so bars line up under the correct tick. */
12
18
  offsetPx: number;
19
+ /** A group row's children are hidden. */
13
20
  collapsed: boolean;
21
+ /** Collapses or expands a group row. */
14
22
  onToggleCollapse?: () => void;
15
23
  /** Preview-only visibility (toggles `system.emitter.visible`); lets you isolate one track. */
16
24
  hidden?: boolean;
25
+ /** Shows or hides the track in the preview. */
17
26
  onToggleVisible?: () => void;
18
27
  /** Membership in the "group selected rows together" set (unrelated to inspector selection). */
19
28
  groupSelected: boolean;
29
+ /** Adds the row to the set, or takes it out. */
20
30
  onToggleGroupSelect: () => void;
21
31
  }
22
32
  /** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
@@ -3,16 +3,25 @@ import type { BatchedRenderer } from 'babylon.quarks';
3
3
  import React from 'react';
4
4
  import type { EffectBinding } from '../core/binding.js';
5
5
  import type { PlaybackState } from '../core/playback.js';
6
+ /** Props of {@link TransportBar}. */
6
7
  export interface TransportBarProps {
8
+ /** The focused effect, which Restart and Step act on. */
7
9
  binding: EffectBinding | null;
10
+ /** The preview scene, redrawn after a restart. */
8
11
  scene: Scene;
12
+ /** The renderer the effects draw through. */
9
13
  renderer: BatchedRenderer;
14
+ /** Playback state shared with the host's render loop. */
10
15
  playbackRef: React.MutableRefObject<PlaybackState>;
11
16
  /** The host's render loop writes time and particle count here. */
12
17
  counterRef: React.RefObject<HTMLSpanElement | null>;
18
+ /** The stage is playing. */
13
19
  stagePlaying: boolean;
20
+ /** Plays or pauses the stage. */
14
21
  setStagePlaying: (playing: boolean) => void;
22
+ /** Playback speed, 1 for real time. */
15
23
  speed: number;
24
+ /** Sets the playback speed. */
16
25
  setSpeed: (speed: number) => void;
17
26
  /** False when the focused effect is hidden. */
18
27
  enabled: boolean;
@@ -1,11 +1,38 @@
1
+ import type { ValueGenerator } from 'babylon.quarks';
1
2
  import React from 'react';
2
- import { ScalarGenerator } from '../core/values.js';
3
- export declare function ValueField(props: {
3
+ import { AnyGenerator, ScalarGenerator } from '../core/values.js';
4
+ /** Props every {@link ValueField} takes. */
5
+ interface ValueFieldBaseProps {
6
+ /** The row's label. */
4
7
  label: string;
8
+ /** Tooltip text of the label. */
5
9
  hint?: string;
10
+ /** Key into the inspector hints, used when `hint` is not given. */
6
11
  hintKey?: string;
7
- generator: ScalarGenerator | undefined;
8
- onChange: (generator: ScalarGenerator) => void;
12
+ /** The generator shown; one that is not a scalar reads as the constant 1. */
13
+ generator: AnyGenerator | undefined;
14
+ /** Smallest value the number fields accept. */
9
15
  min?: number;
16
+ /** Top of the curve editor's value range. */
10
17
  curveMax?: number;
11
- }): React.JSX.Element;
18
+ }
19
+ /** A value field for a setting that takes a curve over the particle's life. */
20
+ export interface CurveValueFieldProps extends ValueFieldBaseProps {
21
+ /** Offers Curve beside Constant and Random (the default). */
22
+ curve?: true;
23
+ /** Receives the edited generator. */
24
+ onChange: (generator: ScalarGenerator) => void;
25
+ }
26
+ /** A value field for a setting sampled once, without a curve: Constant or Random. */
27
+ export interface ConstantValueFieldProps extends ValueFieldBaseProps {
28
+ /** Hides Curve: the setting takes no curve. */
29
+ curve: false;
30
+ /** Receives the edited generator. */
31
+ onChange: (generator: ValueGenerator) => void;
32
+ }
33
+ /**
34
+ * Unity-like value field: Constant | Random Between Two Constants | Curve, bound to a quarks value
35
+ * generator. With `curve={false}` it offers only the first two, for settings sampled once.
36
+ */
37
+ export declare function ValueField(props: CurveValueFieldProps | ConstantValueFieldProps): React.ReactElement;
38
+ export {};
@@ -1,20 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values.js';
2
+ import { buildCurve, buildScalar, readPieces, readScalar, } from '../core/values.js';
3
3
  import { CurveEditor } from './CurveEditor.js';
4
4
  import { NumberField, Row, SelectField } from './fields.js';
5
+ const captionStyle = { fontSize: 10.5, opacity: 0.6, flex: 'none' };
6
+ function isValueGenerator(generator) {
7
+ return generator.type === 'value';
8
+ }
5
9
  /**
6
- * Unity-like value field: Constant | Random Between Two Constants | Curve,
7
- * bound to a quarks value generator.
10
+ * Unity-like value field: Constant | Random Between Two Constants | Curve, bound to a quarks value
11
+ * generator. With `curve={false}` it offers only the first two, for settings sampled once.
8
12
  */
9
- const captionStyle = { fontSize: 10.5, opacity: 0.6, flex: 'none' };
10
13
  export function ValueField(props) {
11
- const state = readScalar(props.generator);
12
- const commit = (patch) => {
13
- props.onChange(buildScalar({ ...state, ...patch }));
14
+ const read = readScalar(props.generator);
15
+ // A setting without curves shows a curve it was loaded with as a constant, so an edit replaces it.
16
+ const state = props.curve === false && read.mode === 'curve' ? { ...read, mode: 'constant' } : read;
17
+ const emit = (generator) => {
18
+ if (props.curve !== false) {
19
+ props.onChange(generator);
20
+ }
21
+ else if (isValueGenerator(generator)) {
22
+ props.onChange(generator);
23
+ }
14
24
  };
25
+ const commit = (patch) => emit(buildScalar({ ...state, ...patch }));
15
26
  return (_jsxs(_Fragment, { children: [_jsx(Row, { label: props.label, hint: props.hint, hintKey: props.hintKey, children: _jsx(SelectField, { value: state.mode, options: [
16
27
  { value: 'constant', label: 'Constant' },
17
28
  { value: 'random', label: 'Random between two' },
18
- { value: 'curve', label: 'Curve' },
19
- ], onChange: (mode) => commit({ mode }) }) }), state.mode === 'constant' && (_jsx(Row, { label: "", children: _jsx(NumberField, { value: state.value, min: props.min, onChange: (value) => commit({ value }) }) })), state.mode === 'random' && (_jsx(Row, { label: "", children: _jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("span", { style: captionStyle, children: "min" }), _jsx(NumberField, { value: state.min, min: props.min, onChange: (min) => commit({ min }) }), _jsx("span", { style: captionStyle, children: "max" }), _jsx(NumberField, { value: state.max, min: props.min, onChange: (max) => commit({ max }) })] }) })), state.mode === 'curve' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(props.generator), maxValue: props.curveMax, onChange: (pieces) => props.onChange(buildCurve(pieces)) }) }))] }));
29
+ ...(props.curve !== false ? [{ value: 'curve', label: 'Curve' }] : []),
30
+ ], onChange: (mode) => commit({ mode }) }) }), state.mode === 'constant' && (_jsx(Row, { label: "", children: _jsx(NumberField, { value: state.value, min: props.min, onChange: (value) => commit({ value }) }) })), state.mode === 'random' && (_jsx(Row, { label: "", children: _jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("span", { style: captionStyle, children: "min" }), _jsx(NumberField, { value: state.min, min: props.min, onChange: (min) => commit({ min }) }), _jsx("span", { style: captionStyle, children: "max" }), _jsx(NumberField, { value: state.max, min: props.min, onChange: (max) => commit({ max }) })] }) })), state.mode === 'curve' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(props.generator), maxValue: props.curveMax, onChange: (pieces) => emit(buildCurve(pieces)) }) }))] }));
20
31
  }
@@ -1,17 +1,19 @@
1
1
  import React from 'react';
2
- import { EffectBinding } from '../core/binding.js';
3
2
  import type { TextureOption } from './modules.js';
4
- interface ModuleProps {
5
- binding: EffectBinding;
6
- }
3
+ import { type ModuleProps } from './ModuleSection.js';
4
+ /** The Speed over Lifetime module (Unity's Speed Modifier): scales speed by a curve over the particle's life. */
7
5
  export declare function SpeedOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
6
+ /** The Limit Speed over Lifetime module (Unity's Limit Velocity over Lifetime): caps speed by a curve, with dampening. */
8
7
  export declare function LimitSpeedOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
8
+ /** The Force over Lifetime module (Unity's Force over Lifetime): a constant push per axis. */
9
9
  export declare function ForceOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
10
10
  export declare function VelocityOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
11
11
  export declare function InheritVelocityModule({ binding }: ModuleProps): React.JSX.Element;
12
12
  /** A point attractor (`GravityForce`): particles fall towards a point, harder the nearer they are. */
13
13
  export declare function AttractorModule({ binding }: ModuleProps): React.JSX.Element;
14
+ /** The Gravity module: pulls particles towards a point. */
14
15
  export declare function GravityModule({ binding }: ModuleProps): React.JSX.Element;
16
+ /** The Rotation over Lifetime module (Unity's Rotation over Lifetime): spin rate over the particle's life. */
15
17
  export declare function RotationOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
16
18
  /** Turns mesh particles by a 3D angular velocity; billboards turn through Rotation over Lifetime. */
17
19
  export declare function Rotation3DOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
@@ -23,14 +25,23 @@ export declare function Rotation3DOverLifeModule({ binding }: ModuleProps): Reac
23
25
  export declare function SequencesModule({ binding, textureOptions }: ModuleProps & {
24
26
  textureOptions?: TextureOption[];
25
27
  }): React.JSX.Element;
28
+ /** The Noise module (Unity's Noise): moves and turns particles by a noise field. */
26
29
  export declare function NoiseModule({ binding }: ModuleProps): React.JSX.Element;
30
+ /** The Turbulence module: a curl noise force field. */
27
31
  export declare function TurbulenceModule({ binding }: ModuleProps): React.JSX.Element;
32
+ /** The Randomize Direction module: tilts each particle's start direction by a random angle. */
28
33
  export declare function EmitDirectionModule({ binding }: ModuleProps): React.JSX.Element;
34
+ /** The Collision module (Unity's Collision): particles bounce off or die on a ground plane. */
29
35
  export declare function CollisionModule({ binding }: ModuleProps): React.JSX.Element;
36
+ /** The Color by Speed module (Unity's Color by Speed): a gradient over a speed range. */
30
37
  export declare function ColorBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
38
+ /** The Size by Speed module (Unity's Size by Speed): size over a speed range. */
31
39
  export declare function SizeBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
40
+ /** The Rotation by Speed module (Unity's Rotation by Speed): spin rate over a speed range. */
32
41
  export declare function RotationBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
42
+ /** The Orbit over Lifetime module (Unity's Velocity over Lifetime orbital): turns particles about an axis. */
33
43
  export declare function OrbitOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
44
+ /** The Width over Trail module (Unity's Trails width over trail): a trail's width along its length. */
34
45
  export declare function WidthOverTrailModule({ binding }: ModuleProps): React.JSX.Element;
46
+ /** The Texture Sheet Animation module (Unity's Texture Sheet Animation): plays the sheet's frames by life or by frame rate. */
35
47
  export declare function TextureSheetModule({ binding }: ModuleProps): React.JSX.Element;
36
- export {};
@@ -28,12 +28,15 @@ function BehaviorModule(props) {
28
28
  }
29
29
  }), children: behavior ? props.children(behavior) : null }));
30
30
  }
31
+ /** The Speed over Lifetime module (Unity's Speed Modifier): scales speed by a curve over the particle's life. */
31
32
  export function SpeedOverLifeModule({ binding }) {
32
33
  return (_jsx(BehaviorModule, { binding: binding, title: "Speed over Lifetime", type: "SpeedOverLife", create: () => new SpeedOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.5, 0] })), children: (behavior) => (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.speed), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces))) }) })) }));
33
34
  }
35
+ /** The Limit Speed over Lifetime module (Unity's Limit Velocity over Lifetime): caps speed by a curve, with dampening. */
34
36
  export function LimitSpeedOverLifeModule({ binding }) {
35
37
  return (_jsx(BehaviorModule, { binding: binding, title: "Limit Speed over Lifetime", type: "LimitSpeedOverLife", create: () => new LimitSpeedOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [4, 2, 1, 1] }), 0.5), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.speed), maxValue: 8, onChange: (pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces))) }) }), _jsx(Row, { label: "Dampen", children: _jsx(NumberField, { value: behavior.dampen, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.dampen = Math.min(1, v))) }) })] })) }));
36
38
  }
39
+ /** The Force over Lifetime module (Unity's Force over Lifetime): a constant push per axis. */
37
40
  export function ForceOverLifeModule({ binding }) {
38
41
  return (_jsx(BehaviorModule, { binding: binding, title: "Force over Lifetime", type: "ForceOverLife", create: () => new ForceOverLife(new ConstantValue(0), new ConstantValue(2), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
39
42
  { value: 'world', label: 'World' },
@@ -56,12 +59,14 @@ export function InheritVelocityModule({ binding }) {
56
59
  export function AttractorModule({ binding }) {
57
60
  return (_jsx(BehaviorModule, { binding: binding, title: "Attractor", type: "GravityForce", create: () => new GravityForce(new Vector3(0, 0, 0), 1), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Center", hintKey: "attractor.center", children: _jsx("div", { style: { display: 'flex', gap: 6 }, children: ['x', 'y', 'z'].map((axis) => (_jsx(NumberField, { value: behavior.center[axis], step: 0.1, onChange: (v) => binding.apply(() => (behavior.center[axis] = v)) }, axis))) }) }), _jsx(Row, { label: "Strength", hintKey: "attractor.magnitude", children: _jsx(NumberField, { value: behavior.magnitude, step: 0.1, onChange: (v) => binding.apply(() => (behavior.magnitude = v)) }) })] })) }));
58
61
  }
62
+ /** The Gravity module: pulls particles towards a point. */
59
63
  export function GravityModule({ binding }) {
60
64
  return (_jsx(BehaviorModule, { binding: binding, title: "Gravity", type: "ApplyForce", create: () => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Strength", hintKey: "gravity.strength", children: _jsx(NumberField, { value: readScalar(behavior.magnitude).value, step: 0.5, onChange: (v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v))) }) }), _jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
61
65
  { value: 'world', label: 'World' },
62
66
  { value: 'local', label: 'Local' },
63
67
  ], onChange: (v) => binding.apply(() => (behavior.space = v)) }) })] })) }));
64
68
  }
69
+ /** The Rotation over Lifetime module (Unity's Rotation over Lifetime): spin rate over the particle's life. */
65
70
  export function RotationOverLifeModule({ binding }) {
66
71
  return (_jsx(BehaviorModule, { binding: binding, title: "Rotation over Lifetime", type: "RotationOverLife", create: () => new RotationOverLife(new ConstantValue(Math.PI)), children: (behavior) => (_jsx(ValueField, { hintKey: "rotationOverLife.velocity", label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) })) }));
67
72
  }
@@ -137,6 +142,7 @@ export function SequencesModule({ binding, textureOptions }) {
137
142
  }, title: "Remove sequence", onClick: () => binding.apply(() => behavior.sequencers.splice(index, 1)), children: _jsx(XMarkIcon, { style: iconStyle(13) }) })] }), _jsx(Row, { label: "Time from / to (s)", children: pair(range.a, range.b, (v) => binding.apply(() => (range.a = v)), (v) => binding.apply(() => (range.b = v)), 0.1) }), texture.locations !== undefined && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Scale X / Y", children: pair(texture.scaleX, texture.scaleY, (v) => binding.apply(() => (texture.scaleX = v)), (v) => binding.apply(() => (texture.scaleY = v)), 0.01) }), _jsx(Vector3Row, { label: "Position", value: texture.position, step: 0.1, onChange: (v) => binding.apply(() => texture.position.copy(v)) })] }))] }, index));
138
143
  }), _jsx(Row, { label: "Image", children: textureOptions && textureOptions.length > 0 ? (_jsx(SelectField, { value: imageUrl, options: textureOptions.map((option) => ({ value: option.url, label: option.label })), onChange: (url) => setImageUrl(url) })) : (_jsx("input", { className: "qe-hover", style: { flex: 1, minWidth: 0, fontSize: 12 }, placeholder: "Image URL", value: imageUrl, onChange: (event) => setImageUrl(event.target.value) })) }), _jsx(Row, { label: "Alpha threshold", children: _jsx(NumberField, { value: threshold, min: 0, step: 0.05, onChange: (v) => setThreshold(Math.min(1, v)) }) }), _jsxs("button", { className: "qe-hover", style: { display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, fontSize: 12 }, onClick: () => void addFromImage(behavior), children: [_jsx(PlusIcon, { style: iconStyle(13) }), "Add sequence from image"] }), status && _jsx("p", { style: { margin: '4px 0 0', color: theme.textDim, fontSize: 11.5 }, children: status })] })) }));
139
144
  }
145
+ /** The Noise module (Unity's Noise): moves and turns particles by a noise field. */
140
146
  export function NoiseModule({ binding }) {
141
147
  return (_jsx(BehaviorModule, { binding: binding, title: "Noise", type: "Noise", create: () => new Noise(new ConstantValue(1), new ConstantValue(1), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "noise.frequency", label: "Frequency", generator: behavior.frequency, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.frequency = g)) }), _jsx(ValueField, { hintKey: "noise.strength", label: "Strength", generator: behavior.power, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.power = g)) }), _jsx(ValueField, { label: "Position amount", generator: behavior.positionAmount, min: 0, curveMax: 2, onChange: (g) => binding.apply(() => (behavior.positionAmount = g)) }), _jsx(ValueField, { label: "Rotation amount", generator: behavior.rotationAmount, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.rotationAmount = g)) }), _jsx(ValueField, { label: "Scroll speed", generator: behavior.scrollSpeed, curveMax: 5, onChange: (g) => binding.apply(() => (behavior.scrollSpeed = g)) }), _jsx(Row, { label: "Remap", children: _jsx(CheckboxField, { value: behavior.remap !== undefined, onChange: (v) => binding.apply(() => (behavior.remap = v
142
148
  ? new PiecewiseBezier([[new Bezier(-1, -1 / 3, 1 / 3, 1), 0]])
@@ -151,6 +157,7 @@ function Vector3Row(props) {
151
157
  };
152
158
  return (_jsx(Row, { label: props.label, hintKey: props.hintKey, children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: props.value.x, min: props.min, step: props.step, onChange: (n) => set('x', n) }), _jsx(NumberField, { value: props.value.y, min: props.min, step: props.step, onChange: (n) => set('y', n) }), _jsx(NumberField, { value: props.value.z, min: props.min, step: props.step, onChange: (n) => set('z', n) })] }) }));
153
159
  }
160
+ /** The Turbulence module: a curl noise force field. */
154
161
  export function TurbulenceModule({ binding }) {
155
162
  return (_jsx(BehaviorModule, { binding: binding, title: "Turbulence", type: "TurbulenceField", create: () => new TurbulenceField(new Vector3(1, 1, 1), 3, new Vector3(1, 1, 1), new Vector3(1, 1, 1)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Vector3Row, { hintKey: "turbulence.scale", label: "Scale", value: behavior.scale, min: 0.01, step: 0.1, onChange: (v) => binding.apply(() => (behavior.scale = v)) }), _jsx(Row, { label: "Octaves", children: _jsx(NumberField, { value: behavior.octaves, min: 1, step: 1, onChange: (v) => binding.apply(() => (behavior.octaves = Math.max(1, Math.round(v)))) }) }), _jsx(Vector3Row, { hintKey: "turbulence.strength", label: "Strength", value: behavior.velocityMultiplier, step: 0.5, onChange: (v) => binding.apply(() => (behavior.velocityMultiplier = v)) }), _jsx(Vector3Row, { label: "Time scale", value: behavior.timeScale, step: 0.1, onChange: (v) => binding.apply(() => (behavior.timeScale = v)) }), _jsx(Row, { label: "Seed", children: _jsx(NumberField, { value: behavior.seed, step: 1, onChange: (v) => binding.apply(() => {
156
163
  const seed = Math.round(v);
@@ -161,9 +168,11 @@ export function TurbulenceModule({ binding }) {
161
168
  behavior.timeOffset.copy(next.initialOffset);
162
169
  }) }) })] })) }));
163
170
  }
171
+ /** The Randomize Direction module: tilts each particle's start direction by a random angle. */
164
172
  export function EmitDirectionModule({ binding }) {
165
- return (_jsx(BehaviorModule, { binding: binding, title: "Randomize Direction", type: "ChangeEmitDirection", create: () => new ChangeEmitDirection(new IntervalValue(0, Math.PI / 6)), children: (behavior) => (_jsx(ValueField, { label: "Angle (rad)", generator: behavior.angle, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.angle = g)) })) }));
173
+ return (_jsx(BehaviorModule, { binding: binding, title: "Randomize Direction", type: "ChangeEmitDirection", create: () => new ChangeEmitDirection(new IntervalValue(0, Math.PI / 6)), children: (behavior) => (_jsx(ValueField, { label: "Angle (rad)", generator: behavior.angle, curve: false, min: 0, onChange: (g) => binding.apply(() => (behavior.angle = g)) })) }));
166
174
  }
175
+ /** The Collision module (Unity's Collision): particles bounce off or die on a ground plane. */
167
176
  export function CollisionModule({ binding }) {
168
177
  // Like Unity: Collision stores bounce only. Colliders are host-provided via
169
178
  // setPhysicsResolver, asked when a particle is tested — the editor never invents a ground plane.
@@ -173,20 +182,25 @@ export function CollisionModule({ binding }) {
173
182
  function SpeedRangeRows({ binding, behavior }) {
174
183
  return (_jsx(Row, { label: "Speed range", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: behavior.speedRange.a, min: 0, onChange: (v) => binding.apply(() => (behavior.speedRange = new IntervalValue(v, behavior.speedRange.b))) }), _jsx(NumberField, { value: behavior.speedRange.b, min: 0, onChange: (v) => binding.apply(() => (behavior.speedRange = new IntervalValue(behavior.speedRange.a, v))) })] }) }));
175
184
  }
185
+ /** The Color by Speed module (Unity's Color by Speed): a gradient over a speed range. */
176
186
  export function ColorBySpeedModule({ binding }) {
177
187
  return (_jsx(BehaviorModule, { binding: binding, title: "Color by Speed", type: "ColorBySpeed", create: () => new ColorBySpeed(buildGradient(DEFAULT_GRADIENT_STOPS), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(GradientEditor, { stops: behavior.color instanceof Gradient
178
188
  ? readGradientStops(behavior.color)
179
189
  : DEFAULT_GRADIENT_STOPS, onChange: (stops) => binding.apply(() => (behavior.color = buildGradient(stops))) }) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
180
190
  }
191
+ /** The Size by Speed module (Unity's Size by Speed): size over a speed range. */
181
192
  export function SizeBySpeedModule({ binding }) {
182
193
  return (_jsx(BehaviorModule, { binding: binding, title: "Size by Speed", type: "SizeBySpeed", create: () => new SizeBySpeed(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [0.5, 0.8, 1.2, 1.5] }), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.size), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.size = buildCurve(pieces))) }) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
183
194
  }
195
+ /** The Rotation by Speed module (Unity's Rotation by Speed): spin rate over a speed range. */
184
196
  export function RotationBySpeedModule({ binding }) {
185
197
  return (_jsx(BehaviorModule, { binding: binding, title: "Rotation by Speed", type: "RotationBySpeed", create: () => new RotationBySpeed(new ConstantValue(Math.PI), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "rotationBySpeed.velocity", label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
186
198
  }
199
+ /** The Orbit over Lifetime module (Unity's Velocity over Lifetime orbital): turns particles about an axis. */
187
200
  export function OrbitOverLifeModule({ binding }) {
188
201
  return (_jsx(BehaviorModule, { binding: binding, title: "Orbit over Lifetime", type: "OrbitOverLife", create: () => new OrbitOverLife(new ConstantValue(Math.PI), new Vector3(0, 1, 0)), children: (behavior) => (_jsx(ValueField, { label: "Orbit speed", generator: behavior.orbitSpeed, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitSpeed = g)) })) }));
189
202
  }
203
+ /** The Width over Trail module (Unity's Trails width over trail): a trail's width along its length. */
190
204
  export function WidthOverTrailModule({ binding }) {
191
205
  return (_jsx(BehaviorModule, { binding: binding, title: "Width over Trail", type: "WidthOverLength", create: () => new WidthOverLength(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0] })), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.width), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.width = buildCurve(pieces))) }) }), _jsx(Row, { label: "Absolute width", children: _jsx(CheckboxField, { value: behavior.absolute, onChange: (v) => binding.apply(() => (behavior.absolute = v)) }) })] })) }));
192
206
  }
@@ -195,6 +209,7 @@ function startFrameCount(startFrame) {
195
209
  const state = readScalar(startFrame);
196
210
  return Math.round(state.mode === 'random' ? state.max : state.value);
197
211
  }
212
+ /** The Texture Sheet Animation module (Unity's Texture Sheet Animation): plays the sheet's frames by life or by frame rate. */
198
213
  export function TextureSheetModule({ binding }) {
199
214
  const system = binding.system;
200
215
  const frameBehavior = findBehavior(system.behaviors, 'FrameOverLife');