@svelterm/core 0.23.0 → 0.24.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/CHANGELOG.md +19 -0
- package/dist/src/css/animation-runner.d.ts +1 -1
- package/dist/src/css/animation-runner.js +9 -1
- package/dist/src/css/compute.d.ts +18 -0
- package/dist/src/css/compute.js +68 -12
- package/dist/src/render/animation-clock.d.ts +9 -1
- package/dist/src/render/animation-clock.js +67 -42
- package/docs/motion.md +10 -9
- package/docs/reference.md +9 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0 — 2026-07-05
|
|
4
|
+
|
|
5
|
+
Motion timing completeness: transitions and keyframes behave per spec.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Per-property transitions** — `transition: color 150ms linear, width
|
|
10
|
+
400ms ease` runs each property on its own duration and timing;
|
|
11
|
+
longhand lists pair cyclically per spec. (Previously the first
|
|
12
|
+
duration and one timing function applied to everything.)
|
|
13
|
+
- **Interrupted transitions continue** — a retargeted transition starts
|
|
14
|
+
from its current blended value instead of jumping back to the previous
|
|
15
|
+
target (reversals no longer flash).
|
|
16
|
+
- **Per-keyframe `animation-timing-function`** — a timing function
|
|
17
|
+
declared inside a keyframe applies from that stop to the next.
|
|
18
|
+
- **Keyframe `var()`/`light-dark()` re-resolution** — scheme flips and
|
|
19
|
+
custom-property changes retarget a running animation in place, without
|
|
20
|
+
restarting it.
|
|
21
|
+
|
|
3
22
|
## 0.23.0 — 2026-07-05
|
|
4
23
|
|
|
5
24
|
Positioning: `relative` offsets apply, and `sticky` arrives.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { KeyframeStop } from './parser.js';
|
|
2
2
|
import { type ResolvedStyle } from './compute.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Easing } from './easing.js';
|
|
4
4
|
/**
|
|
5
5
|
* Runs a CSS animation by applying keyframe properties at the current
|
|
6
6
|
* time. Colours interpolate in RGB space between stops; properties that
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyDeclaration } from './compute.js';
|
|
2
2
|
import { resolveColor } from './color.js';
|
|
3
|
+
import { parseEasing } from './easing.js';
|
|
3
4
|
import { lerpColor, lerpNumber } from './interpolate.js';
|
|
4
5
|
import { parseCellLength } from './values.js';
|
|
5
6
|
/** Properties whose animation only needs repaint; anything else re-layouts. */
|
|
@@ -37,7 +38,11 @@ export class AnimationRunner {
|
|
|
37
38
|
const progress = this.getProgress(elapsedMs);
|
|
38
39
|
const segment = this.segmentAt(progress);
|
|
39
40
|
const { from, to } = segment;
|
|
40
|
-
|
|
41
|
+
// A timing function declared inside a keyframe applies from that
|
|
42
|
+
// stop to the next, overriding the element-level easing (per CSS).
|
|
43
|
+
const override = from.declarations.find(d => d.property === 'animation-timing-function');
|
|
44
|
+
const easing = override ? (parseEasing(override.value) ?? this.easing) : this.easing;
|
|
45
|
+
const localT = easing(segment.localT);
|
|
41
46
|
// Hold the earlier stop's values, then interpolate toward the next
|
|
42
47
|
for (const decl of from.declarations) {
|
|
43
48
|
applyAnimatedProperty(style, decl);
|
|
@@ -87,6 +92,9 @@ export class AnimationRunner {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
function applyAnimatedProperty(style, decl) {
|
|
95
|
+
// Keyframe-level timing functions steer the runner, not the style
|
|
96
|
+
if (decl.property === 'animation-timing-function')
|
|
97
|
+
return;
|
|
90
98
|
applyDeclaration(style, decl.property, decl.value);
|
|
91
99
|
}
|
|
92
100
|
function applyInterpolatedProperty(style, from, to, t) {
|
|
@@ -53,6 +53,15 @@ export interface ResolvedStyle {
|
|
|
53
53
|
transitionProperty: string | null;
|
|
54
54
|
transitionDuration: number;
|
|
55
55
|
transitionTimingFunction: string;
|
|
56
|
+
/** Longhand lists, paired cyclically per spec into `transitions`. */
|
|
57
|
+
transitionDurations: number[];
|
|
58
|
+
transitionTimings: string[];
|
|
59
|
+
/** Per-property transition config, resolved after cascade. */
|
|
60
|
+
transitions: Array<{
|
|
61
|
+
property: string;
|
|
62
|
+
duration: number;
|
|
63
|
+
timing: string;
|
|
64
|
+
}>;
|
|
56
65
|
borderStyle: 'none' | 'single' | 'double' | 'rounded' | 'heavy' | 'ascii' | 'eighth-cell-inner' | 'eighth-cell-outer' | 'half-cell-inner' | 'half-cell-outer' | 'full-cell';
|
|
57
66
|
borderCorner: 'none' | 'h' | 'v';
|
|
58
67
|
borderColor: string;
|
|
@@ -88,3 +97,12 @@ export declare function resolveStyles(root: TermNode, stylesheet: CSSStyleSheet,
|
|
|
88
97
|
export declare function filterByMedia(stylesheet: CSSStyleSheet, context: MediaContext): CSSStyleSheet;
|
|
89
98
|
export declare function resolveNode(node: TermNode, stylesheet: CSSStyleSheet, styles: Map<number, ResolvedStyle>, variables: Map<number, Map<string, string>>, scheme?: 'dark' | 'light'): void;
|
|
90
99
|
export declare function applyDeclaration(style: ResolvedStyle, property: string, value: string, scheme?: 'dark' | 'light'): void;
|
|
100
|
+
/**
|
|
101
|
+
* Pair the transition lists per spec: durations/timings repeat
|
|
102
|
+
* cyclically to cover every listed property.
|
|
103
|
+
*/
|
|
104
|
+
export declare function pairTransitions(style: ResolvedStyle): Array<{
|
|
105
|
+
property: string;
|
|
106
|
+
duration: number;
|
|
107
|
+
timing: string;
|
|
108
|
+
}>;
|
package/dist/src/css/compute.js
CHANGED
|
@@ -56,6 +56,8 @@ export function defaultStyle(tag) {
|
|
|
56
56
|
animationTimingFunction: 'ease',
|
|
57
57
|
transitionProperty: null, transitionDuration: 0,
|
|
58
58
|
transitionTimingFunction: 'ease',
|
|
59
|
+
transitionDurations: [], transitionTimings: [],
|
|
60
|
+
transitions: [],
|
|
59
61
|
borderStyle: 'none', borderColor: 'default', borderCorner: 'none',
|
|
60
62
|
borderTop: true, borderRight: true, borderBottom: true, borderLeft: true,
|
|
61
63
|
boxSizing: 'border-box',
|
|
@@ -272,6 +274,8 @@ function computeStyle(node, stylesheet, vars, parentStyle, scheme = 'dark') {
|
|
|
272
274
|
applyDeclaration(style, decl.property, resolveVar(decl.value, vars), scheme);
|
|
273
275
|
}
|
|
274
276
|
}
|
|
277
|
+
// Pair transition longhand lists now the cascade is complete
|
|
278
|
+
style.transitions = pairTransitions(style);
|
|
275
279
|
return style;
|
|
276
280
|
}
|
|
277
281
|
function parseInlineStyle(text) {
|
|
@@ -563,10 +567,12 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
|
|
|
563
567
|
style.transitionProperty = value === 'none' ? null : value;
|
|
564
568
|
break;
|
|
565
569
|
case 'transition-duration':
|
|
566
|
-
style.
|
|
570
|
+
style.transitionDurations = value.split(',').map(v => parseDuration(v.trim()));
|
|
571
|
+
style.transitionDuration = style.transitionDurations[0] ?? 0;
|
|
567
572
|
break;
|
|
568
573
|
case 'transition-timing-function':
|
|
569
|
-
style.
|
|
574
|
+
style.transitionTimings = splitTimingList(value);
|
|
575
|
+
style.transitionTimingFunction = style.transitionTimings[0] ?? 'ease';
|
|
570
576
|
break;
|
|
571
577
|
case 'animation-name':
|
|
572
578
|
style.animationName = value === 'none' ? null : value;
|
|
@@ -806,35 +812,85 @@ function parseAnimationShorthand(style, value) {
|
|
|
806
812
|
}
|
|
807
813
|
}
|
|
808
814
|
}
|
|
809
|
-
/**
|
|
815
|
+
/** Split a timing-function list on commas, keeping function args intact. */
|
|
816
|
+
function splitTimingList(value) {
|
|
817
|
+
const out = [];
|
|
818
|
+
let depth = 0;
|
|
819
|
+
let current = '';
|
|
820
|
+
for (const ch of value) {
|
|
821
|
+
if (ch === '(')
|
|
822
|
+
depth++;
|
|
823
|
+
if (ch === ')')
|
|
824
|
+
depth--;
|
|
825
|
+
if (ch === ',' && depth === 0) {
|
|
826
|
+
out.push(current.trim());
|
|
827
|
+
current = '';
|
|
828
|
+
}
|
|
829
|
+
else
|
|
830
|
+
current += ch;
|
|
831
|
+
}
|
|
832
|
+
if (current.trim())
|
|
833
|
+
out.push(current.trim());
|
|
834
|
+
return out;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Parse `transition: <property> <duration> <timing> [...]` — each
|
|
838
|
+
* comma-separated group configures one property (or `all`).
|
|
839
|
+
*/
|
|
810
840
|
function parseTransitionShorthand(style, value) {
|
|
811
841
|
const trimmed = value.trim();
|
|
812
842
|
if (trimmed === 'none') {
|
|
813
843
|
style.transitionProperty = null;
|
|
814
844
|
style.transitionDuration = 0;
|
|
845
|
+
style.transitionDurations = [];
|
|
846
|
+
style.transitionTimings = [];
|
|
815
847
|
return;
|
|
816
848
|
}
|
|
817
|
-
const { easing, rest } = extractEasingFunction(trimmed);
|
|
818
|
-
if (easing)
|
|
819
|
-
style.transitionTimingFunction = easing;
|
|
820
849
|
const properties = [];
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
850
|
+
const durations = [];
|
|
851
|
+
const timings = [];
|
|
852
|
+
for (const groupRaw of splitTimingList(trimmed)) {
|
|
853
|
+
const { easing, rest } = extractEasingFunction(groupRaw);
|
|
854
|
+
let property = 'all';
|
|
855
|
+
let duration = 0;
|
|
856
|
+
let timing = easing ?? 'ease';
|
|
857
|
+
for (const token of rest.trim().split(/\s+/)) {
|
|
824
858
|
if (/^\d*\.?\d+(ms|s)$/.test(token)) {
|
|
825
859
|
if (duration === 0)
|
|
826
860
|
duration = parseDuration(token);
|
|
827
861
|
}
|
|
828
862
|
else if (TIMING_KEYWORDS.has(token)) {
|
|
829
|
-
|
|
863
|
+
timing = token;
|
|
830
864
|
}
|
|
831
865
|
else if (token) {
|
|
832
|
-
|
|
866
|
+
property = token;
|
|
833
867
|
}
|
|
834
868
|
}
|
|
869
|
+
properties.push(property);
|
|
870
|
+
durations.push(duration);
|
|
871
|
+
timings.push(timing);
|
|
835
872
|
}
|
|
836
873
|
style.transitionProperty = properties.length > 0 ? properties.join(',') : 'all';
|
|
837
|
-
style.
|
|
874
|
+
style.transitionDurations = durations;
|
|
875
|
+
style.transitionTimings = timings;
|
|
876
|
+
style.transitionDuration = durations[0] ?? 0;
|
|
877
|
+
style.transitionTimingFunction = timings[0] ?? 'ease';
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Pair the transition lists per spec: durations/timings repeat
|
|
881
|
+
* cyclically to cover every listed property.
|
|
882
|
+
*/
|
|
883
|
+
export function pairTransitions(style) {
|
|
884
|
+
if (!style.transitionProperty)
|
|
885
|
+
return [];
|
|
886
|
+
const properties = style.transitionProperty.split(',').map(p => p.trim()).filter(Boolean);
|
|
887
|
+
const durations = style.transitionDurations.length > 0 ? style.transitionDurations : [style.transitionDuration];
|
|
888
|
+
const timings = style.transitionTimings.length > 0 ? style.transitionTimings : [style.transitionTimingFunction];
|
|
889
|
+
return properties.map((property, i) => ({
|
|
890
|
+
property,
|
|
891
|
+
duration: durations[i % durations.length] ?? 0,
|
|
892
|
+
timing: timings[i % timings.length] ?? 'ease',
|
|
893
|
+
}));
|
|
838
894
|
}
|
|
839
895
|
function parseDuration(value) {
|
|
840
896
|
if (value.endsWith('ms'))
|
|
@@ -12,6 +12,7 @@ export type { KeyframeResolution } from '../css/animation.js';
|
|
|
12
12
|
*/
|
|
13
13
|
export declare class AnimationClock {
|
|
14
14
|
private active;
|
|
15
|
+
/** Per-property transition runners, keyed `nodeId:property`. */
|
|
15
16
|
private transitions;
|
|
16
17
|
/** Last-seen target values per transitioned node, as CSS property → value. */
|
|
17
18
|
private transitionTargets;
|
|
@@ -51,7 +52,14 @@ export declare class AnimationClock {
|
|
|
51
52
|
stop(): void;
|
|
52
53
|
private discover;
|
|
53
54
|
private discoverTransitions;
|
|
54
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Snapshot the node's target values; start a per-property transition
|
|
57
|
+
* runner on any change, each with its own duration and timing. An
|
|
58
|
+
* interrupted transition continues from its current blended value
|
|
59
|
+
* rather than restarting from the previous target.
|
|
60
|
+
*/
|
|
55
61
|
private trackTransitionTargets;
|
|
62
|
+
/** Evaluate an in-flight transition's value for one property, now. */
|
|
63
|
+
private currentValue;
|
|
56
64
|
private updateTimer;
|
|
57
65
|
}
|
|
@@ -6,17 +6,6 @@ import { systemClock, clockFromNow } from './clock.js';
|
|
|
6
6
|
function easingFor(value) {
|
|
7
7
|
return parseEasing(value) ?? (t => t);
|
|
8
8
|
}
|
|
9
|
-
/** Parse a transition-property value into the tracked-property filter. */
|
|
10
|
-
function transitionedProperties(value) {
|
|
11
|
-
const names = new Set();
|
|
12
|
-
for (const raw of value.split(',')) {
|
|
13
|
-
const name = raw.trim();
|
|
14
|
-
if (name === 'all')
|
|
15
|
-
return { all: true, names };
|
|
16
|
-
names.add(name === 'background' ? 'background-color' : name);
|
|
17
|
-
}
|
|
18
|
-
return { all: false, names };
|
|
19
|
-
}
|
|
20
9
|
function cellValue(value) {
|
|
21
10
|
return typeof value === 'number' && value >= 0 ? `${value}cell` : null;
|
|
22
11
|
}
|
|
@@ -50,6 +39,7 @@ const FRAME_INTERVAL_MS = 33;
|
|
|
50
39
|
*/
|
|
51
40
|
export class AnimationClock {
|
|
52
41
|
active = new Map();
|
|
42
|
+
/** Per-property transition runners, keyed `nodeId:property`. */
|
|
53
43
|
transitions = new Map();
|
|
54
44
|
/** Last-seen target values per transitioned node, as CSS property → value. */
|
|
55
45
|
transitionTargets = new Map();
|
|
@@ -71,8 +61,13 @@ export class AnimationClock {
|
|
|
71
61
|
}
|
|
72
62
|
/** Whether this node's animation needs re-layout each frame (vs repaint only). */
|
|
73
63
|
touchesLayout(node) {
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
if (this.active.get(node.id)?.runner.touchesLayout)
|
|
65
|
+
return true;
|
|
66
|
+
for (const anim of this.transitions.values()) {
|
|
67
|
+
if (anim.node.id === node.id && anim.runner.touchesLayout)
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
76
71
|
}
|
|
77
72
|
/**
|
|
78
73
|
* Track transitioned elements and start a one-shot transition when a
|
|
@@ -87,7 +82,10 @@ export class AnimationClock {
|
|
|
87
82
|
for (const id of this.transitionTargets.keys()) {
|
|
88
83
|
if (!seen.has(id)) {
|
|
89
84
|
this.transitionTargets.delete(id);
|
|
90
|
-
this.transitions.
|
|
85
|
+
for (const key of this.transitions.keys()) {
|
|
86
|
+
if (key.startsWith(`${id}:`))
|
|
87
|
+
this.transitions.delete(key);
|
|
88
|
+
}
|
|
91
89
|
}
|
|
92
90
|
}
|
|
93
91
|
this.updateTimer();
|
|
@@ -116,15 +114,15 @@ export class AnimationClock {
|
|
|
116
114
|
return dirty;
|
|
117
115
|
}
|
|
118
116
|
applyEntries(entries, styles, dirty) {
|
|
119
|
-
for (const [
|
|
120
|
-
const style = styles.get(id);
|
|
117
|
+
for (const [key, anim] of entries) {
|
|
118
|
+
const style = styles.get(anim.node.id);
|
|
121
119
|
if (!style)
|
|
122
120
|
continue;
|
|
123
121
|
const elapsed = this.now() - anim.start;
|
|
124
122
|
anim.runner.apply(style, elapsed);
|
|
125
123
|
dirty.push({ node: anim.node, touchesLayout: anim.runner.touchesLayout });
|
|
126
124
|
if (anim.runner.isFinished(elapsed))
|
|
127
|
-
entries.delete(
|
|
125
|
+
entries.delete(key);
|
|
128
126
|
}
|
|
129
127
|
}
|
|
130
128
|
stop() {
|
|
@@ -140,18 +138,27 @@ export class AnimationClock {
|
|
|
140
138
|
const stops = name ? keyframes.get(name) : undefined;
|
|
141
139
|
if (style && name && stops && style.animationDuration > 0) {
|
|
142
140
|
const existing = this.active.get(node.id);
|
|
141
|
+
const resolved = resolution
|
|
142
|
+
? resolveKeyframeStops(stops, resolution, node.id)
|
|
143
|
+
: stops;
|
|
144
|
+
const resolvedKey = JSON.stringify(resolved);
|
|
143
145
|
if (!existing || existing.name !== name || existing.duration !== style.animationDuration) {
|
|
144
|
-
const resolved = resolution
|
|
145
|
-
? resolveKeyframeStops(stops, resolution, node.id)
|
|
146
|
-
: stops;
|
|
147
146
|
this.active.set(node.id, {
|
|
148
147
|
node,
|
|
149
148
|
runner: new AnimationRunner(resolved, style.animationDuration, style.animationIterationCount, easingFor(style.animationTimingFunction)),
|
|
150
149
|
name,
|
|
151
150
|
duration: style.animationDuration,
|
|
152
151
|
start: this.now(),
|
|
152
|
+
resolvedKey,
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
+
else if (existing.resolvedKey !== undefined && existing.resolvedKey !== resolvedKey) {
|
|
156
|
+
// var()/light-dark() re-resolved to new values (scheme
|
|
157
|
+
// flip, custom property change): retarget the runner
|
|
158
|
+
// without restarting — the original start time holds.
|
|
159
|
+
existing.runner = new AnimationRunner(resolved, style.animationDuration, style.animationIterationCount, easingFor(style.animationTimingFunction));
|
|
160
|
+
existing.resolvedKey = resolvedKey;
|
|
161
|
+
}
|
|
155
162
|
seen.add(node.id);
|
|
156
163
|
}
|
|
157
164
|
}
|
|
@@ -162,7 +169,7 @@ export class AnimationClock {
|
|
|
162
169
|
const subtreeResolved = parentResolved || (resolvedIds?.has(node.id) ?? false);
|
|
163
170
|
if (node.nodeType === 'element') {
|
|
164
171
|
const style = styles.get(node.id);
|
|
165
|
-
if (style
|
|
172
|
+
if (style && style.transitions.some(t => t.duration > 0)) {
|
|
166
173
|
seen.add(node.id);
|
|
167
174
|
if (subtreeResolved)
|
|
168
175
|
this.trackTransitionTargets(node, style);
|
|
@@ -172,12 +179,20 @@ export class AnimationClock {
|
|
|
172
179
|
this.discoverTransitions(child, styles, subtreeResolved, resolvedIds, seen);
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* Snapshot the node's target values; start a per-property transition
|
|
184
|
+
* runner on any change, each with its own duration and timing. An
|
|
185
|
+
* interrupted transition continues from its current blended value
|
|
186
|
+
* rather than restarting from the previous target.
|
|
187
|
+
*/
|
|
176
188
|
trackTransitionTargets(node, style) {
|
|
177
|
-
const
|
|
189
|
+
const configFor = (css) => style.transitions.find(t => t.property === css
|
|
190
|
+
|| (t.property === 'background' && css === 'background-color'))
|
|
191
|
+
?? style.transitions.find(t => t.property === 'all');
|
|
178
192
|
const targets = {};
|
|
179
193
|
for (const prop of TRANSITIONABLE) {
|
|
180
|
-
|
|
194
|
+
const config = configFor(prop.css);
|
|
195
|
+
if (!config || config.duration <= 0)
|
|
181
196
|
continue;
|
|
182
197
|
const value = prop.read(style);
|
|
183
198
|
if (value !== null)
|
|
@@ -187,28 +202,38 @@ export class AnimationClock {
|
|
|
187
202
|
this.transitionTargets.set(node.id, targets);
|
|
188
203
|
if (!previous)
|
|
189
204
|
return; // first sight — the initial style never transitions
|
|
190
|
-
const fromDecls = [];
|
|
191
|
-
const toDecls = [];
|
|
192
205
|
for (const [property, target] of Object.entries(targets)) {
|
|
193
206
|
const before = previous[property];
|
|
194
|
-
if (before
|
|
195
|
-
|
|
196
|
-
|
|
207
|
+
if (before === undefined || before === target)
|
|
208
|
+
continue;
|
|
209
|
+
const config = configFor(property);
|
|
210
|
+
const key = `${node.id}:${property}`;
|
|
211
|
+
// Interrupted mid-flight? Continue from the current value.
|
|
212
|
+
let from = before;
|
|
213
|
+
const inFlight = this.transitions.get(key);
|
|
214
|
+
if (inFlight) {
|
|
215
|
+
const current = this.currentValue(inFlight, style, property);
|
|
216
|
+
if (current !== null)
|
|
217
|
+
from = current;
|
|
197
218
|
}
|
|
219
|
+
const stops = [
|
|
220
|
+
{ offset: 0, declarations: [{ property, value: from }] },
|
|
221
|
+
{ offset: 1, declarations: [{ property, value: target }] },
|
|
222
|
+
];
|
|
223
|
+
this.transitions.set(key, {
|
|
224
|
+
node,
|
|
225
|
+
runner: new AnimationRunner(stops, config.duration, 1, easingFor(config.timing)),
|
|
226
|
+
name: property,
|
|
227
|
+
duration: config.duration,
|
|
228
|
+
start: this.now(),
|
|
229
|
+
});
|
|
198
230
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.transitions.set(node.id, {
|
|
206
|
-
node,
|
|
207
|
-
runner: new AnimationRunner(stops, style.transitionDuration, 1, easingFor(style.transitionTimingFunction)),
|
|
208
|
-
name: '',
|
|
209
|
-
duration: style.transitionDuration,
|
|
210
|
-
start: this.now(),
|
|
211
|
-
});
|
|
231
|
+
}
|
|
232
|
+
/** Evaluate an in-flight transition's value for one property, now. */
|
|
233
|
+
currentValue(anim, base, property) {
|
|
234
|
+
const scratch = { ...base };
|
|
235
|
+
anim.runner.apply(scratch, this.now() - anim.start);
|
|
236
|
+
return TRANSITIONABLE.find(p => p.css === property)?.read(scratch) ?? null;
|
|
212
237
|
}
|
|
213
238
|
updateTimer() {
|
|
214
239
|
if (this.activeCount > 0 && this.timer === null) {
|
package/docs/motion.md
CHANGED
|
@@ -66,19 +66,20 @@ inline style updates, a `:checked` rule starts applying:
|
|
|
66
66
|
`ms`/`s`. The same interpolation rules as animations apply. The initial
|
|
67
67
|
style never transitions.
|
|
68
68
|
|
|
69
|
+
Transitions are configured per property, per spec: comma-separated
|
|
70
|
+
shorthand groups (`transition: color 150ms linear, width 400ms ease`)
|
|
71
|
+
or longhand lists paired cyclically. An interrupted transition
|
|
72
|
+
continues from its current blended value. A timing function declared
|
|
73
|
+
*inside a keyframe* applies from that stop to the next, overriding the
|
|
74
|
+
element's. Keyframe `var()`/`light-dark()` values re-resolve when the
|
|
75
|
+
scheme or custom properties change mid-animation, retargeting without
|
|
76
|
+
restarting.
|
|
77
|
+
|
|
69
78
|
## Deviations from browsers
|
|
70
79
|
|
|
71
|
-
- One duration and one timing function apply to all listed transition
|
|
72
|
-
properties (per-property lists aren't split).
|
|
73
|
-
- An interrupted transition restarts from its previous target value, not
|
|
74
|
-
the current blended value.
|
|
75
80
|
- `opacity` doesn't interpolate (it applies discretely mid-animation) —
|
|
76
81
|
animate colour toward the background for a smooth fade.
|
|
77
|
-
-
|
|
78
|
-
animation starts; changing a custom property doesn't retarget a
|
|
79
|
-
running animation.
|
|
80
|
-
- Per-keyframe `animation-timing-function` overrides are ignored — the
|
|
81
|
-
element's timing function applies to every segment.
|
|
82
|
+
- `transition-delay` / `animation-delay` are not implemented.
|
|
82
83
|
|
|
83
84
|
## Reduced motion
|
|
84
85
|
|
package/docs/reference.md
CHANGED
|
@@ -176,7 +176,7 @@ adaptations. Lengths are cells (`cell`/`ch`, `%`, or `calc()`).
|
|
|
176
176
|
| [Tables](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_table) | `border-collapse`, `border-spacing`, `caption-side`, `table-layout`, `empty-cells`, `vertical-align` (`baseline` ≈ `top`) |
|
|
177
177
|
| Borders | `border`/`border-style`/`border-color`/`border-corner` + per-side toggles (terminal values above) |
|
|
178
178
|
| [Animation](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations) | `animation` shorthand, `animation-name/-duration/-iteration-count` (incl. `infinite`)/`-timing-function`, `@keyframes` (from/to/percentages, values resolve `var()`/`light-dark()`) |
|
|
179
|
-
| [Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions) | `transition` shorthand
|
|
179
|
+
| [Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions) | `transition` shorthand with per-property comma groups; `transition-property`/`-duration`/`-timing-function` longhand lists paired per spec; interruptions continue from the current value |
|
|
180
180
|
| [Easing](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function) | `linear`, `ease` (default), `ease-in`, `ease-out`, `ease-in-out`, `cubic-bezier()`, `steps()`, `step-start`, `step-end` |
|
|
181
181
|
|
|
182
182
|
### Animation & transition semantics on the grid
|
|
@@ -185,12 +185,14 @@ Colours interpolate in RGB at ~30fps; single cell/ch lengths interpolate
|
|
|
185
185
|
to whole cells (movement steps cell by cell); every other supported
|
|
186
186
|
property applies discretely, switching at the segment midpoint (the CSS
|
|
187
187
|
rule for non-interpolable values). Layout-affecting animations re-flow
|
|
188
|
-
each frame. Easing applies per keyframe segment
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
`
|
|
188
|
+
each frame. Easing applies per keyframe segment (a timing function
|
|
189
|
+
declared inside a keyframe overrides the element's for that segment);
|
|
190
|
+
non-interpolable values switch when eased progress crosses the midpoint.
|
|
191
|
+
Transitions run per property with their own duration/timing;
|
|
192
|
+
interruptions continue from the current blended value. Keyframe
|
|
193
|
+
`var()`/`light-dark()` re-resolves on scheme/custom-property changes
|
|
194
|
+
without restarting the animation. Deviation: no `transition-delay` /
|
|
195
|
+
`animation-delay`.
|
|
194
196
|
|
|
195
197
|
## Values, functions and at-rules
|
|
196
198
|
|