@ng-native/fabric 0.7.0 → 0.9.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
  /**
@@ -28,7 +28,7 @@ function animationOf(props) {
28
28
  let spec = props['$animation'];
29
29
  if (spec !== undefined)
30
30
  delete props['$animation'];
31
- for (const [key, field] of Object.entries(ANIMATION_PARTS)) {
31
+ for (const [key, field] of ANIMATION_ENTRIES) {
32
32
  if (!(key in props))
33
33
  continue;
34
34
  const value = props[key];
@@ -47,6 +47,8 @@ const ANIMATION_PARTS = {
47
47
  $animationFill: 'fill',
48
48
  $playState: 'paused',
49
49
  };
50
+ /** Listed once: `animationOf` reads them for every node that commits. */
51
+ const ANIMATION_ENTRIES = Object.entries(ANIMATION_PARTS);
50
52
  function animationPart(field, value) {
51
53
  switch (field) {
52
54
  case 'iterations':
@@ -394,6 +396,24 @@ const PARAGRAPH = 'Paragraph';
394
396
  * that is laid out in its flow. Words written straight into the view are the paragraph the
395
397
  * engine makes around them, and a child that is not displayed has no view to ask.
396
398
  */
399
+ /**
400
+ * A view with a transition is a native view for as long as it has one, as an `Animated.View` is.
401
+ *
402
+ * React Native gives a view a native view of its own for an opacity under 1, a transform or a
403
+ * background, and takes it away when the last such reason goes. A wrapper that fades in was one
404
+ * for the length of the fade and none after, and Fabric moved everything inside it to the view
405
+ * above as the fade ended. Being moved takes a focused field out of the window for a moment, and
406
+ * it lost its focus: a field focused as its container faded in blurred when the fade finished.
407
+ *
408
+ * A floor, as `keepNative` is: a `collapsable` written on the element is left alone. Not for
409
+ * `transition: none`, which transitions nothing.
410
+ */
411
+ function keepForTransition(node, props, spec) {
412
+ if (viewNameOf(node) !== DEFAULT_VIEW || props['collapsable'] !== undefined)
413
+ return;
414
+ if (Object.keys(spec).length > 0)
415
+ props['collapsable'] = false;
416
+ }
397
417
  function firstInFlow(node) {
398
418
  for (const child of node.children ?? []) {
399
419
  // Not the paragraph itself, which holds the words it was made around.
@@ -519,8 +539,8 @@ function lineBaseline(props, line, [, top, , bottom], fontScale) {
519
539
  * out on both platforms: on Android, `EditText` centres a line box taller than the font's own
520
540
  * about 1.7pt high of centre in a 44pt field, and centres the font's own exactly. A percentage
521
541
  * height sizes the field only where its parent's height is definite, which is known at layout
522
- * alone, so such a field is left as it was. Elsewhere Android sizes the field by the line height
523
- * and centres the text in it itself, and keeps it.
542
+ * alone, so on Android such a field is left as it was. Elsewhere Android sizes the field by the
543
+ * line height and centres the text in it itself, and keeps it.
524
544
  *
525
545
  * On iOS, React Native's field sets `lineHeight` as the paragraph's minimum and maximum line
526
546
  * height and, unlike a paragraph (`RCTApplyBaselineOffset`), never offsets the baseline, so the
@@ -543,10 +563,9 @@ function centreSingleLine(viewName, props, fontScale, onBaseline = false) {
543
563
  heightBaseline(props, height, onBaseline, fontScale);
544
564
  return;
545
565
  }
546
- // A percentage is a fixed height only where the parent's is definite, known at layout alone.
547
- if (isSet(height) && height !== 'auto')
548
- return;
549
- // Android sizes the field by its line height, and centres the text in it, itself.
566
+ // Android sizes the field by its line height, and centres the text in it, itself. A
567
+ // percentage is a fixed height only where the parent's is definite, known at layout alone:
568
+ // there too the line height is left out on iOS, and kept as the least the field is tall.
550
569
  if (viewName !== 'TextInput')
551
570
  return;
552
571
  const edge = (side, logical) => blockEdge(props, side, logical);
@@ -576,6 +595,44 @@ function inlineOf(node) {
576
595
  delete inline[key];
577
596
  return inline;
578
597
  }
598
+ /**
599
+ * What a node's `@keyframes` animation sets this frame, less each property a rule it matches
600
+ * declared `!important`: an important author declaration is above the animation in the cascade.
601
+ * A transition is above both, so `transitioned` takes no such care.
602
+ */
603
+ function playedOver(node) {
604
+ const values = node.playing?.values;
605
+ const important = node.styleCache?.important;
606
+ if (!values || !important)
607
+ return values ?? NO_STYLE;
608
+ const played = { ...values };
609
+ for (const key of important)
610
+ delete played[key];
611
+ return played;
612
+ }
613
+ /**
614
+ * The properties among an animation's tracks that a rule `node` matches declared `!important`,
615
+ * which native is not handed: what native plays it sets on the view, over what the rule says.
616
+ */
617
+ function pinnedIn(node, tracks) {
618
+ const important = node.styleCache?.important;
619
+ return important ? [...tracks.keys()].filter((key) => important.has(key)) : [];
620
+ }
621
+ /** Whether native is playing an animation whose important properties are no longer the ones left out. */
622
+ function pinsChanged(node, running) {
623
+ if (!running.native)
624
+ return false;
625
+ return pinnedIn(node, running.tracks).join(' ') !== running.pinned?.join(' ');
626
+ }
627
+ /** `tracks` less the properties named. */
628
+ function unpinned(tracks, pinned) {
629
+ if (!pinned.length)
630
+ return tracks;
631
+ const kept = new Map(tracks);
632
+ for (const key of pinned)
633
+ kept.delete(key);
634
+ return kept;
635
+ }
579
636
  /**
580
637
  * What a container's own style says for `key`, in the order `mergeProps` applies them: a
581
638
  * component's override, inline, a prop, its stylesheet, its default.
@@ -791,10 +848,11 @@ const TOUCHED_ALPHA = 0.011;
791
848
  function stillTouched(props) {
792
849
  // One told to take no touch is not to be sent one, seen or not.
793
850
  if (props['pointerEvents'] === 'none')
794
- return;
851
+ return props;
795
852
  const opacity = props['opacity'];
796
853
  if (typeof opacity === 'number' && opacity < TOUCHED_ALPHA)
797
854
  props['opacity'] = TOUCHED_ALPHA;
855
+ return props;
798
856
  }
799
857
  /**
800
858
  * Leave `aspect-ratio` out for a box with both a width and a height. CSS takes the ratio only
@@ -1319,6 +1377,86 @@ function repaint(node) {
1319
1377
  }
1320
1378
  /** A sixtieth of a second: how far apart the frames native is given for an animation are. */
1321
1379
  const NATIVE_FRAME = 1000 / 60;
1380
+ /**
1381
+ * What native eases a set of transitions on one clock by: a colour by its own channels, opacity
1382
+ * and a transform as an animation of them is. Nothing where any of them is no value native can
1383
+ * ease, which leaves them all to JavaScript.
1384
+ */
1385
+ function easedChannels(tracks, spec, props) {
1386
+ const tinted = new Map([...tracks].filter(([key]) => EASED_COLOURS.has(key)));
1387
+ const moved = new Map([...tracks].filter(([key]) => !EASED_COLOURS.has(key)));
1388
+ const colors = colourChannels(tinted, spec);
1389
+ const { channels, held } = clockChannels(moved, spec, props);
1390
+ if (!colors || held.length)
1391
+ return null;
1392
+ const any = channels.opacity || channels.transform.length || tinted.size;
1393
+ return any ? { ...channels, colors } : null;
1394
+ }
1395
+ /** The sides a length is written for, and the two it is written for at once. */
1396
+ const EDGES = ['', 'Top', 'Right', 'Bottom', 'Left', 'Horizontal', 'Vertical'];
1397
+ /**
1398
+ * The properties of a transition that resize or move a box, which native eases by laying the
1399
+ * view out where it ends: see `layEased`.
1400
+ */
1401
+ const LAID_OUT = new Set([
1402
+ ...['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'],
1403
+ ...['top', 'right', 'bottom', 'left', 'flexBasis'],
1404
+ ...EDGES.map((edge) => `margin${edge}`),
1405
+ ]);
1406
+ /** Every prop that can resize or move a box, a view's own or another's. */
1407
+ const MOVES_BOXES = new Set([
1408
+ ...LAID_OUT,
1409
+ ...EDGES.map((edge) => `padding${edge}`),
1410
+ ...['borderWidth', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
1411
+ ...['flex', 'flexGrow', 'flexShrink', 'flexDirection', 'flexWrap', 'alignItems', 'alignSelf'],
1412
+ ...['alignContent', 'justifyContent', 'gap', 'rowGap', 'columnGap', 'display', 'position'],
1413
+ ...['aspectRatio', 'fontSize', 'lineHeight', 'fontFamily', 'fontWeight', 'letterSpacing'],
1414
+ ...['numberOfLines', 'start', 'end'],
1415
+ ]);
1416
+ /** Whether native is moving a view to a prop's value, by a transition or an animation it laid out. */
1417
+ function laidByNative(node, key) {
1418
+ const eased = node.transitions?.get(key);
1419
+ if (eased?.laid && !eased.done)
1420
+ return true;
1421
+ return node.playing?.laid === true && node.playing.tracks.has(key);
1422
+ }
1423
+ /** Whether an animation plays once through, forwards, with no wait before it. */
1424
+ const playedOnce = (spec) => spec.delay === 0 && spec.duration > 0 && spec.iterations === 1 && !spec.direction;
1425
+ /** Whether a track is two frames on one curve, the first of them where the view is drawn. */
1426
+ function fromWhereDrawn(track, drawn) {
1427
+ const [first, last] = track;
1428
+ if (track.length !== 2 || first.easing || last.value == null)
1429
+ return false;
1430
+ // One that ends where it starts moves nothing: there is nothing for native to lay out.
1431
+ return Object.is(first.value, drawn) && !Object.is(first.value, last.value);
1432
+ }
1433
+ /** The curves a layout animation has, as the timing function each is. */
1434
+ const LAYOUT_CURVES = [
1435
+ ['linear', [0, 0, 1, 1]],
1436
+ ['easeIn', [0.42, 0, 1, 1]],
1437
+ ['easeOut', [0, 0, 0.58, 1]],
1438
+ ['easeInEaseOut', [0.42, 0, 0.58, 1]],
1439
+ ];
1440
+ /** The curve of a layout animation nearest a timing function, by where each is along the way. */
1441
+ function layoutCurve(easing) {
1442
+ const along = [0.2, 0.4, 0.6, 0.8];
1443
+ const apart = (curve) => along.reduce((sum, at) => sum + (bezier(curve, at) - bezier(easing, at)) ** 2, 0);
1444
+ let nearest = LAYOUT_CURVES[0];
1445
+ for (const curve of LAYOUT_CURVES)
1446
+ if (apart(curve[1]) < apart(nearest[1]))
1447
+ nearest = curve;
1448
+ return nearest[0];
1449
+ }
1450
+ /** The properties of a transition native can play: see `easeNatively`. */
1451
+ const EASED_COLOURS = new Set([
1452
+ 'backgroundColor',
1453
+ 'borderColor',
1454
+ 'borderTopColor',
1455
+ 'borderRightColor',
1456
+ 'borderBottomColor',
1457
+ 'borderLeftColor',
1458
+ ]);
1459
+ const NATIVELY_EASED = new Set(['opacity', 'transform', ...EASED_COLOURS]);
1322
1460
  /** The style keys that are a view's background. */
1323
1461
  const PAINT_KEYS = [
1324
1462
  'backgroundColor',
@@ -1526,6 +1664,7 @@ class RetainedNode {
1526
1664
  customProperties = null;
1527
1665
  styleCache = null;
1528
1666
  styleDirty = true;
1667
+ ownDirty = false;
1529
1668
  styleCommitted = null;
1530
1669
  fitContainer = undefined;
1531
1670
  heightBasis = undefined;
@@ -1742,6 +1881,8 @@ const TRANSITION_TIMING = {
1742
1881
  $transitionDelay: 'delay',
1743
1882
  $transitionEasing: 'easing',
1744
1883
  };
1884
+ /** Listed once, as `ANIMATION_ENTRIES` is. */
1885
+ const TRANSITION_ENTRIES = Object.entries(TRANSITION_TIMING);
1745
1886
  /**
1746
1887
  * The transition spec, with any timing set by a rule of its own laid over it: `.duration-700`
1747
1888
  * beside `.transition`. A rule that writes the timing itself compiles it to `null`, which is how
@@ -1755,7 +1896,7 @@ function transitionSpec(props) {
1755
1896
  let spec = props['$transition'];
1756
1897
  if (spec !== undefined)
1757
1898
  delete props['$transition'];
1758
- for (const [key, part] of Object.entries(TRANSITION_TIMING)) {
1899
+ for (const [key, part] of TRANSITION_ENTRIES) {
1759
1900
  if (!(key in props))
1760
1901
  continue;
1761
1902
  const value = props[key];
@@ -1874,6 +2015,14 @@ const now = () => globalThis.performance?.now?.() ?? Date.now();
1874
2015
  function sameHandles(a, b) {
1875
2016
  return a.length === b.length && a.every((handle, i) => handle === b[i]);
1876
2017
  }
2018
+ /** Whether two sets of the same classes hold them in the same order. */
2019
+ function inOrder(a, b) {
2020
+ const other = b.values();
2021
+ for (const name of a)
2022
+ if (other.next().value !== name)
2023
+ return false;
2024
+ return true;
2025
+ }
1877
2026
  /**
1878
2027
  * Whether a node could be the element a compound is written for, by its classes alone: its own,
1879
2028
  * and those of one alternative of each `:is()` on it, which is where Tailwind names a group or
@@ -2376,34 +2525,46 @@ export class Engine {
2376
2525
  * Mark a node whose classes changed, by what the rules that name those classes can restyle.
2377
2526
  *
2378
2527
  * A class no selector names changes no style: a library's own marker, of an overlay that is
2379
- * animating or a control that was touched. One named only as what a rule's element is
2380
- * inside, `.animating .close`, restyles the elements under the node that such rules are for.
2381
- * Either way a whole subtree is not styled again for a class that has no say in it. Any other
2382
- * class restyles the node and everything under it, as any other change to it does.
2528
+ * animating or a control that was touched. One a rule is for the element of, `.on`, has that
2529
+ * element matched again, and one named as what a rule's element is inside, `.animating .close`
2530
+ * or `.close:is(.animating *)`, the elements under the node that such rules are for. What is
2531
+ * under any of those is styled again from the rules it matched, for what it inherits: a `dark`
2532
+ * class on the root finds no rule again for an element no `dark` rule is for. Any other class
2533
+ * has the node and everything under it matched again, as any other change to it does.
2383
2534
  *
2384
2535
  * A sheet that names a class later restyles what it is for as it arrives, and a node not yet
2385
2536
  * committed is styled when it is, with the class as it is then.
2386
2537
  */
2387
2538
  markClasses(node, changed) {
2539
+ // An element still being built, in no tree and never styled: nothing is styled by it yet,
2540
+ // and it is styled when it is committed, with the classes it has then. Every element of a
2541
+ // screen is given its classes this way, so what they reach is not asked for each.
2542
+ if (node.parent === null && node.styleCache === null)
2543
+ return this.markProps(node, false);
2388
2544
  const reach = this.styles.reachOf(changed);
2389
2545
  if (reach === true)
2390
2546
  return this.markProps(node);
2391
2547
  this.markProps(node, false);
2392
- if (reach)
2548
+ if (!reach)
2549
+ return;
2550
+ if (reach.own)
2551
+ node.ownDirty = true;
2552
+ if (this.styles.reachesUnder(reach))
2393
2553
  this.markUnder(node, reach);
2394
2554
  }
2395
- /** Have styled again each element under a node that one of `subjects`' rules could be for. */
2555
+ /**
2556
+ * Have matched again each element under a node that one of `subjects`' rules could be for. One
2557
+ * under another is marked too: what is under an element marked this way keeps its rules.
2558
+ */
2396
2559
  markUnder(node, subjects) {
2397
2560
  for (const child of node.children) {
2398
2561
  if (child.kind !== 'element')
2399
2562
  continue;
2400
2563
  if (this.styles.isFor(subjects, child)) {
2401
- // And with it all under it, which is styled from it.
2402
- child.styleDirty = true;
2564
+ child.ownDirty = true;
2403
2565
  this.markPath(child);
2404
2566
  }
2405
- else
2406
- this.markUnder(child, subjects);
2567
+ this.markUnder(child, subjects);
2407
2568
  }
2408
2569
  }
2409
2570
  /**
@@ -2429,7 +2590,10 @@ export class Engine {
2429
2590
  node.defaultStyle = style;
2430
2591
  this.markProps(node);
2431
2592
  }
2432
- /** `class="a b"` from a template. Replaces the set rather than adding to it. */
2593
+ /**
2594
+ * `class="a b"` from a template. Replaces the set rather than adding to it. The same classes in
2595
+ * another order are a change too, to a selector that reads the class attribute, `[class^="a"]`.
2596
+ */
2433
2597
  setClasses(node, value) {
2434
2598
  const names = value.split(/\s+/).filter(Boolean);
2435
2599
  const before = node.classes;
@@ -2439,7 +2603,8 @@ export class Engine {
2439
2603
  for (const name of before ?? [])
2440
2604
  if (!node.classes?.has(name))
2441
2605
  changed.push(name);
2442
- this.markClasses(node, changed);
2606
+ const reordered = !changed.length && !!before && !!node.classes && !inOrder(before, node.classes);
2607
+ this.markClasses(node, reordered ? node.classes : changed);
2443
2608
  }
2444
2609
  /** Angular's `@if`/`@for`/`ViewContainerRef` markers. Ordered, never committed. */
2445
2610
  createAnchor() {
@@ -2451,6 +2616,7 @@ export class Engine {
2451
2616
  return node;
2452
2617
  }
2453
2618
  appendChild(parent, child) {
2619
+ this.markAdopted(child, parent);
2454
2620
  this.detach(child);
2455
2621
  child.parent = parent;
2456
2622
  parent.children.push(child);
@@ -2460,6 +2626,7 @@ export class Engine {
2460
2626
  this.markTextContent(parent, child);
2461
2627
  }
2462
2628
  insertBefore(parent, child, ref) {
2629
+ this.markAdopted(child, parent);
2463
2630
  this.detach(child);
2464
2631
  child.parent = parent;
2465
2632
  const at = ref ? parent.children.indexOf(ref) : -1;
@@ -2472,6 +2639,16 @@ export class Engine {
2472
2639
  this.markStructure(parent, child, at < 0 ? parent.children.length - 1 : at);
2473
2640
  this.markTextContent(parent, child);
2474
2641
  }
2642
+ /**
2643
+ * A node coming under a parent other than the one it is under, or under any once it has been
2644
+ * taken out: what is over it may be other than it was styled under. The resolver sees that for
2645
+ * itself by the parent's cache, except between two boxes that share one, where it is told here.
2646
+ * Moved within its own parent, a row of a list reordered, it is styled as it was.
2647
+ */
2648
+ markAdopted(child, parent) {
2649
+ if (child.parent !== parent && this.styles.styledUnderShared(child))
2650
+ child.styleDirty = true;
2651
+ }
2475
2652
  removeChild(parent, child) {
2476
2653
  const target = parent ?? child.parent;
2477
2654
  if (!target)
@@ -2579,6 +2756,7 @@ export class Engine {
2579
2756
  // as. Native gives a view one parent for its life, and aborts on a second.
2580
2757
  if (node.committed)
2581
2758
  this.forgetCommitted(node);
2759
+ this.stopEased(node);
2582
2760
  node.transitions = undefined;
2583
2761
  if (node.playing)
2584
2762
  this.stopNative(node, node.playing);
@@ -2624,7 +2802,10 @@ export class Engine {
2624
2802
  // inline `direction` is the exception, because the paragraphs below it align by it.
2625
2803
  // And a child that takes one of this node's own values, an `inherit`, follows it.
2626
2804
  const heirs = node.styleCache?.heirs === true;
2627
- this.markProps(node, key !== 'style' || this.inlineReachesStyle(node) || heirs);
2805
+ // And only a prop some selector names: `aria-valuenow` on every tick of a progress bar, a
2806
+ // field's text on every key, match nothing they did not.
2807
+ const read = key === 'style' ? this.inlineReachesStyle(node) : this.styles.reads(key);
2808
+ this.markProps(node, read || heirs);
2628
2809
  }
2629
2810
  /**
2630
2811
  * Paint a node's background on one of its children instead of on the node: the node keeps its
@@ -2719,7 +2900,10 @@ export class Engine {
2719
2900
  else {
2720
2901
  node.customProperties = { ...current, [name]: token };
2721
2902
  }
2722
- this.markProps(node);
2903
+ // In scope for the node's own rules and for what is under it, and read by no selector: the
2904
+ // node is styled again, and those under it from the rules they matched.
2905
+ this.markProps(node, false);
2906
+ node.ownDirty = true;
2723
2907
  }
2724
2908
  setText(node, value) {
2725
2909
  const filled = (node.text === '') !== (value === '');
@@ -2934,11 +3118,15 @@ export class Engine {
2934
3118
  // Faces registered before the walk, the global sheet's among them, reach every node in it.
2935
3119
  this.facesAdded = false;
2936
3120
  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));
3121
+ let handles = this.rootHandles();
3122
+ // Again, where transitions it laid out where they end are to be eased from JavaScript.
3123
+ if (this.layOut()) {
3124
+ handles = this.rootHandles();
3125
+ // What that found resized is the transitions themselves, at the frame they are eased to.
3126
+ this.movesOthers = false;
2941
3127
  }
3128
+ for (const handle of handles)
3129
+ this.fabric.appendChildToSet(set, handle);
2942
3130
  this.clearFlags(this.root);
2943
3131
  this.announceCommit();
2944
3132
  this.fabric.completeRoot(this.rootTag, set);
@@ -3411,7 +3599,8 @@ export class Engine {
3411
3599
  }
3412
3600
  return props;
3413
3601
  }
3414
- const frames = this.keyframes.get(spec.name);
3602
+ const written = this.keyframesOf(node, spec.name);
3603
+ const frames = written && this.framesFor(node, written);
3415
3604
  if (!frames) {
3416
3605
  // Keyframes a hot swap deleted, from under an animation that was playing them, or ones a
3417
3606
  // sheet later in this commit has: see `settleKeyframes`.
@@ -3425,7 +3614,50 @@ export class Engine {
3425
3614
  if (spec.timeline)
3426
3615
  return this.scrollAnimated(node, spec, frames, props);
3427
3616
  this.startPlaying(node, spec, frames, props);
3428
- return Object.assign(props, node.playing?.values ?? {});
3617
+ return Object.assign(props, playedOver(node));
3618
+ }
3619
+ /**
3620
+ * The keyframes a node's animation names: those of its own component's sheet where it has
3621
+ * some by that name, as Angular scopes a component's keyframes in a browser, and otherwise
3622
+ * whichever sheet has them, since any other is global.
3623
+ *
3624
+ * ponytail: a host is asked of the sheet it is written in before its own component's, so
3625
+ * a `:host` animation is played by its parent's frames where both have the name.
3626
+ */
3627
+ keyframesOf(node, name) {
3628
+ const own = node.sheet?.keyframes?.[name] ?? node.hostSheet?.keyframes?.[name];
3629
+ return own ?? this.keyframes.get(name);
3630
+ }
3631
+ /** Each node's frames as they settle on it, kept for as long as they settle the same. */
3632
+ settledFrames = new WeakMap();
3633
+ /**
3634
+ * An animation's frames for the node playing it: those with a value that waits for the node,
3635
+ * a `var()` it sets, settled against it. The same frames are answered while they settle the
3636
+ * same, and others once a variable changes, which is what an animation under way is played
3637
+ * on from: see `reframe`.
3638
+ */
3639
+ framesFor(node, frames) {
3640
+ if (!frames.some((frame) => frame.deferred))
3641
+ return frames;
3642
+ const kept = this.settledFrames.get(node);
3643
+ // Styled as it was, it settles as it did: a commit a frame asks, and most change nothing.
3644
+ const styled = node.styleCache;
3645
+ if (kept?.of === frames && kept.styled === styled)
3646
+ return kept.frames;
3647
+ const settled = frames.map((frame) => frame.deferred && this.styles.settleFor(node, frame.deferred, this.styleEpoch));
3648
+ const key = JSON.stringify(settled);
3649
+ if (kept?.of === frames && kept.key === key) {
3650
+ kept.styled = styled;
3651
+ return kept.frames;
3652
+ }
3653
+ const made = frames.map((frame, at) => {
3654
+ const values = Object.entries(settled[at] ?? {}).filter(([, value]) => value !== undefined);
3655
+ return values.length
3656
+ ? { ...frame, declarations: { ...frame.declarations, ...Object.fromEntries(values) } }
3657
+ : frame;
3658
+ });
3659
+ this.settledFrames.set(node, { of: frames, styled, key, frames: made });
3660
+ return made;
3429
3661
  }
3430
3662
  /**
3431
3663
  * A clock animation the scroll plays instead: the same animation where only the timeline moved,
@@ -3451,7 +3683,7 @@ export class Engine {
3451
3683
  if (running && !running.done)
3452
3684
  this.emitTransition(node, 'topAnimationcancel', running.spec.name);
3453
3685
  }
3454
- /** Stop on native each animation whose view has left the tree. */
3686
+ /** Stop on native each animation and transition whose view has left the tree. */
3455
3687
  releasePlayedNatively() {
3456
3688
  for (const node of [...this.playedNatively]) {
3457
3689
  if (this.topOf(node) === this.root)
@@ -3460,6 +3692,10 @@ export class Engine {
3460
3692
  node.playing = undefined;
3461
3693
  this.markProps(node, false);
3462
3694
  }
3695
+ for (const node of [...this.easedNatively]) {
3696
+ if (this.topOf(node) !== this.root)
3697
+ this.stopEased(node);
3698
+ }
3463
3699
  }
3464
3700
  /** Every node whose animation native is playing, with no frame of it in JavaScript. */
3465
3701
  playedNatively = new Set();
@@ -3531,7 +3767,9 @@ export class Engine {
3531
3767
  const sized = this.scrollDriver ? this.sized(node, running, props) : null;
3532
3768
  if (!this.scrollDriver || !sized)
3533
3769
  return false;
3534
- const { channels, held, span } = clockChannels(sized.tracks, spec, sized.resting);
3770
+ const pinned = pinnedIn(node, sized.tracks);
3771
+ const tracks = unpinned(sized.tracks, pinned);
3772
+ const { channels, held, span } = clockChannels(tracks, spec, sized.resting);
3535
3773
  if (held.length || !(channels.opacity || channels.transform.length))
3536
3774
  return false;
3537
3775
  // There and back is one native animation: an odd number of ways has no whole number of them.
@@ -3544,6 +3782,7 @@ export class Engine {
3544
3782
  if (!native)
3545
3783
  return false;
3546
3784
  running.native = native;
3785
+ running.pinned = pinned;
3547
3786
  // As Animated does: Fabric flattens a view that only lays out, and then there is no native
3548
3787
  // view for the animation to move.
3549
3788
  running.values = { ...running.values, collapsable: false };
@@ -3615,13 +3854,14 @@ export class Engine {
3615
3854
  node.playing = started;
3616
3855
  if (spec.paused)
3617
3856
  started.pausedAt = this.now();
3618
- else if (!this.playNatively(node, started, props))
3619
- this.playing.add(node);
3857
+ else
3858
+ this.playSomewhere(node, started, props);
3620
3859
  this.emitTransition(node, 'topAnimationstart', spec.name);
3621
3860
  return;
3622
3861
  }
3623
- // A view made again is another view: the one native was moving is gone.
3624
- if (node.committed === null)
3862
+ // A view made again is another view: the one native was moving is gone. And one whose
3863
+ // important declarations changed is moving what it now must not, or not what it now may.
3864
+ if (node.committed === null || pinsChanged(node, current))
3625
3865
  this.backToScript(node, current);
3626
3866
  if (this.playedFrames.get(current) !== frames || inherited !== current.inherited) {
3627
3867
  this.backToScript(node, current);
@@ -3629,6 +3869,13 @@ export class Engine {
3629
3869
  }
3630
3870
  this.playOrPause(node, node.playing, spec);
3631
3871
  }
3872
+ /** Play an animation that has just started: by native's layout, by native's driver, or here. */
3873
+ playSomewhere(node, started, props) {
3874
+ if (this.layPlayed(node, started))
3875
+ this.laidPlayed.push({ node, running: started });
3876
+ else if (!this.playNatively(node, started, props))
3877
+ this.playing.add(node);
3878
+ }
3632
3879
  /**
3633
3880
  * The colour `node` inherits, for frames that set `color: currentColor`, which is that colour
3634
3881
  * wherever the animation has got to, as in CSS. Undefined for frames that do not.
@@ -3646,7 +3893,8 @@ export class Engine {
3646
3893
  */
3647
3894
  reframe(node, current, frames, props, inherited) {
3648
3895
  const tracks = tracksOf(frames, props, inherited);
3649
- const reframed = { ...current, tracks, inherited };
3896
+ // One native was laying out goes on from JavaScript, by the clock it started on.
3897
+ const reframed = { ...current, tracks, inherited, laid: undefined };
3650
3898
  const { values, finished } = sample(reframed, current.pausedAt ?? this.now());
3651
3899
  const holds = current.spec.fill === 'forwards' || current.spec.fill === 'both';
3652
3900
  reframed.values = finished && !holds ? {} : values;
@@ -3691,25 +3939,28 @@ export class Engine {
3691
3939
  scrollAnimated(node, spec, frames, props) {
3692
3940
  const current = node.scrolled;
3693
3941
  const inherited = this.inheritedColour(node, frames);
3694
- if (current?.frames === frames && sameAnimation(current.spec, spec)) {
3942
+ const same = current?.frames === frames && sameAnimation(current.spec, spec);
3943
+ if (same && pinnedIn(node, current.tracks).join(' ') === current.pinned.join(' ')) {
3695
3944
  return Object.assign(props, this.rescrolled(node, current, frames, props, inherited));
3696
3945
  }
3697
3946
  this.stopScrolled(node);
3698
3947
  this.handOverToScroll(node, spec);
3699
3948
  const tracks = tracksOf(frames, props, inherited);
3949
+ const pinned = pinnedIn(node, tracks);
3950
+ const played = unpinned(tracks, pinned);
3700
3951
  const source = this.scrollSourceOf(node);
3701
3952
  const extent = source ? (this.scrollExtents.get(source)?.[spec.timeline] ?? null) : null;
3702
3953
  const range = rangeOf(spec, extent);
3703
- const first = firstFrame(tracks, spec, range);
3954
+ const first = firstFrame(played, spec, range);
3704
3955
  const resting = { ...props };
3705
3956
  if (!source && this.dev)
3706
3957
  this.reportUnscrolled(spec.name);
3707
- const drive = source ? this.driveScrolled(node, source, spec, tracks, resting, range) : null;
3958
+ const drive = source ? this.driveScrolled(node, source, spec, played, resting, range) : null;
3708
3959
  // As Animated does: Fabric flattens a view that only lays out, and then there is no native
3709
3960
  // view for the animation to move. Committed with the first frame, so it stays put.
3710
3961
  if (drive)
3711
3962
  first['collapsable'] = false;
3712
- node.scrolled = { spec, frames, tracks, resting, first, source, drive, inherited };
3963
+ node.scrolled = { spec, frames, tracks, pinned, resting, first, source, drive, inherited };
3713
3964
  return Object.assign(props, first);
3714
3965
  }
3715
3966
  /**
@@ -3786,7 +4037,7 @@ export class Engine {
3786
4037
  if (!scrolled?.drive)
3787
4038
  continue;
3788
4039
  const range = rangeOf(scrolled.spec, extent[scrolled.spec.timeline]);
3789
- scrolled.drive.update(scrollChannels(scrolled.tracks, scrolled.spec, scrolled.resting, range).channels);
4040
+ scrolled.drive.update(scrollChannels(unpinned(scrolled.tracks, scrolled.pinned), scrolled.spec, scrolled.resting, range).channels);
3790
4041
  }
3791
4042
  }
3792
4043
  reportUnscrolled(name) {
@@ -3812,24 +4063,358 @@ export class Engine {
3812
4063
  `has loaded, so the block has to exist somewhere by the time the animation starts.`);
3813
4064
  }
3814
4065
  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;
4066
+ this.unseenTransitions(node);
3818
4067
  const spec = transitionSpec(props);
3819
4068
  if (!spec && !node.transitions)
3820
4069
  return props;
4070
+ if (spec)
4071
+ keepForTransition(node, props, spec);
3821
4072
  const state = (node.transitions ??= new Map());
3822
4073
  const now = this.now();
4074
+ this.takeBackEased(node, state, props, spec, now);
4075
+ this.takeBackLaid(state, props, spec, now);
4076
+ const started = [];
3823
4077
  for (const key of steppedKeys(props, spec, state)) {
3824
4078
  if (step(state, key, props, spec?.[key] ?? spec?.['all'], now)) {
3825
4079
  this.running.add(node);
4080
+ started.push(key);
3826
4081
  this.emitTransition(node, 'topTransitionstart', key);
3827
4082
  }
3828
4083
  }
4084
+ this.easeNatively(node, state, started, props, now);
3829
4085
  if (state.size === 0)
3830
4086
  node.transitions = undefined;
3831
4087
  return props;
3832
4088
  }
4089
+ /** Nobody saw what the last commit gave a view it created, so this is where the view starts. */
4090
+ unseenTransitions(node) {
4091
+ // One with no commit it was born in has been seen, whatever was committed since.
4092
+ if (node.bornIn === undefined || node.bornIn < this.unseen)
4093
+ return;
4094
+ this.stopEased(node);
4095
+ node.transitions = undefined;
4096
+ }
4097
+ /** The elements native is playing a transition of. */
4098
+ easedNatively = new Set();
4099
+ /**
4100
+ * Hand to native each transition that just started and that it can play: opacity and
4101
+ * transforms, with no wait before they start. Those that start together, take as long and ease
4102
+ * alike share a clock. Nothing of them runs in JavaScript until native says they have ended:
4103
+ * a press that fades or slides something is a commit to start it and a commit to end it.
4104
+ *
4105
+ * ponytail: a colour is eased from JavaScript still, a commit a frame, and so is anything
4106
+ * that waits. Native has a colour node for the first; give the driver a channel for it.
4107
+ */
4108
+ easeNatively(node, state, started, props, now) {
4109
+ if (started.length)
4110
+ this.startEased(node, state, started, props, now);
4111
+ this.layEased(node, state, started, props, now);
4112
+ const inScript = [...state.values()].some((eased) => !eased.done && !eased.native && !eased.laid);
4113
+ if (!inScript)
4114
+ this.running.delete(node);
4115
+ // As Animated does: Fabric flattens a view that only lays out, and then there is no native
4116
+ // view for a transition to move.
4117
+ if (this.easedNatively.has(node))
4118
+ props['collapsable'] = false;
4119
+ }
4120
+ /** The root's children as the views they are committed as, each reconciled. */
4121
+ rootHandles() {
4122
+ const handles = [];
4123
+ for (const child of this.visibleChildren(this.root)) {
4124
+ if (!this.withheld(child))
4125
+ handles.push(this.reconcileUnder(this.root, child));
4126
+ }
4127
+ return handles;
4128
+ }
4129
+ /** The transitions this commit lays out where they end, for native to move the views to. */
4130
+ laying = [];
4131
+ /** Whether this commit resizes or moves a box that is in no such transition. */
4132
+ movesOthers = false;
4133
+ /**
4134
+ * Commit where it ends each transition of a size or a place that just started, for native to
4135
+ * move the view there: a layout animation, which the commit asks for once it is known that
4136
+ * nothing else in it moves. One still on its way is committed where it ends too.
4137
+ *
4138
+ * A width eased from JavaScript is a commit a frame, and a commit lays the whole screen out.
4139
+ */
4140
+ layEased(node, state, started, props, now) {
4141
+ if (!this.fabric.configureNextLayoutAnimation)
4142
+ return;
4143
+ for (const key of started) {
4144
+ const eased = state.get(key);
4145
+ if (!LAID_OUT.has(key) || eased.start > now || eased.duration <= 0)
4146
+ continue;
4147
+ eased.laid = true;
4148
+ this.laying.push({ node, key, eased });
4149
+ }
4150
+ for (const [key, eased] of state)
4151
+ if (eased.laid && !eased.done)
4152
+ props[key] = eased.to;
4153
+ }
4154
+ /** The animations this commit lays out at their last frame, for native to move the views to. */
4155
+ laidPlayed = [];
4156
+ /**
4157
+ * Whether an animation that has just started is one native can play by laying the view out
4158
+ * at its last frame: two frames of sizes or places, played once from where the view is drawn
4159
+ * now, which is where native moves it from. It is then committed at that frame; see `layOut`.
4160
+ */
4161
+ layPlayed(node, running) {
4162
+ const drawn = node.committed?.props;
4163
+ const { spec, tracks } = running;
4164
+ if (!this.fabric.configureNextLayoutAnimation || !drawn || !tracks.size)
4165
+ return false;
4166
+ if (!playedOnce(spec))
4167
+ return false;
4168
+ for (const [key, track] of tracks) {
4169
+ if (!LAID_OUT.has(key) || !fromWhereDrawn(track, drawn[key]))
4170
+ return false;
4171
+ }
4172
+ running.laid = true;
4173
+ running.values = Object.fromEntries([...tracks].map(([key, track]) => [key, track[1].value]));
4174
+ return true;
4175
+ }
4176
+ /** Go on from JavaScript with an animation native was to lay out, by the clock it started on. */
4177
+ playLaidHere(node, running) {
4178
+ running.laid = undefined;
4179
+ running.values = sample(running, this.now()).values;
4180
+ this.playing.add(node);
4181
+ }
4182
+ /** An animation native laid out has had its time: it is at its last frame, and says so. */
4183
+ endLaidPlayed(node, running) {
4184
+ if (!running.laid || running.done || node.playing !== running)
4185
+ return;
4186
+ Object.assign(running, { done: true, laid: undefined });
4187
+ if (!this.keeps(node, running)) {
4188
+ running.values = {};
4189
+ this.markProps(node, false);
4190
+ }
4191
+ this.emitTransition(node, 'topAnimationend', running.spec.name);
4192
+ this.flushTransitionEvents();
4193
+ if (this.pending)
4194
+ this.commit();
4195
+ }
4196
+ /**
4197
+ * Before a commit is handed over: ask native to move what it lays out, and take back to
4198
+ * ease from JavaScript whatever it cannot: all of it where the commit moves anything else,
4199
+ * and otherwise what would not take as long as the longest. Answers whether the tree has to
4200
+ * be reconciled again.
4201
+ */
4202
+ layOut() {
4203
+ const laying = this.laying;
4204
+ const played = this.laidPlayed.filter(({ node, running }) => node.playing === running);
4205
+ const others = this.movesOthers;
4206
+ this.laying = [];
4207
+ this.laidPlayed = [];
4208
+ this.movesOthers = false;
4209
+ if (!laying.length && !played.length)
4210
+ return false;
4211
+ // Native takes one length of time for a commit: the longest asked of it, and whatever
4212
+ // would take another is eased from JavaScript. None of it, where anything else moves.
4213
+ const longest = Math.max(...laying.map(({ eased }) => eased.duration), ...played.map(({ running }) => running.spec.duration));
4214
+ const kept = (duration) => !others && duration === longest;
4215
+ const back = [];
4216
+ for (const { node, eased } of laying) {
4217
+ if (kept(eased.duration))
4218
+ continue;
4219
+ eased.laid = undefined;
4220
+ this.running.add(node);
4221
+ back.push(node);
4222
+ }
4223
+ for (const { node, running } of played) {
4224
+ if (kept(running.spec.duration))
4225
+ continue;
4226
+ this.playLaidHere(node, running);
4227
+ back.push(node);
4228
+ }
4229
+ for (const node of back)
4230
+ this.markProps(node, false);
4231
+ if (!others)
4232
+ this.askLayout(laying, played, longest);
4233
+ return back.length > 0;
4234
+ }
4235
+ /** Ask native to move what the commit lays out over `duration`, and end each when it is up. */
4236
+ askLayout(laying, played, duration) {
4237
+ const easing = laying.find(({ eased }) => eased.laid)?.eased.easing ??
4238
+ played.find(({ running }) => running.laid).running.spec.easing;
4239
+ const nothing = () => undefined;
4240
+ this.fabric.configureNextLayoutAnimation({ duration, update: { type: layoutCurve(easing) } }, nothing, nothing);
4241
+ for (const { node, key, eased } of laying) {
4242
+ if (eased.laid)
4243
+ setTimeout(() => this.endLaid(node, key, eased), duration);
4244
+ }
4245
+ for (const { node, running } of played) {
4246
+ if (running.laid)
4247
+ setTimeout(() => this.endLaidPlayed(node, running), duration);
4248
+ }
4249
+ }
4250
+ /**
4251
+ * Note a commit that resizes or moves a box in no transition native lays out: a prop of one
4252
+ * that moves boxes, children that came or went, text longer or shorter than it was.
4253
+ */
4254
+ noteMoved(node, payload, resized, words) {
4255
+ if (this.movesOthers)
4256
+ return;
4257
+ if (resized)
4258
+ this.movesOthers = true;
4259
+ for (const key in payload) {
4260
+ if (laidByNative(node, key))
4261
+ continue;
4262
+ // Text as long as it was is taken to be as wide, as a grid takes it: see `shapeOf`.
4263
+ const longer = key === 'text' && String(payload[key]).length !== String(words).length;
4264
+ if (MOVES_BOXES.has(key) || longer)
4265
+ this.movesOthers = true;
4266
+ }
4267
+ }
4268
+ /** A transition native laid out has had its time: it is where it ends, and says so. */
4269
+ endLaid(node, key, eased) {
4270
+ if (!eased.laid || eased.done || node.transitions?.get(key) !== eased)
4271
+ return;
4272
+ Object.assign(eased, { current: eased.to, done: true, laid: undefined });
4273
+ this.emitTransition(node, 'topTransitionend', key);
4274
+ // No commit comes of it, which is what tells listeners otherwise: it is where it ends.
4275
+ this.flushTransitionEvents();
4276
+ // And what a listener did, taking away an element that has left, is committed.
4277
+ if (this.pending)
4278
+ this.commit();
4279
+ }
4280
+ /**
4281
+ * Bring a transition native laid out to where its clock has got to, where it is sent another
4282
+ * way or its rule has gone: that is where it goes on from.
4283
+ */
4284
+ takeBackLaid(state, props, spec, now) {
4285
+ for (const [key, eased] of state) {
4286
+ if (!eased.laid || eased.done)
4287
+ continue;
4288
+ const kept = spec?.[key] ?? spec?.['all'];
4289
+ if (kept && !aimsElsewhere(eased, key, props))
4290
+ continue;
4291
+ const progress = Math.min(1, Math.max(0, (now - eased.start) / eased.duration));
4292
+ eased.current = interpolate(eased.from, eased.to, bezier(eased.easing, progress));
4293
+ eased.laid = undefined;
4294
+ }
4295
+ }
4296
+ startEased(node, state, started, props, now) {
4297
+ if (!this.scrollDriver)
4298
+ return;
4299
+ const clocks = new Map();
4300
+ for (const key of started) {
4301
+ const eased = state.get(key);
4302
+ if (!NATIVELY_EASED.has(key) || eased.start > now || eased.duration <= 0)
4303
+ continue;
4304
+ const clock = `${eased.duration} ${eased.easing.join()}`;
4305
+ clocks.set(clock, [...(clocks.get(clock) ?? []), key]);
4306
+ }
4307
+ for (const keys of clocks.values())
4308
+ this.playEased(node, state, keys, props);
4309
+ }
4310
+ playEased(node, state, keys, props) {
4311
+ const first = state.get(keys[0]);
4312
+ // Where the view is committed: one that takes a touch stops short of no opacity.
4313
+ const touched = this.responders.has(node);
4314
+ const seen = (key, value) => touched && key === 'opacity' ? stillTouched({ ...props, opacity: value })['opacity'] : value;
4315
+ const tracks = new Map(keys.map((key) => {
4316
+ const { current, to, easing } = state.get(key);
4317
+ return [
4318
+ key,
4319
+ [
4320
+ { offset: 0, value: seen(key, current), easing },
4321
+ { offset: 1, value: seen(key, to) },
4322
+ ],
4323
+ ];
4324
+ }));
4325
+ const spec = {
4326
+ name: '',
4327
+ duration: first.duration,
4328
+ delay: 0,
4329
+ easing: first.easing,
4330
+ iterations: 1,
4331
+ fill: 'both',
4332
+ };
4333
+ const channels = easedChannels(tracks, spec, props);
4334
+ if (!channels)
4335
+ return;
4336
+ const length = Math.max(2, Math.round(first.duration / NATIVE_FRAME) + 1);
4337
+ const frames = Array.from({ length }, (_, i) => i / (length - 1));
4338
+ const clock = { keys, stop: () => undefined };
4339
+ const native = this.scrollDriver.play(node, channels, { frames, toValue: 1, iterations: 1 }, (view) => this.tagOf(view), () => this.endedEased(node, clock));
4340
+ if (!native)
4341
+ return;
4342
+ clock.stop = () => native.stop();
4343
+ for (const key of keys)
4344
+ state.get(key).native = clock;
4345
+ this.easedNatively.add(node);
4346
+ }
4347
+ /**
4348
+ * Native reached the end of the transitions on one clock. Where they end is committed before
4349
+ * the view is let go, so nothing shows of where they started, which is what it was committed
4350
+ * with.
4351
+ */
4352
+ endedEased(node, clock) {
4353
+ const state = node.transitions;
4354
+ const keys = clock.keys.filter((key) => state?.get(key)?.native === clock);
4355
+ for (const key of keys) {
4356
+ const eased = state.get(key);
4357
+ Object.assign(eased, { current: eased.to, done: true, native: undefined });
4358
+ this.emitTransition(node, 'topTransitionend', key);
4359
+ this.markProps(node, false);
4360
+ }
4361
+ this.forgetEased(node);
4362
+ if (keys.length)
4363
+ this.commit();
4364
+ clock.stop();
4365
+ }
4366
+ /** No longer one native is easing, once none of its transitions is native's. */
4367
+ forgetEased(node) {
4368
+ for (const eased of node.transitions?.values() ?? [])
4369
+ if (eased.native)
4370
+ return;
4371
+ this.easedNatively.delete(node);
4372
+ }
4373
+ /**
4374
+ * Take back from native each transition it can no longer play as it was started: one sent
4375
+ * another way, one whose rule has gone, any of a view that was made again. Each is brought to
4376
+ * where its clock has got to, which is where it goes on from, here or on native again.
4377
+ */
4378
+ takeBackEased(node, state, props, spec, now) {
4379
+ if (!this.easedNatively.has(node))
4380
+ return;
4381
+ const remade = node.committed === null;
4382
+ for (const [key, eased] of state) {
4383
+ if (!eased.native)
4384
+ continue;
4385
+ const kept = spec?.[key] ?? spec?.['all'];
4386
+ if (remade || !kept || aimsElsewhere(eased, key, props)) {
4387
+ this.backToScriptEased(node, eased.native, now);
4388
+ }
4389
+ }
4390
+ }
4391
+ /** Stop a clock native runs, and go on from JavaScript with each transition it was playing. */
4392
+ backToScriptEased(node, clock, now) {
4393
+ for (const key of clock.keys) {
4394
+ const eased = node.transitions?.get(key);
4395
+ if (eased?.native !== clock)
4396
+ continue;
4397
+ eased.native = undefined;
4398
+ const progress = Math.min(1, Math.max(0, (now - eased.start) / eased.duration));
4399
+ eased.current = interpolate(eased.from, eased.to, bezier(eased.easing, progress));
4400
+ this.running.add(node);
4401
+ }
4402
+ clock.stop();
4403
+ this.forgetEased(node);
4404
+ }
4405
+ /** Let go of what native is easing on an element that is going, or gone. */
4406
+ stopEased(node) {
4407
+ if (!this.easedNatively.delete(node))
4408
+ return;
4409
+ const clocks = new Set();
4410
+ for (const eased of node.transitions?.values() ?? []) {
4411
+ if (eased.native)
4412
+ clocks.add(eased.native);
4413
+ eased.native = undefined;
4414
+ }
4415
+ for (const clock of clocks)
4416
+ clock.stop();
4417
+ }
3833
4418
  /**
3834
4419
  * Whether an animation that has finished goes on holding its last frame. With a forwards fill
3835
4420
  * it does, for good: without one the element falls back to whatever the cascade gives it, and
@@ -4101,6 +4686,7 @@ export class Engine {
4101
4686
  const props = propsMoved ? this.mergeProps(node, viewName) : previous.props;
4102
4687
  const propsPayload = propsMoved ? this.propsPayload(node, previous.props, props) : null;
4103
4688
  const childrenChanged = this.childrenMoved(node, childHandles, previous.childHandles);
4689
+ this.noteMoved(node, propsPayload, childHandles.length !== previous.childHandles.length, previous.props['text']);
4104
4690
  let handle = previous.handle;
4105
4691
  if (childrenChanged || propsPayload) {
4106
4692
  handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload), childrenChanged ? childHandles : null);
@@ -4581,6 +5167,11 @@ export class Engine {
4581
5167
  setResponder(node, handlers) {
4582
5168
  this.registerEventHandler();
4583
5169
  this.responders.set(node, handlers);
5170
+ // A fade native is playing was given to it as a view that takes no touch fades: to
5171
+ // nothing. From here it is eased to where the view is committed.
5172
+ const fade = node.transitions?.get('opacity')?.native;
5173
+ if (fade)
5174
+ this.backToScriptEased(node, fade, this.now());
4584
5175
  this.keepNative(node, true);
4585
5176
  // What it is committed with can hang on whether it takes a touch: see `stillTouched`.
4586
5177
  this.markProps(node);
@@ -4808,12 +5399,14 @@ export class Engine {
4808
5399
  this.currentResponder = null;
4809
5400
  this.tellNative(node, false);
4810
5401
  const handlers = this.responders.get(node);
5402
+ // Before the handler is told: what it does is for the render pass after it to commit
5403
+ // whole, and not to be drawn part made with the view no longer pressed.
5404
+ if (changed)
5405
+ this.commit();
4811
5406
  if (terminated)
4812
5407
  handlers?.onResponderTerminate?.(event);
4813
5408
  else
4814
5409
  handlers?.onResponderRelease?.(event);
4815
- if (changed)
4816
- this.commit();
4817
5410
  }
4818
5411
  tellNative(node, isResponder) {
4819
5412
  const handle = node.committed?.handle;