Sample the springs over M3's web durations, and add the Standard scheme

bin/springs.mjs is the sampler behind motion.css: damping ratio and stiffness in,
the 49-point linear() and the settle time out. Run with --settle it reproduces
the six curves the file carried byte for byte, so only the window changed.

Each spring is now sampled over the duration M3 publishes for the web (spatial
350/500/650 ms, effects 150/200/300 ms) instead of stopping at its own settle
time, so the *-duration tokens read Google's numbers. Sampling stays in real
time — point i is the spring at duration × i / 48 — so the bounce lands at the
same millisecond as before; a longer duration just holds the tail flat at 1, and
a shorter one pins the last point, at most 0.3% of the travel in one frame. Each
token's comment records damping, stiffness, settle time and sampled duration.

M3's second motion scheme is here too: [data-motion="standard"] swaps the three
spatial springs (damping 0.9, 0.2% overshoot instead of 9%) and their durations;
the effects springs sample identically in both schemes, so they are shared. The
reduced-motion block names both selectors, so it still zeroes every duration.

Plan: docs/plans/material-3-alignment.md step 5, findings core C8, C9.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 05:29:32 +02:00
co-authored by Claude Fable 5.1
parent 2feb623ec0
commit 5b0ca26857
5 changed files with 289 additions and 28 deletions
+41 -2
View File
@@ -113,13 +113,16 @@ it('reads every safe-area inset through a variable that can replace it', functio
->and(File::get(packageCss('components/app-bar.css')))->toContain('padding-top: var(--material-safe-top, env(safe-area-inset-top));');
});
it('makes every motion token instant under reduced motion', function () {
it('makes every motion token instant under reduced motion, in both schemes', function () {
$motion = File::get(packageCss('tokens/motion.css'));
$reduced = Str::of($motion)->after('prefers-reduced-motion: reduce')->toString();
preg_match_all('/(--md-sys-motion-[\w-]*duration[\w-]*):/', Str::before($motion, '@media'), $durations);
expect($durations[1])->not->toBeEmpty();
expect($durations[1])->not->toBeEmpty()
// The Standard scheme sets the same variables on a selector of its own, so :root alone
// would not reach it: the block has to name both.
->and($reduced)->toMatch('/:root,\s*\[data-motion="standard"\] \{/');
foreach ($durations[1] as $duration) {
expect($reduced)->toContain("{$duration}: 0ms;");
@@ -164,3 +167,39 @@ it('gives every emphasized type style its own tracking, as Compose does', functi
'--md-sys-typescale-emphasized-label-md-tracking' => '0.03125rem',
]);
});
it('reads every spring over the duration M3 publishes for the web', function () {
$expressive = declarations(File::get(packageCss('tokens/motion.css')), ':root');
// docs/reference/m3/styles.md § Motion, "Web curve equivalents for springs".
$durations = [
'spatial-fast' => '350ms', 'spatial-default' => '500ms', 'spatial-slow' => '650ms',
'effects-fast' => '150ms', 'effects-default' => '200ms', 'effects-slow' => '300ms',
];
foreach ($durations as $spring => $duration) {
expect($expressive["--md-sys-motion-{$spring}-duration"])->toBe($duration)
// 49 sampled points, starting at rest and landing exactly on the target.
->and($expressive["--md-sys-motion-{$spring}"])->toStartWith('linear(0, ')->toEndWith(', 1)')
->and(explode(', ', $expressive["--md-sys-motion-{$spring}"]))->toHaveCount(49);
}
});
it('swaps only the spatial springs for the Standard motion scheme', function () {
$motion = File::get(packageCss('tokens/motion.css'));
$expressive = declarations($motion, ':root');
$standard = declarations($motion, '[data-motion="standard"] {');
// Both schemes share the effects springs, so the block carries the three spatial pairs only.
expect(array_keys($standard))->toBe([
'--md-sys-motion-spatial-fast', '--md-sys-motion-spatial-fast-duration',
'--md-sys-motion-spatial-default', '--md-sys-motion-spatial-default-duration',
'--md-sys-motion-spatial-slow', '--md-sys-motion-spatial-slow-duration',
]);
foreach (['fast' => '350ms', 'default' => '500ms', 'slow' => '750ms'] as $speed => $duration) {
expect($standard["--md-sys-motion-spatial-{$speed}-duration"])->toBe($duration)
->and($standard["--md-sys-motion-spatial-{$speed}"])->toStartWith('linear(0, ')
->not->toBe($expressive["--md-sys-motion-spatial-{$speed}"]);
}
});