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,127 @@
1
+ import React, {useRef, useState} from 'react';
2
+ import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors';
3
+ import {NumberField, Row} from './fields';
4
+ import {buttonStyle, theme} from './theme';
5
+
6
+ const BAR_WIDTH = 218;
7
+
8
+ /** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
9
+ export function GradientEditor(props: {stops: GradientStop[]; onChange: (stops: GradientStop[]) => void}) {
10
+ const [selected, setSelected] = useState(0);
11
+ const barRef = useRef<HTMLDivElement>(null);
12
+ const dragging = useRef(false);
13
+
14
+ const stops = props.stops;
15
+ const current = stops[Math.min(selected, stops.length - 1)];
16
+
17
+ const posFromEvent = (clientX: number) => {
18
+ const rect = barRef.current!.getBoundingClientRect();
19
+ return Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
20
+ };
21
+
22
+ const updateStop = (index: number, patch: Partial<GradientStop>) => {
23
+ const next = stops.map((s, i) => (i === index ? {...s, ...patch} : s));
24
+ props.onChange(next);
25
+ };
26
+
27
+ return (
28
+ <div>
29
+ <div
30
+ ref={barRef}
31
+ className="qe-hover"
32
+ style={{
33
+ position: 'relative',
34
+ width: BAR_WIDTH,
35
+ height: 20,
36
+ borderRadius: 6,
37
+ border: `1px solid ${theme.border}`,
38
+ background: `${stopsToCssGradient(stops)}, repeating-conic-gradient(#333 0% 25%, #555 0% 50%) 0 0 / 10px 10px`,
39
+ cursor: 'copy',
40
+ }}
41
+ onPointerDown={(e) => {
42
+ if (e.target !== barRef.current) {
43
+ return;
44
+ }
45
+ const pos = posFromEvent(e.clientX);
46
+ const base = current ?? {r: 1, g: 1, b: 1, a: 1, pos: 0};
47
+ const next = [...stops, {...base, pos}];
48
+ props.onChange(next);
49
+ setSelected(next.length - 1);
50
+ }}
51
+ onPointerMove={(e) => {
52
+ if (dragging.current) {
53
+ updateStop(selected, {pos: posFromEvent(e.clientX)});
54
+ }
55
+ }}
56
+ onPointerUp={() => {
57
+ dragging.current = false;
58
+ }}
59
+ >
60
+ {stops.map((stop, i) => (
61
+ <div
62
+ key={i}
63
+ className="qe-hover"
64
+ style={{
65
+ position: 'absolute',
66
+ left: `calc(${stop.pos * 100}% - 6px)`,
67
+ top: -4,
68
+ width: 12,
69
+ height: 26,
70
+ borderRadius: 4,
71
+ background: rgbToHex(stop.r, stop.g, stop.b),
72
+ border: `2px solid ${i === selected ? theme.accent : '#0a1020'}`,
73
+ cursor: 'ew-resize',
74
+ boxSizing: 'border-box',
75
+ }}
76
+ onPointerDown={(e) => {
77
+ e.stopPropagation();
78
+ setSelected(i);
79
+ dragging.current = true;
80
+ (e.currentTarget as Element).setPointerCapture(e.pointerId);
81
+ }}
82
+ onPointerMove={(e) => {
83
+ if (dragging.current && i === selected) {
84
+ updateStop(i, {pos: posFromEvent(e.clientX)});
85
+ }
86
+ }}
87
+ onPointerUp={(e) => {
88
+ dragging.current = false;
89
+ (e.currentTarget as Element).releasePointerCapture(e.pointerId);
90
+ }}
91
+ />
92
+ ))}
93
+ </div>
94
+ {current && (
95
+ <>
96
+ <Row label="Stop color">
97
+ <input
98
+ type="color"
99
+ className="qe-hover"
100
+ value={rgbToHex(current.r, current.g, current.b)}
101
+ onChange={(e) => updateStop(selected, hexToRgb(e.target.value))}
102
+ style={{width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer'}}
103
+ />
104
+ </Row>
105
+ <Row label="Stop alpha">
106
+ <NumberField value={current.a} min={0} step={0.05} onChange={(a) => updateStop(selected, {a: Math.min(1, a)})} />
107
+ </Row>
108
+ <Row label="Stop position">
109
+ <NumberField value={current.pos} min={0} step={0.05} onChange={(pos) => updateStop(selected, {pos: Math.min(1, pos)})} />
110
+ </Row>
111
+ {stops.length > 2 && (
112
+ <button
113
+ className="qe-hover"
114
+ style={{...buttonStyle, marginTop: 6}}
115
+ onClick={() => {
116
+ props.onChange(stops.filter((_, i) => i !== selected));
117
+ setSelected(0);
118
+ }}
119
+ >
120
+ Remove stop
121
+ </button>
122
+ )}
123
+ </>
124
+ )}
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,54 @@
1
+ import React, {useState} from 'react';
2
+ import {theme} from './theme';
3
+
4
+ /** Unity-style module foldout with an optional enable checkbox. */
5
+ export function ModuleSection(props: {
6
+ title: string;
7
+ children: React.ReactNode;
8
+ enabled?: boolean;
9
+ onToggle?: (enabled: boolean) => void;
10
+ defaultOpen?: boolean;
11
+ }) {
12
+ const [open, setOpen] = useState(props.defaultOpen ?? true);
13
+ const toggleable = props.onToggle !== undefined;
14
+ const dimmed = toggleable && props.enabled === false;
15
+ return (
16
+ <section
17
+ style={{
18
+ border: `1px solid ${theme.border}`,
19
+ borderRadius: 10,
20
+ background: theme.sectionBg,
21
+ marginTop: 8,
22
+ overflow: 'hidden',
23
+ }}
24
+ >
25
+ <header
26
+ className="qe-hover"
27
+ style={{
28
+ display: 'flex',
29
+ alignItems: 'center',
30
+ gap: 8,
31
+ padding: '7px 10px',
32
+ cursor: 'pointer',
33
+ userSelect: 'none',
34
+ background: 'rgba(20, 30, 60, 0.5)',
35
+ }}
36
+ onClick={() => setOpen(!open)}
37
+ >
38
+ <span style={{color: theme.accent, fontSize: 10, width: 10}}>{open ? '▼' : '►'}</span>
39
+ {toggleable && (
40
+ <input
41
+ type="checkbox"
42
+ className="qe-hover"
43
+ checked={props.enabled}
44
+ onClick={(e) => e.stopPropagation()}
45
+ onChange={(e) => props.onToggle?.(e.target.checked)}
46
+ style={{width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer'}}
47
+ />
48
+ )}
49
+ <span style={{color: dimmed ? theme.textDim : theme.text, fontSize: 13, fontWeight: 600}}>{props.title}</span>
50
+ </header>
51
+ {open && !dimmed && <div style={{padding: '4px 10px 10px'}}>{props.children}</div>}
52
+ </section>
53
+ );
54
+ }
@@ -0,0 +1,85 @@
1
+ import React, {useEffect, useRef, useState} from 'react';
2
+ import {buttonStyle, inputStyle, theme} from './theme';
3
+
4
+ export interface PromptDialogProps {
5
+ title: string;
6
+ defaultValue?: string;
7
+ placeholder?: string;
8
+ confirmLabel?: string;
9
+ /** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
10
+ allowEmpty?: boolean;
11
+ onSubmit: (value: string) => void;
12
+ onCancel: () => void;
13
+ }
14
+
15
+ /** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
16
+ * Enter, cancels on Escape or backdrop click. */
17
+ export function PromptDialog(props: PromptDialogProps): React.ReactElement {
18
+ const {title, defaultValue = '', placeholder, confirmLabel = 'OK', allowEmpty = false, onSubmit, onCancel} = props;
19
+ const [value, setValue] = useState(defaultValue);
20
+ const inputRef = useRef<HTMLInputElement>(null);
21
+
22
+ useEffect(() => {
23
+ inputRef.current?.focus();
24
+ inputRef.current?.select();
25
+ }, []);
26
+
27
+ const submit = () => {
28
+ const trimmed = value.trim();
29
+ if (trimmed || allowEmpty) {
30
+ onSubmit(trimmed);
31
+ } else {
32
+ onCancel();
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div
38
+ style={{
39
+ position: 'fixed',
40
+ inset: 0,
41
+ background: 'rgba(3, 5, 12, 0.6)',
42
+ display: 'flex',
43
+ alignItems: 'center',
44
+ justifyContent: 'center',
45
+ zIndex: 1000,
46
+ fontFamily: theme.font,
47
+ }}
48
+ onClick={onCancel}
49
+ >
50
+ <div
51
+ style={{
52
+ background: theme.panelBg,
53
+ border: `1px solid ${theme.border}`,
54
+ borderRadius: 10,
55
+ padding: 16,
56
+ minWidth: 300,
57
+ boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
58
+ }}
59
+ onClick={(e) => e.stopPropagation()}
60
+ >
61
+ <div style={{fontSize: 13, color: theme.text, marginBottom: 10}}>{title}</div>
62
+ <input
63
+ ref={inputRef}
64
+ className="qe-hover"
65
+ style={{...inputStyle, marginBottom: 14}}
66
+ value={value}
67
+ placeholder={placeholder}
68
+ onChange={(e) => setValue(e.target.value)}
69
+ onKeyDown={(e) => {
70
+ if (e.key === 'Enter') submit();
71
+ if (e.key === 'Escape') onCancel();
72
+ }}
73
+ />
74
+ <div style={{display: 'flex', justifyContent: 'flex-end', gap: 8}}>
75
+ <button className="qe-hover" style={buttonStyle} onClick={onCancel}>
76
+ Cancel
77
+ </button>
78
+ <button className="qe-hover" style={{...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent}} onClick={submit}>
79
+ {confirmLabel}
80
+ </button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,80 @@
1
+ import React from 'react';
2
+ import {EmitSubParticleSystem, SubParticleEmitMode} from 'babylon.quarks';
3
+ import type {ParticleEmitter, ParticleSystem} from 'babylon.quarks';
4
+ import {EffectBinding} from '../core/binding';
5
+ import {findBehavior} from '../core/colors';
6
+ import {createChildSystem} from '../core/systems';
7
+ import {ModuleSection} from './ModuleSection';
8
+ import {CheckboxField, NumberField, Row, SelectField} from './fields';
9
+ import {theme} from './theme';
10
+
11
+ /**
12
+ * Sub Emitters module for the currently selected system. The spawned target lives as a
13
+ * child system in the effect hierarchy — edit it by selecting it there (Unity pattern).
14
+ */
15
+ export function SubEmittersModule(props: {binding: EffectBinding; rootBinding: EffectBinding}) {
16
+ const {binding, rootBinding} = props;
17
+ const system = binding.system;
18
+ const behavior = findBehavior<EmitSubParticleSystem>(system.behaviors, 'EmitSubParticleSystem');
19
+ const target = (behavior?.subParticleSystem as ParticleEmitter | undefined)?.system as ParticleSystem | undefined;
20
+
21
+ return (
22
+ <ModuleSection
23
+ title="Sub Emitters"
24
+ enabled={!!behavior}
25
+ defaultOpen={false}
26
+ onToggle={(enabled) =>
27
+ binding.apply((s) => {
28
+ for (let i = s.behaviors.length - 1; i >= 0; i--) {
29
+ if ((s.behaviors[i] as {type?: string}).type === 'EmitSubParticleSystem') {
30
+ s.behaviors.splice(i, 1);
31
+ }
32
+ }
33
+ if (enabled) {
34
+ const sub = createChildSystem(s, {oneShot: true});
35
+ s._renderer?.addSystem(sub);
36
+ rootBinding.addSubSystem(sub);
37
+ s.addBehavior(new EmitSubParticleSystem(s, false, sub.emitter, SubParticleEmitMode.Death, 1));
38
+ } else if (target) {
39
+ s._renderer?.deleteSystem(target);
40
+ target.dispose();
41
+ rootBinding.removeSubSystem(target);
42
+ }
43
+ })
44
+ }
45
+ >
46
+ {behavior && (
47
+ <>
48
+ <Row label="Trigger">
49
+ <SelectField
50
+ value={behavior.mode}
51
+ options={[
52
+ {value: SubParticleEmitMode.Death, label: 'On death'},
53
+ {value: SubParticleEmitMode.Birth, label: 'On birth'},
54
+ {value: SubParticleEmitMode.Frame, label: 'Every frame'},
55
+ ]}
56
+ onChange={(mode) => binding.apply(() => (behavior.mode = mode))}
57
+ />
58
+ </Row>
59
+ <Row label="Probability">
60
+ <NumberField
61
+ value={behavior.emitProbability}
62
+ min={0}
63
+ step={0.05}
64
+ onChange={(v) => binding.apply(() => (behavior.emitProbability = Math.min(1, v)))}
65
+ />
66
+ </Row>
67
+ <Row label="Use velocity">
68
+ <CheckboxField
69
+ value={behavior.useVelocityAsBasis}
70
+ onChange={(v) => binding.apply(() => (behavior.useVelocityAsBasis = v))}
71
+ />
72
+ </Row>
73
+ <p style={{margin: '8px 0 2px', color: theme.textDim, fontSize: 11.5}}>
74
+ Spawns “{target?.emitter.name ?? 'sub-emitter'}” — select it in the hierarchy above to edit it.
75
+ </p>
76
+ </>
77
+ )}
78
+ </ModuleSection>
79
+ );
80
+ }