@flemo/core 2.2.0 → 2.2.1

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ /** The two ratios a face's height is built from, per em. */
2
+ export interface FaceRatios {
3
+ ascent: number;
4
+ descent: number;
5
+ }
6
+ /**
7
+ * The face's ascent and descent per em, or null where they cannot be had.
8
+ *
9
+ * Keyed on the font shorthand, so every element wearing the same type shares
10
+ * one answer for the session.
11
+ */
12
+ export declare const faceRatios: (font: {
13
+ family: string;
14
+ weight: string | number;
15
+ style: string;
16
+ }) => FaceRatios | null;
17
+ /** The grids an engine is known to snap the two halves of a face height to. */
18
+ export declare const faceGrids: () => number[];
19
+ /**
20
+ * The face height the line box will be built from, at one size.
21
+ *
22
+ * `scale` is the reciprocal of the grid: 1 for whole pixels, the device pixel
23
+ * ratio for device pixels.
24
+ */
25
+ export declare const faceHeightAt: (size: number, ratios: FaceRatios, scale: number) => number;
26
+ /** The two halves of a face's box at one size, as the font reports them. */
27
+ export interface FaceParts {
28
+ ascent: number;
29
+ descent: number;
30
+ }
31
+ /**
32
+ * The face's ascent and descent at one size, as the font reports them.
33
+ *
34
+ * The same numbers the line box is built from, got without laying anything out.
35
+ * The BASELINE sits at the ascent below the inline box's top, so a flight that
36
+ * holds its half-leading still is only half done: the ascent is on the same
37
+ * grid and steps just as often (see morphLine).
38
+ */
39
+ export declare const faceParts: (size: number, font: {
40
+ family: string;
41
+ weight: string | number;
42
+ style: string;
43
+ }, scale: number) => FaceParts | null;
44
+ /** The face's own height at one size: its two halves added up. */
45
+ export declare const faceHeight: (size: number, font: {
46
+ family: string;
47
+ weight: string | number;
48
+ style: string;
49
+ }, scale: number) => number | null;
50
+ /**
51
+ * Every size in `[from, to]` at which the face height MIGHT change.
52
+ *
53
+ * Arithmetic only, and deliberately: each half of the height steps where
54
+ * `size * ratio` crosses a half of the grid, which gives the candidates. Where
55
+ * each one really falls is not settled here, because the thing that has to be
56
+ * exact is the TIME the flight meets it, and only the caller knows the curve
57
+ * that maps one to the other (see morphLine).
58
+ */
59
+ export declare const faceAims: (from: number, to: number, ratios: FaceRatios, scale: number) => number[];
60
+ /** Whether two faces are the same height, ascent and descent alike. */
61
+ export declare const sameFace: (a: FaceParts, b: FaceParts) => boolean;
@@ -54,8 +54,56 @@ export interface MorphSnapshot {
54
54
  * complete as the last thing someone noticed on glass.
55
55
  */
56
56
  paint: Record<string, string>;
57
+ /**
58
+ * Whether this end is ONE LINE of text.
59
+ *
60
+ * A flight animates the element's box, and the element in flight is the
61
+ * ARRIVAL's tree — so it re-wraps at every width on the way, under the
62
+ * arrival's own line-breaking rules rather than the departure's. Where both
63
+ * ends are single lines that is always wrong: the label was one line, the
64
+ * heading is one line, and the flight puts a second one in between. Measured
65
+ * on the playground's poster grid, on an iPhone: a cell's meta line broke
66
+ * after the middle dot for the first four frames of a push, because the
67
+ * detail's span has no `truncate` and the cell's width does not fit it.
68
+ *
69
+ * Read for both ends so the flight can hold a single line when it knows the
70
+ * journey has no honest reason to have two (see `holdsOneLine`).
71
+ */
72
+ singleLine: boolean;
73
+ /**
74
+ * The height of this end's first line of text, in px — the face's own ascent
75
+ * plus descent at the size it renders.
76
+ *
77
+ * It is the other half of where a line SITS in its box: the half-leading is
78
+ * `(line-height - this) / 2`, and both engines render that floored to whole
79
+ * pixels. A flight that interpolates the leading has to know where those
80
+ * boundaries are to avoid ending on one (see morphLine's `leadingBias`).
81
+ *
82
+ * Null for anything that is not one run of text, and for a measurement taken
83
+ * through an ancestor's scale, which is not the face's own height.
84
+ */
85
+ textHeight: number | null;
86
+ /**
87
+ * Where this end's line actually SITS: the rendered distance from the box's
88
+ * top to the top of its text run, in px.
89
+ *
90
+ * The engines floor the half-leading to whole pixels, and this is the floored
91
+ * answer they gave — not a derivation. It is what the landing will paint, and
92
+ * a correction that cannot reproduce it from `line-height` and `textHeight`
93
+ * has misread the rule and declines to act (see morphLine's `leadingBias`).
94
+ */
95
+ leadOffset: number | null;
57
96
  }
