/* 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); });