zdymak 0.7.0 → 0.13.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/src/still.mjs CHANGED
@@ -1,62 +1,72 @@
1
- /**
2
- * Still-image renderers — one store screenshot per scene, in a chosen style. Aspect-agnostic (cover-fit),
3
- * so the same renderers serve iPhone, iPad, Mac and Watch dimensions.
4
- *
5
- * premium — the app screen floats on a brand matte (glow + vignette) with a bottom title pill.
6
- * bleed — the app screen fills the frame, with an optional lower-third caption. Best for Watch (raw).
7
- *
8
- * (Device-FRAMED stills iPhone/iPad bezel, Mac window, Watch ring — are the next addition; `framed`
9
- * currently falls back to `bleed`.)
10
- */
11
- import { createCanvas, loadImage } from '@napi-rs/canvas';
12
- import { premiumStill } from './premium.mjs';
13
- import { drawCaption, hexA } from './canvas.mjs';
14
-
15
- function coverCanvas(img, W, H) {
16
- const c = createCanvas(W, H);
17
- const ctx = c.getContext('2d');
18
- const ia = img.height / img.width;
19
- let dw = W;
20
- let dh = W * ia;
21
- if (dh < H) {
22
- dh = H;
23
- dw = H / ia;
24
- }
25
- ctx.drawImage(img, (W - dw) / 2, (H - dh) / 2, dw, dh);
26
- return c;
27
- }
28
-
29
- function bleedStill({ W, H, img, caption, brand }) {
30
- const c = createCanvas(W, H);
31
- const ctx = c.getContext('2d');
32
- ctx.fillStyle = brand.ink;
33
- ctx.fillRect(0, 0, W, H);
34
- ctx.drawImage(coverCanvas(img, W, H), 0, 0);
35
- if (caption.title || caption.sub) {
36
- const g = ctx.createLinearGradient(0, H * 0.55, 0, H);
37
- g.addColorStop(0, hexA(brand.ink, 0));
38
- g.addColorStop(1, hexA(brand.ink, 0.85));
39
- ctx.fillStyle = g;
40
- ctx.fillRect(0, H * 0.55, W, H * 0.45);
41
- drawCaption(ctx, {
42
- title: caption.title,
43
- subtitle: caption.sub,
44
- centerX: W / 2,
45
- top: H * 0.78,
46
- maxWidth: W * 0.86,
47
- titleSize: Math.round(W * 0.066),
48
- subSize: Math.round(W * 0.036),
49
- titleColor: brand.title,
50
- subColor: brand.sub,
51
- });
52
- }
53
- return c;
54
- }
55
-
56
- /** Render one still to a canvas. `style`: premium | framed (device bezel) | bleed. */
57
- export async function renderStill(style, { W, H, imgPath, caption, brand, theme, frame }) {
58
- const img = await loadImage(imgPath);
59
- if (style === 'framed') return premiumStill({ W, H, img, caption, brand, theme, frame: frame || 'phone' });
60
- if (style === 'premium') return premiumStill({ W, H, img, caption, brand, theme });
61
- return bleedStill({ W, H, img, caption, brand });
62
- }
1
+ /**
2
+ * Still-image renderers — one store screenshot per scene, in a chosen style. Aspect-agnostic (cover-fit),
3
+ * so the same renderers serve iPhone, iPad, Mac and Watch dimensions.
4
+ *
5
+ * premium — the app screen floats on a brand matte (glow + vignette) with a bottom title pill.
6
+ * bleed — the app screen fills the frame, with an optional lower-third caption. Best for Watch (raw).
7
+ *
8
+ * framed — the capture inside a device body (iPhone/iPad/Android/Watch), caption on top.
9
+ */
10
+ import { createCanvas, loadImage } from '@napi-rs/canvas';
11
+ import { premiumStill } from './premium.mjs';
12
+ import { drawCaption, hexA } from './canvas.mjs';
13
+ import { loadCapture } from './statusbar.mjs';
14
+
15
+ function coverCanvas(img, W, H, anchor = 'center') {
16
+ const c = createCanvas(W, H);
17
+ const ctx = c.getContext('2d');
18
+ const ia = img.height / img.width;
19
+ let dw = W;
20
+ let dh = W * ia;
21
+ if (dh < H) {
22
+ dh = H;
23
+ dw = H / ia;
24
+ }
25
+ // A phone capture is taller than any store slot (Play caps at 2:1), so a cover fit must drop some of
26
+ // it. `anchor: 'top'` drops it from the BOTTOM — keeping the status bar and the screen's headline,
27
+ // which is what a plain, unframed store shot needs; centring would shave both ends.
28
+ const dy = anchor === 'top' ? 0 : (H - dh) / 2;
29
+ ctx.drawImage(img, (W - dw) / 2, dy, dw, dh);
30
+ return c;
31
+ }
32
+
33
+ function bleedStill({ W, H, img, caption, brand, theme }) {
34
+ const c = createCanvas(W, H);
35
+ const ctx = c.getContext('2d');
36
+ ctx.fillStyle = brand.ink;
37
+ ctx.fillRect(0, 0, W, H);
38
+ ctx.drawImage(coverCanvas(img, W, H, theme?.anchor), 0, 0);
39
+ if (caption.title || caption.sub) {
40
+ const g = ctx.createLinearGradient(0, H * 0.55, 0, H);
41
+ g.addColorStop(0, hexA(brand.ink, 0));
42
+ g.addColorStop(1, hexA(brand.ink, 0.85));
43
+ ctx.fillStyle = g;
44
+ ctx.fillRect(0, H * 0.55, W, H * 0.45);
45
+ drawCaption(ctx, {
46
+ title: caption.title,
47
+ subtitle: caption.sub,
48
+ centerX: W / 2,
49
+ top: H * 0.78,
50
+ maxWidth: W * 0.86,
51
+ titleSize: Math.round(W * 0.066),
52
+ subSize: Math.round(W * 0.036),
53
+ titleColor: brand.title,
54
+ subColor: brand.sub,
55
+ });
56
+ }
57
+ return c;
58
+ }
59
+
60
+ /**
61
+ * Render one still to a canvas. `style`: premium | framed (device bezel) | bleed.
62
+ *
63
+ * The status bar is painted onto the CAPTURE before any framing/scaling, so it lands at the capture's own
64
+ * density and gets scaled down with everything else — drawing it afterwards would size it to the output
65
+ * frame and look pasted on.
66
+ */
67
+ export async function renderStill(style, { W, H, imgPath, caption, brand, theme, frame }) {
68
+ const img = await loadCapture(imgPath, theme, frame);
69
+ if (style === 'framed') return premiumStill({ W, H, img, caption, brand, theme, frame: frame || 'phone' });
70
+ if (style === 'premium') return premiumStill({ W, H, img, caption, brand, theme });
71
+ return bleedStill({ W, H, img, caption, brand, theme });
72
+ }