58
97
  export declare const rectCentre: (rect: MorphRect) => PosePoint;
98
+ /**
99
+ * Is a box of this height a single line of type?
100
+ *
101
+ * Exported because the height to ask about is not always the measured one: a
102
+ * NESTED arrival is measured inside a container that is already staged at its
103
+ * from-box, so its staged height is the wrapped one — the very thing the answer
104
+ * is used to prevent. Its rest height is the honest one.
105
+ */
106
+ export declare const isSingleLine: (height: number, lineHeight: number | null, fontSize: number | null) => boolean;
59
107
  export declare const captureMorphSnapshot: (element: HTMLElement) => MorphSnapshot;
60
108
  /**
61
109
  * A painted rect mapped back into the space its ancestor's transform is applied
@@ -31,6 +31,30 @@ export interface MorphKeyframeSet {
31
31
  * one, the corner where the corner is all that changes.
32
32
  */
33
33
  geometryName: string;
34
+ /**
35
+ * Whether the geometry keyframe is one the COMPOSITOR can run.
36
+ *
37
+ * True only where the travel is a transform and nothing else. A keyframe that
38
+ * also carries a box, a type size or a clip is resolved by the MAIN THREAD,
39
+ * once per frame it manages to produce — and anything that has to stay
40
+ * registered with it has to be presented from there too (see the camera).
41
+ */
42
+ geometryAccelerated: boolean;
43
+ /**
44
+ * The `translate` the caller must set on the element, or null.
45
+ *
46
+ * Non-null where the travel is driven through registered properties, which
47
+ * is what keeps its position on the same thread as its size.
48
+ */
49
+ translate: string | null;
50
+ /**
51
+ * The `transform` the caller must set on the element, or null.
52
+ *
53
+ * Non-null only for a PINNED set, whose keyframes animate the pose's
54
+ * coordinates rather than the transform itself, and which therefore needs the
55
+ * transform that reads them (see PINNED_POSE_TRANSFORM).
56
+ */
57
+ transform: string | null;
34
58
  }
35
59
  /**
36
60
  * The keyframes and the `animation` shorthand for one side of a morph.
@@ -69,6 +93,37 @@ export declare const buildMorphKeyframes: (input: {
69
93
  from: number;
70
94
  to: number;
71
95
  } | null;
96
+ /**
97
+ * The line-height as a STAIRCASE, holding the leading still for the flight.
98
+ *
99
+ * Supersedes `lineHeight` where it is given: the two cannot both author the
100
+ * property, and the staircase is the one that keeps the glyphs from stepping
101
+ * inside the box (see morphLine). It rides its own animation because it needs
102
+ * its own timing — each stop HOLDS until the next, which is what a staircase
103
+ * is, and the geometry keyframe cannot hold one channel while easing the rest.
104
+ */
105
+ leading?: {
106
+ at: number;
107
+ lineHeight: number;
108
+ }[] | null;
109
+ /**
110
+ * The ascent's staircase, carried backwards on the box so the two cancel.
111
+ *
112
+ * A held leading still leaves the BASELINE stepping, because it sits an
113
+ * ascent below the inline box's top and the ascent is on the same grid. The
114
+ * two terms are both grid-locked, so nothing done to the line-height can make
115
+ * their sum smooth. The box under them is not grid-locked, so the flight
116
+ * sends the box the OTHER way by the same amount and the glyphs come out
117
+ * still: the box travels to `top + ascent` and a transform takes the ascent
118
+ * straight back off, exactly at both ends and within half a pixel between.
119
+ *
120
+ * A box wobbling half a pixel is nothing to look at; a line of type doing it
121
+ * is the tremor this exists to remove.
122
+ */
123
+ lift?: {
124
+ at: number;
125
+ ascent: number;
126
+ }[] | null;
72
127
  /** Type's other two dimensions, so it re-typesets rather than merely re-sizing. */
