clipwise 0.7.1 → 0.9.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 +130 -15
- package/README.md +138 -22
- package/dist/cli/index.js +2982 -1936
- package/dist/compose/frame-worker.js +146 -50
- package/dist/index.d.ts +6531 -331
- package/dist/index.js +943 -133
- package/package.json +3 -2
- package/skills/clipwise.md +112 -9
- package/templates/motion/feature-callout.html +81 -0
- package/templates/motion/intro-title.html +146 -0
- package/templates/motion/kinetic-type.html +205 -0
- package/templates/motion/vignette.html +288 -0
|
@@ -7,13 +7,11 @@ import sharp7 from "sharp";
|
|
|
7
7
|
|
|
8
8
|
// src/effects/frame.ts
|
|
9
9
|
import sharp from "sharp";
|
|
10
|
-
var TITLE_BAR_HEIGHT =
|
|
11
|
-
var
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var
|
|
15
|
-
var ADDRESS_BAR_HEIGHT = 24;
|
|
16
|
-
var ADDRESS_BAR_MARGIN = 70;
|
|
10
|
+
var TITLE_BAR_HEIGHT = 48;
|
|
11
|
+
var TRAFFIC_LIGHT_RADIUS = 6.5;
|
|
12
|
+
var TRAFFIC_LIGHTS_START_X = 22;
|
|
13
|
+
var TRAFFIC_LIGHT_GAP = 20;
|
|
14
|
+
var URL_PILL_HEIGHT = 30;
|
|
17
15
|
var IPHONE_BEZEL = { sides: 12, top: 50, bottom: 34 };
|
|
18
16
|
var IPHONE_OUTER_RADIUS = 47;
|
|
19
17
|
var IPHONE_INNER_RADIUS = 39;
|
|
@@ -26,38 +24,89 @@ var ANDROID_BEZEL = { sides: 8, top: 32, bottom: 20 };
|
|
|
26
24
|
var ANDROID_OUTER_RADIUS = 35;
|
|
27
25
|
var ANDROID_INNER_RADIUS = 30;
|
|
28
26
|
var ANDROID_CAMERA_RADIUS = 6;
|
|
29
|
-
function buildBrowserChromeSvg(width, darkMode, dpr = 1) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
function buildBrowserChromeSvg(width, darkMode, dpr = 1, url = "localhost") {
|
|
28
|
+
const c = darkMode ? {
|
|
29
|
+
bgTop: "#3c3c3e",
|
|
30
|
+
bgBottom: "#343436",
|
|
31
|
+
border: "#232325",
|
|
32
|
+
pillBg: "#28282a",
|
|
33
|
+
pillBorder: "#48484b",
|
|
34
|
+
text: "#d8d8da",
|
|
35
|
+
icon: "#a8a8ac",
|
|
36
|
+
iconDim: "#5f5f63"
|
|
37
|
+
} : {
|
|
38
|
+
bgTop: "#f8f7f6",
|
|
39
|
+
bgBottom: "#eeedeb",
|
|
40
|
+
border: "#d8d6d3",
|
|
41
|
+
pillBg: "#ffffff",
|
|
42
|
+
pillBorder: "#dedcd9",
|
|
43
|
+
text: "#3a3a3c",
|
|
44
|
+
icon: "#6f6f72",
|
|
45
|
+
iconDim: "#bdbdc0"
|
|
46
|
+
};
|
|
47
|
+
const h = TITLE_BAR_HEIGHT * dpr;
|
|
48
|
+
const midY = h / 2;
|
|
36
49
|
const tlR = TRAFFIC_LIGHT_RADIUS * dpr;
|
|
37
50
|
const tlStartX = TRAFFIC_LIGHTS_START_X * dpr;
|
|
38
51
|
const tlGap = TRAFFIC_LIGHT_GAP * dpr;
|
|
39
|
-
const
|
|
40
|
-
const aBarMargin = ADDRESS_BAR_MARGIN * dpr;
|
|
41
|
-
const fontSize = 12 * dpr;
|
|
52
|
+
const s = dpr;
|
|
42
53
|
const trafficLights = [
|
|
43
|
-
{ cx: tlStartX, fill: "#ff5f57" },
|
|
44
|
-
{ cx: tlStartX + tlGap, fill: "#febc2e" },
|
|
45
|
-
{ cx: tlStartX + tlGap * 2, fill: "#28c840" }
|
|
46
|
-
].map(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
{ cx: tlStartX, fill: "#ff5f57", stroke: "#e0443e" },
|
|
55
|
+
{ cx: tlStartX + tlGap, fill: "#febc2e", stroke: "#d89e24" },
|
|
56
|
+
{ cx: tlStartX + tlGap * 2, fill: "#28c840", stroke: "#1ea133" }
|
|
57
|
+
].map((l) => `<circle cx="${l.cx}" cy="${midY}" r="${tlR}" fill="${l.fill}" stroke="${l.stroke}" stroke-width="${0.5 * s}"/>`).join("\n ");
|
|
58
|
+
const navX = tlStartX + tlGap * 2 + 34 * s;
|
|
59
|
+
const back = `<path d="M ${navX + 4 * s} ${midY - 6 * s} l ${-6 * s} ${6 * s} l ${6 * s} ${6 * s}"
|
|
60
|
+
fill="none" stroke="${c.icon}" stroke-width="${1.8 * s}" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
61
|
+
const fwdX = navX + 28 * s;
|
|
62
|
+
const forward = `<path d="M ${fwdX - 4 * s} ${midY - 6 * s} l ${6 * s} ${6 * s} l ${-6 * s} ${6 * s}"
|
|
63
|
+
fill="none" stroke="${c.iconDim}" stroke-width="${1.8 * s}" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
64
|
+
const relX = fwdX + 28 * s;
|
|
65
|
+
const reload = `<path d="M ${relX + 6 * s} ${midY - 3.5 * s} a ${6 * s} ${6 * s} 0 1 0 ${1.2 * s} ${5.5 * s}"
|
|
66
|
+
fill="none" stroke="${c.icon}" stroke-width="${1.7 * s}" stroke-linecap="round"/>
|
|
67
|
+
<path d="M ${relX + 6.4 * s} ${midY - 7.5 * s} l ${0.4 * s} ${4.6 * s} l ${-4.6 * s} ${-0.4 * s} z" fill="${c.icon}"/>`;
|
|
68
|
+
const fontSize = 12.5 * dpr;
|
|
69
|
+
const pillH = URL_PILL_HEIGHT * dpr;
|
|
70
|
+
const pillW = Math.max(200 * s, Math.min(width * 0.42, 520 * s));
|
|
71
|
+
const pillX = (width - pillW) / 2;
|
|
72
|
+
const pillY = midY - pillH / 2;
|
|
73
|
+
const textW = url.length * fontSize * 0.56;
|
|
74
|
+
const lockX = width / 2 - textW / 2 - 16 * s;
|
|
75
|
+
const lockY = midY - 5 * s;
|
|
76
|
+
const padlock = `
|
|
77
|
+
<rect x="${lockX}" y="${lockY + 4 * s}" width="${9 * s}" height="${7 * s}" rx="${1.5 * s}" fill="${c.icon}"/>
|
|
78
|
+
<path d="M ${lockX + 2 * s} ${lockY + 4 * s} v ${-2 * s} a ${2.5 * s} ${2.5 * s} 0 0 1 ${5 * s} 0 v ${2 * s}"
|
|
79
|
+
fill="none" stroke="${c.icon}" stroke-width="${1.4 * s}"/>`;
|
|
80
|
+
const dotsX = width - 26 * s;
|
|
81
|
+
const dots = [-4.5, 0, 4.5].map((dy) => `<circle cx="${dotsX}" cy="${midY + dy * s}" r="${1.6 * s}" fill="${c.icon}"/>`).join("");
|
|
82
|
+
const avatar = `
|
|
83
|
+
<circle cx="${width - 56 * s}" cy="${midY}" r="${11 * s}" fill="url(#cwAvatar)"/>
|
|
84
|
+
<text x="${width - 56 * s}" y="${midY + 4 * s}" text-anchor="middle"
|
|
85
|
+
font-family="system-ui, -apple-system, sans-serif" font-size="${10.5 * dpr}" font-weight="600" fill="#ffffff">S</text>`;
|
|
86
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${h}">
|
|
87
|
+
<defs>
|
|
88
|
+
<linearGradient id="cwChromeBg" x1="0" y1="0" x2="0" y2="1">
|
|
89
|
+
<stop offset="0" stop-color="${c.bgTop}"/>
|
|
90
|
+
<stop offset="1" stop-color="${c.bgBottom}"/>
|
|
91
|
+
</linearGradient>
|
|
92
|
+
<linearGradient id="cwAvatar" x1="0" y1="0" x2="1" y2="1">
|
|
93
|
+
<stop offset="0" stop-color="#818cf8"/>
|
|
94
|
+
<stop offset="1" stop-color="#6366f1"/>
|
|
95
|
+
</linearGradient>
|
|
96
|
+
</defs>
|
|
97
|
+
<rect width="${width}" height="${h}" fill="url(#cwChromeBg)"/>
|
|
98
|
+
<rect y="${h - s}" width="${width}" height="${s}" fill="${c.border}"/>
|
|
54
99
|
${trafficLights}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
${back}
|
|
101
|
+
${forward}
|
|
102
|
+
${reload}
|
|
103
|
+
<rect x="${pillX}" y="${pillY}" width="${pillW}" height="${pillH}"
|
|
104
|
+
rx="${pillH / 2}" ry="${pillH / 2}" fill="${c.pillBg}" stroke="${c.pillBorder}" stroke-width="${s}"/>
|
|
105
|
+
${padlock}
|
|
106
|
+
<text x="${width / 2 + 7 * s}" y="${midY + fontSize * 0.35}" text-anchor="middle"
|
|
107
|
+
font-family="system-ui, -apple-system, sans-serif" font-size="${fontSize}" fill="${c.text}">${url}</text>
|
|
108
|
+
${dots}
|
|
109
|
+
${avatar}
|
|
61
110
|
</svg>`;
|
|
62
111
|
}
|
|
63
112
|
function buildIPhoneFrameSvg(totalWidth, totalHeight, screenWidth, screenHeight, darkMode, dpr = 1) {
|
|
@@ -203,9 +252,9 @@ async function applyMobileFrame(frameBuffer, deviceType, darkMode, frameWidth, f
|
|
|
203
252
|
{ input: maskedScreen, left: bezel.sides, top: bezel.top }
|
|
204
253
|
]).png().toBuffer();
|
|
205
254
|
}
|
|
206
|
-
async function buildBrowserChromeBuffer(viewportWidth, darkMode, dpr = 1) {
|
|
255
|
+
async function buildBrowserChromeBuffer(viewportWidth, darkMode, dpr = 1, url = "localhost") {
|
|
207
256
|
const tbarH = TITLE_BAR_HEIGHT * dpr;
|
|
208
|
-
const chromeSvg = buildBrowserChromeSvg(viewportWidth, darkMode, dpr);
|
|
257
|
+
const chromeSvg = buildBrowserChromeSvg(viewportWidth, darkMode, dpr, url);
|
|
209
258
|
return sharp(Buffer.from(chromeSvg)).resize(viewportWidth, tbarH).png().toBuffer();
|
|
210
259
|
}
|
|
211
260
|
async function applyDeviceFrame(frameBuffer, config, frameWidth, frameHeight, dpr = 1) {
|
|
@@ -214,7 +263,7 @@ async function applyDeviceFrame(frameBuffer, config, frameWidth, frameHeight, dp
|
|
|
214
263
|
case "browser": {
|
|
215
264
|
const tbarH = TITLE_BAR_HEIGHT * dpr;
|
|
216
265
|
const totalHeight = frameHeight + tbarH;
|
|
217
|
-
const chromeSvg = buildBrowserChromeSvg(frameWidth, config.darkMode, dpr);
|
|
266
|
+
const chromeSvg = buildBrowserChromeSvg(frameWidth, config.darkMode, dpr, config.url);
|
|
218
267
|
const chromeBuffer = Buffer.from(chromeSvg);
|
|
219
268
|
const canvas = await sharp({
|
|
220
269
|
create: {
|
|
@@ -460,6 +509,44 @@ import sharp5 from "sharp";
|
|
|
460
509
|
function escapeXml(s) {
|
|
461
510
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
462
511
|
}
|
|
512
|
+
function isCJK(ch) {
|
|
513
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
514
|
+
return code >= 4352 && code <= 4607 || // 한글 자모
|
|
515
|
+
code >= 11904 && code <= 40959 || // CJK 부수, 한자
|
|
516
|
+
code >= 44032 && code <= 55215 || // 한글 음절
|
|
517
|
+
code >= 63744 && code <= 64255 || // CJK 호환 한자
|
|
518
|
+
code >= 65072 && code <= 65103 || // CJK 호환 형태
|
|
519
|
+
code >= 65280 && code <= 65519 || // Full-width 문자
|
|
520
|
+
code >= 12288 && code <= 12543 || // CJK 기호, 히라가나, 가타카나
|
|
521
|
+
code >= 12784 && code <= 12799 || // 가타카나 확장
|
|
522
|
+
code >= 131072 && code <= 195103;
|
|
523
|
+
}
|
|
524
|
+
function displayWidth(text) {
|
|
525
|
+
let w = 0;
|
|
526
|
+
for (const ch of text) {
|
|
527
|
+
w += isCJK(ch) ? 1.7 : 1;
|
|
528
|
+
}
|
|
529
|
+
return w;
|
|
530
|
+
}
|
|
531
|
+
function wrapText(text, maxWidth) {
|
|
532
|
+
if (displayWidth(text) <= maxWidth) return [text];
|
|
533
|
+
const lines = [];
|
|
534
|
+
let current = "";
|
|
535
|
+
let currentWidth = 0;
|
|
536
|
+
for (const ch of text) {
|
|
537
|
+
const chWidth = isCJK(ch) ? 1.7 : 1;
|
|
538
|
+
if (currentWidth + chWidth > maxWidth && current.length > 0) {
|
|
539
|
+
lines.push(current);
|
|
540
|
+
current = ch;
|
|
541
|
+
currentWidth = chWidth;
|
|
542
|
+
} else {
|
|
543
|
+
current += ch;
|
|
544
|
+
currentWidth += chWidth;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (current.length > 0) lines.push(current);
|
|
548
|
+
return lines;
|
|
549
|
+
}
|
|
463
550
|
function buildSessions(keystrokes) {
|
|
464
551
|
const hasSessionIds = keystrokes.some((k) => k.sessionId !== void 0);
|
|
465
552
|
if (!hasSessionIds) {
|
|
@@ -493,16 +580,22 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
|
|
|
493
580
|
const lineGap = Math.round(fontSize * 0.45);
|
|
494
581
|
const charWidth = fontSize * 0.615;
|
|
495
582
|
const maxHudWidth = frameWidth - 60 * dpr;
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
583
|
+
const maxDisplayWidth = Math.max(10, (maxHudWidth - hudPadH * 2) / charWidth);
|
|
584
|
+
const wrappedLines = [];
|
|
585
|
+
sessions.forEach((text, sIdx) => {
|
|
586
|
+
const wrapped = wrapText(text, maxDisplayWidth);
|
|
587
|
+
for (const line of wrapped) {
|
|
588
|
+
wrappedLines.push({ text: line, sessionIdx: sIdx });
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
const lines = wrappedLines.map((l) => l.text);
|
|
592
|
+
const totalLineCount = lines.length;
|
|
593
|
+
const maxLineDisplayWidth = Math.max(...lines.map((l) => displayWidth(l)));
|
|
501
594
|
const hudWidth = Math.min(
|
|
502
|
-
Math.ceil(
|
|
595
|
+
Math.ceil(maxLineDisplayWidth * charWidth) + hudPadH * 2,
|
|
503
596
|
maxHudWidth
|
|
504
597
|
);
|
|
505
|
-
const hudHeight = Math.ceil(fontSize *
|
|
598
|
+
const hudHeight = Math.ceil(fontSize * totalLineCount + lineGap * (totalLineCount - 1) + hudPadV * 2);
|
|
506
599
|
const margin = 30 * dpr;
|
|
507
600
|
const hudY = frameHeight - hudHeight - margin;
|
|
508
601
|
let hudX;
|
|
@@ -517,18 +610,20 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
|
|
|
517
610
|
default:
|
|
518
611
|
hudX = Math.round((frameWidth - hudWidth) / 2);
|
|
519
612
|
}
|
|
520
|
-
const
|
|
521
|
-
const
|
|
613
|
+
const SESSION_OPACITY_FACTORS = [0.45, 0.7, 1];
|
|
614
|
+
const sessionOpacities = SESSION_OPACITY_FACTORS.slice(-lineCount);
|
|
522
615
|
const rx = (8 * dpr).toFixed(1);
|
|
523
616
|
const boxOp = (globalOpacity * 0.92).toFixed(3);
|
|
524
617
|
const textX = hudX + hudPadH;
|
|
525
618
|
const baselineY = hudY + hudPadV + fontSize * 0.82;
|
|
526
|
-
const textElements =
|
|
527
|
-
const
|
|
619
|
+
const textElements = wrappedLines.map(({ text, sessionIdx }, i) => {
|
|
620
|
+
const sessionPos = sessions.length <= 3 ? sessionIdx : sessionIdx - (sessions.length - 3);
|
|
621
|
+
const opFactor = sessionOpacities[Math.max(0, sessionPos)] ?? 1;
|
|
622
|
+
const op = (globalOpacity * opFactor).toFixed(3);
|
|
528
623
|
const lineY = baselineY + i * (fontSize + lineGap);
|
|
529
624
|
return `<text x="${textX}" y="${lineY}"
|
|
530
625
|
font-family="monospace, Menlo, Consolas" font-size="${fontSize}"
|
|
531
|
-
fill="${config.textColor}" opacity="${op}">${escapeXml(
|
|
626
|
+
fill="${config.textColor}" opacity="${op}">${escapeXml(text)}</text>`;
|
|
532
627
|
}).join("\n ");
|
|
533
628
|
const hudSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
|
|
534
629
|
<rect x="${hudX}" y="${hudY}" width="${hudWidth}" height="${hudHeight}"
|
|
@@ -597,7 +692,8 @@ async function buildStaticLayers(effects, output, viewportWidth, dpr) {
|
|
|
597
692
|
browserChromePng = await buildBrowserChromeBuffer(
|
|
598
693
|
viewportWidth,
|
|
599
694
|
effects.deviceFrame.darkMode,
|
|
600
|
-
dpr
|
|
695
|
+
dpr,
|
|
696
|
+
effects.deviceFrame.url
|
|
601
697
|
);
|
|
602
698
|
}
|
|
603
699
|
return {
|