babylon.quarks-editor 0.23.0 → 0.24.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.
- package/CHANGELOG.md +22 -0
- package/dist/core/material.d.ts +3 -0
- package/dist/core/material.js +12 -0
- package/dist/core/shapes.d.ts +8 -2
- package/dist/core/shapes.js +25 -1
- package/dist/react/EffectEditor.js +2 -2
- package/dist/react/EffectEditorHost.js +14 -2
- package/dist/react/SubEmittersModule.js +3 -1
- package/dist/react/ValueField.d.ts +2 -5
- package/dist/react/ValueField.js +2 -1
- package/dist/react/behaviorModules.d.ts +11 -0
- package/dist/react/behaviorModules.js +81 -3
- package/dist/react/modules.js +74 -23
- package/package.json +4 -3
- package/src/core/material.ts +15 -0
- package/src/core/shapes.ts +36 -0
- package/src/react/EffectEditor.tsx +4 -0
- package/src/react/EffectEditorHost.tsx +17 -2
- package/src/react/SubEmittersModule.tsx +15 -0
- package/src/react/ValueField.tsx +6 -1
- package/src/react/behaviorModules.tsx +315 -0
- package/src/react/modules.tsx +282 -28
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import {PlusIcon, XMarkIcon} from '@heroicons/react/24/solid';
|
|
1
2
|
import type {Behavior} from 'babylon.quarks';
|
|
2
3
|
import {
|
|
3
4
|
ApplyCollision,
|
|
4
5
|
ApplyForce,
|
|
6
|
+
ApplySequences,
|
|
7
|
+
AxisAngleGenerator,
|
|
8
|
+
Bezier,
|
|
5
9
|
ChangeEmitDirection,
|
|
6
10
|
ColorBySpeed,
|
|
7
11
|
ConstantValue,
|
|
12
|
+
EulerGenerator,
|
|
8
13
|
ForceOverLife,
|
|
9
14
|
FrameOverLife,
|
|
10
15
|
getPhysicsResolver,
|
|
@@ -14,10 +19,14 @@ import {
|
|
|
14
19
|
LimitSpeedOverLife,
|
|
15
20
|
Noise,
|
|
16
21
|
OrbitOverLife,
|
|
22
|
+
PiecewiseBezier,
|
|
23
|
+
RandomQuatGenerator,
|
|
24
|
+
Rotation3DOverLife,
|
|
17
25
|
RotationBySpeed,
|
|
18
26
|
RotationOverLife,
|
|
19
27
|
SizeBySpeed,
|
|
20
28
|
SpeedOverLife,
|
|
29
|
+
TextureSequencer,
|
|
21
30
|
TurbulenceField,
|
|
22
31
|
Vector3,
|
|
23
32
|
VelocityOverLife,
|
|
@@ -30,7 +39,10 @@ import {buildCurve, buildScalar, readPieces, readScalar} from '../core/values';
|
|
|
30
39
|
import {CurveEditor} from './CurveEditor';
|
|
31
40
|
import {CheckboxField, NumberField, Row, SelectField} from './fields';
|
|
32
41
|
import {GradientEditor} from './GradientEditor';
|
|
42
|
+
import {iconStyle} from './icons';
|
|
43
|
+
import type {TextureOption} from './modules';
|
|
33
44
|
import {ModuleSection} from './ModuleSection';
|
|
45
|
+
import {theme} from './theme';
|
|
34
46
|
import {ValueField} from './ValueField';
|
|
35
47
|
|
|
36
48
|
interface ModuleProps {
|
|
@@ -227,6 +239,17 @@ export function VelocityOverLifeModule({binding}: ModuleProps) {
|
|
|
227
239
|
curveMax={Math.PI * 4}
|
|
228
240
|
onChange={(g) => binding.apply(() => (behavior.orbitalZ = g))}
|
|
229
241
|
/>
|
|
242
|
+
<Vector3Row
|
|
243
|
+
label="Orbital offset"
|
|
244
|
+
value={behavior.orbitalOffset}
|
|
245
|
+
onChange={(v) => binding.apply(() => (behavior.orbitalOffset = v))}
|
|
246
|
+
/>
|
|
247
|
+
<ValueField
|
|
248
|
+
label="Radial"
|
|
249
|
+
generator={behavior.radial}
|
|
250
|
+
curveMax={10}
|
|
251
|
+
onChange={(g) => binding.apply(() => (behavior.radial = g))}
|
|
252
|
+
/>
|
|
230
253
|
</>
|
|
231
254
|
)}
|
|
232
255
|
</BehaviorModule>
|
|
@@ -307,6 +330,271 @@ export function RotationOverLifeModule({binding}: ModuleProps) {
|
|
|
307
330
|
);
|
|
308
331
|
}
|
|
309
332
|
|
|
333
|
+
/** Turns mesh particles by a 3D angular velocity; billboards turn through Rotation over Lifetime. */
|
|
334
|
+
export function Rotation3DOverLifeModule({binding}: ModuleProps) {
|
|
335
|
+
return (
|
|
336
|
+
<BehaviorModule<Rotation3DOverLife>
|
|
337
|
+
binding={binding}
|
|
338
|
+
title="Rotation over Lifetime (3D)"
|
|
339
|
+
type="Rotation3DOverLife"
|
|
340
|
+
create={() =>
|
|
341
|
+
new Rotation3DOverLife(new AxisAngleGenerator(new Vector3(0, 1, 0), new ConstantValue(Math.PI)))
|
|
342
|
+
}
|
|
343
|
+
>
|
|
344
|
+
{(behavior) => {
|
|
345
|
+
const generator = behavior.angularVelocity;
|
|
346
|
+
const mode =
|
|
347
|
+
generator instanceof AxisAngleGenerator
|
|
348
|
+
? 'axisAngle'
|
|
349
|
+
: generator instanceof EulerGenerator
|
|
350
|
+
? 'euler'
|
|
351
|
+
: 'random';
|
|
352
|
+
const axisAngle = generator instanceof AxisAngleGenerator ? generator : undefined;
|
|
353
|
+
const euler = generator instanceof EulerGenerator ? generator : undefined;
|
|
354
|
+
return (
|
|
355
|
+
<>
|
|
356
|
+
<p style={{margin: '2px 0 6px', color: theme.textDim, fontSize: 11.5}}>
|
|
357
|
+
Mesh particles only; billboards spin with Rotation over Lifetime.
|
|
358
|
+
</p>
|
|
359
|
+
<Row label="Mode">
|
|
360
|
+
<SelectField
|
|
361
|
+
value={mode}
|
|
362
|
+
options={[
|
|
363
|
+
{value: 'axisAngle', label: 'Axis + angle'},
|
|
364
|
+
{value: 'euler', label: 'Per axis'},
|
|
365
|
+
{value: 'random', label: 'Random'},
|
|
366
|
+
]}
|
|
367
|
+
onChange={(next) =>
|
|
368
|
+
binding.apply(() => {
|
|
369
|
+
behavior.angularVelocity =
|
|
370
|
+
next === 'axisAngle'
|
|
371
|
+
? new AxisAngleGenerator(
|
|
372
|
+
new Vector3(0, 1, 0),
|
|
373
|
+
new ConstantValue(Math.PI)
|
|
374
|
+
)
|
|
375
|
+
: next === 'euler'
|
|
376
|
+
? new EulerGenerator(
|
|
377
|
+
new ConstantValue(0),
|
|
378
|
+
new ConstantValue(Math.PI),
|
|
379
|
+
new ConstantValue(0)
|
|
380
|
+
)
|
|
381
|
+
: new RandomQuatGenerator();
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
/>
|
|
385
|
+
</Row>
|
|
386
|
+
{axisAngle && (
|
|
387
|
+
<>
|
|
388
|
+
<Vector3Row
|
|
389
|
+
label="Axis"
|
|
390
|
+
value={axisAngle.axis}
|
|
391
|
+
step={0.1}
|
|
392
|
+
onChange={(v) =>
|
|
393
|
+
binding.apply(
|
|
394
|
+
() =>
|
|
395
|
+
(behavior.angularVelocity = new AxisAngleGenerator(v, axisAngle.angle))
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
/>
|
|
399
|
+
<ValueField
|
|
400
|
+
label="Angle (rad/s)"
|
|
401
|
+
generator={axisAngle.angle}
|
|
402
|
+
curveMax={Math.PI * 4}
|
|
403
|
+
onChange={(g) =>
|
|
404
|
+
binding.apply(
|
|
405
|
+
() =>
|
|
406
|
+
(behavior.angularVelocity = new AxisAngleGenerator(
|
|
407
|
+
axisAngle.axis.clone(),
|
|
408
|
+
g
|
|
409
|
+
))
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
/>
|
|
413
|
+
</>
|
|
414
|
+
)}
|
|
415
|
+
{euler &&
|
|
416
|
+
(['angleX', 'angleY', 'angleZ'] as const).map((key) => (
|
|
417
|
+
<ValueField
|
|
418
|
+
key={key}
|
|
419
|
+
label={`${key.slice(-1)} (rad/s)`}
|
|
420
|
+
generator={euler[key]}
|
|
421
|
+
curveMax={Math.PI * 4}
|
|
422
|
+
onChange={(g) =>
|
|
423
|
+
binding.apply(
|
|
424
|
+
() =>
|
|
425
|
+
(behavior.angularVelocity = new EulerGenerator(
|
|
426
|
+
key === 'angleX' ? g : euler.angleX,
|
|
427
|
+
key === 'angleY' ? g : euler.angleY,
|
|
428
|
+
key === 'angleZ' ? g : euler.angleZ
|
|
429
|
+
))
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
/>
|
|
433
|
+
))}
|
|
434
|
+
</>
|
|
435
|
+
);
|
|
436
|
+
}}
|
|
437
|
+
</BehaviorModule>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Moves particles, one after another, onto the opaque pixels of an image (a
|
|
443
|
+
* logo or a word drawn from sparks). Each sequence is read from an image once;
|
|
444
|
+
* its points, scale, position and time window are what gets saved.
|
|
445
|
+
*/
|
|
446
|
+
export function SequencesModule({binding, textureOptions}: ModuleProps & {textureOptions?: TextureOption[]}) {
|
|
447
|
+
const [imageUrl, setImageUrl] = React.useState(textureOptions?.[0]?.url ?? '');
|
|
448
|
+
const [threshold, setThreshold] = React.useState(0.5);
|
|
449
|
+
const [status, setStatus] = React.useState<string | null>(null);
|
|
450
|
+
const addFromImage = async (behavior: ApplySequences) => {
|
|
451
|
+
if (!imageUrl) {
|
|
452
|
+
setStatus('Pick an image first.');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const image = new Image();
|
|
457
|
+
image.crossOrigin = 'anonymous';
|
|
458
|
+
image.src = imageUrl;
|
|
459
|
+
await image.decode();
|
|
460
|
+
const sequencer = new TextureSequencer(0.05, 0.05, new Vector3());
|
|
461
|
+
sequencer.fromImage(image, threshold);
|
|
462
|
+
const last = behavior.sequencers[behavior.sequencers.length - 1];
|
|
463
|
+
const start = last ? last[0].b : 0;
|
|
464
|
+
binding.apply(() => behavior.appendSequencer(new IntervalValue(start, start + 2), sequencer));
|
|
465
|
+
setStatus(`${sequencer.locations.length} points read.`);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
setStatus(`Could not read the image: ${error instanceof Error ? error.message : String(error)}`);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const pair = (a: number, b: number, onA: (v: number) => void, onB: (v: number) => void, step: number) => (
|
|
471
|
+
<div style={{display: 'flex', gap: 6}}>
|
|
472
|
+
<NumberField value={a} step={step} onChange={onA} />
|
|
473
|
+
<NumberField value={b} step={step} onChange={onB} />
|
|
474
|
+
</div>
|
|
475
|
+
);
|
|
476
|
+
return (
|
|
477
|
+
<BehaviorModule<ApplySequences>
|
|
478
|
+
binding={binding}
|
|
479
|
+
title="Sequences"
|
|
480
|
+
type="ApplySequences"
|
|
481
|
+
create={() => new ApplySequences(0.001)}
|
|
482
|
+
>
|
|
483
|
+
{(behavior) => (
|
|
484
|
+
<>
|
|
485
|
+
<p style={{margin: '2px 0 6px', color: theme.textDim, fontSize: 11.5}}>
|
|
486
|
+
Particles fly, one after another, onto the opaque pixels of an image during each sequence's time
|
|
487
|
+
window.
|
|
488
|
+
</p>
|
|
489
|
+
<Row label="Delay per particle">
|
|
490
|
+
<NumberField
|
|
491
|
+
value={behavior.delay}
|
|
492
|
+
min={0}
|
|
493
|
+
step={0.001}
|
|
494
|
+
onChange={(v) => binding.apply(() => (behavior.delay = Math.max(0, v)))}
|
|
495
|
+
/>
|
|
496
|
+
</Row>
|
|
497
|
+
{behavior.sequencers.map(([range, sequencer], index) => {
|
|
498
|
+
const texture = sequencer as TextureSequencer;
|
|
499
|
+
return (
|
|
500
|
+
<div
|
|
501
|
+
key={index}
|
|
502
|
+
style={{
|
|
503
|
+
border: `1px solid ${theme.border}`,
|
|
504
|
+
borderRadius: 6,
|
|
505
|
+
padding: '4px 6px',
|
|
506
|
+
marginTop: 6,
|
|
507
|
+
}}
|
|
508
|
+
>
|
|
509
|
+
<Row label={`Sequence ${index + 1}`}>
|
|
510
|
+
<span style={{fontSize: 11.5, color: theme.textDim}}>
|
|
511
|
+
{texture.locations?.length ?? 0} points
|
|
512
|
+
</span>
|
|
513
|
+
<button
|
|
514
|
+
className="qe-hover"
|
|
515
|
+
style={{
|
|
516
|
+
marginLeft: 'auto',
|
|
517
|
+
background: 'none',
|
|
518
|
+
border: 'none',
|
|
519
|
+
cursor: 'pointer',
|
|
520
|
+
}}
|
|
521
|
+
title="Remove sequence"
|
|
522
|
+
onClick={() => binding.apply(() => behavior.sequencers.splice(index, 1))}
|
|
523
|
+
>
|
|
524
|
+
<XMarkIcon style={iconStyle(13)} />
|
|
525
|
+
</button>
|
|
526
|
+
</Row>
|
|
527
|
+
<Row label="Time from / to (s)">
|
|
528
|
+
{pair(
|
|
529
|
+
range.a,
|
|
530
|
+
range.b,
|
|
531
|
+
(v) => binding.apply(() => (range.a = v)),
|
|
532
|
+
(v) => binding.apply(() => (range.b = v)),
|
|
533
|
+
0.1
|
|
534
|
+
)}
|
|
535
|
+
</Row>
|
|
536
|
+
{texture.locations !== undefined && (
|
|
537
|
+
<>
|
|
538
|
+
<Row label="Scale X / Y">
|
|
539
|
+
{pair(
|
|
540
|
+
texture.scaleX,
|
|
541
|
+
texture.scaleY,
|
|
542
|
+
(v) => binding.apply(() => (texture.scaleX = v)),
|
|
543
|
+
(v) => binding.apply(() => (texture.scaleY = v)),
|
|
544
|
+
0.01
|
|
545
|
+
)}
|
|
546
|
+
</Row>
|
|
547
|
+
<Vector3Row
|
|
548
|
+
label="Position"
|
|
549
|
+
value={texture.position}
|
|
550
|
+
step={0.1}
|
|
551
|
+
onChange={(v) => binding.apply(() => texture.position.copy(v))}
|
|
552
|
+
/>
|
|
553
|
+
</>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
})}
|
|
558
|
+
<Row label="Image">
|
|
559
|
+
{textureOptions && textureOptions.length > 0 ? (
|
|
560
|
+
<SelectField
|
|
561
|
+
value={imageUrl}
|
|
562
|
+
options={textureOptions.map((option) => ({value: option.url, label: option.label}))}
|
|
563
|
+
onChange={(url) => setImageUrl(url)}
|
|
564
|
+
/>
|
|
565
|
+
) : (
|
|
566
|
+
<input
|
|
567
|
+
className="qe-hover"
|
|
568
|
+
style={{flex: 1, minWidth: 0, fontSize: 12}}
|
|
569
|
+
placeholder="Image URL"
|
|
570
|
+
value={imageUrl}
|
|
571
|
+
onChange={(event) => setImageUrl(event.target.value)}
|
|
572
|
+
/>
|
|
573
|
+
)}
|
|
574
|
+
</Row>
|
|
575
|
+
<Row label="Alpha threshold">
|
|
576
|
+
<NumberField
|
|
577
|
+
value={threshold}
|
|
578
|
+
min={0}
|
|
579
|
+
step={0.05}
|
|
580
|
+
onChange={(v) => setThreshold(Math.min(1, v))}
|
|
581
|
+
/>
|
|
582
|
+
</Row>
|
|
583
|
+
<button
|
|
584
|
+
className="qe-hover"
|
|
585
|
+
style={{display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, fontSize: 12}}
|
|
586
|
+
onClick={() => void addFromImage(behavior)}
|
|
587
|
+
>
|
|
588
|
+
<PlusIcon style={iconStyle(13)} />
|
|
589
|
+
Add sequence from image
|
|
590
|
+
</button>
|
|
591
|
+
{status && <p style={{margin: '4px 0 0', color: theme.textDim, fontSize: 11.5}}>{status}</p>}
|
|
592
|
+
</>
|
|
593
|
+
)}
|
|
594
|
+
</BehaviorModule>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
310
598
|
export function NoiseModule({binding}: ModuleProps) {
|
|
311
599
|
return (
|
|
312
600
|
<BehaviorModule<Noise>
|
|
@@ -349,6 +637,33 @@ export function NoiseModule({binding}: ModuleProps) {
|
|
|
349
637
|
curveMax={Math.PI}
|
|
350
638
|
onChange={(g) => binding.apply(() => (behavior.rotationAmount = g))}
|
|
351
639
|
/>
|
|
640
|
+
<ValueField
|
|
641
|
+
label="Scroll speed"
|
|
642
|
+
generator={behavior.scrollSpeed}
|
|
643
|
+
curveMax={5}
|
|
644
|
+
onChange={(g) => binding.apply(() => (behavior.scrollSpeed = g))}
|
|
645
|
+
/>
|
|
646
|
+
<Row label="Remap">
|
|
647
|
+
<CheckboxField
|
|
648
|
+
value={behavior.remap !== undefined}
|
|
649
|
+
onChange={(v) =>
|
|
650
|
+
binding.apply(
|
|
651
|
+
() =>
|
|
652
|
+
(behavior.remap = v
|
|
653
|
+
? new PiecewiseBezier([[new Bezier(-1, -1 / 3, 1 / 3, 1), 0]])
|
|
654
|
+
: undefined)
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
/>
|
|
658
|
+
</Row>
|
|
659
|
+
{behavior.remap !== undefined && (
|
|
660
|
+
<ValueField
|
|
661
|
+
label="Remap curve"
|
|
662
|
+
generator={behavior.remap}
|
|
663
|
+
curveMax={1}
|
|
664
|
+
onChange={(g) => binding.apply(() => (behavior.remap = g as PiecewiseBezier))}
|
|
665
|
+
/>
|
|
666
|
+
)}
|
|
352
667
|
</>
|
|
353
668
|
)}
|
|
354
669
|
</BehaviorModule>
|