73
128
  fontWeight?: {
74
129
  from: number;
@@ -147,15 +202,69 @@ export declare const buildMorphKeyframes: (input: {
147
202
  from: string;
148
203
  to: string;
149
204
  }[];
205
+ /**
206
+ * Keep this set on the main thread even where its geometry is a transform the
207
+ * compositor could have run.
208
+ *
209
+ * A flight is one composition, and its parts are placed relative to each
210
+ * other. The moment one of them travels by its box — which is every flight
211
+ * where the element GROWS rather than being scaled — the frames it can be
212
+ * drawn on are the main thread's, and a part that keeps advancing without it
213
+ * separates from it by however far behind that thread is. A ghost is the
214
+ * clearest case: it is a copy of the departure whose only job is to sit on
215
+ * the element it dissolves into, and one that leads prints the card twice.
216
+ *
217
+ * So the flight decides once, and every part it emits abides by it. Ignored
218
+ * where the geometry is already layout-bound, which needs no help.
219
+ */
220
+ pinned?: boolean;
221
+ /**
222
+ * Drive the travel's own position through registered properties.
223
+ *
224
+ * False only where they could not be registered, and there the position goes
225
+ * back to `left` and `top`: a literal `translate` would be run by WebKit's
226
+ * compositor while the size it belongs to waits for the main thread.
227
+ */
228
+ travelPinned?: boolean;
150
229
  }) => MorphKeyframeSet;
