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,658 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ApplyCollision,
|
|
4
|
+
ApplyForce,
|
|
5
|
+
ChangeEmitDirection,
|
|
6
|
+
ColorBySpeed,
|
|
7
|
+
ConstantValue,
|
|
8
|
+
ForceOverLife,
|
|
9
|
+
FrameOverLife,
|
|
10
|
+
Gradient,
|
|
11
|
+
InheritVelocity,
|
|
12
|
+
IntervalValue,
|
|
13
|
+
LimitSpeedOverLife,
|
|
14
|
+
Noise,
|
|
15
|
+
OrbitOverLife,
|
|
16
|
+
RotationBySpeed,
|
|
17
|
+
RotationOverLife,
|
|
18
|
+
SizeBySpeed,
|
|
19
|
+
SpeedOverLife,
|
|
20
|
+
TurbulenceField,
|
|
21
|
+
Vector3,
|
|
22
|
+
VelocityOverLife,
|
|
23
|
+
WidthOverLength,
|
|
24
|
+
} from 'babylon.quarks';
|
|
25
|
+
import type {Behavior} from 'babylon.quarks';
|
|
26
|
+
import {EffectBinding} from '../core/binding';
|
|
27
|
+
import {ensureGroundResolver} from '../core/collision';
|
|
28
|
+
import {DEFAULT_GRADIENT_STOPS, buildGradient, findBehavior, readGradientStops} from '../core/colors';
|
|
29
|
+
import {buildCurve, buildScalar, readPieces, readScalar} from '../core/values';
|
|
30
|
+
import {GradientEditor} from './GradientEditor';
|
|
31
|
+
import {CurveEditor} from './CurveEditor';
|
|
32
|
+
import {ModuleSection} from './ModuleSection';
|
|
33
|
+
import {NumberField, Row, SelectField} from './fields';
|
|
34
|
+
import {ValueField} from './ValueField';
|
|
35
|
+
|
|
36
|
+
interface ModuleProps {
|
|
37
|
+
binding: EffectBinding;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeBehaviorOfType(behaviors: Behavior[], type: string): void {
|
|
41
|
+
for (let i = behaviors.length - 1; i >= 0; i--) {
|
|
42
|
+
if ((behaviors[i] as {type?: string}).type === type) {
|
|
43
|
+
behaviors.splice(i, 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Shared shell for modules backed by a single toggleable quarks behavior. */
|
|
49
|
+
function BehaviorModule<T extends Behavior>(props: {
|
|
50
|
+
binding: EffectBinding;
|
|
51
|
+
title: string;
|
|
52
|
+
type: string;
|
|
53
|
+
create: () => T;
|
|
54
|
+
children: (behavior: T) => React.ReactNode;
|
|
55
|
+
}) {
|
|
56
|
+
const behavior = findBehavior<T>(props.binding.system.behaviors, props.type);
|
|
57
|
+
return (
|
|
58
|
+
<ModuleSection
|
|
59
|
+
title={props.title}
|
|
60
|
+
enabled={!!behavior}
|
|
61
|
+
defaultOpen={false}
|
|
62
|
+
onToggle={(enabled) =>
|
|
63
|
+
props.binding.apply((s) => {
|
|
64
|
+
removeBehaviorOfType(s.behaviors, props.type);
|
|
65
|
+
if (enabled) {
|
|
66
|
+
s.addBehavior(props.create());
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
{behavior ? props.children(behavior) : null}
|
|
72
|
+
</ModuleSection>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function SpeedOverLifeModule({binding}: ModuleProps) {
|
|
77
|
+
return (
|
|
78
|
+
<BehaviorModule<SpeedOverLife>
|
|
79
|
+
binding={binding}
|
|
80
|
+
title="Speed over Lifetime"
|
|
81
|
+
type="SpeedOverLife"
|
|
82
|
+
create={() => new SpeedOverLife(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.5, 0]}) as never)}
|
|
83
|
+
>
|
|
84
|
+
{(behavior) => (
|
|
85
|
+
<div style={{marginTop: 6}}>
|
|
86
|
+
<CurveEditor
|
|
87
|
+
pieces={readPieces(behavior.speed)}
|
|
88
|
+
maxValue={2}
|
|
89
|
+
onChange={(pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces) as never))}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</BehaviorModule>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function LimitSpeedOverLifeModule({binding}: ModuleProps) {
|
|
98
|
+
return (
|
|
99
|
+
<BehaviorModule<LimitSpeedOverLife>
|
|
100
|
+
binding={binding}
|
|
101
|
+
title="Limit Speed over Lifetime"
|
|
102
|
+
type="LimitSpeedOverLife"
|
|
103
|
+
create={() => new LimitSpeedOverLife(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [4, 2, 1, 1]}) as never, 0.5)}
|
|
104
|
+
>
|
|
105
|
+
{(behavior) => (
|
|
106
|
+
<>
|
|
107
|
+
<div style={{marginTop: 6}}>
|
|
108
|
+
<CurveEditor
|
|
109
|
+
pieces={readPieces(behavior.speed)}
|
|
110
|
+
maxValue={8}
|
|
111
|
+
onChange={(pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces) as never))}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
<Row label="Dampen">
|
|
115
|
+
<NumberField
|
|
116
|
+
value={behavior.dampen}
|
|
117
|
+
min={0}
|
|
118
|
+
step={0.05}
|
|
119
|
+
onChange={(v) => binding.apply(() => (behavior.dampen = Math.min(1, v)))}
|
|
120
|
+
/>
|
|
121
|
+
</Row>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</BehaviorModule>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function ForceOverLifeModule({binding}: ModuleProps) {
|
|
129
|
+
return (
|
|
130
|
+
<BehaviorModule<ForceOverLife>
|
|
131
|
+
binding={binding}
|
|
132
|
+
title="Force over Lifetime"
|
|
133
|
+
type="ForceOverLife"
|
|
134
|
+
create={() => new ForceOverLife(new ConstantValue(0), new ConstantValue(2), new ConstantValue(0))}
|
|
135
|
+
>
|
|
136
|
+
{(behavior) => (
|
|
137
|
+
<>
|
|
138
|
+
<ValueField label="X" generator={behavior.x} curveMax={10} onChange={(g) => binding.apply(() => (behavior.x = g))} />
|
|
139
|
+
<ValueField label="Y" generator={behavior.y} curveMax={10} onChange={(g) => binding.apply(() => (behavior.y = g))} />
|
|
140
|
+
<ValueField label="Z" generator={behavior.z} curveMax={10} onChange={(g) => binding.apply(() => (behavior.z = g))} />
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</BehaviorModule>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function VelocityOverLifeModule({binding}: ModuleProps) {
|
|
148
|
+
return (
|
|
149
|
+
<BehaviorModule<VelocityOverLife>
|
|
150
|
+
binding={binding}
|
|
151
|
+
title="Velocity over Lifetime"
|
|
152
|
+
type="VelocityOverLife"
|
|
153
|
+
create={() => new VelocityOverLife(new ConstantValue(0), new ConstantValue(1), new ConstantValue(0))}
|
|
154
|
+
>
|
|
155
|
+
{(behavior) => (
|
|
156
|
+
<>
|
|
157
|
+
<Row label="Space">
|
|
158
|
+
<SelectField
|
|
159
|
+
value={behavior.space}
|
|
160
|
+
options={[
|
|
161
|
+
{value: 'local', label: 'Local'},
|
|
162
|
+
{value: 'world', label: 'World'},
|
|
163
|
+
]}
|
|
164
|
+
onChange={(v) => binding.apply(() => (behavior.space = v))}
|
|
165
|
+
/>
|
|
166
|
+
</Row>
|
|
167
|
+
<ValueField label="Linear X" generator={behavior.linearX} curveMax={10} onChange={(g) => binding.apply(() => (behavior.linearX = g))} />
|
|
168
|
+
<ValueField label="Linear Y" generator={behavior.linearY} curveMax={10} onChange={(g) => binding.apply(() => (behavior.linearY = g))} />
|
|
169
|
+
<ValueField label="Linear Z" generator={behavior.linearZ} curveMax={10} onChange={(g) => binding.apply(() => (behavior.linearZ = g))} />
|
|
170
|
+
<ValueField label="Orbital X" generator={behavior.orbitalX} curveMax={Math.PI * 4} onChange={(g) => binding.apply(() => (behavior.orbitalX = g))} />
|
|
171
|
+
<ValueField label="Orbital Y" generator={behavior.orbitalY} curveMax={Math.PI * 4} onChange={(g) => binding.apply(() => (behavior.orbitalY = g))} />
|
|
172
|
+
<ValueField label="Orbital Z" generator={behavior.orbitalZ} curveMax={Math.PI * 4} onChange={(g) => binding.apply(() => (behavior.orbitalZ = g))} />
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
</BehaviorModule>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function InheritVelocityModule({binding}: ModuleProps) {
|
|
180
|
+
return (
|
|
181
|
+
<BehaviorModule<InheritVelocity>
|
|
182
|
+
binding={binding}
|
|
183
|
+
title="Inherit Velocity"
|
|
184
|
+
type="InheritVelocity"
|
|
185
|
+
create={() => new InheritVelocity(new ConstantValue(1), 'initial')}
|
|
186
|
+
>
|
|
187
|
+
{(behavior) => (
|
|
188
|
+
<>
|
|
189
|
+
<Row label="Mode">
|
|
190
|
+
<SelectField
|
|
191
|
+
value={behavior.mode}
|
|
192
|
+
options={[
|
|
193
|
+
{value: 'initial', label: 'Initial'},
|
|
194
|
+
{value: 'current', label: 'Current'},
|
|
195
|
+
]}
|
|
196
|
+
onChange={(v) => binding.apply(() => (behavior.mode = v))}
|
|
197
|
+
/>
|
|
198
|
+
</Row>
|
|
199
|
+
<ValueField
|
|
200
|
+
label="Multiplier"
|
|
201
|
+
generator={behavior.multiplier}
|
|
202
|
+
curveMax={2}
|
|
203
|
+
onChange={(g) => binding.apply(() => (behavior.multiplier = g))}
|
|
204
|
+
/>
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
</BehaviorModule>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function GravityModule({binding}: ModuleProps) {
|
|
212
|
+
return (
|
|
213
|
+
<BehaviorModule<ApplyForce>
|
|
214
|
+
binding={binding}
|
|
215
|
+
title="Gravity"
|
|
216
|
+
type="ApplyForce"
|
|
217
|
+
create={() => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81))}
|
|
218
|
+
>
|
|
219
|
+
{(behavior) => (
|
|
220
|
+
<Row label="Strength">
|
|
221
|
+
<NumberField
|
|
222
|
+
value={readScalar(behavior.magnitude).value}
|
|
223
|
+
step={0.5}
|
|
224
|
+
onChange={(v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v)))}
|
|
225
|
+
/>
|
|
226
|
+
</Row>
|
|
227
|
+
)}
|
|
228
|
+
</BehaviorModule>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function RotationOverLifeModule({binding}: ModuleProps) {
|
|
233
|
+
return (
|
|
234
|
+
<BehaviorModule<RotationOverLife>
|
|
235
|
+
binding={binding}
|
|
236
|
+
title="Rotation over Lifetime"
|
|
237
|
+
type="RotationOverLife"
|
|
238
|
+
create={() => new RotationOverLife(new ConstantValue(Math.PI))}
|
|
239
|
+
>
|
|
240
|
+
{(behavior) => (
|
|
241
|
+
<ValueField
|
|
242
|
+
label="Velocity (rad/s)"
|
|
243
|
+
generator={behavior.angularVelocity}
|
|
244
|
+
curveMax={Math.PI * 4}
|
|
245
|
+
onChange={(g) => binding.apply(() => (behavior.angularVelocity = g))}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
</BehaviorModule>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function NoiseModule({binding}: ModuleProps) {
|
|
253
|
+
return (
|
|
254
|
+
<BehaviorModule<Noise>
|
|
255
|
+
binding={binding}
|
|
256
|
+
title="Noise"
|
|
257
|
+
type="Noise"
|
|
258
|
+
create={() => new Noise(new ConstantValue(1), new ConstantValue(1), new ConstantValue(1), new ConstantValue(0))}
|
|
259
|
+
>
|
|
260
|
+
{(behavior) => (
|
|
261
|
+
<>
|
|
262
|
+
<ValueField
|
|
263
|
+
label="Frequency"
|
|
264
|
+
generator={behavior.frequency}
|
|
265
|
+
min={0}
|
|
266
|
+
curveMax={10}
|
|
267
|
+
onChange={(g) => binding.apply(() => (behavior.frequency = g))}
|
|
268
|
+
/>
|
|
269
|
+
<ValueField
|
|
270
|
+
label="Strength"
|
|
271
|
+
generator={behavior.power}
|
|
272
|
+
min={0}
|
|
273
|
+
curveMax={10}
|
|
274
|
+
onChange={(g) => binding.apply(() => (behavior.power = g))}
|
|
275
|
+
/>
|
|
276
|
+
<ValueField
|
|
277
|
+
label="Position amount"
|
|
278
|
+
generator={behavior.positionAmount}
|
|
279
|
+
min={0}
|
|
280
|
+
curveMax={2}
|
|
281
|
+
onChange={(g) => binding.apply(() => (behavior.positionAmount = g))}
|
|
282
|
+
/>
|
|
283
|
+
<ValueField
|
|
284
|
+
label="Rotation amount"
|
|
285
|
+
generator={behavior.rotationAmount}
|
|
286
|
+
min={0}
|
|
287
|
+
curveMax={Math.PI}
|
|
288
|
+
onChange={(g) => binding.apply(() => (behavior.rotationAmount = g))}
|
|
289
|
+
/>
|
|
290
|
+
</>
|
|
291
|
+
)}
|
|
292
|
+
</BehaviorModule>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Three numeric inputs for a Vector3 field, replacing the whole vector on any edit. */
|
|
297
|
+
function Vector3Row(props: {label: string; value: Vector3; min?: number; step?: number; onChange: (v: Vector3) => void}) {
|
|
298
|
+
const set = (axis: 'x' | 'y' | 'z', n: number) => {
|
|
299
|
+
const next = props.value.clone();
|
|
300
|
+
next[axis] = n;
|
|
301
|
+
props.onChange(next);
|
|
302
|
+
};
|
|
303
|
+
return (
|
|
304
|
+
<Row label={props.label}>
|
|
305
|
+
<div style={{display: 'flex', gap: 6}}>
|
|
306
|
+
<NumberField value={props.value.x} min={props.min} step={props.step} onChange={(n) => set('x', n)} />
|
|
307
|
+
<NumberField value={props.value.y} min={props.min} step={props.step} onChange={(n) => set('y', n)} />
|
|
308
|
+
<NumberField value={props.value.z} min={props.min} step={props.step} onChange={(n) => set('z', n)} />
|
|
309
|
+
</div>
|
|
310
|
+
</Row>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function TurbulenceModule({binding}: ModuleProps) {
|
|
315
|
+
return (
|
|
316
|
+
<BehaviorModule<TurbulenceField>
|
|
317
|
+
binding={binding}
|
|
318
|
+
title="Turbulence"
|
|
319
|
+
type="TurbulenceField"
|
|
320
|
+
create={() => new TurbulenceField(new Vector3(1, 1, 1), 3, new Vector3(1, 1, 1), new Vector3(1, 1, 1))}
|
|
321
|
+
>
|
|
322
|
+
{(behavior) => (
|
|
323
|
+
<>
|
|
324
|
+
{/* scale divides particle position, so keep each axis strictly positive. */}
|
|
325
|
+
<Vector3Row label="Scale" value={behavior.scale} min={0.01} step={0.1} onChange={(v) => binding.apply(() => (behavior.scale = v))} />
|
|
326
|
+
<Row label="Octaves">
|
|
327
|
+
<NumberField
|
|
328
|
+
value={behavior.octaves}
|
|
329
|
+
min={1}
|
|
330
|
+
step={1}
|
|
331
|
+
onChange={(v) => binding.apply(() => (behavior.octaves = Math.max(1, Math.round(v))))}
|
|
332
|
+
/>
|
|
333
|
+
</Row>
|
|
334
|
+
<Vector3Row
|
|
335
|
+
label="Strength"
|
|
336
|
+
value={behavior.velocityMultiplier}
|
|
337
|
+
step={0.5}
|
|
338
|
+
onChange={(v) => binding.apply(() => (behavior.velocityMultiplier = v))}
|
|
339
|
+
/>
|
|
340
|
+
<Vector3Row
|
|
341
|
+
label="Time scale"
|
|
342
|
+
value={behavior.timeScale}
|
|
343
|
+
step={0.1}
|
|
344
|
+
onChange={(v) => binding.apply(() => (behavior.timeScale = v))}
|
|
345
|
+
/>
|
|
346
|
+
</>
|
|
347
|
+
)}
|
|
348
|
+
</BehaviorModule>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function EmitDirectionModule({binding}: ModuleProps) {
|
|
353
|
+
return (
|
|
354
|
+
<BehaviorModule<ChangeEmitDirection>
|
|
355
|
+
binding={binding}
|
|
356
|
+
title="Randomize Direction"
|
|
357
|
+
type="ChangeEmitDirection"
|
|
358
|
+
create={() => new ChangeEmitDirection(new IntervalValue(0, Math.PI / 6))}
|
|
359
|
+
>
|
|
360
|
+
{(behavior) => (
|
|
361
|
+
<ValueField
|
|
362
|
+
label="Angle (rad)"
|
|
363
|
+
generator={behavior.angle as never}
|
|
364
|
+
min={0}
|
|
365
|
+
curveMax={Math.PI}
|
|
366
|
+
onChange={(g) => binding.apply(() => (behavior.angle = g as never))}
|
|
367
|
+
/>
|
|
368
|
+
)}
|
|
369
|
+
</BehaviorModule>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function CollisionModule({binding}: ModuleProps) {
|
|
374
|
+
// Ensure the shared ground-plane resolver exists before any ApplyCollision is created,
|
|
375
|
+
// otherwise its update() would dereference an undefined resolver.
|
|
376
|
+
const resolver = ensureGroundResolver();
|
|
377
|
+
return (
|
|
378
|
+
<BehaviorModule<ApplyCollision>
|
|
379
|
+
binding={binding}
|
|
380
|
+
title="Collision"
|
|
381
|
+
type="ApplyCollision"
|
|
382
|
+
create={() => new ApplyCollision(ensureGroundResolver(), 0.5)}
|
|
383
|
+
>
|
|
384
|
+
{(behavior) => (
|
|
385
|
+
<>
|
|
386
|
+
<Row label="Bounce">
|
|
387
|
+
<NumberField
|
|
388
|
+
value={behavior.bounce}
|
|
389
|
+
min={0}
|
|
390
|
+
step={0.05}
|
|
391
|
+
onChange={(v) => binding.apply(() => (behavior.bounce = v))}
|
|
392
|
+
/>
|
|
393
|
+
</Row>
|
|
394
|
+
<Row label="Floor Y">
|
|
395
|
+
<NumberField
|
|
396
|
+
value={resolver.y}
|
|
397
|
+
step={0.1}
|
|
398
|
+
onChange={(v) => binding.apply(() => (resolver.y = v))}
|
|
399
|
+
/>
|
|
400
|
+
</Row>
|
|
401
|
+
<div style={{fontSize: 11, opacity: 0.6, marginTop: 4}}>
|
|
402
|
+
Collides against a ground plane. Bounce is saved with the effect; Floor Y is an
|
|
403
|
+
editor preview aid (a host can register its own colliders).
|
|
404
|
+
</div>
|
|
405
|
+
</>
|
|
406
|
+
)}
|
|
407
|
+
</BehaviorModule>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Shared speed-range rows for the "by Speed" trio. */
|
|
412
|
+
function SpeedRangeRows({binding, behavior}: {binding: EffectBinding; behavior: {speedRange: IntervalValue}}) {
|
|
413
|
+
return (
|
|
414
|
+
<Row label="Speed range">
|
|
415
|
+
<div style={{display: 'flex', gap: 6}}>
|
|
416
|
+
<NumberField
|
|
417
|
+
value={behavior.speedRange.a}
|
|
418
|
+
min={0}
|
|
419
|
+
onChange={(v) => binding.apply(() => (behavior.speedRange = new IntervalValue(v, behavior.speedRange.b)))}
|
|
420
|
+
/>
|
|
421
|
+
<NumberField
|
|
422
|
+
value={behavior.speedRange.b}
|
|
423
|
+
min={0}
|
|
424
|
+
onChange={(v) => binding.apply(() => (behavior.speedRange = new IntervalValue(behavior.speedRange.a, v)))}
|
|
425
|
+
/>
|
|
426
|
+
</div>
|
|
427
|
+
</Row>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function ColorBySpeedModule({binding}: ModuleProps) {
|
|
432
|
+
return (
|
|
433
|
+
<BehaviorModule<ColorBySpeed>
|
|
434
|
+
binding={binding}
|
|
435
|
+
title="Color by Speed"
|
|
436
|
+
type="ColorBySpeed"
|
|
437
|
+
create={() => new ColorBySpeed(buildGradient(DEFAULT_GRADIENT_STOPS), new IntervalValue(0, 5))}
|
|
438
|
+
>
|
|
439
|
+
{(behavior) => (
|
|
440
|
+
<>
|
|
441
|
+
<div style={{marginTop: 6}}>
|
|
442
|
+
<GradientEditor
|
|
443
|
+
stops={behavior.color instanceof Gradient ? readGradientStops(behavior.color) : DEFAULT_GRADIENT_STOPS}
|
|
444
|
+
onChange={(stops) => binding.apply(() => (behavior.color = buildGradient(stops)))}
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
<SpeedRangeRows binding={binding} behavior={behavior} />
|
|
448
|
+
</>
|
|
449
|
+
)}
|
|
450
|
+
</BehaviorModule>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function SizeBySpeedModule({binding}: ModuleProps) {
|
|
455
|
+
return (
|
|
456
|
+
<BehaviorModule<SizeBySpeed>
|
|
457
|
+
binding={binding}
|
|
458
|
+
title="Size by Speed"
|
|
459
|
+
type="SizeBySpeed"
|
|
460
|
+
create={() => new SizeBySpeed(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [0.5, 0.8, 1.2, 1.5]}) as never, new IntervalValue(0, 5))}
|
|
461
|
+
>
|
|
462
|
+
{(behavior) => (
|
|
463
|
+
<>
|
|
464
|
+
<div style={{marginTop: 6}}>
|
|
465
|
+
<CurveEditor
|
|
466
|
+
pieces={readPieces(behavior.size as never)}
|
|
467
|
+
maxValue={2}
|
|
468
|
+
onChange={(pieces) => binding.apply(() => (behavior.size = buildCurve(pieces) as never))}
|
|
469
|
+
/>
|
|
470
|
+
</div>
|
|
471
|
+
<SpeedRangeRows binding={binding} behavior={behavior} />
|
|
472
|
+
</>
|
|
473
|
+
)}
|
|
474
|
+
</BehaviorModule>
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function RotationBySpeedModule({binding}: ModuleProps) {
|
|
479
|
+
return (
|
|
480
|
+
<BehaviorModule<RotationBySpeed>
|
|
481
|
+
binding={binding}
|
|
482
|
+
title="Rotation by Speed"
|
|
483
|
+
type="RotationBySpeed"
|
|
484
|
+
create={() => new RotationBySpeed(new ConstantValue(Math.PI), new IntervalValue(0, 5))}
|
|
485
|
+
>
|
|
486
|
+
{(behavior) => (
|
|
487
|
+
<>
|
|
488
|
+
<ValueField
|
|
489
|
+
label="Velocity (rad/s)"
|
|
490
|
+
generator={behavior.angularVelocity}
|
|
491
|
+
curveMax={Math.PI * 4}
|
|
492
|
+
onChange={(g) => binding.apply(() => (behavior.angularVelocity = g))}
|
|
493
|
+
/>
|
|
494
|
+
<SpeedRangeRows binding={binding} behavior={behavior} />
|
|
495
|
+
</>
|
|
496
|
+
)}
|
|
497
|
+
</BehaviorModule>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function OrbitOverLifeModule({binding}: ModuleProps) {
|
|
502
|
+
return (
|
|
503
|
+
<BehaviorModule<OrbitOverLife>
|
|
504
|
+
binding={binding}
|
|
505
|
+
title="Orbit over Lifetime"
|
|
506
|
+
type="OrbitOverLife"
|
|
507
|
+
create={() => new OrbitOverLife(new ConstantValue(Math.PI), new Vector3(0, 1, 0))}
|
|
508
|
+
>
|
|
509
|
+
{(behavior) => (
|
|
510
|
+
<ValueField
|
|
511
|
+
label="Orbit speed"
|
|
512
|
+
generator={behavior.orbitSpeed}
|
|
513
|
+
curveMax={Math.PI * 4}
|
|
514
|
+
onChange={(g) => binding.apply(() => (behavior.orbitSpeed = g))}
|
|
515
|
+
/>
|
|
516
|
+
)}
|
|
517
|
+
</BehaviorModule>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function WidthOverTrailModule({binding}: ModuleProps) {
|
|
522
|
+
return (
|
|
523
|
+
<BehaviorModule<WidthOverLength>
|
|
524
|
+
binding={binding}
|
|
525
|
+
title="Width over Trail"
|
|
526
|
+
type="WidthOverLength"
|
|
527
|
+
create={() => new WidthOverLength(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0]}) as never)}
|
|
528
|
+
>
|
|
529
|
+
{(behavior) => (
|
|
530
|
+
<div style={{marginTop: 6}}>
|
|
531
|
+
<CurveEditor
|
|
532
|
+
pieces={readPieces(behavior.width as never)}
|
|
533
|
+
maxValue={2}
|
|
534
|
+
onChange={(pieces) => binding.apply(() => (behavior.width = buildCurve(pieces) as never))}
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
</BehaviorModule>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function TextureSheetModule({binding}: ModuleProps) {
|
|
543
|
+
const system = binding.system;
|
|
544
|
+
const frameBehavior = findBehavior<FrameOverLife>(system.behaviors, 'FrameOverLife');
|
|
545
|
+
const tiles = Math.max(1, system.uTileCount * system.vTileCount);
|
|
546
|
+
const textureUrl = (system.texture as {url?: string} | null)?.url;
|
|
547
|
+
const u = Math.max(1, system.uTileCount);
|
|
548
|
+
const v = Math.max(1, system.vTileCount);
|
|
549
|
+
// Start tile → cell (row-major, top-left origin — matches the shader's tile UV mapping).
|
|
550
|
+
const startIdx = Math.min(Math.max(0, Math.round(readScalar(system.startTileIndex as never).value)), tiles - 1);
|
|
551
|
+
const startCol = startIdx % u;
|
|
552
|
+
const startRow = Math.floor(startIdx / u);
|
|
553
|
+
return (
|
|
554
|
+
<ModuleSection title="Texture Sheet Animation" defaultOpen={false}>
|
|
555
|
+
{textureUrl && (
|
|
556
|
+
<div style={{marginTop: 6}}>
|
|
557
|
+
<div style={{position: 'relative', width: 218, border: '1px solid #2b3761', borderRadius: 6, overflow: 'hidden'}}>
|
|
558
|
+
<img src={textureUrl} alt="texture atlas" style={{width: '100%', display: 'block'}} />
|
|
559
|
+
<div
|
|
560
|
+
style={{
|
|
561
|
+
position: 'absolute',
|
|
562
|
+
inset: 0,
|
|
563
|
+
pointerEvents: 'none',
|
|
564
|
+
backgroundImage:
|
|
565
|
+
`repeating-linear-gradient(to right, rgba(158,185,255,0.55) 0 1px, transparent 1px calc(100% / ${u})),` +
|
|
566
|
+
`repeating-linear-gradient(to bottom, rgba(158,185,255,0.55) 0 1px, transparent 1px calc(100% / ${v}))`,
|
|
567
|
+
}}
|
|
568
|
+
/>
|
|
569
|
+
{/* Highlight the start tile so it's obvious which sprite playback begins on. */}
|
|
570
|
+
<div
|
|
571
|
+
style={{
|
|
572
|
+
position: 'absolute',
|
|
573
|
+
pointerEvents: 'none',
|
|
574
|
+
left: `${(startCol * 100) / u}%`,
|
|
575
|
+
top: `${(startRow * 100) / v}%`,
|
|
576
|
+
width: `${100 / u}%`,
|
|
577
|
+
height: `${100 / v}%`,
|
|
578
|
+
boxSizing: 'border-box',
|
|
579
|
+
border: '2px solid #ffce6b',
|
|
580
|
+
background: 'rgba(255, 206, 107, 0.22)',
|
|
581
|
+
}}
|
|
582
|
+
>
|
|
583
|
+
<span
|
|
584
|
+
style={{
|
|
585
|
+
position: 'absolute',
|
|
586
|
+
top: 1,
|
|
587
|
+
left: 2,
|
|
588
|
+
fontSize: 9,
|
|
589
|
+
fontWeight: 700,
|
|
590
|
+
color: '#ffce6b',
|
|
591
|
+
textShadow: '0 0 3px #000',
|
|
592
|
+
}}
|
|
593
|
+
>
|
|
594
|
+
{startIdx}
|
|
595
|
+
</span>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
<div style={{fontSize: 11, color: '#b7c6ea', marginTop: 3}}>
|
|
599
|
+
{u} × {v} = {tiles} tiles; frame indices 0–{tiles - 1}; start tile{' '}
|
|
600
|
+
<span style={{color: '#ffce6b', fontWeight: 600}}>{startIdx}</span>
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
)}
|
|
604
|
+
<Row label="Tiles U">
|
|
605
|
+
<NumberField value={system.uTileCount} min={1} step={1} onChange={(v) => binding.apply((s) => (s.uTileCount = Math.round(v)))} />
|
|
606
|
+
</Row>
|
|
607
|
+
<Row label="Tiles V">
|
|
608
|
+
<NumberField value={system.vTileCount} min={1} step={1} onChange={(v) => binding.apply((s) => (s.vTileCount = Math.round(v)))} />
|
|
609
|
+
</Row>
|
|
610
|
+
<Row label="Blend tiles">
|
|
611
|
+
<input
|
|
612
|
+
type="checkbox"
|
|
613
|
+
className="qe-hover"
|
|
614
|
+
checked={system.blendTiles}
|
|
615
|
+
style={{justifySelf: 'start', width: 15, height: 15, accentColor: '#78a5ff', cursor: 'pointer'}}
|
|
616
|
+
onChange={(e) => binding.apply((s) => (s.blendTiles = e.target.checked))}
|
|
617
|
+
/>
|
|
618
|
+
</Row>
|
|
619
|
+
<Row label="Start tile">
|
|
620
|
+
<NumberField
|
|
621
|
+
value={readScalar(system.startTileIndex as never).value}
|
|
622
|
+
min={0}
|
|
623
|
+
step={1}
|
|
624
|
+
onChange={(v) => binding.apply((s) => (s.startTileIndex = new ConstantValue(Math.round(v))))}
|
|
625
|
+
/>
|
|
626
|
+
</Row>
|
|
627
|
+
<Row label="Animate frames">
|
|
628
|
+
<input
|
|
629
|
+
type="checkbox"
|
|
630
|
+
className="qe-hover"
|
|
631
|
+
checked={!!frameBehavior}
|
|
632
|
+
style={{justifySelf: 'start', width: 15, height: 15, accentColor: '#78a5ff', cursor: 'pointer'}}
|
|
633
|
+
onChange={(e) =>
|
|
634
|
+
binding.apply((s) => {
|
|
635
|
+
removeBehaviorOfType(s.behaviors, 'FrameOverLife');
|
|
636
|
+
if (e.target.checked) {
|
|
637
|
+
s.addBehavior(
|
|
638
|
+
new FrameOverLife(
|
|
639
|
+
buildScalar({mode: 'curve', value: 0, min: 0, max: tiles - 1, curve: [0, (tiles - 1) / 3, (2 * (tiles - 1)) / 3, tiles - 1]}) as never
|
|
640
|
+
)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
})
|
|
644
|
+
}
|
|
645
|
+
/>
|
|
646
|
+
</Row>
|
|
647
|
+
{frameBehavior && (
|
|
648
|
+
<div style={{marginTop: 6}}>
|
|
649
|
+
<CurveEditor
|
|
650
|
+
pieces={readPieces(frameBehavior.frame)}
|
|
651
|
+
maxValue={Math.max(1, tiles - 1)}
|
|
652
|
+
onChange={(pieces) => binding.apply(() => (frameBehavior.frame = buildCurve(pieces) as never))}
|
|
653
|
+
/>
|
|
654
|
+
</div>
|
|
655
|
+
)}
|
|
656
|
+
</ModuleSection>
|
|
657
|
+
);
|
|
658
|
+
}
|