Files

516 lines
17 KiB
JavaScript

/* device/shared/motion.js: Glass Shell motion library (package P5).
GENERATED by docs/phase2/research/springs.py --write. Do not edit by hand:
change springs.py and re-run it (PLAN 1.5). Contract: docs/phase2/contracts/motion.md section 3.
version ed4e044810
Closed-form springs (MO 8), the token table (D2 11.2) and a motion audit.
No dependencies and no side effects at load other than publishing the API:
module.exports (P1's loader: rt.shared.motion; node) and globalThis.__LGS_MOTION
(P1 deletes it at teardown; in vr:systemui call __LGS_MOTION.remove()).
Times are milliseconds, except spring(), which keeps MO 8's seconds. */
(function (root, factory) {
'use strict';
var api = factory(root);
if (typeof module === 'object' && module && typeof module.exports === 'object') module.exports = api;
if (root) root.__LGS_MOTION = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function (root) {
'use strict';
const DATA = {
"beziers": {
"b0": "cubic-bezier(.327, .692, .114, 1)",
"b15": "cubic-bezier(.311, .589, .077, 1.118)",
"b20": "cubic-bezier(.33, .632, .076, 1.138)",
"b25": "cubic-bezier(.344, .63, .084, 1.171)"
},
"composites": {
"covIn": "linear(0, .008 1.4%, .031 2.9%, .071 4.5%, .128 6.2%, .196 7.8%, .287 9.8%, .672 17.0%, .819 20.1%, .885 21.8%, .936 23.4%, .972 24.9%, .993 26.3%, 1.000 27.6%, 1)",
"covOut": "linear(0, .007 4.9%, .028 10.1%, .064 15.4%, .115 21.1%, .184 27.4%, .272 34.3%, .685 62.6%, .774 69.2%, .844 75.1%, .914 81.9%, .962 88.2%, .990 94.2%, 1)",
"covOutAt": "86.5%",
"glassIn": "linear(0, 1 92%, 1)",
"largeCovIn": "linear(0, 1.000 73.6%, 1)",
"matOut": "linear(0, 0 55%, 1)",
"morphContent": "linear(0, 1 41.2%, 1)",
"sheetContent": "linear(0, 1 60%, 1)",
"sheetCovIn": "linear(0, .006 1.1%, .025 2.3%, .057 3.7%, .104 5.2%, .205 7.8%, .554 16.3%, .658 19.1%, .751 21.9%, .826 24.5%, .892 27.1%, .950 29.8%, 1.000 32.4%, 1)",
"sheetCovOut": "linear(0, .001 0.5%, .235 6.6%, .378 10.7%, .482 14.0%, .572 17.3%, .660 21.2%, .734 25.2%, .798 29.4%, .849 33.9%, .891 38.7%, .924 44.0%, .949 49.8%, .968 56.4%, .990 72.5%, 1)",
"sheetCovOutAt": "8.5%",
"toastCov": "linear(0, .008 0.8%, .031 1.5%, .068 2.3%, .122 3.1%, .188 3.9%, .279 4.9%, .676 8.8%, .829 10.4%, .898 11.3%, .948 12.2%, .982 13.0%, 1.000 13.8%, 1)",
"toastGlass": "linear(0, 1 47.1%, 1)"
},
"curves": {
"b0": "linear(0, .005 1.2%, .020 2.3%, .084 5.2%, .159 7.7%, .461 16.9%, .556 20.2%, .639 23.5%, .709 26.9%, .767 30.2%, .817 33.7%, .861 37.6%, .900 42.1%, .930 46.9%, .954 52.4%, .971 58.6%, .991 73.8%, 1)",
"b15": "linear(0, .006 1.3%, .024 2.7%, .054 4.2%, .093 5.7%, .188 8.7%, .518 18.2%, .613 21.4%, .696 24.5%, .767 27.7%, .825 30.9%, .872 34.1%, .913 37.6%, .945 41.4%, .970 45.6%, .988 50.3%, 1.000 55.8%, 1.006 67.9%, 1)",
"b20": "linear(0, .006 1.3%, .025 2.8%, .056 4.3%, .099 6.0%, .199 9.2%, .536 18.9%, .633 22.0%, .717 25.2%, .785 28.2%, .844 31.4%, .894 34.7%, .933 38.1%, .964 41.7%, .987 45.7%, 1.003 50.3%, 1.012 55.3%, 1.015 64.8%, 1)",
"b25": "linear(0, .007 1.5%, .026 3.0%, .058 4.7%, .104 6.5%, .207 9.8%, .556 20.0%, .656 23.4%, .739 26.5%, .806 29.5%, .865 32.7%, .913 35.9%, .952 39.2%, .983 42.9%, 1.005 46.7%, 1.019 50.9%, 1.027 55.8%, 1.027 63.8%, 1)"
},
"delays": {
"back-title": 600,
"dwell": 80,
"page": 40,
"reveal": 800,
"reveal-out": 200,
"tab-label": 400
},
"keyframes": [
"lgs-mat-glass-in",
"lgs-mat-glass-out",
"lgs-mat-large-in",
"lgs-mat-content-in",
"lgs-mat-content-out",
"lgs-toast-in",
"lgs-sheet-in",
"lgs-sheet-out",
"lgs-sheet-content-in",
"lgs-sheet-content-out",
"lgs-morph",
"lgs-morph-content",
"lgs-catch",
"lgs-focus-in",
"lgs-page-in",
"lgs-page-out",
"lgs-arrive",
"lgs-knob-lift",
"lgs-shift-in",
"lgs-fade-in",
"lgs-fade-out",
"lgs-edge-in"
],
"linear": [
{
"ms": 250,
"name": "mat-in"
},
{
"ms": 350,
"name": "mat-out"
},
{
"ms": 150,
"name": "page-out"
},
{
"ms": 90,
"name": "glow-off"
},
{
"ms": 120,
"name": "swap"
},
{
"ms": 180,
"name": "reduce"
}
],
"ramps": [
[
"Light",
0.0,
0.6
],
[
"Lens",
0.0,
0.7
],
[
"Frost",
0.2,
0.92
],
[
"Tint",
0.2,
0.92
],
[
"Shade",
0.3,
0.92
],
[
"Shadow",
0.4,
1.0
],
[
"Alpha",
0.0,
0.3
]
],
"reduceMs": {
"depth": 180,
"fade": 180,
"glow-off": 90,
"hover-in": 150,
"interactive": 150,
"mat-in": 180,
"mat-out": 180,
"morph-close": 150,
"morph-open": 180,
"page": 180,
"page-out": 150,
"reduce": 180,
"release-touch": 180,
"sheet-in": 200,
"sheet-out": 180,
"snappy": 180,
"swap": 120
},
"stagger": [
30,
5
],
"tokens": [
{
"b": 0.14,
"d": 0.15,
"ease": "b15",
"ms": 210,
"name": "interactive",
"overshoot": 0.005,
"t50": 38,
"t90": 78,
"t98": 103
},
{
"b": 0.0,
"d": 0.2,
"ease": "b0",
"ms": 294,
"name": "hover-in",
"overshoot": 0.0,
"t50": 54,
"t90": 124,
"t98": 186
},
{
"b": 0.0,
"d": 0.3,
"ease": "b0",
"ms": 441,
"name": "fade",
"overshoot": 0.0,
"t50": 80,
"t90": 186,
"t98": 279
},
{
"b": 0.15,
"d": 0.35,
"ease": "b15",
"ms": 488,
"name": "snappy",
"overshoot": 0.0063,
"t50": 87,
"t90": 178,
"t98": 234
},
{
"b": 0.25,
"d": 0.4,
"ease": "b25",
"ms": 510,
"name": "release-touch",
"overshoot": 0.0284,
"t50": 94,
"t90": 178,
"t98": 216
},
{
"b": 0.2,
"d": 0.45,
"ease": "b20",
"ms": 607,
"name": "morph-open",
"overshoot": 0.0152,
"t50": 108,
"t90": 214,
"t98": 269
},
{
"b": 0.0,
"d": 0.3,
"ease": "b0",
"ms": 441,
"name": "morph-close",
"overshoot": 0.0,
"t50": 80,
"t90": 186,
"t98": 279
},
{
"b": 0.0,
"d": 0.5,
"ease": "b0",
"ms": 735,
"name": "sheet-in",
"overshoot": 0.0,
"t50": 134,
"t90": 310,
"t98": 464
},
{
"b": 0.0,
"d": 0.35,
"ease": "b0",
"ms": 514,
"name": "sheet-out",
"overshoot": 0.0,
"t50": 94,
"t90": 217,
"t98": 325
},
{
"b": 0.0,
"d": 0.45,
"ease": "b0",
"ms": 662,
"name": "page",
"overshoot": 0.0,
"t50": 120,
"t90": 279,
"t98": 418
},
{
"b": 0.0,
"d": 0.3,
"ease": "b0",
"ms": 441,
"name": "depth",
"overshoot": 0.0,
"t50": 80,
"t90": 186,
"t98": 279
}
],
"version": "ed4e044810"
};
const clock = () => (root && root.performance && root.performance.now ? root.performance.now() : Date.now());
const tokens = {};
for (const t of DATA.tokens) {
tokens[t.name] = Object.freeze(Object.assign({}, t, {
durVar: '--lgs-d-' + t.name, easeVar: '--lgs-ease-' + t.ease }));
}
for (const t of DATA.linear) {
tokens[t.name] = Object.freeze({ name: t.name, linear: true, ms: t.ms, durVar: '--lgs-d-' + t.name,
easing: t.name === 'mat-out' ? DATA.composites.matOut : 'linear' });
}
Object.freeze(tokens);
const curves = Object.freeze(Object.assign({}, DATA.curves, { matOut: DATA.composites.matOut }));
const reduceMs = Object.freeze(Object.assign({}, DATA.reduceMs));
const delays = Object.freeze(Object.assign({}, DATA.delays, { stagger: DATA.stagger[0], staggerMax: DATA.stagger[1] }));
function tok(name) {
const k = tokens[name];
if (!k) throw new Error('lgs motion: unknown token ' + name);
return k;
}
// MO 8. y = x - target; returns [y(t), y'(t)] for y(0) = y0, y'(0) = v0.
// d = perceptual duration (s), b = bounce (-1..1), mass 1, t in seconds.
function spring(y0, v0, t, d, b) {
const w = 2 * Math.PI / d, z = b >= 0 ? 1 - b : 1 / (1 + b);
if (Math.abs(z - 1) < 1e-6) { // critically damped
const e = Math.exp(-w * t), B = v0 + w * y0;
return [e * (y0 + B * t), e * (B - w * (y0 + B * t))];
}
if (z < 1) { // underdamped
const wd = w * Math.sqrt(1 - z * z), e = Math.exp(-z * w * t);
const B = (v0 + z * w * y0) / wd, c = Math.cos(wd * t), s = Math.sin(wd * t);
return [e * (y0 * c + B * s), e * ((-z * w) * (y0 * c + B * s) + (-y0 * wd * s + B * wd * c))];
}
const q = Math.sqrt(z * z - 1), r1 = -w * (z - q), r2 = -w * (z + q); // overdamped
const A = (v0 - r2 * y0) / (r1 - r2), C = y0 - A;
return [A * Math.exp(r1 * t) + C * Math.exp(r2 * t), A * r1 * Math.exp(r1 * t) + C * r2 * Math.exp(r2 * t)];
}
// Progress of a 0 -> 1 step `ms` after it started (overshoot included); 1 from the settle on.
function sample(name, ms) {
const k = tok(name);
if (!(ms > 0)) return 0;
if (ms >= k.ms) return 1;
if (k.linear) return ms / k.ms;
return 1 + spring(-1, 0, ms / 1000, k.d, k.b)[0];
}
// d(progress)/dt in 1/s.
function velocity(name, ms) {
const k = tok(name);
if (!(ms >= 0) || ms >= k.ms) return 0;
if (k.linear) return 1000 / k.ms;
return spring(-1, 0, ms / 1000, k.d, k.b)[1];
}
function settle(name, opts) {
const k = tok(name);
return opts && opts.reduce ? reduceMs[name] : k.ms;
}
function easingOf(name, reduce) {
const k = tok(name);
if (k.linear) return k.easing;
return reduce ? curves.b0 : curves[k.ease];
}
// For el.animate(): literal strings, no var().
function timing(name, opts) {
opts = opts || {};
return { duration: settle(name, opts), easing: easingOf(name, !!opts.reduce), delay: opts.delay || 0,
fill: opts.fill || 'backwards', iterations: 1 };
}
function css(name) {
const k = tok(name);
if (k.linear) return 'var(--lgs-d-' + name + ') ' + (name === 'mat-out' ? 'var(--lgs-ease-mat-out)' : 'linear');
return 'var(--lgs-d-' + name + ') var(--lgs-ease-' + name + ')';
}
// A retargetable spring (MO 8, C5): .to() starts from the current value and
// velocity; .done() latches once |y| < 0.001 travel and |v| < 0.01 travel / d,
// and the value is then exactly the target (push it once, then stop).
function create(name, opts) {
const k = tok(name);
opts = opts || {};
const reduce = !!opts.reduce;
let target = +opts.value || 0, from = target, y0 = 0, v0 = 0, t0 = 0, travel = 1, still = true;
function state(now) {
if (still) return [target, 0];
const ms = Math.max(0, now - t0);
if (k.linear) {
if (ms >= k.ms) { still = true; return [target, 0]; }
return [from + (target - from) * ms / k.ms, (target - from) * 1000 / k.ms];
}
const r = spring(y0, v0, ms / 1000, k.d, k.b);
if ((Math.abs(r[0]) < 1e-3 * travel && Math.abs(r[1]) < 1e-2 * travel / k.d) || ms > 4 * k.ms) {
still = true;
return [target, 0];
}
return [target + r[0], r[1]];
}
const at = (now) => state(now === undefined ? clock() : now);
const s = {
get target() { return target; },
get token() { return name; },
to(next, now) {
next = +next;
if (now === undefined) now = clock();
const cur = state(now);
if (next === target) return s;
target = next;
if (reduce) { still = true; return s; }
from = cur[0]; y0 = cur[0] - next; v0 = cur[1]; t0 = now;
travel = Math.max(Math.abs(y0), Math.abs(v0) * k.d / (2 * Math.PI) || 0, 1e-9);
still = false;
return s;
},
value(now) { return at(now)[0]; },
velocity(now) { return at(now)[1]; },
done(now) { at(now); return still; },
jump(v) { target = +v; still = true; return s; },
};
return s;
}
function reduced(win) {
try { return !!(win || root).matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (e) { return false; }
}
// ---- audit (G-MOTION, VP P-52 / P-58)
function parseLinear(str) {
const m = /^\s*linear\(([^)]*)\)\s*$/.exec(String(str));
if (!m) return null;
const pts = [];
for (const item of m[1].split(',')) {
const parts = item.trim().split(/\s+/);
const v = parseFloat(parts[0]);
const ps = parts.slice(1).map(parseFloat);
if (!ps.length) pts.push([v, null]);
for (const p of ps) pts.push([v, p]);
}
if (!pts.length) return null;
if (pts[0][1] === null) pts[0][1] = 0;
if (pts[pts.length - 1][1] === null) pts[pts.length - 1][1] = 100;
let hi = pts[0][1];
for (const p of pts) if (p[1] !== null) { if (p[1] < hi) p[1] = hi; hi = p[1]; }
for (let i = 1; i < pts.length; i++) {
if (pts[i][1] !== null) continue;
let j = i;
while (pts[j][1] === null) j++;
const a = pts[i - 1][1], b = pts[j][1];
for (let q = i; q < j; q++) pts[q][1] = a + (b - a) * (q - i + 1) / (j - i + 1);
i = j;
}
return pts;
}
function sameLinear(a, b) {
if (!a || !b || a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (Math.abs(a[i][0] - b[i][0]) > 1.5e-3 || Math.abs(a[i][1] - b[i][1]) > 0.06) return false;
}
return true;
}
const allowedEasings = Object.values(DATA.curves).concat(Object.values(DATA.composites).filter((s) => /^linear\(/.test(s)));
const allowedLinear = allowedEasings.map(parseLinear);
const allowedBezier = Object.values(DATA.beziers).map((s) => s.match(/-?[\d.]+/g).map(Number));
const allowedDurations = [...new Set(Object.values(tokens).map((t) => t.ms).concat(Object.values(reduceMs)))].sort((a, b) => a - b);
function isTokenEasing(str) {
str = String(str).trim();
if (str === 'linear') return true;
if (/^cubic-bezier\(/.test(str)) {
const v = str.match(/-?[\d.]+/g).map(Number);
return allowedBezier.some((b) => b.every((x, i) => Math.abs(x - v[i]) < 1.5e-3));
}
const p = parseLinear(str);
return !!p && allowedLinear.some((q) => sameLinear(p, q));
}
function isTokenDuration(ms) {
return allowedDurations.some((d) => Math.abs(d - ms) <= 0.5);
}
function describe(el, pseudo) {
if (!el) return '';
let s = (el.tagName || '').toLowerCase();
if (el.id) s += '#' + el.id;
const cls = el.classList ? [...el.classList].slice(0, 3) : [];
if (cls.length) s += '.' + cls.join('.');
return s + (pseudo || '');
}
// A scroll-driven animation (animation-timeline: a scroll() / view() / named scroll timeline,
// e.g. lgs-edge-in, MO 4.13) tracks the scroll position, not time: it is "running" for as long
// as its scroller exists, yet nothing moves while the scroll is still. Probe P5-K4 (wp/P5.md).
function isScrollDriven(a) {
const tl = a && a.timeline;
if (!tl) return false;
const n = (tl.constructor && tl.constructor.name) || '';
return /^(Scroll|View)Timeline$/.test(n) || (typeof tl.source !== 'undefined' && typeof tl.axis !== 'undefined');
}
function audit(doc) {
doc = doc || (root && root.document);
const out = [];
for (const a of doc.getAnimations()) {
const e = a.effect;
if (!e) continue;
const tm = e.getTiming();
const kind = (a.constructor && a.constructor.name) || 'Animation';
const scroll = isScrollDriven(a);
let easings = [tm.easing];
if (kind === 'CSSAnimation' && e.getKeyframes) easings = [...new Set(e.getKeyframes().map((f) => f.easing))];
out.push({ name: a.animationName || a.transitionProperty || a.id || '', kind,
target: describe(e.target, e.pseudoElement), duration: tm.duration, delay: tm.delay, easings,
iterations: tm.iterations, fill: tm.fill, playState: a.playState, scroll,
tokenDuration: scroll || isTokenDuration(+tm.duration), tokenEasing: easings.every(isTokenEasing) });
}
return out;
}
const api = {
version: DATA.version,
tokens, names: Object.keys(tokens), reduceMs, delays, curves,
beziers: Object.freeze(Object.assign({}, DATA.beziers)),
composites: Object.freeze(Object.assign({}, DATA.composites)),
keyframes: Object.freeze(DATA.keyframes.slice()),
ramps: Object.freeze(DATA.ramps.map((r) => Object.freeze({ name: r[0], a: r[1], b: r[2] }))),
spring, sample, velocity, settle, timing, css, create, reduced,
allowed: Object.freeze({ durations: Object.freeze(allowedDurations), easings: Object.freeze(allowedEasings.slice()) }),
isTokenEasing, isTokenDuration, isScrollDriven, audit,
remove() { try { if (root && root.__LGS_MOTION === api) delete root.__LGS_MOTION; } catch (e) { /* frozen global */ } },
};
return Object.freeze(api);
});