151
230
  /**
152
231
  * The CAMERA: the transform that takes a screen from resting to "zoomed onto
153
232
  * this element", for a flight that carries its screen.
154
233
  *
155
- * One uniform scale and one translate, both literal — the same discipline the
156
- * travel keeps, and for the same reason: a compiled animation whose values come
157
- * from custom properties was device-bisected off the compositor on WebKit
158
- * (see the literal-timing note in compileTransitionStyles).
234
+ * One uniform scale and one translate.
235
+ *
236
+ * THE CAMERA IS NOT AN ANIMATION OF ITS OWN. It is defined as exactly the zoom
237
+ * that carries the element from one end of the flight to the other, and the two
238
+ * are emitted on one clock: same duration, same delay, same easing, released
239
+ * together. Measured, they agree to a thousandth of a frame at every sample.
240
+ *
241
+ * They still do not agree ON GLASS, because they are not PRESENTED by the same
242
+ * thread. A transform is one of the few things a compositor can run by itself,
243
+ * so the camera advances every vsync whatever the page is doing; the element
244
+ * travels by its box, which no compositor can interpolate, so it advances only
245
+ * on frames the main thread manages to produce. Isolated on both engines: with
246
+ * the main thread blocked mid-flight, a transform twin of a box travel ran 146px
247
+ * (Blink) and 167px (WebKit) ahead of it before the box moved at all. That gap
248
+ * is the whole defect — a card trailing the grid it is supposed to be opening
249
+ * out of, reported from iOS Safari as the camera being a beat ahead of the card.
250
+ *
251
+ * So where the element it carries is main-thread bound, the camera is too: the
252
+ * transform is composed from REGISTERED custom properties and the keyframes
253
+ * animate those, which no compositor can run because the substitution is style
254
+ * resolution's work. It then advances on exactly the frames the element does,
255
+ * and the pair is rigid again at whatever rate the device can hold.
256
+ *
257
+ * That is the only lever that works. `calc(var())` in the timing — the one this
258
+ * codebase already knew took a fade off WebKit's compositor — leaves a literal
259
+ * transform accelerated on both engines, as do constant `left`, `background-color`
260
+ * and `clip-path` channels alongside it; all four were measured to run away from
261
+ * the main thread exactly as the plain transform did. It costs nothing: the
262
+ * screen keeps its layer, so the per-frame work is a style resolution and a
263
+ * transform update, and frame times were indistinguishable from the literal form
264
+ * at 1x, 6x and 12x CPU throttle.
265
+ *
266
+ * Where the element's own travel IS a transform, the camera stays literal and
267
+ * accelerated: there is then nothing main-thread bound for it to wait for.
159
268
  *
160
269
  * The scale comes from WIDTH alone. The element's own box changes aspect across
161
270
  * the flight, so no single uniform scale can match both axes, and width is the
@@ -184,6 +293,14 @@ export declare const buildCameraKeyframes: (input: {
184
293
  start: number;
185
294
  ease: AnimationOptions["ease"];
186
295
  selector: string;
296
+ /**
297
+ * Emit the literal transform the compositor can run.
298
+ *
299
+ * True only where the element this camera carries is itself on the
300
+ * compositor; false pins the camera to the main thread's cadence, which is
301
+ * where a box travel lives (see above).
302
+ */
303
+ accelerated: boolean;
187
304
  }) => {
188
305
  rules: string[];
189
306
  name: string;
@@ -0,0 +1,73 @@
1
+ import { AnimationOptions } from '../transition/cssTypes';
2
+ /** The properties the hold writes, so a caller can reason about what it costs. */
3
+ export declare const LINE_HOLD: {
4
+ readonly whiteSpace: "nowrap";
5
+ readonly overflow: "hidden";
6
+ readonly textOverflow: "ellipsis";
7
+ };
8
+ /**
9
+ * Hold the flying element to one line for the duration of a flight.
10
+ *
11
+ * Written as inline style, which is what the landing restores wholesale — the
12
+ * hold needs no undo of its own.
13
+ */
14
+ export declare const holdOneLine: (element: HTMLElement) => void;
15
+ /** Whether a flight between these two ends should hold a single line. */
16
+ export declare const holdsOneLine: (from: boolean, to: boolean) => boolean;
17
+ /**
18
+ * Extra leading, in px, for BOTH ends of a flight so the half-leading it
19
+ * renders never crosses a pixel boundary.
20
+ *
21
+ * Zero when either end cannot be measured, and zero when the travel is wider
22
+ * than one pixel of half-leading — no offset fits it in one, and a step
23
+ * somewhere is then the honest outcome. Mid-flight is where it belongs.
24
+ */
25
+ export interface LeadingEnd {
26
+ lineHeight: number | null;
27
+ textHeight: number | null;
28
+ /** What the engine actually rendered for this end (see MorphSnapshot). */
29
+ leadOffset: number | null;
30
+ }
31
+ export declare const leadingBias: (from: LeadingEnd, to: LeadingEnd) => number;
32
+ export default holdOneLine;
33
+ export interface LeadingEndType {
34
+ fontSize: number | null;
35
+ lineHeight: number | null;
36
+ /** The face height the engine actually reported for this end. */
37
+ textHeight: number | null;
38
+ }
39
+ export interface LeadingStop {
40
+ /** Percent of the flight, 0 to 100. */
41
+ at: number;
42
+ lineHeight: number;
43
+ /**
44
+ * The face's ascent from this stop on.
45
+ *
46
+ * A held leading is only half the answer. The BASELINE sits an ascent below
47
+ * the inline box's top, and the ascent is on the same grid the leading is, so
48
+ * it steps just as often — device-measured at seventeen steps of half a pixel
49
+ * across one flight of forty-nine frames, which is a jump every third frame.
50
+ *
51
+ * Neither term can be made smooth: both are on the grid, so their sum is too,
52
+ * and a baseline that has nine and a half pixels of grid to climb must climb
53
+ * it in steps. What CAN be smooth is the box under them, because a box's
54
+ * position is not on any grid — so the flight carries the ascent's staircase
55
+ * BACKWARDS on the box and lets the two cancel (see `lift`).
56
+ */
57
+ ascent: number;
58
+ }
59
+ /**
60
+ * The line-height stops that hold a type morph's leading still, or null.
61
+ *
62
+ * Null wherever the correction cannot be justified: a face whose metrics cannot
63
+ * be read, an end that was never measured, a flight whose type does not change
64
+ * size, and — the one that matters — a prediction that does not reproduce what
65
+ * the engine actually reported at BOTH ends. That last is what stands in for a
66
+ * browser check: an engine that does not quantise its face heights fails it at
67
+ * every size, and is left alone.
68
+ */
69
+ export declare const leadingStops: (from: LeadingEndType, to: LeadingEndType, font: {
70
+ family: string;
71
+ weight: string | number;
72
+ style: string;
73
+ } | null, ease: AnimationOptions["ease"]) => LeadingStop[] | null;
@@ -40,5 +40,35 @@ export declare const poseToCss: (pose: MorphPose) => string;
40
40
  * measured travel and an author's flourish stack on one element instead of
41
41
  * needing a wrapper each.
42
42
  */
43
+ export declare const composePoses: (poses: MorphPose[]) => MorphPose[];
43
44
  export declare const composePosesToCss: (poses: MorphPose[]) => string;
45
+ /** The `translate` an element wears while its travel is pinned. */
46
+ export declare const PINNED_TRAVEL: string;
47
+ /** One end of a pinned travel, as the keyframe declarations that drive it. */
48
+ export declare const pinnedTravelDecls: (x: number, y: number, indent?: string) => string;
49
+ /** The `transform` an element wears while its pose is pinned. */
50
+ export declare const PINNED_POSE_TRANSFORM: string;
51
+ /**
52
+ * The registrations the pinned form needs, inserted once per document.
53
+ *
54
+ * They have to be REGISTERED: an unregistered custom property is a string to
55
+ * the engine and animates discretely, which would teleport a pose at its
56
+ * midpoint instead of interpolating it.
57
+ *
58
+ * One set of names for every flight rather than one per participant, because a
59
+ * registration is document-wide and re-registering invalidates style for the
60
+ * whole page. `inherits: false` is what makes that safe: each element holds its
61
+ * own values, so two flights never read each other's and no descendant inherits
62
+ * a pose meant for its parent.
63
+ */
64
+ export declare const PINNED_POSE_PROPERTY_RULES: string[];
65
+ /**
66
+ * One pose as the keyframe declarations that drive `PINNED_POSE_TRANSFORM`.
67
+ *
68
+ * Every channel is written at both ends even where it does not change, because
69
+ * these are the coordinates of one transform rather than five animations: an
70
+ * end that omitted a channel would interpolate it from its registered initial
71
+ * value instead of holding it.
72
+ */
73
+ export declare const pinnedPoseDecls: (pose: MorphPose, indent?: string) => string;
44
74
  export declare const interpolatePose: (from: MorphPose, to: MorphPose, progress: number) => MorphPose;
@@ -7,3 +7,12 @@
7
7
  * would by then point at someone else's keyframes.
8
8
  */
9
9
  export declare const insertMorphRules: (rules: string[]) => (() => void);
10
+ /**
11
+ * Register the pinned pose's custom properties, and report whether they took.
12
+ *
13
+ * A browser that does not understand `@property` refuses the rule, and there
14
+ * every pose has to stay literal: unregistered, those properties would animate
15
+ * discretely and teleport a pose at its midpoint rather than interpolating it.
16
+ * A part that leads the rest of its flight is a flaw; one that jumps is a break.
17
+ */
18
+ export declare const ensurePinnedPoses: () => boolean;
@@ -22,6 +22,12 @@ export interface MorphSide {
22
22
  padding: string;
23
23
  margin: string;
24
24
  paint: Record<string, string>;
25
+ /** Whether this end is one line of text (see MorphSnapshot.singleLine). */
26
+ singleLine: boolean;
27
+ /** This end's first text run height (see MorphSnapshot.textHeight). */
28
+ textHeight: number | null;
29
+ /** Where this end's line was actually rendered (see MorphSnapshot.leadOffset). */
30
+ leadOffset: number | null;
25
31
  /**
26
32
  * Whether this screen's transition MOVES it — read from the DEFINITION, not
27
33
  * from the element: at the moment a flight is staged the arriving screen is
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flemo/core",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Framework-agnostic primitives for flemo: history, navigation, transitions, task manager.",
5
5
  "main": "./dist/index.mjs",
6
6
  "module": "./dist/index.mjs",