babylon.quarks-editor 0.23.0 → 0.24.1

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.
@@ -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>