versioncam 0.5.3 → 0.7.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 +103 -0
- package/README.md +108 -6
- package/dist/cli/commands/check.js +25 -0
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/doctor.js +54 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/preview.js +4 -2
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/cli/commands/record.d.ts +6 -0
- package/dist/cli/commands/record.js +24 -3
- package/dist/cli/commands/record.js.map +1 -1
- package/dist/cli/commands/stability.js +10 -3
- package/dist/cli/commands/stability.js.map +1 -1
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +2 -0
- package/dist/cli/usage.js.map +1 -1
- package/dist/config.d.ts +45 -4
- package/dist/config.js +134 -8
- package/dist/config.js.map +1 -1
- package/dist/core/cursor.d.ts +22 -0
- package/dist/core/cursor.js +6 -0
- package/dist/core/cursor.js.map +1 -1
- package/dist/core/motion-defaults.d.ts +8 -0
- package/dist/core/motion-defaults.js +8 -0
- package/dist/core/motion-defaults.js.map +1 -1
- package/dist/core/timeline.d.ts +24 -0
- package/dist/core/timeline.js.map +1 -1
- package/dist/driver/clip.d.ts +6 -0
- package/dist/driver/clip.js +33 -2
- package/dist/driver/clip.js.map +1 -1
- package/dist/driver/file-window.d.ts +66 -0
- package/dist/driver/file-window.js +245 -0
- package/dist/driver/file-window.js.map +1 -0
- package/dist/driver/page-hooks.d.ts +23 -1
- package/dist/driver/page-hooks.js +36 -1
- package/dist/driver/page-hooks.js.map +1 -1
- package/dist/driver/replay.d.ts +263 -0
- package/dist/driver/replay.js +558 -0
- package/dist/driver/replay.js.map +1 -0
- package/dist/driver/session.d.ts +70 -4
- package/dist/driver/session.js +267 -29
- package/dist/driver/session.js.map +1 -1
- package/dist/driver/settle.js +6 -0
- package/dist/driver/settle.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js.map +1 -1
- package/dist/page/assets/index-CnRDAiUz.js +2 -0
- package/dist/page/index.html +1 -1
- package/dist/publish/protocol.d.ts +12 -1
- package/dist/publish/protocol.js.map +1 -1
- package/dist/render/render.d.ts +7 -1
- package/dist/render/render.js +51 -17
- package/dist/render/render.js.map +1 -1
- package/dist/render/sequence.d.ts +10 -5
- package/dist/render/sequence.js +44 -29
- package/dist/render/sequence.js.map +1 -1
- package/dsl.md +68 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/versioncam/authoring.md +15 -1
- package/plugin/skills/versioncam/onboarding.md +9 -3
- package/dist/page/assets/index-BtUbUEdN.js +0 -2
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export type { AppHandle } from "./app-server.js";
|
|
|
5
5
|
export { clip, runClip, RECORDER_VERSION } from "./driver/clip.js";
|
|
6
6
|
export type { RunClipOptions } from "./driver/clip.js";
|
|
7
7
|
export type { ClipBody, ClipDefinition, ClipOptions, RunClipResult, } from "./driver/clip.js";
|
|
8
|
-
export type { RecordingSession, SessionOptions } from "./driver/session.js";
|
|
8
|
+
export type { DragOptions, FileWindow, Modifier, RecordingSession, ScrollOptions, ScrollWhere, SessionOptions, } from "./driver/session.js";
|
|
9
|
+
export type { FileWindowOptions, StagedFile } from "./driver/file-window.js";
|
|
9
10
|
export { createGate } from "./driver/gate.js";
|
|
10
11
|
export type { Gate } from "./driver/gate.js";
|
|
11
12
|
export type { SettleOptions, SettleReport } from "./driver/settle.js";
|
|
@@ -17,7 +18,8 @@ export type { LoopPlanner, LoopOptions } from "./core/loop.js";
|
|
|
17
18
|
export { sseBody } from "./core/sse.js";
|
|
18
19
|
export type { SseFixtureEvent } from "./core/sse.js";
|
|
19
20
|
export { MOTION } from "./core/motion-defaults.js";
|
|
20
|
-
export type {
|
|
21
|
+
export type { ReplayConfig, ReplayMode, ReplayPattern, } from "./driver/replay.js";
|
|
22
|
+
export type { Caption, CameraKey, Highlight, HighlightStyle, Mark, PointerAction, Rect, Scroll, Timeline, Vec2, } from "./core/timeline.js";
|
|
21
23
|
export type { Scene, Size } from "./scene.js";
|
|
22
24
|
export { loadClips, loadConfig, loadSceneIds, bundleScenes } from "./loader.js";
|
|
23
25
|
export { contactSheet, renderFrame, renderRecording, renderScene, withBrowser, readTimeline, } from "./render/render.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiBxE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiBxE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAkBnE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAqBnD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,YAAY,EACZ,WAAW,EACX,eAAe,EACf,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,SAAS,EACT,UAAU,EACV,WAAW,EACX,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,YAAY,EACZ,eAAe,EACf,cAAc,GACf,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAExE,OAAO,EACL,qBAAqB,EACrB,QAAQ,EACR,OAAO,EACP,SAAS,GACV,MAAM,uBAAuB,CAAC;AAE/B,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let n=t(e);return n*n*n*(10+n*(-15+6*n))}function t(e){return e<=0?0:e>=1?1:e}function n(e,t,n){return e+(t-e)*n}function r(e,t,r){let i=e.x+e.w/2,a=e.y+e.h/2,o=t.x+t.w/2,s=t.y+t.h/2,c=e.w*(t.w/e.w)**+r,l=e.h*(t.h/e.h)**+r,u=n(i,o,r),d=n(a,s,r);return{x:u-c/2,y:d-l/2,w:c,h:l}}function i(e,t,r){return{x:n(e.x,t.x,r),y:n(e.y,t.y,r),w:n(e.w,t.w,r),h:n(e.h,t.h,r)}}function a(t,n){if(t.length===0)throw Error(`evalCamera called with no keys`);if(n<=t[0].t)return t[0].rect;for(let a=1;a<t.length;a+=1){let o=t[a-1],s=t[a];if(n>=s.t)continue;if(s.ease===`hold`)return o.rect;let c=s.t-o.t,l=c<=0?1:(n-o.t)/c;return s.ease===`linear`?i(o.rect,s.rect,l):r(o.rect,s.rect,e(l))}return t[t.length-1].rect}function o(e,t,n,r){let i=1-r,a=i*i,o=2*i*r,s=r*r;return[a*e[0]+o*t[0]+s*n[0],a*e[1]+o*t[1]+s*n[1]]}function s(t,n){if(t.length===0)return[0,0];let r=t[0];if(n<=r.t0)return r.kind===`move`?r.from:r.at;for(let r of t){if(n>r.t1)continue;if(r.kind===`hold`)return r.at;let t=r.t1-r.t0,i=t<=0?1:(n-r.t0)/t;return o(r.from,r.ctrl,r.to,e(i))}let i=t[t.length-1];return i.kind===`move`?i.to:i.at}var c={style:`light`,ripple:!0};function l(e,t,n){let r=n.w/t.w;return[n.x+(e[0]-t.x)*r,n.y+(e[1]-t.y)*r]}function u(e,t,n){let r=n.w/t.w;return{x:n.x+(e.x-t.x)*r,y:n.y+(e.y-t.y)*r,w:e.w*r,h:e.h*r}}function d(e,t){return{x:e.x*t,y:e.y*t,w:e.w*t,h:e.h*t}}function f(e,t,n){let r=Math.min(e.width,e.height)*n,i=e.width-r*2,a=e.height-r*2,o=t.width/t.height,s=i,c=s/o;return c>a&&(c=a,s=c*o),{x:(e.width-s)/2,y:(e.height-c)/2,w:s,h:c}}function p(e,n,r){return r<=0?1:t((e-n+1)/r)}function m(e,t,n){let r=e.frames[t],i=null;for(let a of e.cuts)t>a&&t<=a+n&&e.frames[a]!==r&&(i===null||a>i)&&(i=a);return i===null?{current:r,previous:null,alpha:1}:{current:r,previous:e.frames[i],alpha:p(t,i+1,n)}}function h(e,n,r,i){return e<=n||e>=r?0:i<=0?1:Math.min(t((e-n)/i),t((r-e)/i))}function g(e,t){return e/t}function _(e,t,n){let r=n?.padding??0;return!n||r<=0?{x:0,y:0,w:e.width,h:e.height}:f(e,t,r)}function v(e){let t=e?.caption??`auto`;return t===`none`?`none`:t===`below`||t===`overlay`?t:(e?.padding??0)>0?`below`:`overlay`}function y(e,t){let n=e?.captionInset;return n?.right===void 0?{x:n?.left??40,align:`left`}:{x:t-n.right,align:`right`}}var b={radius:16,hairline:`rgba(255, 255, 255, 0.16)`,shadow:`rgba(0, 0, 0, 0.45)`,shadowBlur:60,shadowOffsetY:18,captionFont:`600 30px "Helvetica Neue", Helvetica, Arial, "Liberation Sans", sans-serif`,captionColor:`rgba(255, 255, 255, 0.94)`,captionShadow:`rgba(0, 0, 0, 0.5)`,captionScrim:`rgba(0, 0, 0, 0.45)`,captionScrimHeight:86,ringWidth:3,ringRadius:10,spotlightDim:.55,spotlightTint:`6, 2, 16`},x={light:{fill:`#ffffff`,stroke:`rgba(0, 0, 0, 0.55)`,shadow:`rgba(0, 0, 0, 0.35)`},dark:{fill:`#000000`,stroke:`rgba(255, 255, 255, 0.9)`,shadow:`rgba(0, 0, 0, 0.3)`}};function ee(e,t){let n=/^#([0-9a-f]{6})$/i.exec(e.trim());if(!n)return e;let r=Number.parseInt(n[1],16);return`rgba(${r>>16&255}, ${r>>8&255}, ${r&255}, ${t})`}function S(e,t,n){let r=Math.min(n,t.w/2,t.h/2);e.beginPath(),e.moveTo(t.x+r,t.y),e.arcTo(t.x+t.w,t.y,t.x+t.w,t.y+t.h,r),e.arcTo(t.x+t.w,t.y+t.h,t.x,t.y+t.h,r),e.arcTo(t.x,t.y+t.h,t.x,t.y,r),e.arcTo(t.x,t.y,t.x+t.w,t.y,r),e.closePath()}function te(e,t,n){if(typeof n==`string`)e.fillStyle=n;else{let r=e.createLinearGradient(0,0,t.width,t.height);r.addColorStop(0,n[0]),r.addColorStop(1,n[1]),e.fillStyle=r}e.fillRect(0,0,t.width,t.height)}function ne(e,t,n,r,i,a,o){let s=d(i,t.viewport.dpr),{current:c,previous:l,alpha:u}=m(t,r,o.motion.crossfadeFrames),f=o.theme.presentation?.frame??!1;e.save(),f&&(S(e,a,b.radius),e.clip());let p=(t,r)=>{let i=n.images[t];i&&(e.globalAlpha=r,e.drawImage(i,s.x,s.y,s.w,s.h,a.x,a.y,a.w,a.h))};l!==null&&p(l,1),p(c,l===null?1:u),e.globalAlpha=1,e.restore()}function C(e,t){e.save(),e.shadowColor=b.shadow,e.shadowBlur=b.shadowBlur,e.shadowOffsetY=b.shadowOffsetY,e.fillStyle=`rgba(0,0,0,1)`,S(e,t,b.radius),e.fill(),e.restore()}function w(e,t){e.save(),e.strokeStyle=b.hairline,e.lineWidth=1,S(e,t,b.radius),e.stroke(),e.restore()}function T(e,t){return{x:e.x-t,y:e.y-t,w:e.w+t*2,h:e.h+t*2}}function E(e,t,n,r,i,a){let o=a.theme.presentation?.frame??!1;for(let s of t.highlights){let t=h(n,s.t0,s.t1,.25);if(t<=0)continue;let c=u(s.rect,r,i);e.save(),e.globalAlpha=t,s.style===`spotlight`?(e.save(),S(e,i,o?b.radius:0),e.clip(),e.fillStyle=`rgba(${b.spotlightTint}, ${b.spotlightDim})`,e.fillRect(i.x,i.y,i.w,i.h),e.globalCompositeOperation=`destination-out`,S(e,T(c,8),b.ringRadius),e.fill(),e.restore()):(e.strokeStyle=a.theme.accent,e.lineWidth=b.ringWidth,e.shadowColor=ee(a.theme.accent,.35),e.shadowBlur=18,S(e,T(c,6),b.ringRadius),e.stroke()),e.restore()}}function D(e,t,n){let r=!1,i=[],a=0;for(let o of e.pointer){if(o.t>t)break;if(o.kind===`down`)r=!0,a=o.t;else{r=!1;let e=t-o.t;e>=0&&e<=n.motion.rippleMs/1e3&&i.push({at:o.at,age:e})}}let o=r?t-a:0,s=r?Math.min(1,o/(n.motion.pressScaleMs/1e3)):0;return{scale:1-(1-n.motion.pressScale)*s,ripples:i}}function re(e,t,n,r,i){for(let a of t){let t=a.age/(i.motion.rippleMs/1e3),[o,s]=l(a.at,n,r);e.save(),e.globalAlpha=(1-t)*.55,e.strokeStyle=i.theme.accent,e.lineWidth=2.5,e.beginPath(),e.arc(o,s,6+t*i.motion.rippleRadius,0,Math.PI*2),e.stroke(),e.restore()}}function ie(e,t,n,r,i){let a=r*n;e.save(),e.translate(t[0],t[1]),e.scale(a,a),e.shadowColor=i.shadow,e.shadowBlur=6/a,e.shadowOffsetY=2/a,e.beginPath(),e.moveTo(0,0),e.lineTo(0,16.6),e.lineTo(4.1,12.9),e.lineTo(6.9,18.9),e.lineTo(9.6,17.6),e.lineTo(6.9,11.8),e.lineTo(12.1,11.5),e.closePath(),e.fillStyle=i.fill,e.fill(),e.shadowColor=`transparent`,e.strokeStyle=i.stroke,e.lineWidth=1/a,e.stroke(),e.restore()}function O(e,t,n,r,i,a){let o=v(a.theme.presentation);if(o!==`none`)for(let s of t.captions){let t=h(n,s.t0,s.t1,a.motion.captionFadeS);if(t<=0)continue;if(e.save(),e.globalAlpha=t,o===`overlay`){let t=b.captionScrimHeight,n=e.createLinearGradient(0,i.height-t,0,i.height);n.addColorStop(0,`rgba(0, 0, 0, 0)`),n.addColorStop(1,b.captionScrim),e.fillStyle=n,e.fillRect(0,i.height-t,i.width,t)}e.font=b.captionFont,e.fillStyle=b.captionColor,e.textBaseline=`middle`,e.shadowColor=b.captionShadow,e.shadowBlur=8;let c=r.x,l=(r.y+r.h+i.height)/2;if(o===`overlay`){let t=y(a.theme.presentation,i.width);e.textAlign=t.align,c=t.x,l=i.height-b.captionScrimHeight/2+4}e.fillText(s.text,c,l),e.restore()}}function k(e,t,n,r,i,o){let u=g(r,t.fps),d=a(t.camera,u),f=o.theme.presentation,p=_(i,t.viewport,f);f?.backdrop&&te(e,i,f.backdrop),f?.frame&&C(e,p),ne(e,t,n,r,d,p,o),f?.frame&&w(e,p),E(e,t,u,d,p,o);let m=o.theme.cursor??c,{scale:h,ripples:v}=D(t,u,o);m.ripple&&re(e,v,d,p,o),ie(e,l(s(t.cursor,u),d,p),h,o.theme.cursorScale,x[m.style]),O(e,t,u,p,i,o)}var A="versioncam reads scenes from a module's default export: a Record<string, Scene> keyed by scene id, as in `export default { [intro.id]: intro }`.";function j(e){return e==null?String(e):Array.isArray(e)?`an array`:`a ${typeof e}`}function M(e){return typeof e==`string`?JSON.stringify(e):String(e)}function N(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ae(e,t){if(!N(t))return`"${e}" is ${j(t)}, not a scene`;let{draw:n,durationFrames:r,fps:i}=t;return typeof n==`function`?!Number.isInteger(r)||r<1?`"${e}" has durationFrames ${M(r)}, where a whole number of frames, 1 or more, belongs`:typeof i!=`number`||!Number.isFinite(i)||i<=0?`"${e}" has fps ${M(i)}, where a number above 0 belongs`:null:`"${e}" has no draw(ctx, frame, canvas)`}function P(e){if(!N(e))return j(e);if(typeof e.draw==`function`)return`a single scene rather than a record of them`;for(let[t,n]of Object.entries(e)){let e=ae(t,n);if(e)return`a record in which ${e}`}return null}function oe(e,t){if(e.default===void 0){let n=Object.keys(e).filter(e=>e!=="default"),r=[`${t} has no default export.`+(n.length>0?` It exports ${n.join(`, `)}.`:``),A],i=n.filter(t=>P(e[t])===null&&Object.keys(e[t]).length>0);return i.length===1&&r.push(`${i[0]} is one: add \`export default ${i[0]}\`.`),{error:r.join(`
|
|
2
|
+
`)}}let n=P(e.default);return n?{error:`${t} default-exports ${n}.\n${A}`}:{scenes:e.default}}function F(e,t,n){let r=e.map(e=>`"${e}"`).join(`, `),i=`No scene${e.length===1?``:`s`} called ${r}.`;return n===null?`${i} The config names no \`scenes\` module.`:`${i} ${n} defines ${t.length>0?t.join(`, `):`none`}.`}var I=`modulepreload`,L=function(e,t){return new URL(e,t).href},R={},z=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=L(t,n),t=s(t),t in R)return;R[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:I,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}).filter(e=>e!==void 0))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},B=new URLSearchParams(location.search),V=B.has(`recording`),H=B.get(`scene`),U=B.get(`headless`)===`1`;U&&document.body.classList.add(`headless`);var W=document.getElementById(`stage`),G=document.getElementById(`scrub`),K=document.getElementById(`status`),q=document.getElementById(`play`),se=document.getElementById(`error`);function J(e){throw se.textContent=e,Error(e)}async function Y(e,t){let n=await fetch(e);return n.ok||J(`Could not load ${t} (${e}): ${n.status}`),await n.json()}async function ce(e){let t=await fetch(e);return t.ok||J(`Could not load ${e}: ${t.status}`),createImageBitmap(await t.blob())}function X(e){W.width=e.output.width,W.height=e.output.height,U?(W.style.width=`${e.output.width}px`,W.style.height=`${e.output.height}px`):(W.style.width=`min(100%, 1280px)`,W.style.height=`auto`);let t=W.getContext(`2d`,{alpha:!1});return t||J(`No 2D context`),t.imageSmoothingEnabled=!0,t.imageSmoothingQuality=`high`,t}function Z(e,t,n,r){let i=!1;q.addEventListener(`click`,()=>{i=!i,q.textContent=i?`Pause`:`Play`;let a=performance.now(),o=s=>{i&&(s-a>=1e3/e&&(a=s,r((n()+1)%t)),requestAnimationFrame(o))};requestAnimationFrame(o)})}function Q(e,t,n,r){e.durationFrames=t,e.fps=n,e.setFrame=e=>new Promise(t=>{r(e),requestAnimationFrame(()=>requestAnimationFrame(()=>t()))})}async function le(e,t){let n=await Y(`/recording/timeline.json`,`the recording`),r=X(t),i={images:await Promise.all(n.states.map(e=>ce(`/recording/states/${e}`)))};await document.fonts.ready;let a=-1,o=e=>{let o=Math.max(0,Math.min(n.durationFrames-1,e));k(r,n,i,o,W,t),a=o,U||(G.value=String(o),K.textContent=`${o+1} / ${n.durationFrames} · ${(o/n.fps).toFixed(2)}s · state ${n.frames[o]}`)};G.max=String(n.durationFrames-1),G.addEventListener(`input`,()=>o(Number(G.value))),Z(n.fps,n.durationFrames,()=>a,o),o(Number(B.get(`frame`)??0)),Q(e,n.durationFrames,n.fps,o)}async function ue(e,t,n){t.scenes===null&&J(F([n],[],null));let r=oe(await z(()=>import(`/scenes.js`),[],import.meta.url),t.scenes);`error`in r&&J(r.error);let i=r.scenes[n];i||J(F([n],Object.keys(r.scenes),t.scenes));let a=X(t);await document.fonts.ready;let o=-1,s=e=>{let t=Math.max(0,Math.min(i.durationFrames-1,e));i.draw(a,t,W),o=t,U||(G.value=String(t),K.textContent=`${t+1} / ${i.durationFrames} · ${(t/i.fps).toFixed(2)}s`)};G.max=String(i.durationFrames-1),G.addEventListener(`input`,()=>s(Number(G.value))),Z(i.fps,i.durationFrames,()=>o,s),s(Number(B.get(`frame`)??0)),Q(e,i.durationFrames,i.fps,s)}async function de(e){let t=await Y(`/render-config.json`,`the render config`);if(H)return ue(e,t,H);if(V)return le(e,t);J(`Pass ?recording or ?scene=<id>.`)}var $={ready:Promise.resolve(),setFrame:async()=>{},durationFrames:0,fps:60};window.__render=$,$.ready=de($);
|
package/dist/page/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<title>versioncam render</title>
|
|
6
6
|
<style>*{box-sizing:border-box;margin:0;padding:0;transition:none!important;animation:none!important}body{color:#e8e2f5;background:#0b0614;flex-direction:column;align-items:center;min-height:100vh;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;display:flex}body.headless{background:0 0;display:block}#stage{display:block}body.headless #stage{position:fixed;inset:0}#controls{align-items:center;gap:14px;width:min(100%,1280px);padding:14px 18px 28px;font-size:13px;display:flex}body.headless #controls{display:none}#scrub{flex:1}#status{font-variant-numeric:tabular-nums;white-space:nowrap;opacity:.75}button{font:inherit;color:#fff;cursor:pointer;background:#2f6fed;border:0;border-radius:6px;padding:6px 14px}#error{color:#ff9a9a;white-space:pre-wrap;padding:24px;font-family:ui-monospace,Menlo,monospace}</style>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-CnRDAiUz.js"></script>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<canvas id="stage"></canvas>
|
|
@@ -71,10 +71,21 @@ export type PublishedRenderConfig = {
|
|
|
71
71
|
theme: {
|
|
72
72
|
accent: string;
|
|
73
73
|
cursorScale: number;
|
|
74
|
+
/**
|
|
75
|
+
* The config's `theme.cursor`, from 0.6.0, and only when it is not the
|
|
76
|
+
* default; a renderer draws the default without it.
|
|
77
|
+
*/
|
|
78
|
+
cursor?: {
|
|
79
|
+
style: "light" | "dark";
|
|
80
|
+
ripple: boolean;
|
|
81
|
+
};
|
|
74
82
|
/** The config's `theme.presentation`, or null when it is off. */
|
|
75
83
|
presentation: Record<string, unknown> | null;
|
|
76
84
|
};
|
|
77
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* The config's motion, every default filled in, but for the settings
|
|
87
|
+
* added since 0.5.3, which are sent only when they are not the default.
|
|
88
|
+
*/
|
|
78
89
|
motion: Record<string, unknown>;
|
|
79
90
|
clock: {
|
|
80
91
|
timezoneId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/publish/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,uDAAuD;AACvD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAG,yBAAyB,CAAC;AAErD;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,gBAAgB,EAAE,GAAG;IACrB,UAAU,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC3B,aAAa,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC9B,YAAY,EAAE,GAAG,GAAG,IAAI;IACxB,YAAY,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC7B,gBAAgB,EAAE,EAAE;CACZ,CAAC;
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/publish/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,uDAAuD;AACvD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAG,yBAAyB,CAAC;AAErD;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,gBAAgB,EAAE,GAAG;IACrB,UAAU,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC3B,aAAa,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC9B,YAAY,EAAE,GAAG,GAAG,IAAI;IACxB,YAAY,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC7B,gBAAgB,EAAE,EAAE;CACZ,CAAC;AA4JX,2CAA2C;AAC3C,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,KAAK,EAAE,mBAAmB;IAC1B,KAAK,EAAE,CAAC,EAAU,EAAE,IAAY,EAAE,EAAE,CAAC,eAAe,EAAE,UAAU,IAAI,EAAE;IACtE,QAAQ,EAAE,CAAC,EAAU,EAAE,IAAY,EAAE,EAAE,CACrC,eAAe,EAAE,aAAa,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAC1D,OAAO,EAAE,CAAC,EAAU,EAAE,IAAY,EAAE,EAAE,CACpC,eAAe,EAAE,YAAY,kBAAkB,CAAC,IAAI,CAAC,EAAE;IACzD,OAAO,EAAE,CAAC,EAAU,EAAE,IAAY,EAAE,EAAE,CACpC,eAAe,EAAE,YAAY,kBAAkB,CAAC,IAAI,CAAC,EAAE;IACzD,MAAM,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,eAAe,EAAE,SAAS;CAC1C,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAEjC;IACC,MAAM,EACJ,UAAU,EAAE,WAAW,EACvB,SAAS,EAAE,UAAU,EACrB,GAAG,IAAI,EACR,GAAG,QAAQ,CAAC,IAAI,CAAC;IAClB,OAAO,EAAE,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC/B,CAAC"}
|
package/dist/render/render.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Browser, Page } from "playwright";
|
|
2
|
-
import type { ResolvedConfig } from "../config.js";
|
|
2
|
+
import type { RenderConfig, ResolvedConfig } from "../config.js";
|
|
3
3
|
import type { Timeline } from "../core/timeline.js";
|
|
4
4
|
import type { EncodeResult } from "./encode.js";
|
|
5
5
|
export type RenderTarget = {
|
|
@@ -10,6 +10,8 @@ export type RenderTarget = {
|
|
|
10
10
|
kind: "scene";
|
|
11
11
|
id: string;
|
|
12
12
|
};
|
|
13
|
+
/** `renderConfigWith` for a target, for a page that is not opened here. */
|
|
14
|
+
export declare function renderConfigFor(config: ResolvedConfig, target: RenderTarget): Promise<RenderConfig>;
|
|
13
15
|
/**
|
|
14
16
|
* Open the render page at the exact output size, at DPR 1.
|
|
15
17
|
*
|
|
@@ -17,11 +19,15 @@ export type RenderTarget = {
|
|
|
17
19
|
* device scale factor of 2 would make the screenshot twice as wide and then
|
|
18
20
|
* need downscaling — a second resample on top of the one `drawImage` already
|
|
19
21
|
* did.
|
|
22
|
+
*
|
|
23
|
+
* The recording's timeline is read here, once, for the size, and handed back
|
|
24
|
+
* for the sheet, which needs its cuts.
|
|
20
25
|
*/
|
|
21
26
|
export declare function openRenderPage(browser: Browser, config: ResolvedConfig, target: RenderTarget): Promise<{
|
|
22
27
|
page: Page;
|
|
23
28
|
durationFrames: number;
|
|
24
29
|
fps: number;
|
|
30
|
+
timeline: Timeline | null;
|
|
25
31
|
}>;
|
|
26
32
|
export type RenderOutcome = EncodeResult & {
|
|
27
33
|
id: string;
|
package/dist/render/render.js
CHANGED
|
@@ -2,11 +2,41 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { launchBrowser } from "../driver/launch.js";
|
|
5
|
-
import { renderConfigOf } from "../config.js";
|
|
5
|
+
import { outputSize, renderConfigOf } from "../config.js";
|
|
6
6
|
import { bundleScenes } from "../loader.js";
|
|
7
7
|
import { contactSheet as encodeContactSheet, encodeFromPage, } from "./encode.js";
|
|
8
8
|
import { evenFrames } from "./sampling.js";
|
|
9
9
|
import { distPageDir, installRenderRoutes, RENDER_ORIGIN } from "./serve.js";
|
|
10
|
+
/** A recording's timeline, read from its directory, wherever that is. */
|
|
11
|
+
async function timelineAt(dir) {
|
|
12
|
+
return JSON.parse(await readFile(join(dir, "timeline.json"), "utf8"));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The render config a target is drawn with: the config's, at the target's
|
|
16
|
+
* own size.
|
|
17
|
+
*
|
|
18
|
+
* A recording renders at the shape it was recorded at, which is the clip's
|
|
19
|
+
* own viewport when it sets one. `config.output` is the config's viewport
|
|
20
|
+
* at `output.width`, and a phone clip recorded at 390x844 drawn on that
|
|
21
|
+
* canvas was the top of its page blown up to the width. A scene has no
|
|
22
|
+
* recording and draws at the config's size.
|
|
23
|
+
*
|
|
24
|
+
* `renderConfigOf` itself is left as it is: it is also what `versioncam
|
|
25
|
+
* publish` sends, and version.cam sizes each render from the recording's
|
|
26
|
+
* timeline by the same rule, so a publish keeps its bytes and its hash.
|
|
27
|
+
*/
|
|
28
|
+
function renderConfigWith(config, timeline) {
|
|
29
|
+
return {
|
|
30
|
+
...renderConfigOf(config),
|
|
31
|
+
output: timeline
|
|
32
|
+
? outputSize(timeline.viewport, config.output.width)
|
|
33
|
+
: config.output,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** `renderConfigWith` for a target, for a page that is not opened here. */
|
|
37
|
+
export async function renderConfigFor(config, target) {
|
|
38
|
+
return renderConfigWith(config, target.kind === "recording" ? await timelineAt(target.dir) : null);
|
|
39
|
+
}
|
|
10
40
|
/**
|
|
11
41
|
* Open the render page at the exact output size, at DPR 1.
|
|
12
42
|
*
|
|
@@ -14,10 +44,15 @@ import { distPageDir, installRenderRoutes, RENDER_ORIGIN } from "./serve.js";
|
|
|
14
44
|
* device scale factor of 2 would make the screenshot twice as wide and then
|
|
15
45
|
* need downscaling — a second resample on top of the one `drawImage` already
|
|
16
46
|
* did.
|
|
47
|
+
*
|
|
48
|
+
* The recording's timeline is read here, once, for the size, and handed back
|
|
49
|
+
* for the sheet, which needs its cuts.
|
|
17
50
|
*/
|
|
18
51
|
export async function openRenderPage(browser, config, target) {
|
|
52
|
+
const timeline = target.kind === "recording" ? await timelineAt(target.dir) : null;
|
|
53
|
+
const renderConfig = renderConfigWith(config, timeline);
|
|
19
54
|
const context = await browser.newContext({
|
|
20
|
-
viewport:
|
|
55
|
+
viewport: renderConfig.output,
|
|
21
56
|
deviceScaleFactor: 1,
|
|
22
57
|
reducedMotion: "reduce",
|
|
23
58
|
timezoneId: config.clock.timezoneId,
|
|
@@ -32,7 +67,7 @@ export async function openRenderPage(browser, config, target) {
|
|
|
32
67
|
});
|
|
33
68
|
await installRenderRoutes(page, {
|
|
34
69
|
distDir: distPageDir(),
|
|
35
|
-
renderConfig
|
|
70
|
+
renderConfig,
|
|
36
71
|
recordingDir: target.kind === "recording" ? target.dir : undefined,
|
|
37
72
|
scenesBundle: (await bundleScenes(config)) ?? undefined,
|
|
38
73
|
});
|
|
@@ -60,25 +95,25 @@ export async function openRenderPage(browser, config, target) {
|
|
|
60
95
|
throw new Error(`The render page reported no frames for ${target.kind} "${target.id}".` +
|
|
61
96
|
(problems.length ? `\n${problems.join("\n")}` : ""));
|
|
62
97
|
}
|
|
63
|
-
return { page, ...info };
|
|
98
|
+
return { page, ...info, timeline };
|
|
64
99
|
}
|
|
65
100
|
/**
|
|
66
101
|
* The frames a sheet shows when nobody chose them.
|
|
67
102
|
*
|
|
68
103
|
* For a recording, evenly spread and moved past the renderer's dissolves,
|
|
69
|
-
* which only the recording's own cuts can say where they are
|
|
70
|
-
* directory rather than by id, since a target
|
|
71
|
-
*
|
|
72
|
-
*
|
|
104
|
+
* which only the recording's own cuts can say where they are: the timeline
|
|
105
|
+
* `openRenderPage` read from its directory rather than by id, since a target
|
|
106
|
+
* can be a recording anywhere. A scene, which has none, draws every frame
|
|
107
|
+
* itself and never dissolves, so its sheet is the plain even spread the
|
|
108
|
+
* encoder falls back on.
|
|
73
109
|
*/
|
|
74
|
-
|
|
75
|
-
if (
|
|
110
|
+
function sheetFrames(config, timeline, durationFrames) {
|
|
111
|
+
if (!timeline)
|
|
76
112
|
return undefined;
|
|
77
|
-
const timeline = JSON.parse(await readFile(join(target.dir, "timeline.json"), "utf8"));
|
|
78
113
|
return evenFrames(durationFrames, undefined, config.motion.crossfadeFrames, timeline.cuts);
|
|
79
114
|
}
|
|
80
115
|
async function renderTarget(browser, config, target) {
|
|
81
|
-
const { page, durationFrames, fps } = await openRenderPage(browser, config, target);
|
|
116
|
+
const { page, durationFrames, fps, timeline } = await openRenderPage(browser, config, target);
|
|
82
117
|
try {
|
|
83
118
|
const encoded = await encodeFromPage(page, {
|
|
84
119
|
id: target.id,
|
|
@@ -90,7 +125,7 @@ async function renderTarget(browser, config, target) {
|
|
|
90
125
|
const sheet = await encodeContactSheet(page, {
|
|
91
126
|
id: target.id,
|
|
92
127
|
durationFrames,
|
|
93
|
-
frames:
|
|
128
|
+
frames: sheetFrames(config, timeline, durationFrames),
|
|
94
129
|
outDir: config.outputDir,
|
|
95
130
|
});
|
|
96
131
|
return { ...encoded, id: target.id, durationFrames, fps, sheet };
|
|
@@ -110,12 +145,12 @@ export async function renderScene(browser, config, id) {
|
|
|
110
145
|
return renderTarget(browser, config, { kind: "scene", id });
|
|
111
146
|
}
|
|
112
147
|
export async function contactSheet(browser, config, target, options = {}) {
|
|
113
|
-
const { page, durationFrames } = await openRenderPage(browser, config, target);
|
|
148
|
+
const { page, durationFrames, timeline } = await openRenderPage(browser, config, target);
|
|
114
149
|
try {
|
|
115
150
|
return await encodeContactSheet(page, {
|
|
116
151
|
id: target.id,
|
|
117
152
|
durationFrames,
|
|
118
|
-
frames: options.frames ??
|
|
153
|
+
frames: options.frames ?? sheetFrames(config, timeline, durationFrames),
|
|
119
154
|
outDir: config.outputDir,
|
|
120
155
|
});
|
|
121
156
|
}
|
|
@@ -147,8 +182,7 @@ export async function renderFrame(browser, config, target, frame) {
|
|
|
147
182
|
}
|
|
148
183
|
/** The timeline of a recording on disk, for reporting without a browser. */
|
|
149
184
|
export async function readTimeline(config, id) {
|
|
150
|
-
|
|
151
|
-
return JSON.parse(await readFile(file, "utf8"));
|
|
185
|
+
return timelineAt(join(config.recordingsDir, id));
|
|
152
186
|
}
|
|
153
187
|
export async function withBrowser(fn) {
|
|
154
188
|
const browser = await launchBrowser();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
* A rendered clip. `frameRate` is the fraction ffprobe gives, `60/1` or
|
|
3
3
|
* `30000/1001`, kept as written rather than divided out: it is the rate the
|
|
4
4
|
* filter may convert every clip to, the fastest clip included, and only the
|
|
5
|
-
* exact rate leaves that clip's frames where they were.
|
|
5
|
+
* exact rate leaves that clip's frames where they were. `width` and
|
|
6
|
+
* `height` are the video's, which `xfade` needs to be the same for all.
|
|
6
7
|
*/
|
|
7
8
|
export type SequenceInput = {
|
|
8
9
|
id: string;
|
|
9
10
|
path: string;
|
|
10
11
|
duration: number;
|
|
11
12
|
frameRate: string;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
12
15
|
};
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* What a sequence needs to know of a video file, from one ffprobe call:
|
|
18
|
+
* its duration in seconds, its frame rate as ffprobe gives it, and its
|
|
19
|
+
* size. Blocking; ffprobe is quick.
|
|
20
|
+
*/
|
|
21
|
+
export declare function probeVideo(path: string): Pick<SequenceInput, "duration" | "frameRate" | "width" | "height">;
|
|
17
22
|
/**
|
|
18
23
|
* The `xfade` chain. Each transition's `offset` is measured from the start of
|
|
19
24
|
* everything accumulated so far, and every crossfade shortens the total by its
|
package/dist/render/sequence.js
CHANGED
|
@@ -16,47 +16,66 @@ import { ffmpegPath, ffprobeSync } from "./ffmpeg.js";
|
|
|
16
16
|
* card per clip adds a sixth of the running time for nothing.
|
|
17
17
|
*/
|
|
18
18
|
const TRANSITION_S = 0.4;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* What a sequence needs to know of a video file, from one ffprobe call:
|
|
21
|
+
* its duration in seconds, its frame rate as ffprobe gives it, and its
|
|
22
|
+
* size. Blocking; ffprobe is quick.
|
|
23
|
+
*/
|
|
24
|
+
export function probeVideo(path) {
|
|
21
25
|
const out = ffprobeSync([
|
|
22
26
|
"-v",
|
|
23
27
|
"error",
|
|
28
|
+
"-select_streams",
|
|
29
|
+
"v:0",
|
|
24
30
|
"-show_entries",
|
|
25
|
-
"format=duration",
|
|
31
|
+
"format=duration:stream=r_frame_rate,width,height",
|
|
26
32
|
"-of",
|
|
27
|
-
"
|
|
33
|
+
"json",
|
|
28
34
|
path,
|
|
29
35
|
]);
|
|
30
|
-
const
|
|
31
|
-
|
|
36
|
+
const probed = JSON.parse(out);
|
|
37
|
+
const duration = Number.parseFloat(probed.format?.duration ?? "");
|
|
38
|
+
if (!Number.isFinite(duration)) {
|
|
32
39
|
throw new Error(`Could not read a duration from ${path}`);
|
|
33
40
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export function probeFrameRate(path) {
|
|
38
|
-
const out = ffprobeSync([
|
|
39
|
-
"-v",
|
|
40
|
-
"error",
|
|
41
|
-
"-select_streams",
|
|
42
|
-
"v:0",
|
|
43
|
-
"-show_entries",
|
|
44
|
-
"stream=r_frame_rate",
|
|
45
|
-
"-of",
|
|
46
|
-
"default=nw=1:nk=1",
|
|
47
|
-
path,
|
|
48
|
-
]).trim();
|
|
49
|
-
const rate = rateOf(out);
|
|
41
|
+
const stream = probed.streams?.[0];
|
|
42
|
+
const frameRate = stream?.r_frame_rate ?? "";
|
|
43
|
+
const rate = rateOf(frameRate);
|
|
50
44
|
if (!Number.isFinite(rate) || rate <= 0) {
|
|
51
45
|
throw new Error(`Could not read a frame rate from ${path}`);
|
|
52
46
|
}
|
|
53
|
-
|
|
47
|
+
const { width, height } = stream ?? {};
|
|
48
|
+
if (!width || !height) {
|
|
49
|
+
throw new Error(`Could not read a size from ${path}`);
|
|
50
|
+
}
|
|
51
|
+
return { duration, frameRate, width, height };
|
|
54
52
|
}
|
|
55
53
|
/** A fraction as ffprobe prints one, `30000/1001`, as a number. */
|
|
56
54
|
function rateOf(fraction) {
|
|
57
55
|
const [num, den = "1"] = fraction.split("/");
|
|
58
56
|
return Number(num) / Number(den);
|
|
59
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* `xfade` joins clips of one size only, and when they are not, the error is
|
|
60
|
+
* ffmpeg's: a screen of filter graph that names no clip. Since a clip can
|
|
61
|
+
* have a viewport of its own, a config's `sequence` can name clips of two
|
|
62
|
+
* shapes, a phone clip among desktop ones most likely, so the sequence
|
|
63
|
+
* stops before the graph is built, and names two clips that differ and
|
|
64
|
+
* what to do.
|
|
65
|
+
*/
|
|
66
|
+
function refuseTwoSizes(inputs) {
|
|
67
|
+
const [first] = inputs;
|
|
68
|
+
const other = inputs.find((input) => input.width !== first.width || input.height !== first.height);
|
|
69
|
+
if (!other)
|
|
70
|
+
return;
|
|
71
|
+
const size = (input) => `${input.width}x${input.height}`;
|
|
72
|
+
const portrait = [first, other].some((input) => input.height > input.width);
|
|
73
|
+
throw new Error(`A sequence stitches clips of one size: ${first.id} is ${size(first)} ` +
|
|
74
|
+
`and ${other.id} is ${size(other)}. ` +
|
|
75
|
+
(portrait
|
|
76
|
+
? 'Leave the phone clips out of "sequence".'
|
|
77
|
+
: 'Give them one viewport, or leave one out of "sequence".'));
|
|
78
|
+
}
|
|
60
79
|
/**
|
|
61
80
|
* The `xfade` chain. Each transition's `offset` is measured from the start of
|
|
62
81
|
* everything accumulated so far, and every crossfade shortens the total by its
|
|
@@ -73,6 +92,7 @@ function rateOf(fraction) {
|
|
|
73
92
|
* is the one it always was.
|
|
74
93
|
*/
|
|
75
94
|
export function buildFilter(inputs) {
|
|
95
|
+
refuseTwoSizes(inputs);
|
|
76
96
|
if (inputs.length === 1)
|
|
77
97
|
return { filter: "", label: "0:v", total: inputs[0].duration };
|
|
78
98
|
const parts = [];
|
|
@@ -103,12 +123,7 @@ export async function buildSequence(ids, outDir, options = {}) {
|
|
|
103
123
|
missing.push(id);
|
|
104
124
|
continue;
|
|
105
125
|
}
|
|
106
|
-
inputs.push({
|
|
107
|
-
id,
|
|
108
|
-
path,
|
|
109
|
-
duration: probeDuration(path),
|
|
110
|
-
frameRate: probeFrameRate(path),
|
|
111
|
-
});
|
|
126
|
+
inputs.push({ id, path, ...probeVideo(path) });
|
|
112
127
|
}
|
|
113
128
|
if (inputs.length < 2) {
|
|
114
129
|
throw new Error(`Need at least two rendered clips to build a sequence; have ${inputs.length}.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sequence.js","sourceRoot":"","sources":["../../src/render/sequence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;;;;;GAWG;AAEH,MAAM,YAAY,GAAG,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"sequence.js","sourceRoot":"","sources":["../../src/render/sequence.ts"],"names":[],"mappings":"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"}
|
package/dsl.md
CHANGED
|
@@ -129,6 +129,7 @@ already is. The press lasts 70 to 100 milliseconds, seeded.
|
|
|
129
129
|
| Option | Default | What it sets |
|
|
130
130
|
|---|---|---|
|
|
131
131
|
| `dwell` | `80` to `150`, seeded | How long to rest on the target before pressing. |
|
|
132
|
+
| `modifiers` | none | Keys held through the press: `["Shift"]` for a shift-click, `["Meta"]`, `["Alt"]`, `["Control"]`. |
|
|
132
133
|
|
|
133
134
|
### `drag(target, from, to, options?)`
|
|
134
135
|
|
|
@@ -141,10 +142,15 @@ the drag survives a layout change: `{ x: 0.18, y: 0.5 }` to
|
|
|
141
142
|
Mind the edges. A chart's axis labels live inside its own box, and a drag
|
|
142
143
|
that starts at x 0.08 selects that text instead of the control.
|
|
143
144
|
|
|
145
|
+
`drag(from, to, options?)` is the other form: from one thing to another,
|
|
146
|
+
each a locator or a point, such as a file onto a dropzone or a card onto a
|
|
147
|
+
column. It is told apart by its second argument, which is a locator or a
|
|
148
|
+
point there, and a fraction `{ x, y }` in the first form.
|
|
149
|
+
|
|
144
150
|
| Option | Default | What it sets |
|
|
145
151
|
|---|---|---|
|
|
146
152
|
| `duration` | `1400` | How long the stroke takes, from press to release. |
|
|
147
|
-
| `modifiers` | none | Keys held through the stroke: `["Shift"]`, `["Alt"]`,
|
|
153
|
+
| `modifiers` | none | Keys held through the stroke: `["Shift"]`, `["Alt"]`, `["Meta"]`, `["Control"]`. |
|
|
148
154
|
|
|
149
155
|
### `lasso(target, planner, options?)`
|
|
150
156
|
|
|
@@ -163,6 +169,67 @@ selection from a frame after the loop closes.
|
|
|
163
169
|
| `duration` | `1400` | How long the stroke takes, from press to release. |
|
|
164
170
|
| `modifiers` | none | Keys held through the stroke: `["Shift"]`, `["Alt"]`, or both. |
|
|
165
171
|
|
|
172
|
+
### `scroll(target, where, options?)`
|
|
173
|
+
|
|
174
|
+
Scroll a pane, or the page, on the clip's clock: one eased step a frame, so
|
|
175
|
+
the frames inside the scroll are the same on every run. `target` is the
|
|
176
|
+
element that scrolls, as a locator, or `"page"` for the document. `where` is
|
|
177
|
+
one of three:
|
|
178
|
+
|
|
179
|
+
- `{ by: px }` from where it is now: `{ by: 400 }` goes down, `{ by: -400 }`
|
|
180
|
+
back up.
|
|
181
|
+
- `{ to: "top" | "bottom" | px }` to one end, or to an offset.
|
|
182
|
+
- `{ into: locator, block? }` until that element sits at the `"start"`,
|
|
183
|
+
`"center"` or `"end"` of the pane's visible box. The default is
|
|
184
|
+
`"center"`.
|
|
185
|
+
|
|
186
|
+
The end is clamped to what the element can scroll, and the recording keeps
|
|
187
|
+
the end it reached. An element with nothing to scroll is refused: it is
|
|
188
|
+
nearly always a wrapper around the one that scrolls, which `inspect` shows.
|
|
189
|
+
|
|
190
|
+
The cursor stays where it is. For a scroll that reads as a mouse wheel, move
|
|
191
|
+
the cursor over the pane first. Never scroll with `offCamera` and
|
|
192
|
+
`scrollIntoView` or `mouse.wheel`: the browser animates those on its own
|
|
193
|
+
clock, and the frames inside them differ between runs. While recording, a
|
|
194
|
+
page's own smooth scrolling, from its stylesheet or from its script, is
|
|
195
|
+
instant.
|
|
196
|
+
|
|
197
|
+
| Option | Default | What it sets |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `duration` | `700` | How long the scroll takes. Two frames at the least. `motion.scrollDurationMs` sets the default. |
|
|
200
|
+
| `axis` | `"y"` | `"x"` scrolls sideways. `"top"` is then the left end and `"bottom"` the right. |
|
|
201
|
+
|
|
202
|
+
### `fileWindow(files, options?)`
|
|
203
|
+
|
|
204
|
+
Draw a file window over the page, listing `files`, to drag onto the app's
|
|
205
|
+
dropzone: the browser's own file dialog is outside the page, and nothing can
|
|
206
|
+
film it. It looks like a Finder window. Returns `{ item(i), close() }`:
|
|
207
|
+
`item(i)` is row `i` as a locator, for `click`, `hover` and `drag`.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const files = await s.fileWindow(
|
|
211
|
+
[{ name: "cases.csv", content: csv }, { name: "sites.csv", content: sites }],
|
|
212
|
+
{ title: "Case data" },
|
|
213
|
+
);
|
|
214
|
+
await s.click(files.item(0));
|
|
215
|
+
await s.click(files.item(1), { modifiers: ["Shift"] });
|
|
216
|
+
await s.drag(files.item(0), s.byTestId("dropzone"), { duration: 1000 });
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
A click selects one row, a click with Shift extends to it, and one with
|
|
220
|
+
Meta adds or removes it. A press on a selected row drags the whole
|
|
221
|
+
selection, and letting go over the page sends the element under the pointer
|
|
222
|
+
the drag events a real drag sends, carrying the files as `File` objects, so
|
|
223
|
+
the app takes them through its own dropzone. The window then closes. A file
|
|
224
|
+
is `{ name, content?, type?, size? }`; the type and the Size and Kind columns
|
|
225
|
+
follow from the name and the content unless given. The window is staged:
|
|
226
|
+
say so wherever the clip is shown.
|
|
227
|
+
|
|
228
|
+
| Option | Default | What it sets |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| `title` | `"Files"` | The title bar. |
|
|
231
|
+
| `at` | `{ x: 0.52, y: 0.42 }` | The window's top left corner, as fractions of the viewport. |
|
|
232
|
+
|
|
166
233
|
## Keyboard
|
|
167
234
|
|
|
168
235
|
### `typeInto(target, text)`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "versioncam",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Record a short, true demo clip of a web app from one sentence: a skill that sets the repository up on its first run, writes the clip, and has a reviewer pass it from a contact sheet.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Peter Bulovec"
|
|
@@ -131,6 +131,15 @@ export default clip(
|
|
|
131
131
|
`seed` fixes the motion, so two runs of the clip produce the same frames. Pick
|
|
132
132
|
one and leave it alone.
|
|
133
133
|
|
|
134
|
+
A phone clip is a second clip file, `<id>-mobile.clip.ts`, with
|
|
135
|
+
`viewport: { width: 390, height: 844 }` in its options beside the title and
|
|
136
|
+
the seed. It uses the same test ids as `<id>`, and its own steps where the
|
|
137
|
+
phone layout differs: a menu behind a button, a list under the form rather
|
|
138
|
+
than beside it. A draft's contact sheet shows which. It is not listed in the
|
|
139
|
+
config's `sequence`, which stitches clips of one size. On version.cam,
|
|
140
|
+
`<id>-mobile` is shown in place of `<id>` on a narrow screen, so the name
|
|
141
|
+
is the whole of joining them.
|
|
142
|
+
|
|
134
143
|
## Write the beat sheet before you record
|
|
135
144
|
|
|
136
145
|
`.versioncam/author/<id>/beats.json`, an array of `{ label, expect }`:
|
|
@@ -193,9 +202,14 @@ happened again the first time an agent used it.
|
|
|
193
202
|
|---|---|
|
|
194
203
|
| locator not found at step N | `inspect` at that moment; pick a control that exists; if none does, the beat is wrong |
|
|
195
204
|
| beat produced no new state | missing `settle`, wrong target, or effect off-screen; check cuts and states |
|
|
196
|
-
| settle timed out on `X` | add a `ready` hook, raise the timeout, or the source is live
|
|
205
|
+
| settle timed out on `X` | add a `ready` hook, raise the timeout, or the source is live: name it in `replay.record` so its answer is recorded once and served after |
|
|
206
|
+
| two runs differ where the app fetched something (tiles, a count, an answer) | `replay.record` the request; `replay.pace` it too if the page reads it over time |
|
|
207
|
+
| the clip fails with "its replay does not hold" | the app fetches something new; `versioncam record <id> --replay record` and look at the diff of `replay/<id>.har` |
|
|
197
208
|
| document reloaded mid-clip | something navigated; find the trigger and avoid it |
|
|
198
209
|
| drag or lasso selected nothing | `measure`, re-author geometry as fractions of the real box |
|
|
210
|
+
| the frames inside a scroll differ between runs, or the page scrolls on its own | `s.scroll(pane, { by })`, `{ to }` or `{ into }`; never `scrollIntoView` or `mouse.wheel` from `offCamera`, which the browser animates on its own clock |
|
|
211
|
+
| the clip has to show files being uploaded | `s.fileWindow([...])`, click the rows (Shift to extend), then `s.drag(files.item(0), dropzone)`; never `setInputFiles` from `offCamera`, which shows the files arriving from nowhere. Say in the summary that the window is staged |
|
|
212
|
+
| `scroll` says the element does not scroll | it is a wrapper; `inspect` at that moment and scroll the child with `overflow: auto` |
|
|
199
213
|
| duration drift | adjust holds, never the beats |
|
|
200
214
|
| tooltip / menu open in a frame it should not be | move the cursor start; add a hold before the next action |
|
|
201
215
|
| the caption covers something a beat points at | `theme.presentation.captionInset: { right: 40 }` in the config moves it to the bottom-right corner; it moves every clip's caption, so say so in what you write down |
|