@ng-native/fabric 0.8.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.d.ts CHANGED
@@ -206,6 +206,8 @@ export interface EngineNode extends HostNode {
206
206
  styleCache: StyleCache | null;
207
207
  /** Set when something that could change what this node matches has changed. */
208
208
  styleDirty: boolean;
209
+ /** See `StyleTarget.ownDirty`. */
210
+ ownDirty: boolean;
209
211
  /** See `StyleTarget.hasDirty`. */
210
212
  hasDirty: boolean;
211
213
  /** See `StyleTarget.stateDirty`. */
@@ -777,16 +779,21 @@ export declare class Engine implements HostEngine {
777
779
  * Mark a node whose classes changed, by what the rules that name those classes can restyle.
778
780
  *
779
781
  * A class no selector names changes no style: a library's own marker, of an overlay that is
780
- * animating or a control that was touched. One named only as what a rule's element is
781
- * inside, `.animating .close`, restyles the elements under the node that such rules are for.
782
- * Either way a whole subtree is not styled again for a class that has no say in it. Any other
783
- * class restyles the node and everything under it, as any other change to it does.
782
+ * animating or a control that was touched. One a rule is for the element of, `.on`, has that
783
+ * element matched again, and one named as what a rule's element is inside, `.animating .close`
784
+ * or `.close:is(.animating *)`, the elements under the node that such rules are for. What is
785
+ * under any of those is styled again from the rules it matched, for what it inherits: a `dark`
786
+ * class on the root finds no rule again for an element no `dark` rule is for. Any other class
787
+ * has the node and everything under it matched again, as any other change to it does.
784
788
  *
785
789
  * A sheet that names a class later restyles what it is for as it arrives, and a node not yet
786
790
  * committed is styled when it is, with the class as it is then.
787
791
  */
788
792
  private markClasses;
789
- /** Have styled again each element under a node that one of `subjects`' rules could be for. */
793
+ /**
794
+ * Have matched again each element under a node that one of `subjects`' rules could be for. One
795
+ * under another is marked too: what is under an element marked this way keeps its rules.
796
+ */
790
797
  private markUnder;
791
798
  /**
792
799
  * The sheet of the component `node` hosts, whose `:host` rules it matches. Set again when a hot
@@ -800,13 +807,23 @@ export declare class Engine implements HostEngine {
800
807
  * the web's mount point has, unless the app's `:host` sizes it otherwise.
801
808
  */
802
809
  setDefaultStyle(node: EngineNode, style: Readonly<Record<string, unknown>> | undefined): void;
803
- /** `class="a b"` from a template. Replaces the set rather than adding to it. */
810
+ /**
811
+ * `class="a b"` from a template. Replaces the set rather than adding to it. The same classes in
812
+ * another order are a change too, to a selector that reads the class attribute, `[class^="a"]`.
813
+ */
804
814
  setClasses(node: EngineNode, value: string): void;
805
815
  /** Angular's `@if`/`@for`/`ViewContainerRef` markers. Ordered, never committed. */
806
816
  createAnchor(): EngineNode;
807
817
  createText(value: string): EngineNode;
808
818
  appendChild(parent: EngineNode, child: EngineNode): void;
809
819
  insertBefore(parent: EngineNode, child: EngineNode, ref: EngineNode | null): void;
820
+ /**
821
+ * A node coming under a parent other than the one it is under, or under any once it has been
822
+ * taken out: what is over it may be other than it was styled under. The resolver sees that for
823
+ * itself by the parent's cache, except between two boxes that share one, where it is told here.
824
+ * Moved within its own parent, a row of a list reordered, it is styled as it was.
825
+ */
826
+ private markAdopted;
810
827
  removeChild(parent: EngineNode | null, child: EngineNode): void;
811
828
  /**
812
829
  * A child came or went under a view that takes its text as a prop: that prop changed. A span
@@ -1122,6 +1139,24 @@ export declare class Engine implements HostEngine {
1122
1139
  * own frames, which is what lets an element animate in without differing from anything.
1123
1140
  */
1124
1141
  private animated;
1142
+ /**
1143
+ * The keyframes a node's animation names: those of its own component's sheet where it has
1144
+ * some by that name, as Angular scopes a component's keyframes in a browser, and otherwise
1145
+ * whichever sheet has them, since any other is global.
1146
+ *
1147
+ * ponytail: a host is asked of the sheet it is written in before its own component's, so
1148
+ * a `:host` animation is played by its parent's frames where both have the name.
1149
+ */
1150
+ private keyframesOf;
1151
+ /** Each node's frames as they settle on it, kept for as long as they settle the same. */
1152
+ private readonly settledFrames;
1153
+ /**
1154
+ * An animation's frames for the node playing it: those with a value that waits for the node,
1155
+ * a `var()` it sets, settled against it. The same frames are answered while they settle the
1156
+ * same, and others once a variable changes, which is what an animation under way is played
1157
+ * on from: see `reframe`.
1158
+ */
1159
+ private framesFor;
1125
1160
  /**
1126
1161
  * A clock animation the scroll plays instead: the same animation where only the timeline moved,
1127
1162
  * and one cancelled where the name changed.
@@ -1179,6 +1214,8 @@ export declare class Engine implements HostEngine {
1179
1214
  private backToScript;
1180
1215
  /** Start the clock on an animation, unless the node is already playing this one. */
1181
1216
  private startPlaying;
1217
+ /** Play an animation that has just started: by native's layout, by native's driver, or here. */
1218
+ private playSomewhere;
1182
1219
  /**
1183
1220
  * The colour `node` inherits, for frames that set `color: currentColor`, which is that colour
1184
1221
  * wherever the animation has got to, as in CSS. Undefined for frames that do not.
@@ -1258,12 +1295,27 @@ export declare class Engine implements HostEngine {
1258
1295
  * A width eased from JavaScript is a commit a frame, and a commit lays the whole screen out.
1259
1296
  */
1260
1297
  private layEased;
1298
+ /** The animations this commit lays out at their last frame, for native to move the views to. */
1299
+ private laidPlayed;
1300
+ /**
1301
+ * Whether an animation that has just started is one native can play by laying the view out
1302
+ * at its last frame: two frames of sizes or places, played once from where the view is drawn
1303
+ * now, which is where native moves it from. It is then committed at that frame; see `layOut`.
1304
+ */
1305
+ private layPlayed;
1306
+ /** Go on from JavaScript with an animation native was to lay out, by the clock it started on. */
1307
+ private playLaidHere;
1308
+ /** An animation native laid out has had its time: it is at its last frame, and says so. */
1309
+ private endLaidPlayed;
1261
1310
  /**
1262
- * Before a commit is handed over: ask native to move what it lays out, or take the
1263
- * transitions back to ease them from JavaScript where the commit moves anything else, and
1264
- * where they would not all take as long. Answers whether the tree has to be reconciled again.
1311
+ * Before a commit is handed over: ask native to move what it lays out, and take back to
1312
+ * ease from JavaScript whatever it cannot: all of it where the commit moves anything else,
1313
+ * and otherwise what would not take as long as the longest. Answers whether the tree has to
1314
+ * be reconciled again.
1265
1315
  */
1266
1316
  private layOut;
1317
+ /** Ask native to move what the commit lays out over `duration`, and end each when it is up. */
1318
+ private askLayout;
1267
1319
  /**
1268
1320
  * Note a commit that resizes or moves a box in no transition native lays out: a prop of one
1269
1321
  * that moves boxes, children that came or went, text longer or shorter than it was.
package/dist/engine.js CHANGED
@@ -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':
@@ -537,8 +539,8 @@ function lineBaseline(props, line, [, top, , bottom], fontScale) {
537
539
  * out on both platforms: on Android, `EditText` centres a line box taller than the font's own
538
540
  * about 1.7pt high of centre in a 44pt field, and centres the font's own exactly. A percentage
539
541
  * height sizes the field only where its parent's height is definite, which is known at layout
540
- * alone, so such a field is left as it was. Elsewhere Android sizes the field by the line height
541
- * 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.
542
544
  *
543
545
  * On iOS, React Native's field sets `lineHeight` as the paragraph's minimum and maximum line
544
546
  * height and, unlike a paragraph (`RCTApplyBaselineOffset`), never offsets the baseline, so the
@@ -561,10 +563,9 @@ function centreSingleLine(viewName, props, fontScale, onBaseline = false) {
561
563
  heightBaseline(props, height, onBaseline, fontScale);
562
564
  return;
563
565
  }
564
- // A percentage is a fixed height only where the parent's is definite, known at layout alone.
565
- if (isSet(height) && height !== 'auto')
566
- return;
567
- // 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.
568
569
  if (viewName !== 'TextInput')
569
570
  return;
570
571
  const edge = (side, logical) => blockEdge(props, side, logical);
@@ -847,10 +848,11 @@ const TOUCHED_ALPHA = 0.011;
847
848
  function stillTouched(props) {
848
849
  // One told to take no touch is not to be sent one, seen or not.
849
850
  if (props['pointerEvents'] === 'none')
850
- return;
851
+ return props;
851
852
  const opacity = props['opacity'];
852
853
  if (typeof opacity === 'number' && opacity < TOUCHED_ALPHA)
853
854
  props['opacity'] = TOUCHED_ALPHA;
855
+ return props;
854
856
  }
855
857
  /**
856
858
  * Leave `aspect-ratio` out for a box with both a width and a height. CSS takes the ratio only
@@ -1411,6 +1413,23 @@ const MOVES_BOXES = new Set([
1411
1413
  ...['aspectRatio', 'fontSize', 'lineHeight', 'fontFamily', 'fontWeight', 'letterSpacing'],
1412
1414
  ...['numberOfLines', 'start', 'end'],
1413
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
+ }
1414
1433
  /** The curves a layout animation has, as the timing function each is. */
1415
1434
  const LAYOUT_CURVES = [
1416
1435
  ['linear', [0, 0, 1, 1]],
@@ -1645,6 +1664,7 @@ class RetainedNode {
1645
1664
  customProperties = null;
1646
1665
  styleCache = null;
1647
1666
  styleDirty = true;
1667
+ ownDirty = false;
1648
1668
  styleCommitted = null;
1649
1669
  fitContainer = undefined;
1650
1670
  heightBasis = undefined;
@@ -1861,6 +1881,8 @@ const TRANSITION_TIMING = {
1861
1881
  $transitionDelay: 'delay',
1862
1882
  $transitionEasing: 'easing',
1863
1883
  };
1884
+ /** Listed once, as `ANIMATION_ENTRIES` is. */
1885
+ const TRANSITION_ENTRIES = Object.entries(TRANSITION_TIMING);
1864
1886
  /**
1865
1887
  * The transition spec, with any timing set by a rule of its own laid over it: `.duration-700`
1866
1888
  * beside `.transition`. A rule that writes the timing itself compiles it to `null`, which is how
@@ -1874,7 +1896,7 @@ function transitionSpec(props) {
1874
1896
  let spec = props['$transition'];
1875
1897
  if (spec !== undefined)
1876
1898
  delete props['$transition'];
1877
- for (const [key, part] of Object.entries(TRANSITION_TIMING)) {
1899
+ for (const [key, part] of TRANSITION_ENTRIES) {
1878
1900
  if (!(key in props))
1879
1901
  continue;
1880
1902
  const value = props[key];
@@ -1993,6 +2015,14 @@ const now = () => globalThis.performance?.now?.() ?? Date.now();
1993
2015
  function sameHandles(a, b) {
1994
2016
  return a.length === b.length && a.every((handle, i) => handle === b[i]);
1995
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
+ }
1996
2026
  /**
1997
2027
  * Whether a node could be the element a compound is written for, by its classes alone: its own,
1998
2028
  * and those of one alternative of each `:is()` on it, which is where Tailwind names a group or
@@ -2495,34 +2525,46 @@ export class Engine {
2495
2525
  * Mark a node whose classes changed, by what the rules that name those classes can restyle.
2496
2526
  *
2497
2527
  * A class no selector names changes no style: a library's own marker, of an overlay that is
2498
- * animating or a control that was touched. One named only as what a rule's element is
2499
- * inside, `.animating .close`, restyles the elements under the node that such rules are for.
2500
- * Either way a whole subtree is not styled again for a class that has no say in it. Any other
2501
- * 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.
2502
2534
  *
2503
2535
  * A sheet that names a class later restyles what it is for as it arrives, and a node not yet
2504
2536
  * committed is styled when it is, with the class as it is then.
2505
2537
  */
2506
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);
2507
2544
  const reach = this.styles.reachOf(changed);
2508
2545
  if (reach === true)
2509
2546
  return this.markProps(node);
2510
2547
  this.markProps(node, false);
2511
- if (reach)
2548
+ if (!reach)
2549
+ return;
2550
+ if (reach.own)
2551
+ node.ownDirty = true;
2552
+ if (this.styles.reachesUnder(reach))
2512
2553
  this.markUnder(node, reach);
2513
2554
  }
2514
- /** 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
+ */
2515
2559
  markUnder(node, subjects) {
2516
2560
  for (const child of node.children) {
2517
2561
  if (child.kind !== 'element')
2518
2562
  continue;
2519
2563
  if (this.styles.isFor(subjects, child)) {
2520
- // And with it all under it, which is styled from it.
2521
- child.styleDirty = true;
2564
+ child.ownDirty = true;
2522
2565
  this.markPath(child);
2523
2566
  }
2524
- else
2525
- this.markUnder(child, subjects);
2567
+ this.markUnder(child, subjects);
2526
2568
  }
2527
2569
  }
2528
2570
  /**
@@ -2548,7 +2590,10 @@ export class Engine {
2548
2590
  node.defaultStyle = style;
2549
2591
  this.markProps(node);
2550
2592
  }
2551
- /** `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
+ */
2552
2597
  setClasses(node, value) {
2553
2598
  const names = value.split(/\s+/).filter(Boolean);
2554
2599
  const before = node.classes;
@@ -2558,7 +2603,8 @@ export class Engine {
2558
2603
  for (const name of before ?? [])
2559
2604
  if (!node.classes?.has(name))
2560
2605
  changed.push(name);
2561
- this.markClasses(node, changed);
2606
+ const reordered = !changed.length && !!before && !!node.classes && !inOrder(before, node.classes);
2607
+ this.markClasses(node, reordered ? node.classes : changed);
2562
2608
  }
2563
2609
  /** Angular's `@if`/`@for`/`ViewContainerRef` markers. Ordered, never committed. */
2564
2610
  createAnchor() {
@@ -2570,6 +2616,7 @@ export class Engine {
2570
2616
  return node;
2571
2617
  }
2572
2618
  appendChild(parent, child) {
2619
+ this.markAdopted(child, parent);
2573
2620
  this.detach(child);
2574
2621
  child.parent = parent;
2575
2622
  parent.children.push(child);
@@ -2579,6 +2626,7 @@ export class Engine {
2579
2626
  this.markTextContent(parent, child);
2580
2627
  }
2581
2628
  insertBefore(parent, child, ref) {
2629
+ this.markAdopted(child, parent);
2582
2630
  this.detach(child);
2583
2631
  child.parent = parent;
2584
2632
  const at = ref ? parent.children.indexOf(ref) : -1;
@@ -2591,6 +2639,16 @@ export class Engine {
2591
2639
  this.markStructure(parent, child, at < 0 ? parent.children.length - 1 : at);
2592
2640
  this.markTextContent(parent, child);
2593
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
+ }
2594
2652
  removeChild(parent, child) {
2595
2653
  const target = parent ?? child.parent;
2596
2654
  if (!target)
@@ -2842,7 +2900,10 @@ export class Engine {
2842
2900
  else {
2843
2901
  node.customProperties = { ...current, [name]: token };
2844
2902
  }
2845
- 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;
2846
2907
  }
2847
2908
  setText(node, value) {
2848
2909
  const filled = (node.text === '') !== (value === '');
@@ -3538,7 +3599,8 @@ export class Engine {
3538
3599
  }
3539
3600
  return props;
3540
3601
  }
3541
- const frames = this.keyframes.get(spec.name);
3602
+ const written = this.keyframesOf(node, spec.name);
3603
+ const frames = written && this.framesFor(node, written);
3542
3604
  if (!frames) {
3543
3605
  // Keyframes a hot swap deleted, from under an animation that was playing them, or ones a
3544
3606
  // sheet later in this commit has: see `settleKeyframes`.
@@ -3554,6 +3616,49 @@ export class Engine {
3554
3616
  this.startPlaying(node, spec, frames, props);
3555
3617
  return Object.assign(props, playedOver(node));
3556
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;
3661
+ }
3557
3662
  /**
3558
3663
  * A clock animation the scroll plays instead: the same animation where only the timeline moved,
3559
3664
  * and one cancelled where the name changed.
@@ -3749,8 +3854,8 @@ export class Engine {
3749
3854
  node.playing = started;
3750
3855
  if (spec.paused)
3751
3856
  started.pausedAt = this.now();
3752
- else if (!this.playNatively(node, started, props))
3753
- this.playing.add(node);
3857
+ else
3858
+ this.playSomewhere(node, started, props);
3754
3859
  this.emitTransition(node, 'topAnimationstart', spec.name);
3755
3860
  return;
3756
3861
  }
@@ -3764,6 +3869,13 @@ export class Engine {
3764
3869
  }
3765
3870
  this.playOrPause(node, node.playing, spec);
3766
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
+ }
3767
3879
  /**
3768
3880
  * The colour `node` inherits, for frames that set `color: currentColor`, which is that colour
3769
3881
  * wherever the animation has got to, as in CSS. Undefined for frames that do not.
@@ -3781,7 +3893,8 @@ export class Engine {
3781
3893
  */
3782
3894
  reframe(node, current, frames, props, inherited) {
3783
3895
  const tracks = tracksOf(frames, props, inherited);
3784
- 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 };
3785
3898
  const { values, finished } = sample(reframed, current.pausedAt ?? this.now());
3786
3899
  const holds = current.spec.fill === 'forwards' || current.spec.fill === 'both';
3787
3900
  reframed.values = finished && !holds ? {} : values;
@@ -3975,7 +4088,8 @@ export class Engine {
3975
4088
  }
3976
4089
  /** Nobody saw what the last commit gave a view it created, so this is where the view starts. */
3977
4090
  unseenTransitions(node) {
3978
- if (node.bornIn < this.unseen)
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)
3979
4093
  return;
3980
4094
  this.stopEased(node);
3981
4095
  node.transitions = undefined;
@@ -4037,36 +4151,101 @@ export class Engine {
4037
4151
  if (eased.laid && !eased.done)
4038
4152
  props[key] = eased.to;
4039
4153
  }
4154
+ /** The animations this commit lays out at their last frame, for native to move the views to. */
4155
+ laidPlayed = [];
4040
4156
  /**
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.
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.
4044
4201
  */
4045
4202
  layOut() {
4046
4203
  const laying = this.laying;
4204
+ const played = this.laidPlayed.filter(({ node, running }) => node.playing === running);
4047
4205
  const others = this.movesOthers;
4048
4206
  this.laying = [];
4207
+ this.laidPlayed = [];
4049
4208
  this.movesOthers = false;
4050
- if (!laying.length)
4209
+ if (!laying.length && !played.length)
4051
4210
  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;
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);
4062
4228
  }
4063
- const { duration, easing } = first.eased;
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;
4064
4239
  const nothing = () => undefined;
4065
4240
  this.fabric.configureNextLayoutAnimation({ duration, update: { type: layoutCurve(easing) } }, nothing, nothing);
4066
4241
  for (const { node, key, eased } of laying) {
4067
- setTimeout(() => this.endLaid(node, key, eased), duration);
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);
4068
4248
  }
4069
- return false;
4070
4249
  }
4071
4250
  /**
4072
4251
  * Note a commit that resizes or moves a box in no transition native lays out: a prop of one
@@ -4078,8 +4257,7 @@ export class Engine {
4078
4257
  if (resized)
4079
4258
  this.movesOthers = true;
4080
4259
  for (const key in payload) {
4081
- const eased = node.transitions?.get(key);
4082
- if (eased?.laid && !eased.done)
4260
+ if (laidByNative(node, key))
4083
4261
  continue;
4084
4262
  // Text as long as it was is taken to be as wide, as a grid takes it: see `shapeOf`.
4085
4263
  const longer = key === 'text' && String(payload[key]).length !== String(words).length;
@@ -4131,13 +4309,16 @@ export class Engine {
4131
4309
  }
4132
4310
  playEased(node, state, keys, props) {
4133
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;
4134
4315
  const tracks = new Map(keys.map((key) => {
4135
4316
  const { current, to, easing } = state.get(key);
4136
4317
  return [
4137
4318
  key,
4138
4319
  [
4139
- { offset: 0, value: current, easing },
4140
- { offset: 1, value: to },
4320
+ { offset: 0, value: seen(key, current), easing },
4321
+ { offset: 1, value: seen(key, to) },
4141
4322
  ],
4142
4323
  ];
4143
4324
  }));
@@ -4986,6 +5167,11 @@ export class Engine {
4986
5167
  setResponder(node, handlers) {
4987
5168
  this.registerEventHandler();
4988
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());
4989
5175
  this.keepNative(node, true);
4990
5176
  // What it is committed with can hang on whether it takes a touch: see `stillTouched`.
4991
5177
  this.markProps(node);
@@ -5213,12 +5399,14 @@ export class Engine {
5213
5399
  this.currentResponder = null;
5214
5400
  this.tellNative(node, false);
5215
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();
5216
5406
  if (terminated)
5217
5407
  handlers?.onResponderTerminate?.(event);
5218
5408
  else
5219
5409
  handlers?.onResponderRelease?.(event);
5220
- if (changed)
5221
- this.commit();
5222
5410
  }
5223
5411
  tellNative(node, isResponder) {
5224
5412
  const handle = node.committed?.handle;