clipwise 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -264,8 +264,11 @@ async function renderCursor(frameBuffer, position, config, frameWidth, frameHeig
264
264
  const size = Math.round(config.size * dpr);
265
265
  const cursorSvg = buildCursorSvg(size, config.color);
266
266
  const cursorBuffer = Buffer.from(cursorSvg);
267
- const left = Math.max(0, Math.min(Math.round(position.x * dpr), frameWidth - 1));
268
- const top = Math.max(0, Math.min(Math.round(position.y * dpr), frameHeight - 1));
267
+ const tipOffsetX = Math.round(4 / 24 * size);
268
+ const px = Math.round(position.x * dpr);
269
+ const py = Math.round(position.y * dpr);
270
+ const left = Math.max(0, Math.min(px - tipOffsetX, frameWidth - size));
271
+ const top = Math.max(0, Math.min(py, frameHeight - size));
269
272
  return sharp2(frameBuffer).composite([{ input: cursorBuffer, left, top }]).png().toBuffer();
270
273
  }
271
274
  async function renderClickEffect(frameBuffer, position, config, progress, frameWidth, frameHeight, dpr = 1) {
@@ -458,30 +461,55 @@ async function applyBackground(frameBuffer, config, outputWidth, outputHeight) {
458
461
 
459
462
  // src/effects/keystroke.ts
460
463
  import sharp5 from "sharp";
464
+ function escapeXml(s) {
465
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
466
+ }
467
+ function buildSessions(keystrokes) {
468
+ const hasSessionIds = keystrokes.some((k) => k.sessionId !== void 0);
469
+ if (!hasSessionIds) {
470
+ const text = keystrokes.map((k) => k.key).join("");
471
+ return text.length > 0 ? [text] : [];
472
+ }
473
+ const map = /* @__PURE__ */ new Map();
474
+ for (const k of keystrokes) {
475
+ const sid = k.sessionId ?? 0;
476
+ map.set(sid, (map.get(sid) ?? "") + k.key);
477
+ }
478
+ return Array.from(map.entries()).sort(([a], [b]) => a - b).map(([, text]) => text).filter((t) => t.length > 0);
479
+ }
461
480
  async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, config, frameWidth, frameHeight, dpr = 1) {
462
481
  if (!config.enabled || keystrokes.length === 0) return frameBuffer;
463
- const recentKeys = keystrokes.filter(
464
- (k) => frameTimestamp - k.timestamp < config.fadeAfter
465
- );
466
- if (recentKeys.length === 0) return frameBuffer;
467
- const displayText = recentKeys.map((k) => k.key).join("");
468
- if (displayText.length === 0) return frameBuffer;
482
+ if (!config.showTyping) return frameBuffer;
483
+ const lastKeystroke = keystrokes[keystrokes.length - 1];
484
+ const age = frameTimestamp - lastKeystroke.timestamp;
485
+ if (age >= config.fadeAfter) return frameBuffer;
486
+ const fadeStart = config.fadeAfter * 0.6;
487
+ const globalOpacity = age > fadeStart ? Math.max(0, 1 - (age - fadeStart) / (config.fadeAfter - fadeStart)) : 1;
488
+ if (globalOpacity <= 0) return frameBuffer;
489
+ const allSessions = buildSessions(keystrokes);
490
+ if (allSessions.length === 0) return frameBuffer;
491
+ const sessions = allSessions.slice(-3);
492
+ const lineCount = sessions.length;
469
493
  const fontSize = config.fontSize * dpr;
470
494
  const padding = config.padding * dpr;
471
- const charWidth = fontSize * 0.62;
472
- const textWidth = Math.ceil(displayText.length * charWidth);
473
495
  const hudPadH = padding * 2;
474
- const hudPadV = padding * 1.5;
475
- const hudWidth = Math.min(textWidth + hudPadH * 2, frameWidth - 40 * dpr);
476
- const hudHeight = Math.ceil(fontSize + hudPadV * 2);
477
- const newest = recentKeys[recentKeys.length - 1];
478
- const age = frameTimestamp - newest.timestamp;
479
- const fadeStart = config.fadeAfter * 0.6;
480
- const opacity = age > fadeStart ? Math.max(0, 1 - (age - fadeStart) / (config.fadeAfter - fadeStart)) : 1;
481
- if (opacity <= 0) return frameBuffer;
496
+ const hudPadV = padding * 1.4;
497
+ const lineGap = Math.round(fontSize * 0.45);
498
+ const charWidth = fontSize * 0.615;
499
+ const maxHudWidth = frameWidth - 60 * dpr;
500
+ const maxCharsPerLine = Math.max(10, Math.floor((maxHudWidth - hudPadH * 2) / charWidth));
501
+ const lines = sessions.map(
502
+ (text) => text.length > maxCharsPerLine ? text.slice(-maxCharsPerLine) : text
503
+ );
504
+ const maxLineLen = Math.max(...lines.map((l) => l.length));
505
+ const hudWidth = Math.min(
506
+ Math.ceil(maxLineLen * charWidth) + hudPadH * 2,
507
+ maxHudWidth
508
+ );
509
+ const hudHeight = Math.ceil(fontSize * lineCount + lineGap * (lineCount - 1) + hudPadV * 2);
482
510
  const margin = 30 * dpr;
483
- let hudX;
484
511
  const hudY = frameHeight - hudHeight - margin;
512
+ let hudX;
485
513
  switch (config.position) {
486
514
  case "bottom-left":
487
515
  hudX = margin;
@@ -492,17 +520,24 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
492
520
  case "bottom-center":
493
521
  default:
494
522
  hudX = Math.round((frameWidth - hudWidth) / 2);
495
- break;
496
523
  }
497
- const maxChars = Math.floor((hudWidth - hudPadH * 2) / charWidth);
498
- const truncated = displayText.length > maxChars ? displayText.slice(-maxChars) : displayText;
499
- const escaped = truncated.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
524
+ const LINE_OPACITY_FACTORS = [0.45, 0.7, 1];
525
+ const opacityFactors = LINE_OPACITY_FACTORS.slice(-lineCount);
526
+ const rx = (8 * dpr).toFixed(1);
527
+ const boxOp = (globalOpacity * 0.92).toFixed(3);
528
+ const textX = hudX + hudPadH;
529
+ const baselineY = hudY + hudPadV + fontSize * 0.82;
530
+ const textElements = lines.map((line, i) => {
531
+ const op = (globalOpacity * opacityFactors[i]).toFixed(3);
532
+ const lineY = baselineY + i * (fontSize + lineGap);
533
+ return `<text x="${textX}" y="${lineY}"
534
+ font-family="monospace, Menlo, Consolas" font-size="${fontSize}"
535
+ fill="${config.textColor}" opacity="${op}">${escapeXml(line)}</text>`;
536
+ }).join("\n ");
500
537
  const hudSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
501
538
  <rect x="${hudX}" y="${hudY}" width="${hudWidth}" height="${hudHeight}"
502
- rx="${8 * dpr}" ry="${8 * dpr}" fill="${config.backgroundColor}" opacity="${opacity.toFixed(3)}" />
503
- <text x="${hudX + hudPadH}" y="${hudY + hudPadV + fontSize * 0.75}"
504
- font-family="monospace, Menlo, Consolas" font-size="${fontSize}"
505
- fill="${config.textColor}" opacity="${opacity.toFixed(3)}">${escaped}</text>
539
+ rx="${rx}" ry="${rx}" fill="${config.backgroundColor}" opacity="${boxOp}" />
540
+ ${textElements}
506
541
  </svg>`;
507
542
  return sharp5(frameBuffer).composite([{ input: Buffer.from(hudSvg), left: 0, top: 0 }]).png().toBuffer();
508
543
  }
@@ -603,6 +638,11 @@ async function composeFrame(frame, effects, output, context) {
603
638
  clickProgress: context?.clickProgress ?? null,
604
639
  cursorTrail: context?.cursorTrail ?? []
605
640
  };
641
+ const frameOffset = getFrameOffset(effects.deviceFrame, dpr);
642
+ const withFrameOffset = (pos) => ({
643
+ x: pos.x + frameOffset.left / Math.max(1, dpr),
644
+ y: pos.y + frameOffset.top / Math.max(1, dpr)
645
+ });
606
646
  if (effects.deviceFrame.enabled) {
607
647
  const sl2 = ctx.staticLayers;
608
648
  if (sl2?.browserChromePng && effects.deviceFrame.type === "browser") {
@@ -623,7 +663,7 @@ async function composeFrame(frame, effects, output, context) {
623
663
  if (effects.cursor.enabled && effects.cursor.highlight && frame.cursorPosition) {
624
664
  buffer = await renderCursorHighlight(
625
665
  buffer,
626
- frame.cursorPosition,
666
+ withFrameOffset(frame.cursorPosition),
627
667
  effects.cursor,
628
668
  width,
629
669
  height,
@@ -633,7 +673,7 @@ async function composeFrame(frame, effects, output, context) {
633
673
  if (effects.cursor.enabled && effects.cursor.trail && ctx.cursorTrail.length >= 2) {
634
674
  buffer = await renderCursorTrail(
635
675
  buffer,
636
- ctx.cursorTrail,
676
+ ctx.cursorTrail.map(withFrameOffset),
637
677
  effects.cursor,
638
678
  width,
639
679
  height,
@@ -643,7 +683,7 @@ async function composeFrame(frame, effects, output, context) {
643
683
  if (effects.cursor.enabled && frame.cursorPosition) {
644
684
  buffer = await renderCursor(
645
685
  buffer,
646
- frame.cursorPosition,
686
+ withFrameOffset(frame.cursorPosition),
647
687
  effects.cursor,
648
688
  width,
649
689
  height,
@@ -654,7 +694,7 @@ async function composeFrame(frame, effects, output, context) {
654
694
  const progress = ctx.clickProgress ?? frame.clickProgress ?? 0.5;
655
695
  buffer = await renderClickEffect(
656
696
  buffer,
657
- frame.clickPosition,
697
+ withFrameOffset(frame.clickPosition),
658
698
  effects.cursor,
659
699
  progress,
660
700
  width,
@@ -662,6 +702,16 @@ async function composeFrame(frame, effects, output, context) {
662
702
  dpr
663
703
  );
664
704
  }
705
+ const scale = ctx.zoomScale;
706
+ if (effects.zoom.enabled && scale > 1) {
707
+ const rawFocus = frame.clickPosition ?? frame.cursorPosition ?? { x: frame.viewport.width / 2, y: frame.viewport.height / 2 };
708
+ const offset = getFrameOffset(effects.deviceFrame, dpr);
709
+ const focusPoint = {
710
+ x: rawFocus.x * dpr + offset.left,
711
+ y: rawFocus.y * dpr + offset.top
712
+ };
713
+ buffer = await applyZoom(buffer, focusPoint, scale, width, height);
714
+ }
665
715
  if (effects.keystroke.enabled && frame.keystrokes) {
666
716
  buffer = await renderKeystrokeHud(
667
717
  buffer,
@@ -673,16 +723,6 @@ async function composeFrame(frame, effects, output, context) {
673
723
  dpr
674
724
  );
675
725
  }
676
- const scale = ctx.zoomScale;
677
- if (effects.zoom.enabled && scale > 1) {
678
- const rawFocus = frame.clickPosition ?? frame.cursorPosition ?? { x: frame.viewport.width / 2, y: frame.viewport.height / 2 };
679
- const offset = getFrameOffset(effects.deviceFrame, dpr);
680
- const focusPoint = {
681
- x: rawFocus.x * dpr + offset.left,
682
- y: rawFocus.y * dpr + offset.top
683
- };
684
- buffer = await applyZoom(buffer, focusPoint, scale, width, height);
685
- }
686
726
  const sl = ctx.staticLayers;
687
727
  if (sl) {
688
728
  const padding = effects.background.padding;
package/dist/index.d.ts CHANGED
@@ -174,11 +174,21 @@ type StepAction = z.infer<typeof StepActionSchema>;
174
174
  declare const EffectsConfigSchema: z.ZodObject<{
175
175
  zoom: z.ZodDefault<z.ZodObject<{
176
176
  enabled: z.ZodDefault<z.ZodBoolean>;
177
+ /**
178
+ * Numeric zoom scale (1.0 = no zoom). Overridden by `intensity` when set.
179
+ * Default lowered from 1.8 → 1.35 to match "moderate" intensity.
180
+ */
177
181
  scale: z.ZodDefault<z.ZodNumber>;
182
+ /**
183
+ * Intensity preset — overrides `scale` when set.
184
+ * Calibrated against Loom (light≈1.25x) and Camtasia (moderate≈1.35x).
185
+ */
186
+ intensity: z.ZodOptional<z.ZodEnum<["subtle", "light", "moderate", "strong", "dramatic"]>>;
178
187
  duration: z.ZodDefault<z.ZodNumber>;
179
188
  easing: z.ZodDefault<z.ZodEnum<["ease-in-out", "ease-in", "ease-out", "linear"]>>;
180
189
  autoZoom: z.ZodDefault<z.ZodObject<{
181
190
  followCursor: z.ZodDefault<z.ZodBoolean>;
191
+ /** @deprecated Use `intensity` on the parent zoom config instead. */
182
192
  maxScale: z.ZodDefault<z.ZodNumber>;
183
193
  transitionDuration: z.ZodDefault<z.ZodNumber>;
184
194
  padding: z.ZodDefault<z.ZodNumber>;
@@ -204,10 +214,12 @@ declare const EffectsConfigSchema: z.ZodObject<{
204
214
  transitionDuration: number;
205
215
  padding: number;
206
216
  };
217
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
207
218
  }, {
208
219
  duration?: number | undefined;
209
220
  enabled?: boolean | undefined;
210
221
  scale?: number | undefined;
222
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
211
223
  easing?: "ease-in-out" | "ease-in" | "ease-out" | "linear" | undefined;
212
224
  autoZoom?: {
213
225
  followCursor?: boolean | undefined;
@@ -312,6 +324,17 @@ declare const EffectsConfigSchema: z.ZodObject<{
312
324
  }>>;
313
325
  keystroke: z.ZodDefault<z.ZodObject<{
314
326
  enabled: z.ZodDefault<z.ZodBoolean>;
327
+ /**
328
+ * Show regular typed text (alphabetic/numeric characters) in the HUD.
329
+ *
330
+ * Industry default is false — Screen Studio, KeyCastr, and ScreenFlow all
331
+ * hide regular typing by default, showing only modifier+key shortcuts.
332
+ * Typed content is already visible inside the focused input element, so
333
+ * displaying it again in the HUD is redundant and creates overflow issues.
334
+ *
335
+ * Set to true to display a 2-line rolling HUD that follows the typed text.
336
+ */
337
+ showTyping: z.ZodDefault<z.ZodBoolean>;
315
338
  position: z.ZodDefault<z.ZodEnum<["bottom-center", "bottom-left", "bottom-right"]>>;
316
339
  fontSize: z.ZodDefault<z.ZodNumber>;
317
340
  backgroundColor: z.ZodDefault<z.ZodString>;
@@ -321,6 +344,7 @@ declare const EffectsConfigSchema: z.ZodObject<{
321
344
  }, "strip", z.ZodTypeAny, {
322
345
  padding: number;
323
346
  enabled: boolean;
347
+ showTyping: boolean;
324
348
  position: "bottom-center" | "bottom-left" | "bottom-right";
325
349
  fontSize: number;
326
350
  backgroundColor: string;
@@ -329,6 +353,7 @@ declare const EffectsConfigSchema: z.ZodObject<{
329
353
  }, {
330
354
  padding?: number | undefined;
331
355
  enabled?: boolean | undefined;
356
+ showTyping?: boolean | undefined;
332
357
  position?: "bottom-center" | "bottom-left" | "bottom-right" | undefined;
333
358
  fontSize?: number | undefined;
334
359
  backgroundColor?: string | undefined;
@@ -369,6 +394,7 @@ declare const EffectsConfigSchema: z.ZodObject<{
369
394
  transitionDuration: number;
370
395
  padding: number;
371
396
  };
397
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
372
398
  };
373
399
  cursor: {
374
400
  enabled: boolean;
@@ -407,6 +433,7 @@ declare const EffectsConfigSchema: z.ZodObject<{
407
433
  keystroke: {
408
434
  padding: number;
409
435
  enabled: boolean;
436
+ showTyping: boolean;
410
437
  position: "bottom-center" | "bottom-left" | "bottom-right";
411
438
  fontSize: number;
412
439
  backgroundColor: string;
@@ -426,6 +453,7 @@ declare const EffectsConfigSchema: z.ZodObject<{
426
453
  duration?: number | undefined;
427
454
  enabled?: boolean | undefined;
428
455
  scale?: number | undefined;
456
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
429
457
  easing?: "ease-in-out" | "ease-in" | "ease-out" | "linear" | undefined;
430
458
  autoZoom?: {
431
459
  followCursor?: boolean | undefined;
@@ -471,6 +499,7 @@ declare const EffectsConfigSchema: z.ZodObject<{
471
499
  keystroke?: {
472
500
  padding?: number | undefined;
473
501
  enabled?: boolean | undefined;
502
+ showTyping?: boolean | undefined;
474
503
  position?: "bottom-center" | "bottom-left" | "bottom-right" | undefined;
475
504
  fontSize?: number | undefined;
476
505
  backgroundColor?: string | undefined;
@@ -832,11 +861,21 @@ declare const ScenarioSchema: z.ZodObject<{
832
861
  effects: z.ZodDefault<z.ZodObject<{
833
862
  zoom: z.ZodDefault<z.ZodObject<{
834
863
  enabled: z.ZodDefault<z.ZodBoolean>;
864
+ /**
865
+ * Numeric zoom scale (1.0 = no zoom). Overridden by `intensity` when set.
866
+ * Default lowered from 1.8 → 1.35 to match "moderate" intensity.
867
+ */
835
868
  scale: z.ZodDefault<z.ZodNumber>;
869
+ /**
870
+ * Intensity preset — overrides `scale` when set.
871
+ * Calibrated against Loom (light≈1.25x) and Camtasia (moderate≈1.35x).
872
+ */
873
+ intensity: z.ZodOptional<z.ZodEnum<["subtle", "light", "moderate", "strong", "dramatic"]>>;
836
874
  duration: z.ZodDefault<z.ZodNumber>;
837
875
  easing: z.ZodDefault<z.ZodEnum<["ease-in-out", "ease-in", "ease-out", "linear"]>>;
838
876
  autoZoom: z.ZodDefault<z.ZodObject<{
839
877
  followCursor: z.ZodDefault<z.ZodBoolean>;
878
+ /** @deprecated Use `intensity` on the parent zoom config instead. */
840
879
  maxScale: z.ZodDefault<z.ZodNumber>;
841
880
  transitionDuration: z.ZodDefault<z.ZodNumber>;
842
881
  padding: z.ZodDefault<z.ZodNumber>;
@@ -862,10 +901,12 @@ declare const ScenarioSchema: z.ZodObject<{
862
901
  transitionDuration: number;
863
902
  padding: number;
864
903
  };
904
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
865
905
  }, {
866
906
  duration?: number | undefined;
867
907
  enabled?: boolean | undefined;
868
908
  scale?: number | undefined;
909
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
869
910
  easing?: "ease-in-out" | "ease-in" | "ease-out" | "linear" | undefined;
870
911
  autoZoom?: {
871
912
  followCursor?: boolean | undefined;
@@ -970,6 +1011,17 @@ declare const ScenarioSchema: z.ZodObject<{
970
1011
  }>>;
971
1012
  keystroke: z.ZodDefault<z.ZodObject<{
972
1013
  enabled: z.ZodDefault<z.ZodBoolean>;
1014
+ /**
1015
+ * Show regular typed text (alphabetic/numeric characters) in the HUD.
1016
+ *
1017
+ * Industry default is false — Screen Studio, KeyCastr, and ScreenFlow all
1018
+ * hide regular typing by default, showing only modifier+key shortcuts.
1019
+ * Typed content is already visible inside the focused input element, so
1020
+ * displaying it again in the HUD is redundant and creates overflow issues.
1021
+ *
1022
+ * Set to true to display a 2-line rolling HUD that follows the typed text.
1023
+ */
1024
+ showTyping: z.ZodDefault<z.ZodBoolean>;
973
1025
  position: z.ZodDefault<z.ZodEnum<["bottom-center", "bottom-left", "bottom-right"]>>;
974
1026
  fontSize: z.ZodDefault<z.ZodNumber>;
975
1027
  backgroundColor: z.ZodDefault<z.ZodString>;
@@ -979,6 +1031,7 @@ declare const ScenarioSchema: z.ZodObject<{
979
1031
  }, "strip", z.ZodTypeAny, {
980
1032
  padding: number;
981
1033
  enabled: boolean;
1034
+ showTyping: boolean;
982
1035
  position: "bottom-center" | "bottom-left" | "bottom-right";
983
1036
  fontSize: number;
984
1037
  backgroundColor: string;
@@ -987,6 +1040,7 @@ declare const ScenarioSchema: z.ZodObject<{
987
1040
  }, {
988
1041
  padding?: number | undefined;
989
1042
  enabled?: boolean | undefined;
1043
+ showTyping?: boolean | undefined;
990
1044
  position?: "bottom-center" | "bottom-left" | "bottom-right" | undefined;
991
1045
  fontSize?: number | undefined;
992
1046
  backgroundColor?: string | undefined;
@@ -1027,6 +1081,7 @@ declare const ScenarioSchema: z.ZodObject<{
1027
1081
  transitionDuration: number;
1028
1082
  padding: number;
1029
1083
  };
1084
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
1030
1085
  };
1031
1086
  cursor: {
1032
1087
  enabled: boolean;
@@ -1065,6 +1120,7 @@ declare const ScenarioSchema: z.ZodObject<{
1065
1120
  keystroke: {
1066
1121
  padding: number;
1067
1122
  enabled: boolean;
1123
+ showTyping: boolean;
1068
1124
  position: "bottom-center" | "bottom-left" | "bottom-right";
1069
1125
  fontSize: number;
1070
1126
  backgroundColor: string;
@@ -1084,6 +1140,7 @@ declare const ScenarioSchema: z.ZodObject<{
1084
1140
  duration?: number | undefined;
1085
1141
  enabled?: boolean | undefined;
1086
1142
  scale?: number | undefined;
1143
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
1087
1144
  easing?: "ease-in-out" | "ease-in" | "ease-out" | "linear" | undefined;
1088
1145
  autoZoom?: {
1089
1146
  followCursor?: boolean | undefined;
@@ -1129,6 +1186,7 @@ declare const ScenarioSchema: z.ZodObject<{
1129
1186
  keystroke?: {
1130
1187
  padding?: number | undefined;
1131
1188
  enabled?: boolean | undefined;
1189
+ showTyping?: boolean | undefined;
1132
1190
  position?: "bottom-center" | "bottom-left" | "bottom-right" | undefined;
1133
1191
  fontSize?: number | undefined;
1134
1192
  backgroundColor?: string | undefined;
@@ -1489,6 +1547,7 @@ declare const ScenarioSchema: z.ZodObject<{
1489
1547
  transitionDuration: number;
1490
1548
  padding: number;
1491
1549
  };
1550
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
1492
1551
  };
1493
1552
  cursor: {
1494
1553
  enabled: boolean;
@@ -1527,6 +1586,7 @@ declare const ScenarioSchema: z.ZodObject<{
1527
1586
  keystroke: {
1528
1587
  padding: number;
1529
1588
  enabled: boolean;
1589
+ showTyping: boolean;
1530
1590
  position: "bottom-center" | "bottom-left" | "bottom-right";
1531
1591
  fontSize: number;
1532
1592
  backgroundColor: string;
@@ -1691,6 +1751,7 @@ declare const ScenarioSchema: z.ZodObject<{
1691
1751
  duration?: number | undefined;
1692
1752
  enabled?: boolean | undefined;
1693
1753
  scale?: number | undefined;
1754
+ intensity?: "subtle" | "light" | "moderate" | "strong" | "dramatic" | undefined;
1694
1755
  easing?: "ease-in-out" | "ease-in" | "ease-out" | "linear" | undefined;
1695
1756
  autoZoom?: {
1696
1757
  followCursor?: boolean | undefined;
@@ -1736,6 +1797,7 @@ declare const ScenarioSchema: z.ZodObject<{
1736
1797
  keystroke?: {
1737
1798
  padding?: number | undefined;
1738
1799
  enabled?: boolean | undefined;
1800
+ showTyping?: boolean | undefined;
1739
1801
  position?: "bottom-center" | "bottom-left" | "bottom-right" | undefined;
1740
1802
  fontSize?: number | undefined;
1741
1803
  backgroundColor?: string | undefined;
@@ -1766,6 +1828,12 @@ type Scenario = z.infer<typeof ScenarioSchema>;
1766
1828
  interface KeystrokeEvent {
1767
1829
  key: string;
1768
1830
  timestamp: number;
1831
+ /**
1832
+ * Typing session ID — incremented each time a new input is focused via the
1833
+ * `type` action. HUD groups keystrokes by sessionId so each input field's
1834
+ * typed content appears on its own line. Undefined for legacy recordings.
1835
+ */
1836
+ sessionId?: number;
1769
1837
  }
1770
1838
  interface CapturedFrame {
1771
1839
  index: number;
@@ -1845,6 +1913,9 @@ declare class ClipwiseRecorder {
1845
1913
  private cursorTimeline;
1846
1914
  private clickTimeline;
1847
1915
  private keystrokeTimeline;
1916
+ /** Incremented at the start of each `type` action so the HUD can render
1917
+ * each input field's text on a separate line. */
1918
+ private keystrokeSessionId;
1848
1919
  private currentStepIndex;
1849
1920
  private cursorPosition;
1850
1921
  private viewport;
@@ -1895,6 +1966,21 @@ declare class ClipwiseRecorder {
1895
1966
  * Cursor/click data reflects the timeline up to this moment.
1896
1967
  */
1897
1968
  private buildFrameOnline;
1969
+ /**
1970
+ * Force a unique DOM repaint visible in the top scanlines of the captured PNG.
1971
+ *
1972
+ * Uses a 1×1 px fixed-position element at z-index MAX, sitting above ALL
1973
+ * overlays including modals (position:fixed;z-index:100;backdrop-filter:blur).
1974
+ * Alternates background between #000001 and #000100 — two colors that are
1975
+ * visually indistinguishable (1/255 difference in R or G channel against a
1976
+ * dark page) but produce distinct PNG byte sequences, defeating dedup.
1977
+ *
1978
+ * This replaces the previous `document.documentElement.style.outline` approach
1979
+ * which failed whenever a full-viewport fixed overlay (e.g. modal backdrop)
1980
+ * was composited on top of the outline, making y=0 PNG bytes identical across
1981
+ * frames and causing dedup to collapse all modal-typing frames into one.
1982
+ */
1983
+ private forceRepaint;
1898
1984
  /**
1899
1985
  * Wait for a given duration while forcing periodic repaints
1900
1986
  * so CDP screencast keeps sending frames even on static pages.
@@ -1913,9 +1999,38 @@ declare class ClipwiseRecorder {
1913
1999
  */
1914
2000
  private executeAction;
1915
2001
  /**
1916
- * Move cursor smoothly from current position to target using
1917
- * manual step-by-step movement with delays between each step.
1918
- * Speed is controlled by the cursor.speed preset (fast/normal/slow).
2002
+ * Suppress all CSS transitions and animations on the page during cursor
2003
+ * movement. Hover-state transitions (background, transform, box-shadow,
2004
+ * etc.) on elements the cursor passes over generate CSS-animation-driven
2005
+ * CDP frames that arrive asynchronously relative to our cursor step
2006
+ * intervals. Those extra frames are timestamped when they're ACK-drained,
2007
+ * which can be many milliseconds after the actual cursor moved — causing
2008
+ * interpolateCursorAt() to map them to a newer cursor position while the
2009
+ * screenshot still shows older content → visible stutter.
2010
+ *
2011
+ * Suppressing transitions during movement eliminates these extra frames
2012
+ * entirely regardless of which elements the path crosses. Transitions are
2013
+ * restored immediately after arrival, so hover effects on the final target
2014
+ * element still appear during the subsequent holdDuration.
2015
+ */
2016
+ private suppressTransitions;
2017
+ private restoreTransitions;
2018
+ /**
2019
+ * Move cursor smoothly from current position to target.
2020
+ *
2021
+ * Key design decisions:
2022
+ * 1. Adaptive step count — proportional to travel distance so short and
2023
+ * long movements feel equally paced (pixelsPerStep controls speed).
2024
+ * 2. Forced repaint per step — moving the mouse in headless Chrome does NOT
2025
+ * visually change the screenshot (the cursor is rendered in post-processing).
2026
+ * Without a forced repaint, dedup collapses every intermediate frame into
2027
+ * the first one, making the cursor appear to teleport.
2028
+ * 3. Transition suppression — CSS transitions on hovered elements generate
2029
+ * asynchronous CDP frames that desync cursor position from screenshot
2030
+ * content. All transitions are suppressed for the duration of movement
2031
+ * and restored on arrival (see suppressTransitions / restoreTransitions).
2032
+ * 4. Capped bezier curve — perpendicular offset is capped at 30 px regardless
2033
+ * of distance, preventing a visible arc on long-distance movements.
1919
2034
  */
1920
2035
  private moveCursorSmooth;
1921
2036
  /**
@@ -2207,6 +2322,26 @@ declare class StreamingSession extends EventEmitter {
2207
2322
  run(): Promise<Buffer>;
2208
2323
  }
2209
2324
 
2325
+ /**
2326
+ * Preset zoom intensity levels for auto-zoom.
2327
+ *
2328
+ * Calibrated against industry tools (Loom ≈1.2x, Camtasia SmartFocus ≈1.3-1.4x,
2329
+ * ScreenFlow ≈1.25x) so the focus point is clear without cutting off surrounding
2330
+ * context (navigation bars, sidebars, etc.).
2331
+ *
2332
+ * - subtle 1.15x — barely noticeable; good for dense UIs or large viewports
2333
+ * - light 1.25x — Loom-style gentle pull-in; draws attention, keeps context
2334
+ * - moderate 1.35x — balanced default; Camtasia-range, works for most demos
2335
+ * - strong 1.5x — clear focus; some peripheral context sacrificed
2336
+ * - dramatic 1.8x — maximum emphasis; use only for simple, sparse UIs
2337
+ */
2338
+ type ZoomIntensity = "subtle" | "light" | "moderate" | "strong" | "dramatic";
2339
+ declare const ZOOM_INTENSITY_SCALES: Record<ZoomIntensity, number>;
2340
+ /**
2341
+ * Resolve the effective zoom scale from an explicit `scale` value or an
2342
+ * `intensity` preset. When both are provided, `intensity` takes precedence.
2343
+ */
2344
+ declare function resolveZoomScale(scale: number, intensity?: ZoomIntensity): number;
2210
2345
  /**
2211
2346
  * Calculate adaptive zoom scale based on proximity to click/action frames.
2212
2347
  * Zooms in smoothly near important actions, stays at 1.0 during idle.
@@ -2304,8 +2439,19 @@ declare function renderCursorTrail(frameBuffer: Buffer, positions: Array<{
2304
2439
 
2305
2440
  type KeystrokeConfig = EffectsConfig["keystroke"];
2306
2441
  /**
2307
- * Render a keystroke HUD overlay showing recently typed keys.
2308
- * Inspired by KeyCastr / CursorClip.
2442
+ * Render a keystroke HUD overlay on the frame.
2443
+ *
2444
+ * showTyping: false (default, industry standard)
2445
+ * No typing text is shown — consistent with Screen Studio, KeyCastr, and
2446
+ * ScreenFlow which hide regular typing by default.
2447
+ *
2448
+ * showTyping: true
2449
+ * Multi-session rolling HUD. Each `type` action (= each input field) gets
2450
+ * its own line. Up to 3 recent sessions are shown simultaneously, oldest
2451
+ * at the top and dimmed, newest at the bottom at full brightness. Lines
2452
+ * that are too long to fit are truncated from the left (showing the most
2453
+ * recently typed characters). The HUD fades `fadeAfter` ms after the last
2454
+ * keystroke of the last session.
2309
2455
  */
2310
2456
  declare function renderKeystrokeHud(frameBuffer: Buffer, keystrokes: KeystrokeEvent[], frameTimestamp: number, config: KeystrokeConfig, frameWidth: number, frameHeight: number, dpr?: number): Promise<Buffer>;
2311
2457
 
@@ -2358,4 +2504,4 @@ interface ValidationResult {
2358
2504
  */
2359
2505
  declare function validateScenario(scenario: Scenario): ValidationResult;
2360
2506
 
2361
- export { CanvasRenderer, type CapturedFrame, ClipwiseRecorder, type ComposedFrame, type ConcurrentResult, ConcurrentSession, type EffectsConfig, type FrameContext, type KeystrokeEvent, type OutputConfig, type PipelineProgress, type RecordingHandle, type RecordingSession, type Scenario, type Step, type StepAction, StreamingSession, applyCrossfade, buildZoomClickLookup, calculateAdaptiveZoom, calculateAdaptiveZoomFromLookup, calculateAdaptiveZoomInWindow, calculatePanOffset, encodeGif, encodeMp4, encodeMp4Stream, lerpZoom, loadScenario, parseScenario, renderCursorHighlight, renderCursorTrail, renderKeystrokeHud, renderWatermark, savePngSequence, validateScenario };
2507
+ export { CanvasRenderer, type CapturedFrame, ClipwiseRecorder, type ComposedFrame, type ConcurrentResult, ConcurrentSession, type EffectsConfig, type FrameContext, type KeystrokeEvent, type OutputConfig, type PipelineProgress, type RecordingHandle, type RecordingSession, type Scenario, type Step, type StepAction, StreamingSession, ZOOM_INTENSITY_SCALES, type ZoomIntensity, applyCrossfade, buildZoomClickLookup, calculateAdaptiveZoom, calculateAdaptiveZoomFromLookup, calculateAdaptiveZoomInWindow, calculatePanOffset, encodeGif, encodeMp4, encodeMp4Stream, lerpZoom, loadScenario, parseScenario, renderCursorHighlight, renderCursorTrail, renderKeystrokeHud, renderWatermark, resolveZoomScale, savePngSequence, validateScenario };