clipwise 0.7.0 → 0.7.2
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 +2 -2
- package/README.md +34 -9
- package/dist/cli/index.js +224 -216
- package/dist/compose/frame-worker.js +58 -12
- package/dist/index.d.ts +312 -13
- package/dist/index.js +195 -70
- package/package.json +1 -1
- package/skills/clipwise.md +29 -1
package/dist/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ var __export = (target, all) => {
|
|
|
11
11
|
|
|
12
12
|
// src/script/types.ts
|
|
13
13
|
import { z } from "zod";
|
|
14
|
-
var SafeSelectorSchema, NavigateActionSchema, ClickActionSchema, TypeActionSchema, ScrollActionSchema, WaitActionSchema, HoverActionSchema, ScreenshotActionSchema, WaitForSelectorActionSchema, WaitForNavigationActionSchema, WaitForURLActionSchema, WaitForFunctionActionSchema, WaitForResponseActionSchema, SmartWaitActionSchema, StepActionSchema, ZoomIntensitySchema, AutoZoomConfigSchema, ZoomEffectSchema, CursorEffectSchema, BackgroundSchema, DeviceFrameSchema, SpeedRampConfigSchema, SmartSpeedConfigSchema, KeystrokeConfigSchema, WatermarkConfigSchema, EffectsConfigSchema, OutputConfigSchema, StepEffectsOverrideSchema, TransitionTypeSchema, StepSchema, AudioConfigSchema, ScenarioSchema;
|
|
14
|
+
var SafeSelectorSchema, NavigateActionSchema, ClickActionSchema, TypeActionSchema, ScrollActionSchema, WaitActionSchema, HoverActionSchema, ScreenshotActionSchema, WaitForSelectorActionSchema, WaitForNavigationActionSchema, WaitForURLActionSchema, WaitForFunctionActionSchema, WaitForResponseActionSchema, SmartWaitActionSchema, StepActionSchema, ZoomIntensitySchema, AutoZoomConfigSchema, ZoomEffectSchema, CursorEffectSchema, BackgroundSchema, DeviceFrameSchema, SpeedRampConfigSchema, SmartSpeedConfigSchema, KeystrokeConfigSchema, WatermarkConfigSchema, EffectsConfigSchema, OutputConfigSchema, StepEffectsOverrideSchema, TransitionTypeSchema, StepSchema, AudioConfigSchema, AuthConfigSchema, ScenarioSchema;
|
|
15
15
|
var init_types = __esm({
|
|
16
16
|
"src/script/types.ts"() {
|
|
17
17
|
"use strict";
|
|
@@ -63,29 +63,41 @@ var init_types = __esm({
|
|
|
63
63
|
action: z.literal("waitForSelector"),
|
|
64
64
|
selector: SafeSelectorSchema,
|
|
65
65
|
state: z.enum(["visible", "attached", "hidden"]).default("visible"),
|
|
66
|
-
timeout: z.number().min(0).default(15e3)
|
|
66
|
+
timeout: z.number().min(0).default(15e3),
|
|
67
|
+
/** 대기 중 프레임 연속 캡처 (로딩 애니메이션 보존). */
|
|
68
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
69
|
+
/** captureWhileWaiting 사용 시 출력 영상 속도 배율 (1-32). */
|
|
70
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
67
71
|
});
|
|
68
72
|
WaitForNavigationActionSchema = z.object({
|
|
69
73
|
action: z.literal("waitForNavigation"),
|
|
70
74
|
waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).default("networkidle"),
|
|
71
|
-
timeout: z.number().min(0).default(15e3)
|
|
75
|
+
timeout: z.number().min(0).default(15e3),
|
|
76
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
77
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
72
78
|
});
|
|
73
79
|
WaitForURLActionSchema = z.object({
|
|
74
80
|
action: z.literal("waitForURL"),
|
|
75
81
|
url: z.string().min(1),
|
|
76
|
-
timeout: z.number().min(0).default(15e3)
|
|
82
|
+
timeout: z.number().min(0).default(15e3),
|
|
83
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
84
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
77
85
|
});
|
|
78
86
|
WaitForFunctionActionSchema = z.object({
|
|
79
87
|
action: z.literal("waitForFunction"),
|
|
80
88
|
expression: z.string().min(1),
|
|
81
89
|
polling: z.union([z.literal("raf"), z.number().min(0)]).default("raf"),
|
|
82
|
-
timeout: z.number().min(0).default(3e4)
|
|
90
|
+
timeout: z.number().min(0).default(3e4),
|
|
91
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
92
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
83
93
|
});
|
|
84
94
|
WaitForResponseActionSchema = z.object({
|
|
85
95
|
action: z.literal("waitForResponse"),
|
|
86
96
|
url: z.string().min(1),
|
|
87
97
|
status: z.number().min(100).max(599).optional(),
|
|
88
|
-
timeout: z.number().min(0).default(3e4)
|
|
98
|
+
timeout: z.number().min(0).default(3e4),
|
|
99
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
100
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
89
101
|
});
|
|
90
102
|
SmartWaitActionSchema = z.object({
|
|
91
103
|
action: z.literal("smartWait"),
|
|
@@ -280,6 +292,22 @@ var init_types = __esm({
|
|
|
280
292
|
/** Fade-out duration in milliseconds. */
|
|
281
293
|
fadeOut: z.number().min(0).default(0)
|
|
282
294
|
});
|
|
295
|
+
AuthConfigSchema = z.object({
|
|
296
|
+
/** Path to a Playwright storageState JSON file (cookies + localStorage). */
|
|
297
|
+
storageState: z.string().optional(),
|
|
298
|
+
/** Inline cookie definitions (applied after storageState if both specified). */
|
|
299
|
+
cookies: z.array(
|
|
300
|
+
z.object({
|
|
301
|
+
name: z.string(),
|
|
302
|
+
value: z.string(),
|
|
303
|
+
domain: z.string(),
|
|
304
|
+
path: z.string().default("/"),
|
|
305
|
+
httpOnly: z.boolean().default(false),
|
|
306
|
+
secure: z.boolean().default(false),
|
|
307
|
+
sameSite: z.enum(["Strict", "Lax", "None"]).default("Lax")
|
|
308
|
+
})
|
|
309
|
+
).optional()
|
|
310
|
+
});
|
|
283
311
|
ScenarioSchema = z.object({
|
|
284
312
|
name: z.string(),
|
|
285
313
|
description: z.string().optional(),
|
|
@@ -287,6 +315,8 @@ var init_types = __esm({
|
|
|
287
315
|
width: z.number().default(1280),
|
|
288
316
|
height: z.number().default(800)
|
|
289
317
|
}).default({}),
|
|
318
|
+
/** Optional authentication — restores browser session for logged-in pages. */
|
|
319
|
+
auth: AuthConfigSchema.optional(),
|
|
290
320
|
effects: EffectsConfigSchema.default({}),
|
|
291
321
|
output: OutputConfigSchema.default({}),
|
|
292
322
|
/** Optional audio narration — muxed into MP4 output. */
|
|
@@ -567,9 +597,16 @@ var ClipwiseRecorder = class {
|
|
|
567
597
|
this.cursorSpeed = scenario.effects.cursor.speed;
|
|
568
598
|
this.loaderDetectionEnabled = scenario.effects.smartSpeed?.enabled ?? false;
|
|
569
599
|
this.browser = await chromium.launch({ headless: true });
|
|
570
|
-
|
|
600
|
+
const contextOptions = {
|
|
571
601
|
viewport: this.viewport
|
|
572
|
-
}
|
|
602
|
+
};
|
|
603
|
+
if (scenario.auth?.storageState) {
|
|
604
|
+
contextOptions.storageState = scenario.auth.storageState;
|
|
605
|
+
}
|
|
606
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
607
|
+
if (scenario.auth?.cookies?.length) {
|
|
608
|
+
await this.context.addCookies(scenario.auth.cookies);
|
|
609
|
+
}
|
|
573
610
|
this.page = await this.context.newPage();
|
|
574
611
|
this.rawFrames = [];
|
|
575
612
|
this.cursorTimeline = [];
|
|
@@ -892,6 +929,31 @@ var ClipwiseRecorder = class {
|
|
|
892
929
|
}
|
|
893
930
|
}
|
|
894
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* 조건 대기 중 프레임을 연속 캡처하는 헬퍼.
|
|
934
|
+
* smartWait과 동일한 패턴: isWaitingPhase 플래그 + forceRepaint 루프를 조건 promise와 병렬 실행.
|
|
935
|
+
*/
|
|
936
|
+
async waitForConditionWithCapture(conditionPromise, displaySpeed) {
|
|
937
|
+
this.isWaitingPhase = true;
|
|
938
|
+
this.currentDisplaySpeed = displaySpeed;
|
|
939
|
+
try {
|
|
940
|
+
let waitDone = false;
|
|
941
|
+
const repaintLoop = (async () => {
|
|
942
|
+
let toggle = false;
|
|
943
|
+
while (!waitDone && this.isCapturing && this.page) {
|
|
944
|
+
await this.forceRepaint(toggle);
|
|
945
|
+
toggle = !toggle;
|
|
946
|
+
await new Promise((r) => setTimeout(r, REPAINT_INTERVAL_MS));
|
|
947
|
+
}
|
|
948
|
+
})();
|
|
949
|
+
await conditionPromise;
|
|
950
|
+
waitDone = true;
|
|
951
|
+
await repaintLoop;
|
|
952
|
+
} finally {
|
|
953
|
+
this.isWaitingPhase = false;
|
|
954
|
+
this.currentDisplaySpeed = void 0;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
895
957
|
/**
|
|
896
958
|
* Pre-register waitForResponse listeners at the start of each step.
|
|
897
959
|
* This ensures the listener is active before any preceding action
|
|
@@ -982,6 +1044,17 @@ var ClipwiseRecorder = class {
|
|
|
982
1044
|
lastClickRefresh = now;
|
|
983
1045
|
}
|
|
984
1046
|
}
|
|
1047
|
+
await this.page.evaluate((sel) => {
|
|
1048
|
+
const el = document.querySelector(sel);
|
|
1049
|
+
if (!el) return;
|
|
1050
|
+
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
1051
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
1052
|
+
if (setter) {
|
|
1053
|
+
setter.call(el, el.value);
|
|
1054
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1055
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1056
|
+
}
|
|
1057
|
+
}, action.selector);
|
|
985
1058
|
this.clickTimeline.push({
|
|
986
1059
|
position: { ...inputTarget },
|
|
987
1060
|
timestamp: Date.now()
|
|
@@ -1055,83 +1128,89 @@ var ClipwiseRecorder = class {
|
|
|
1055
1128
|
}
|
|
1056
1129
|
case "waitForSelector": {
|
|
1057
1130
|
const locator = this.page.locator(action.selector).first();
|
|
1058
|
-
|
|
1131
|
+
const selectorPromise = locator.waitFor({ state: action.state, timeout: action.timeout });
|
|
1132
|
+
if (action.captureWhileWaiting) {
|
|
1133
|
+
await this.waitForConditionWithCapture(selectorPromise, action.displaySpeed);
|
|
1134
|
+
} else {
|
|
1135
|
+
await selectorPromise;
|
|
1136
|
+
}
|
|
1059
1137
|
break;
|
|
1060
1138
|
}
|
|
1061
1139
|
case "waitForNavigation": {
|
|
1062
|
-
|
|
1140
|
+
const navPromise = this.page.waitForLoadState(action.waitUntil, { timeout: action.timeout });
|
|
1141
|
+
if (action.captureWhileWaiting) {
|
|
1142
|
+
await this.waitForConditionWithCapture(navPromise, action.displaySpeed);
|
|
1143
|
+
} else {
|
|
1144
|
+
await navPromise;
|
|
1145
|
+
}
|
|
1063
1146
|
break;
|
|
1064
1147
|
}
|
|
1065
1148
|
case "waitForURL": {
|
|
1066
|
-
|
|
1149
|
+
const urlPromise = this.page.waitForURL(action.url, { timeout: action.timeout });
|
|
1150
|
+
if (action.captureWhileWaiting) {
|
|
1151
|
+
await this.waitForConditionWithCapture(urlPromise, action.displaySpeed);
|
|
1152
|
+
} else {
|
|
1153
|
+
await urlPromise;
|
|
1154
|
+
}
|
|
1067
1155
|
break;
|
|
1068
1156
|
}
|
|
1069
1157
|
case "waitForFunction": {
|
|
1070
|
-
|
|
1158
|
+
const fnPromise = this.page.waitForFunction(action.expression, void 0, {
|
|
1071
1159
|
polling: action.polling,
|
|
1072
1160
|
timeout: action.timeout
|
|
1073
1161
|
});
|
|
1162
|
+
if (action.captureWhileWaiting) {
|
|
1163
|
+
await this.waitForConditionWithCapture(fnPromise, action.displaySpeed);
|
|
1164
|
+
} else {
|
|
1165
|
+
await fnPromise;
|
|
1166
|
+
}
|
|
1074
1167
|
break;
|
|
1075
1168
|
}
|
|
1076
1169
|
case "waitForResponse": {
|
|
1077
1170
|
const pending = this.pendingResponsePromises.get(actionIndex);
|
|
1078
1171
|
if (pending) {
|
|
1079
|
-
|
|
1172
|
+
if (action.captureWhileWaiting) {
|
|
1173
|
+
await this.waitForConditionWithCapture(pending, action.displaySpeed);
|
|
1174
|
+
} else {
|
|
1175
|
+
await pending;
|
|
1176
|
+
}
|
|
1080
1177
|
}
|
|
1081
1178
|
break;
|
|
1082
1179
|
}
|
|
1083
1180
|
case "smartWait": {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
let timer;
|
|
1099
|
-
const observer = new MutationObserver(() => {
|
|
1100
|
-
clearTimeout(timer);
|
|
1101
|
-
timer = setTimeout(() => {
|
|
1102
|
-
observer.disconnect();
|
|
1103
|
-
resolve2(true);
|
|
1104
|
-
}, 500);
|
|
1105
|
-
});
|
|
1106
|
-
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
1181
|
+
let conditionPromise;
|
|
1182
|
+
switch (action.until) {
|
|
1183
|
+
case "networkIdle":
|
|
1184
|
+
conditionPromise = this.page.waitForLoadState("networkidle", { timeout: action.timeout });
|
|
1185
|
+
break;
|
|
1186
|
+
case "selector":
|
|
1187
|
+
conditionPromise = action.selector ? this.page.locator(action.selector).first().waitFor({ state: "visible", timeout: action.timeout }) : Promise.resolve();
|
|
1188
|
+
break;
|
|
1189
|
+
case "domStable":
|
|
1190
|
+
conditionPromise = this.page.waitForFunction(
|
|
1191
|
+
() => new Promise((resolve2) => {
|
|
1192
|
+
let timer;
|
|
1193
|
+
const observer = new MutationObserver(() => {
|
|
1194
|
+
clearTimeout(timer);
|
|
1107
1195
|
timer = setTimeout(() => {
|
|
1108
1196
|
observer.disconnect();
|
|
1109
1197
|
resolve2(true);
|
|
1110
1198
|
}, 500);
|
|
1111
|
-
})
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
toggle = !toggle;
|
|
1125
|
-
await new Promise((r) => setTimeout(r, REPAINT_INTERVAL_MS));
|
|
1126
|
-
}
|
|
1127
|
-
})();
|
|
1128
|
-
await conditionPromise;
|
|
1129
|
-
waitDone = true;
|
|
1130
|
-
await repaintLoop;
|
|
1131
|
-
} finally {
|
|
1132
|
-
this.isWaitingPhase = false;
|
|
1133
|
-
this.currentDisplaySpeed = void 0;
|
|
1199
|
+
});
|
|
1200
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
1201
|
+
timer = setTimeout(() => {
|
|
1202
|
+
observer.disconnect();
|
|
1203
|
+
resolve2(true);
|
|
1204
|
+
}, 500);
|
|
1205
|
+
}),
|
|
1206
|
+
void 0,
|
|
1207
|
+
{ timeout: action.timeout }
|
|
1208
|
+
);
|
|
1209
|
+
break;
|
|
1210
|
+
default:
|
|
1211
|
+
conditionPromise = Promise.resolve();
|
|
1134
1212
|
}
|
|
1213
|
+
await this.waitForConditionWithCapture(conditionPromise, action.displaySpeed);
|
|
1135
1214
|
break;
|
|
1136
1215
|
}
|
|
1137
1216
|
}
|
|
@@ -1906,6 +1985,44 @@ import sharp5 from "sharp";
|
|
|
1906
1985
|
function escapeXml(s) {
|
|
1907
1986
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1908
1987
|
}
|
|
1988
|
+
function isCJK(ch) {
|
|
1989
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1990
|
+
return code >= 4352 && code <= 4607 || // 한글 자모
|
|
1991
|
+
code >= 11904 && code <= 40959 || // CJK 부수, 한자
|
|
1992
|
+
code >= 44032 && code <= 55215 || // 한글 음절
|
|
1993
|
+
code >= 63744 && code <= 64255 || // CJK 호환 한자
|
|
1994
|
+
code >= 65072 && code <= 65103 || // CJK 호환 형태
|
|
1995
|
+
code >= 65280 && code <= 65519 || // Full-width 문자
|
|
1996
|
+
code >= 12288 && code <= 12543 || // CJK 기호, 히라가나, 가타카나
|
|
1997
|
+
code >= 12784 && code <= 12799 || // 가타카나 확장
|
|
1998
|
+
code >= 131072 && code <= 195103;
|
|
1999
|
+
}
|
|
2000
|
+
function displayWidth(text) {
|
|
2001
|
+
let w = 0;
|
|
2002
|
+
for (const ch of text) {
|
|
2003
|
+
w += isCJK(ch) ? 1.7 : 1;
|
|
2004
|
+
}
|
|
2005
|
+
return w;
|
|
2006
|
+
}
|
|
2007
|
+
function wrapText(text, maxWidth) {
|
|
2008
|
+
if (displayWidth(text) <= maxWidth) return [text];
|
|
2009
|
+
const lines = [];
|
|
2010
|
+
let current = "";
|
|
2011
|
+
let currentWidth = 0;
|
|
2012
|
+
for (const ch of text) {
|
|
2013
|
+
const chWidth = isCJK(ch) ? 1.7 : 1;
|
|
2014
|
+
if (currentWidth + chWidth > maxWidth && current.length > 0) {
|
|
2015
|
+
lines.push(current);
|
|
2016
|
+
current = ch;
|
|
2017
|
+
currentWidth = chWidth;
|
|
2018
|
+
} else {
|
|
2019
|
+
current += ch;
|
|
2020
|
+
currentWidth += chWidth;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
if (current.length > 0) lines.push(current);
|
|
2024
|
+
return lines;
|
|
2025
|
+
}
|
|
1909
2026
|
function buildSessions(keystrokes) {
|
|
1910
2027
|
const hasSessionIds = keystrokes.some((k) => k.sessionId !== void 0);
|
|
1911
2028
|
if (!hasSessionIds) {
|
|
@@ -1939,16 +2056,22 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
|
|
|
1939
2056
|
const lineGap = Math.round(fontSize * 0.45);
|
|
1940
2057
|
const charWidth = fontSize * 0.615;
|
|
1941
2058
|
const maxHudWidth = frameWidth - 60 * dpr;
|
|
1942
|
-
const
|
|
1943
|
-
const
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2059
|
+
const maxDisplayWidth = Math.max(10, (maxHudWidth - hudPadH * 2) / charWidth);
|
|
2060
|
+
const wrappedLines = [];
|
|
2061
|
+
sessions.forEach((text, sIdx) => {
|
|
2062
|
+
const wrapped = wrapText(text, maxDisplayWidth);
|
|
2063
|
+
for (const line of wrapped) {
|
|
2064
|
+
wrappedLines.push({ text: line, sessionIdx: sIdx });
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
const lines = wrappedLines.map((l) => l.text);
|
|
2068
|
+
const totalLineCount = lines.length;
|
|
2069
|
+
const maxLineDisplayWidth = Math.max(...lines.map((l) => displayWidth(l)));
|
|
1947
2070
|
const hudWidth = Math.min(
|
|
1948
|
-
Math.ceil(
|
|
2071
|
+
Math.ceil(maxLineDisplayWidth * charWidth) + hudPadH * 2,
|
|
1949
2072
|
maxHudWidth
|
|
1950
2073
|
);
|
|
1951
|
-
const hudHeight = Math.ceil(fontSize *
|
|
2074
|
+
const hudHeight = Math.ceil(fontSize * totalLineCount + lineGap * (totalLineCount - 1) + hudPadV * 2);
|
|
1952
2075
|
const margin = 30 * dpr;
|
|
1953
2076
|
const hudY = frameHeight - hudHeight - margin;
|
|
1954
2077
|
let hudX;
|
|
@@ -1963,18 +2086,20 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
|
|
|
1963
2086
|
default:
|
|
1964
2087
|
hudX = Math.round((frameWidth - hudWidth) / 2);
|
|
1965
2088
|
}
|
|
1966
|
-
const
|
|
1967
|
-
const
|
|
2089
|
+
const SESSION_OPACITY_FACTORS = [0.45, 0.7, 1];
|
|
2090
|
+
const sessionOpacities = SESSION_OPACITY_FACTORS.slice(-lineCount);
|
|
1968
2091
|
const rx = (8 * dpr).toFixed(1);
|
|
1969
2092
|
const boxOp = (globalOpacity * 0.92).toFixed(3);
|
|
1970
2093
|
const textX = hudX + hudPadH;
|
|
1971
2094
|
const baselineY = hudY + hudPadV + fontSize * 0.82;
|
|
1972
|
-
const textElements =
|
|
1973
|
-
const
|
|
2095
|
+
const textElements = wrappedLines.map(({ text, sessionIdx }, i) => {
|
|
2096
|
+
const sessionPos = sessions.length <= 3 ? sessionIdx : sessionIdx - (sessions.length - 3);
|
|
2097
|
+
const opFactor = sessionOpacities[Math.max(0, sessionPos)] ?? 1;
|
|
2098
|
+
const op = (globalOpacity * opFactor).toFixed(3);
|
|
1974
2099
|
const lineY = baselineY + i * (fontSize + lineGap);
|
|
1975
2100
|
return `<text x="${textX}" y="${lineY}"
|
|
1976
2101
|
font-family="monospace, Menlo, Consolas" font-size="${fontSize}"
|
|
1977
|
-
fill="${config.textColor}" opacity="${op}">${escapeXml(
|
|
2102
|
+
fill="${config.textColor}" opacity="${op}">${escapeXml(text)}</text>`;
|
|
1978
2103
|
}).join("\n ");
|
|
1979
2104
|
const hudSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
|
|
1980
2105
|
<rect x="${hudX}" y="${hudY}" width="${hudWidth}" height="${hudHeight}"
|
|
@@ -3454,6 +3579,7 @@ var StreamingSession = class extends EventEmitter {
|
|
|
3454
3579
|
|
|
3455
3580
|
// src/cli/index.ts
|
|
3456
3581
|
import { writeFile as writeFile2, mkdir as mkdir2, access, copyFile, readFile as readFile3 } from "fs/promises";
|
|
3582
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3457
3583
|
import { join as join2, resolve, dirname } from "path";
|
|
3458
3584
|
import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
|
|
3459
3585
|
import { homedir } from "os";
|
|
@@ -3723,152 +3849,34 @@ program.command("demo").description("Record a demo video of the Clipwise showcas
|
|
|
3723
3849
|
).action(async (options) => {
|
|
3724
3850
|
const spinner = ora();
|
|
3725
3851
|
try {
|
|
3852
|
+
const { loadScenario: loadScenario2 } = await Promise.resolve().then(() => (init_parser(), parser_exports));
|
|
3853
|
+
const demoYamlCandidates = [
|
|
3854
|
+
resolve(fileURLToPath2(import.meta.url), "../../..", "examples", "demo.yaml"),
|
|
3855
|
+
// from dist/cli/
|
|
3856
|
+
resolve(fileURLToPath2(import.meta.url), "../..", "examples", "demo.yaml")
|
|
3857
|
+
// from src/cli/
|
|
3858
|
+
];
|
|
3859
|
+
let demoYamlPath = "";
|
|
3860
|
+
for (const candidate of demoYamlCandidates) {
|
|
3861
|
+
if (existsSync2(candidate)) {
|
|
3862
|
+
demoYamlPath = candidate;
|
|
3863
|
+
break;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
if (!demoYamlPath) {
|
|
3867
|
+
throw new Error("Cannot find examples/demo.yaml. Run from the project root.");
|
|
3868
|
+
}
|
|
3869
|
+
const scenario = await loadScenario2(demoYamlPath);
|
|
3870
|
+
scenario.output.format = options.format;
|
|
3871
|
+
scenario.output.outputDir = options.output;
|
|
3872
|
+
scenario.output.filename = `clipwise-demo-${options.device}`;
|
|
3726
3873
|
const demoUrl = options.url ?? "https://kwakseongjae.github.io/clipwise/demo/";
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
const vpHeight = isMobile ? 844 : isTablet ? 768 : 800;
|
|
3732
|
-
const outWidth = isMobile ? 540 : 1280;
|
|
3733
|
-
const outHeight = isMobile ? 1080 : isTablet ? 960 : 800;
|
|
3734
|
-
const { parseScenario: parseScenario2 } = await Promise.resolve().then(() => (init_parser(), parser_exports));
|
|
3735
|
-
const yaml = await import("yaml");
|
|
3736
|
-
const steps = [
|
|
3737
|
-
{
|
|
3738
|
-
name: "Load dashboard",
|
|
3739
|
-
captureDelay: 100,
|
|
3740
|
-
holdDuration: 1e3,
|
|
3741
|
-
actions: [
|
|
3742
|
-
{ action: "navigate", url: demoUrl, waitUntil: "load" },
|
|
3743
|
-
{ action: "waitForSelector", selector: "#stat-users", state: "visible", timeout: 15e3 }
|
|
3744
|
-
]
|
|
3745
|
-
},
|
|
3746
|
-
{
|
|
3747
|
-
name: "Hover Users stat",
|
|
3748
|
-
captureDelay: 50,
|
|
3749
|
-
holdDuration: 700,
|
|
3750
|
-
actions: [{ action: "hover", selector: "#stat-users" }]
|
|
3751
|
-
},
|
|
3752
|
-
{
|
|
3753
|
-
name: "Hover Revenue",
|
|
3754
|
-
captureDelay: 50,
|
|
3755
|
-
holdDuration: 700,
|
|
3756
|
-
actions: [{ action: "hover", selector: "#stat-revenue" }]
|
|
3757
|
-
},
|
|
3758
|
-
{
|
|
3759
|
-
name: "Switch chart",
|
|
3760
|
-
captureDelay: 50,
|
|
3761
|
-
holdDuration: 800,
|
|
3762
|
-
actions: [{ action: "click", selector: "#tab-monthly" }]
|
|
3763
|
-
},
|
|
3764
|
-
{
|
|
3765
|
-
name: "Search",
|
|
3766
|
-
captureDelay: 50,
|
|
3767
|
-
holdDuration: 800,
|
|
3768
|
-
actions: [
|
|
3769
|
-
{ action: "click", selector: "#search-input" },
|
|
3770
|
-
{ action: "type", selector: "#search-input", text: "conversion", delay: 18 }
|
|
3771
|
-
]
|
|
3772
|
-
},
|
|
3773
|
-
...!isMobile ? [{
|
|
3774
|
-
name: "Scroll to projects",
|
|
3775
|
-
captureDelay: 100,
|
|
3776
|
-
holdDuration: 600,
|
|
3777
|
-
actions: [{ action: "scroll", y: 420, smooth: true }]
|
|
3778
|
-
}] : [{
|
|
3779
|
-
name: "Scroll to chart",
|
|
3780
|
-
captureDelay: 100,
|
|
3781
|
-
holdDuration: 600,
|
|
3782
|
-
actions: [{ action: "scroll", y: 250, smooth: true }]
|
|
3783
|
-
}],
|
|
3784
|
-
{
|
|
3785
|
-
name: "Hover row",
|
|
3786
|
-
captureDelay: 50,
|
|
3787
|
-
holdDuration: 600,
|
|
3788
|
-
actions: [{ action: "hover", selector: "#row-1" }]
|
|
3789
|
-
},
|
|
3790
|
-
{
|
|
3791
|
-
name: "Open modal",
|
|
3792
|
-
captureDelay: 100,
|
|
3793
|
-
holdDuration: 800,
|
|
3794
|
-
actions: [{ action: "click", selector: "#btn-new-project" }]
|
|
3795
|
-
},
|
|
3796
|
-
{
|
|
3797
|
-
name: "Type name",
|
|
3798
|
-
captureDelay: 50,
|
|
3799
|
-
holdDuration: 600,
|
|
3800
|
-
actions: [
|
|
3801
|
-
{ action: "click", selector: "#project-name" },
|
|
3802
|
-
{ action: "type", selector: "#project-name", text: "Clipwise Demo", delay: 20 }
|
|
3803
|
-
]
|
|
3804
|
-
},
|
|
3805
|
-
{
|
|
3806
|
-
name: "Type desc",
|
|
3807
|
-
captureDelay: 50,
|
|
3808
|
-
holdDuration: 600,
|
|
3809
|
-
actions: [
|
|
3810
|
-
{ action: "click", selector: "#project-desc" },
|
|
3811
|
-
{ action: "type", selector: "#project-desc", text: "Automated screen recording", delay: 16 }
|
|
3812
|
-
]
|
|
3813
|
-
},
|
|
3814
|
-
{
|
|
3815
|
-
name: "Create",
|
|
3816
|
-
captureDelay: 100,
|
|
3817
|
-
holdDuration: 1e3,
|
|
3818
|
-
actions: [{ action: "click", selector: "#btn-create" }]
|
|
3874
|
+
if (scenario.steps.length > 0) {
|
|
3875
|
+
const navAction = scenario.steps[0].actions.find((a) => a.action === "navigate");
|
|
3876
|
+
if (navAction && "url" in navAction) {
|
|
3877
|
+
navAction.url = demoUrl;
|
|
3819
3878
|
}
|
|
3820
|
-
|
|
3821
|
-
const scenarioObj = {
|
|
3822
|
-
name: `Clipwise Demo (${device})`,
|
|
3823
|
-
viewport: { width: vpWidth, height: vpHeight },
|
|
3824
|
-
effects: {
|
|
3825
|
-
zoom: {
|
|
3826
|
-
enabled: true,
|
|
3827
|
-
scale: 1.8,
|
|
3828
|
-
duration: 500,
|
|
3829
|
-
autoZoom: { followCursor: true, maxScale: 2 }
|
|
3830
|
-
},
|
|
3831
|
-
cursor: {
|
|
3832
|
-
enabled: true,
|
|
3833
|
-
size: isMobile ? 16 : 20,
|
|
3834
|
-
clickEffect: true,
|
|
3835
|
-
highlight: true,
|
|
3836
|
-
trail: !isMobile,
|
|
3837
|
-
trailLength: 6
|
|
3838
|
-
},
|
|
3839
|
-
background: {
|
|
3840
|
-
type: "gradient",
|
|
3841
|
-
value: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)",
|
|
3842
|
-
padding: isMobile ? 60 : 48,
|
|
3843
|
-
borderRadius: 14,
|
|
3844
|
-
shadow: true
|
|
3845
|
-
},
|
|
3846
|
-
deviceFrame: { enabled: true, type: device, darkMode: true },
|
|
3847
|
-
keystroke: {
|
|
3848
|
-
enabled: true,
|
|
3849
|
-
position: "bottom-center",
|
|
3850
|
-
fontSize: isMobile ? 14 : 16
|
|
3851
|
-
},
|
|
3852
|
-
watermark: {
|
|
3853
|
-
enabled: true,
|
|
3854
|
-
text: "Clipwise",
|
|
3855
|
-
position: "bottom-right",
|
|
3856
|
-
opacity: 0.35,
|
|
3857
|
-
fontSize: 13
|
|
3858
|
-
}
|
|
3859
|
-
},
|
|
3860
|
-
output: {
|
|
3861
|
-
format: options.format,
|
|
3862
|
-
width: outWidth,
|
|
3863
|
-
height: outHeight,
|
|
3864
|
-
fps: 30,
|
|
3865
|
-
preset: "social",
|
|
3866
|
-
outputDir: options.output,
|
|
3867
|
-
filename: `clipwise-demo-${device}`
|
|
3868
|
-
},
|
|
3869
|
-
steps
|
|
3870
|
-
};
|
|
3871
|
-
const scenario = parseScenario2(yaml.stringify(scenarioObj));
|
|
3879
|
+
}
|
|
3872
3880
|
spinner.succeed(`Demo scenario ready: ${chalk.bold(scenario.name)}`);
|
|
3873
3881
|
spinner.start("Checking browser...");
|
|
3874
3882
|
try {
|
|
@@ -3890,7 +3898,7 @@ program.command("demo").description("Record a demo video of the Clipwise showcas
|
|
|
3890
3898
|
await mkdir2(options.output, { recursive: true });
|
|
3891
3899
|
const demoRenderer = new CanvasRenderer(scenario.effects, scenario.output, scenario.steps);
|
|
3892
3900
|
const ext = scenario.output.format === "gif" ? "gif" : "mp4";
|
|
3893
|
-
const outputPath = join2(options.output, `clipwise-demo-${device}.${ext}`);
|
|
3901
|
+
const outputPath = join2(options.output, `clipwise-demo-${options.device}.${ext}`);
|
|
3894
3902
|
const isConcurrentEligible = ext === "mp4" && demoRenderer.canStreamOnline();
|
|
3895
3903
|
if (isConcurrentEligible) {
|
|
3896
3904
|
const recorder = new ClipwiseRecorder();
|