@midscene/visualizer 1.7.7-beta-20260429033400.0 → 1.7.7

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.
@@ -198,9 +198,11 @@
198
198
 
199
199
  .player-container .player-custom-controls {
200
200
  flex-direction: row;
201
+ justify-content: center;
201
202
  align-items: center;
202
- gap: 8px;
203
- margin-right: 8px;
203
+ gap: 2px;
204
+ min-width: 58px;
205
+ margin-left: 4px;
204
206
  display: flex;
205
207
  }
206
208
 
@@ -208,6 +210,23 @@
208
210
  color: #fff;
209
211
  }
210
212
 
213
+ .player-container.player-container-empty {
214
+ justify-content: center;
215
+ align-items: center;
216
+ }
217
+
218
+ .player-container.player-container-empty .player-empty-state {
219
+ color: #6b7280;
220
+ flex-direction: column;
221
+ align-items: center;
222
+ gap: 12px;
223
+ display: flex;
224
+ }
225
+
226
+ .player-container.player-container-empty .player-empty-state .player-empty-text {
227
+ font-size: 14px;
228
+ }
229
+
211
230
  .chapter-tooltip .ant-tooltip-inner {
212
231
  -webkit-backdrop-filter: blur(8px);
213
232
  backdrop-filter: blur(8px);
@@ -250,6 +269,12 @@
250
269
  margin: 4px 0;
251
270
  }
252
271
 
272
+ .player-export-label {
273
+ font-variant-numeric: tabular-nums;
274
+ font-feature-settings: "tnum";
275
+ font-size: 14px;
276
+ }
277
+
253
278
  .player-speed-option:hover, .player-settings-item:hover {
254
279
  background: rgba(0, 0, 0, .04);
255
280
  }
@@ -3,10 +3,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import "./index.css";
5
5
  import { CaretRightOutlined, CompressOutlined, DownloadOutlined, ExpandOutlined, ExportOutlined, FontSizeOutlined, PauseOutlined, ThunderboltOutlined } from "@ant-design/icons";
6
- import { Dropdown, Spin, Switch, Tooltip, message } from "antd";
6
+ import { Button, Dropdown, Progress, Switch, Tooltip, message } from "antd";
7
7
  import global_perspective from "../../icons/global-perspective.mjs";
8
8
  import player_setting from "../../icons/player-setting.mjs";
9
9
  import { useGlobalPreference } from "../../store/store.mjs";
10
+ import { shouldRestartPlaybackFromBeginning } from "./playback-controls.mjs";
10
11
  import { triggerReportDownload } from "./report-download.mjs";
11
12
  import { StepsTimeline } from "./scenes/StepScene.mjs";
12
13
  import { exportBrandedVideo } from "./scenes/export-branded-video.mjs";
@@ -111,20 +112,38 @@ function Player(props) {
111
112
  loop: false,
112
113
  playbackRate: playbackSpeed
113
114
  });
115
+ const effectiveEndFrame = useMemo(()=>{
116
+ if (!frameMap) return 0;
117
+ for(let i = frameMap.scriptFrames.length - 1; i >= 0; i--){
118
+ const sf = frameMap.scriptFrames[i];
119
+ if (sf.taskId) return sf.startFrame + sf.durationInFrames - 1;
120
+ }
121
+ return Math.max(0, frameMap.totalDurationInFrames - 1);
122
+ }, [
123
+ frameMap
124
+ ]);
125
+ const handlePlaybackToggle = useCallback(()=>{
126
+ if (player.playing) return void player.pause();
127
+ if (shouldRestartPlaybackFromBeginning(player.currentFrame, effectiveEndFrame)) player.seekTo(0);
128
+ player.play();
129
+ }, [
130
+ effectiveEndFrame,
131
+ player.currentFrame,
132
+ player.pause,
133
+ player.play,
134
+ player.playing,
135
+ player.seekTo
136
+ ]);
114
137
  useEffect(()=>{
115
138
  if (!frameMap || player.playing) return;
116
- const { scriptFrames, totalDurationInFrames } = frameMap;
117
- if (player.currentFrame < totalDurationInFrames - 1) return;
118
- for(let i = scriptFrames.length - 1; i >= 0; i--){
119
- const sf = scriptFrames[i];
120
- if (sf.taskId) {
121
- player.seekTo(sf.startFrame + sf.durationInFrames - 1);
122
- break;
123
- }
124
- }
139
+ if (player.currentFrame < frameMap.totalDurationInFrames - 1) return;
140
+ if (effectiveEndFrame > 0) player.seekTo(effectiveEndFrame);
125
141
  }, [
142
+ effectiveEndFrame,
126
143
  frameMap,
127
- player.playing
144
+ player.currentFrame,
145
+ player.playing,
146
+ player.seekTo
128
147
  ]);
