@midscene/visualizer 1.5.2 → 1.5.3-beta-20260305031416.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.
Files changed (43) hide show
  1. package/dist/es/component/blackboard/index.css +82 -4
  2. package/dist/es/component/blackboard/index.mjs +73 -301
  3. package/dist/es/component/player/index.css +144 -119
  4. package/dist/es/component/player/index.mjs +468 -830
  5. package/dist/es/component/player/remotion/StepScene.mjs +190 -0
  6. package/dist/es/component/player/remotion/derive-frame-state.mjs +207 -0
  7. package/dist/es/component/player/remotion/export-branded-video.mjs +210 -0
  8. package/dist/es/component/player/remotion/frame-calculator.mjs +149 -0
  9. package/dist/es/component/player/use-frame-player.mjs +88 -0
  10. package/dist/es/component/universal-playground/index.mjs +14 -1
  11. package/dist/es/hooks/usePlaygroundExecution.mjs +11 -7
  12. package/dist/es/index.mjs +2 -2
  13. package/dist/es/store/store.mjs +9 -0
  14. package/dist/es/utils/replay-scripts.mjs +78 -59
  15. package/dist/lib/component/blackboard/index.css +82 -4
  16. package/dist/lib/component/blackboard/index.js +73 -307
  17. package/dist/lib/component/player/index.css +144 -119
  18. package/dist/lib/component/player/index.js +466 -828
  19. package/dist/lib/component/player/remotion/StepScene.js +224 -0
  20. package/dist/lib/component/player/remotion/derive-frame-state.js +241 -0
  21. package/dist/lib/component/player/remotion/export-branded-video.js +244 -0
  22. package/dist/lib/component/player/remotion/frame-calculator.js +186 -0
  23. package/dist/lib/component/player/use-frame-player.js +122 -0
  24. package/dist/lib/component/universal-playground/index.js +14 -1
  25. package/dist/lib/hooks/usePlaygroundExecution.js +11 -7
  26. package/dist/lib/index.js +3 -0
  27. package/dist/lib/store/store.js +9 -0
  28. package/dist/lib/utils/replay-scripts.js +80 -58
  29. package/dist/types/component/blackboard/index.d.ts +0 -4
  30. package/dist/types/component/player/index.d.ts +0 -1
  31. package/dist/types/component/player/remotion/StepScene.d.ts +9 -0
  32. package/dist/types/component/player/remotion/derive-frame-state.d.ts +38 -0
  33. package/dist/types/component/player/remotion/export-branded-video.d.ts +2 -0
  34. package/dist/types/component/player/remotion/frame-calculator.d.ts +35 -0
  35. package/dist/types/component/player/use-frame-player.d.ts +17 -0
  36. package/dist/types/hooks/usePlaygroundExecution.d.ts +15 -1
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/store/store.d.ts +2 -0
  39. package/dist/types/utils/replay-scripts.d.ts +16 -1
  40. package/package.json +5 -8
  41. package/dist/es/utils/pixi-loader.mjs +0 -42
  42. package/dist/lib/utils/pixi-loader.js +0 -82
  43. package/dist/types/utils/pixi-loader.d.ts +0 -5
