clipwise 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +26 -11
- package/README.md +23 -10
- package/dist/cli/index.js +292 -100
- package/dist/compose/frame-worker.js +81 -41
- package/dist/index.d.ts +152 -6
- package/dist/index.js +293 -99
- package/package.json +1 -1
|
@@ -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
|
|
268
|
-
const
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (
|
|
467
|
-
const
|
|
468
|
-
|
|
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.
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
498
|
-
const
|
|
499
|
-
const
|
|
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="${
|
|
503
|
-
|
|
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
|
-
*
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
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
|
|
2308
|
-
*
|
|
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 };
|