retake-dev 0.5.0 → 0.5.1
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 +76 -1
- package/README.md +141 -7
- package/bin/retake.js +14 -1
- package/package.json +6 -1
- package/src/core.js +14 -1
- package/src/next-dev.js +183 -0
- package/src/next.js +81 -0
- package/src/note-text.js +844 -0
- package/src/plugin.js +6 -2
- package/src/runtime/10-animations.js +9 -0
- package/src/runtime/60-preview.js +3 -1
- package/src/runtime/62-motion.js +106 -0
- package/src/runtime/65-timeline.js +39 -0
- package/src/runtime/67-csssource.js +73 -1
- package/src/runtime/70-boot.js +9 -2
- package/src/server/api.js +73 -1
- package/src/server/front.js +3 -1
- package/src/server/mcp.js +213 -16
- package/src/server/moments.js +317 -0
- package/src/server/sourcemap.js +192 -0
- package/src/shell/10-dock.js +27 -5
- package/src/shell/15-session.js +11 -5
- package/src/shell/20-timeline.js +30 -5
- package/src/shell/25-input.js +25 -2
- package/src/shell/30-notes.js +704 -110
- package/src/shell/32-anims.js +1353 -0
- package/src/shell/shell.css +24 -0
- package/src/shell/shell.html +1 -0
- package/types/client.d.ts +7 -0
- package/types/index.d.ts +2 -0
- package/types/next.d.ts +50 -0
package/src/note-text.js
ADDED
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
// A note as text for a coding agent, written once for both readers: the dock's
|
|
2
|
+
// "Copy for agent" (core.js inlines this file into the dock, `export`s
|
|
3
|
+
// stripped) and `retake mcp` (get_note, get_animation, list_notes). Plain
|
|
4
|
+
// functions, no DOM, no Node.
|
|
5
|
+
//
|
|
6
|
+
// The words it uses (CONTRACT.md "Note"):
|
|
7
|
+
// recording time ms on the recording's clock, shown as 00:01.23 from its start
|
|
8
|
+
// local time ms on one animation's own clock: 0 = the end of its delay
|
|
9
|
+
// progress local / duration (0-1, linear); eased = after the effect's easing
|
|
10
|
+
// segment the two keyframes the eased progress is between
|
|
11
|
+
// Keyframe offsets apply to the eased progress, so a keyframe's local time goes
|
|
12
|
+
// through the inverse of the effect's easing (localOfOffset).
|
|
13
|
+
|
|
14
|
+
const FRAME = 1000 / 60
|
|
15
|
+
|
|
16
|
+
/** ms → "00:10.91" */
|
|
17
|
+
const clockText = (ms) => {
|
|
18
|
+
const s = Math.max(0, Number(ms) || 0) / 1000
|
|
19
|
+
return `${String(Math.floor(s / 60)).padStart(2, "0")}:${(s % 60).toFixed(2).padStart(5, "0")}`
|
|
20
|
+
}
|
|
21
|
+
/** ms → "200ms" / "1210ms" (local times stay in ms: what CSS, WAAPI and Motion write) */
|
|
22
|
+
const msText = (ms) => `${Math.round(Number(ms) || 0)}ms`
|
|
23
|
+
const num = (v, d = 2) => {
|
|
24
|
+
const n = Number(v)
|
|
25
|
+
if (!Number.isFinite(n)) return String(v)
|
|
26
|
+
const s = n.toFixed(d)
|
|
27
|
+
return s.includes(".") ? s.replace(/0+$/, "").replace(/\.$/, "") || "0" : s
|
|
28
|
+
}
|
|
29
|
+
const pct = (p) => `${num((Number(p) || 0) * 100, 1)}%`
|
|
30
|
+
const kebab = (p) => String(p).replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()).replace(/^webkit-/, "-webkit-")
|
|
31
|
+
|
|
32
|
+
// ---- easing -------------------------------------------------------------------------
|
|
33
|
+
// CSS easing functions, evaluated without a browser (the MCP server has none):
|
|
34
|
+
// linear, ease*, cubic-bezier(), steps(), step-start/end, linear(…).
|
|
35
|
+
|
|
36
|
+
const NAMED = { ease: [0.25, 0.1, 0.25, 1], "ease-in": [0.42, 0, 1, 1], "ease-out": [0, 0, 0.58, 1], "ease-in-out": [0.42, 0, 0.58, 1] }
|
|
37
|
+
|
|
38
|
+
function bezier(x1, y1, x2, y2) {
|
|
39
|
+
const at = (a, b, t) => 3 * a * (1 - t) * (1 - t) * t + 3 * b * (1 - t) * t * t + t * t * t
|
|
40
|
+
return (x) => {
|
|
41
|
+
if (x <= 0) return 0
|
|
42
|
+
if (x >= 1) return 1
|
|
43
|
+
let lo = 0
|
|
44
|
+
let hi = 1
|
|
45
|
+
for (let i = 0; i < 40; i++) {
|
|
46
|
+
const mid = (lo + hi) / 2
|
|
47
|
+
if (at(x1, x2, mid) < x) lo = mid
|
|
48
|
+
else hi = mid
|
|
49
|
+
}
|
|
50
|
+
return at(y1, y2, (lo + hi) / 2)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function steps(n, pos) {
|
|
55
|
+
const jumps = pos === "jump-none" ? n - 1 : pos === "jump-both" ? n + 1 : n
|
|
56
|
+
const start = pos === "start" || pos === "jump-start" || pos === "jump-both"
|
|
57
|
+
return (x) => {
|
|
58
|
+
if (x >= 1) return 1
|
|
59
|
+
let step = Math.floor(x * n)
|
|
60
|
+
if (start) step += 1
|
|
61
|
+
return Math.min(Math.max(step / Math.max(jumps, 1), 0), 1)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// linear(0, 0.25 40%, 1): points with optional input percentages.
|
|
66
|
+
function linearFn(body) {
|
|
67
|
+
const pts = []
|
|
68
|
+
for (const part of body.split(/,(?![^(]*\))/)) {
|
|
69
|
+
const bits = part.trim().split(/\s+/)
|
|
70
|
+
const y = Number(bits[0])
|
|
71
|
+
const xs = bits.slice(1).map((b) => parseFloat(b) / 100)
|
|
72
|
+
if (!xs.length) pts.push({ y, x: null })
|
|
73
|
+
for (const x of xs) pts.push({ y, x })
|
|
74
|
+
}
|
|
75
|
+
if (!pts.length) return (x) => x
|
|
76
|
+
if (pts[0].x == null) pts[0].x = 0
|
|
77
|
+
if (pts[pts.length - 1].x == null) pts[pts.length - 1].x = 1
|
|
78
|
+
for (let i = 1; i < pts.length; i++) if (pts[i].x != null && pts[i].x < pts[i - 1].x) pts[i].x = pts[i - 1].x
|
|
79
|
+
for (let i = 1; i < pts.length; i++) {
|
|
80
|
+
if (pts[i].x != null) continue
|
|
81
|
+
let j = i
|
|
82
|
+
while (pts[j].x == null) j++
|
|
83
|
+
const a = pts[i - 1].x
|
|
84
|
+
const step = (pts[j].x - a) / (j - i + 1)
|
|
85
|
+
for (let k = i; k < j; k++) pts[k].x = a + step * (k - i + 1)
|
|
86
|
+
}
|
|
87
|
+
return (x) => {
|
|
88
|
+
if (x <= pts[0].x) return pts[0].y
|
|
89
|
+
for (let i = 1; i < pts.length; i++) {
|
|
90
|
+
const a = pts[i - 1]
|
|
91
|
+
const b = pts[i]
|
|
92
|
+
if (x <= b.x) return b.x === a.x ? b.y : a.y + ((x - a.x) / (b.x - a.x)) * (b.y - a.y)
|
|
93
|
+
}
|
|
94
|
+
return pts[pts.length - 1].y
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const easings = new Map()
|
|
99
|
+
/** An easing string → x ↦ y. Anything it can't read is linear. */
|
|
100
|
+
export function easingFn(easing) {
|
|
101
|
+
const e = String(easing || "linear").trim()
|
|
102
|
+
if (easings.has(e)) return easings.get(e)
|
|
103
|
+
let fn = (x) => x
|
|
104
|
+
let m
|
|
105
|
+
if (NAMED[e]) fn = bezier(...NAMED[e])
|
|
106
|
+
else if ((m = /^cubic-bezier\(([^)]*)\)$/.exec(e))) {
|
|
107
|
+
const [a, b, c, d] = m[1].split(",").map(Number)
|
|
108
|
+
if ([a, b, c, d].every(Number.isFinite)) fn = bezier(a, b, c, d)
|
|
109
|
+
} else if ((m = /^steps\(\s*(\d+)\s*(?:,\s*([\w-]+)\s*)?\)$/.exec(e))) fn = steps(Number(m[1]), m[2] || "end")
|
|
110
|
+
else if (e === "step-start") fn = steps(1, "start")
|
|
111
|
+
else if (e === "step-end") fn = steps(1, "end")
|
|
112
|
+
else if ((m = /^linear\((.*)\)$/.exec(e))) fn = linearFn(m[1])
|
|
113
|
+
easings.set(e, fn)
|
|
114
|
+
return fn
|
|
115
|
+
}
|
|
116
|
+
const isLinear = (e) => !e || e === "linear" || e === "linear(0, 1)" || e === "cubic-bezier(0, 0, 1, 1)"
|
|
117
|
+
|
|
118
|
+
// ---- an animation's own clock -----------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
const iterCount = (t) => (t.iterations === "infinite" || t.iterations === Infinity ? Infinity : t.iterations == null ? 1 : Number(t.iterations))
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Where recording time T falls on the animation's own clock.
|
|
124
|
+
* @param {any} anim { timing: { delay, duration, iterations, direction, easing, playbackRate, start, activeStart }, keyframes }
|
|
125
|
+
* @param {number} T
|
|
126
|
+
*/
|
|
127
|
+
export function pointOf(anim, T) {
|
|
128
|
+
const t = anim.timing || {}
|
|
129
|
+
const rate = Number(t.playbackRate) || 1
|
|
130
|
+
const delay = Number(t.delay) || 0
|
|
131
|
+
const dur = Math.max(Number(t.duration) || 0, 0)
|
|
132
|
+
const activeStart = t.activeStart != null ? Number(t.activeStart) : (Number(t.start) || 0) + delay / rate
|
|
133
|
+
const iters = iterCount(t)
|
|
134
|
+
const local = (T - activeStart) * rate
|
|
135
|
+
const activeDur = dur * iters
|
|
136
|
+
let phase = "active"
|
|
137
|
+
let iteration = 0
|
|
138
|
+
let iterLocal = local
|
|
139
|
+
if (local < 0) {
|
|
140
|
+
phase = "delay"
|
|
141
|
+
iterLocal = 0
|
|
142
|
+
} else if (local >= activeDur && Number.isFinite(activeDur)) {
|
|
143
|
+
phase = "after"
|
|
144
|
+
iteration = Math.max(0, Math.ceil(iters) - 1)
|
|
145
|
+
iterLocal = activeDur - iteration * dur
|
|
146
|
+
} else if (dur > 0) {
|
|
147
|
+
iteration = Math.floor(local / dur)
|
|
148
|
+
iterLocal = local - iteration * dur
|
|
149
|
+
}
|
|
150
|
+
const dir = t.direction || "normal"
|
|
151
|
+
const reversed = dir === "reverse" || (dir === "alternate" && iteration % 2 === 1) || (dir === "alternate-reverse" && iteration % 2 === 0)
|
|
152
|
+
const directed = reversed ? dur - iterLocal : iterLocal
|
|
153
|
+
const progress = dur > 0 ? Math.min(Math.max(directed / dur, 0), 1) : 1
|
|
154
|
+
const eased = easingFn(t.easing)(progress)
|
|
155
|
+
return { T: Math.round(T * 10) / 10, local: Math.round(local * 10) / 10, iteration, phase, progress: round(progress, 4), eased: round(eased, 4), segment: segmentOf(anim.keyframes, eased) }
|
|
156
|
+
}
|
|
157
|
+
const round = (v, d) => Math.round(v * 10 ** d) / 10 ** d
|
|
158
|
+
|
|
159
|
+
/** The keyframe pair an eased progress is between: { index, fromOffset, toOffset, easing, progress }. */
|
|
160
|
+
export function segmentOf(keyframes, eased) {
|
|
161
|
+
const kf = (keyframes || []).filter((k) => k && k.offset != null)
|
|
162
|
+
if (kf.length < 2) return null
|
|
163
|
+
const x = Math.min(Math.max(eased, 0), 1)
|
|
164
|
+
let i = 0
|
|
165
|
+
while (i < kf.length - 2 && x >= kf[i + 1].offset) i++
|
|
166
|
+
const a = kf[i].offset
|
|
167
|
+
const b = kf[i + 1].offset
|
|
168
|
+
const easing = kf[i].easing || "linear"
|
|
169
|
+
return { index: i, fromOffset: a, toOffset: b, easing, progress: round(b > a ? (x - a) / (b - a) : 1, 4) }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Numeric keyframe values at recording time T (Motion keyframes read off its
|
|
174
|
+
* props: x, y, scale, rotate, opacity), through each segment's easing.
|
|
175
|
+
* @returns {Record<string, string>}
|
|
176
|
+
*/
|
|
177
|
+
export function valuesAt(anim, T) {
|
|
178
|
+
const p = pointOf(anim, T)
|
|
179
|
+
const kf = (anim.keyframes || []).filter((k) => k && k.offset != null)
|
|
180
|
+
const s = p.segment
|
|
181
|
+
if (!s || kf.length < 2) return {}
|
|
182
|
+
const a = kf[s.index].values || {}
|
|
183
|
+
const b = kf[s.index + 1].values || {}
|
|
184
|
+
const y = easingFn(s.easing)(Math.min(Math.max(s.progress, 0), 1))
|
|
185
|
+
const out = {}
|
|
186
|
+
for (const k of Object.keys(a)) {
|
|
187
|
+
const va = Number(a[k])
|
|
188
|
+
const vb = Number(b[k] != null ? b[k] : a[k])
|
|
189
|
+
if (Number.isFinite(va) && Number.isFinite(vb)) out[k] = num(va + (vb - va) * y, 2)
|
|
190
|
+
}
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Local ms at which the effect reaches a keyframe offset (through the inverse of its easing; first iteration). */
|
|
195
|
+
export function localOfOffset(anim, offset) {
|
|
196
|
+
const t = anim.timing || {}
|
|
197
|
+
const dur = Number(t.duration) || 0
|
|
198
|
+
const f = easingFn(t.easing)
|
|
199
|
+
if (isLinear(t.easing)) return offset * dur
|
|
200
|
+
let lo = 0
|
|
201
|
+
let hi = 1
|
|
202
|
+
for (let i = 0; i < 40; i++) {
|
|
203
|
+
const mid = (lo + hi) / 2
|
|
204
|
+
if (f(mid) < offset) lo = mid
|
|
205
|
+
else hi = mid
|
|
206
|
+
}
|
|
207
|
+
return ((lo + hi) / 2) * dur
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Recording time of a local time (first iteration). */
|
|
211
|
+
export const recordingOf = (anim, local) => {
|
|
212
|
+
const t = anim.timing || {}
|
|
213
|
+
const rate = Number(t.playbackRate) || 1
|
|
214
|
+
const activeStart = t.activeStart != null ? Number(t.activeStart) : (Number(t.start) || 0) + (Number(t.delay) || 0) / rate
|
|
215
|
+
return activeStart + local / rate
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- reading numbers the user typed ------------------------------------------------------
|
|
219
|
+
// "at 100ms", "200–400 ms", "between 200 and 400", "after 300", "from 1s to 1.5s",
|
|
220
|
+
// "20%–40%", "first 300ms", "last 200 ms". With a primary animation they are its
|
|
221
|
+
// local time; without, recording time from the note's moment.
|
|
222
|
+
|
|
223
|
+
const unitMs = (v, u) => (/^s/.test(u || "") && !/^ms/.test(u || "") ? Number(v) * 1000 : Number(v))
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} text
|
|
226
|
+
* @param {any} [primary] the note's primary animation (with timing)
|
|
227
|
+
* @returns {{ text: string, reading: "local" | "recording", from: number, to: number | null } | null}
|
|
228
|
+
*/
|
|
229
|
+
export function readAsked(text, primary) {
|
|
230
|
+
const s = String(text || "")
|
|
231
|
+
const N = "(\\d+(?:\\.\\d+)?)"
|
|
232
|
+
const U = "\\s*(ms|milliseconds?|s|secs?|seconds?)?"
|
|
233
|
+
const dur = primary && primary.timing ? Number(primary.timing.duration) || 0 : 0
|
|
234
|
+
/** @type {"local" | "recording"} */
|
|
235
|
+
const reading = primary ? "local" : "recording"
|
|
236
|
+
const make = (m, from, to) => ({ text: m[0].trim(), reading, from: Math.round(from), to: to == null ? null : Math.round(to) })
|
|
237
|
+
let m
|
|
238
|
+
if ((m = new RegExp(`${N}\\s*%\\s*(?:-|–|—|to)\\s*${N}\\s*%`, "i").exec(s)) && dur) return make(m, (Number(m[1]) / 100) * dur, (Number(m[2]) / 100) * dur)
|
|
239
|
+
if ((m = new RegExp(`between\\s+${N}${U}\\s+and\\s+${N}${U}`, "i").exec(s))) return make(m, unitMs(m[1], m[2] || m[4]), unitMs(m[3], m[4] || m[2]))
|
|
240
|
+
if ((m = new RegExp(`from\\s+${N}${U}\\s+to\\s+${N}${U}`, "i").exec(s))) return make(m, unitMs(m[1], m[2] || m[4]), unitMs(m[3], m[4] || m[2]))
|
|
241
|
+
if ((m = new RegExp(`${N}${U}\\s*(?:-|–|—|to)\\s*${N}${U}`, "i").exec(s)) && (m[2] || m[4])) return make(m, unitMs(m[1], m[2] || m[4]), unitMs(m[3], m[4] || m[2]))
|
|
242
|
+
if ((m = new RegExp(`first\\s+${N}${U}`, "i").exec(s))) return make(m, 0, unitMs(m[1], m[2]))
|
|
243
|
+
if ((m = new RegExp(`last\\s+${N}${U}`, "i").exec(s)) && dur) return make(m, dur - unitMs(m[1], m[2]), dur)
|
|
244
|
+
if ((m = new RegExp(`after\\s+${N}\\s*%`, "i").exec(s)) && dur) return make(m, (Number(m[1]) / 100) * dur, dur)
|
|
245
|
+
if ((m = new RegExp(`after\\s+${N}${U}`, "i").exec(s)) && (m[2] || primary)) return make(m, unitMs(m[1], m[2]), dur || null)
|
|
246
|
+
if ((m = new RegExp(`at\\s+${N}\\s*%`, "i").exec(s)) && dur) return make(m, (Number(m[1]) / 100) * dur, null)
|
|
247
|
+
if ((m = new RegExp(`(?:at|@)\\s*${N}${U}`, "i").exec(s)) && (m[2] || primary)) return make(m, unitMs(m[1], m[2]), null)
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---- describing ----------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
const KIND_WORD = {
|
|
254
|
+
"css-animation": "CSS animation",
|
|
255
|
+
"css-transition": "CSS transition",
|
|
256
|
+
waapi: "element.animate()",
|
|
257
|
+
js: "script-driven motion (inline style writes)",
|
|
258
|
+
"scroll-driven": "scroll-driven animation",
|
|
259
|
+
smil: "SVG (SMIL) animation",
|
|
260
|
+
}
|
|
261
|
+
const LIB_WORD = { motion: "Motion (framer-motion)", gsap: "GSAP", anime: "anime.js", "react-spring": "react-spring" }
|
|
262
|
+
/** "@keyframes rise", "opacity transition", "element.animate()" … */
|
|
263
|
+
const animTitle = (a) => {
|
|
264
|
+
if (a.kind === "css-animation") return `@keyframes ${a.name || "?"}`
|
|
265
|
+
if (a.kind === "css-transition") return `${a.name || "transition"}`
|
|
266
|
+
if (a.kind === "js" && a.motionKeyframes) return `Motion keyframes (${a.name || "values"}) from the motion element's animate / transition props, run from script`
|
|
267
|
+
if (a.kind === "js") return `${a.lib && a.lib !== "raf" ? `${LIB_WORD[a.lib] || a.lib}, ` : ""}${KIND_WORD.js}`
|
|
268
|
+
if (a.kind === "waapi" && a.lib) return `${LIB_WORD[a.lib] || a.lib} animation (it runs as element.animate())`
|
|
269
|
+
return a.name && a.kind !== "waapi" ? `${a.name} (${KIND_WORD[a.kind] || a.kind})` : KIND_WORD[a.kind] || a.name || "animation"
|
|
270
|
+
}
|
|
271
|
+
const whereText = (d) => (d && d.file ? `${d.file}${d.line ? ":" + d.line : ""}` : null)
|
|
272
|
+
// A computed transform (always a matrix) read the way it would be written:
|
|
273
|
+
// matrix(1, 0, 0, 1, 19.72, 0) → translate(19.7px, 0px).
|
|
274
|
+
function prettyTransform(v) {
|
|
275
|
+
const m = /^matrix\(([^)]*)\)$/.exec(String(v).trim())
|
|
276
|
+
if (!m) return String(v)
|
|
277
|
+
const [a, b, c, d, e, f] = m[1].split(",").map(Number)
|
|
278
|
+
if (![a, b, c, d, e, f].every(Number.isFinite)) return String(v)
|
|
279
|
+
const parts = []
|
|
280
|
+
if (e || f) parts.push(`translate(${num(e, 1)}px, ${num(f, 1)}px)`)
|
|
281
|
+
const rot = (Math.atan2(b, a) * 180) / Math.PI
|
|
282
|
+
if (Math.abs(rot) >= 0.05) parts.push(`rotate(${num(rot, 1)}deg)`)
|
|
283
|
+
const sx = Math.hypot(a, b)
|
|
284
|
+
const sy = (a * d - b * c) / (sx || 1)
|
|
285
|
+
if (Math.abs(sx - 1) >= 0.001 || Math.abs(sy - 1) >= 0.001) parts.push(Math.abs(sx - sy) < 0.001 ? `scale(${num(sx, 3)})` : `scale(${num(sx, 3)}, ${num(sy, 3)})`)
|
|
286
|
+
return parts.length ? parts.join(" ") : "none (identity)"
|
|
287
|
+
}
|
|
288
|
+
const pretty = (k, x) => (/^(transform|webkitTransform)$/.test(k) ? prettyTransform(x) : /^opacity$/.test(k) && Number.isFinite(Number(x)) ? num(x, 3) : x)
|
|
289
|
+
const valuesText = (v, max = 6) => {
|
|
290
|
+
const e = Object.entries(v || {}).slice(0, max)
|
|
291
|
+
return e.length ? e.map(([k, x]) => `${kebab(k)} ${pretty(k, x)}`).join("; ") : ""
|
|
292
|
+
}
|
|
293
|
+
const boxText = (g) => {
|
|
294
|
+
if (!g || !g.page) return ""
|
|
295
|
+
const p = g.page
|
|
296
|
+
return `box x ${Math.round(p.x)} y ${Math.round(p.y)} ${Math.round(p.w)}×${Math.round(p.h)}`
|
|
297
|
+
}
|
|
298
|
+
const deltaText = (g, g0) => {
|
|
299
|
+
if (!g || !g0 || !g.page || !g0.page) return ""
|
|
300
|
+
const dx = Math.round(g.page.x - g0.page.x)
|
|
301
|
+
const dy = Math.round(g.page.y - g0.page.y)
|
|
302
|
+
return dx || dy ? ` (Δ ${dx >= 0 ? "+" : ""}${dx}, ${dy >= 0 ? "+" : ""}${dy} from the range start)` : ""
|
|
303
|
+
}
|
|
304
|
+
const pointLine = (label, p, g0) => {
|
|
305
|
+
const bits = [valuesText(p.values), boxText(p.geometry) + deltaText(p.geometry, g0)].filter(Boolean)
|
|
306
|
+
return `${label}: ${bits.join(" · ") || "(values not sampled)"}`
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The animation an element note is about first, or null. */
|
|
310
|
+
export const primaryOf = (note) => ((note && note.anims) || []).find((a) => a.primary) || ((note && note.anims) || [])[0] || null
|
|
311
|
+
|
|
312
|
+
/** The element block: page, element, selector, component, source, classes, box. */
|
|
313
|
+
export function elementBlock(note, ctx = {}) {
|
|
314
|
+
const el = note.el || {}
|
|
315
|
+
const tg = note.target || {}
|
|
316
|
+
const lines = []
|
|
317
|
+
if (el.page) lines.push(`Page: ${el.page}`)
|
|
318
|
+
const label = tg.tag || el.label
|
|
319
|
+
const text = tg.text != null ? tg.text : el.text
|
|
320
|
+
if (label) lines.push(`Element: ${label}${text ? ` "${text}"` : ""}${tg.shadowHost ? ` (inside ${tg.shadowHost}'s shadow root)` : ""}`)
|
|
321
|
+
const sel = tg.selector || note.selector || el.selector
|
|
322
|
+
if (sel) lines.push(`Selector: ${sel}${tg.matches > 1 ? ` (matches ${tg.matches} elements; this is the ${ordinal(tg.index || 1)})` : tg.matches === 1 ? " (1 match)" : ""}`)
|
|
323
|
+
const comps = el.components && el.components.length ? el.components : note.component ? [note.component] : []
|
|
324
|
+
if (comps.length) lines.push(`Component: ${comps.join(" < ")}`)
|
|
325
|
+
if (ctx.sourceLine) lines.push(ctx.sourceLine)
|
|
326
|
+
else {
|
|
327
|
+
const src = (tg.source && tg.source.file ? tg.source : null) || note.source
|
|
328
|
+
if (src && src.file) {
|
|
329
|
+
const how = src.confidence === "owner" ? " (its component's call site)" : src.confidence === "exact" ? " (the element's JSX)" : ""
|
|
330
|
+
lines.push(src.compiled ? `Source: not mapped (only a compiled bundle); find it by the component and selector` : `Source: ${src.file}${src.line ? ":" + src.line : ""}${how}`)
|
|
331
|
+
} else if (src && src.confidence === "unknown") lines.push(`Source: unknown${src.component ? ` (server component ${src.component})` : ""}`)
|
|
332
|
+
}
|
|
333
|
+
if (el.cssSource && el.cssSource.file) lines.push(`CSS: ${el.cssSource.file}${el.cssSource.line ? ":" + el.cssSource.line : ""}`)
|
|
334
|
+
const classes = note.classes || el.classes
|
|
335
|
+
if (classes && classes.length) lines.push(`Classes: ${[].concat(classes).join(" ")}`)
|
|
336
|
+
if (ctx.computed) lines.push(ctx.computed)
|
|
337
|
+
// The box where the note is: the primary animation's point (or range start), sampled there; else as picked.
|
|
338
|
+
const pa = primaryOf(note)
|
|
339
|
+
const pp = pa && (pa.from || pa.at)
|
|
340
|
+
const g = (pp && pp.geometry && pp.geometry.page ? pp.geometry : null) || tg.geometry
|
|
341
|
+
if (g && g.page) {
|
|
342
|
+
const at = note.range ? ` at the range start` : ""
|
|
343
|
+
const tr = g.transform && g.transform !== "none" ? `; transform ${prettyTransform(g.transform)}` : ""
|
|
344
|
+
const vp = g.view && g.scroll ? ` (page px; viewport ${Math.round(g.view.w)}×${Math.round(g.view.h)}, scrolled ${Math.round(g.scroll.x)}, ${Math.round(g.scroll.y)})` : " (page px)"
|
|
345
|
+
lines.push(`Box${at}: x ${Math.round(g.page.x)} y ${Math.round(g.page.y)}, ${Math.round(g.page.w)}×${Math.round(g.page.h)}${vp}${tr}${g.opacity != null && Number(g.opacity) !== 1 ? `; opacity ${num(g.opacity)}` : ""}`)
|
|
346
|
+
if (tr && g.decomposed && (g.decomposed.tx || g.decomposed.ty)) lines.push(` (layout position = this box minus the translate ${num(g.decomposed.tx, 1)}, ${num(g.decomposed.ty, 1)})`)
|
|
347
|
+
} else if (note.rect) lines.push(`Box: ${Math.round(note.rect.w)}×${Math.round(note.rect.h)} at (${Math.round(note.rect.x)}, ${Math.round(note.rect.y)}) in the viewport`)
|
|
348
|
+
return lines
|
|
349
|
+
}
|
|
350
|
+
const ordinal = (n) => `${n}${n % 10 === 1 && n % 100 !== 11 ? "st" : n % 10 === 2 && n % 100 !== 12 ? "nd" : n % 10 === 3 && n % 100 !== 13 ? "rd" : "th"}`
|
|
351
|
+
|
|
352
|
+
function timingLine(a) {
|
|
353
|
+
const t = a.timing || {}
|
|
354
|
+
if (a.kind === "js" && a.motionKeyframes) return `${msText(t.duration)}, delay ${msText(t.delay)}, ${iterCount(t) === Infinity ? "looping" : `${num(iterCount(t))} iteration${iterCount(t) === 1 ? "" : "s"}`}; each segment has its own ease (below; Motion applies a single ease to every segment); local 0 placed from its first style write${a.leadMs ? ` (${num(a.leadMs, 1)}ms before it)` : ""}, ±1 frame`
|
|
355
|
+
if (a.kind === "js") return `${msText(t.duration)} of inline style writes (the curve is in the script: Retake saw the values it wrote, not its easing)`
|
|
356
|
+
const iters = iterCount(t)
|
|
357
|
+
const bits = [
|
|
358
|
+
`${msText(t.duration)}`,
|
|
359
|
+
`delay ${msText(t.delay)}`,
|
|
360
|
+
iters === Infinity ? "looping" : `${num(iters)} iteration${iters === 1 ? "" : "s"}`,
|
|
361
|
+
t.direction && t.direction !== "normal" ? `direction ${t.direction}` : null,
|
|
362
|
+
`fill ${t.fill || "none"}`,
|
|
363
|
+
`easing ${t.easing || "linear"} on the whole effect`,
|
|
364
|
+
t.playbackRate && Number(t.playbackRate) !== 1 ? `playback rate ${t.playbackRate}` : null,
|
|
365
|
+
].filter(Boolean)
|
|
366
|
+
return bits.join(", ")
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function keyframeLines(a) {
|
|
370
|
+
const kf = a.keyframes || []
|
|
371
|
+
if (!kf.length) return []
|
|
372
|
+
if (a.kind === "js" && !a.motionKeyframes) {
|
|
373
|
+
const side = (v) => Object.entries(v || {}).map(([k, x]) => `${kebab(k)}: ${x || "(not set)"}`).join("; ") || "?"
|
|
374
|
+
return [` inline style: ${side(kf[0].values)} → ${side(kf[kf.length - 1].values)} (first and last write)`]
|
|
375
|
+
}
|
|
376
|
+
const out = [` keyframes (offset · values · easing to the next)${a.approx ? ", only the first and last were recorded" : ""}:`]
|
|
377
|
+
kf.forEach((k, i) => out.push(` ${num(k.offset, 3).padEnd(5)} ${valuesText(k.values, 8).padEnd(40)} ${i < kf.length - 1 ? k.easing || "linear" : ""}`.trimEnd()))
|
|
378
|
+
return out
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const segText = (s) => (s ? `segment ${num(s.fromOffset, 3)} → ${num(s.toOffset, 3)} (${s.easing})` : "")
|
|
382
|
+
|
|
383
|
+
// ---- an exact edit -------------------------------------------------------------------------
|
|
384
|
+
// The keyframes again on plain time (the effect's easing moved into them),
|
|
385
|
+
// with the note's point or range edges as keyframes of their own. Each piece
|
|
386
|
+
// keeps the exact part of the curve it had (part of a cubic-bezier is a
|
|
387
|
+
// cubic-bezier; anything else is written as linear() stops), so the motion
|
|
388
|
+
// outside the asked part stays what it was and only the marked piece changes.
|
|
389
|
+
|
|
390
|
+
const bezierOf = (e) => {
|
|
391
|
+
const s = String(e || "linear").trim()
|
|
392
|
+
if (isLinear(s)) return [0, 0, 1, 1]
|
|
393
|
+
if (NAMED[s]) return NAMED[s]
|
|
394
|
+
const m = /^cubic-bezier\(([^)]*)\)$/.exec(s)
|
|
395
|
+
const v = m ? m[1].split(",").map(Number) : []
|
|
396
|
+
return v.length === 4 && v.every(Number.isFinite) ? v : null
|
|
397
|
+
}
|
|
398
|
+
// A computed value as something to write back: a transform's matrix as translate/rotate/scale.
|
|
399
|
+
const cssValue = (k, v) => {
|
|
400
|
+
if (!/^(transform|webkitTransform)$/.test(k)) return pretty(k, v)
|
|
401
|
+
const s = prettyTransform(v)
|
|
402
|
+
return s === "none (identity)" ? "translate(0px, 0px)" : s
|
|
403
|
+
}
|
|
404
|
+
const isSteps = (e) => /^(steps\(|step-)/.test(String(e || "").trim())
|
|
405
|
+
|
|
406
|
+
// The part of a cubic-bezier timing curve between inputs xa and xb, as a timing curve of its own.
|
|
407
|
+
function subCurve(p, xa, xb) {
|
|
408
|
+
if (xb - xa < 1e-6) return "linear"
|
|
409
|
+
const X = [0, p[0], p[2], 1]
|
|
410
|
+
const Y = [0, p[1], p[3], 1]
|
|
411
|
+
const at = (c, t) => (1 - t) ** 3 * c[0] + 3 * (1 - t) ** 2 * t * c[1] + 3 * (1 - t) * t * t * c[2] + t ** 3 * c[3]
|
|
412
|
+
const tOf = (x) => {
|
|
413
|
+
if (x <= 0) return 0
|
|
414
|
+
if (x >= 1) return 1
|
|
415
|
+
let lo = 0
|
|
416
|
+
let hi = 1
|
|
417
|
+
for (let i = 0; i < 50; i++) {
|
|
418
|
+
const mid = (lo + hi) / 2
|
|
419
|
+
if (at(X, mid) < x) lo = mid
|
|
420
|
+
else hi = mid
|
|
421
|
+
}
|
|
422
|
+
return (lo + hi) / 2
|
|
423
|
+
}
|
|
424
|
+
// de Casteljau: [left, right] of control points c split at t.
|
|
425
|
+
const split = (c, t) => {
|
|
426
|
+
const l = (a, b) => a + (b - a) * t
|
|
427
|
+
const a1 = l(c[0], c[1]), b1 = l(c[1], c[2]), c1 = l(c[2], c[3])
|
|
428
|
+
const a2 = l(a1, b1), b2 = l(b1, c1)
|
|
429
|
+
const m = l(a2, b2)
|
|
430
|
+
return [[c[0], a1, a2, m], [m, b2, c1, c[3]]]
|
|
431
|
+
}
|
|
432
|
+
const ta = tOf(xa)
|
|
433
|
+
const tb = tOf(xb)
|
|
434
|
+
let cx = split(X, tb)[0]
|
|
435
|
+
let cy = split(Y, tb)[0]
|
|
436
|
+
const s = tb > 0 ? ta / tb : 0
|
|
437
|
+
cx = split(cx, s)[1]
|
|
438
|
+
cy = split(cy, s)[1]
|
|
439
|
+
const dx = cx[3] - cx[0]
|
|
440
|
+
const dy = cy[3] - cy[0]
|
|
441
|
+
if (Math.abs(dy) < 1e-9 || dx < 1e-9) return "linear"
|
|
442
|
+
const q = [(cx[1] - cx[0]) / dx, (cy[1] - cy[0]) / dy, (cx[2] - cx[0]) / dx, (cy[2] - cy[0]) / dy]
|
|
443
|
+
if (Math.abs(q[0] - q[1]) < 1e-3 && Math.abs(q[2] - q[3]) < 1e-3) return "linear"
|
|
444
|
+
return `cubic-bezier(${q.map((v) => num(v, 4)).join(", ")})`
|
|
445
|
+
}
|
|
446
|
+
// Any curve on [0, 1] as linear() stops, as few as keep it within 0.2% of its rise.
|
|
447
|
+
function stopsCurve(f) {
|
|
448
|
+
for (const n of [8, 12, 16, 24, 32]) {
|
|
449
|
+
const ys = Array.from({ length: n + 1 }, (_, i) => f(i / n))
|
|
450
|
+
let worst = 0
|
|
451
|
+
for (let i = 0; i < n; i++) worst = Math.max(worst, Math.abs(f((i + 0.5) / n) - (ys[i] + ys[i + 1]) / 2))
|
|
452
|
+
if (worst <= 0.002 || n === 32) return `linear(${ys.map((y) => num(y, 4)).join(", ")})`
|
|
453
|
+
}
|
|
454
|
+
return "linear"
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* The keyframes with the note's point or range edges put in, on plain time: [{ offset, values, easing, mark }],
|
|
459
|
+
* or null when it can't be written exactly (steps(), several iterations, reversed, nothing sampled).
|
|
460
|
+
* @param {any} a an anims[] entry with keyframes, timing and at or from/to
|
|
461
|
+
*/
|
|
462
|
+
export function splitKeyframes(a) {
|
|
463
|
+
const t = a.timing || {}
|
|
464
|
+
const kf = (a.keyframes || []).filter((k) => k && k.offset != null)
|
|
465
|
+
const dur = Number(t.duration) || 0
|
|
466
|
+
if (kf.length < 2 || dur <= 0 || iterCount(t) !== 1 || (t.direction && t.direction !== "normal")) return null
|
|
467
|
+
if (isSteps(t.easing) || kf.some((k) => isSteps(k.easing))) return null
|
|
468
|
+
const edges = (a.from && a.to ? [[a.from, "from"], [a.to, "to"]] : a.at ? [[a.at, "at"]] : []).filter(([p]) => p && p.phase !== "delay" && p.values && Object.keys(p.values).length)
|
|
469
|
+
if (!edges.length) return null
|
|
470
|
+
const E = easingFn(t.easing)
|
|
471
|
+
const effectCurve = bezierOf(t.easing)
|
|
472
|
+
const timeOf = (o) => localOfOffset(a, o) / dur
|
|
473
|
+
/** @type {{ offset: number, eased: number, values: any, seg: number, mark: string | null }[]} */
|
|
474
|
+
const stops = kf.map((k, i) => ({ offset: timeOf(k.offset), eased: k.offset, values: k.values || {}, seg: Math.min(i, kf.length - 2), mark: null }))
|
|
475
|
+
for (const [p, mark] of edges) {
|
|
476
|
+
const q = Math.min(Math.max(Number(p.progress) || 0, 0), 1)
|
|
477
|
+
const near = stops.find((s) => Math.abs(s.offset - q) < 5e-4)
|
|
478
|
+
if (near) {
|
|
479
|
+
near.mark = near.mark ? `${near.mark}+${mark}` : mark
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
const e = E(q)
|
|
483
|
+
let seg = 0
|
|
484
|
+
while (seg < kf.length - 2 && e >= kf[seg + 1].offset) seg++
|
|
485
|
+
const values = {}
|
|
486
|
+
for (const [k, v] of Object.entries(p.values)) values[k] = cssValue(k, v)
|
|
487
|
+
stops.push({ offset: q, eased: e, values, seg, mark })
|
|
488
|
+
}
|
|
489
|
+
stops.sort((x, y) => x.offset - y.offset)
|
|
490
|
+
return stops.map((s, i) => {
|
|
491
|
+
const next = stops[i + 1]
|
|
492
|
+
/** @type {string | null} */
|
|
493
|
+
let easing = null
|
|
494
|
+
if (next) {
|
|
495
|
+
const k = kf[s.seg]
|
|
496
|
+
const o0 = kf[s.seg].offset
|
|
497
|
+
const o1 = kf[s.seg + 1].offset
|
|
498
|
+
const segOf = (p) => (o1 > o0 ? (E(p) - o0) / (o1 - o0) : 0)
|
|
499
|
+
const kCurve = bezierOf(k.easing)
|
|
500
|
+
const effectLinear = isLinear(t.easing)
|
|
501
|
+
const kLinear = isLinear(k.easing)
|
|
502
|
+
const whole = Math.abs(segOf(s.offset)) < 1e-6 && Math.abs(segOf(next.offset) - 1) < 1e-6
|
|
503
|
+
if (effectLinear && kLinear) easing = "linear"
|
|
504
|
+
else if (effectLinear && whole) easing = k.easing || "linear"
|
|
505
|
+
else if (effectLinear && kCurve) easing = subCurve(kCurve, segOf(s.offset), segOf(next.offset))
|
|
506
|
+
else if (kLinear && effectCurve) easing = subCurve(effectCurve, s.offset, next.offset)
|
|
507
|
+
else {
|
|
508
|
+
const kf2 = easingFn(k.easing)
|
|
509
|
+
const y0 = kf2(segOf(s.offset))
|
|
510
|
+
const y1 = kf2(segOf(next.offset))
|
|
511
|
+
easing = Math.abs(y1 - y0) < 1e-9 ? "linear" : stopsCurve((u) => (kf2(segOf(s.offset + u * (next.offset - s.offset))) - y0) / (y1 - y0))
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return { offset: Math.round(s.offset * 10000) / 10000, values: s.values, easing, mark: s.mark }
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const MARK_WORD = { from: "range start", to: "range end", at: "the note's point", "from+to": "range" }
|
|
519
|
+
const markText = (s, a) => {
|
|
520
|
+
if (!s.mark) return ""
|
|
521
|
+
const p = s.mark.startsWith("to") ? a.to : s.mark.startsWith("at") ? a.at : a.from
|
|
522
|
+
if (p && p.phase === "after") return ` ← its end state (the note is ${msText(p.local - (Number((a.timing || {}).duration) || 0) * iterCount(a.timing || {}))} after it ended)`
|
|
523
|
+
return ` ← ${MARK_WORD[s.mark] || s.mark}, local ${msText(p && p.local)}`
|
|
524
|
+
}
|
|
525
|
+
const jsValue = (v) => JSON.stringify(String(v))
|
|
526
|
+
|
|
527
|
+
// A CSS transition's timing function as linear() stops, with the note's
|
|
528
|
+
// point or range edges as stops of their own: [{ x, y, mark }]. Each stop is
|
|
529
|
+
// "output (0 = the start value, 1 = the end value) at input %".
|
|
530
|
+
function transitionStops(a) {
|
|
531
|
+
const t = a.timing || {}
|
|
532
|
+
const E = easingFn(t.easing)
|
|
533
|
+
if (isSteps(t.easing)) return null
|
|
534
|
+
const edges = (a.from && a.to ? [[a.from, "from"], [a.to, "to"]] : a.at ? [[a.at, "at"]] : []).filter(([p]) => p && p.phase === "active" || (p && p.phase === "after"))
|
|
535
|
+
const marks = edges.map(([p, mark]) => ({ x: p.phase === "after" ? 1 : Math.min(Math.max(Number(p.progress) || 0, 0), 1), mark }))
|
|
536
|
+
const cuts = [...new Set([0, 1, ...marks.map((m) => Math.round(m.x * 10000) / 10000)])].sort((x, y) => x - y)
|
|
537
|
+
const out = []
|
|
538
|
+
for (let i = 0; i < cuts.length - 1; i++) {
|
|
539
|
+
const a0 = cuts[i]
|
|
540
|
+
const a1 = cuts[i + 1]
|
|
541
|
+
let n = 2
|
|
542
|
+
for (; n < 24; n += 2) {
|
|
543
|
+
let worst = 0
|
|
544
|
+
for (let k = 0; k < n; k++) {
|
|
545
|
+
const xa = a0 + ((a1 - a0) * k) / n
|
|
546
|
+
const xb = a0 + ((a1 - a0) * (k + 1)) / n
|
|
547
|
+
worst = Math.max(worst, Math.abs(E((xa + xb) / 2) - (E(xa) + E(xb)) / 2))
|
|
548
|
+
}
|
|
549
|
+
if (worst <= 0.002) break
|
|
550
|
+
}
|
|
551
|
+
for (let k = i === 0 ? 0 : 1; k <= n; k++) out.push({ x: a0 + ((a1 - a0) * k) / n, y: 0 })
|
|
552
|
+
}
|
|
553
|
+
for (const s of out) {
|
|
554
|
+
s.y = E(s.x)
|
|
555
|
+
const m = marks.find((m) => Math.abs(m.x - s.x) < 1e-4)
|
|
556
|
+
s.mark = m ? m.mark : null
|
|
557
|
+
}
|
|
558
|
+
return out
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** The exact-edit block for an animation (CSS @keyframes, element.animate(), Motion, a CSS transition), or []. */
|
|
562
|
+
export function editPlan(a) {
|
|
563
|
+
if (a.kind === "css-transition" && !a.approx) {
|
|
564
|
+
const stops = transitionStops(a)
|
|
565
|
+
const kf = a.keyframes || []
|
|
566
|
+
if (!stops || kf.length < 2) return []
|
|
567
|
+
const t = a.timing || {}
|
|
568
|
+
const prop = String(a.name || "").replace(/ transition$/, "") || "the property"
|
|
569
|
+
const v0 = valuesText(kf[0].values)
|
|
570
|
+
const v1 = valuesText(kf[kf.length - 1].values)
|
|
571
|
+
const where = a.rule && a.rule.file ? ` (the rule ${a.rule.selector || ""} at ${whereText(a.rule)})` : ""
|
|
572
|
+
const lines = [`Exact edit (CSS transition): the same timing as now (${t.easing}) written as linear() stops, with the note's ${a.from && a.to ? "range edges" : "point"} as stops of their own (marked below). Each stop is "output input%": output 0 = the start value (${v0}), 1 = the end value (${v1}); input = local / duration. Use it as the timing function of ${prop} only${where}, keep the other properties' transitions as they are, then change only the stops ${a.from && a.to ? "between the marks" : "at the mark"}. A jump is two stops at the same input%.`]
|
|
573
|
+
lines.push(` ${prop} ${msText(t.duration)}${Number(t.delay) ? ` ${msText(t.delay)}` : ""} linear(${stops.map((s) => `${num(s.y, 4)} ${num(s.x * 100, 2)}%`).join(", ")}) (its entry in the transition list)`)
|
|
574
|
+
for (const s of stops.filter((x) => x.mark)) {
|
|
575
|
+
const p = s.mark === "to" ? a.to : s.mark === "at" ? a.at : a.from
|
|
576
|
+
lines.push(` marked: ${num(s.y, 4)} ${num(s.x * 100, 2)}% = ${MARK_WORD[s.mark] || s.mark}, local ${msText(p && p.local)}${p && p.values ? ` (${valuesText(p.values)})` : ""}`)
|
|
577
|
+
}
|
|
578
|
+
return lines
|
|
579
|
+
}
|
|
580
|
+
if (a.approx) return []
|
|
581
|
+
if (!["css-animation", "waapi", "js"].includes(a.kind) || (a.kind === "js" && a.lib !== "motion")) return []
|
|
582
|
+
const stops = splitKeyframes(a)
|
|
583
|
+
if (!stops) return []
|
|
584
|
+
const t = a.timing || {}
|
|
585
|
+
const range = a.from && a.to
|
|
586
|
+
const what = range ? "between the two marked keyframes" : "at the marked keyframe (and as little around it as the request allows)"
|
|
587
|
+
const lines = []
|
|
588
|
+
const easedNote = isLinear(t.easing) ? "" : ` The effect's easing (${t.easing}) is moved into the keyframes, so offsets are plain time: local ms / ${msText(t.duration)}.`
|
|
589
|
+
if (a.kind === "css-animation") {
|
|
590
|
+
const shared = a.shared && a.shared.length
|
|
591
|
+
const last = String(a.selector || "").split(/\s*>\s*|\s+/).pop() || ""
|
|
592
|
+
const nth = (/:nth-[\w-]+\((\d+)\)/.exec(last) || [])[1] || ""
|
|
593
|
+
const name = shared ? `${a.name}-${(last.replace(/:[\w-]+(\([^)]*\))?/g, "").split(/[.#[\]=]+/).filter(Boolean).pop() || "own").replace(/[^\w-]/g, "")}${nth}` : a.name
|
|
594
|
+
lines.push(`Exact edit: the same motion as now, with the note's ${range ? "range edges" : "point"} as stops of their own (marked).${easedNote} ${shared ? `Add these as @keyframes ${name} and point only this element at it (${a.selector || "its selector"} { animation-name: ${name} }); @keyframes ${a.name} is shared.` : `Replace @keyframes ${a.name} with them.`} Then change only what is ${what}:`)
|
|
595
|
+
lines.push(` @keyframes ${name} {`)
|
|
596
|
+
for (const s of stops) {
|
|
597
|
+
const decl = Object.entries(s.values).map(([k, v]) => `${kebab(k)}: ${v}`)
|
|
598
|
+
if (s.easing) decl.push(`animation-timing-function: ${s.easing}`)
|
|
599
|
+
lines.push(` ${num(s.offset * 100, 2)}% { ${decl.join("; ")} }${markText(s, a) ? ` /*${markText(s, a)} */` : ""}`)
|
|
600
|
+
}
|
|
601
|
+
lines.push(" }")
|
|
602
|
+
} else if (a.lib === "motion") {
|
|
603
|
+
const props = [...new Set(stops.flatMap((s) => Object.keys(s.values)))]
|
|
604
|
+
lines.push(`Exact edit (Motion): the same motion as now, with the note's ${range ? "range edges" : "point"} as keyframes of their own.${isLinear(t.easing) ? "" : ` The single ease (${t.easing}) becomes one ease per segment, so times are plain time: local ms / ${msText(t.duration)}.`} Then change only what is ${what} (marked: ${stops.map((s, i) => (s.mark ? `index ${i}` : null)).filter(Boolean).join(", ")}):`)
|
|
605
|
+
lines.push(` animate={{ ${props.map((p) => `${p}: [${stops.map((s) => (s.values[p] == null ? "null" : /^-?[\d.]+$/.test(String(s.values[p])) ? String(s.values[p]) : jsValue(s.values[p]))).join(", ")}]`).join(", ")} }} (other values in animate as they are)`)
|
|
606
|
+
const ease = stops.slice(0, -1).map((s) => {
|
|
607
|
+
const e = s.easing || "linear"
|
|
608
|
+
return e === "linear" ? '"linear"' : /^cubic-bezier/.test(e) ? `[${e.slice(13, -1)}]` : jsValue(e)
|
|
609
|
+
})
|
|
610
|
+
const own = `{ duration: ${num(t.duration / 1000, 3)}, delay: ${num((Number(t.delay) || 0) / 1000, 3)}, times: [${stops.map((s) => num(s.offset, 4)).join(", ")}], ease: [${ease.join(", ")}] }`
|
|
611
|
+
lines.push(` transition={{ ...the transition as it is, ${props.length === 1 ? props[0] : `${props.join(" and ")} each`}: ${own} }} (a transition of ${props.length === 1 ? "its" : "their"} own, so other values keep their times)`)
|
|
612
|
+
} else {
|
|
613
|
+
lines.push(`Exact edit (element.animate): the same motion as now, with the note's ${range ? "range edges" : "point"} as keyframes of their own (marked).${easedNote} Then change only what is ${what}:`)
|
|
614
|
+
lines.push(" [")
|
|
615
|
+
for (const s of stops) lines.push(` { offset: ${num(s.offset, 4)}, ${Object.entries(s.values).map(([k, v]) => `${k}: ${jsValue(v)}`).join(", ")}${s.easing ? `, easing: ${jsValue(s.easing)}` : ""} },${markText(s, a) ? ` //${markText(s, a)}` : ""}`)
|
|
616
|
+
lines.push(` ], { duration: ${num(t.duration, 1)}, delay: ${num(Number(t.delay) || 0, 1)}, easing: "linear", fill: ${jsValue(t.fill || "none")} }`)
|
|
617
|
+
}
|
|
618
|
+
return lines
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function motionHow(a, at, check) {
|
|
622
|
+
const t = a.timing || {}
|
|
623
|
+
const eased = !isLinear(t.easing)
|
|
624
|
+
const where = a.from && a.to ? `${num(a.from.progress, 3)} and ${num(a.to.progress, 3)}` : a.at ? num(a.at.progress, 3) : "the point"
|
|
625
|
+
return `How (Motion): values are arrays with times (0-1 of the duration, delay excluded) and an ease per segment (a single ease is used for every segment).${eased ? ` This one runs as element.animate() with one easing on the whole effect (${t.easing}), applied before times, so times are not plain time here: the exact edit below gives one ease per segment instead, with times = local / duration.` : ""} Insert entries at ${where} (local / duration) with the values given there, so the motion outside doesn't move; each new segment needs the matching part of the curve it splits (the exact edit below has it). To change other values (y, scale...) over only that part, give them arrays of their own with the same times, holding their current value outside it. A spring has no ms range: convert it to keyframes + times, or change stiffness/damping/bounce. ${check}`
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** How to change only the part the note means, for each kind of animation. */
|
|
629
|
+
export function howTo(a) {
|
|
630
|
+
const t = a.timing || {}
|
|
631
|
+
const easedEffect = !isLinear(t.easing)
|
|
632
|
+
const check = "Check the values at the range edges and one frame either side against the ones above."
|
|
633
|
+
const at = a.from && a.to ? `${num(a.from.progress * 100, 1)}% and ${num(a.to.progress * 100, 1)}%` : a.at && a.at.phase === "after" ? "100% (its end: the note is after it ended)" : a.at ? `${num(a.at.progress * 100, 1)}%` : "the point"
|
|
634
|
+
switch (a.kind) {
|
|
635
|
+
case "css-animation":
|
|
636
|
+
return `How (CSS @keyframes): add stops at ${at} (local / duration) with the edge values above, so nothing outside moves, and change only what is between them. animation-timing-function inside a keyframe applies to the segment after it (default ease), so a stop added inside a curved segment needs the matching part of that curve on both sides (the exact edit below has it); a hold is two stops with the same value. If the duration changes, recompute every %. ${a.matches > 1 ? "The selector matches more than one element: scope the change (:nth-child, a modifier class) unless all were meant. " : ""}${a.shared && a.shared.length ? `@keyframes ${a.name} also runs on other elements (above): give this element its own copy unless the user meant all of them. ` : ""}${check}`
|
|
637
|
+
case "css-transition":
|
|
638
|
+
return `How (CSS transition): a transition has one segment. For a hold, a two-step motion or a change over part of it, use linear(…) stops or a cubic-bezier in transition-timing-function, or replace it with a keyframes animation started by the same trigger${a.trigger ? ` (${a.trigger.what})` : ""}. ${check}`
|
|
639
|
+
case "waapi":
|
|
640
|
+
if (a.lib === "motion") return motionHow(a, at, check)
|
|
641
|
+
return `How (WAAPI): edit the keyframe array: add { offset, ...values, easing } at ${at}.${easedEffect ? ` The effect has one easing (${t.easing}), so keyframe offsets apply after it, not to time: the exact edit below moves that easing into the keyframes (effect easing linear, offsets = local / duration) with the edges as keyframes of their own.` : ""} ${check}`
|
|
642
|
+
case "js":
|
|
643
|
+
if (a.lib === "gsap") return `How (GSAP): tweens sit at positions on a timeline; retime with the position parameter ("<", "-=0.4", absolute seconds). Change part of a tween by splitting it into two .to() calls at the edge values above.${a.gsap && a.gsap.tweens ? " The tweens Retake read are listed above." : ""} ${check}`
|
|
644
|
+
if (a.lib === "motion") return motionHow(a, at, check)
|
|
645
|
+
return `How (script-driven): Retake saw inline style writes, not the definition. Edit the code${whereText(a.defined) ? ` at ${whereText(a.defined)}` : ""} that writes them and gate the change on the same elapsed time, or move it into keyframes. ${check}`
|
|
646
|
+
case "scroll-driven":
|
|
647
|
+
return `How (scroll-driven): the axis is scroll progress, not time. Edit the keyframe % or animation-range; the scroll position and progress at the note are given. For JS parallax, clamp to a scroll range instead of changing the factor. ${check}`
|
|
648
|
+
case "smil":
|
|
649
|
+
return `How (SVG SMIL): edit keyTimes/values on the <animate> element: add entries at ${at} with the edge values. ${check}`
|
|
650
|
+
default:
|
|
651
|
+
return check
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// One animation: what, where it is defined, its timing and keyframes, then the
|
|
656
|
+
// note's point or range on its own clock.
|
|
657
|
+
function animLines(a, ctx, note) {
|
|
658
|
+
const start = ctx.start || 0
|
|
659
|
+
const t = a.timing || {}
|
|
660
|
+
const rel = a.relation === "on" ? "this element" : a.relation && a.relation.startsWith("pseudo") ? `this element's ${a.relation.replace(/^pseudo\s*/, "")}` : `${a.selector || "an ancestor"} (an ancestor that moves this element)`
|
|
661
|
+
const lines = []
|
|
662
|
+
lines.push(`Animation: ${animTitle(a)} on ${rel}${a.id ? ` (clip ${a.id})` : ""}${whereText(a.defined) ? `, defined at ${whereText(a.defined)}${a.defined.what ? ` (${a.defined.what})` : ""}` : ""}`)
|
|
663
|
+
if (a.rule && a.rule.file) lines.push(` applied by ${a.rule.selector ? `rule ${a.rule.selector} at ` : ""}${whereText(a.rule)}`)
|
|
664
|
+
if (a.motionProps && a.motionProps.props) lines.push(` ${a.motionProps.component} props: ${Object.entries(a.motionProps.props).map(([k, v]) => `${k}=${v}`).join(" ")}`)
|
|
665
|
+
if (a.shared && a.shared.length) lines.push(` shared: the same ${a.kind === "css-animation" ? `@keyframes ${a.name}` : "animation"} also runs on ${a.shared.length} other element${a.shared.length > 1 ? "s" : ""} (${a.shared.slice(0, 4).map((x) => x.selector || "?").join(", ")}${a.shared.length > 4 ? ", …" : ""}); editing it changes ${a.shared.length > 1 ? "them" : "it"} too`)
|
|
666
|
+
if (a.trigger && a.trigger.what) lines.push(` started by ${a.trigger.what}${a.trigger.t != null ? ` at ${clockText(a.trigger.t - start)} (recording)` : ""}`)
|
|
667
|
+
lines.push(` ${timingLine(a)}`)
|
|
668
|
+
lines.push(` runs recording ${clockText(t.start - start)} → ${t.end != null ? clockText(t.end - start) : "still running"}; local 0 = recording ${clockText(t.activeStart - start)}`)
|
|
669
|
+
lines.push(...keyframeLines(a))
|
|
670
|
+
if (a.gsap && a.gsap.tweens) for (const tw of a.gsap.tweens.slice(0, 6)) lines.push(` gsap tween ${num(tw.start, 3)}s → ${num(tw.end, 3)}s ${tw.ease || ""} ${valuesText(tw.props)}`.trimEnd())
|
|
671
|
+
if (a.scroll) lines.push(` scroll: ${a.scroll.timeline || "scroll"} timeline${a.scroll.range ? `, range ${a.scroll.range}` : ""}${a.scroll.scrollY != null ? `, scrollY ${Math.round(a.scroll.scrollY)}` : ""}${a.scroll.progress != null ? `, progress ${pct(a.scroll.progress)}` : ""}`)
|
|
672
|
+
const dur = Number(t.duration) || 0
|
|
673
|
+
if (a.from && a.to) {
|
|
674
|
+
const f = a.from
|
|
675
|
+
const to = a.to
|
|
676
|
+
const end = note && note.range && a.openEnd ? " (to the end)" : ""
|
|
677
|
+
lines.push(`Range: local ${msText(f.local)} → ${msText(to.local)}${end} of ${msText(dur)} = progress ${num(f.progress, 3)} → ${num(to.progress, 3)} (eased ${num(f.eased, 3)} → ${num(to.eased, 3)}); recording ${clockText(f.T - start)} → ${clockText(to.T - start)}`)
|
|
678
|
+
const segs = f.segment && to.segment ? (f.segment.index === to.segment.index ? `inside ${segText(f.segment)}` : `from ${segText(f.segment)} to ${segText(to.segment)}`) : ""
|
|
679
|
+
const inside = a.keyframesInside || []
|
|
680
|
+
lines.push(` ${[segs, inside.length ? `keyframe${inside.length > 1 ? "s" : ""} inside the range at ${inside.map((o) => num(o, 3)).join(", ")}` : "no keyframe inside the range"].filter(Boolean).join("; ")}`)
|
|
681
|
+
const g0 = (a.samples && a.samples[0] && a.samples[0].geometry) || f.geometry
|
|
682
|
+
lines.push(" " + pointLine(`at ${msText(f.local)}`, f, null))
|
|
683
|
+
for (const s of (a.samples || []).slice(1, -1)) lines.push(" " + pointLine(`at ${msText(s.local)}`, s, g0))
|
|
684
|
+
lines.push(" " + pointLine(`at ${msText(to.local)}`, to, g0))
|
|
685
|
+
if (f.frame && f.frame.before) lines.push(` one frame before ${msText(f.local)}: ${valuesText(f.frame.before) || "?"}; one frame after ${msText(to.local)}: ${valuesText(to.frame && to.frame.after) || "?"}`)
|
|
686
|
+
} else if (a.at) {
|
|
687
|
+
const p = a.at
|
|
688
|
+
const ended = t.end != null ? ` (at recording ${clockText(t.end - start)}, ${msText(p.T - t.end)} before the note's moment)` : ""
|
|
689
|
+
const phase = p.phase === "delay" ? `in its delay, ${msText(-p.local)} before local 0` : p.phase === "after" ? `after it ended${ended}: its end state` : `local ${msText(p.local)} of ${msText(dur)}`
|
|
690
|
+
const iter = p.iteration ? `iteration ${p.iteration + 1}, ` : ""
|
|
691
|
+
const sampled = a.kind !== "js" || a.motionKeyframes
|
|
692
|
+
const detail = p.phase === "after" ? "" : !sampled ? ` = progress ${num(p.progress, 3)}` : ` = progress ${num(p.progress, 3)} (eased ${num(p.eased, 3)})`
|
|
693
|
+
lines.push(`At: ${iter}${phase}${detail}; recording ${clockText(p.T - start)}${p.segment && p.phase === "active" && sampled ? `; in ${segText(p.segment)}, ${pct(p.segment.progress)} through it` : ""}`)
|
|
694
|
+
lines.push(" " + pointLine("values", p, null))
|
|
695
|
+
if (p.frame) lines.push(` one frame before: ${valuesText(p.frame.before) || "?"}; one frame after: ${valuesText(p.frame.after) || "?"}`)
|
|
696
|
+
}
|
|
697
|
+
return lines
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function scopeLine(note, a) {
|
|
701
|
+
if (!a) return note.range ? `Scope: the user means recording ${clockText(note.range.from - 0)} → ${clockText(note.range.to - 0)} on this element.` : null
|
|
702
|
+
const dur = Number((a.timing || {}).duration) || 0
|
|
703
|
+
if (a.from && a.to) return `Scope: change only local ${Math.round(a.from.local)}–${Math.round(a.to.local)}ms of this animation; keep the values at every other point and the total duration (${msText(dur)}).`
|
|
704
|
+
if (a.at && a.at.phase === "after") return `Scope: the note is after this animation ended, so the user means where it ends (its last values above). Change the end values; keep the start and the timing unless asked.`
|
|
705
|
+
if (a.at) return `Scope: the user means local ${msText(a.at.local)} (progress ${num(a.at.progress, 3)}) of this animation. Change it there and as little around it as the request allows; keep the start, the end and the total duration unless asked.`
|
|
706
|
+
return null
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** The animation block: the primary animation first, then the others, the scope and the How line. */
|
|
710
|
+
export function animationBlock(note, ctx = {}) {
|
|
711
|
+
const anims = note.anims || []
|
|
712
|
+
const lines = []
|
|
713
|
+
if (!anims.length) {
|
|
714
|
+
lines.push(note.range ? "Nothing animates on this element in this range." : "Nothing animates on this element at this moment.")
|
|
715
|
+
} else {
|
|
716
|
+
const p = primaryOf(note)
|
|
717
|
+
lines.push(...animLines(p, ctx, note))
|
|
718
|
+
const sc = scopeLine(note, p)
|
|
719
|
+
if (sc) lines.push(sc)
|
|
720
|
+
lines.push(howTo({ ...p, matches: note.target && note.target.matches }))
|
|
721
|
+
lines.push(...editPlan(p.relation === "on" && note.target && note.target.selector ? { ...p, selector: note.target.selector } : p))
|
|
722
|
+
for (const a of anims.filter((x) => x !== p)) lines.push("", ...animLines(a, ctx, note).map((l, i) => (i === 0 ? l.replace(/^Animation:/, "Also running:") : l)))
|
|
723
|
+
}
|
|
724
|
+
const inside = note.inside || []
|
|
725
|
+
lines.push(`Also inside the element (not this note's subject): ${inside.length ? inside.slice(0, 6).map((c) => `${c.name || "animation"} on ${c.selector || "?"}${c.id ? ` (clip ${c.id})` : ""}`).join(", ") + (inside.length > 6 ? `, +${inside.length - 6} more` : "") : "none"}`)
|
|
726
|
+
return lines
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** One line for list_notes: "fadeUp @ 100ms (20%)", "ticker 200–400ms (10–20%)". */
|
|
730
|
+
export function animationSummary(note) {
|
|
731
|
+
const a = primaryOf(note)
|
|
732
|
+
if (!a) return note.anims ? "nothing animating" : note.clip ? `${note.clip.label || "animation"} +${Math.round(note.clip.offset || 0)}ms` : null
|
|
733
|
+
const name = a.name || a.kind
|
|
734
|
+
if (a.from && a.to) return `${name} ${Math.round(a.from.local)}–${Math.round(a.to.local)}ms (${num(a.from.progress * 100, 0)}–${num(a.to.progress * 100, 0)}%)`
|
|
735
|
+
if (a.at) return `${name} @ ${Math.round(a.at.local)}ms (${num(a.at.progress * 100, 0)}%)`
|
|
736
|
+
return name
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** The note's moment in words, for headers: "at 100ms (20%) of fadeUp on <h1.title>". */
|
|
740
|
+
export function atPhrase(note) {
|
|
741
|
+
const a = primaryOf(note)
|
|
742
|
+
const el = (note.target && note.target.tag) || (note.el && note.el.label) || note.selector || "the element"
|
|
743
|
+
if (!a) return null
|
|
744
|
+
if (a.from && a.to) return `${Math.round(a.from.local)}–${Math.round(a.to.local)}ms (${num(a.from.progress * 100, 0)}–${num(a.to.progress * 100, 0)}%) of ${a.name || "its animation"} on ${el}`
|
|
745
|
+
if (a.at) return a.at.phase === "delay" ? `in the delay of ${a.name || "its animation"} on ${el}` : `at ${Math.round(a.at.local)}ms (${num(a.at.progress * 100, 0)}%) of ${a.name || "its animation"} on ${el}`
|
|
746
|
+
return null
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* The whole note as text for an agent (Copy for agent, get_note).
|
|
751
|
+
* @param {any} note the note as the server keeps it (CONTRACT.md "Note")
|
|
752
|
+
* @param {{ start?: number, timeline?: string, parent?: string | null, forkAt?: number | null, sourceLine?: string, computed?: string, status?: boolean }} [ctx]
|
|
753
|
+
*/
|
|
754
|
+
export function noteText(note, ctx = {}) {
|
|
755
|
+
const start = ctx.start || 0
|
|
756
|
+
const lines = [
|
|
757
|
+
`> Retake note ${note.id}: pinned to a moment in a Retake recording of the running app. "Moment" is the time into that recording; times marked "local" are on the animation's own clock (0 = the end of its delay).`,
|
|
758
|
+
`> To see what happened before and after it, use the Retake MCP tools (get_note, get_moment, get_animation, get_timeline_events), or ask the user to replay that moment in the Retake dock.`,
|
|
759
|
+
"",
|
|
760
|
+
`## ${note.text}`,
|
|
761
|
+
"",
|
|
762
|
+
]
|
|
763
|
+
if (ctx.status) lines.push(`Status: ${note.status || "pending"}`)
|
|
764
|
+
lines.push(...elementBlock(note, ctx), "")
|
|
765
|
+
// A note saved before notes knew their animations has only the clip it was in.
|
|
766
|
+
if (Array.isArray(note.anims)) lines.push(...animationBlock(note, ctx))
|
|
767
|
+
else if (note.clip) lines.push(`During an animation: ${Math.round(Number(note.clip.offset) || 0)}ms into ${note.clip.label || "an animation"} (clip ${note.clip.id}; counted from its start, delay included)`)
|
|
768
|
+
else lines.push("Nothing was animating on it at this moment (as recorded).")
|
|
769
|
+
if (note.asked) lines.push(`Read "${note.asked.text}" as ${note.asked.reading === "local" ? `local time of ${primaryOf(note) && primaryOf(note).id ? `clip ${primaryOf(note).id}` : "the animation"}` : "recording time from the note's moment"}${note.asked.from != null ? ` (${Math.round(note.asked.from)}${note.asked.to != null ? `–${Math.round(note.asked.to)}` : ""}ms)` : ""}.`)
|
|
770
|
+
const moment = note.range ? `${clockText(note.range.from - start)} → ${clockText(note.range.to - start)}` : clockText(note.t - start)
|
|
771
|
+
lines.push(`Moment: ${moment} into the recording · Timeline: "${ctx.timeline || "Timeline"}"${ctx.parent ? `, branched from "${ctx.parent}" at ${clockText((ctx.forkAt || 0) - start)}` : ""}`)
|
|
772
|
+
const replies = note.replies || []
|
|
773
|
+
if (replies.length) lines.push("", "Conversation:", ...replies.map((r) => `- ${r.from}: ${r.text}`))
|
|
774
|
+
return lines.join("\n")
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* get_animation: one animation's identity, timing, keyframes, and recording times
|
|
779
|
+
* mapped onto its clock, with the conversion to each mechanism's units.
|
|
780
|
+
* @param {any} a an anims[] entry, or a clip turned into one (animFromClip)
|
|
781
|
+
* @param {number[]} Ts recording times
|
|
782
|
+
* @param {{ start?: number, sampled?: any[] }} [ctx]
|
|
783
|
+
*/
|
|
784
|
+
export function animationReport(a, Ts, ctx = {}) {
|
|
785
|
+
const start = ctx.start || 0
|
|
786
|
+
const lines = animLines({ ...a, at: null, from: null, to: null }, ctx, null)
|
|
787
|
+
const dur = Number((a.timing || {}).duration) || 0
|
|
788
|
+
const delay = Number((a.timing || {}).delay) || 0
|
|
789
|
+
if (a.keyframes && a.keyframes.length > 2) {
|
|
790
|
+
lines.push(" keyframes on its own clock:")
|
|
791
|
+
for (const k of a.keyframes) lines.push(` ${num(k.offset, 3)} → local ${msText(localOfOffset(a, k.offset))} = recording ${clockText(recordingOf(a, localOfOffset(a, k.offset)) - start)}`)
|
|
792
|
+
}
|
|
793
|
+
for (const T of Ts) {
|
|
794
|
+
const p = pointOf(a, T)
|
|
795
|
+
const sampled = (ctx.sampled || []).find((s) => s && Math.abs(s.T - T) < FRAME)
|
|
796
|
+
lines.push("", `Recording ${clockText(T - start)}: ${p.phase === "delay" ? `in the delay (${msText(-p.local)} before local 0)` : p.phase === "after" ? "after the end" : `local ${msText(p.local)}`}, progress ${num(p.progress, 4)}, eased ${num(p.eased, 4)}${p.segment ? `, ${segText(p.segment)} at ${pct(p.segment.progress)}` : ""}`)
|
|
797
|
+
const eased = !isLinear((a.timing || {}).easing)
|
|
798
|
+
lines.push(
|
|
799
|
+
eased
|
|
800
|
+
? ` as a keyframe offset with the effect's easing (${a.timing.easing}) kept: ${num(p.eased, 4)} · on plain time (that easing moved into the keyframes): ${num(p.progress, 4)} · as GSAP: ${num((p.local + delay) / 1000, 3)}s into the tween (${num(p.local / 1000, 3)}s after its delay)`
|
|
801
|
+
: ` as CSS: ${num(p.progress * 100, 2)}% of the keyframes · as Motion times: ${num(p.progress, 4)} · as GSAP: ${num((p.local + delay) / 1000, 3)}s into the tween (${num(p.local / 1000, 3)}s after its delay)`,
|
|
802
|
+
)
|
|
803
|
+
if (sampled) lines.push(" " + pointLine("sampled", sampled, null))
|
|
804
|
+
}
|
|
805
|
+
if (!Ts.length) lines.push("", `(pass at, from/to as recording times, like 00:01.20, to map them onto this animation; its duration is ${msText(dur)})`)
|
|
806
|
+
return lines.join("\n")
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** A recorded clip (rec.clips / timeline().clips) as an anims[] entry, as far as the clip knows it. */
|
|
810
|
+
export function animFromClip(c) {
|
|
811
|
+
const kindOf = { transition: "css-transition", "css-animation": "css-animation", waapi: "waapi" }
|
|
812
|
+
const iterations = c.iterations === "infinite" ? Infinity : Number(c.iterations) || 1
|
|
813
|
+
const timing = c.timing
|
|
814
|
+
? { ...c.timing }
|
|
815
|
+
: {
|
|
816
|
+
delay: Number(c.delay) || 0,
|
|
817
|
+
duration: c.dur != null ? Number(c.dur) : c.end != null ? Math.max(0, (c.end - c.start - (Number(c.delay) || 0)) / (iterations === Infinity ? 1 : iterations)) : 0,
|
|
818
|
+
iterations,
|
|
819
|
+
direction: "normal",
|
|
820
|
+
fill: "none",
|
|
821
|
+
easing: "linear",
|
|
822
|
+
playbackRate: 1,
|
|
823
|
+
}
|
|
824
|
+
timing.start = c.start
|
|
825
|
+
timing.end = c.end == null ? null : c.end
|
|
826
|
+
if (timing.activeStart == null) timing.activeStart = c.activeStart != null ? c.activeStart : c.start + (Number(timing.delay) || 0) / (Number(timing.playbackRate) || 1)
|
|
827
|
+
const keyframes = c.kf || (c.from && c.to ? [{ offset: 0, easing: "linear", values: c.from }, { offset: 1, easing: "linear", values: c.to }] : [])
|
|
828
|
+
return {
|
|
829
|
+
id: c.id,
|
|
830
|
+
relation: "on",
|
|
831
|
+
selector: c.selector || null,
|
|
832
|
+
kind: kindOf[c.kind] || c.kind || "waapi",
|
|
833
|
+
...(c.lib ? { lib: c.lib } : {}),
|
|
834
|
+
name: c.kind === "transition" ? `${c.property || "?"} transition` : c.label || c.property || "animation",
|
|
835
|
+
defined: c.def || null,
|
|
836
|
+
...(c.trigger ? { trigger: c.trigger } : {}),
|
|
837
|
+
timing,
|
|
838
|
+
keyframes,
|
|
839
|
+
approx: !c.kf,
|
|
840
|
+
...(c.gsap ? { gsap: c.gsap } : {}),
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export { clockText, msText, FRAME }
|