@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/background-layers.d.ts +18 -0
- package/dist/background-layers.js +70 -0
- package/dist/background-layers.js.map +1 -0
- package/dist/css.d.ts +67 -3
- package/dist/css.js +329 -62
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +137 -7
- package/dist/engine.js +641 -48
- package/dist/engine.js.map +1 -1
- package/dist/fabric.d.ts +1 -1
- package/dist/fabric.js +1 -0
- package/dist/fabric.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/native-drive.js +9 -0
- package/dist/native-drive.js.map +1 -1
- package/dist/scroll-animation.d.ts +11 -0
- package/dist/scroll-animation.js +30 -1
- package/dist/scroll-animation.js.map +1 -1
- package/dist/transition.d.ts +34 -0
- package/dist/transition.js +32 -9
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
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
|
|
2380
|
-
*
|
|
2381
|
-
*
|
|
2382
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
2402
|
-
child.styleDirty = true;
|
|
2564
|
+
child.ownDirty = true;
|
|
2403
2565
|
this.markPath(child);
|
|
2404
2566
|
}
|
|
2405
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3619
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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;
|