@ng-native/fabric 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +21 -0
- package/dist/css.js +174 -24
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +104 -1
- package/dist/engine.js +617 -36
- 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 +48 -1
- package/dist/transition.js +119 -29
- 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, 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
|
/**
|
|
@@ -389,6 +389,51 @@ const VIRTUAL_TEXT = 'VirtualText';
|
|
|
389
389
|
const DEFAULT_VIEW = 'View';
|
|
390
390
|
/** A top-level text, the one view that aligns its lines. A nested text is a `VirtualText`. */
|
|
391
391
|
const PARAGRAPH = 'Paragraph';
|
|
392
|
+
/**
|
|
393
|
+
* The box Yoga takes a view's baseline from where the view has none of its own: its first child
|
|
394
|
+
* that is laid out in its flow. Words written straight into the view are the paragraph the
|
|
395
|
+
* engine makes around them, and a child that is not displayed has no view to ask.
|
|
396
|
+
*/
|
|
397
|
+
/**
|
|
398
|
+
* A view with a transition is a native view for as long as it has one, as an `Animated.View` is.
|
|
399
|
+
*
|
|
400
|
+
* React Native gives a view a native view of its own for an opacity under 1, a transform or a
|
|
401
|
+
* background, and takes it away when the last such reason goes. A wrapper that fades in was one
|
|
402
|
+
* for the length of the fade and none after, and Fabric moved everything inside it to the view
|
|
403
|
+
* above as the fade ended. Being moved takes a focused field out of the window for a moment, and
|
|
404
|
+
* it lost its focus: a field focused as its container faded in blurred when the fade finished.
|
|
405
|
+
*
|
|
406
|
+
* A floor, as `keepNative` is: a `collapsable` written on the element is left alone. Not for
|
|
407
|
+
* `transition: none`, which transitions nothing.
|
|
408
|
+
*/
|
|
409
|
+
function keepForTransition(node, props, spec) {
|
|
410
|
+
if (viewNameOf(node) !== DEFAULT_VIEW || props['collapsable'] !== undefined)
|
|
411
|
+
return;
|
|
412
|
+
if (Object.keys(spec).length > 0)
|
|
413
|
+
props['collapsable'] = false;
|
|
414
|
+
}
|
|
415
|
+
function firstInFlow(node) {
|
|
416
|
+
for (const child of node.children ?? []) {
|
|
417
|
+
// Not the paragraph itself, which holds the words it was made around.
|
|
418
|
+
if (child.kind === 'text' && child.box)
|
|
419
|
+
return child.box === node ? undefined : child.box;
|
|
420
|
+
if (child.kind !== 'element' || ownLayout(child, 'display') === 'none')
|
|
421
|
+
continue;
|
|
422
|
+
if (ownLayout(child, 'position') !== 'absolute')
|
|
423
|
+
return child;
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
const isNode = (node) => node !== undefined;
|
|
428
|
+
/** Whether a row lays a child out among its others, and so can take its baseline from it. */
|
|
429
|
+
const inFlow = (child) => child.kind === 'element' && ownLayout(child, 'position') !== 'absolute';
|
|
430
|
+
/** The text field a row's box takes its baseline from: itself, or the first in its flow down. */
|
|
431
|
+
function fieldOnBaseline(item) {
|
|
432
|
+
let at = item;
|
|
433
|
+
while (at?.kind === 'element' && !TEXT_INPUTS.has(viewNameOf(at)))
|
|
434
|
+
at = firstInFlow(at);
|
|
435
|
+
return at?.kind === 'element' ? at : undefined;
|
|
436
|
+
}
|
|
392
437
|
const textDirection = (value) => value === 'ltr' || value === 'rtl' ? value : undefined;
|
|
393
438
|
/**
|
|
394
439
|
* What iOS scales a text's `lineHeight` by: the system text size, capped by the text's own
|
|
@@ -433,6 +478,57 @@ function rowsTall(props, fontScale) {
|
|
|
433
478
|
const lines = rows * line * textScale(props, fontScale);
|
|
434
479
|
props['height'] = edges.reduce((sum, part) => sum + part, lines);
|
|
435
480
|
}
|
|
481
|
+
/** A field's own minimum height: none where it says `auto`, which a web element starts with. */
|
|
482
|
+
const ownMinimum = (minHeight) => (minHeight === 'auto' ? 0 : (minHeight ?? 0));
|
|
483
|
+
/** How tall the system's font is, as a share of its size: its rise over the baseline and fall under. */
|
|
484
|
+
const FONT_LINE = 0.95 + 0.24;
|
|
485
|
+
/**
|
|
486
|
+
* How tall a field is by its line: the line, padding and border of a border-box field, Yoga's
|
|
487
|
+
* default, and its content alone for a content-box one, which Yoga adds the padding and border
|
|
488
|
+
* to itself. In a row aligned by baseline some of the line is kept as padding: `lineBaseline`.
|
|
489
|
+
*/
|
|
490
|
+
function lineBox(props, box, onBaseline, fontScale) {
|
|
491
|
+
const half = onBaseline ? lineBaseline(props, box[0], box, fontScale) : 0;
|
|
492
|
+
if (props['boxSizing'] === 'content-box')
|
|
493
|
+
return box[0] - 2 * half;
|
|
494
|
+
return box.reduce((sum, part) => sum + part, 0);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* `lineBaseline` for a field a height sizes: the room is what the height leaves its text, and
|
|
498
|
+
* the field stays that height. A content-box field's height is its content's, so what is now
|
|
499
|
+
* padding comes out of it.
|
|
500
|
+
*/
|
|
501
|
+
function heightBaseline(props, height, onBaseline, fontScale) {
|
|
502
|
+
if (!onBaseline)
|
|
503
|
+
return;
|
|
504
|
+
const edges = [...blockEdge(props, 'Top', 'Start'), ...blockEdge(props, 'Bottom', 'End')];
|
|
505
|
+
if (!edges.every((part) => typeof part === 'number'))
|
|
506
|
+
return;
|
|
507
|
+
const content = props['boxSizing'] === 'content-box';
|
|
508
|
+
const room = content ? height : edges.reduce((left, part) => left - part, height);
|
|
509
|
+
const half = lineBaseline(props, room, [room, ...edges], fontScale);
|
|
510
|
+
if (content)
|
|
511
|
+
props['height'] = height - 2 * half;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Give a field with its line height left out the baseline of a paragraph in a line as tall, for
|
|
515
|
+
* a field in a row aligned by baseline: half the room the line has over the font is kept as
|
|
516
|
+
* padding over and under the text. React Native says a field's baseline from its text and its
|
|
517
|
+
* top padding, so without this it is the font's own, and the row lifts the paragraphs beside the
|
|
518
|
+
* field to it: a form field's prefix and suffix sit higher than what is typed. Answers the half.
|
|
519
|
+
*
|
|
520
|
+
* ponytail: the font is taken to be as tall as the system's, `FONT_LINE` of its size. Read from
|
|
521
|
+
* the font if a field in another face is seen a point off its prefix.
|
|
522
|
+
*/
|
|
523
|
+
function lineBaseline(props, line, [, top, , bottom], fontScale) {
|
|
524
|
+
const size = props['fontSize'];
|
|
525
|
+
if (typeof size !== 'number')
|
|
526
|
+
return 0;
|
|
527
|
+
const half = Math.max(0, (line - size * textScale(props, fontScale) * FONT_LINE) / 2);
|
|
528
|
+
props['paddingTop'] = top + half;
|
|
529
|
+
props['paddingBottom'] = bottom + half;
|
|
530
|
+
return half;
|
|
531
|
+
}
|
|
436
532
|
/**
|
|
437
533
|
* Centre the text of a single-line text field that has a line height, as Chrome centres an
|
|
438
534
|
* input's, keeping the height the line height gives it.
|
|
@@ -453,7 +549,7 @@ function rowsTall(props, fontScale) {
|
|
|
453
549
|
* height alone for a content-box one. A null `height` is no height. A value that is not a number
|
|
454
550
|
* cannot be added up, and leaves the field as it was.
|
|
455
551
|
*/
|
|
456
|
-
function centreSingleLine(viewName, props, fontScale) {
|
|
552
|
+
function centreSingleLine(viewName, props, fontScale, onBaseline = false) {
|
|
457
553
|
const lineHeight = props['lineHeight'];
|
|
458
554
|
if (!TEXT_INPUTS.has(viewName) || typeof lineHeight !== 'number')
|
|
459
555
|
return;
|
|
@@ -462,6 +558,7 @@ function centreSingleLine(viewName, props, fontScale) {
|
|
|
462
558
|
const height = props['height'];
|
|
463
559
|
if (typeof height === 'number') {
|
|
464
560
|
delete props['lineHeight'];
|
|
561
|
+
heightBaseline(props, height, onBaseline, fontScale);
|
|
465
562
|
return;
|
|
466
563
|
}
|
|
467
564
|
// A percentage is a fixed height only where the parent's is definite, known at layout alone.
|
|
@@ -473,15 +570,12 @@ function centreSingleLine(viewName, props, fontScale) {
|
|
|
473
570
|
const edge = (side, logical) => blockEdge(props, side, logical);
|
|
474
571
|
const line = lineHeight * textScale(props, fontScale);
|
|
475
572
|
const box = [line, ...edge('Top', 'Start'), ...edge('Bottom', 'End')];
|
|
476
|
-
const own = props['minHeight']
|
|
573
|
+
const own = ownMinimum(props['minHeight']);
|
|
477
574
|
const max = props['maxHeight'] ?? Infinity;
|
|
478
575
|
if (![...box, own, max].every((part) => typeof part === 'number'))
|
|
479
576
|
return;
|
|
480
577
|
delete props['lineHeight'];
|
|
481
|
-
|
|
482
|
-
const content = props['boxSizing'] === 'content-box'
|
|
483
|
-
? line
|
|
484
|
-
: box.reduce((sum, part) => sum + part, 0);
|
|
578
|
+
const content = lineBox(props, box, onBaseline, fontScale);
|
|
485
579
|
props['minHeight'] = Math.max(own, Math.min(content, max));
|
|
486
580
|
}
|
|
487
581
|
const NO_STYLE = {};
|
|
@@ -500,6 +594,44 @@ function inlineOf(node) {
|
|
|
500
594
|
delete inline[key];
|
|
501
595
|
return inline;
|
|
502
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* What a node's `@keyframes` animation sets this frame, less each property a rule it matches
|
|
599
|
+
* declared `!important`: an important author declaration is above the animation in the cascade.
|
|
600
|
+
* A transition is above both, so `transitioned` takes no such care.
|
|
601
|
+
*/
|
|
602
|
+
function playedOver(node) {
|
|
603
|
+
const values = node.playing?.values;
|
|
604
|
+
const important = node.styleCache?.important;
|
|
605
|
+
if (!values || !important)
|
|
606
|
+
return values ?? NO_STYLE;
|
|
607
|
+
const played = { ...values };
|
|
608
|
+
for (const key of important)
|
|
609
|
+
delete played[key];
|
|
610
|
+
return played;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The properties among an animation's tracks that a rule `node` matches declared `!important`,
|
|
614
|
+
* which native is not handed: what native plays it sets on the view, over what the rule says.
|
|
615
|
+
*/
|
|
616
|
+
function pinnedIn(node, tracks) {
|
|
617
|
+
const important = node.styleCache?.important;
|
|
618
|
+
return important ? [...tracks.keys()].filter((key) => important.has(key)) : [];
|
|
619
|
+
}
|
|
620
|
+
/** Whether native is playing an animation whose important properties are no longer the ones left out. */
|
|
621
|
+
function pinsChanged(node, running) {
|
|
622
|
+
if (!running.native)
|
|
623
|
+
return false;
|
|
624
|
+
return pinnedIn(node, running.tracks).join(' ') !== running.pinned?.join(' ');
|
|
625
|
+
}
|
|
626
|
+
/** `tracks` less the properties named. */
|
|
627
|
+
function unpinned(tracks, pinned) {
|
|
628
|
+
if (!pinned.length)
|
|
629
|
+
return tracks;
|
|
630
|
+
const kept = new Map(tracks);
|
|
631
|
+
for (const key of pinned)
|
|
632
|
+
kept.delete(key);
|
|
633
|
+
return kept;
|
|
634
|
+
}
|
|
503
635
|
/**
|
|
504
636
|
* What a container's own style says for `key`, in the order `mergeProps` applies them: a
|
|
505
637
|
* component's override, inline, a prop, its stylesheet, its default.
|
|
@@ -544,6 +676,10 @@ const STRETCHES = new Set([undefined, null, 'auto', 'stretch']);
|
|
|
544
676
|
* kept, since it stops the stretch already.
|
|
545
677
|
*/
|
|
546
678
|
function fitContent(node, props) {
|
|
679
|
+
const capped = {
|
|
680
|
+
row: capAtContent(props, 'width', 'maxWidth'),
|
|
681
|
+
column: capAtContent(props, 'height', 'maxHeight'),
|
|
682
|
+
};
|
|
547
683
|
const width = props['width'] === 'fit-content';
|
|
548
684
|
const height = props['height'] === 'fit-content';
|
|
549
685
|
if (!width && !height)
|
|
@@ -558,11 +694,46 @@ function fitContent(node, props) {
|
|
|
558
694
|
const container = containerOf(parent);
|
|
559
695
|
parent.fitContainer = container;
|
|
560
696
|
const [direction, align] = container.split(' ');
|
|
697
|
+
if (capped[direction === 'row' ? 'row' : 'column'])
|
|
698
|
+
withoutGrowth(props);
|
|
561
699
|
const across = direction === 'row' ? height : width;
|
|
562
700
|
if (across && align === 'stretch' && STRETCHES.has(props['alignSelf'])) {
|
|
563
701
|
props['alignSelf'] = 'flex-start';
|
|
564
702
|
}
|
|
565
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* A box capped at its content along its container's main axis, where it would grow: a browser
|
|
706
|
+
* clamps a growing item by its maximum, and there is no such cap to hand Yoga. So it does not
|
|
707
|
+
* grow, and starts from its content, which is as far as the cap would let it get. It may still
|
|
708
|
+
* shrink.
|
|
709
|
+
*/
|
|
710
|
+
function withoutGrowth(props) {
|
|
711
|
+
if (typeof props['flex'] === 'number' && props['flex'] > 0) {
|
|
712
|
+
delete props['flex'];
|
|
713
|
+
props['flexShrink'] ??= 1;
|
|
714
|
+
}
|
|
715
|
+
delete props['flexGrow'];
|
|
716
|
+
delete props['flexBasis'];
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* `max-width: max-content`, and the same of a height: no bigger than its content, which for a
|
|
720
|
+
* box that would fill its container is `fit-content`. Yoga has no such cap. Answers whether the
|
|
721
|
+
* box is the size of its content for it: one that would fill, or one that was `fit-content` as
|
|
722
|
+
* written.
|
|
723
|
+
*
|
|
724
|
+
* ponytail: a size in points is left as it is, since nothing here knows how big the content
|
|
725
|
+
* is. Measure it if a box is ever given both.
|
|
726
|
+
*/
|
|
727
|
+
function capAtContent(props, size, cap) {
|
|
728
|
+
if (props[cap] !== 'max-content')
|
|
729
|
+
return false;
|
|
730
|
+
delete props[cap];
|
|
731
|
+
const own = props[size];
|
|
732
|
+
const fills = own == null || own === 'auto' || (typeof own === 'string' && own.endsWith('%'));
|
|
733
|
+
if (fills)
|
|
734
|
+
props[size] = 'fit-content';
|
|
735
|
+
return fills || own === 'fit-content';
|
|
736
|
+
}
|
|
566
737
|
const isPercent = (value) => typeof value === 'string' && value.endsWith('%');
|
|
567
738
|
/** Whether a `flex-basis` is a length or a percentage of something: not zero, and not `auto`. */
|
|
568
739
|
const isSizedBasis = (basis) => isPercent(basis)
|
|
@@ -1204,6 +1375,69 @@ function repaint(node) {
|
|
|
1204
1375
|
}
|
|
1205
1376
|
/** A sixtieth of a second: how far apart the frames native is given for an animation are. */
|
|
1206
1377
|
const NATIVE_FRAME = 1000 / 60;
|
|
1378
|
+
/**
|
|
1379
|
+
* What native eases a set of transitions on one clock by: a colour by its own channels, opacity
|
|
1380
|
+
* and a transform as an animation of them is. Nothing where any of them is no value native can
|
|
1381
|
+
* ease, which leaves them all to JavaScript.
|
|
1382
|
+
*/
|
|
1383
|
+
function easedChannels(tracks, spec, props) {
|
|
1384
|
+
const tinted = new Map([...tracks].filter(([key]) => EASED_COLOURS.has(key)));
|
|
1385
|
+
const moved = new Map([...tracks].filter(([key]) => !EASED_COLOURS.has(key)));
|
|
1386
|
+
const colors = colourChannels(tinted, spec);
|
|
1387
|
+
const { channels, held } = clockChannels(moved, spec, props);
|
|
1388
|
+
if (!colors || held.length)
|
|
1389
|
+
return null;
|
|
1390
|
+
const any = channels.opacity || channels.transform.length || tinted.size;
|
|
1391
|
+
return any ? { ...channels, colors } : null;
|
|
1392
|
+
}
|
|
1393
|
+
/** The sides a length is written for, and the two it is written for at once. */
|
|
1394
|
+
const EDGES = ['', 'Top', 'Right', 'Bottom', 'Left', 'Horizontal', 'Vertical'];
|
|
1395
|
+
/**
|
|
1396
|
+
* The properties of a transition that resize or move a box, which native eases by laying the
|
|
1397
|
+
* view out where it ends: see `layEased`.
|
|
1398
|
+
*/
|
|
1399
|
+
const LAID_OUT = new Set([
|
|
1400
|
+
...['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'],
|
|
1401
|
+
...['top', 'right', 'bottom', 'left', 'flexBasis'],
|
|
1402
|
+
...EDGES.map((edge) => `margin${edge}`),
|
|
1403
|
+
]);
|
|
1404
|
+
/** Every prop that can resize or move a box, a view's own or another's. */
|
|
1405
|
+
const MOVES_BOXES = new Set([
|
|
1406
|
+
...LAID_OUT,
|
|
1407
|
+
...EDGES.map((edge) => `padding${edge}`),
|
|
1408
|
+
...['borderWidth', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
|
|
1409
|
+
...['flex', 'flexGrow', 'flexShrink', 'flexDirection', 'flexWrap', 'alignItems', 'alignSelf'],
|
|
1410
|
+
...['alignContent', 'justifyContent', 'gap', 'rowGap', 'columnGap', 'display', 'position'],
|
|
1411
|
+
...['aspectRatio', 'fontSize', 'lineHeight', 'fontFamily', 'fontWeight', 'letterSpacing'],
|
|
1412
|
+
...['numberOfLines', 'start', 'end'],
|
|
1413
|
+
]);
|
|
1414
|
+
/** The curves a layout animation has, as the timing function each is. */
|
|
1415
|
+
const LAYOUT_CURVES = [
|
|
1416
|
+
['linear', [0, 0, 1, 1]],
|
|
1417
|
+
['easeIn', [0.42, 0, 1, 1]],
|
|
1418
|
+
['easeOut', [0, 0, 0.58, 1]],
|
|
1419
|
+
['easeInEaseOut', [0.42, 0, 0.58, 1]],
|
|
1420
|
+
];
|
|
1421
|
+
/** The curve of a layout animation nearest a timing function, by where each is along the way. */
|
|
1422
|
+
function layoutCurve(easing) {
|
|
1423
|
+
const along = [0.2, 0.4, 0.6, 0.8];
|
|
1424
|
+
const apart = (curve) => along.reduce((sum, at) => sum + (bezier(curve, at) - bezier(easing, at)) ** 2, 0);
|
|
1425
|
+
let nearest = LAYOUT_CURVES[0];
|
|
1426
|
+
for (const curve of LAYOUT_CURVES)
|
|
1427
|
+
if (apart(curve[1]) < apart(nearest[1]))
|
|
1428
|
+
nearest = curve;
|
|
1429
|
+
return nearest[0];
|
|
1430
|
+
}
|
|
1431
|
+
/** The properties of a transition native can play: see `easeNatively`. */
|
|
1432
|
+
const EASED_COLOURS = new Set([
|
|
1433
|
+
'backgroundColor',
|
|
1434
|
+
'borderColor',
|
|
1435
|
+
'borderTopColor',
|
|
1436
|
+
'borderRightColor',
|
|
1437
|
+
'borderBottomColor',
|
|
1438
|
+
'borderLeftColor',
|
|
1439
|
+
]);
|
|
1440
|
+
const NATIVELY_EASED = new Set(['opacity', 'transform', ...EASED_COLOURS]);
|
|
1207
1441
|
/** The style keys that are a view's background. */
|
|
1208
1442
|
const PAINT_KEYS = [
|
|
1209
1443
|
'backgroundColor',
|
|
@@ -1759,6 +1993,16 @@ const now = () => globalThis.performance?.now?.() ?? Date.now();
|
|
|
1759
1993
|
function sameHandles(a, b) {
|
|
1760
1994
|
return a.length === b.length && a.every((handle, i) => handle === b[i]);
|
|
1761
1995
|
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Whether a node could be the element a compound is written for, by its classes alone: its own,
|
|
1998
|
+
* and those of one alternative of each `:is()` on it, which is where Tailwind names a group or
|
|
1999
|
+
* a peer. A compound with no class in either could be any element.
|
|
2000
|
+
*/
|
|
2001
|
+
function couldBe(compound, node) {
|
|
2002
|
+
if (!compound.classes.every((name) => node.classes?.has(name)))
|
|
2003
|
+
return false;
|
|
2004
|
+
return (compound.is ?? []).every((alternatives) => alternatives.some((option) => couldBe(option, node)));
|
|
2005
|
+
}
|
|
1762
2006
|
const FRAME_BUDGET_MS = 8;
|
|
1763
2007
|
export class Engine {
|
|
1764
2008
|
/** Scopes the CSS resolver's memo. Bumped once per commit. */
|
|
@@ -1959,9 +2203,7 @@ export class Engine {
|
|
|
1959
2203
|
const { own, elsewhere } = this.activeUse;
|
|
1960
2204
|
if (!own && !elsewhere.length)
|
|
1961
2205
|
return;
|
|
1962
|
-
|
|
1963
|
-
const asked = elsewhere.some((compound) => compound.classes.every((name) => node.classes?.has(name)));
|
|
1964
|
-
if (asked)
|
|
2206
|
+
if (elsewhere.some((compound) => couldBe(compound, node)))
|
|
1965
2207
|
return this.markProps(node);
|
|
1966
2208
|
node.stateDirty = true;
|
|
1967
2209
|
this.markPath(node.parent ?? node);
|
|
@@ -2456,6 +2698,7 @@ export class Engine {
|
|
|
2456
2698
|
// as. Native gives a view one parent for its life, and aborts on a second.
|
|
2457
2699
|
if (node.committed)
|
|
2458
2700
|
this.forgetCommitted(node);
|
|
2701
|
+
this.stopEased(node);
|
|
2459
2702
|
node.transitions = undefined;
|
|
2460
2703
|
if (node.playing)
|
|
2461
2704
|
this.stopNative(node, node.playing);
|
|
@@ -2501,7 +2744,10 @@ export class Engine {
|
|
|
2501
2744
|
// inline `direction` is the exception, because the paragraphs below it align by it.
|
|
2502
2745
|
// And a child that takes one of this node's own values, an `inherit`, follows it.
|
|
2503
2746
|
const heirs = node.styleCache?.heirs === true;
|
|
2504
|
-
|
|
2747
|
+
// And only a prop some selector names: `aria-valuenow` on every tick of a progress bar, a
|
|
2748
|
+
// field's text on every key, match nothing they did not.
|
|
2749
|
+
const read = key === 'style' ? this.inlineReachesStyle(node) : this.styles.reads(key);
|
|
2750
|
+
this.markProps(node, read || heirs);
|
|
2505
2751
|
}
|
|
2506
2752
|
/**
|
|
2507
2753
|
* Paint a node's background on one of its children instead of on the node: the node keeps its
|
|
@@ -2811,11 +3057,15 @@ export class Engine {
|
|
|
2811
3057
|
// Faces registered before the walk, the global sheet's among them, reach every node in it.
|
|
2812
3058
|
this.facesAdded = false;
|
|
2813
3059
|
const set = this.fabric.createChildSet(this.rootTag);
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
3060
|
+
let handles = this.rootHandles();
|
|
3061
|
+
// Again, where transitions it laid out where they end are to be eased from JavaScript.
|
|
3062
|
+
if (this.layOut()) {
|
|
3063
|
+
handles = this.rootHandles();
|
|
3064
|
+
// What that found resized is the transitions themselves, at the frame they are eased to.
|
|
3065
|
+
this.movesOthers = false;
|
|
2818
3066
|
}
|
|
3067
|
+
for (const handle of handles)
|
|
3068
|
+
this.fabric.appendChildToSet(set, handle);
|
|
2819
3069
|
this.clearFlags(this.root);
|
|
2820
3070
|
this.announceCommit();
|
|
2821
3071
|
this.fabric.completeRoot(this.rootTag, set);
|
|
@@ -3054,7 +3304,11 @@ export class Engine {
|
|
|
3054
3304
|
alignMultiline(viewName, style);
|
|
3055
3305
|
rowsTall(style, this.fontScale);
|
|
3056
3306
|
const merged = composeTransform(node, this.animated(node, this.transitioned(node, style)));
|
|
3057
|
-
|
|
3307
|
+
// On iOS: Android's field says the baseline of the line it is sized and centred by.
|
|
3308
|
+
// Still under the row that marked it: a row taken away is not committed again to unmark it.
|
|
3309
|
+
const row = node.onBaseline;
|
|
3310
|
+
const onBaseline = viewName === 'TextInput' && row !== undefined && isWithin(node, row);
|
|
3311
|
+
centreSingleLine(viewName, merged, this.fontScale, onBaseline);
|
|
3058
3312
|
// After a transition, which eases the basis as the basis it was written as.
|
|
3059
3313
|
basisAsSize(node, merged);
|
|
3060
3314
|
// After the basis, which is a size given where it is committed as one.
|
|
@@ -3298,7 +3552,7 @@ export class Engine {
|
|
|
3298
3552
|
if (spec.timeline)
|
|
3299
3553
|
return this.scrollAnimated(node, spec, frames, props);
|
|
3300
3554
|
this.startPlaying(node, spec, frames, props);
|
|
3301
|
-
return Object.assign(props, node
|
|
3555
|
+
return Object.assign(props, playedOver(node));
|
|
3302
3556
|
}
|
|
3303
3557
|
/**
|
|
3304
3558
|
* A clock animation the scroll plays instead: the same animation where only the timeline moved,
|
|
@@ -3324,7 +3578,7 @@ export class Engine {
|
|
|
3324
3578
|
if (running && !running.done)
|
|
3325
3579
|
this.emitTransition(node, 'topAnimationcancel', running.spec.name);
|
|
3326
3580
|
}
|
|
3327
|
-
/** Stop on native each animation whose view has left the tree. */
|
|
3581
|
+
/** Stop on native each animation and transition whose view has left the tree. */
|
|
3328
3582
|
releasePlayedNatively() {
|
|
3329
3583
|
for (const node of [...this.playedNatively]) {
|
|
3330
3584
|
if (this.topOf(node) === this.root)
|
|
@@ -3333,6 +3587,10 @@ export class Engine {
|
|
|
3333
3587
|
node.playing = undefined;
|
|
3334
3588
|
this.markProps(node, false);
|
|
3335
3589
|
}
|
|
3590
|
+
for (const node of [...this.easedNatively]) {
|
|
3591
|
+
if (this.topOf(node) !== this.root)
|
|
3592
|
+
this.stopEased(node);
|
|
3593
|
+
}
|
|
3336
3594
|
}
|
|
3337
3595
|
/** Every node whose animation native is playing, with no frame of it in JavaScript. */
|
|
3338
3596
|
playedNatively = new Set();
|
|
@@ -3404,7 +3662,9 @@ export class Engine {
|
|
|
3404
3662
|
const sized = this.scrollDriver ? this.sized(node, running, props) : null;
|
|
3405
3663
|
if (!this.scrollDriver || !sized)
|
|
3406
3664
|
return false;
|
|
3407
|
-
const
|
|
3665
|
+
const pinned = pinnedIn(node, sized.tracks);
|
|
3666
|
+
const tracks = unpinned(sized.tracks, pinned);
|
|
3667
|
+
const { channels, held, span } = clockChannels(tracks, spec, sized.resting);
|
|
3408
3668
|
if (held.length || !(channels.opacity || channels.transform.length))
|
|
3409
3669
|
return false;
|
|
3410
3670
|
// There and back is one native animation: an odd number of ways has no whole number of them.
|
|
@@ -3417,6 +3677,7 @@ export class Engine {
|
|
|
3417
3677
|
if (!native)
|
|
3418
3678
|
return false;
|
|
3419
3679
|
running.native = native;
|
|
3680
|
+
running.pinned = pinned;
|
|
3420
3681
|
// As Animated does: Fabric flattens a view that only lays out, and then there is no native
|
|
3421
3682
|
// view for the animation to move.
|
|
3422
3683
|
running.values = { ...running.values, collapsable: false };
|
|
@@ -3493,8 +3754,9 @@ export class Engine {
|
|
|
3493
3754
|
this.emitTransition(node, 'topAnimationstart', spec.name);
|
|
3494
3755
|
return;
|
|
3495
3756
|
}
|
|
3496
|
-
// A view made again is another view: the one native was moving is gone.
|
|
3497
|
-
|
|
3757
|
+
// A view made again is another view: the one native was moving is gone. And one whose
|
|
3758
|
+
// important declarations changed is moving what it now must not, or not what it now may.
|
|
3759
|
+
if (node.committed === null || pinsChanged(node, current))
|
|
3498
3760
|
this.backToScript(node, current);
|
|
3499
3761
|
if (this.playedFrames.get(current) !== frames || inherited !== current.inherited) {
|
|
3500
3762
|
this.backToScript(node, current);
|
|
@@ -3564,25 +3826,28 @@ export class Engine {
|
|
|
3564
3826
|
scrollAnimated(node, spec, frames, props) {
|
|
3565
3827
|
const current = node.scrolled;
|
|
3566
3828
|
const inherited = this.inheritedColour(node, frames);
|
|
3567
|
-
|
|
3829
|
+
const same = current?.frames === frames && sameAnimation(current.spec, spec);
|
|
3830
|
+
if (same && pinnedIn(node, current.tracks).join(' ') === current.pinned.join(' ')) {
|
|
3568
3831
|
return Object.assign(props, this.rescrolled(node, current, frames, props, inherited));
|
|
3569
3832
|
}
|
|
3570
3833
|
this.stopScrolled(node);
|
|
3571
3834
|
this.handOverToScroll(node, spec);
|
|
3572
3835
|
const tracks = tracksOf(frames, props, inherited);
|
|
3836
|
+
const pinned = pinnedIn(node, tracks);
|
|
3837
|
+
const played = unpinned(tracks, pinned);
|
|
3573
3838
|
const source = this.scrollSourceOf(node);
|
|
3574
3839
|
const extent = source ? (this.scrollExtents.get(source)?.[spec.timeline] ?? null) : null;
|
|
3575
3840
|
const range = rangeOf(spec, extent);
|
|
3576
|
-
const first = firstFrame(
|
|
3841
|
+
const first = firstFrame(played, spec, range);
|
|
3577
3842
|
const resting = { ...props };
|
|
3578
3843
|
if (!source && this.dev)
|
|
3579
3844
|
this.reportUnscrolled(spec.name);
|
|
3580
|
-
const drive = source ? this.driveScrolled(node, source, spec,
|
|
3845
|
+
const drive = source ? this.driveScrolled(node, source, spec, played, resting, range) : null;
|
|
3581
3846
|
// As Animated does: Fabric flattens a view that only lays out, and then there is no native
|
|
3582
3847
|
// view for the animation to move. Committed with the first frame, so it stays put.
|
|
3583
3848
|
if (drive)
|
|
3584
3849
|
first['collapsable'] = false;
|
|
3585
|
-
node.scrolled = { spec, frames, tracks, resting, first, source, drive, inherited };
|
|
3850
|
+
node.scrolled = { spec, frames, tracks, pinned, resting, first, source, drive, inherited };
|
|
3586
3851
|
return Object.assign(props, first);
|
|
3587
3852
|
}
|
|
3588
3853
|
/**
|
|
@@ -3659,7 +3924,7 @@ export class Engine {
|
|
|
3659
3924
|
if (!scrolled?.drive)
|
|
3660
3925
|
continue;
|
|
3661
3926
|
const range = rangeOf(scrolled.spec, extent[scrolled.spec.timeline]);
|
|
3662
|
-
scrolled.drive.update(scrollChannels(scrolled.tracks, scrolled.spec, scrolled.resting, range).channels);
|
|
3927
|
+
scrolled.drive.update(scrollChannels(unpinned(scrolled.tracks, scrolled.pinned), scrolled.spec, scrolled.resting, range).channels);
|
|
3663
3928
|
}
|
|
3664
3929
|
}
|
|
3665
3930
|
reportUnscrolled(name) {
|
|
@@ -3685,29 +3950,290 @@ export class Engine {
|
|
|
3685
3950
|
`has loaded, so the block has to exist somewhere by the time the animation starts.`);
|
|
3686
3951
|
}
|
|
3687
3952
|
transitioned(node, props) {
|
|
3688
|
-
|
|
3689
|
-
if (node.bornIn >= this.unseen)
|
|
3690
|
-
node.transitions = undefined;
|
|
3953
|
+
this.unseenTransitions(node);
|
|
3691
3954
|
const spec = transitionSpec(props);
|
|
3692
3955
|
if (!spec && !node.transitions)
|
|
3693
3956
|
return props;
|
|
3957
|
+
if (spec)
|
|
3958
|
+
keepForTransition(node, props, spec);
|
|
3694
3959
|
const state = (node.transitions ??= new Map());
|
|
3695
3960
|
const now = this.now();
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
const named = spec ? Object.keys(spec).filter((key) => key !== 'all') : [];
|
|
3701
|
-
for (const key of new Set([...Object.keys(props), ...named])) {
|
|
3961
|
+
this.takeBackEased(node, state, props, spec, now);
|
|
3962
|
+
this.takeBackLaid(state, props, spec, now);
|
|
3963
|
+
const started = [];
|
|
3964
|
+
for (const key of steppedKeys(props, spec, state)) {
|
|
3702
3965
|
if (step(state, key, props, spec?.[key] ?? spec?.['all'], now)) {
|
|
3703
3966
|
this.running.add(node);
|
|
3967
|
+
started.push(key);
|
|
3704
3968
|
this.emitTransition(node, 'topTransitionstart', key);
|
|
3705
3969
|
}
|
|
3706
3970
|
}
|
|
3971
|
+
this.easeNatively(node, state, started, props, now);
|
|
3707
3972
|
if (state.size === 0)
|
|
3708
3973
|
node.transitions = undefined;
|
|
3709
3974
|
return props;
|
|
3710
3975
|
}
|
|
3976
|
+
/** Nobody saw what the last commit gave a view it created, so this is where the view starts. */
|
|
3977
|
+
unseenTransitions(node) {
|
|
3978
|
+
if (node.bornIn < this.unseen)
|
|
3979
|
+
return;
|
|
3980
|
+
this.stopEased(node);
|
|
3981
|
+
node.transitions = undefined;
|
|
3982
|
+
}
|
|
3983
|
+
/** The elements native is playing a transition of. */
|
|
3984
|
+
easedNatively = new Set();
|
|
3985
|
+
/**
|
|
3986
|
+
* Hand to native each transition that just started and that it can play: opacity and
|
|
3987
|
+
* transforms, with no wait before they start. Those that start together, take as long and ease
|
|
3988
|
+
* alike share a clock. Nothing of them runs in JavaScript until native says they have ended:
|
|
3989
|
+
* a press that fades or slides something is a commit to start it and a commit to end it.
|
|
3990
|
+
*
|
|
3991
|
+
* ponytail: a colour is eased from JavaScript still, a commit a frame, and so is anything
|
|
3992
|
+
* that waits. Native has a colour node for the first; give the driver a channel for it.
|
|
3993
|
+
*/
|
|
3994
|
+
easeNatively(node, state, started, props, now) {
|
|
3995
|
+
if (started.length)
|
|
3996
|
+
this.startEased(node, state, started, props, now);
|
|
3997
|
+
this.layEased(node, state, started, props, now);
|
|
3998
|
+
const inScript = [...state.values()].some((eased) => !eased.done && !eased.native && !eased.laid);
|
|
3999
|
+
if (!inScript)
|
|
4000
|
+
this.running.delete(node);
|
|
4001
|
+
// As Animated does: Fabric flattens a view that only lays out, and then there is no native
|
|
4002
|
+
// view for a transition to move.
|
|
4003
|
+
if (this.easedNatively.has(node))
|
|
4004
|
+
props['collapsable'] = false;
|
|
4005
|
+
}
|
|
4006
|
+
/** The root's children as the views they are committed as, each reconciled. */
|
|
4007
|
+
rootHandles() {
|
|
4008
|
+
const handles = [];
|
|
4009
|
+
for (const child of this.visibleChildren(this.root)) {
|
|
4010
|
+
if (!this.withheld(child))
|
|
4011
|
+
handles.push(this.reconcileUnder(this.root, child));
|
|
4012
|
+
}
|
|
4013
|
+
return handles;
|
|
4014
|
+
}
|
|
4015
|
+
/** The transitions this commit lays out where they end, for native to move the views to. */
|
|
4016
|
+
laying = [];
|
|
4017
|
+
/** Whether this commit resizes or moves a box that is in no such transition. */
|
|
4018
|
+
movesOthers = false;
|
|
4019
|
+
/**
|
|
4020
|
+
* Commit where it ends each transition of a size or a place that just started, for native to
|
|
4021
|
+
* move the view there: a layout animation, which the commit asks for once it is known that
|
|
4022
|
+
* nothing else in it moves. One still on its way is committed where it ends too.
|
|
4023
|
+
*
|
|
4024
|
+
* A width eased from JavaScript is a commit a frame, and a commit lays the whole screen out.
|
|
4025
|
+
*/
|
|
4026
|
+
layEased(node, state, started, props, now) {
|
|
4027
|
+
if (!this.fabric.configureNextLayoutAnimation)
|
|
4028
|
+
return;
|
|
4029
|
+
for (const key of started) {
|
|
4030
|
+
const eased = state.get(key);
|
|
4031
|
+
if (!LAID_OUT.has(key) || eased.start > now || eased.duration <= 0)
|
|
4032
|
+
continue;
|
|
4033
|
+
eased.laid = true;
|
|
4034
|
+
this.laying.push({ node, key, eased });
|
|
4035
|
+
}
|
|
4036
|
+
for (const [key, eased] of state)
|
|
4037
|
+
if (eased.laid && !eased.done)
|
|
4038
|
+
props[key] = eased.to;
|
|
4039
|
+
}
|
|
4040
|
+
/**
|
|
4041
|
+
* Before a commit is handed over: ask native to move what it lays out, or take the
|
|
4042
|
+
* transitions back to ease them from JavaScript where the commit moves anything else, and
|
|
4043
|
+
* where they would not all take as long. Answers whether the tree has to be reconciled again.
|
|
4044
|
+
*/
|
|
4045
|
+
layOut() {
|
|
4046
|
+
const laying = this.laying;
|
|
4047
|
+
const others = this.movesOthers;
|
|
4048
|
+
this.laying = [];
|
|
4049
|
+
this.movesOthers = false;
|
|
4050
|
+
if (!laying.length)
|
|
4051
|
+
return false;
|
|
4052
|
+
const [first] = laying;
|
|
4053
|
+
const alike = laying.every(({ eased }) => eased.duration === first.eased.duration);
|
|
4054
|
+
if (others || !alike) {
|
|
4055
|
+
for (const { node, eased } of laying) {
|
|
4056
|
+
eased.laid = undefined;
|
|
4057
|
+
this.running.add(node);
|
|
4058
|
+
}
|
|
4059
|
+
for (const { node } of laying)
|
|
4060
|
+
this.markProps(node, false);
|
|
4061
|
+
return true;
|
|
4062
|
+
}
|
|
4063
|
+
const { duration, easing } = first.eased;
|
|
4064
|
+
const nothing = () => undefined;
|
|
4065
|
+
this.fabric.configureNextLayoutAnimation({ duration, update: { type: layoutCurve(easing) } }, nothing, nothing);
|
|
4066
|
+
for (const { node, key, eased } of laying) {
|
|
4067
|
+
setTimeout(() => this.endLaid(node, key, eased), duration);
|
|
4068
|
+
}
|
|
4069
|
+
return false;
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Note a commit that resizes or moves a box in no transition native lays out: a prop of one
|
|
4073
|
+
* that moves boxes, children that came or went, text longer or shorter than it was.
|
|
4074
|
+
*/
|
|
4075
|
+
noteMoved(node, payload, resized, words) {
|
|
4076
|
+
if (this.movesOthers)
|
|
4077
|
+
return;
|
|
4078
|
+
if (resized)
|
|
4079
|
+
this.movesOthers = true;
|
|
4080
|
+
for (const key in payload) {
|
|
4081
|
+
const eased = node.transitions?.get(key);
|
|
4082
|
+
if (eased?.laid && !eased.done)
|
|
4083
|
+
continue;
|
|
4084
|
+
// Text as long as it was is taken to be as wide, as a grid takes it: see `shapeOf`.
|
|
4085
|
+
const longer = key === 'text' && String(payload[key]).length !== String(words).length;
|
|
4086
|
+
if (MOVES_BOXES.has(key) || longer)
|
|
4087
|
+
this.movesOthers = true;
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
/** A transition native laid out has had its time: it is where it ends, and says so. */
|
|
4091
|
+
endLaid(node, key, eased) {
|
|
4092
|
+
if (!eased.laid || eased.done || node.transitions?.get(key) !== eased)
|
|
4093
|
+
return;
|
|
4094
|
+
Object.assign(eased, { current: eased.to, done: true, laid: undefined });
|
|
4095
|
+
this.emitTransition(node, 'topTransitionend', key);
|
|
4096
|
+
// No commit comes of it, which is what tells listeners otherwise: it is where it ends.
|
|
4097
|
+
this.flushTransitionEvents();
|
|
4098
|
+
// And what a listener did, taking away an element that has left, is committed.
|
|
4099
|
+
if (this.pending)
|
|
4100
|
+
this.commit();
|
|
4101
|
+
}
|
|
4102
|
+
/**
|
|
4103
|
+
* Bring a transition native laid out to where its clock has got to, where it is sent another
|
|
4104
|
+
* way or its rule has gone: that is where it goes on from.
|
|
4105
|
+
*/
|
|
4106
|
+
takeBackLaid(state, props, spec, now) {
|
|
4107
|
+
for (const [key, eased] of state) {
|
|
4108
|
+
if (!eased.laid || eased.done)
|
|
4109
|
+
continue;
|
|
4110
|
+
const kept = spec?.[key] ?? spec?.['all'];
|
|
4111
|
+
if (kept && !aimsElsewhere(eased, key, props))
|
|
4112
|
+
continue;
|
|
4113
|
+
const progress = Math.min(1, Math.max(0, (now - eased.start) / eased.duration));
|
|
4114
|
+
eased.current = interpolate(eased.from, eased.to, bezier(eased.easing, progress));
|
|
4115
|
+
eased.laid = undefined;
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
startEased(node, state, started, props, now) {
|
|
4119
|
+
if (!this.scrollDriver)
|
|
4120
|
+
return;
|
|
4121
|
+
const clocks = new Map();
|
|
4122
|
+
for (const key of started) {
|
|
4123
|
+
const eased = state.get(key);
|
|
4124
|
+
if (!NATIVELY_EASED.has(key) || eased.start > now || eased.duration <= 0)
|
|
4125
|
+
continue;
|
|
4126
|
+
const clock = `${eased.duration} ${eased.easing.join()}`;
|
|
4127
|
+
clocks.set(clock, [...(clocks.get(clock) ?? []), key]);
|
|
4128
|
+
}
|
|
4129
|
+
for (const keys of clocks.values())
|
|
4130
|
+
this.playEased(node, state, keys, props);
|
|
4131
|
+
}
|
|
4132
|
+
playEased(node, state, keys, props) {
|
|
4133
|
+
const first = state.get(keys[0]);
|
|
4134
|
+
const tracks = new Map(keys.map((key) => {
|
|
4135
|
+
const { current, to, easing } = state.get(key);
|
|
4136
|
+
return [
|
|
4137
|
+
key,
|
|
4138
|
+
[
|
|
4139
|
+
{ offset: 0, value: current, easing },
|
|
4140
|
+
{ offset: 1, value: to },
|
|
4141
|
+
],
|
|
4142
|
+
];
|
|
4143
|
+
}));
|
|
4144
|
+
const spec = {
|
|
4145
|
+
name: '',
|
|
4146
|
+
duration: first.duration,
|
|
4147
|
+
delay: 0,
|
|
4148
|
+
easing: first.easing,
|
|
4149
|
+
iterations: 1,
|
|
4150
|
+
fill: 'both',
|
|
4151
|
+
};
|
|
4152
|
+
const channels = easedChannels(tracks, spec, props);
|
|
4153
|
+
if (!channels)
|
|
4154
|
+
return;
|
|
4155
|
+
const length = Math.max(2, Math.round(first.duration / NATIVE_FRAME) + 1);
|
|
4156
|
+
const frames = Array.from({ length }, (_, i) => i / (length - 1));
|
|
4157
|
+
const clock = { keys, stop: () => undefined };
|
|
4158
|
+
const native = this.scrollDriver.play(node, channels, { frames, toValue: 1, iterations: 1 }, (view) => this.tagOf(view), () => this.endedEased(node, clock));
|
|
4159
|
+
if (!native)
|
|
4160
|
+
return;
|
|
4161
|
+
clock.stop = () => native.stop();
|
|
4162
|
+
for (const key of keys)
|
|
4163
|
+
state.get(key).native = clock;
|
|
4164
|
+
this.easedNatively.add(node);
|
|
4165
|
+
}
|
|
4166
|
+
/**
|
|
4167
|
+
* Native reached the end of the transitions on one clock. Where they end is committed before
|
|
4168
|
+
* the view is let go, so nothing shows of where they started, which is what it was committed
|
|
4169
|
+
* with.
|
|
4170
|
+
*/
|
|
4171
|
+
endedEased(node, clock) {
|
|
4172
|
+
const state = node.transitions;
|
|
4173
|
+
const keys = clock.keys.filter((key) => state?.get(key)?.native === clock);
|
|
4174
|
+
for (const key of keys) {
|
|
4175
|
+
const eased = state.get(key);
|
|
4176
|
+
Object.assign(eased, { current: eased.to, done: true, native: undefined });
|
|
4177
|
+
this.emitTransition(node, 'topTransitionend', key);
|
|
4178
|
+
this.markProps(node, false);
|
|
4179
|
+
}
|
|
4180
|
+
this.forgetEased(node);
|
|
4181
|
+
if (keys.length)
|
|
4182
|
+
this.commit();
|
|
4183
|
+
clock.stop();
|
|
4184
|
+
}
|
|
4185
|
+
/** No longer one native is easing, once none of its transitions is native's. */
|
|
4186
|
+
forgetEased(node) {
|
|
4187
|
+
for (const eased of node.transitions?.values() ?? [])
|
|
4188
|
+
if (eased.native)
|
|
4189
|
+
return;
|
|
4190
|
+
this.easedNatively.delete(node);
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* Take back from native each transition it can no longer play as it was started: one sent
|
|
4194
|
+
* another way, one whose rule has gone, any of a view that was made again. Each is brought to
|
|
4195
|
+
* where its clock has got to, which is where it goes on from, here or on native again.
|
|
4196
|
+
*/
|
|
4197
|
+
takeBackEased(node, state, props, spec, now) {
|
|
4198
|
+
if (!this.easedNatively.has(node))
|
|
4199
|
+
return;
|
|
4200
|
+
const remade = node.committed === null;
|
|
4201
|
+
for (const [key, eased] of state) {
|
|
4202
|
+
if (!eased.native)
|
|
4203
|
+
continue;
|
|
4204
|
+
const kept = spec?.[key] ?? spec?.['all'];
|
|
4205
|
+
if (remade || !kept || aimsElsewhere(eased, key, props)) {
|
|
4206
|
+
this.backToScriptEased(node, eased.native, now);
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
/** Stop a clock native runs, and go on from JavaScript with each transition it was playing. */
|
|
4211
|
+
backToScriptEased(node, clock, now) {
|
|
4212
|
+
for (const key of clock.keys) {
|
|
4213
|
+
const eased = node.transitions?.get(key);
|
|
4214
|
+
if (eased?.native !== clock)
|
|
4215
|
+
continue;
|
|
4216
|
+
eased.native = undefined;
|
|
4217
|
+
const progress = Math.min(1, Math.max(0, (now - eased.start) / eased.duration));
|
|
4218
|
+
eased.current = interpolate(eased.from, eased.to, bezier(eased.easing, progress));
|
|
4219
|
+
this.running.add(node);
|
|
4220
|
+
}
|
|
4221
|
+
clock.stop();
|
|
4222
|
+
this.forgetEased(node);
|
|
4223
|
+
}
|
|
4224
|
+
/** Let go of what native is easing on an element that is going, or gone. */
|
|
4225
|
+
stopEased(node) {
|
|
4226
|
+
if (!this.easedNatively.delete(node))
|
|
4227
|
+
return;
|
|
4228
|
+
const clocks = new Set();
|
|
4229
|
+
for (const eased of node.transitions?.values() ?? []) {
|
|
4230
|
+
if (eased.native)
|
|
4231
|
+
clocks.add(eased.native);
|
|
4232
|
+
eased.native = undefined;
|
|
4233
|
+
}
|
|
4234
|
+
for (const clock of clocks)
|
|
4235
|
+
clock.stop();
|
|
4236
|
+
}
|
|
3711
4237
|
/**
|
|
3712
4238
|
* Whether an animation that has finished goes on holding its last frame. With a forwards fill
|
|
3713
4239
|
* it does, for good: without one the element falls back to whatever the cascade gives it, and
|
|
@@ -3867,6 +4393,58 @@ export class Engine {
|
|
|
3867
4393
|
};
|
|
3868
4394
|
});
|
|
3869
4395
|
}
|
|
4396
|
+
/**
|
|
4397
|
+
* Mark the text field each box of a row aligned by baseline takes its baseline from, before
|
|
4398
|
+
* the field is merged, and unmark it when the row is aligned another way.
|
|
4399
|
+
*/
|
|
4400
|
+
noteBaselineFields(row) {
|
|
4401
|
+
const aligned = ownLayout(row, 'alignItems') === 'baseline';
|
|
4402
|
+
const before = row.baselineFields;
|
|
4403
|
+
if (!aligned && !before)
|
|
4404
|
+
return;
|
|
4405
|
+
const now = new Set(aligned ? row.children.filter(inFlow).map(fieldOnBaseline).filter(isNode) : []);
|
|
4406
|
+
// One that is no longer this row's, and that no other row has marked since.
|
|
4407
|
+
for (const field of before ?? []) {
|
|
4408
|
+
if (!now.has(field) && field.onBaseline === row)
|
|
4409
|
+
this.onBaselineOf(field, undefined);
|
|
4410
|
+
}
|
|
4411
|
+
for (const field of now)
|
|
4412
|
+
if (field.onBaseline !== row)
|
|
4413
|
+
this.onBaselineOf(field, row);
|
|
4414
|
+
row.baselineFields = now.size ? now : undefined;
|
|
4415
|
+
}
|
|
4416
|
+
onBaselineOf(field, row) {
|
|
4417
|
+
field.onBaseline = row;
|
|
4418
|
+
this.markProps(field, false);
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Have the text a row aligned by baseline takes a baseline from measured again, where that
|
|
4422
|
+
* text is inside one of the row's boxes and the row is committed again. React Native keeps
|
|
4423
|
+
* what a paragraph measured on the one copy of its node that was measured, and Yoga copies a
|
|
4424
|
+
* node it lays out around: a row that is laid out again asks a box for its baseline, the box
|
|
4425
|
+
* asks the paragraph in it, and a copy that was never measured works it out then, on a node
|
|
4426
|
+
* that is by then not to be changed. A debug build stops there. Committed to be measured
|
|
4427
|
+
* again (`remeasure`), the paragraph is measured in this commit and has the answer.
|
|
4428
|
+
*
|
|
4429
|
+
* ponytail: the first box in the flow at each level, which is the one Yoga asks unless
|
|
4430
|
+
* another is aligned by baseline itself. A row a commit lays out again without the engine
|
|
4431
|
+
* cloning it, as its room changing does, is not reached: clone on a layout event if one is
|
|
4432
|
+
* seen to stop there.
|
|
4433
|
+
*/
|
|
4434
|
+
freshBaselines(row) {
|
|
4435
|
+
if (!row.committed || ownLayout(row, 'alignItems') !== 'baseline')
|
|
4436
|
+
return;
|
|
4437
|
+
for (const item of row.children) {
|
|
4438
|
+
let at = firstInFlow(item);
|
|
4439
|
+
while (at?.committed && committedViewName(at) !== PARAGRAPH)
|
|
4440
|
+
at = firstInFlow(at);
|
|
4441
|
+
if (!at?.committed)
|
|
4442
|
+
continue;
|
|
4443
|
+
at.remeasure = true;
|
|
4444
|
+
for (let up = at; up && up !== row; up = up.parent)
|
|
4445
|
+
up.subtreeDirty = true;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
3870
4448
|
/**
|
|
3871
4449
|
* Copy a list of gradients, running every stop's colour through the host's converter.
|
|
3872
4450
|
*
|
|
@@ -3910,6 +4488,8 @@ export class Engine {
|
|
|
3910
4488
|
this.refitChildren(node);
|
|
3911
4489
|
const viewName = committedViewName(node);
|
|
3912
4490
|
this.forgetRenamed(node, viewName);
|
|
4491
|
+
this.noteBaselineFields(node);
|
|
4492
|
+
this.freshBaselines(node);
|
|
3913
4493
|
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
3914
4494
|
noteTouches(node, style);
|
|
3915
4495
|
const previous = node.committed;
|
|
@@ -3925,6 +4505,7 @@ export class Engine {
|
|
|
3925
4505
|
const props = propsMoved ? this.mergeProps(node, viewName) : previous.props;
|
|
3926
4506
|
const propsPayload = propsMoved ? this.propsPayload(node, previous.props, props) : null;
|
|
3927
4507
|
const childrenChanged = this.childrenMoved(node, childHandles, previous.childHandles);
|
|
4508
|
+
this.noteMoved(node, propsPayload, childHandles.length !== previous.childHandles.length, previous.props['text']);
|
|
3928
4509
|
let handle = previous.handle;
|
|
3929
4510
|
if (childrenChanged || propsPayload) {
|
|
3930
4511
|
handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload), childrenChanged ? childHandles : null);
|