@react-x11/components 0.16.1 → 0.17.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.
@@ -0,0 +1,466 @@
1
+ // `animation` and its longhands (CSS Animations 1, 4), read into the lists
2
+ // an element's animations are, and what those animations leave on its
3
+ // style.
4
+ //
5
+ // Nothing here runs an animation: a document is drawn as it stands once
6
+ // each of its animations has run one iteration at no length. One that
7
+ // fills forwards then holds the frame it ended on, and any other leaves
8
+ // the style it started from. That is the moment the Zen Garden bench holds
9
+ // Chrome at (`scripts/zengarden/chrome.ts`), and the one a page that fades
10
+ // its panels in and holds them is meant to be seen at: drawn as it starts,
11
+ // a panel that animates in from nothing is never there. So the cascade asks
12
+ // `restingFrames` for the declarations of each such frame and applies them
13
+ // above the author's own, as the animation origin is (CSS Cascade 5, 6.1).
14
+
15
+ import { parseMath } from './calc.js';
16
+ import type { Declaration, KeyframesRule } from './parse.js';
17
+ import { parseNumber, splitCommas, splitValue } from './values.js';
18
+
19
+ /**
20
+ * An easing function (CSS Easing 2), as an animation's timing function or
21
+ * a keyframe's: the identity, a cubic Bézier, steps, or the points of a
22
+ * `linear()` with every input placed.
23
+ */
24
+ export type Easing =
25
+ | { type: 'linear' }
26
+ | { type: 'cubic'; x1: number; y1: number; x2: number; y2: number }
27
+ | { type: 'steps'; count: number; position: StepPosition }
28
+ | { type: 'points'; points: readonly { input: number; output: number }[] };
29
+
30
+ export type StepPosition =
31
+ 'jump-start' | 'jump-end' | 'jump-none' | 'jump-both';
32
+
33
+ export type AnimationDirection =
34
+ 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
35
+
36
+ export type FillMode = 'none' | 'forwards' | 'backwards' | 'both';
37
+
38
+ export type PlayState = 'running' | 'paused';
39
+
40
+ /**
41
+ * An element's animations: each longhand's list as computed. There are as
42
+ * many animations as `names` has entries; a shorter list of another
43
+ * longhand repeats, and a longer one is cut (CSS Animations 1, 4).
44
+ */
45
+ export interface Animations {
46
+ /** `animation-name`: the `@keyframes` each runs, null for `none`. */
47
+ readonly names: readonly (string | null)[];
48
+ /** `animation-duration`, in milliseconds: `auto` is 0. */
49
+ readonly durations: readonly number[];
50
+ /** `animation-timing-function`. */
51
+ readonly easings: readonly Easing[];
52
+ /** `animation-delay`, in milliseconds, before the first iteration. */
53
+ readonly delays: readonly number[];
54
+ /** `animation-iteration-count`: `infinite` is `Infinity`. */
55
+ readonly iterations: readonly number[];
56
+ readonly directions: readonly AnimationDirection[];
57
+ readonly fillModes: readonly FillMode[];
58
+ readonly playStates: readonly PlayState[];
59
+ }
60
+
61
+ const LINEAR: Easing = { type: 'linear' };
62
+ const EASE: Easing = { type: 'cubic', x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 };
63
+
64
+ /** The easing keywords (CSS Easing 2, 2.2 and 3.1). */
65
+ const EASINGS: Record<string, Easing> = {
66
+ linear: LINEAR,
67
+ ease: EASE,
68
+ 'ease-in': { type: 'cubic', x1: 0.42, y1: 0, x2: 1, y2: 1 },
69
+ 'ease-out': { type: 'cubic', x1: 0, y1: 0, x2: 0.58, y2: 1 },
70
+ 'ease-in-out': { type: 'cubic', x1: 0.42, y1: 0, x2: 0.58, y2: 1 },
71
+ 'step-start': { type: 'steps', count: 1, position: 'jump-start' },
72
+ 'step-end': { type: 'steps', count: 1, position: 'jump-end' },
73
+ };
74
+
75
+ /** The initial value of every longhand: one animation, of nothing. Every
76
+ * element without one shares it, which is how the cascade tells. */
77
+ export const NO_ANIMATIONS: Animations = {
78
+ names: [null],
79
+ durations: [0],
80
+ easings: [EASE],
81
+ delays: [0],
82
+ iterations: [1],
83
+ directions: ['normal'],
84
+ fillModes: ['none'],
85
+ playStates: ['running'],
86
+ };
87
+
88
+ /** The longhands, by property name, and the list each is. */
89
+ export const ANIMATION_LONGHANDS: Record<string, keyof Animations> = {
90
+ 'animation-name': 'names',
91
+ 'animation-duration': 'durations',
92
+ 'animation-timing-function': 'easings',
93
+ 'animation-delay': 'delays',
94
+ 'animation-iteration-count': 'iterations',
95
+ 'animation-direction': 'directions',
96
+ 'animation-fill-mode': 'fillModes',
97
+ 'animation-play-state': 'playStates',
98
+ };
99
+
100
+ const DIRECTIONS = new Set<string>([
101
+ 'normal',
102
+ 'reverse',
103
+ 'alternate',
104
+ 'alternate-reverse',
105
+ ]);
106
+ const FILL_MODES = new Set<string>(['none', 'forwards', 'backwards', 'both']);
107
+ const PLAY_STATES = new Set<string>(['running', 'paused']);
108
+
109
+ /** One value of each longhand's list; undefined where it is none, and so
110
+ * is null but for a name, where it is `none`. */
111
+ const ONE: {
112
+ [K in keyof Animations]: (
113
+ value: string,
114
+ ) => Animations[K][number] | null | undefined;
115
+ } = {
116
+ names: animationName,
117
+ durations: (v) => (v.toLowerCase() === 'auto' ? 0 : duration(v)),
118
+ easings: parseEasing,
119
+ delays: parseTime,
120
+ iterations: iterationCount,
121
+ directions: (v) => keyword(v, DIRECTIONS) as AnimationDirection | null,
122
+ fillModes: (v) => keyword(v, FILL_MODES) as FillMode | null,
123
+ playStates: (v) => keyword(v, PLAY_STATES) as PlayState | null,
124
+ };
125
+
126
+ /**
127
+ * `animations` with one longhand's list set from a declaration's value, or
128
+ * null where the value is not a list of that longhand's values and the
129
+ * declaration is dropped.
130
+ */
131
+ export function animationLonghand(
132
+ animations: Animations,
133
+ key: keyof Animations,
134
+ value: string,
135
+ ): Animations | null {
136
+ const read = ONE[key] as (value: string) => unknown;
137
+ const list: unknown[] = [];
138
+ for (const item of splitCommas(value)) {
139
+ const one = read(item.trim());
140
+ if (one === undefined || (one === null && key !== 'names')) return null;
141
+ list.push(one);
142
+ }
143
+ return { ...animations, [key]: list };
144
+ }
145
+
146
+ /**
147
+ * The `animation` shorthand: a comma list of animations, each its
148
+ * longhands' values in any order and the rest at their initial values — or
149
+ * null where an item is not one. The first time is the duration and the
150
+ * second the delay; a keyword goes to the first longhand it is a value of
151
+ * that the item has not set, and only then is it the name, so that
152
+ * `animation: none` is a fill mode and `animation: 1s forwards slide` names
153
+ * `slide` (CSS Animations 1, 4.9).
154
+ */
155
+ export function parseAnimation(value: string): Animations | null {
156
+ const names: (string | null)[] = [];
157
+ const durations: number[] = [];
158
+ const easings: Easing[] = [];
159
+ const delays: number[] = [];
160
+ const iterations: number[] = [];
161
+ const directions: AnimationDirection[] = [];
162
+ const fillModes: FillMode[] = [];
163
+ const playStates: PlayState[] = [];
164
+ for (const item of splitCommas(value)) {
165
+ const tokens = splitValue(item.trim());
166
+ if (!tokens.length) return null;
167
+ let name: string | null | undefined;
168
+ let time = 0;
169
+ let dur: number | undefined;
170
+ let del: number | undefined;
171
+ let easing: Easing | undefined;
172
+ let count: number | undefined;
173
+ let direction: AnimationDirection | undefined;
174
+ let fill: FillMode | undefined;
175
+ let play: PlayState | undefined;
176
+ for (const token of tokens) {
177
+ const lower = token.toLowerCase();
178
+ const t = parseTime(token);
179
+ if (t !== null) {
180
+ if (time === 0) {
181
+ if (t < 0) return null;
182
+ dur = t;
183
+ } else if (time === 1) {
184
+ del = t;
185
+ } else {
186
+ return null;
187
+ }
188
+ time += 1;
189
+ continue;
190
+ }
191
+ if (easing === undefined) {
192
+ const e = parseEasing(token);
193
+ if (e !== null) {
194
+ easing = e;
195
+ continue;
196
+ }
197
+ }
198
+ if (count === undefined) {
199
+ const n = iterationCount(token);
200
+ if (n !== null) {
201
+ count = n;
202
+ continue;
203
+ }
204
+ }
205
+ if (direction === undefined && DIRECTIONS.has(lower)) {
206
+ direction = lower as AnimationDirection;
207
+ } else if (fill === undefined && FILL_MODES.has(lower)) {
208
+ fill = lower as FillMode;
209
+ } else if (play === undefined && PLAY_STATES.has(lower)) {
210
+ play = lower as PlayState;
211
+ } else if (name === undefined) {
212
+ const n = animationName(token);
213
+ if (n === undefined) return null;
214
+ name = n;
215
+ } else {
216
+ return null;
217
+ }
218
+ }
219
+ names.push(name ?? null);
220
+ durations.push(dur ?? 0);
221
+ easings.push(easing ?? EASE);
222
+ delays.push(del ?? 0);
223
+ iterations.push(count ?? 1);
224
+ directions.push(direction ?? 'normal');
225
+ fillModes.push(fill ?? 'none');
226
+ playStates.push(play ?? 'running');
227
+ }
228
+ return {
229
+ names,
230
+ durations,
231
+ easings,
232
+ delays,
233
+ iterations,
234
+ directions,
235
+ fillModes,
236
+ playStates,
237
+ };
238
+ }
239
+
240
+ /** An `animation-name` entry: the name, null for `none`, and undefined for
241
+ * what is neither — a CSS-wide keyword, `default`, or no identifier. */
242
+ function animationName(value: string): string | null | undefined {
243
+ const v = value.trim();
244
+ if (v[0] === '"' || v[0] === "'") {
245
+ const q = v[0];
246
+ return v.length >= 2 && v[v.length - 1] === q ? v.slice(1, -1) : undefined;
247
+ }
248
+ const lower = v.toLowerCase();
249
+ if (lower === 'none') return null;
250
+ if (!IDENT.test(v) || RESERVED.has(lower)) return undefined;
251
+ return v;
252
+ }
253
+
254
+ const IDENT = /^-?(?:[_a-zA-Z\u0080-￿]|--)[\w\u0080-￿-]*$/;
255
+
256
+ const RESERVED = new Set([
257
+ 'inherit',
258
+ 'initial',
259
+ 'unset',
260
+ 'revert',
261
+ 'revert-layer',
262
+ 'default',
263
+ ]);
264
+
265
+ /** A `<time>` in milliseconds: `2s`, `150ms`, a `calc()` of them; null
266
+ * for anything else. */
267
+ export function parseTime(value: string): number | null {
268
+ const v = value.trim().toLowerCase();
269
+ const m = TIME.exec(v);
270
+ if (m) return Number(m[1]) * (m[2] === 's' ? 1000 : 1);
271
+ if (!v.includes('(')) return null;
272
+ const sum = parseMath(v, (token) => {
273
+ const unit = TIME.exec(token);
274
+ return unit ? Number(unit[1]) * (unit[2] === 's' ? 1000 : 1) : null;
275
+ });
276
+ return typeof sum === 'number' ? sum : null;
277
+ }
278
+
279
+ const TIME = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(s|ms)$/;
280
+
281
+ /** A duration: a time that is not negative. */
282
+ function duration(value: string): number | null {
283
+ const t = parseTime(value);
284
+ return t !== null && t >= 0 ? t : null;
285
+ }
286
+
287
+ /** `infinite`, or a number that is not negative. */
288
+ function iterationCount(value: string): number | null {
289
+ const v = value.trim().toLowerCase();
290
+ if (v === 'infinite') return Infinity;
291
+ const n = parseNumber(v);
292
+ return n !== null && n >= 0 ? n : null;
293
+ }
294
+
295
+ function keyword(value: string, set: ReadonlySet<string>): string | null {
296
+ const v = value.trim().toLowerCase();
297
+ return set.has(v) ? v : null;
298
+ }
299
+
300
+ /**
301
+ * An easing function (CSS Easing 2): a keyword, `cubic-bezier()`,
302
+ * `steps()` or `linear()`; null for anything else.
303
+ */
304
+ export function parseEasing(value: string): Easing | null {
305
+ const v = value.trim().toLowerCase();
306
+ const named = EASINGS[v];
307
+ if (named) return named;
308
+ const m = /^(cubic-bezier|steps|linear)\((.*)\)$/s.exec(v);
309
+ if (!m) return null;
310
+ const args = splitCommas(m[2]);
311
+ if (m[1] === 'cubic-bezier') {
312
+ if (args.length !== 4) return null;
313
+ const [x1, y1, x2, y2] = args.map(parseNumber);
314
+ if (x1 === null || y1 === null || x2 === null || y2 === null) return null;
315
+ // the x of each control point is a time, inside the interval
316
+ if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) return null;
317
+ return { type: 'cubic', x1, y1, x2, y2 };
318
+ }
319
+ if (m[1] === 'steps') {
320
+ if (args.length < 1 || args.length > 2) return null;
321
+ const count = parseNumber(args[0]);
322
+ if (count === null || !Number.isInteger(count)) return null;
323
+ const position = stepPosition(args[1]);
324
+ if (!position) return null;
325
+ if (count < (position === 'jump-none' ? 2 : 1)) return null;
326
+ return { type: 'steps', count, position };
327
+ }
328
+ return linearPoints(args);
329
+ }
330
+
331
+ function stepPosition(value: string | undefined): StepPosition | null {
332
+ const v = value?.trim() ?? 'jump-end';
333
+ if (v === 'start') return 'jump-start';
334
+ if (v === 'end') return 'jump-end';
335
+ return v === 'jump-start' ||
336
+ v === 'jump-end' ||
337
+ v === 'jump-none' ||
338
+ v === 'jump-both'
339
+ ? v
340
+ : null;
341
+ }
342
+
343
+ /**
344
+ * `linear()`'s stops as points (CSS Easing 2, 2.1.1): each an output with
345
+ * up to two input percentages, two making two points. The first point
346
+ * with no input is at 0 and the last at 1, an input less than one before
347
+ * it is that one, and the points between two that have one are spread
348
+ * evenly between them.
349
+ */
350
+ function linearPoints(args: string[]): Easing | null {
351
+ const points: { input: number | null; output: number }[] = [];
352
+ for (const arg of args) {
353
+ const parts = splitValue(arg.trim());
354
+ let output: number | null = null;
355
+ const inputs: number[] = [];
356
+ for (const part of parts) {
357
+ if (part.endsWith('%')) {
358
+ const pct = parseNumber(part.slice(0, -1));
359
+ if (pct === null || inputs.length === 2) return null;
360
+ inputs.push(pct / 100);
361
+ } else {
362
+ if (output !== null) return null;
363
+ output = parseNumber(part);
364
+ if (output === null) return null;
365
+ }
366
+ }
367
+ if (output === null) return null;
368
+ if (!inputs.length) points.push({ input: null, output });
369
+ for (const input of inputs) points.push({ input, output });
370
+ }
371
+ if (points.length < 2) return null;
372
+ if (points[0].input === null) points[0].input = 0;
373
+ const last = points[points.length - 1];
374
+ if (last.input === null) last.input = Math.max(1, highest(points));
375
+ let largest = -Infinity;
376
+ for (const point of points) {
377
+ if (point.input === null) continue;
378
+ if (point.input < largest) point.input = largest;
379
+ largest = point.input;
380
+ }
381
+ for (let i = 1; i < points.length;) {
382
+ if (points[i].input !== null) {
383
+ i += 1;
384
+ continue;
385
+ }
386
+ let j = i + 1;
387
+ while (points[j].input === null) j += 1;
388
+ const from = points[i - 1].input!;
389
+ const to = points[j].input!;
390
+ for (let k = i; k < j; k += 1) {
391
+ points[k].input = from + ((to - from) * (k - i + 1)) / (j - i + 1);
392
+ }
393
+ i = j + 1;
394
+ }
395
+ return {
396
+ type: 'points',
397
+ points: points as { input: number; output: number }[],
398
+ };
399
+ }
400
+
401
+ /** The largest input placed so far. */
402
+ function highest(points: { input: number | null }[]): number {
403
+ let out = -Infinity;
404
+ for (const point of points) {
405
+ if (point.input !== null && point.input > out) out = point.input;
406
+ }
407
+ return out;
408
+ }
409
+
410
+ /**
411
+ * The declarations an element's animations leave on its style once each
412
+ * has run one iteration at no length, one list an animation, in the order
413
+ * they are named — a later one over an earlier where both set a property
414
+ * (CSS Animations 1, 4.2). An animation leaves the frame it ends on where
415
+ * it fills forwards: the `to` frame, or the `from` frame where it plays in
416
+ * reverse, merged where several frames are at that offset, a later one's
417
+ * declaration over an earlier one's. A property the frame does not set is
418
+ * the element's own, which there is nothing to say about. Null where no
419
+ * animation leaves anything.
420
+ */
421
+ export function restingFrames(
422
+ animations: Animations,
423
+ keyframesOf: (name: string) => KeyframesRule | null,
424
+ ): Declaration[][] | null {
425
+ let out: Declaration[][] | null = null;
426
+ const { names, directions, fillModes } = animations;
427
+ for (let i = 0; i < names.length; i += 1) {
428
+ const name = names[i];
429
+ if (name === null) continue;
430
+ const fill = fillModes[i % fillModes.length];
431
+ if (fill !== 'forwards' && fill !== 'both') continue;
432
+ const rule = keyframesOf(name);
433
+ if (!rule) continue;
434
+ const direction = directions[i % directions.length];
435
+ const reversed =
436
+ direction === 'reverse' || direction === 'alternate-reverse';
437
+ const frame = endFrame(rule, reversed ? 0 : 1);
438
+ if (frame.length) (out ??= []).push(frame);
439
+ }
440
+ return out;
441
+ }
442
+
443
+ /** The declarations at one offset of a `@keyframes`, in the order written:
444
+ * one array per rule and offset, so the cascade sees the same one each
445
+ * time it asks. */
446
+ function endFrame(rule: KeyframesRule, offset: 0 | 1): Declaration[] {
447
+ let ends = END_FRAMES.get(rule);
448
+ if (!ends) {
449
+ ends = [null, null];
450
+ END_FRAMES.set(rule, ends);
451
+ }
452
+ let frame = ends[offset];
453
+ if (!frame) {
454
+ frame = [];
455
+ for (const f of rule.frames) {
456
+ if (f.offsets.includes(offset)) frame.push(...f.declarations);
457
+ }
458
+ ends[offset] = frame;
459
+ }
460
+ return frame;
461
+ }
462
+
463
+ const END_FRAMES = new WeakMap<
464
+ KeyframesRule,
465
+ [Declaration[] | null, Declaration[] | null]
466
+ >();
@@ -30,7 +30,13 @@ import {
30
30
  readIdent,
31
31
  startsIdent,
32
32
  } from './parse.js';
