@ng-native/fabric 0.7.0 → 0.8.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/dist/engine.js CHANGED
@@ -10,12 +10,12 @@
10
10
  import { boundDeclaration, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, placeReadElsewhere, siblingReach, } from "./css.js";
11
11
  import { applyAria } from "./aria-props.js";
12
12
  import { STYLED_ELEMENTS } from "./element-styles.js";
13
- import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, steppedKeys, tracksOf, } from "./transition.js";
13
+ import { aimsElsewhere, animationEvent, bezier, interpolate, readsInheritedColour, sample, step, steppedKeys, tracksOf, } from "./transition.js";
14
14
  import { isFormsInput } from "./forms-inputs.js";
15
15
  import { transformList } from "./inline-transform.js";
16
16
  import { tokenFromValue } from "./inline-token.js";
17
17
  import { NativeScrollDriver, scrollFeed, } from "./native-drive.js";
18
- import { clockChannels, firstFrame, movesByShare, rangeOf, scrollChannels, sharesAsPoints, } from "./scroll-animation.js";
18
+ import { clockChannels, colourChannels, firstFrame, movesByShare, rangeOf, scrollChannels, sharesAsPoints, } from "./scroll-animation.js";
19
19
  import { FontFaces } from "./font-faces.js";
20
20
  import { premultipliedStops } from "./premultiplied-stops.js";
21
21
  /**
@@ -394,6 +394,24 @@ const PARAGRAPH = 'Paragraph';
394
394
  * that is laid out in its flow. Words written straight into the view are the paragraph the
395
395
  * engine makes around them, and a child that is not displayed has no view to ask.
396
396
  */