@@ -0,0 +1,190 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { mouseLoading } from "../../../utils/index.mjs";
4
+ import { deriveFrameState } from "./derive-frame-state.mjs";
5
+ const POINTER_PHASE = 0.375;
6
+ const CROSSFADE_FRAMES = 10;
7
+ const StepsTimeline = ({ frameMap, autoZoom, frame, width: compWidth, height: compHeight, fps })=>{
8
+ const { scriptFrames, imageWidth: baseImgW, imageHeight: baseImgH } = frameMap;
9
+ const state = useMemo(()=>deriveFrameState(scriptFrames, frame, baseImgW, baseImgH, fps), [
10
+ scriptFrames,
11
+ frame,
12
+ baseImgW,
13
+ baseImgH,
14
+ fps
15
+ ]);
16
+ if (!state.img) return null;
17
+ const { img, imageWidth: imgW, imageHeight: imgH, prevImg, camera, prevCamera, insights, spinning: spinningPointer, spinningElapsedMs, currentPointerImg, title, subTitle, frameInScript, imageChanged, pointerMoved, rawProgress } = state;
18
+ const pT = pointerMoved ? Math.min(rawProgress / POINTER_PHASE, 1) : rawProgress;
19
+ const cT = pointerMoved ? rawProgress <= POINTER_PHASE ? 0 : Math.min((rawProgress - POINTER_PHASE) / (1 - POINTER_PHASE), 1) : rawProgress;
20
+ const pointerLeft = prevCamera.pointerLeft + (camera.pointerLeft - prevCamera.pointerLeft) * pT;
21
+ const pointerTop = prevCamera.pointerTop + (camera.pointerTop - prevCamera.pointerTop) * pT;
22
+ const cameraLeft = autoZoom ? prevCamera.left + (camera.left - prevCamera.left) * cT : 0;
23
+ const cameraTop = autoZoom ? prevCamera.top + (camera.top - prevCamera.top) * cT : 0;
24
+ const cameraWidth = autoZoom ? prevCamera.width + (camera.width - prevCamera.width) * cT : imgW;
25
+ const isPortraitImage = imgH > imgW;
26
+ const browserW = isPortraitImage ? Math.round(imgW / imgH * compHeight) : compWidth;
27
+ const portraitLeft = Math.round((compWidth - browserW) / 2);
28
+ const zoom = imgW / cameraWidth;
29
+ const tx = browserW / imgW * -cameraLeft;
30
+ const ty = compHeight / imgH * -cameraTop;
31
+ const transformStyle = `scale(${zoom}) translate(${tx}px, ${ty}px)`;
32
+ const camH = imgH / imgW * cameraWidth;
33
+ const ptrX = (pointerLeft - cameraLeft) / cameraWidth * browserW;
34
+ const ptrY = (pointerTop - cameraTop) / camH * compHeight;
35
+ const showCursor = camera.pointerLeft !== Math.round(imgW / 2) || camera.pointerTop !== Math.round(imgH / 2) || prevCamera.pointerLeft !== Math.round(imgW / 2) || prevCamera.pointerTop !== Math.round(imgH / 2);
36
+ const crossfadeAlpha = imageChanged ? Math.min(frameInScript / CROSSFADE_FRAMES, 1) : 1;
37
+ const spinRotation = spinningPointer ? (Math.sin(spinningElapsedMs / 500 - Math.PI / 2) + 1) / 2 * Math.PI * 2 : 0;
38
+ const renderInsightOverlays = ()=>{
39
+ if (0 === insights.length) return null;
40
+ return insights.map((insight, idx)=>{
41
+ const overlays = [];
42
+ if (insight.highlightElement) {
43
+ const rect = insight.highlightElement.rect;
44
+ overlays.push(/*#__PURE__*/ jsx("div", {
45
+ style: {
46
+ position: 'absolute',
47
+ left: rect.left,
48
+ top: rect.top,
49
+ width: rect.width,
50
+ height: rect.height,
51
+ background: 'rgba(253, 89, 7, 0.4)',
52
+ border: '1px solid #fd5907',
53
+ boxShadow: '4px 4px 2px rgba(51, 51, 51, 0.4)',
54
+ opacity: insight.alpha,
55
+ pointerEvents: 'none'
56
+ }
57
+ }, `highlight-${idx}`));
58
+ }
59
+ if (insight.searchArea) {
60
+ const rect = insight.searchArea;
61
+ overlays.push(/*#__PURE__*/ jsx("div", {
62
+ style: {
63
+ position: 'absolute',
64
+ left: rect.left,
65
+ top: rect.top,
66
+ width: rect.width,
67
+ height: rect.height,
68
+ background: 'rgba(2, 131, 145, 0.4)',
69
+ border: '1px solid #028391',
70
+ boxShadow: '4px 4px 2px rgba(51, 51, 51, 0.4)',
71
+ opacity: insight.alpha,
72
+ pointerEvents: 'none'
73
+ }
74
+ }, `search-${idx}`));
75
+ }
76
+ return overlays;
77
+ });
78
+ };
79
+ const renderContentArea = (w, h)=>/*#__PURE__*/ jsxs("div", {
80
+ style: {
81
+ width: w,
82
+ height: h,
83
+ position: 'relative',
84
+ overflow: 'hidden'
85
+ },
86
+ children: [
87
+ imageChanged && prevImg && crossfadeAlpha < 1 && /*#__PURE__*/ jsx("div", {
88
+ style: {
89
+ position: 'absolute',
90
+ width: w,
91
+ height: h,
92
+ overflow: 'hidden',
93
+ opacity: 1 - crossfadeAlpha
94
+ },
95
+ children: /*#__PURE__*/ jsx("img", {
96
+ alt: "",
97
+ src: prevImg,
98
+ style: {
99
+ width: w,
100
+ height: h,
101
+ transformOrigin: '0 0',
102
+ transform: transformStyle
103
+ }
104
+ })
105
+ }),
106
+ /*#__PURE__*/ jsxs("div", {
107
+ style: {
108
+ position: 'absolute',
109
+ width: w,
110
+ height: h,
111
+ overflow: 'hidden',
112
+ opacity: imageChanged ? crossfadeAlpha : 1
113
+ },
114
+ children: [
115
+ /*#__PURE__*/ jsx("img", {
116
+ alt: "",
117
+ src: img,
118
+ style: {
119
+ width: w,
120
+ height: h,
121
+ transformOrigin: '0 0',
122
+ transform: transformStyle
123
+ }
124
+ }),
125
+ /*#__PURE__*/ jsx("div", {
126
+ style: {
127
+ position: 'absolute',
128
+ top: 0,
129
+ left: 0,
130
+ width: w,
131
+ height: h,
132
+ transformOrigin: '0 0',
133
+ transform: transformStyle,
134
+ pointerEvents: 'none'
135
+ },
136
+ children: renderInsightOverlays()
137
+ })
138
+ ]
139
+ }),
140
+ spinningPointer && /*#__PURE__*/ jsx("img", {
141
+ alt: "",
142
+ src: mouseLoading,
143
+ style: {
144
+ position: 'absolute',
145
+ left: ptrX - 11,
146
+ top: ptrY - 14,
147
+ width: 22,
148
+ height: 28,
149
+ transform: `rotate(${spinRotation}rad)`,
150
+ transformOrigin: 'center center',
151
+ filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))'
152
+ }
153
+ }),
154
+ showCursor && !spinningPointer && /*#__PURE__*/ jsx("img", {
155
+ alt: "",
156
+ src: currentPointerImg,
157
+ style: {
158
+ position: 'absolute',
159
+ left: ptrX - 3,
160
+ top: ptrY - 2,
161
+ width: 22,
162
+ height: 28,
163
+ filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))'
164
+ }
165
+ })
166
+ ]
167
+ });
168
+ return /*#__PURE__*/ jsx("div", {
169
+ style: {
170
+ position: 'absolute',
171
+ top: 0,
172
+ left: 0,
173
+ right: 0,
174
+ bottom: 0,
175
+ backgroundColor: '#000'
176
+ },
177
+ children: isPortraitImage ? /*#__PURE__*/ jsx("div", {
178
+ style: {
179
+ position: 'absolute',
180
+ left: portraitLeft,
181
+ top: 0,
182
+ width: browserW,
183
+ height: compHeight,
184
+ overflow: 'hidden'
185
+ },
186
+ children: renderContentArea(browserW, compHeight)
187
+ }) : renderContentArea(compWidth, compHeight)
188
+ });
189
+ };
190
+ export { StepsTimeline };
@@ -0,0 +1,207 @@
1
+ import { mousePointer } from "../../../utils/index.mjs";
2
+ function _define_property(obj, key, value) {
3
+ if (key in obj) Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ else obj[key] = value;
10
+ return obj;
11
+ }
12
+ function _object_spread(target) {
13
+ for(var i = 1; i < arguments.length; i++){
14
+ var source = null != arguments[i] ? arguments[i] : {};
15
+ var ownKeys = Object.keys(source);
16
+ if ("function" == typeof Object.getOwnPropertySymbols) ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
17
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
18
+ }));
19
+ ownKeys.forEach(function(key) {
20
+ _define_property(target, key, source[key]);
21
+ });
22
+ }
23
+ return target;
24
+ }
25
+ function derive_frame_state_ownKeys(object, enumerableOnly) {
26
+ var keys = Object.keys(object);
27
+ if (Object.getOwnPropertySymbols) {
28
+ var symbols = Object.getOwnPropertySymbols(object);
29
+ if (enumerableOnly) symbols = symbols.filter(function(sym) {
30
+ return Object.getOwnPropertyDescriptor(object, sym).enumerable;
31
+ });
32
+ keys.push.apply(keys, symbols);
33
+ }
34
+ return keys;
35
+ }
36
+ function _object_spread_props(target, source) {
37
+ source = null != source ? source : {};
38
+ if (Object.getOwnPropertyDescriptors) Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
39
+ else derive_frame_state_ownKeys(Object(source)).forEach(function(key) {
40
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
41
+ });
42
+ return target;
43
+ }
44
+ function updateImage(acc, sf, baseW, baseH) {
45
+ if (!sf.img) return;
46
+ if (acc.img && sf.img !== acc.img) {
47
+ acc.prevImg = acc.img;
48
+ acc.imageChanged = true;
49
+ }
50
+ acc.img = sf.img;
51
+ acc.imgW = sf.imageWidth || baseW;
52
+ acc.imgH = sf.imageHeight || baseH;
53
+ }
54
+ function checkPointerMoved(prev, cur) {
55
+ return Math.abs(prev.pointerLeft - cur.pointerLeft) > 1 || Math.abs(prev.pointerTop - cur.pointerTop) > 1;
56
+ }
57
+ function handleImg(acc, sf, frame, baseW, baseH) {
58
+ updateImage(acc, sf, baseW, baseH);
59
+ const sfEnd = sf.startFrame + sf.durationInFrames;
60
+ if (sf.cameraTarget) {
61
+ acc.prevCamera = _object_spread({}, acc.camera);
62
+ acc.camera = _object_spread({}, sf.cameraTarget);
63
+ acc.pointerMoved = checkPointerMoved(acc.prevCamera, acc.camera);
64
+ } else if (frame >= sfEnd) {
65
+ acc.pointerMoved = false;
66
+ acc.imageChanged = false;
67
+ }
68
+ acc.spinning = false;
69
+ }
70
+ function handleInsight(acc, sf, frame, baseW, baseH) {
71
+ updateImage(acc, sf, baseW, baseH);
72
+ const alreadyAdded = acc.insights.some((ai)=>ai.highlightElement === sf.highlightElement && ai.searchArea === sf.searchArea);
73
+ if (!alreadyAdded) acc.insights.push({
74
+ highlightElement: sf.highlightElement,
75
+ searchArea: sf.searchArea,
76
+ alpha: 1
77
+ });
78
+ if (sf.cameraTarget && void 0 !== sf.insightPhaseFrames) {
79
+ const cameraStartFrame = sf.startFrame + sf.insightPhaseFrames;
80
+ if (frame >= cameraStartFrame) {
81
+ acc.prevCamera = _object_spread({}, acc.camera);
82
+ acc.camera = _object_spread({}, sf.cameraTarget);
83
+ const cameraFrameIn = frame - cameraStartFrame;
84
+ const cameraDur = sf.cameraPhaseFrames || 1;
85
+ acc.rawProgress = Math.min(cameraFrameIn / cameraDur, 1);
86
+ acc.pointerMoved = checkPointerMoved(acc.prevCamera, acc.camera);
87
+ }
88
+ }
89
+ acc.spinning = false;
90
+ }
91
+ function handleClearInsight(acc, sf, frame) {
92
+ const sfEnd = sf.startFrame + sf.durationInFrames;
93
+ const alpha = 1 - acc.rawProgress;
94
+ acc.insights = acc.insights.map((ai)=>_object_spread_props(_object_spread({}, ai), {
95
+ alpha
96
+ }));
97
+ if (frame >= sfEnd) acc.insights = [];
98
+ acc.spinning = false;
99
+ }
100
+ function handleSpinningPointer(acc, fps) {
101
+ acc.spinning = true;
102
+ acc.spinningElapsedMs = acc.frameInScript / fps * 1000;
103
+ }
104
+ function deriveFrameState(scriptFrames, frame, baseW, baseH, fps) {
105
+ const defaultCamera = {
106
+ left: 0,
107
+ top: 0,
108
+ width: baseW,
109
+ pointerLeft: Math.round(baseW / 2),
110
+ pointerTop: Math.round(baseH / 2)
111
+ };
112
+ const acc = {
113
+ img: '',
114
+ imgW: baseW,
115
+ imgH: baseH,
116
+ camera: _object_spread({}, defaultCamera),
117
+ prevCamera: _object_spread({}, defaultCamera),
118
+ prevImg: null,
119
+ insights: [],
120
+ spinning: false,
121
+ spinningElapsedMs: 0,
122
+ pointerImg: mousePointer,
123
+ title: '',
124
+ subTitle: '',
125
+ taskId: void 0,
126
+ frameInScript: 0,
127
+ scriptIndex: 0,
128
+ imageChanged: false,
129
+ pointerMoved: false,
130
+ rawProgress: 0
131
+ };
132
+ for(let i = 0; i < scriptFrames.length; i++){
133
+ const sf = scriptFrames[i];
134
+ const sfEnd = sf.startFrame + sf.durationInFrames;
135
+ if (0 === sf.durationInFrames) {
136
+ if (sf.startFrame <= frame) {
137
+ if ('pointer' === sf.type && sf.pointerImg) acc.pointerImg = sf.pointerImg;
138
+ acc.title = sf.title || acc.title;
139
+ acc.subTitle = sf.subTitle || acc.subTitle;
140
+ var _sf_taskId;
141
+ acc.taskId = null != (_sf_taskId = sf.taskId) ? _sf_taskId : acc.taskId;
142
+ acc.scriptIndex = i;
143
+ }
144
+ continue;
145
+ }
146
+ if (frame < sf.startFrame) break;
147
+ acc.title = sf.title || acc.title;
148
+ acc.subTitle = sf.subTitle || acc.subTitle;
149
+ var _sf_taskId1;
150
+ acc.taskId = null != (_sf_taskId1 = sf.taskId) ? _sf_taskId1 : acc.taskId;
151
+ acc.scriptIndex = i;
152
+ acc.frameInScript = frame - sf.startFrame;
153
+ acc.rawProgress = Math.min(acc.frameInScript / sf.durationInFrames, 1);
154
+ switch(sf.type){
155
+ case 'img':
156
+ handleImg(acc, sf, frame, baseW, baseH);
157
+ break;
158
+ case 'insight':
159
+ handleInsight(acc, sf, frame, baseW, baseH);
160
+ break;
161
+ case 'clear-insight':
162
+ handleClearInsight(acc, sf, frame);
163
+ break;
164
+ case 'spinning-pointer':
165
+ handleSpinningPointer(acc, fps);
166
+ break;
167
+ case 'sleep':
168
+ acc.spinning = false;
169
+ break;
170
+ }
171
+ if (frame >= sfEnd) {
172
+ if ('clear-insight' !== sf.type) acc.imageChanged = false;
173
+ acc.pointerMoved = false;
174
+ acc.rawProgress = 1;
175
+ if (sf.cameraTarget) acc.prevCamera = _object_spread({}, acc.camera);
176
+ }
177
+ }
178
+ if (!acc.img) {
179
+ const firstImgScript = scriptFrames.find((sf)=>'img' === sf.type && sf.img);
180
+ if (firstImgScript) {
181
+ acc.img = firstImgScript.img;
182
+ acc.imgW = firstImgScript.imageWidth || baseW;
183
+ acc.imgH = firstImgScript.imageHeight || baseH;
184
+ }
185
+ }
186
+ return {
187
+ img: acc.img,
188
+ imageWidth: acc.imgW,
189
+ imageHeight: acc.imgH,
190
+ prevImg: acc.imageChanged ? acc.prevImg : null,
191
+ camera: acc.camera,
192
+ prevCamera: acc.prevCamera,
193
+ insights: acc.insights,
194
+ spinning: acc.spinning,
195
+ spinningElapsedMs: acc.spinningElapsedMs,
196
+ currentPointerImg: acc.pointerImg,
197
+ title: acc.title,
198
+ subTitle: acc.subTitle,
199
+ taskId: acc.taskId,
200
+ frameInScript: acc.frameInScript,
201
+ scriptIndex: acc.scriptIndex,
202
+ imageChanged: acc.imageChanged,
203
+ pointerMoved: acc.pointerMoved,
204
+ rawProgress: acc.rawProgress
205
+ };
206
+ }
207
+ export { deriveFrameState };
@@ -0,0 +1,210 @@
1
+ import { mouseLoading, mousePointer } from "../../../utils/index.mjs";
2
+ import { deriveFrameState } from "./derive-frame-state.mjs";
3
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
4
+ try {
5
+ var info = gen[key](arg);
6
+ var value = info.value;
7
+ } catch (error) {
8
+ reject(error);
9
+ return;
10
+ }
11
+ if (info.done) resolve(value);
12
+ else Promise.resolve(value).then(_next, _throw);
13
+ }
14
+ function _async_to_generator(fn) {
15
+ return function() {
16
+ var self = this, args = arguments;
17
+ return new Promise(function(resolve, reject) {
18
+ var gen = fn.apply(self, args);
19
+ function _next(value) {
20
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
21
+ }
22
+ function _throw(err) {
23
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
24
+ }
25
+ _next(void 0);
26
+ });
27
+ };
28
+ }
29
+ const W = 960;
30
+ const H = 540;
31
+ const POINTER_PHASE = 0.375;
32
+ const CROSSFADE_FRAMES = 10;
33
+ function clamp(v, lo, hi) {
34
+ return Math.min(Math.max(v, lo), hi);
35
+ }
36
+ function lerp(a, b, t) {
37
+ return a + (b - a) * t;
38
+ }
39
+ function loadImage(src) {
40
+ return new Promise((resolve, reject)=>{
41
+ const img = new Image();
42
+ img.crossOrigin = 'anonymous';
43
+ img.onload = ()=>resolve(img);
44
+ img.onerror = reject;
45
+ img.src = src;
46
+ });
47
+ }
48
+ function drawInsightOverlays(ctx, insights, cameraTransform, bx, contentY) {
49
+ for (const insight of insights)if (!(insight.alpha <= 0)) {
50
+ ctx.save();
51
+ ctx.globalAlpha *= insight.alpha;
52
+ if (insight.highlightElement) {
53
+ const r = insight.highlightElement.rect;
54
+ const rx = bx + (r.left * cameraTransform.zoom + cameraTransform.tx * cameraTransform.zoom);
55
+ const ry = contentY + (r.top * cameraTransform.zoom + cameraTransform.ty * cameraTransform.zoom);
56
+ const rw = r.width * cameraTransform.zoom;
57
+ const rh = r.height * cameraTransform.zoom;
58
+ ctx.fillStyle = 'rgba(253, 89, 7, 0.4)';
59
+ ctx.fillRect(rx, ry, rw, rh);
60
+ ctx.strokeStyle = '#fd5907';
61
+ ctx.lineWidth = 1;
62
+ ctx.strokeRect(rx, ry, rw, rh);
63
+ ctx.shadowColor = 'rgba(51, 51, 51, 0.4)';
64
+ ctx.shadowBlur = 2;
65
+ ctx.shadowOffsetX = 4;
66
+ ctx.shadowOffsetY = 4;
67
+ ctx.strokeRect(rx, ry, rw, rh);
68
+ ctx.shadowBlur = 0;
69
+ ctx.shadowOffsetX = 0;
70
+ ctx.shadowOffsetY = 0;
71
+ }
72
+ if (insight.searchArea) {
73
+ const r = insight.searchArea;
74
+ const rx = bx + (r.left * cameraTransform.zoom + cameraTransform.tx * cameraTransform.zoom);
75
+ const ry = contentY + (r.top * cameraTransform.zoom + cameraTransform.ty * cameraTransform.zoom);
76
+ const rw = r.width * cameraTransform.zoom;
77
+ const rh = r.height * cameraTransform.zoom;
78
+ ctx.fillStyle = 'rgba(2, 131, 145, 0.4)';
79
+ ctx.fillRect(rx, ry, rw, rh);
80
+ ctx.strokeStyle = '#028391';
81
+ ctx.lineWidth = 1;
82
+ ctx.strokeRect(rx, ry, rw, rh);
83
+ }
84
+ ctx.restore();
85
+ }
86
+ }
87
+ function drawSpinningPointer(ctx, img, x, y, elapsedMs) {
88
+ const progress = (Math.sin(elapsedMs / 500 - Math.PI / 2) + 1) / 2;
89
+ const rotation = progress * Math.PI * 2;
90
+ ctx.save();
91
+ ctx.translate(x, y);
92
+ ctx.rotate(rotation);
93
+ ctx.drawImage(img, -11, -14, 22, 28);
94
+ ctx.restore();
95
+ }
96
+ function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg) {
97
+ const { scriptFrames, imageWidth: baseW, imageHeight: baseH, fps } = frameMap;
98
+ const st = deriveFrameState(scriptFrames, stepsFrame, baseW, baseH, fps);
99
+ if (!st.img) return;
100
+ const { img, prevImg, imageWidth: imgW, imageHeight: imgH, camera, prevCamera, pointerMoved, imageChanged, rawProgress, frameInScript: fInScript, spinning, spinningElapsedMs, insights } = st;
101
+ const pT = pointerMoved ? Math.min(rawProgress / POINTER_PHASE, 1) : rawProgress;
102
+ const cT = pointerMoved ? rawProgress <= POINTER_PHASE ? 0 : Math.min((rawProgress - POINTER_PHASE) / (1 - POINTER_PHASE), 1) : rawProgress;
103
+ const camL = lerp(prevCamera.left, camera.left, cT);
104
+ const camT2 = lerp(prevCamera.top, camera.top, cT);
105
+ const camW = lerp(prevCamera.width, camera.width, cT);
106
+ const ptrX = lerp(prevCamera.pointerLeft, camera.pointerLeft, pT);
107
+ const ptrY = lerp(prevCamera.pointerTop, camera.pointerTop, pT);
108
+ const zoom = imgW / camW;
109
+ const tx = W / imgW * -camL;
110
+ const ty = H / imgH * -camT2;
111
+ const crossAlpha = imageChanged ? clamp(fInScript / CROSSFADE_FRAMES, 0, 1) : 1;
112
+ ctx.fillStyle = '#000';
113
+ ctx.fillRect(0, 0, W, H);
114
+ const drawImg = (src, alpha)=>{
115
+ const imgEl = imgCache.get(src);
116
+ if (!imgEl || alpha <= 0) return;
117
+ ctx.save();
118
+ ctx.globalAlpha = alpha;
119
+ ctx.beginPath();
120
+ ctx.rect(0, 0, W, H);
121
+ ctx.clip();
122
+ ctx.translate(tx * zoom, ty * zoom);
123
+ ctx.scale(zoom, zoom);
124
+ ctx.drawImage(imgEl, 0, 0, W, H);
125
+ ctx.restore();
126
+ };
127
+ if (imageChanged && prevImg && crossAlpha < 1) drawImg(prevImg, 1 - crossAlpha);
128
+ drawImg(img, imageChanged ? crossAlpha : 1);
129
+ if (insights.length > 0) drawInsightOverlays(ctx, insights, {
130
+ zoom,
131
+ tx,
132
+ ty
133
+ }, 0, 0);
134
+ const camH = imgH / imgW * camW;
135
+ const sX = (ptrX - camL) / camW * W;
136
+ const sY = (ptrY - camT2) / camH * H;
137
+ const hasPtrData = Math.abs(camera.pointerLeft - Math.round(baseW / 2)) > 1 || Math.abs(camera.pointerTop - Math.round(baseH / 2)) > 1 || Math.abs(prevCamera.pointerLeft - Math.round(baseW / 2)) > 1 || Math.abs(prevCamera.pointerTop - Math.round(baseH / 2)) > 1;
138
+ if (spinning && spinnerImg) drawSpinningPointer(ctx, spinnerImg, sX, sY, spinningElapsedMs);
139
+ if (!spinning && hasPtrData && cursorImg) ctx.drawImage(cursorImg, sX - 3, sY - 2, 22, 28);
140
+ }
141
+ function exportBrandedVideo(frameMap, onProgress) {
142
+ return _async_to_generator(function*() {
143
+ const { totalDurationInFrames: total, fps } = frameMap;
144
+ const imgSrcs = new Set();
145
+ for (const sf of frameMap.scriptFrames)if (sf.img) imgSrcs.add(sf.img);
146
+ const imgCache = new Map();
147
+ yield Promise.all([
148
+ ...imgSrcs
149
+ ].map((src)=>_async_to_generator(function*() {
150
+ try {
151
+ imgCache.set(src, (yield loadImage(src)));
152
+ } catch (e) {}
153
+ })()));
154
+ let cursorImg = null;
155
+ let spinnerImg = null;
156
+ try {
157
+ cursorImg = yield loadImage(mousePointer);
158
+ } catch (e) {}
159
+ try {
160
+ spinnerImg = yield loadImage(mouseLoading);
161
+ } catch (e) {}
162
+ const canvas = document.createElement('canvas');
163
+ canvas.width = W;
164
+ canvas.height = H;
165
+ const ctx = canvas.getContext('2d');
166
+ const stream = canvas.captureStream(fps);
167
+ const recorder = new MediaRecorder(stream, {
168
+ mimeType: 'video/webm'
169
+ });
170
+ const chunks = [];
171
+ recorder.ondataavailable = (e)=>{
172
+ if (e.data.size > 0) chunks.push(e.data);
173
+ };
174
+ return new Promise((resolve, reject)=>{
175
+ recorder.onerror = ()=>reject(new Error('MediaRecorder error'));
176
+ recorder.onstop = ()=>{
177
+ if (0 === chunks.length) return void reject(new Error('No video data'));
178
+ const blob = new Blob(chunks, {
179
+ type: 'video/webm'
180
+ });
181
+ const url = URL.createObjectURL(blob);
182
+ const a = document.createElement('a');
183
+ a.href = url;
184
+ a.download = 'midscene_replay.webm';
185
+ a.click();
186
+ stream.getTracks().forEach((track)=>track.stop());
187
+ setTimeout(()=>URL.revokeObjectURL(url), 1000);
188
+ resolve();
189
+ };
190
+ recorder.start();
191
+ const frameDuration = 1000 / fps;
192
+ const startTime = performance.now();
193
+ let lastFrame = -1;
194
+ const tick = ()=>{
195
+ const elapsed = performance.now() - startTime;
196
+ const targetFrame = Math.min(Math.floor(elapsed / frameDuration), total - 1);
197
+ if (targetFrame > lastFrame) {
198
+ lastFrame = targetFrame;
199
+ ctx.clearRect(0, 0, W, H);
200
+ drawSteps(ctx, targetFrame, frameMap, imgCache, cursorImg, spinnerImg);
201
+ null == onProgress || onProgress((targetFrame + 1) / total);
202
+ }
203
+ if (targetFrame < total - 1) requestAnimationFrame(tick);
204
+ else setTimeout(()=>recorder.stop(), 2 * frameDuration);
205
+ };
206
+ requestAnimationFrame(tick);
207
+ });
208
+ })();
209
+ }
210
+ export { exportBrandedVideo };