33
- import type { Declaration, StyleRule, Stylesheet } from './parse.js';
33
+ import type {
34
+ Declaration,
35
+ KeyframesRule,
36
+ StyleRule,
37
+ Stylesheet,
38
+ } from './parse.js';
39
+ import { NO_ANIMATIONS, restingFrames } from './animation.js';
34
40
  import {
35
41
  applyDeclaration,
36
42
  blockify,
@@ -73,8 +79,11 @@ const enum Origin {
73
79
  Presentation = 1,
74
80
  Author = 2,
75
81
  Inline = 3,
76
- AuthorImportant = 4,
77
- InlineImportant = 5,
82
+ /** What an animation leaves (`restingFrames`): over every declaration
83
+ * but an `!important` one (CSS Cascade 5, 6.1). */
84
+ Animation = 4,
85
+ AuthorImportant = 5,
86
+ InlineImportant = 6,
78
87
  }
79
88
 
80
89
  interface Candidate {
@@ -1094,6 +1103,9 @@ export class Cascade {
1094
1103
  readonly breakpoints: number[];
1095
1104
  /** The counter styles the sheets define, over the predefined ones. */
1096
1105
  readonly counterStyles: CounterStyles;
1106
+ /** The `@keyframes` rules, by name, in document order: which one a name
1107
+ * finds turns on the media in force (`keyframes`). */
1108
+ private _keyframes = new Map<string, KeyframesRule[]>();
1097
1109
 
1098
1110
  /** A face's x-height, for `ex`, where the fonts can say. */
1099
1111
  private _xHeightOf: FaceMetric | null;
@@ -1212,6 +1224,17 @@ export class Cascade {
1212
1224
  this._hoverRules.note(selector);
1213
1225
  this._focusRules.note(selector);
1214
1226
  }
1227
+ for (const rule of sheet.keyframes ?? []) {
1228
+ let named = this._keyframes.get(rule.name);
1229
+ if (!named) this._keyframes.set(rule.name, (named = []));
1230
+ named.push(rule);
1231
+ for (const frame of rule.frames) {
1232
+ const declarations = frame.declarations;
1233
+ if (!this._vars && usesVars(declarations)) this._vars = true;
1234
+ this._noteViewportUnits(declarations);
1235
+ if (!this._lh && usesLh(declarations)) this._lh = true;
1236
+ }
1237
+ }
1215
1238
  for (const bp of sheet.breakpoints) breakpoints.add(bp);
1216
1239
  // a query on the viewport's height reads it as a `vh` does
1217
1240
  if (sheet.readsHeight) this.readsViewportHeight = true;
@@ -1400,6 +1423,42 @@ export class Cascade {
1400
1423
  return band;
1401
1424
  }
1402
1425
 
1426
+ /**
1427
+ * The `@keyframes` an animation's name finds, or null: of the rules of
1428
+ * the name whose media hold, the one in the latest layer — no layer
1429
+ * outranking any, as for a style rule — and the last of those, but that
1430
+ * an `@-webkit-keyframes` never takes the place of an `@keyframes`, as in
1431
+ * Blink (`ScopedStyleResolver::AddKeyframeStyle`).
1432
+ */
1433
+ keyframes(name: string): KeyframesRule | null {
1434
+ const named = this._keyframes.get(name);
1435
+ if (!named) return null;
1436
+ const width = this.viewportWidth / this.scale;
1437
+ const height = this.viewportHeight / this.scale;
1438
+ let found: KeyframesRule | null = null;
1439
+ for (const rule of named) {
1440
+ if (
1441
+ !mediaMatches(
1442
+ rule.media,
1443
+ width,
1444
+ this.look.colorScheme,
1445
+ height,
1446
+ this.scale,
1447
+ )
1448
+ ) {
1449
+ continue;
1450
+ }
1451
+ if (found) {
1452
+ const by = compareLayers(rule.layer, found.layer);
1453
+ if (by < 0 || (by === 0 && rule.prefixed && !found.prefixed)) {
1454
+ continue;
1455
+ }
1456
+ }
1457
+ found = rule;
1458
+ }
1459
+ return found;
1460
+ }
1461
+
1403
1462
  /** Computed styles by sharing key, for one build (`sharedStyleFor`): one
1404
1463
  * map for the elements shared without matching, one for the elements
1405
1464
  * shared by what they matched. */
@@ -1840,7 +1899,14 @@ export class Cascade {
1840
1899
  return line;
1841
1900
  }
1842
1901
 
1843
- /** `styleFor`, from the rules and hints already gathered for `el`. */
1902
+ /**
1903
+ * `styleFor`, from the rules and hints already gathered for `el`, and
1904
+ * what its animations leave on it (`restingFrames`). Which animations an
1905
+ * element has is the cascade's answer, so an element with one that
1906
+ * leaves a frame is styled twice: the second time with the frame's
1907
+ * declarations among the rest, at the animation origin, over the
1908
+ * author's normal ones and under their `!important` ones.
1909
+ */
1844
1910
  private _computeStyle(
1845
1911
  el: Element,
1846
1912
  parentStyle: ComputedStyle,
@@ -1849,6 +1915,41 @@ export class Cascade {
1849
1915
  /** Whether the style is the element's own, and not that of one of its
1850
1916
  * pseudo-elements. */
1851
1917
  own = false,
1918
+ ): ComputedStyle {
1919
+ let style = this._cascadeStyle(
1920
+ el,
1921
+ parentStyle,
1922
+ inFlexContainer,
1923
+ candidates,
1924
+ own,
1925
+ );
1926
+ if (style.animations !== NO_ANIMATIONS && this._keyframes.size) {
1927
+ const frames = restingFrames(style.animations, (name) =>
1928
+ this.keyframes(name),
1929
+ );
1930
+ if (frames) {
1931
+ style = this._cascadeStyle(
1932
+ el,
1933
+ parentStyle,
1934
+ inFlexContainer,
1935
+ withAnimations(candidates, frames),
1936
+ own,
1937
+ );
1938
+ }
1939
+ }
1940
+ // which faces of the document's own families this family, weight and
1941
+ // slant ask for — known only now, with all three computed
1942
+ this._families?.note(style);
1943
+ return style;
1944
+ }
1945
+
1946
+ /** The style the candidates come to, in their order. */
1947
+ private _cascadeStyle(
1948
+ el: Element,
1949
+ parentStyle: ComputedStyle,
1950
+ inFlexContainer: boolean,
1951
+ candidates: Candidate[],
1952
+ own: boolean,
1852
1953
  ): ComputedStyle {
1853
1954
  const style = inherit(parentStyle, this.initial);
1854
1955
  // custom properties first, in cascade order, so every `var()` in the
@@ -2004,9 +2105,6 @@ export class Cascade {
2004
2105
  settleButton(style);
2005
2106
  }
2006
2107
  decorate(style);
2007
- // which faces of the document's own families this family, weight and
2008
- // slant ask for — known only now, with all three computed
2009
- this._families?.note(style);
2010
2108
  return style;
2011
2109
  }
2012
2110
 
@@ -2885,6 +2983,31 @@ function pick(c: Candidate): Declaration[] {
2885
2983
  return c.only < 0 ? c.declarations : [c.declarations[c.only]];
2886
2984
  }
2887
2985
 
2986
+ /** Candidates in cascade order with what an element's animations leave
2987
+ * among them: after every normal declaration and before the first
2988
+ * `!important` one, a later animation after an earlier. */
2989
+ function withAnimations(
2990
+ candidates: readonly Candidate[],
2991
+ frames: readonly Declaration[][],
2992
+ ): Candidate[] {
2993
+ let at = 0;
2994
+ while (at < candidates.length && candidates[at].origin < Origin.Animation) {
2995
+ at += 1;
2996
+ }
2997
+ return [
2998
+ ...candidates.slice(0, at),
2999
+ ...frames.map((declarations, order) => ({
3000
+ origin: Origin.Animation,
3001
+ layer: null,
3002
+ specificity: 0,
3003
+ order,
3004
+ declarations,
3005
+ only: -1,
3006
+ })),
3007
+ ...candidates.slice(at),
3008
+ ];
3009
+ }
3010
+
2888
3011
  function pushRule(out: Candidate[], rule: StyleRule, origin: Origin): void {
2889
3012
  let hasImportant = false;
2890
3013
  for (const d of rule.declarations) {