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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/core/binding.d.ts +43 -0
- package/dist/core/binding.js +82 -0
- package/dist/core/collision.d.ts +20 -0
- package/dist/core/collision.js +39 -0
- package/dist/core/colors.d.ts +31 -0
- package/dist/core/colors.js +60 -0
- package/dist/core/effectTree.d.ts +25 -0
- package/dist/core/effectTree.js +115 -0
- package/dist/core/groups.d.ts +12 -0
- package/dist/core/groups.js +27 -0
- package/dist/core/history.d.ts +25 -0
- package/dist/core/history.js +69 -0
- package/dist/core/loadEffect.d.ts +8 -0
- package/dist/core/loadEffect.js +87 -0
- package/dist/core/scrub.d.ts +11 -0
- package/dist/core/scrub.js +24 -0
- package/dist/core/shapes.d.ts +31 -0
- package/dist/core/shapes.js +98 -0
- package/dist/core/systems.d.ts +25 -0
- package/dist/core/systems.js +76 -0
- package/dist/core/textureExport.d.ts +9 -0
- package/dist/core/textureExport.js +58 -0
- package/dist/core/timeline.d.ts +35 -0
- package/dist/core/timeline.js +69 -0
- package/dist/core/values.d.ts +31 -0
- package/dist/core/values.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +9 -0
- package/dist/react/CurveEditor.d.ts +12 -0
- package/dist/react/CurveEditor.js +106 -0
- package/dist/react/EffectEditor.d.ts +22 -0
- package/dist/react/EffectEditor.js +30 -0
- package/dist/react/EffectEditorHost.d.ts +42 -0
- package/dist/react/EffectEditorHost.js +247 -0
- package/dist/react/GradientEditor.d.ts +7 -0
- package/dist/react/GradientEditor.js +72 -0
- package/dist/react/ModuleSection.d.ts +9 -0
- package/dist/react/ModuleSection.js +24 -0
- package/dist/react/PromptDialog.d.ts +14 -0
- package/dist/react/PromptDialog.js +45 -0
- package/dist/react/SubEmittersModule.d.ts +10 -0
- package/dist/react/SubEmittersModule.js +39 -0
- package/dist/react/TimelinePanel.d.ts +32 -0
- package/dist/react/TimelinePanel.js +225 -0
- package/dist/react/TimelineTrackRow.d.ts +23 -0
- package/dist/react/TimelineTrackRow.js +107 -0
- package/dist/react/ValueField.d.ts +13 -0
- package/dist/react/ValueField.js +19 -0
- package/dist/react/behaviorModules.d.ts +23 -0
- package/dist/react/behaviorModules.js +139 -0
- package/dist/react/fields.d.ts +24 -0
- package/dist/react/fields.js +30 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react/index.js +14 -0
- package/dist/react/modules.d.ts +35 -0
- package/dist/react/modules.js +439 -0
- package/dist/react/theme.d.ts +26 -0
- package/dist/react/theme.js +78 -0
- package/package.json +63 -0
- package/src/core/binding.ts +105 -0
- package/src/core/collision.ts +40 -0
- package/src/core/colors.ts +84 -0
- package/src/core/effectTree.ts +143 -0
- package/src/core/groups.ts +29 -0
- package/src/core/history.ts +77 -0
- package/src/core/loadEffect.ts +102 -0
- package/src/core/scrub.ts +28 -0
- package/src/core/shapes.ts +133 -0
- package/src/core/systems.ts +98 -0
- package/src/core/textureExport.ts +63 -0
- package/src/core/timeline.ts +94 -0
- package/src/core/values.ts +113 -0
- package/src/index.ts +32 -0
- package/src/react/CurveEditor.tsx +168 -0
- package/src/react/EffectEditor.tsx +111 -0
- package/src/react/EffectEditorHost.tsx +377 -0
- package/src/react/GradientEditor.tsx +127 -0
- package/src/react/ModuleSection.tsx +54 -0
- package/src/react/PromptDialog.tsx +85 -0
- package/src/react/SubEmittersModule.tsx +80 -0
- package/src/react/TimelinePanel.tsx +354 -0
- package/src/react/TimelineTrackRow.tsx +267 -0
- package/src/react/ValueField.tsx +60 -0
- package/src/react/behaviorModules.tsx +658 -0
- package/src/react/fields.tsx +76 -0
- package/src/react/index.ts +43 -0
- package/src/react/modules.tsx +1034 -0
- 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
|
+
}
|