397
+ /**
398
+ * A view with a transition is a native view for as long as it has one, as an `Animated.View` is.
399
+ *
400
+ * React Native gives a view a native view of its own for an opacity under 1, a transform or a
401
+ * background, and takes it away when the last such reason goes. A wrapper that fades in was one
402
+ * for the length of the fade and none after, and Fabric moved everything inside it to the view
403
+ * above as the fade ended. Being moved takes a focused field out of the window for a moment, and
404
+ * it lost its focus: a field focused as its container faded in blurred when the fade finished.
405
+ *
406
+ * A floor, as `keepNative` is: a `collapsable` written on the element is left alone. Not for
407
+ * `transition: none`, which transitions nothing.
408
+ */
409
+ function keepForTransition(node, props, spec) {
410
+ if (viewNameOf(node) !== DEFAULT_VIEW || props['collapsable'] !== undefined)
411
+ return;
412
+ if (Object.keys(spec).length > 0)
413
+ props['collapsable'] = false;
414
+ }
397
415
  function firstInFlow(node) {
398
416
  for (const child of node.children ?? []) {
399
417
  // Not the paragraph itself, which holds the words it was made around.
@@ -576,6 +594,44 @@ function inlineOf(node) {
576
594
  delete inline[key];
577
595
  return inline;
578
596
  }
597
+ /**
598
+ * What a node's `@keyframes` animation sets this frame, less each property a rule it matches
599
+ * declared `!important`: an important author declaration is above the animation in the cascade.
600
+ * A transition is above both, so `transitioned` takes no such care.
601
+ */
602
+ function playedOver(node) {
603
+ const values = node.playing?.values;
604
+ const important = node.styleCache?.important;
605
+ if (!values || !important)
606
+ return values ?? NO_STYLE;
607
+ const played = { ...values };
608
+ for (const key of important)
609
+ delete played[key];
610
+ return played;
611
+ }
612
+ /**
613
+ * The properties among an animation's tracks that a rule `node` matches declared `!important`,
614
+ * which native is not handed: what native plays it sets on the view, over what the rule says.
615
+ */
616
+ function pinnedIn(node, tracks) {
617
+ const important = node.styleCache?.important;
618
+ return important ? [...tracks.keys()].filter((key) => important.has(key)) : [];
619
+ }
620
+ /** Whether native is playing an animation whose important properties are no longer the ones left out. */
621
+ function pinsChanged(node, running) {
622
+ if (!running.native)
623
+ return false;
624
+ return pinnedIn(node, running.tracks).join(' ') !== running.pinned?.join(' ');
625
+ }
626
+ /** `tracks` less the properties named. */
627
+ function unpinned(tracks, pinned) {
628
+ if (!pinned.length)
629
+ return tracks;
630
+ const kept = new Map(tracks);
631
+ for (const key of pinned)
632
+ kept.delete(key);
633
+ return kept;
634
+ }
579
635
  /**
580
636
  * What a container's own style says for `key`, in the order `mergeProps` applies them: a
581
637
  * component's override, inline, a prop, its stylesheet, its default.
@@ -1319,6 +1375,69 @@ function repaint(node) {
1319
1375
  }
1320
1376
  /** A sixtieth of a second: how far apart the frames native is given for an animation are. */
1321
1377
  const NATIVE_FRAME = 1000 / 60;
1378
+ /**
1379
+ * What native eases a set of transitions on one clock by: a colour by its own channels, opacity
1380
+ * and a transform as an animation of them is. Nothing where any of them is no value native can
1381
+ * ease, which leaves them all to JavaScript.
1382
+ */
1383
+ function easedChannels(tracks, spec, props) {
1384
+ const tinted = new Map([...tracks].filter(([key]) => EASED_COLOURS.has(key)));
1385
+ const moved = new Map([...tracks].filter(([key]) => !EASED_COLOURS.has(key)));
1386
+ const colors = colourChannels(tinted, spec);
1387
+ const { channels, held } = clockChannels(moved, spec, props);
1388
+ if (!colors || held.length)
1389
+ return null;
1390
+ const any = channels.opacity || channels.transform.length || tinted.size;
1391
+ return any ? { ...channels, colors } : null;
1392
+ }
1393
+ /** The sides a length is written for, and the two it is written for at once. */
1394
+ const EDGES = ['', 'Top', 'Right', 'Bottom', 'Left', 'Horizontal', 'Vertical'];
1395
+ /**
1396
+ * The properties of a transition that resize or move a box, which native eases by laying the
1397
+ * view out where it ends: see `layEased`.
1398
+ */
1399
+ const LAID_OUT = new Set([
1400
+ ...['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'],
1401
+ ...['top', 'right', 'bottom', 'left', 'flexBasis'],
1402
+ ...EDGES.map((edge) => `margin${edge}`),
1403
+ ]);
1404
+ /** Every prop that can resize or move a box, a view's own or another's. */
1405
+ const MOVES_BOXES = new Set([
1406
+ ...LAID_OUT,
1407
+ ...EDGES.map((edge) => `padding${edge}`),
1408
+ ...['borderWidth', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
1409
+ ...['flex', 'flexGrow', 'flexShrink', 'flexDirection', 'flexWrap', 'alignItems', 'alignSelf'],
1410
+ ...['alignContent', 'justifyContent', 'gap', 'rowGap', 'columnGap', 'display', 'position'],
1411
+ ...['aspectRatio', 'fontSize', 'lineHeight', 'fontFamily', 'fontWeight', 'letterSpacing'],
1412
+ ...['numberOfLines', 'start', 'end'],
1413
+ ]);
1414
+ /** The curves a layout animation has, as the timing function each is. */
1415
+ const LAYOUT_CURVES = [
1416
+ ['linear', [0, 0, 1, 1]],
1417
+ ['easeIn', [0.42, 0, 1, 1]],
1418
+ ['easeOut', [0, 0, 0.58, 1]],
1419
+ ['easeInEaseOut', [0.42, 0, 0.58, 1]],
1420
+ ];
1421
+ /** The curve of a layout animation nearest a timing function, by where each is along the way. */
1422
+ function layoutCurve(easing) {
1423
+ const along = [0.2, 0.4, 0.6, 0.8];
1424
+ const apart = (curve) => along.reduce((sum, at) => sum + (bezier(curve, at) - bezier(easing, at)) ** 2, 0);
1425
+ let nearest = LAYOUT_CURVES[0];
1426
+ for (const curve of LAYOUT_CURVES)
1427
+ if (apart(curve[1]) < apart(nearest[1]))
1428
+ nearest = curve;
1429
+ return nearest[0];
1430
+ }
1431
+ /** The properties of a transition native can play: see `easeNatively`. */
1432
+ const EASED_COLOURS = new Set([
1433
+ 'backgroundColor',
1434
+ 'borderColor',
1435
+ 'borderTopColor',
1436
+ 'borderRightColor',
1437
+ 'borderBottomColor',
1438
+ 'borderLeftColor',
1439
+ ]);
1440
+ const NATIVELY_EASED = new Set(['opacity', 'transform', ...EASED_COLOURS]);
1322
1441
  /** The style keys that are a view's background. */
1323
1442
  const PAINT_KEYS = [
1324
1443
  'backgroundColor',
@@ -2579,6 +2698,7 @@ export class Engine {
2579
2698
  // as. Native gives a view one parent for its life, and aborts on a second.
2580
2699
  if (node.committed)
2581
2700
  this.forgetCommitted(node);
2701
+ this.stopEased(node);
2582
2702
  node.transitions = undefined;
2583
2703
  if (node.playing)
2584
2704
  this.stopNative(node, node.playing);
@@ -2624,7 +2744,10 @@ export class Engine {
2624
2744
  // inline `direction` is the exception, because the paragraphs below it align by it.
2625
2745
  // And a child that takes one of this node's own values, an `inherit`, follows it.
2626
2746
  const heirs = node.styleCache?.heirs === true;
2627
- this.markProps(node, key !== 'style' || this.inlineReachesStyle(node) || heirs);
2747
+ // And only a prop some selector names: `aria-valuenow` on every tick of a progress bar, a
2748
+ // field's text on every key, match nothing they did not.
2749
+ const read = key === 'style' ? this.inlineReachesStyle(node) : this.styles.reads(key);
2750
+ this.markProps(node, read || heirs);
2628
2751
  }
2629
2752
  /**
2630
2753
  * Paint a node's background on one of its children instead of on the node: the node keeps its
@@ -2934,11 +3057,15 @@ export class Engine {
2934
3057
  // Faces registered before the walk, the global sheet's among them, reach every node in it.
2935
3058
  this.facesAdded = false;
2936
3059
  const set = this.fabric.createChildSet(this.rootTag);
2937
- for (const child of this.visibleChildren(this.root)) {
2938
- if (this.withheld(child))
2939
- continue;
2940
- this.fabric.appendChildToSet(set, this.reconcileUnder(this.root, child));
3060
+ let handles = this.rootHandles();
3061
+ // Again, where transitions it laid out where they end are to be eased from JavaScript.
3062
+ if (this.layOut()) {
3063
+ handles = this.rootHandles();
3064
+ // What that found resized is the transitions themselves, at the frame they are eased to.
3065
+ this.movesOthers = false;
2941
3066
  }
3067
+ for (const handle of handles)
3068
+ this.fabric.appendChildToSet(set, handle);
2942
3069
  this.clearFlags(this.root);
2943
3070
  this.announceCommit();
2944
3071
  this.fabric.completeRoot(this.rootTag, set);
@@ -3425,7 +3552,7 @@ export class Engine {
3425
3552
  if (spec.timeline)
3426
3553
  return this.scrollAnimated(node, spec, frames, props);
3427
3554
  this.startPlaying(node, spec, frames, props);
3428
- return Object.assign(props, node.playing?.values ?? {});
3555
+ return Object.assign(props, playedOver(node));
3429
3556
  }
3430
3557
  /**
3431
3558
  * A clock animation the scroll plays instead: the same animation where only the timeline moved,
@@ -3451,7 +3578,7 @@ export class Engine {
3451
3578
  if (running && !running.done)
3452
3579
  this.emitTransition(node, 'topAnimationcancel', running.spec.name);
3453
3580
  }
3454
- /** Stop on native each animation whose view has left the tree. */
3581
+ /** Stop on native each animation and transition whose view has left the tree. */
3455
3582
  releasePlayedNatively() {
3456
3583
  for (const node of [...this.playedNatively]) {
3457
3584
  if (this.topOf(node) === this.root)
@@ -3460,6 +3587,10 @@ export class Engine {
3460
3587
  node.playing = undefined;
3461
3588
  this.markProps(node, false);
3462
3589
  }
3590
+ for (const node of [...this.easedNatively]) {
3591
+ if (this.topOf(node) !== this.root)
3592
+ this.stopEased(node);
3593
+ }
3463
3594
  }
3464
3595
  /** Every node whose animation native is playing, with no frame of it in JavaScript. */
3465
3596
  playedNatively = new Set();
@@ -3531,7 +3662,9 @@ export class Engine {
3531
3662
  const sized = this.scrollDriver ? this.sized(node, running, props) : null;
3532
3663
  if (!this.scrollDriver || !sized)
3533
3664
  return false;
3534
- const { channels, held, span } = clockChannels(sized.tracks, spec, sized.resting);
3665
+ const pinned = pinnedIn(node, sized.tracks);
3666
+ const tracks = unpinned(sized.tracks, pinned);
3667
+ const { channels, held, span } = clockChannels(tracks, spec, sized.resting);
3535
3668
  if (held.length || !(channels.opacity || channels.transform.length))
3536
3669
  return false;
3537
3670
  // There and back is one native animation: an odd number of ways has no whole number of them.
@@ -3544,6 +3677,7 @@ export class Engine {
3544
3677
  if (!native)
3545
3678
  return false;
3546
3679
  running.native = native;
3680
+ running.pinned = pinned;
3547
3681
  // As Animated does: Fabric flattens a view that only lays out, and then there is no native
3548
3682
  // view for the animation to move.
3549
3683
  running.values = { ...running.values, collapsable: false };
@@ -3620,8 +3754,9 @@ export class Engine {
3620
3754
  this.emitTransition(node, 'topAnimationstart', spec.name);
3621
3755
  return;
3622
3756
  }
3623
- // A view made again is another view: the one native was moving is gone.
3624
- if (node.committed === null)
3757
+ // A view made again is another view: the one native was moving is gone. And one whose
3758
+ // important declarations changed is moving what it now must not, or not what it now may.
3759
+ if (node.committed === null || pinsChanged(node, current))
3625
3760
  this.backToScript(node, current);
3626
3761
  if (this.playedFrames.get(current) !== frames || inherited !== current.inherited) {
3627
3762
  this.backToScript(node, current);
@@ -3691,25 +3826,28 @@ export class Engine {
3691
3826
  scrollAnimated(node, spec, frames, props) {
3692
3827
  const current = node.scrolled;
3693
3828
  const inherited = this.inheritedColour(node, frames);
3694
- if (current?.frames === frames && sameAnimation(current.spec, spec)) {
3829
+ const same = current?.frames === frames && sameAnimation(current.spec, spec);
3830
+ if (same && pinnedIn(node, current.tracks).join(' ') === current.pinned.join(' ')) {
3695
3831
  return Object.assign(props, this.rescrolled(node, current, frames, props, inherited));
3696
3832
  }
3697
3833
  this.stopScrolled(node);
3698
3834
  this.handOverToScroll(node, spec);
3699
3835
  const tracks = tracksOf(frames, props, inherited);
3836
+ const pinned = pinnedIn(node, tracks);
3837
+ const played = unpinned(tracks, pinned);
3700
3838
  const source = this.scrollSourceOf(node);
3701
3839
  const extent = source ? (this.scrollExtents.get(source)?.[spec.timeline] ?? null) : null;
3702
3840
  const range = rangeOf(spec, extent);
3703
- const first = firstFrame(tracks, spec, range);
3841
+ const first = firstFrame(played, spec, range);
3704
3842
  const resting = { ...props };
3705
3843
  if (!source && this.dev)
3706
3844
  this.reportUnscrolled(spec.name);
3707
- const drive = source ? this.driveScrolled(node, source, spec, tracks, resting, range) : null;
3845
+ const drive = source ? this.driveScrolled(node, source, spec, played, resting, range) : null;
3708
3846
  // As Animated does: Fabric flattens a view that only lays out, and then there is no native
3709
3847
  // view for the animation to move. Committed with the first frame, so it stays put.
3710
3848
  if (drive)
3711
3849
  first['collapsable'] = false;
3712
- node.scrolled = { spec, frames, tracks, resting, first, source, drive, inherited };
3850
+ node.scrolled = { spec, frames, tracks, pinned, resting, first, source, drive, inherited };
3713
3851
  return Object.assign(props, first);
3714
3852
  }
3715
3853
  /**
@@ -3786,7 +3924,7 @@ export class Engine {
3786
3924
  if (!scrolled?.drive)
3787
3925
  continue;
3788
3926
  const range = rangeOf(scrolled.spec, extent[scrolled.spec.timeline]);
3789
- scrolled.drive.update(scrollChannels(scrolled.tracks, scrolled.spec, scrolled.resting, range).channels);
3927
+ scrolled.drive.update(scrollChannels(unpinned(scrolled.tracks, scrolled.pinned), scrolled.spec, scrolled.resting, range).channels);
3790
3928
  }
3791
3929
  }
3792
3930
  reportUnscrolled(name) {
@@ -3812,24 +3950,290 @@ export class Engine {
3812
3950
  `has loaded, so the block has to exist somewhere by the time the animation starts.`);
3813
3951
  }
3814
3952
  transitioned(node, props) {
3815
- // Nobody saw what the last commit gave a view it created, so this is where the view starts.
3816
- if (node.bornIn >= this.unseen)
3817
- node.transitions = undefined;
3953
+ this.unseenTransitions(node);
3818
3954
  const spec = transitionSpec(props);
3819
3955
  if (!spec && !node.transitions)
3820
3956
  return props;
3957
+ if (spec)
3958
+ keepForTransition(node, props, spec);
3821
3959
  const state = (node.transitions ??= new Map());
3822
3960
  const now = this.now();
3961
+ this.takeBackEased(node, state, props, spec, now);
3962
+ this.takeBackLaid(state, props, spec, now);
3963
+ const started = [];
3823
3964
  for (const key of steppedKeys(props, spec, state)) {
3824
3965
  if (step(state, key, props, spec?.[key] ?? spec?.['all'], now)) {
3825
3966
  this.running.add(node);
3967
+ started.push(key);
3826
3968
  this.emitTransition(node, 'topTransitionstart', key);
3827
3969
  }
3828
3970
  }
3971
+ this.easeNatively(node, state, started, props, now);
3829
3972
  if (state.size === 0)
3830
3973
  node.transitions = undefined;
3831
3974
  return props;
3832
3975
  }
3976
+ /** Nobody saw what the last commit gave a view it created, so this is where the view starts. */
3977
+ unseenTransitions(node) {
3978
+ if (node.bornIn < this.unseen)
3979
+ return;
3980
+ this.stopEased(node);
3981
+ node.transitions = undefined;
3982
+ }
3983
+ /** The elements native is playing a transition of. */
3984
+ easedNatively = new Set();
3985
+ /**
3986
+ * Hand to native each transition that just started and that it can play: opacity and
3987
+ * transforms, with no wait before they start. Those that start together, take as long and ease
3988
+ * alike share a clock. Nothing of them runs in JavaScript until native says they have ended:
3989
+ * a press that fades or slides something is a commit to start it and a commit to end it.
3990
+ *
3991
+ * ponytail: a colour is eased from JavaScript still, a commit a frame, and so is anything
3992
+ * that waits. Native has a colour node for the first; give the driver a channel for it.
3993
+ */
3994
+ easeNatively(node, state, started, props, now) {
3995
+ if (started.length)
3996
+ this.startEased(node, state, started, props, now);
3997
+ this.layEased(node, state, started, props, now);
3998
+ const inScript = [...state.values()].some((eased) => !eased.done && !eased.native && !eased.laid);
3999
+ if (!inScript)
4000
+ this.running.delete(node);
4001
+ // As Animated does: Fabric flattens a view that only lays out, and then there is no native
4002
+ // view for a transition to move.
4003
+ if (this.easedNatively.has(node))
4004
+ props['collapsable'] = false;
4005
+ }
4006
+ /** The root's children as the views they are committed as, each reconciled. */
4007
+ rootHandles() {
4008
+ const handles = [];
4009
+ for (const child of this.visibleChildren(this.root)) {
4010
+ if (!this.withheld(child))
4011
+ handles.push(this.reconcileUnder(this.root, child));
4012
+ }
4013
+ return handles;
4014
+ }
4015
+ /** The transitions this commit lays out where they end, for native to move the views to. */
4016
+ laying = [];
4017
+ /** Whether this commit resizes or moves a box that is in no such transition. */
4018
+ movesOthers = false;
4019
+ /**
4020
+ * Commit where it ends each transition of a size or a place that just started, for native to
4021
+ * move the view there: a layout animation, which the commit asks for once it is known that
4022
+ * nothing else in it moves. One still on its way is committed where it ends too.
4023
+ *
4024
+ * A width eased from JavaScript is a commit a frame, and a commit lays the whole screen out.
4025
+ */
4026
+ layEased(node, state, started, props, now) {
4027
+ if (!this.fabric.configureNextLayoutAnimation)
4028
+ return;
4029
+ for (const key of started) {
4030
+ const eased = state.get(key);
4031
+ if (!LAID_OUT.has(key) || eased.start > now || eased.duration <= 0)
4032
+ continue;
4033
+ eased.laid = true;
4034
+ this.laying.push({ node, key, eased });
4035
+ }
4036
+ for (const [key, eased] of state)
4037
+ if (eased.laid && !eased.done)
4038
+ props[key] = eased.to;
4039
+ }
4040
+ /**
4041
+ * Before a commit is handed over: ask native to move what it lays out, or take the
4042
+ * transitions back to ease them from JavaScript where the commit moves anything else, and
4043
+ * where they would not all take as long. Answers whether the tree has to be reconciled again.
4044
+ */
4045
+ layOut() {
4046
+ const laying = this.laying;
4047
+ const others = this.movesOthers;
4048
+ this.laying = [];
4049
+ this.movesOthers = false;
4050
+ if (!laying.length)
4051
+ return false;
4052
+ const [first] = laying;
4053
+ const alike = laying.every(({ eased }) => eased.duration === first.eased.duration);
4054
+ if (others || !alike) {
4055
+ for (const { node, eased } of laying) {
4056
+ eased.laid = undefined;
4057
+ this.running.add(node);
4058
+ }
4059
+ for (const { node } of laying)
4060
+ this.markProps(node, false);
4061
+ return true;
4062
+ }
4063
+ const { duration, easing } = first.eased;
4064
+ const nothing = () => undefined;
4065
+ this.fabric.configureNextLayoutAnimation({ duration, update: { type: layoutCurve(easing) } }, nothing, nothing);
4066
+ for (const { node, key, eased } of laying) {
4067
+ setTimeout(() => this.endLaid(node, key, eased), duration);
4068
+ }
4069
+ return false;
4070
+ }
4071
+ /**
4072
+ * Note a commit that resizes or moves a box in no transition native lays out: a prop of one
4073
+ * that moves boxes, children that came or went, text longer or shorter than it was.
4074
+ */
4075
+ noteMoved(node, payload, resized, words) {
4076
+ if (this.movesOthers)
4077
+ return;
4078
+ if (resized)
4079
+ this.movesOthers = true;
4080
+ for (const key in payload) {
4081
+ const eased = node.transitions?.get(key);
4082
+ if (eased?.laid && !eased.done)
4083
+ continue;
4084
+ // Text as long as it was is taken to be as wide, as a grid takes it: see `shapeOf`.
4085
+ const longer = key === 'text' && String(payload[key]).length !== String(words).length;
4086
+ if (MOVES_BOXES.has(key) || longer)
4087
+ this.movesOthers = true;
4088
+ }
4089
+ }
4090
+ /** A transition native laid out has had its time: it is where it ends, and says so. */
4091
+ endLaid(node, key, eased) {
4092
+ if (!eased.laid || eased.done || node.transitions?.get(key) !== eased)
4093
+ return;
4094
+ Object.assign(eased, { current: eased.to, done: true, laid: undefined });
4095
+ this.emitTransition(node, 'topTransitionend', key);
4096
+ // No commit comes of it, which is what tells listeners otherwise: it is where it ends.
4097
+ this.flushTransitionEvents();
4098
+ // And what a listener did, taking away an element that has left, is committed.
4099
+ if (this.pending)
4100
+ this.commit();
4101
+ }
4102
+ /**
4103
+ * Bring a transition native laid out to where its clock has got to, where it is sent another
4104
+ * way or its rule has gone: that is where it goes on from.
4105
+ */
4106
+ takeBackLaid(state, props, spec, now) {
4107
+ for (const [key, eased] of state) {
4108
+ if (!eased.laid || eased.done)
4109
+ continue;
4110
+ const kept = spec?.[key] ?? spec?.['all'];
4111
+ if (kept && !aimsElsewhere(eased, key, props))
4112
+ continue;
4113
+ const progress = Math.min(1, Math.max(0, (now - eased.start) / eased.duration));
4114
+ eased.current = interpolate(eased.from, eased.to, bezier(eased.easing, progress));
4115
+ eased.laid = undefined;
4116
+ }
4117
+ }
4118
+ startEased(node, state, started, props, now) {
4119
+ if (!this.scrollDriver)
4120
+ return;
4121
+ const clocks = new Map();
4122
+ for (const key of started) {
4123
+ const eased = state.get(key);
4124
+ if (!NATIVELY_EASED.has(key) || eased.start > now || eased.duration <= 0)
4125
+ continue;
4126
+ const clock = `${eased.duration} ${eased.easing.join()}`;
4127
+ clocks.set(clock, [...(clocks.get(clock) ?? []), key]);
4128
+ }
4129
+ for (const keys of clocks.values())
4130
+ this.playEased(node, state, keys, props);
4131
+ }
4132
+ playEased(node, state, keys, props) {
4133
+ const first = state.get(keys[0]);
4134
+ const tracks = new Map(keys.map((key) => {
4135
+ const { current, to, easing } = state.get(key);
4136
+ return [
4137
+ key,
4138
+ [
4139
+ { offset: 0, value: current, easing },
4140
+ { offset: 1, value: to },
4141
+ ],
4142
+ ];
4143
+ }));
4144
+ const spec = {
4145
+ name: '',
4146
+ duration: first.duration,
4147
+ delay: 0,
4148
+ easing: first.easing,
4149
+ iterations: 1,
4150
+ fill: 'both',
4151
+ };
4152
+ const channels = easedChannels(tracks, spec, props);
4153
+ if (!channels)
4154
+ return;
4155
+ const length = Math.max(2, Math.round(first.duration / NATIVE_FRAME) + 1);
4156
+ const frames = Array.from({ length }, (_, i) => i / (length - 1));
4157
+ const clock = { keys, stop: () => undefined };
4158
+ const native = this.scrollDriver.play(node, channels, { frames, toValue: 1, iterations: 1 }, (view) => this.tagOf(view), () => this.endedEased(node, clock));
4159
+ if (!native)
4160
+ return;
4161
+ clock.stop = () => native.stop();
4162
+ for (const key of keys)
4163
+ state.get(key).native = clock;
4164
+ this.easedNatively.add(node);
4165
+ }
4166
+ /**
4167
+ * Native reached the end of the transitions on one clock. Where they end is committed before
4168
+ * the view is let go, so nothing shows of where they started, which is what it was committed
4169
+ * with.
4170
+ */
4171
+ endedEased(node, clock) {
4172
+ const state = node.transitions;
4173
+ const keys = clock.keys.filter((key) => state?.get(key)?.native === clock);
4174
+ for (const key of keys) {
4175
+ const eased = state.get(key);
4176
+ Object.assign(eased, { current: eased.to, done: true, native: undefined });
4177
+ this.emitTransition(node, 'topTransitionend', key);
4178
+ this.markProps(node, false);
4179
+ }
4180
+ this.forgetEased(node);
4181
+ if (keys.length)
4182
+ this.commit();
4183
+ clock.stop();
4184
+ }
4185
+ /** No longer one native is easing, once none of its transitions is native's. */
4186
+ forgetEased(node) {
4187
+ for (const eased of node.transitions?.values() ?? [])
4188
+ if (eased.native)
4189
+ return;
4190
+ this.easedNatively.delete(node);
4191
+ }
4192
+ /**
4193
+ * Take back from native each transition it can no longer play as it was started: one sent
4194
+ * another way, one whose rule has gone, any of a view that was made again. Each is brought to
4195
+ * where its clock has got to, which is where it goes on from, here or on native again.
4196
+ */
4197
+ takeBackEased(node, state, props, spec, now) {
4198
+ if (!this.easedNatively.has(node))
4199
+ return;
4200
+ const remade = node.committed === null;
4201
+ for (const [key, eased] of state) {
4202
+ if (!eased.native)
4203
+ continue;
4204
+ const kept = spec?.[key] ?? spec?.['all'];
4205
+ if (remade || !kept || aimsElsewhere(eased, key, props)) {
4206
+ this.backToScriptEased(node, eased.native, now);
4207
+ }
4208
+ }
4209
+ }
4210
+ /** Stop a clock native runs, and go on from JavaScript with each transition it was playing. */
4211
+ backToScriptEased(node, clock, now) {
4212
+ for (const key of clock.keys) {
4213
+ const eased = node.transitions?.get(key);
4214
+ if (eased?.native !== clock)
4215
+ continue;
4216
+ eased.native = undefined;
4217
+ const progress = Math.min(1, Math.max(0, (now - eased.start) / eased.duration));
4218
+ eased.current = interpolate(eased.from, eased.to, bezier(eased.easing, progress));
4219
+ this.running.add(node);
4220
+ }
4221
+ clock.stop();
4222
+ this.forgetEased(node);
4223
+ }
4224
+ /** Let go of what native is easing on an element that is going, or gone. */
4225
+ stopEased(node) {
4226
+ if (!this.easedNatively.delete(node))
4227
+ return;
4228
+ const clocks = new Set();
4229
+ for (const eased of node.transitions?.values() ?? []) {
4230
+ if (eased.native)
4231
+ clocks.add(eased.native);
4232
+ eased.native = undefined;
4233
+ }
4234
+ for (const clock of clocks)
4235
+ clock.stop();
4236
+ }
3833
4237
  /**
3834
4238
  * Whether an animation that has finished goes on holding its last frame. With a forwards fill
3835
4239
  * it does, for good: without one the element falls back to whatever the cascade gives it, and
@@ -4101,6 +4505,7 @@ export class Engine {
4101
4505
  const props = propsMoved ? this.mergeProps(node, viewName) : previous.props;
4102
4506
  const propsPayload = propsMoved ? this.propsPayload(node, previous.props, props) : null;
4103
4507
  const childrenChanged = this.childrenMoved(node, childHandles, previous.childHandles);
4508
+ this.noteMoved(node, propsPayload, childHandles.length !== previous.childHandles.length, previous.props['text']);
4104
4509
  let handle = previous.handle;
4105
4510
  if (childrenChanged || propsPayload) {
4106
4511
  handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload), childrenChanged ? childHandles : null);