@erclx/canon 5.66.0 → 5.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/app-drive/REQUIREMENT.md +35 -0
- package/claude/skills/app-drive/SKILL.md +112 -0
- package/claude/skills/app-drive/references/config.md +38 -0
- package/claude/skills/app-drive/references/drive-record.md +70 -0
- package/claude/skills/app-drive/references/isolate.md +46 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/create-skill/references/skill.md +1 -1
- package/claude/skills/plan-feature/references/plan.md +1 -1
- package/claude/skills/plan-intake/references/intake.md +12 -2
- package/claude/skills/pr-evidence/REQUIREMENT.md +1 -1
- package/claude/skills/pr-evidence/SKILL.md +7 -1
- package/claude/skills/pr-evidence/references/evidence.md +33 -3
- package/claude/skills/review-craft/references/ui.md +30 -10
- package/claude/skills/review-ui/SKILL.md +5 -4
- package/claude/skills/review-ui/references/drive-copy.md +4 -0
- package/claude/skills/review-ui/references/driving.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +15 -2
- package/claude/skills/role-planner/SKILL.md +2 -2
- package/claude/skills/role-worker/SKILL.md +4 -6
- package/claude/skills/session-map/REQUIREMENT.md +1 -1
- package/claude/skills/session-map/SKILL.md +8 -1
- package/claude/skills/session-map/references/session.md +19 -5
- package/claude/skills/ui-record/REQUIREMENT.md +36 -0
- package/claude/skills/ui-record/SKILL.md +114 -0
- package/claude/skills/ui-record/references/config.md +40 -0
- package/claude/skills/ui-record/references/handoff.md +67 -0
- package/claude/skills/ui-record/references/pr-clip.md +84 -0
- package/claude/skills/ui-record/references/take-craft.md +99 -0
- package/docs/agents/records.md +2 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +1 -0
- package/docs/workflow/skill-map.md +1 -0
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/package.json +5 -2
- package/src/audits/catalog.ts +5 -1
- package/src/browser/agent-cli.ts +68 -0
- package/src/claude/cases/workflow.ts +20 -0
- package/src/cli/help.ts +3 -0
- package/src/cli.ts +4 -0
- package/src/commands/browser.ts +102 -0
- package/src/commands/intake.ts +181 -1
- package/src/commands/pr.ts +252 -85
- package/src/commands/records.ts +18 -1
- package/src/commands/take.ts +302 -0
- package/src/context/requirements.ts +1 -11
- package/src/git/status.ts +73 -0
- package/src/intake/drive-check.ts +355 -0
- package/src/intake/folder.ts +1 -1
- package/src/markdown/scan.ts +10 -0
- package/src/pr/drive-copy.ts +97 -84
- package/src/pr/evidence.ts +208 -78
- package/src/pr/frames.ts +42 -4
- package/src/pr/pixels.ts +44 -0
- package/src/records/backup.ts +28 -4
- package/src/records/session-map.ts +75 -0
- package/src/records/validate.ts +64 -8
- package/src/take/camera.ts +309 -0
- package/src/take/encode.ts +207 -0
- package/src/take/film.ts +211 -0
- package/src/take/fixtures/fade.html +49 -0
- package/src/take/fixtures/fetch.html +38 -0
- package/src/take/log.ts +109 -0
- package/src/take/perform.ts +200 -0
- package/src/take/plan.ts +193 -0
- package/src/take/pointer.ts +168 -0
- package/src/take/reel.ts +215 -0
- package/src/take/screencast.ts +97 -0
- package/src/take/shell.ts +287 -0
- package/src/take/stepper.ts +255 -0
- package/src/take/zoom/NOTICE.md +45 -0
- package/src/take/zoom/math-utils.ts +69 -0
- package/src/take/zoom/region-utils.ts +55 -0
- package/src/take/zoom/spring.ts +133 -0
- package/src/tasks/archive.ts +1 -3
- package/claude/skills/review-ui/scripts/pw.sh +0 -60
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { openFrameEncoder } from '@/take/encode'
|
|
2
|
+
import { FPS, type LogEvent, START, VIEWPORT } from '@/take/log'
|
|
3
|
+
import { type PointerInput, type Stage, performSteps } from '@/take/perform'
|
|
4
|
+
import type { Beat } from '@/take/plan'
|
|
5
|
+
import { type ShellPage, openShell } from '@/take/shell'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Films a beat by frame stepping. The page's clock is virtual and advances one
|
|
9
|
+
* 60 fps frame per step, so every transition, animation, and timer moves
|
|
10
|
+
* exactly one frame between screenshots however long a frame takes to render,
|
|
11
|
+
* and each screenshot comes back at twice the viewport, 3840x2160.
|
|
12
|
+
*
|
|
13
|
+
* Three orderings hold a take together, and each one broken deadlocks or
|
|
14
|
+
* freezes it rather than failing loudly:
|
|
15
|
+
*
|
|
16
|
+
* - An input is sent, a frame is stepped, and only then is the input awaited,
|
|
17
|
+
* since under begin-frame control an input is acknowledged by a frame
|
|
18
|
+
* - Frame time continues Chromium's monotonic clock, since a frame stamped on
|
|
19
|
+
* any other base leaves every CSS animation at its first frame
|
|
20
|
+
* - No frame is begun before the load event, since one sent mid-navigation can
|
|
21
|
+
* go unanswered, and each loading frame is capped so a lost one cannot hang
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const FRAME_MS = 1000 / FPS
|
|
25
|
+
|
|
26
|
+
/** Budgets of 17, 17, and 16 ms average one 60 fps frame exactly. */
|
|
27
|
+
const BUDGETS = [17, 17, 16] as const
|
|
28
|
+
|
|
29
|
+
/** A frame slower than this means the page will not step, so the beat falls back. */
|
|
30
|
+
export const STALL_MS = 2000
|
|
31
|
+
|
|
32
|
+
/** The still the edit can cut on, before the beat's first action and after its last. */
|
|
33
|
+
export const HOLD_MS = 600
|
|
34
|
+
|
|
35
|
+
const LOAD_CAP_MS = 15_000
|
|
36
|
+
const SETTLE_MS = 2500
|
|
37
|
+
const LOADING_FRAME_CAP_MS = 250
|
|
38
|
+
|
|
39
|
+
/** A frame that took longer than `STALL_MS`, so the beat is filmed another way. */
|
|
40
|
+
export class StepperStalled extends Error {
|
|
41
|
+
constructor(readonly frame: number) {
|
|
42
|
+
super(`frame ${frame} took over ${STALL_MS / 1000} s under frame stepping`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SteppedTake {
|
|
47
|
+
readonly frames: number
|
|
48
|
+
readonly events: readonly LogEvent[]
|
|
49
|
+
/** The last frame encoded, as the JPEG the shell returned. */
|
|
50
|
+
readonly still: Buffer
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SteppedRequest {
|
|
54
|
+
readonly executable: string
|
|
55
|
+
readonly url: string
|
|
56
|
+
readonly beat: Beat
|
|
57
|
+
readonly clip: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function filmStepped(
|
|
61
|
+
request: SteppedRequest,
|
|
62
|
+
): Promise<SteppedTake> {
|
|
63
|
+
const page = await openShell(request.executable, VIEWPORT)
|
|
64
|
+
const encoder = openFrameEncoder(request.clip, FPS)
|
|
65
|
+
try {
|
|
66
|
+
let ticks = await load(page, request.url)
|
|
67
|
+
const beginFrame = (screenshot: boolean) =>
|
|
68
|
+
page.send<{ screenshotData?: string }>(
|
|
69
|
+
'HeadlessExperimental.beginFrame',
|
|
70
|
+
{
|
|
71
|
+
frameTimeTicks: (ticks += FRAME_MS),
|
|
72
|
+
interval: FRAME_MS,
|
|
73
|
+
...(screenshot
|
|
74
|
+
? { screenshot: { format: 'jpeg', quality: 95 } }
|
|
75
|
+
: {}),
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
await pump(beginFrame, SETTLE_MS)
|
|
79
|
+
|
|
80
|
+
const live = liveStage(page, beginFrame)
|
|
81
|
+
await performSteps(live, request.beat.setup, { pointer: START })
|
|
82
|
+
if (request.beat.setup.length > 0) await live.hold(1000)
|
|
83
|
+
await live.pointer({ type: 'move', ...START })
|
|
84
|
+
|
|
85
|
+
await page.send('Emulation.setVirtualTimePolicy', { policy: 'pause' })
|
|
86
|
+
|
|
87
|
+
const events: LogEvent[] = []
|
|
88
|
+
let frames = 0
|
|
89
|
+
let budget = 0
|
|
90
|
+
let last: Buffer | undefined
|
|
91
|
+
const expired: (() => void)[] = []
|
|
92
|
+
page.on('Emulation.virtualTimeBudgetExpired', () => expired.shift()?.())
|
|
93
|
+
|
|
94
|
+
const frame = async (): Promise<void> => {
|
|
95
|
+
const advanced = new Promise<void>((done) => expired.push(done))
|
|
96
|
+
// Plain advance rather than pausing on pending fetches, which held the
|
|
97
|
+
// clock through every page switch that loads its view, past the stall
|
|
98
|
+
// limit. A refused advance never expires its budget, which the stall
|
|
99
|
+
// reports.
|
|
100
|
+
page
|
|
101
|
+
.send('Emulation.setVirtualTimePolicy', {
|
|
102
|
+
policy: 'advance',
|
|
103
|
+
budget: BUDGETS[budget++ % BUDGETS.length],
|
|
104
|
+
})
|
|
105
|
+
.catch(() => {})
|
|
106
|
+
const reply = await withinStall(
|
|
107
|
+
advanced.then(() => beginFrame(true)),
|
|
108
|
+
frames,
|
|
109
|
+
)
|
|
110
|
+
const data = reply.screenshotData
|
|
111
|
+
? Buffer.from(reply.screenshotData, 'base64')
|
|
112
|
+
: last
|
|
113
|
+
if (!data) throw new Error('the first frame carried no screenshot')
|
|
114
|
+
last = data
|
|
115
|
+
await encoder.write(data)
|
|
116
|
+
frames += 1
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const recording: Stage = {
|
|
120
|
+
now: () => frames * FRAME_MS,
|
|
121
|
+
hold: async (ms) => {
|
|
122
|
+
for (let i = 0; i < Math.round(ms / FRAME_MS); i += 1) await frame()
|
|
123
|
+
},
|
|
124
|
+
pointer: async (input) => {
|
|
125
|
+
const sent = page.send('Input.dispatchMouseEvent', mouseEvent(input))
|
|
126
|
+
await frame()
|
|
127
|
+
await sent
|
|
128
|
+
},
|
|
129
|
+
key: async (text) => {
|
|
130
|
+
const sent = page.send('Input.insertText', { text })
|
|
131
|
+
await frame()
|
|
132
|
+
await sent
|
|
133
|
+
},
|
|
134
|
+
evaluate: (expression) => page.evaluate(expression),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await recording.hold(HOLD_MS)
|
|
138
|
+
await performSteps(recording, request.beat.steps, {
|
|
139
|
+
pointer: START,
|
|
140
|
+
log: events,
|
|
141
|
+
})
|
|
142
|
+
await recording.hold(HOLD_MS)
|
|
143
|
+
|
|
144
|
+
await encoder.close()
|
|
145
|
+
if (!last) throw new Error('the take encoded no frame')
|
|
146
|
+
return { frames, events, still: last }
|
|
147
|
+
} catch (error) {
|
|
148
|
+
encoder.abort()
|
|
149
|
+
throw error
|
|
150
|
+
} finally {
|
|
151
|
+
page.close()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Navigates and resolves with Chromium's monotonic clock in milliseconds, read
|
|
157
|
+
* off the load event's own timestamp. The runtime's clock cannot stand in for
|
|
158
|
+
* it: Node's `hrtime` shares the base on Linux, Bun's counts from its own
|
|
159
|
+
* start, and a frame stamped on the wrong base freezes every animation.
|
|
160
|
+
*/
|
|
161
|
+
async function load(page: ShellPage, url: string): Promise<number> {
|
|
162
|
+
const loaded = new Promise<number>((done) =>
|
|
163
|
+
page.on('Page.loadEventFired', ({ timestamp }) => {
|
|
164
|
+
if (typeof timestamp === 'number') done(timestamp * 1000)
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
const navigated = await page.send<{ errorText?: string }>('Page.navigate', {
|
|
168
|
+
url,
|
|
169
|
+
})
|
|
170
|
+
if (navigated.errorText) {
|
|
171
|
+
throw new Error(`${navigated.errorText} at ${url}`)
|
|
172
|
+
}
|
|
173
|
+
const timestamp = await Promise.race([loaded, sleep(LOAD_CAP_MS)])
|
|
174
|
+
if (typeof timestamp !== 'number') {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`${url} fired no load event within ${LOAD_CAP_MS / 1000} s, so no frame clock could be read`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
return timestamp
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Begins frames in real time so loading, fonts, and layout settle. */
|
|
183
|
+
async function pump(
|
|
184
|
+
beginFrame: (screenshot: boolean) => Promise<unknown>,
|
|
185
|
+
ms: number,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const end = Date.now() + ms
|
|
188
|
+
while (Date.now() < end) {
|
|
189
|
+
await Promise.race([
|
|
190
|
+
beginFrame(false).catch(() => {}),
|
|
191
|
+
sleep(LOADING_FRAME_CAP_MS),
|
|
192
|
+
])
|
|
193
|
+
await sleep(12)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The stage setup runs on, before virtual time is paused. Its frames are begun
|
|
199
|
+
* without screenshots, since nothing before the first held frame is kept.
|
|
200
|
+
*/
|
|
201
|
+
function liveStage(
|
|
202
|
+
page: ShellPage,
|
|
203
|
+
beginFrame: (screenshot: boolean) => Promise<unknown>,
|
|
204
|
+
): Stage {
|
|
205
|
+
const started = Date.now()
|
|
206
|
+
return {
|
|
207
|
+
now: () => Date.now() - started,
|
|
208
|
+
hold: (ms) => pump(beginFrame, ms),
|
|
209
|
+
pointer: (input) => {
|
|
210
|
+
const sent = page.send('Input.dispatchMouseEvent', mouseEvent(input))
|
|
211
|
+
return withinStall(
|
|
212
|
+
beginFrame(false).then(() => sent),
|
|
213
|
+
0,
|
|
214
|
+
).then(() => {})
|
|
215
|
+
},
|
|
216
|
+
key: (text) => {
|
|
217
|
+
const sent = page.send('Input.insertText', { text })
|
|
218
|
+
return withinStall(
|
|
219
|
+
beginFrame(false).then(() => sent),
|
|
220
|
+
0,
|
|
221
|
+
).then(() => {})
|
|
222
|
+
},
|
|
223
|
+
evaluate: (expression) => page.evaluate(expression),
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function mouseEvent(input: PointerInput): Record<string, unknown> {
|
|
228
|
+
const at = { x: input.x, y: input.y }
|
|
229
|
+
switch (input.type) {
|
|
230
|
+
case 'move':
|
|
231
|
+
return { type: 'mouseMoved', ...at, button: 'none' }
|
|
232
|
+
case 'down':
|
|
233
|
+
return { type: 'mousePressed', ...at, button: 'left', clickCount: 1 }
|
|
234
|
+
case 'up':
|
|
235
|
+
return { type: 'mouseReleased', ...at, button: 'left', clickCount: 1 }
|
|
236
|
+
case 'wheel':
|
|
237
|
+
return { type: 'mouseWheel', ...at, deltaX: 0, deltaY: input.dy }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function withinStall<T>(work: Promise<T>, frame: number): Promise<T> {
|
|
242
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
243
|
+
const stalled = new Promise<never>((_, fail) => {
|
|
244
|
+
timer = setTimeout(() => fail(new StepperStalled(frame)), STALL_MS)
|
|
245
|
+
})
|
|
246
|
+
try {
|
|
247
|
+
return await Promise.race([work, stalled])
|
|
248
|
+
} finally {
|
|
249
|
+
clearTimeout(timer)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function sleep(ms: number): Promise<void> {
|
|
254
|
+
return new Promise((done) => setTimeout(done, ms))
|
|
255
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Vendored zoom notice
|
|
2
|
+
|
|
3
|
+
The files in this folder carry the zoom timing of [OpenScreen](https://github.com/siddharthvaddem/openscreen) at commit `ca8104c279da0d4b4481ec992ff103949e22c772`, under the MIT license below.
|
|
4
|
+
|
|
5
|
+
## What came from where
|
|
6
|
+
|
|
7
|
+
| File | Source in OpenScreen, under `src/components/video-editor/videoPlayback/` |
|
|
8
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `math-utils.ts` | `mathUtils.ts` |
|
|
10
|
+
| `region-utils.ts` | `zoomRegionUtils.ts`, with `TRANSITION_WINDOW_MS` and `ZOOM_IN_TRANSITION_WINDOW_MS` from `constants.ts` |
|
|
11
|
+
| `spring.ts` | `motionSmoothing.ts` and `zoomSpring.ts` |
|
|
12
|
+
|
|
13
|
+
## What changed
|
|
14
|
+
|
|
15
|
+
- The file names are kebab-case, as this repository names every TypeScript file, and the code is reformatted to its style
|
|
16
|
+
- `math-utils.ts` keeps `clamp01`, `cubicBezier`, and `easeOutScreenStudio`, and drops the easings nothing here calls
|
|
17
|
+
- `region-utils.ts` keeps `computeRegionStrength`, the connected-pan easing, and the window and gap constants. It drops the dominant-region search, its cache, the 3D rotation, and the cursor-follow focus, since the camera here aims at clicks and holds its focus by its own rule.
|
|
18
|
+
- The zoom-out window is 700 ms rather than 1015.05 ms, and the connected pan runs 600 ms rather than 1000 ms, both on the operator's eye in groundwork track `193-ui-record`. The zoom-in window keeps OpenScreen's 1522.575 ms.
|
|
19
|
+
- `spring.ts` keeps the zoom spring's stiffness 320, damping 40, mass 0.92, its rest thresholds, the delta clamp, and the overshoot clamp from `stepAxis`. OpenScreen steps that spring through the `motion` package, and this solves it in closed form instead, so no dependency comes with it.
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
```plaintext
|
|
24
|
+
MIT License
|
|
25
|
+
|
|
26
|
+
Copyright (c) 2025 Siddharth Vaddem
|
|
27
|
+
|
|
28
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
29
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
30
|
+
in the Software without restriction, including without limitation the rights
|
|
31
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
32
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
33
|
+
furnished to do so, subject to the following conditions:
|
|
34
|
+
|
|
35
|
+
The above copyright notice and this permission notice shall be included in all
|
|
36
|
+
copies or substantial portions of the Software.
|
|
37
|
+
|
|
38
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
39
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
40
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
41
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
42
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
43
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
44
|
+
SOFTWARE.
|
|
45
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Vendored from OpenScreen at ca8104c, MIT. See NOTICE.md beside this file.
|
|
2
|
+
// Source: src/components/video-editor/videoPlayback/mathUtils.ts
|
|
3
|
+
|
|
4
|
+
export function clamp01(value: number) {
|
|
5
|
+
return Math.max(0, Math.min(1, value))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function sampleCubicBezier(a1: number, a2: number, t: number) {
|
|
9
|
+
const oneMinusT = 1 - t
|
|
10
|
+
return (
|
|
11
|
+
3 * a1 * oneMinusT * oneMinusT * t + 3 * a2 * oneMinusT * t * t + t * t * t
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sampleCubicBezierDerivative(a1: number, a2: number, t: number) {
|
|
16
|
+
const oneMinusT = 1 - t
|
|
17
|
+
return (
|
|
18
|
+
3 * a1 * oneMinusT * oneMinusT +
|
|
19
|
+
6 * (a2 - a1) * oneMinusT * t +
|
|
20
|
+
3 * (1 - a2) * t * t
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function cubicBezier(
|
|
25
|
+
x1: number,
|
|
26
|
+
y1: number,
|
|
27
|
+
x2: number,
|
|
28
|
+
y2: number,
|
|
29
|
+
t: number,
|
|
30
|
+
) {
|
|
31
|
+
const targetX = clamp01(t)
|
|
32
|
+
let solvedT = targetX
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < 8; i += 1) {
|
|
35
|
+
const currentX = sampleCubicBezier(x1, x2, solvedT) - targetX
|
|
36
|
+
const currentDerivative = sampleCubicBezierDerivative(x1, x2, solvedT)
|
|
37
|
+
|
|
38
|
+
if (Math.abs(currentX) < 1e-6 || Math.abs(currentDerivative) < 1e-6) {
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
solvedT -= currentX / currentDerivative
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let lower = 0
|
|
46
|
+
let upper = 1
|
|
47
|
+
solvedT = clamp01(solvedT)
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < 10; i += 1) {
|
|
50
|
+
const currentX = sampleCubicBezier(x1, x2, solvedT)
|
|
51
|
+
if (Math.abs(currentX - targetX) < 1e-6) {
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (currentX < targetX) {
|
|
56
|
+
lower = solvedT
|
|
57
|
+
} else {
|
|
58
|
+
upper = solvedT
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
solvedT = (lower + upper) / 2
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return sampleCubicBezier(y1, y2, solvedT)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function easeOutScreenStudio(t: number) {
|
|
68
|
+
return cubicBezier(0.16, 1, 0.3, 1, t)
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Vendored from OpenScreen at ca8104c, MIT. See NOTICE.md beside this file.
|
|
2
|
+
// Source: src/components/video-editor/videoPlayback/zoomRegionUtils.ts and the
|
|
3
|
+
// two window constants from constants.ts. The zoom-out window and the chained
|
|
4
|
+
// pan's duration carry the operator's tuning, and NOTICE.md lists both.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
clamp01,
|
|
8
|
+
cubicBezier,
|
|
9
|
+
easeOutScreenStudio,
|
|
10
|
+
} from '@/take/zoom/math-utils'
|
|
11
|
+
|
|
12
|
+
/** OpenScreen's 1015.05 ms, shortened to 700 ms for a quicker zoom-out. */
|
|
13
|
+
export const TRANSITION_WINDOW_MS = 700
|
|
14
|
+
/** OpenScreen's zoom-in window, 1.5 times its original 1015.05 ms window. */
|
|
15
|
+
export const ZOOM_IN_TRANSITION_WINDOW_MS = 1015.05 * 1.5
|
|
16
|
+
|
|
17
|
+
export const CHAINED_ZOOM_PAN_GAP_MS = 1500
|
|
18
|
+
/** OpenScreen's 1000 ms, shortened to 600 ms. */
|
|
19
|
+
export const CONNECTED_ZOOM_PAN_DURATION_MS = 600
|
|
20
|
+
export const ZOOM_IN_OVERLAP_MS = 500
|
|
21
|
+
|
|
22
|
+
export interface ZoomRegion {
|
|
23
|
+
startMs: number
|
|
24
|
+
endMs: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function lerp(start: number, end: number, amount: number) {
|
|
28
|
+
return start + (end - start) * amount
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function easeConnectedPan(value: number) {
|
|
32
|
+
return cubicBezier(0.1, 0.0, 0.2, 1.0, value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function computeRegionStrength(region: ZoomRegion, timeMs: number) {
|
|
36
|
+
const zoomInEnd = region.startMs + ZOOM_IN_OVERLAP_MS
|
|
37
|
+
const leadInStart = zoomInEnd - ZOOM_IN_TRANSITION_WINDOW_MS
|
|
38
|
+
const leadOutEnd = region.endMs + TRANSITION_WINDOW_MS
|
|
39
|
+
|
|
40
|
+
if (timeMs < leadInStart || timeMs > leadOutEnd) {
|
|
41
|
+
return 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (timeMs < zoomInEnd) {
|
|
45
|
+
const progress = (timeMs - leadInStart) / ZOOM_IN_TRANSITION_WINDOW_MS
|
|
46
|
+
return easeOutScreenStudio(progress)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (timeMs <= region.endMs) {
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const progress = clamp01((timeMs - region.endMs) / TRANSITION_WINDOW_MS)
|
|
54
|
+
return 1 - easeOutScreenStudio(progress)
|
|
55
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Adapted from OpenScreen at ca8104c, MIT. See NOTICE.md beside this file.
|
|
2
|
+
// Source: getZoomSpringConfig, stepSpringValue, and clampDeltaMs in
|
|
3
|
+
// src/components/video-editor/videoPlayback/motionSmoothing.ts, and stepAxis in
|
|
4
|
+
// zoomSpring.ts. OpenScreen steps its spring through the `motion` package, and
|
|
5
|
+
// this solves the same damped spring in closed form instead, so a target
|
|
6
|
+
// inherits no dependency for one function.
|
|
7
|
+
|
|
8
|
+
export interface SpringState {
|
|
9
|
+
value: number
|
|
10
|
+
velocity: number
|
|
11
|
+
initialized: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SpringConfig {
|
|
15
|
+
stiffness: number
|
|
16
|
+
damping: number
|
|
17
|
+
mass: number
|
|
18
|
+
restDelta: number
|
|
19
|
+
restSpeed: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ZOOM_SPRING: SpringConfig = {
|
|
23
|
+
stiffness: 320,
|
|
24
|
+
damping: 40,
|
|
25
|
+
mass: 0.92,
|
|
26
|
+
restDelta: 0.0005,
|
|
27
|
+
restSpeed: 0.015,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createSpringState(initialValue = 0): SpringState {
|
|
31
|
+
return { value: initialValue, velocity: 0, initialized: false }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function clampDeltaMs(deltaMs: number, fallbackMs = 1000 / 60) {
|
|
35
|
+
if (!Number.isFinite(deltaMs) || deltaMs <= 0) {
|
|
36
|
+
return fallbackMs
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Math.min(80, Math.max(1, deltaMs))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Advances one axis toward `target` by `deltaMs`. The first call snaps to the
|
|
44
|
+
* target, a spring at rest stays there, and a step that would cross a moving
|
|
45
|
+
* target lands on it with no velocity, which keeps a quick chase from wobbling
|
|
46
|
+
* on a reversal.
|
|
47
|
+
*/
|
|
48
|
+
export function stepSpring(
|
|
49
|
+
state: SpringState,
|
|
50
|
+
target: number,
|
|
51
|
+
deltaMs: number,
|
|
52
|
+
config: SpringConfig = ZOOM_SPRING,
|
|
53
|
+
): number {
|
|
54
|
+
if (!state.initialized || !Number.isFinite(state.value)) {
|
|
55
|
+
state.value = target
|
|
56
|
+
state.velocity = 0
|
|
57
|
+
state.initialized = true
|
|
58
|
+
return state.value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
Math.abs(target - state.value) <= config.restDelta &&
|
|
63
|
+
Math.abs(state.velocity) <= config.restSpeed
|
|
64
|
+
) {
|
|
65
|
+
state.value = target
|
|
66
|
+
state.velocity = 0
|
|
67
|
+
return state.value
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const before = state.value
|
|
71
|
+
const [offset, velocity] = solve(
|
|
72
|
+
state.value - target,
|
|
73
|
+
state.velocity,
|
|
74
|
+
clampDeltaMs(deltaMs) / 1000,
|
|
75
|
+
config,
|
|
76
|
+
)
|
|
77
|
+
const after = target + offset
|
|
78
|
+
|
|
79
|
+
const crossed =
|
|
80
|
+
(before <= target && after > target) || (before >= target && after < target)
|
|
81
|
+
if (crossed) {
|
|
82
|
+
state.value = target
|
|
83
|
+
state.velocity = 0
|
|
84
|
+
return target
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
state.value = after
|
|
88
|
+
state.velocity = velocity
|
|
89
|
+
return after
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The displacement and velocity of a damped spring `seconds` after it sat at
|
|
94
|
+
* `x0` from rest length moving at `v0`, for each of the three damping regimes.
|
|
95
|
+
*/
|
|
96
|
+
function solve(
|
|
97
|
+
x0: number,
|
|
98
|
+
v0: number,
|
|
99
|
+
seconds: number,
|
|
100
|
+
{ stiffness, damping, mass }: SpringConfig,
|
|
101
|
+
): [number, number] {
|
|
102
|
+
const omega = Math.sqrt(stiffness / mass)
|
|
103
|
+
const zeta = damping / (2 * Math.sqrt(stiffness * mass))
|
|
104
|
+
const t = seconds
|
|
105
|
+
|
|
106
|
+
if (zeta < 1) {
|
|
107
|
+
const omegaD = omega * Math.sqrt(1 - zeta * zeta)
|
|
108
|
+
const decay = Math.exp(-zeta * omega * t)
|
|
109
|
+
const b = (v0 + zeta * omega * x0) / omegaD
|
|
110
|
+
const cos = Math.cos(omegaD * t)
|
|
111
|
+
const sin = Math.sin(omegaD * t)
|
|
112
|
+
const x = decay * (x0 * cos + b * sin)
|
|
113
|
+
const v =
|
|
114
|
+
decay *
|
|
115
|
+
(-zeta * omega * (x0 * cos + b * sin) + omegaD * (b * cos - x0 * sin))
|
|
116
|
+
return [x, v]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (zeta === 1) {
|
|
120
|
+
const decay = Math.exp(-omega * t)
|
|
121
|
+
const b = v0 + omega * x0
|
|
122
|
+
return [(x0 + b * t) * decay, (b - omega * (x0 + b * t)) * decay]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const root = Math.sqrt(zeta * zeta - 1)
|
|
126
|
+
const r1 = -omega * (zeta - root)
|
|
127
|
+
const r2 = -omega * (zeta + root)
|
|
128
|
+
const c1 = (v0 - r2 * x0) / (r1 - r2)
|
|
129
|
+
const c2 = x0 - c1
|
|
130
|
+
const e1 = Math.exp(r1 * t)
|
|
131
|
+
const e2 = Math.exp(r2 * t)
|
|
132
|
+
return [c1 * e1 + c2 * e2, c1 * r1 * e1 + c2 * r2 * e2]
|
|
133
|
+
}
|
package/src/tasks/archive.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { regenOne } from '@/indexes/regen'
|
|
|
7
7
|
import { rerenderRow } from '@/tasks/board'
|
|
8
8
|
import { planBranch } from '@/tasks/branch'
|
|
9
9
|
import { isUnder } from '@/tasks/paths'
|
|
10
|
+
import { SESSION_MAP_PREFIX } from '@/records/session-map'
|
|
10
11
|
import { recordDir, recordDirs } from '@/roots/record'
|
|
11
12
|
|
|
12
13
|
const TASKS = 'tasks'
|
|
@@ -29,9 +30,6 @@ export const RESERVED_STEMS = [
|
|
|
29
30
|
'session',
|
|
30
31
|
] as const
|
|
31
32
|
|
|
32
|
-
/** The pre-compaction handoff takes one file per session, so its stems vary. */
|
|
33
|
-
const SESSION_MAP_PREFIX = 'session-'
|
|
34
|
-
|
|
35
33
|
export function isReservedStem(stem: string): boolean {
|
|
36
34
|
const reserved: readonly string[] = RESERVED_STEMS
|
|
37
35
|
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Pins the agent browser CLI the UI pass drives through, so every call in a run
|
|
3
|
-
# and every run on a machine reach one version. `open` is forced onto the
|
|
4
|
-
# bundled Chromium, since the CLI's default channel is a branded Chrome most
|
|
5
|
-
# machines lack, and it refuses rather than downloading when that browser is
|
|
6
|
-
# absent, since a dispatched reviewer that installs mid-pass spends its budget
|
|
7
|
-
# on the download.
|
|
8
|
-
set -euo pipefail
|
|
9
|
-
|
|
10
|
-
PW_CLI="@playwright/cli@0.1.22"
|
|
11
|
-
# The Chromium revision that CLI version requires. It moves with the pin, and
|
|
12
|
-
# it is separate from the revision any other Playwright on the machine uses.
|
|
13
|
-
PW_CHROMIUM_REVISION="1247"
|
|
14
|
-
|
|
15
|
-
die() {
|
|
16
|
-
echo "pw.sh: $*" >&2
|
|
17
|
-
exit 1
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
browsers_root() {
|
|
21
|
-
if [ -n "${PLAYWRIGHT_BROWSERS_PATH:-}" ]; then
|
|
22
|
-
echo "$PLAYWRIGHT_BROWSERS_PATH"
|
|
23
|
-
elif [ "$(uname -s 2>/dev/null || echo unknown)" = Darwin ]; then
|
|
24
|
-
echo "$HOME/Library/Caches/ms-playwright"
|
|
25
|
-
else
|
|
26
|
-
echo "$HOME/.cache/ms-playwright"
|
|
27
|
-
fi
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
command -v bunx >/dev/null 2>&1 || die "bunx is not on PATH, so the pinned CLI cannot run"
|
|
31
|
-
|
|
32
|
-
# Global flags may lead the verb, as in `-s=<name> --raw open <url>`.
|
|
33
|
-
args=("$@")
|
|
34
|
-
verb_at=0
|
|
35
|
-
for i in "${!args[@]}"; do
|
|
36
|
-
case "${args[$i]}" in
|
|
37
|
-
-*) ;;
|
|
38
|
-
*)
|
|
39
|
-
verb_at=$i
|
|
40
|
-
break
|
|
41
|
-
;;
|
|
42
|
-
esac
|
|
43
|
-
done
|
|
44
|
-
|
|
45
|
-
if [ "${args[$verb_at]:-}" = open ]; then
|
|
46
|
-
if [ ! -d "$(browsers_root)/chromium-$PW_CHROMIUM_REVISION" ]; then
|
|
47
|
-
die "Chromium revision $PW_CHROMIUM_REVISION is not installed. Run once, outside the pass: bunx -y $PW_CLI install-browser chromium"
|
|
48
|
-
fi
|
|
49
|
-
has_browser=false
|
|
50
|
-
for arg in "${args[@]}"; do
|
|
51
|
-
case "$arg" in
|
|
52
|
-
--browser=* | --browser) has_browser=true ;;
|
|
53
|
-
esac
|
|
54
|
-
done
|
|
55
|
-
if [ "$has_browser" = false ]; then
|
|
56
|
-
args=("${args[@]:0:$((verb_at + 1))}" --browser=chromium "${args[@]:$((verb_at + 1))}")
|
|
57
|
-
fi
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
exec bunx -y "$PW_CLI" "${args[@]}"
|