129
148
  useEffect(()=>{
130
149
  if (!frameMap || !(null == props ? void 0 : props.onTaskChange)) return;
@@ -178,21 +197,40 @@ function Player(props) {
178
197
  }, [
179
198
  currentFrameState
180
199
  ]);
200
+ const [isExporting, setIsExporting] = useState(false);
201
+ const [exportProgress, setExportProgress] = useState(0);
202
+ const exportInFlightRef = useRef(false);
181
203
  const [controlsVisible, setControlsVisible] = useState(true);
182
204
  const hideTimerRef = useRef(null);
183
205
  const showControls = useCallback(()=>{
184
206
  setControlsVisible(true);
185
207
  if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
208
+ if (isExporting) return;
186
209
  hideTimerRef.current = setTimeout(()=>setControlsVisible(false), 3000);
187
- }, []);
210
+ }, [
211
+ isExporting
212
+ ]);
188
213
  const onMouseEnter = useCallback(()=>{
189
214
  setControlsVisible(true);
190
215
  if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
191
216
  }, []);
192
217
  const onMouseLeave = useCallback(()=>{
193
218
  if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
219
+ if (isExporting) return;
194
220
  hideTimerRef.current = setTimeout(()=>setControlsVisible(false), 1000);
195
- }, []);
221
+ }, [
222
+ isExporting
223
+ ]);
224
+ useEffect(()=>{
225
+ if (!isExporting) return;
226
+ setControlsVisible(true);
227
+ if (hideTimerRef.current) {
228
+ clearTimeout(hideTimerRef.current);
229
+ hideTimerRef.current = null;
230
+ }
231
+ }, [
232
+ isExporting
233
+ ]);
196
234
  useEffect(()=>{
197
235
  const handleKeyDown = (e)=>{
198
236
  var _e_target;
@@ -200,13 +238,13 @@ function Player(props) {
200
238
  if ('INPUT' === tag || 'TEXTAREA' === tag || 'SELECT' === tag) return;
201
239
  if ('Space' === e.code) {
202
240
  e.preventDefault();
203
- player.toggle();
241
+ handlePlaybackToggle();
204
242
  }
205
243
  };
206
244
  document.addEventListener('keydown', handleKeyDown);
207
245
  return ()=>document.removeEventListener('keydown', handleKeyDown);
208
246
  }, [
209
- player
247
+ handlePlaybackToggle
210
248
  ]);
211
249
  const seekBarRef = useRef(null);
212
250
  const handleSeekPointerDown = useCallback((e)=>{
@@ -216,7 +254,7 @@ function Player(props) {
216
254
  const seek = (clientX)=>{
217
255
  const rect = bar.getBoundingClientRect();
218
256
  const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
219
- player.seekTo(Math.round(ratio * (frameMap.totalDurationInFrames - 1)));
257
+ player.seekTo(Math.round(ratio * effectiveEndFrame));
220
258
  };
221
259
  seek(e.clientX);
222
260
  const onMove = (ev)=>seek(ev.clientX);
@@ -228,7 +266,8 @@ function Player(props) {
228
266
  bar.addEventListener('pointerup', onUp);
229
267
  }, [
230
268
  frameMap,
231
- player
269
+ player,
270
+ effectiveEndFrame
232
271
  ]);
233
272
  const [isFullscreen, setIsFullscreen] = useState(false);
234
273
  const toggleFullscreen = useCallback(()=>{
@@ -242,10 +281,9 @@ function Player(props) {
242
281
  document.addEventListener('fullscreenchange', handler);
243
282
  return ()=>document.removeEventListener('fullscreenchange', handler);
244
283
  }, []);
245
- const [isExporting, setIsExporting] = useState(false);
246
- const [exportProgress, setExportProgress] = useState(0);
247
284
  const handleExportVideo = useCallback(()=>_async_to_generator(function*() {
248
- if (!frameMap || isExporting) return;
285
+ if (!frameMap || exportInFlightRef.current) return;
286
+ exportInFlightRef.current = true;
249
287
  setIsExporting(true);
250
288
  setExportProgress(0);
251
289
  try {
@@ -255,25 +293,24 @@ function Player(props) {
255
293
  message.success('Video exported');
256
294
  } catch (e) {
257
295
  console.error('Export failed:', e);
258
- message.error('Export failed');
296
+ const errorMessage = e instanceof Error ? e.message : 'Export failed';
297
+ message.error(errorMessage);
259
298
  } finally{
299
+ exportInFlightRef.current = false;
260
300
  setIsExporting(false);
261
301
  setExportProgress(0);
262
302
  }
263
303
  })(), [
264
304
  autoZoom,
265
- frameMap,
266
- isExporting
305
+ frameMap
267
306
  ]);
268
307
  const chapterMarkers = useMemo(()=>{
269
- if (!frameMap) return [];
270
- const { scriptFrames, totalDurationInFrames } = frameMap;
271
- if (0 === totalDurationInFrames) return [];
308
+ if (!frameMap || effectiveEndFrame <= 0) return [];
272
309
  const markers = [];
273
- for (const sf of scriptFrames){
310
+ for (const sf of frameMap.scriptFrames){
274
311
  if ('img' !== sf.type && 'insight' !== sf.type || 0 === sf.durationInFrames) continue;
275
312
  const globalFrame = sf.startFrame;
276
- const percent = globalFrame / totalDurationInFrames * 100;
313
+ const percent = globalFrame / effectiveEndFrame * 100;
277
314
  if (percent > 1 && percent < 99) {
278
315
  const parts = [
279
316
  sf.title,
@@ -288,16 +325,38 @@ function Player(props) {
288
325
  }
289
326
  return markers;
290
327
  }, [
291
- frameMap
328
+ frameMap,
329
+ effectiveEndFrame
292
330
  ]);
293
- if (!scripts || 0 === scripts.length || !frameMap) return /*#__PURE__*/ jsx("div", {
294
- className: "player-container"
295
- });
331
+ var _props_reportFileContent;
332
+ const reportFileContent = null != (_props_reportFileContent = null == props ? void 0 : props.reportFileContent) ? _props_reportFileContent : null;
333
+ const canDownloadReport = (null == props ? void 0 : props.canDownloadReport) !== false;
334
+ if (!scripts || 0 === scripts.length || !frameMap) {
335
+ if (reportFileContent && canDownloadReport) return /*#__PURE__*/ jsx("div", {
336
+ className: "player-container player-container-empty",
337
+ children: /*#__PURE__*/ jsxs("div", {
338
+ className: "player-empty-state",
339
+ children: [
340
+ /*#__PURE__*/ jsx("span", {
341
+ className: "player-empty-text",
342
+ children: "No replay available"
343
+ }),
344
+ /*#__PURE__*/ jsx(Button, {
345
+ icon: /*#__PURE__*/ jsx(DownloadOutlined, {}),
346
+ onClick: ()=>{
347
+ handleDownloadReport();
348
+ },
349
+ children: "Download report"
350
+ })
351
+ ]
352
+ })
353
+ });
354
+ return null;
355
+ }
296
356
  const compositionWidth = (null == currentFrameState ? void 0 : currentFrameState.imageWidth) || frameMap.imageWidth;
297
357
  const compositionHeight = (null == currentFrameState ? void 0 : currentFrameState.imageHeight) || frameMap.imageHeight;
298
358
  const isPortraitCanvas = compositionHeight > compositionWidth;
299
- const totalFrames = frameMap.totalDurationInFrames;
300
- const seekPercent = totalFrames > 1 ? player.currentFrame / (totalFrames - 1) * 100 : 0;
359
+ const seekPercent = effectiveEndFrame > 0 ? Math.min(100, player.currentFrame / effectiveEndFrame * 100) : 0;
301
360
  return /*#__PURE__*/ jsx("div", {
302
361
  className: "player-container",
303
362
  "data-fit-mode": null == props ? void 0 : props.fitMode,
@@ -324,7 +383,7 @@ function Player(props) {
324
383
  height: '100%',
325
384
  overflow: 'hidden'
326
385
  },
327
- onClick: player.toggle,
386
+ onClick: handlePlaybackToggle,
328
387
  children: (()=>{
329
388
  const scale = containerSize.width > 0 && containerSize.height > 0 ? Math.min(containerSize.width / compositionWidth, containerSize.height / compositionHeight) : 1;
330
389
  return /*#__PURE__*/ jsx("div", {
@@ -374,16 +433,16 @@ function Player(props) {
374
433
  children: [
375
434
  /*#__PURE__*/ jsx("div", {
376
435
  className: "status-icon",
377
- onClick: player.toggle,
436
+ onClick: handlePlaybackToggle,
378
437
  children: player.playing ? /*#__PURE__*/ jsx(PauseOutlined, {}) : /*#__PURE__*/ jsx(CaretRightOutlined, {})
379
438
  }),
380
439
  /*#__PURE__*/ jsxs("span", {
381
440
  className: "time-display",
382
441
  children: [
383
- formatTime(player.currentFrame, frameMap.fps),
384
- " /",
442
+ formatTime(Math.min(player.currentFrame, effectiveEndFrame), frameMap.fps),
385
443
  ' ',
386
- formatTime(totalFrames, frameMap.fps)
444
+ "/ ",
445
+ formatTime(effectiveEndFrame + 1, frameMap.fps)
387
446
  ]
388
447
  }),
389
448
  /*#__PURE__*/ jsxs("div", {
@@ -422,7 +481,7 @@ function Player(props) {
422
481
  /*#__PURE__*/ jsxs("div", {
423
482
  className: "player-custom-controls",
424
483
  children: [
425
- (null == props ? void 0 : props.reportFileContent) && (null == props ? void 0 : props.canDownloadReport) !== false ? /*#__PURE__*/ jsx(Tooltip, {
484
+ reportFileContent && canDownloadReport ? /*#__PURE__*/ jsx(Tooltip, {
426
485
  title: "Download Report",
427
486
  children: /*#__PURE__*/ jsx("div", {
428
487
  className: "status-icon",
@@ -458,18 +517,32 @@ function Player(props) {
458
517
  },
459
518
  onClick: isExporting ? void 0 : handleExportVideo,
460
519
  children: [
461
- isExporting ? /*#__PURE__*/ jsx(Spin, {
462
- size: "small"
463
- }) : /*#__PURE__*/ jsx(ExportOutlined, {
464
- style: {
465
- width: '16px',
466
- height: '16px'
467
- }
468
- }),
469
520
  /*#__PURE__*/ jsx("span", {
470
521
  style: {
471
- fontSize: '14px'
522
+ width: 16,
523
+ height: 16,
524
+ display: 'inline-flex',
525
+ alignItems: 'center',
526
+ justifyContent: 'center',
527
+ flexShrink: 0
472
528
  },
529
+ children: isExporting ? /*#__PURE__*/ jsx(Progress, {
530
+ type: "circle",
531
+ percent: exportProgress,
532
+ size: 16,
533
+ strokeWidth: 14,
534
+ showInfo: false,
535
+ strokeColor: "#1677ff",
536
+ trailColor: "rgba(0, 0, 0, 0.12)"
537
+ }) : /*#__PURE__*/ jsx(ExportOutlined, {
538
+ style: {
539
+ width: '16px',
540
+ height: '16px'
541
+ }
542
+ })
543
+ }),
544
+ /*#__PURE__*/ jsx("span", {
545
+ className: "player-export-label",
473
546
  children: isExporting ? `Exporting ${exportProgress}%` : 'Export video'
474
547
  })
475
548
  ]
@@ -593,7 +666,7 @@ function Player(props) {
593
666
  style: {
594
667
  height: '32px',
595
668
  lineHeight: '32px',
596
- padding: '0 8px 0 24px',
669
+ padding: '0 8px 0 28px',
597
670
  fontSize: '14px',
598
671
  cursor: 'pointer',
599
672
  borderRadius: '4px'
@@ -0,0 +1,4 @@
1
+ function shouldRestartPlaybackFromBeginning(currentFrame, effectiveEndFrame) {
2
+ return effectiveEndFrame > 0 && currentFrame >= effectiveEndFrame;
3
+ }
4
+ export { shouldRestartPlaybackFromBeginning };
@@ -2,8 +2,9 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { mouseLoading } from "../../../utils/index.mjs";
4
4
  import { getCenterHighlightBox } from "../../../utils/highlight-element.mjs";
5
- import { deriveFrameState } from "./derive-frame-state.mjs";
5
+ import { deriveFrameState, shouldRenderCursor } from "./derive-frame-state.mjs";
6
6
  import { getPlaybackViewport } from "./playback-layout.mjs";
7
+ import { resolvePointerLayout, resolveSpinnerLayout } from "./pointer-layout.mjs";
7
8
  const POINTER_PHASE = 0.375;
8
9
  const CROSSFADE_FRAMES = 10;
9
10
  const StepsTimeline = ({ frameMap, autoZoom, frame, width: compWidth, height: compHeight, fps })=>{
@@ -16,8 +17,8 @@ const StepsTimeline = ({ frameMap, autoZoom, frame, width: compWidth, height: co
16
17
  fps
17
18
  ]);
18
19
  if (!state.img) return null;
19
- const { img, imageWidth: imgW, imageHeight: imgH, prevImg, camera, prevCamera, insights, spinning: spinningPointer, spinningElapsedMs, currentPointerImg, title, subTitle, frameInScript, imageChanged, pointerMoved, rawProgress } = state;
20
- const pT = pointerMoved ? Math.min(rawProgress / POINTER_PHASE, 1) : rawProgress;
20
+ const { img, imageWidth: imgW, imageHeight: imgH, prevImg, camera, prevCamera, insights, spinning: spinningPointer, spinningElapsedMs, currentPointerImg, pointerVisible, title, subTitle, frameInScript, imageChanged, pointerMoved, rawProgress } = state;
21
+ const pT = autoZoom ? pointerMoved ? Math.min(rawProgress / POINTER_PHASE, 1) : rawProgress : 1;
21
22
  const cT = pointerMoved ? rawProgress <= POINTER_PHASE ? 0 : Math.min((rawProgress - POINTER_PHASE) / (1 - POINTER_PHASE), 1) : rawProgress;
22
23
  const pointerLeft = prevCamera.pointerLeft + (camera.pointerLeft - prevCamera.pointerLeft) * pT;
23
24
  const pointerTop = prevCamera.pointerTop + (camera.pointerTop - prevCamera.pointerTop) * pT;
@@ -32,8 +33,10 @@ const StepsTimeline = ({ frameMap, autoZoom, frame, width: compWidth, height: co
32
33
  const camH = imgH / imgW * cameraWidth;
33
34
  const ptrX = (pointerLeft - cameraLeft) / cameraWidth * contentWidth;
34
35
  const ptrY = (pointerTop - cameraTop) / camH * contentHeight;
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 resScale = Math.max(1, Math.sqrt(imgW / 1920));
36
+ const showCursor = shouldRenderCursor(pointerVisible, camera, prevCamera, imgW, imgH);
37
+ const pointerLayout = resolvePointerLayout(imgW);
38
+ const spinnerLayout = resolveSpinnerLayout(pointerLayout);
39
+ const resScale = pointerLayout.scale;
37
40
  const crossfadeAlpha = imageChanged ? Math.min(frameInScript / CROSSFADE_FRAMES, 1) : 1;
38
41
  const spinRotation = spinningPointer ? (Math.sin(spinningElapsedMs / 500 - Math.PI / 2) + 1) / 2 * Math.PI * 2 : 0;
39
42
  const renderInsightOverlays = ()=>{
@@ -143,10 +146,10 @@ const StepsTimeline = ({ frameMap, autoZoom, frame, width: compWidth, height: co
143
146
  src: mouseLoading,
144
147
  style: {
145
148
  position: 'absolute',
146
- left: ptrX - 22 * resScale,
147
- top: ptrY - 28 * resScale,
148
- width: 44 * resScale,
149
- height: 56 * resScale,
149
+ left: ptrX - spinnerLayout.centerOffset,
150
+ top: ptrY - spinnerLayout.centerOffset,
151
+ width: spinnerLayout.size,
152
+ height: spinnerLayout.size,
150
153
  transform: `rotate(${spinRotation}rad)`,
151
154
  transformOrigin: 'center center',
152
155
  filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))'
@@ -157,10 +160,10 @@ const StepsTimeline = ({ frameMap, autoZoom, frame, width: compWidth, height: co
157
160
  src: currentPointerImg,
158
161
  style: {
159
162
  position: 'absolute',
160
- left: ptrX - 6 * resScale,
161
- top: ptrY - 4 * resScale,
162
- width: 44 * resScale,
163
- height: 56 * resScale,
163
+ left: ptrX - pointerLayout.hotspotX,
164
+ top: ptrY - pointerLayout.hotspotY,
165
+ width: pointerLayout.width,
166
+ height: pointerLayout.height,
164
167
  filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))'
165
168
  }
166
169
  })
@@ -72,6 +72,11 @@ function updateImage(acc, sf, baseW, baseH) {
72
72
  function checkPointerMoved(prev, cur) {
73
73
  return Math.abs(prev.pointerLeft - cur.pointerLeft) > 1 || Math.abs(prev.pointerTop - cur.pointerTop) > 1;
74
74
  }
75
+ function shouldRenderCursor(pointerVisible, camera, prevCamera, imageWidth, imageHeight) {
76
+ const centerX = Math.round(imageWidth / 2);
77
+ const centerY = Math.round(imageHeight / 2);
78
+ return pointerVisible || Math.abs(camera.pointerLeft - centerX) > 1 || Math.abs(camera.pointerTop - centerY) > 1 || Math.abs(prevCamera.pointerLeft - centerX) > 1 || Math.abs(prevCamera.pointerTop - centerY) > 1;
79
+ }
75
80
  function handleImg(acc, sf, frame, baseW, baseH) {
76
81
  updateImage(acc, sf, baseW, baseH);
77
82
  const sfEnd = sf.startFrame + sf.durationInFrames;
@@ -132,6 +137,7 @@ function deriveFrameState(scriptFrames, frame, baseW, baseH, fps) {
132
137
  spinning: false,
133
138
  spinningElapsedMs: 0,
134
139
  pointerImg: mousePointer,
140
+ pointerVisible: false,
135
141
  title: '',
136
142
  subTitle: '',
137
143
  taskId: void 0,
@@ -146,7 +152,10 @@ function deriveFrameState(scriptFrames, frame, baseW, baseH, fps) {
146
152
  const sfEnd = sf.startFrame + sf.durationInFrames;
147
153
  if (0 === sf.durationInFrames) {
148
154
  if (sf.startFrame <= frame) {
149
- if ('pointer' === sf.type && sf.pointerImg) acc.pointerImg = sf.pointerImg;
155
+ if ('pointer' === sf.type && sf.pointerImg) {
156
+ acc.pointerImg = sf.pointerImg;
157
+ acc.pointerVisible = true;
158
+ }
150
159
  acc.title = sf.title || acc.title;
151
160
  acc.subTitle = sf.subTitle || acc.subTitle;
152
161
  var _sf_taskId;
@@ -206,6 +215,7 @@ function deriveFrameState(scriptFrames, frame, baseW, baseH, fps) {
206
215
  spinning: acc.spinning,
207
216
  spinningElapsedMs: acc.spinningElapsedMs,
208
217
  currentPointerImg: acc.pointerImg,
218
+ pointerVisible: acc.pointerVisible,
209
219
  title: acc.title,
210
220
  subTitle: acc.subTitle,
211
221
  taskId: acc.taskId,
@@ -216,4 +226,4 @@ function deriveFrameState(scriptFrames, frame, baseW, baseH, fps) {
216
226
  rawProgress: acc.rawProgress
217
227
  };
218
228
  }
219
- export { deriveFrameState };
229
+ export { deriveFrameState, shouldRenderCursor };