clipwise 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +1 -1
- package/README.md +42 -8
- package/dist/cli/index.js +480 -106
- package/dist/compose/frame-worker.js +85 -76
- package/dist/index.d.ts +477 -34
- package/dist/index.js +582 -129
- package/package.json +1 -1
|
@@ -240,52 +240,31 @@ async function applyDeviceFrame(frameBuffer, config, frameWidth, frameHeight, dp
|
|
|
240
240
|
|
|
241
241
|
// src/effects/cursor.ts
|
|
242
242
|
import sharp2 from "sharp";
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" shape-rendering="geometricPrecision">
|
|
246
|
-
<path d="M4 0 L4 22 L10 16 L16 24 L20 22 L14 14 L22 14 Z"
|
|
247
|
-
fill="${color}" stroke="#ffffff" stroke-width="1.5" stroke-linejoin="round"/>
|
|
248
|
-
</svg>`;
|
|
249
|
-
}
|
|
250
|
-
function buildClickRippleSvg(radius, color, progress) {
|
|
251
|
-
const currentRadius = radius * progress;
|
|
252
|
-
const opacity = Math.max(0, 1 - progress);
|
|
253
|
-
const size = Math.ceil(radius * 2 + 4);
|
|
254
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" shape-rendering="geometricPrecision">
|
|
255
|
-
<circle cx="${size / 2}" cy="${size / 2}" r="${currentRadius}"
|
|
256
|
-
fill="none" stroke="${color}" stroke-width="2"
|
|
257
|
-
opacity="${opacity.toFixed(3)}"/>
|
|
258
|
-
<circle cx="${size / 2}" cy="${size / 2}" r="${currentRadius * 0.6}"
|
|
259
|
-
fill="${color}" opacity="${(opacity * 0.4).toFixed(3)}"/>
|
|
260
|
-
</svg>`;
|
|
261
|
-
}
|
|
262
|
-
async function renderCursor(frameBuffer, position, config, frameWidth, frameHeight, dpr = 1) {
|
|
263
|
-
if (!config.enabled) return frameBuffer;
|
|
243
|
+
function buildCursorOverlay(position, config, frameWidth, frameHeight, dpr = 1) {
|
|
244
|
+
if (!config.enabled) return null;
|
|
264
245
|
const size = Math.round(config.size * dpr);
|
|
265
246
|
const cursorSvg = buildCursorSvg(size, config.color);
|
|
266
|
-
const cursorBuffer = Buffer.from(cursorSvg);
|
|
267
247
|
const tipOffsetX = Math.round(4 / 24 * size);
|
|
268
248
|
const px = Math.round(position.x * dpr);
|
|
269
249
|
const py = Math.round(position.y * dpr);
|
|
270
250
|
const left = Math.max(0, Math.min(px - tipOffsetX, frameWidth - size));
|
|
271
251
|
const top = Math.max(0, Math.min(py, frameHeight - size));
|
|
272
|
-
return
|
|
252
|
+
return { input: Buffer.from(cursorSvg), left, top };
|
|
273
253
|
}
|
|
274
|
-
|
|
275
|
-
if (!config.enabled || !config.clickEffect) return
|
|
254
|
+
function buildClickRippleOverlay(position, config, progress, frameWidth, frameHeight, dpr = 1) {
|
|
255
|
+
if (!config.enabled || !config.clickEffect) return null;
|
|
276
256
|
const radius = config.clickRadius * dpr;
|
|
277
257
|
const clampedProgress = Math.max(0, Math.min(1, progress));
|
|
278
258
|
const rippleSvg = buildClickRippleSvg(radius, config.clickColor, clampedProgress);
|
|
279
|
-
const rippleBuffer = Buffer.from(rippleSvg);
|
|
280
259
|
const rippleSize = Math.ceil(radius * 2 + 4);
|
|
281
260
|
const px = Math.round(position.x * dpr);
|
|
282
261
|
const py = Math.round(position.y * dpr);
|
|
283
262
|
const left = Math.max(0, Math.min(px - Math.round(rippleSize / 2), frameWidth - rippleSize));
|
|
284
263
|
const top = Math.max(0, Math.min(py - Math.round(rippleSize / 2), frameHeight - rippleSize));
|
|
285
|
-
return
|
|
264
|
+
return { input: Buffer.from(rippleSvg), left, top };
|
|
286
265
|
}
|
|
287
|
-
|
|
288
|
-
if (!config.enabled || !config.highlight) return
|
|
266
|
+
function buildHighlightOverlay(position, config, frameWidth, frameHeight, dpr = 1) {
|
|
267
|
+
if (!config.enabled || !config.highlight) return null;
|
|
289
268
|
const r = config.highlightRadius * dpr;
|
|
290
269
|
const size = Math.ceil(r * 2 + 4);
|
|
291
270
|
const cx = size / 2;
|
|
@@ -304,12 +283,10 @@ async function renderCursorHighlight(frameBuffer, position, config, frameWidth,
|
|
|
304
283
|
const py = Math.round(position.y * dpr);
|
|
305
284
|
const left = Math.max(0, Math.min(px - Math.round(cx), frameWidth - size));
|
|
306
285
|
const top = Math.max(0, Math.min(py - Math.round(cy), frameHeight - size));
|
|
307
|
-
return
|
|
286
|
+
return { input: Buffer.from(highlightSvg), left, top };
|
|
308
287
|
}
|
|
309
|
-
|
|
310
|
-
if (!config.enabled || !config.trail || positions.length < 2)
|
|
311
|
-
return frameBuffer;
|
|
312
|
-
}
|
|
288
|
+
function buildTrailOverlay(positions, config, frameWidth, frameHeight, dpr = 1) {
|
|
289
|
+
if (!config.enabled || !config.trail || positions.length < 2) return null;
|
|
313
290
|
const segments = [];
|
|
314
291
|
for (let i = 1; i < positions.length; i++) {
|
|
315
292
|
const opacity = i / positions.length * 0.6;
|
|
@@ -325,7 +302,26 @@ async function renderCursorTrail(frameBuffer, positions, config, frameWidth, fra
|
|
|
325
302
|
const trailSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}" shape-rendering="geometricPrecision">
|
|
326
303
|
${segments.join("\n ")}
|
|
327
304
|
</svg>`;
|
|
328
|
-
return
|
|
305
|
+
return { input: Buffer.from(trailSvg), left: 0, top: 0 };
|
|
306
|
+
}
|
|
307
|
+
function buildCursorSvg(size, color) {
|
|
308
|
+
const s = size;
|
|
309
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" shape-rendering="geometricPrecision">
|
|
310
|
+
<path d="M4 0 L4 22 L10 16 L16 24 L20 22 L14 14 L22 14 Z"
|
|
311
|
+
fill="${color}" stroke="#ffffff" stroke-width="1.5" stroke-linejoin="round"/>
|
|
312
|
+
</svg>`;
|
|
313
|
+
}
|
|
314
|
+
function buildClickRippleSvg(radius, color, progress) {
|
|
315
|
+
const currentRadius = radius * progress;
|
|
316
|
+
const opacity = Math.max(0, 1 - progress);
|
|
317
|
+
const size = Math.ceil(radius * 2 + 4);
|
|
318
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" shape-rendering="geometricPrecision">
|
|
319
|
+
<circle cx="${size / 2}" cy="${size / 2}" r="${currentRadius}"
|
|
320
|
+
fill="none" stroke="${color}" stroke-width="2"
|
|
321
|
+
opacity="${opacity.toFixed(3)}"/>
|
|
322
|
+
<circle cx="${size / 2}" cy="${size / 2}" r="${currentRadius * 0.6}"
|
|
323
|
+
fill="${color}" opacity="${(opacity * 0.4).toFixed(3)}"/>
|
|
324
|
+
</svg>`;
|
|
329
325
|
}
|
|
330
326
|
|
|
331
327
|
// src/effects/zoom.ts
|
|
@@ -639,73 +635,87 @@ async function composeFrame(frame, effects, output, context) {
|
|
|
639
635
|
cursorTrail: context?.cursorTrail ?? []
|
|
640
636
|
};
|
|
641
637
|
const frameOffset = getFrameOffset(effects.deviceFrame, dpr);
|
|
642
|
-
const
|
|
638
|
+
const sl = context?.staticLayers;
|
|
639
|
+
const preZoomOverlays = [];
|
|
640
|
+
const hasBrowserChrome = effects.deviceFrame.enabled && effects.deviceFrame.type === "browser" && sl?.browserChromePng;
|
|
641
|
+
const extTop = hasBrowserChrome ? sl.browserChromeHeight : 0;
|
|
642
|
+
const extWidth = width;
|
|
643
|
+
const extHeight = height + extTop;
|
|
644
|
+
if (hasBrowserChrome) {
|
|
645
|
+
preZoomOverlays.push({ input: sl.browserChromePng, left: 0, top: 0 });
|
|
646
|
+
}
|
|
647
|
+
const withExtFrameOffset = (pos) => ({
|
|
643
648
|
x: pos.x + frameOffset.left / Math.max(1, dpr),
|
|
644
649
|
y: pos.y + frameOffset.top / Math.max(1, dpr)
|
|
645
650
|
});
|
|
646
|
-
if (effects.deviceFrame.enabled) {
|
|
647
|
-
const sl2 = ctx.staticLayers;
|
|
648
|
-
if (sl2?.browserChromePng && effects.deviceFrame.type === "browser") {
|
|
649
|
-
buffer = await sharp7(buffer).extend({
|
|
650
|
-
top: sl2.browserChromeHeight,
|
|
651
|
-
bottom: 0,
|
|
652
|
-
left: 0,
|
|
653
|
-
right: 0,
|
|
654
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
655
|
-
}).composite([{ input: sl2.browserChromePng, left: 0, top: 0 }]).png().toBuffer();
|
|
656
|
-
} else {
|
|
657
|
-
buffer = await applyDeviceFrame(buffer, effects.deviceFrame, width, height, dpr);
|
|
658
|
-
}
|
|
659
|
-
const meta2 = await sharp7(buffer).metadata();
|
|
660
|
-
width = meta2.width ?? width;
|
|
661
|
-
height = meta2.height ?? height;
|
|
662
|
-
}
|
|
663
651
|
if (effects.cursor.enabled && effects.cursor.highlight && frame.cursorPosition) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
withFrameOffset(frame.cursorPosition),
|
|
652
|
+
const overlay = buildHighlightOverlay(
|
|
653
|
+
withExtFrameOffset(frame.cursorPosition),
|
|
667
654
|
effects.cursor,
|
|
668
|
-
|
|
669
|
-
|
|
655
|
+
extWidth,
|
|
656
|
+
extHeight,
|
|
670
657
|
dpr
|
|
671
658
|
);
|
|
659
|
+
if (overlay) preZoomOverlays.push(overlay);
|
|
672
660
|
}
|
|
673
661
|
if (effects.cursor.enabled && effects.cursor.trail && ctx.cursorTrail.length >= 2) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
ctx.cursorTrail.map(withFrameOffset),
|
|
662
|
+
const overlay = buildTrailOverlay(
|
|
663
|
+
ctx.cursorTrail.map(withExtFrameOffset),
|
|
677
664
|
effects.cursor,
|
|
678
|
-
|
|
679
|
-
|
|
665
|
+
extWidth,
|
|
666
|
+
extHeight,
|
|
680
667
|
dpr
|
|
681
668
|
);
|
|
669
|
+
if (overlay) preZoomOverlays.push(overlay);
|
|
682
670
|
}
|
|
683
671
|
if (effects.cursor.enabled && frame.cursorPosition) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
withFrameOffset(frame.cursorPosition),
|
|
672
|
+
const overlay = buildCursorOverlay(
|
|
673
|
+
withExtFrameOffset(frame.cursorPosition),
|
|
687
674
|
effects.cursor,
|
|
688
|
-
|
|
689
|
-
|
|
675
|
+
extWidth,
|
|
676
|
+
extHeight,
|
|
690
677
|
dpr
|
|
691
678
|
);
|
|
679
|
+
if (overlay) preZoomOverlays.push(overlay);
|
|
692
680
|
}
|
|
693
681
|
if (effects.cursor.enabled && effects.cursor.clickEffect && frame.clickPosition) {
|
|
694
682
|
const progress = ctx.clickProgress ?? frame.clickProgress ?? 0.5;
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
withFrameOffset(frame.clickPosition),
|
|
683
|
+
const overlay = buildClickRippleOverlay(
|
|
684
|
+
withExtFrameOffset(frame.clickPosition),
|
|
698
685
|
effects.cursor,
|
|
699
686
|
progress,
|
|
700
|
-
|
|
701
|
-
|
|
687
|
+
extWidth,
|
|
688
|
+
extHeight,
|
|
702
689
|
dpr
|
|
703
690
|
);
|
|
691
|
+
if (overlay) preZoomOverlays.push(overlay);
|
|
692
|
+
}
|
|
693
|
+
if (hasBrowserChrome || preZoomOverlays.length > 0) {
|
|
694
|
+
let pipeline = sharp7(buffer);
|
|
695
|
+
if (hasBrowserChrome) {
|
|
696
|
+
pipeline = pipeline.extend({
|
|
697
|
+
top: extTop,
|
|
698
|
+
bottom: 0,
|
|
699
|
+
left: 0,
|
|
700
|
+
right: 0,
|
|
701
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
if (preZoomOverlays.length > 0) {
|
|
705
|
+
pipeline = pipeline.composite(preZoomOverlays);
|
|
706
|
+
}
|
|
707
|
+
buffer = await pipeline.png().toBuffer();
|
|
708
|
+
width = extWidth;
|
|
709
|
+
height = extHeight;
|
|
710
|
+
} else if (effects.deviceFrame.enabled) {
|
|
711
|
+
buffer = await applyDeviceFrame(buffer, effects.deviceFrame, width, height, dpr);
|
|
712
|
+
const devMeta = await sharp7(buffer).metadata();
|
|
713
|
+
width = devMeta.width ?? width;
|
|
714
|
+
height = devMeta.height ?? height;
|
|
704
715
|
}
|
|
705
716
|
const scale = ctx.zoomScale;
|
|
706
717
|
if (effects.zoom.enabled && scale > 1) {
|
|
707
|
-
const
|
|
708
|
-
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 };
|
|
718
|
+
const rawFocus = ctx.focusOverride ?? (effects.zoom.autoZoom.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 });
|
|
709
719
|
const offset = getFrameOffset(effects.deviceFrame, dpr);
|
|
710
720
|
const focusPoint = {
|
|
711
721
|
x: rawFocus.x * dpr + offset.left,
|
|
@@ -724,7 +734,6 @@ async function composeFrame(frame, effects, output, context) {
|
|
|
724
734
|
dpr
|
|
725
735
|
);
|
|
726
736
|
}
|
|
727
|
-
const sl = ctx.staticLayers;
|
|
728
737
|
if (sl) {
|
|
729
738
|
const padding = effects.background.padding;
|
|
730
739
|
const contentWidth = output.width - padding * 2;
|