clipwise 0.5.1 → 0.6.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 +112 -15
- package/README.md +111 -22
- package/dist/cli/index.js +312 -61
- package/dist/compose/frame-worker.js +2 -1
- package/dist/index.d.ts +1649 -186
- package/dist/index.js +269 -62
- package/package.json +2 -1
- package/skills/clipwise.md +373 -0
package/dist/index.js
CHANGED
|
@@ -105,12 +105,13 @@ var ClipwiseRecorder = class {
|
|
|
105
105
|
* each input field's text on a separate line. */
|
|
106
106
|
keystrokeSessionId = 0;
|
|
107
107
|
currentStepIndex = 0;
|
|
108
|
+
isScrolling = false;
|
|
108
109
|
cursorPosition = { x: 0, y: 0 };
|
|
109
110
|
viewport = { width: 1280, height: 800 };
|
|
110
111
|
deviceScaleFactor = 1;
|
|
111
112
|
isCapturing = false;
|
|
112
113
|
targetFps = 30;
|
|
113
|
-
cursorSpeed = "
|
|
114
|
+
cursorSpeed = "normal";
|
|
114
115
|
firstContentTimestamp = 0;
|
|
115
116
|
pendingResponsePromises = /* @__PURE__ */ new Map();
|
|
116
117
|
// ── 중복 프레임 제거 (Phase 1-A) ──────────────────────────────────────────
|
|
@@ -143,6 +144,7 @@ var ClipwiseRecorder = class {
|
|
|
143
144
|
this.keystrokeTimeline = [];
|
|
144
145
|
this.keystrokeSessionId = 0;
|
|
145
146
|
this.currentStepIndex = 0;
|
|
147
|
+
this.isScrolling = false;
|
|
146
148
|
this.cursorPosition = { x: 0, y: 0 };
|
|
147
149
|
this.isCapturing = false;
|
|
148
150
|
this.firstContentTimestamp = 0;
|
|
@@ -172,11 +174,12 @@ var ClipwiseRecorder = class {
|
|
|
172
174
|
} else {
|
|
173
175
|
this.lastFrameSignature = Buffer.from(signature);
|
|
174
176
|
const captureTime = Date.now();
|
|
175
|
-
|
|
177
|
+
const rawFrame = { buffer, timestamp: captureTime, stepIndex: this.currentStepIndex, isScrolling: this.isScrolling };
|
|
178
|
+
this.rawFrames.push(rawFrame);
|
|
176
179
|
this.dedupStats.stored++;
|
|
177
180
|
if (this.frameChannel && this.firstContentTimestamp > 0) {
|
|
178
181
|
const frame = this.buildFrameOnline(
|
|
179
|
-
|
|
182
|
+
rawFrame,
|
|
180
183
|
this.channelIndex++
|
|
181
184
|
);
|
|
182
185
|
this.frameChannel.push(frame);
|
|
@@ -381,7 +384,8 @@ var ClipwiseRecorder = class {
|
|
|
381
384
|
viewport: { ...this.viewport },
|
|
382
385
|
deviceScaleFactor: this.deviceScaleFactor,
|
|
383
386
|
stepIndex: raw.stepIndex,
|
|
384
|
-
keystrokes: frameKeystrokes.length > 0 ? frameKeystrokes : void 0
|
|
387
|
+
keystrokes: frameKeystrokes.length > 0 ? frameKeystrokes : void 0,
|
|
388
|
+
isScrolling: raw.isScrolling || void 0
|
|
385
389
|
};
|
|
386
390
|
}
|
|
387
391
|
/**
|
|
@@ -517,6 +521,7 @@ var ClipwiseRecorder = class {
|
|
|
517
521
|
case "scroll": {
|
|
518
522
|
const scrollTarget = action.selector ? await getElementCenter(this.page, action.selector, action.timeout) : null;
|
|
519
523
|
const scrollDistance = Math.abs(action.y) + Math.abs(action.x);
|
|
524
|
+
this.isScrolling = true;
|
|
520
525
|
if (action.smooth && scrollDistance > 0) {
|
|
521
526
|
const scrollSteps = Math.max(12, Math.round(scrollDistance / 25));
|
|
522
527
|
const yStep = action.y / scrollSteps;
|
|
@@ -556,6 +561,7 @@ var ClipwiseRecorder = class {
|
|
|
556
561
|
timestamp: Date.now()
|
|
557
562
|
});
|
|
558
563
|
}
|
|
564
|
+
this.isScrolling = false;
|
|
559
565
|
await this.waitWithRepaints(120);
|
|
560
566
|
break;
|
|
561
567
|
}
|
|
@@ -721,8 +727,9 @@ var ClipwiseRecorder = class {
|
|
|
721
727
|
viewport: { ...this.viewport },
|
|
722
728
|
deviceScaleFactor: this.deviceScaleFactor,
|
|
723
729
|
keystrokes: frameKeystrokes.length > 0 ? frameKeystrokes : void 0,
|
|
724
|
-
stepIndex: raw.stepIndex
|
|
730
|
+
stepIndex: raw.stepIndex,
|
|
725
731
|
// use per-frame step index captured at event time
|
|
732
|
+
isScrolling: raw.isScrolling || void 0
|
|
726
733
|
};
|
|
727
734
|
});
|
|
728
735
|
}
|
|
@@ -1573,7 +1580,8 @@ async function composeFrame(frame, effects, output, context) {
|
|
|
1573
1580
|
}
|
|
1574
1581
|
const scale = ctx.zoomScale;
|
|
1575
1582
|
if (effects.zoom.enabled && scale > 1) {
|
|
1576
|
-
const
|
|
1583
|
+
const followCursor = effects.zoom.autoZoom.followCursor;
|
|
1584
|
+
const rawFocus = followCursor ? frame.cursorPosition ?? frame.clickPosition ?? { x: frame.viewport.width / 2, y: frame.viewport.height / 2 } : frame.clickPosition ?? frame.cursorPosition ?? { x: frame.viewport.width / 2, y: frame.viewport.height / 2 };
|
|
1577
1585
|
const offset = getFrameOffset(effects.deviceFrame, dpr);
|
|
1578
1586
|
const focusPoint = {
|
|
1579
1587
|
x: rawFocus.x * dpr + offset.left,
|
|
@@ -1641,37 +1649,170 @@ async function composeFrame(frame, effects, output, context) {
|
|
|
1641
1649
|
|
|
1642
1650
|
// src/effects/transition.ts
|
|
1643
1651
|
import sharp8 from "sharp";
|
|
1652
|
+
async function decodeToRaw(buf, rawInfo, targetWidth, targetHeight) {
|
|
1653
|
+
const src = rawInfo ? sharp8(buf, { raw: { width: rawInfo.width, height: rawInfo.height, channels: rawInfo.channels } }) : sharp8(buf);
|
|
1654
|
+
const pipeline = targetWidth && targetHeight ? src.resize(targetWidth, targetHeight, { fit: "fill" }).ensureAlpha().raw() : src.ensureAlpha().raw();
|
|
1655
|
+
const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
|
1656
|
+
return { data: Buffer.from(data), width: info.width, height: info.height };
|
|
1657
|
+
}
|
|
1658
|
+
function returnRaw(buf, rawInfo, w, h) {
|
|
1659
|
+
if (rawInfo) return { buffer: buf, rawInfo };
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1644
1662
|
async function applyCrossfade(fromBuffer, toBuffer, progress, width, height, fromRawInfo, toRawInfo) {
|
|
1645
1663
|
const t = Math.max(0, Math.min(1, progress));
|
|
1646
1664
|
if (t <= 0) {
|
|
1647
|
-
const
|
|
1648
|
-
if (
|
|
1649
|
-
const
|
|
1650
|
-
return { buffer:
|
|
1665
|
+
const fast = returnRaw(fromBuffer, fromRawInfo, width, height);
|
|
1666
|
+
if (fast) return fast;
|
|
1667
|
+
const d = await decodeToRaw(fromBuffer, void 0);
|
|
1668
|
+
return { buffer: d.data, rawInfo: { width: d.width, height: d.height, channels: 4 } };
|
|
1651
1669
|
}
|
|
1652
1670
|
if (t >= 1) {
|
|
1653
|
-
const
|
|
1654
|
-
if (
|
|
1655
|
-
const
|
|
1656
|
-
return { buffer:
|
|
1671
|
+
const fast = returnRaw(toBuffer, toRawInfo, width, height);
|
|
1672
|
+
if (fast) return fast;
|
|
1673
|
+
const d = await decodeToRaw(toBuffer, void 0);
|
|
1674
|
+
return { buffer: d.data, rawInfo: { width: d.width, height: d.height, channels: 4 } };
|
|
1675
|
+
}
|
|
1676
|
+
const from = await decodeToRaw(fromBuffer, fromRawInfo);
|
|
1677
|
+
const to = await decodeToRaw(toBuffer, toRawInfo, from.width, from.height);
|
|
1678
|
+
const pixels = Buffer.alloc(from.data.length);
|
|
1679
|
+
for (let i = 0; i < from.data.length; i++) {
|
|
1680
|
+
pixels[i] = Math.round(from.data[i] * (1 - t) + to.data[i] * t);
|
|
1681
|
+
}
|
|
1682
|
+
return { buffer: pixels, rawInfo: { width: from.width, height: from.height, channels: 4 } };
|
|
1683
|
+
}
|
|
1684
|
+
async function applySlide(fromBuffer, toBuffer, progress, width, height, direction, fromRawInfo, toRawInfo) {
|
|
1685
|
+
const t = Math.max(0, Math.min(1, progress));
|
|
1686
|
+
if (t <= 0) {
|
|
1687
|
+
const fast = returnRaw(fromBuffer, fromRawInfo, width, height);
|
|
1688
|
+
if (fast) return fast;
|
|
1689
|
+
const d = await decodeToRaw(fromBuffer, void 0);
|
|
1690
|
+
return { buffer: d.data, rawInfo: { width: d.width, height: d.height, channels: 4 } };
|
|
1691
|
+
}
|
|
1692
|
+
if (t >= 1) {
|
|
1693
|
+
const fast = returnRaw(toBuffer, toRawInfo, width, height);
|
|
1694
|
+
if (fast) return fast;
|
|
1695
|
+
const d = await decodeToRaw(toBuffer, void 0);
|
|
1696
|
+
return { buffer: d.data, rawInfo: { width: d.width, height: d.height, channels: 4 } };
|
|
1697
|
+
}
|
|
1698
|
+
const from = await decodeToRaw(fromBuffer, fromRawInfo);
|
|
1699
|
+
const to = await decodeToRaw(toBuffer, toRawInfo, from.width, from.height);
|
|
1700
|
+
const w = from.width;
|
|
1701
|
+
const h = from.height;
|
|
1702
|
+
const pixels = Buffer.alloc(from.data.length);
|
|
1703
|
+
const eased = easeInOutCubic3(t);
|
|
1704
|
+
if (direction === "left") {
|
|
1705
|
+
const offsetX = Math.round(w * (1 - eased));
|
|
1706
|
+
for (let y = 0; y < h; y++) {
|
|
1707
|
+
for (let x = 0; x < w; x++) {
|
|
1708
|
+
const dstIdx = (y * w + x) * 4;
|
|
1709
|
+
const srcX = x + offsetX;
|
|
1710
|
+
if (srcX < w) {
|
|
1711
|
+
const srcIdx = (y * w + srcX) * 4;
|
|
1712
|
+
pixels[dstIdx] = to.data[srcIdx];
|
|
1713
|
+
pixels[dstIdx + 1] = to.data[srcIdx + 1];
|
|
1714
|
+
pixels[dstIdx + 2] = to.data[srcIdx + 2];
|
|
1715
|
+
pixels[dstIdx + 3] = to.data[srcIdx + 3];
|
|
1716
|
+
} else {
|
|
1717
|
+
const fromX = srcX - w;
|
|
1718
|
+
if (fromX < w) {
|
|
1719
|
+
const srcIdx = (y * w + fromX) * 4;
|
|
1720
|
+
pixels[dstIdx] = from.data[srcIdx];
|
|
1721
|
+
pixels[dstIdx + 1] = from.data[srcIdx + 1];
|
|
1722
|
+
pixels[dstIdx + 2] = from.data[srcIdx + 2];
|
|
1723
|
+
pixels[dstIdx + 3] = from.data[srcIdx + 3];
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
} else {
|
|
1729
|
+
const offsetY = Math.round(h * (1 - eased));
|
|
1730
|
+
for (let y = 0; y < h; y++) {
|
|
1731
|
+
for (let x = 0; x < w; x++) {
|
|
1732
|
+
const dstIdx = (y * w + x) * 4;
|
|
1733
|
+
const srcY = y + offsetY;
|
|
1734
|
+
if (srcY < h) {
|
|
1735
|
+
const srcIdx = (srcY * w + x) * 4;
|
|
1736
|
+
pixels[dstIdx] = to.data[srcIdx];
|
|
1737
|
+
pixels[dstIdx + 1] = to.data[srcIdx + 1];
|
|
1738
|
+
pixels[dstIdx + 2] = to.data[srcIdx + 2];
|
|
1739
|
+
pixels[dstIdx + 3] = to.data[srcIdx + 3];
|
|
1740
|
+
} else {
|
|
1741
|
+
const fromY = srcY - h;
|
|
1742
|
+
if (fromY < h) {
|
|
1743
|
+
const srcIdx = (fromY * w + x) * 4;
|
|
1744
|
+
pixels[dstIdx] = from.data[srcIdx];
|
|
1745
|
+
pixels[dstIdx + 1] = from.data[srcIdx + 1];
|
|
1746
|
+
pixels[dstIdx + 2] = from.data[srcIdx + 2];
|
|
1747
|
+
pixels[dstIdx + 3] = from.data[srcIdx + 3];
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return { buffer: pixels, rawInfo: { width: w, height: h, channels: 4 } };
|
|
1754
|
+
}
|
|
1755
|
+
async function applyBlur(fromBuffer, toBuffer, progress, width, height, fromRawInfo, toRawInfo) {
|
|
1756
|
+
const t = Math.max(0, Math.min(1, progress));
|
|
1757
|
+
if (t <= 0) {
|
|
1758
|
+
const fast = returnRaw(fromBuffer, fromRawInfo, width, height);
|
|
1759
|
+
if (fast) return fast;
|
|
1760
|
+
const d = await decodeToRaw(fromBuffer, void 0);
|
|
1761
|
+
return { buffer: d.data, rawInfo: { width: d.width, height: d.height, channels: 4 } };
|
|
1657
1762
|
}
|
|
1763
|
+
if (t >= 1) {
|
|
1764
|
+
const fast = returnRaw(toBuffer, toRawInfo, width, height);
|
|
1765
|
+
if (fast) return fast;
|
|
1766
|
+
const d = await decodeToRaw(toBuffer, void 0);
|
|
1767
|
+
return { buffer: d.data, rawInfo: { width: d.width, height: d.height, channels: 4 } };
|
|
1768
|
+
}
|
|
1769
|
+
const sigma = t * 20;
|
|
1658
1770
|
const fromSrc = fromRawInfo ? sharp8(fromBuffer, { raw: { width: fromRawInfo.width, height: fromRawInfo.height, channels: fromRawInfo.channels } }) : sharp8(fromBuffer);
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
1661
|
-
const
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
pixels[i] = Math.round(
|
|
1665
|
-
fromRaw.data[i] * (1 - t) + toRaw.data[i] * t
|
|
1666
|
-
);
|
|
1771
|
+
const blurredFrom = await fromSrc.blur(Math.max(0.3, sigma)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
1772
|
+
const to = await decodeToRaw(toBuffer, toRawInfo, blurredFrom.info.width, blurredFrom.info.height);
|
|
1773
|
+
const pixels = Buffer.alloc(blurredFrom.data.length);
|
|
1774
|
+
for (let i = 0; i < blurredFrom.data.length; i++) {
|
|
1775
|
+
pixels[i] = Math.round(blurredFrom.data[i] * (1 - t) + to.data[i] * t);
|
|
1667
1776
|
}
|
|
1668
1777
|
return {
|
|
1669
1778
|
buffer: pixels,
|
|
1670
|
-
rawInfo: { width:
|
|
1779
|
+
rawInfo: { width: blurredFrom.info.width, height: blurredFrom.info.height, channels: 4 }
|
|
1671
1780
|
};
|
|
1672
1781
|
}
|
|
1782
|
+
async function applyTransition(type, fromBuffer, toBuffer, progress, width, height, fromRawInfo, toRawInfo) {
|
|
1783
|
+
switch (type) {
|
|
1784
|
+
case "fade":
|
|
1785
|
+
return applyCrossfade(fromBuffer, toBuffer, progress, width, height, fromRawInfo, toRawInfo);
|
|
1786
|
+
case "slide-left":
|
|
1787
|
+
return applySlide(fromBuffer, toBuffer, progress, width, height, "left", fromRawInfo, toRawInfo);
|
|
1788
|
+
case "slide-up":
|
|
1789
|
+
return applySlide(fromBuffer, toBuffer, progress, width, height, "up", fromRawInfo, toRawInfo);
|
|
1790
|
+
case "blur":
|
|
1791
|
+
return applyBlur(fromBuffer, toBuffer, progress, width, height, fromRawInfo, toRawInfo);
|
|
1792
|
+
case "none":
|
|
1793
|
+
default:
|
|
1794
|
+
const d = await decodeToRaw(toBuffer, toRawInfo);
|
|
1795
|
+
return { buffer: d.data, rawInfo: { width: d.width, height: d.height, channels: 4 } };
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
function easeInOutCubic3(t) {
|
|
1799
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
1800
|
+
}
|
|
1673
1801
|
|
|
1674
1802
|
// src/compose/canvas-renderer.ts
|
|
1803
|
+
function mergeStepEffects(global, stepIndex, steps) {
|
|
1804
|
+
if (stepIndex === void 0 || !steps[stepIndex]?.effects) return global;
|
|
1805
|
+
const override = steps[stepIndex].effects;
|
|
1806
|
+
return {
|
|
1807
|
+
zoom: override.zoom ? { ...global.zoom, ...override.zoom } : global.zoom,
|
|
1808
|
+
cursor: override.cursor ? { ...global.cursor, ...override.cursor } : global.cursor,
|
|
1809
|
+
background: override.background ? { ...global.background, ...override.background } : global.background,
|
|
1810
|
+
deviceFrame: override.deviceFrame ? { ...global.deviceFrame, ...override.deviceFrame } : global.deviceFrame,
|
|
1811
|
+
speedRamp: override.speedRamp ? { ...global.speedRamp, ...override.speedRamp } : global.speedRamp,
|
|
1812
|
+
keystroke: override.keystroke ? { ...global.keystroke, ...override.keystroke } : global.keystroke,
|
|
1813
|
+
watermark: override.watermark ? { ...global.watermark, ...override.watermark } : global.watermark
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1675
1816
|
var MIN_FRAMES_PER_WORKER = 4;
|
|
1676
1817
|
var cachedWorkerUrl = null;
|
|
1677
1818
|
function getWorkerUrl() {
|
|
@@ -1727,14 +1868,17 @@ var CanvasRenderer = class {
|
|
|
1727
1868
|
const cpuCount = os.cpus().length;
|
|
1728
1869
|
const workerCount = Math.min(cpuCount, 8);
|
|
1729
1870
|
const useWorkers = workerCount >= 2 && processFrames.length >= workerCount * MIN_FRAMES_PER_WORKER;
|
|
1871
|
+
const perFrameEffects = processFrames.map(
|
|
1872
|
+
(f) => mergeStepEffects(this.effects, f.stepIndex, this.steps)
|
|
1873
|
+
);
|
|
1730
1874
|
let composed;
|
|
1731
1875
|
if (useWorkers) {
|
|
1732
|
-
composed = await this.processWithWorkers(processFrames, contexts, workerCount);
|
|
1876
|
+
composed = await this.processWithWorkers(processFrames, contexts, workerCount, perFrameEffects);
|
|
1733
1877
|
} else {
|
|
1734
1878
|
composed = [];
|
|
1735
1879
|
for (let i = 0; i < processFrames.length; i++) {
|
|
1736
1880
|
composed.push(
|
|
1737
|
-
await composeFrame(processFrames[i],
|
|
1881
|
+
await composeFrame(processFrames[i], perFrameEffects[i], this.output, contexts[i])
|
|
1738
1882
|
);
|
|
1739
1883
|
}
|
|
1740
1884
|
}
|
|
@@ -1747,7 +1891,7 @@ var CanvasRenderer = class {
|
|
|
1747
1891
|
* Distribute frame composition across a pool of worker threads.
|
|
1748
1892
|
* Workers process frames concurrently; results are collected in order.
|
|
1749
1893
|
*/
|
|
1750
|
-
processWithWorkers(frames, contexts, workerCount) {
|
|
1894
|
+
processWithWorkers(frames, contexts, workerCount, perFrameEffects) {
|
|
1751
1895
|
return new Promise((resolve, reject) => {
|
|
1752
1896
|
const results = new Array(frames.length);
|
|
1753
1897
|
let completed = 0;
|
|
@@ -1761,7 +1905,7 @@ var CanvasRenderer = class {
|
|
|
1761
1905
|
worker.postMessage({
|
|
1762
1906
|
taskId: i,
|
|
1763
1907
|
frame: frames[i],
|
|
1764
|
-
effects: this.effects,
|
|
1908
|
+
effects: perFrameEffects ? perFrameEffects[i] : this.effects,
|
|
1765
1909
|
output: this.output,
|
|
1766
1910
|
context: contexts[i]
|
|
1767
1911
|
});
|
|
@@ -1824,6 +1968,9 @@ var CanvasRenderer = class {
|
|
|
1824
1968
|
effectiveScale,
|
|
1825
1969
|
transitionFrames
|
|
1826
1970
|
);
|
|
1971
|
+
if (frame.isScrolling && zoomScale > 1) {
|
|
1972
|
+
zoomScale = 1;
|
|
1973
|
+
}
|
|
1827
1974
|
}
|
|
1828
1975
|
const clickProgress = frame.clickPosition != null ? frame.clickProgress ?? 0.5 : null;
|
|
1829
1976
|
const trailLength = this.effects.cursor.trailLength;
|
|
@@ -1898,7 +2045,7 @@ var CanvasRenderer = class {
|
|
|
1898
2045
|
* using the same applyTransitionsToStream() logic as composeStream().
|
|
1899
2046
|
*/
|
|
1900
2047
|
async *composeStreamOnline(source) {
|
|
1901
|
-
const hasFadeTransitions = this.steps.some((s) => s.transition
|
|
2048
|
+
const hasFadeTransitions = this.steps.some((s) => s.transition !== "none");
|
|
1902
2049
|
if (!hasFadeTransitions) {
|
|
1903
2050
|
const cpuCount = os.cpus().length;
|
|
1904
2051
|
const workerCount = Math.min(cpuCount, 8);
|
|
@@ -1954,6 +2101,9 @@ var CanvasRenderer = class {
|
|
|
1954
2101
|
effectiveScale,
|
|
1955
2102
|
transitionFrames
|
|
1956
2103
|
);
|
|
2104
|
+
if (frame.isScrolling && zoomScale > 1) {
|
|
2105
|
+
zoomScale = 1;
|
|
2106
|
+
}
|
|
1957
2107
|
}
|
|
1958
2108
|
const clickProgress = frame.clickPosition != null ? frame.clickProgress ?? 0.5 : null;
|
|
1959
2109
|
const trail = [];
|
|
@@ -1965,10 +2115,11 @@ var CanvasRenderer = class {
|
|
|
1965
2115
|
const dispatch = (worker) => {
|
|
1966
2116
|
if (canDispatch(nextToDispatch)) {
|
|
1967
2117
|
const i = nextToDispatch++;
|
|
2118
|
+
const frameEffects = mergeStepEffects(this.effects, frames[i].stepIndex, this.steps);
|
|
1968
2119
|
worker.postMessage({
|
|
1969
2120
|
taskId: i,
|
|
1970
2121
|
frame: frames[i],
|
|
1971
|
-
effects:
|
|
2122
|
+
effects: frameEffects,
|
|
1972
2123
|
output: this.output,
|
|
1973
2124
|
context: computeContext(i)
|
|
1974
2125
|
});
|
|
@@ -2078,13 +2229,16 @@ var CanvasRenderer = class {
|
|
|
2078
2229
|
const workerUrl = getWorkerUrl();
|
|
2079
2230
|
const workers = [];
|
|
2080
2231
|
let nextToDispatch = 0;
|
|
2232
|
+
const perFrameEffects = frames.map(
|
|
2233
|
+
(f) => mergeStepEffects(this.effects, f.stepIndex, this.steps)
|
|
2234
|
+
);
|
|
2081
2235
|
const dispatch = (worker) => {
|
|
2082
2236
|
if (nextToDispatch >= frames.length || workerError) return;
|
|
2083
2237
|
const i = nextToDispatch++;
|
|
2084
2238
|
worker.postMessage({
|
|
2085
2239
|
taskId: i,
|
|
2086
2240
|
frame: frames[i],
|
|
2087
|
-
effects:
|
|
2241
|
+
effects: perFrameEffects[i],
|
|
2088
2242
|
output: this.output,
|
|
2089
2243
|
context: contexts[i]
|
|
2090
2244
|
});
|
|
@@ -2135,7 +2289,8 @@ var CanvasRenderer = class {
|
|
|
2135
2289
|
*/
|
|
2136
2290
|
async *streamSequential(frames, contexts) {
|
|
2137
2291
|
for (let i = 0; i < frames.length; i++) {
|
|
2138
|
-
|
|
2292
|
+
const frameEffects = mergeStepEffects(this.effects, frames[i].stepIndex, this.steps);
|
|
2293
|
+
yield await composeFrame(frames[i], frameEffects, this.output, contexts[i]);
|
|
2139
2294
|
}
|
|
2140
2295
|
}
|
|
2141
2296
|
/**
|
|
@@ -2150,11 +2305,11 @@ var CanvasRenderer = class {
|
|
|
2150
2305
|
if (frames[i].stepIndex !== void 0 && frames[i - 1].stepIndex !== void 0 && frames[i].stepIndex !== frames[i - 1].stepIndex) {
|
|
2151
2306
|
const stepIdx = frames[i].stepIndex;
|
|
2152
2307
|
const step = this.steps[stepIdx];
|
|
2153
|
-
if (step && step.transition
|
|
2308
|
+
if (step && step.transition !== "none") {
|
|
2154
2309
|
const startIdx = Math.max(0, i - Math.floor(transitionFrames / 2));
|
|
2155
2310
|
const endIdx = Math.min(frames.length - 1, i + Math.ceil(transitionFrames / 2));
|
|
2156
2311
|
if (endIdx - startIdx >= 2) {
|
|
2157
|
-
windows.push({ startIdx, endIdx });
|
|
2312
|
+
windows.push({ startIdx, endIdx, type: step.transition });
|
|
2158
2313
|
}
|
|
2159
2314
|
}
|
|
2160
2315
|
}
|
|
@@ -2201,10 +2356,12 @@ var CanvasRenderer = class {
|
|
|
2201
2356
|
const fromBuf = state.frames[0].buffer;
|
|
2202
2357
|
const toBuf = state.frames[state.frames.length - 1].buffer;
|
|
2203
2358
|
const range = state.frames.length - 1;
|
|
2359
|
+
const transType = win.type;
|
|
2204
2360
|
const fromRawInfo = state.frames[0].rawInfo;
|
|
2205
2361
|
const toRawInfo = state.frames[state.frames.length - 1].rawInfo;
|
|
2206
2362
|
for (let j = 1; j < state.frames.length - 1; j++) {
|
|
2207
|
-
const blended = await
|
|
2363
|
+
const blended = await applyTransition(
|
|
2364
|
+
transType,
|
|
2208
2365
|
fromBuf,
|
|
2209
2366
|
toBuf,
|
|
2210
2367
|
j / range,
|
|
@@ -2246,8 +2403,8 @@ var CanvasRenderer = class {
|
|
|
2246
2403
|
if (frames[i].stepIndex !== void 0 && frames[i - 1].stepIndex !== void 0 && frames[i].stepIndex !== frames[i - 1].stepIndex) {
|
|
2247
2404
|
const stepIdx = frames[i].stepIndex;
|
|
2248
2405
|
const step = this.steps[stepIdx];
|
|
2249
|
-
if (step && step.transition
|
|
2250
|
-
boundaries.push({ index: i, stepIndex: stepIdx });
|
|
2406
|
+
if (step && step.transition !== "none") {
|
|
2407
|
+
boundaries.push({ index: i, stepIndex: stepIdx, type: step.transition });
|
|
2251
2408
|
}
|
|
2252
2409
|
}
|
|
2253
2410
|
}
|
|
@@ -2262,7 +2419,8 @@ var CanvasRenderer = class {
|
|
|
2262
2419
|
const toRawInfo = composed[endIdx].rawInfo;
|
|
2263
2420
|
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
2264
2421
|
const progress = (i - startIdx) / range;
|
|
2265
|
-
const blended = await
|
|
2422
|
+
const blended = await applyTransition(
|
|
2423
|
+
boundary.type,
|
|
2266
2424
|
fromBuffer,
|
|
2267
2425
|
toBuffer,
|
|
2268
2426
|
progress,
|
|
@@ -2339,7 +2497,7 @@ async function encodeGif(frames, config) {
|
|
|
2339
2497
|
gif.finish();
|
|
2340
2498
|
return Buffer.from(gif.bytes());
|
|
2341
2499
|
}
|
|
2342
|
-
async function encodeMp4(frames, config) {
|
|
2500
|
+
async function encodeMp4(frames, config, audio) {
|
|
2343
2501
|
if (frames.length === 0) {
|
|
2344
2502
|
throw new Error("Cannot encode MP4: no frames provided");
|
|
2345
2503
|
}
|
|
@@ -2347,14 +2505,14 @@ async function encodeMp4(frames, config) {
|
|
|
2347
2505
|
try {
|
|
2348
2506
|
const encoder = await detectVideoEncoder();
|
|
2349
2507
|
const params = resolveEncodingParams(config);
|
|
2350
|
-
await pipeFramesToFfmpeg(frames, config, params, encoder, outputPath);
|
|
2508
|
+
await pipeFramesToFfmpeg(frames, config, params, encoder, outputPath, audio);
|
|
2351
2509
|
return await readFile(outputPath);
|
|
2352
2510
|
} finally {
|
|
2353
2511
|
await rm(outputPath, { force: true }).catch(() => {
|
|
2354
2512
|
});
|
|
2355
2513
|
}
|
|
2356
2514
|
}
|
|
2357
|
-
async function pipeFramesToFfmpeg(frames, config, params, encoder, outputPath) {
|
|
2515
|
+
async function pipeFramesToFfmpeg(frames, config, params, encoder, outputPath, audio) {
|
|
2358
2516
|
const videoArgs = encoder === "hevc_videotoolbox" ? [
|
|
2359
2517
|
"-c:v",
|
|
2360
2518
|
"hevc_videotoolbox",
|
|
@@ -2388,6 +2546,14 @@ async function pipeFramesToFfmpeg(frames, config, params, encoder, outputPath) {
|
|
|
2388
2546
|
"-pix_fmt",
|
|
2389
2547
|
"yuv420p"
|
|
2390
2548
|
];
|
|
2549
|
+
const audioInputArgs = audio ? ["-i", audio.file] : ["-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo"];
|
|
2550
|
+
const audioFilters = [];
|
|
2551
|
+
if (audio) {
|
|
2552
|
+
if (audio.volume !== 1) audioFilters.push(`volume=${audio.volume}`);
|
|
2553
|
+
if (audio.fadeIn > 0) audioFilters.push(`afade=t=in:d=${audio.fadeIn / 1e3}`);
|
|
2554
|
+
if (audio.fadeOut > 0) audioFilters.push(`afade=t=out:st=999999:d=${audio.fadeOut / 1e3}`);
|
|
2555
|
+
}
|
|
2556
|
+
const audioFilterArgs = audioFilters.length > 0 ? ["-af", audioFilters.join(",")] : [];
|
|
2391
2557
|
return new Promise((resolve, reject) => {
|
|
2392
2558
|
const ffmpeg = spawn(
|
|
2393
2559
|
"ffmpeg",
|
|
@@ -2404,16 +2570,14 @@ async function pipeFramesToFfmpeg(frames, config, params, encoder, outputPath) {
|
|
|
2404
2570
|
String(config.fps),
|
|
2405
2571
|
"-i",
|
|
2406
2572
|
"pipe:0",
|
|
2407
|
-
//
|
|
2408
|
-
|
|
2409
|
-
"lavfi",
|
|
2410
|
-
"-i",
|
|
2411
|
-
"anullsrc=r=48000:cl=stereo",
|
|
2573
|
+
// Audio input
|
|
2574
|
+
...audioInputArgs,
|
|
2412
2575
|
...videoArgs,
|
|
2413
2576
|
"-c:a",
|
|
2414
2577
|
"aac",
|
|
2415
2578
|
"-b:a",
|
|
2416
2579
|
"128k",
|
|
2580
|
+
...audioFilterArgs,
|
|
2417
2581
|
"-shortest",
|
|
2418
2582
|
"-movflags",
|
|
2419
2583
|
"+faststart",
|
|
@@ -2458,19 +2622,19 @@ async function pipeFramesToFfmpeg(frames, config, params, encoder, outputPath) {
|
|
|
2458
2622
|
})().catch(reject);
|
|
2459
2623
|
});
|
|
2460
2624
|
}
|
|
2461
|
-
async function encodeMp4Stream(frames, config) {
|
|
2625
|
+
async function encodeMp4Stream(frames, config, audio) {
|
|
2462
2626
|
const outputPath = join(tmpdir(), `clipwise-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`);
|
|
2463
2627
|
try {
|
|
2464
2628
|
const encoder = await detectVideoEncoder();
|
|
2465
2629
|
const params = resolveEncodingParams(config);
|
|
2466
|
-
await pipeStreamToFfmpeg(frames, config, params, encoder, outputPath);
|
|
2630
|
+
await pipeStreamToFfmpeg(frames, config, params, encoder, outputPath, audio);
|
|
2467
2631
|
return await readFile(outputPath);
|
|
2468
2632
|
} finally {
|
|
2469
2633
|
await rm(outputPath, { force: true }).catch(() => {
|
|
2470
2634
|
});
|
|
2471
2635
|
}
|
|
2472
2636
|
}
|
|
2473
|
-
async function pipeStreamToFfmpeg(frames, config, params, encoder, outputPath) {
|
|
2637
|
+
async function pipeStreamToFfmpeg(frames, config, params, encoder, outputPath, audio) {
|
|
2474
2638
|
const videoArgs = encoder === "hevc_videotoolbox" ? [
|
|
2475
2639
|
"-c:v",
|
|
2476
2640
|
"hevc_videotoolbox",
|
|
@@ -2503,6 +2667,14 @@ async function pipeStreamToFfmpeg(frames, config, params, encoder, outputPath) {
|
|
|
2503
2667
|
"-pix_fmt",
|
|
2504
2668
|
"yuv420p"
|
|
2505
2669
|
];
|
|
2670
|
+
const audioInputArgs = audio ? ["-i", audio.file] : ["-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo"];
|
|
2671
|
+
const audioFilters = [];
|
|
2672
|
+
if (audio) {
|
|
2673
|
+
if (audio.volume !== 1) audioFilters.push(`volume=${audio.volume}`);
|
|
2674
|
+
if (audio.fadeIn > 0) audioFilters.push(`afade=t=in:d=${audio.fadeIn / 1e3}`);
|
|
2675
|
+
if (audio.fadeOut > 0) audioFilters.push(`afade=t=out:st=999999:d=${audio.fadeOut / 1e3}`);
|
|
2676
|
+
}
|
|
2677
|
+
const audioFilterArgs = audioFilters.length > 0 ? ["-af", audioFilters.join(",")] : [];
|
|
2506
2678
|
return new Promise((resolve, reject) => {
|
|
2507
2679
|
const ffmpeg = spawn(
|
|
2508
2680
|
"ffmpeg",
|
|
@@ -2518,15 +2690,13 @@ async function pipeStreamToFfmpeg(frames, config, params, encoder, outputPath) {
|
|
|
2518
2690
|
String(config.fps),
|
|
2519
2691
|
"-i",
|
|
2520
2692
|
"pipe:0",
|
|
2521
|
-
|
|
2522
|
-
"lavfi",
|
|
2523
|
-
"-i",
|
|
2524
|
-
"anullsrc=r=48000:cl=stereo",
|
|
2693
|
+
...audioInputArgs,
|
|
2525
2694
|
...videoArgs,
|
|
2526
2695
|
"-c:a",
|
|
2527
2696
|
"aac",
|
|
2528
2697
|
"-b:a",
|
|
2529
2698
|
"128k",
|
|
2699
|
+
...audioFilterArgs,
|
|
2530
2700
|
"-shortest",
|
|
2531
2701
|
"-movflags",
|
|
2532
2702
|
"+faststart",
|
|
@@ -2615,7 +2785,8 @@ var ConcurrentSession = class extends EventEmitter {
|
|
|
2615
2785
|
yield frame;
|
|
2616
2786
|
}
|
|
2617
2787
|
})(),
|
|
2618
|
-
this.scenario.output
|
|
2788
|
+
this.scenario.output,
|
|
2789
|
+
this.scenario.audio
|
|
2619
2790
|
);
|
|
2620
2791
|
const session = await handle.done;
|
|
2621
2792
|
this.emit("progress", { composed, total: composed, pct: 100 });
|
|
@@ -2655,7 +2826,8 @@ var StreamingSession = class extends EventEmitter {
|
|
|
2655
2826
|
yield frame;
|
|
2656
2827
|
}
|
|
2657
2828
|
})(),
|
|
2658
|
-
scenario.output
|
|
2829
|
+
scenario.output,
|
|
2830
|
+
scenario.audio
|
|
2659
2831
|
);
|
|
2660
2832
|
}
|
|
2661
2833
|
};
|
|
@@ -2770,15 +2942,16 @@ var ZoomEffectSchema = z.object({
|
|
|
2770
2942
|
enabled: z.boolean().default(true),
|
|
2771
2943
|
/**
|
|
2772
2944
|
* Numeric zoom scale (1.0 = no zoom). Overridden by `intensity` when set.
|
|
2773
|
-
* Default
|
|
2945
|
+
* Default: 1.25 to match "light" intensity (industry standard).
|
|
2774
2946
|
*/
|
|
2775
|
-
scale: z.number().min(1).max(5).default(1.
|
|
2947
|
+
scale: z.number().min(1).max(5).default(1.25),
|
|
2776
2948
|
/**
|
|
2777
2949
|
* Intensity preset — overrides `scale` when set.
|
|
2778
2950
|
* Calibrated against Loom (light≈1.25x) and Camtasia (moderate≈1.35x).
|
|
2951
|
+
* Default: "light" (1.25x) — matches industry standard (Screen Studio, Loom).
|
|
2779
2952
|
*/
|
|
2780
|
-
intensity: ZoomIntensitySchema.
|
|
2781
|
-
duration: z.number().default(
|
|
2953
|
+
intensity: ZoomIntensitySchema.default("light"),
|
|
2954
|
+
duration: z.number().default(800),
|
|
2782
2955
|
easing: z.enum(["ease-in-out", "ease-in", "ease-out", "linear"]).default("ease-in-out"),
|
|
2783
2956
|
autoZoom: AutoZoomConfigSchema.default({})
|
|
2784
2957
|
});
|
|
@@ -2786,7 +2959,7 @@ var CursorEffectSchema = z.object({
|
|
|
2786
2959
|
enabled: z.boolean().default(true),
|
|
2787
2960
|
size: z.number().default(20),
|
|
2788
2961
|
color: z.string().default("#000000"),
|
|
2789
|
-
speed: z.enum(["fast", "normal", "slow"]).default("
|
|
2962
|
+
speed: z.enum(["fast", "normal", "slow"]).default("normal"),
|
|
2790
2963
|
smoothing: z.boolean().default(true),
|
|
2791
2964
|
clickEffect: z.boolean().default(true),
|
|
2792
2965
|
clickColor: z.string().default("rgba(59, 130, 246, 0.3)"),
|
|
@@ -2812,7 +2985,7 @@ var DeviceFrameSchema = z.object({
|
|
|
2812
2985
|
});
|
|
2813
2986
|
var SpeedRampConfigSchema = z.object({
|
|
2814
2987
|
enabled: z.boolean().default(false),
|
|
2815
|
-
idleSpeed: z.number().min(0.5).max(8).default(
|
|
2988
|
+
idleSpeed: z.number().min(0.5).max(8).default(2),
|
|
2816
2989
|
actionSpeed: z.number().min(0.25).max(2).default(0.8),
|
|
2817
2990
|
transitionFrames: z.number().default(15)
|
|
2818
2991
|
});
|
|
@@ -2854,10 +3027,11 @@ var EffectsConfigSchema = z.object({
|
|
|
2854
3027
|
watermark: WatermarkConfigSchema.default({})
|
|
2855
3028
|
});
|
|
2856
3029
|
var OutputConfigSchema = z.object({
|
|
2857
|
-
format: z.enum(["gif", "mp4", "webm", "png-sequence"]).default("
|
|
3030
|
+
format: z.enum(["gif", "mp4", "webm", "png-sequence"]).default("mp4"),
|
|
2858
3031
|
width: z.number().default(1280),
|
|
2859
3032
|
height: z.number().default(800),
|
|
2860
3033
|
fps: z.number().min(1).max(60).default(30),
|
|
3034
|
+
/** @deprecated Use `preset` instead. Will be removed in v0.7. */
|
|
2861
3035
|
quality: z.number().min(1).max(100).default(80),
|
|
2862
3036
|
// Encoding preset for MP4 output. Overrides quality when set.
|
|
2863
3037
|
// social — optimized for Twitter/X and YouTube (CRF 25, capped bitrate)
|
|
@@ -2867,12 +3041,40 @@ var OutputConfigSchema = z.object({
|
|
|
2867
3041
|
outputDir: z.string().default("./output"),
|
|
2868
3042
|
filename: z.string().default("clipwise-recording")
|
|
2869
3043
|
});
|
|
3044
|
+
var StepEffectsOverrideSchema = z.object({
|
|
3045
|
+
zoom: ZoomEffectSchema.partial().optional(),
|
|
3046
|
+
cursor: CursorEffectSchema.partial().optional(),
|
|
3047
|
+
background: BackgroundSchema.partial().optional(),
|
|
3048
|
+
deviceFrame: DeviceFrameSchema.partial().optional(),
|
|
3049
|
+
speedRamp: SpeedRampConfigSchema.partial().optional(),
|
|
3050
|
+
keystroke: KeystrokeConfigSchema.partial().optional(),
|
|
3051
|
+
watermark: WatermarkConfigSchema.partial().optional()
|
|
3052
|
+
}).optional();
|
|
3053
|
+
var TransitionTypeSchema = z.enum([
|
|
3054
|
+
"none",
|
|
3055
|
+
"fade",
|
|
3056
|
+
"slide-left",
|
|
3057
|
+
"slide-up",
|
|
3058
|
+
"blur"
|
|
3059
|
+
]);
|
|
2870
3060
|
var StepSchema = z.object({
|
|
2871
3061
|
name: z.string().optional(),
|
|
2872
3062
|
actions: z.array(StepActionSchema),
|
|
2873
3063
|
captureDelay: z.number().default(300),
|
|
2874
3064
|
holdDuration: z.number().default(1500),
|
|
2875
|
-
transition:
|
|
3065
|
+
transition: TransitionTypeSchema.default("none"),
|
|
3066
|
+
/** Per-step effects override — merges with global effects config. */
|
|
3067
|
+
effects: StepEffectsOverrideSchema
|
|
3068
|
+
});
|
|
3069
|
+
var AudioConfigSchema = z.object({
|
|
3070
|
+
/** Path to the audio file (MP3, WAV, AAC, etc.). */
|
|
3071
|
+
file: z.string().min(1),
|
|
3072
|
+
/** Volume level (0.0 = silent, 1.0 = full). */
|
|
3073
|
+
volume: z.number().min(0).max(1).default(1),
|
|
3074
|
+
/** Fade-in duration in milliseconds. */
|
|
3075
|
+
fadeIn: z.number().min(0).default(0),
|
|
3076
|
+
/** Fade-out duration in milliseconds. */
|
|
3077
|
+
fadeOut: z.number().min(0).default(0)
|
|
2876
3078
|
});
|
|
2877
3079
|
var ScenarioSchema = z.object({
|
|
2878
3080
|
name: z.string(),
|
|
@@ -2883,6 +3085,8 @@ var ScenarioSchema = z.object({
|
|
|
2883
3085
|
}).default({}),
|
|
2884
3086
|
effects: EffectsConfigSchema.default({}),
|
|
2885
3087
|
output: OutputConfigSchema.default({}),
|
|
3088
|
+
/** Optional audio narration — muxed into MP4 output. */
|
|
3089
|
+
audio: AudioConfigSchema.optional(),
|
|
2886
3090
|
steps: z.array(StepSchema).min(1)
|
|
2887
3091
|
});
|
|
2888
3092
|
|
|
@@ -3000,7 +3204,10 @@ export {
|
|
|
3000
3204
|
ConcurrentSession,
|
|
3001
3205
|
StreamingSession,
|
|
3002
3206
|
ZOOM_INTENSITY_SCALES,
|
|
3207
|
+
applyBlur,
|
|
3003
3208
|
applyCrossfade,
|
|
3209
|
+
applySlide,
|
|
3210
|
+
applyTransition,
|
|
3004
3211
|
buildZoomClickLookup,
|
|
3005
3212
|
calculateAdaptiveZoom,
|
|
3006
3213
|
calculateAdaptiveZoomFromLookup,
|