@sprid/cli 1.0.0 → 1.0.4
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/CHANGELOG.md +33 -0
- package/README-post.md +21 -15
- package/package.json +1 -1
- package/src/docs/guides.generated.mjs +6 -6
- package/src/post/preview.mjs +382 -621
- package/src/post/recipes/screen.mjs +2 -1
- package/src/post/rules/video-safe-zones.d.mts +6 -0
- package/src/post/rules/video-safe-zones.mjs +27 -0
- package/src/post/screen/compose.mjs +6 -3
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
kit, readJson, resolveDevice, runScript, simkit, sleep, startRecording, writeJson,
|
|
24
24
|
} from "../screen/sim.mjs";
|
|
25
25
|
import { settle as settleFor } from "../screen/sim.mjs";
|
|
26
|
+
import { captionSafeRect } from "../rules/video-safe-zones.mjs";
|
|
26
27
|
import { layout } from "../screen/captions.mjs";
|
|
27
28
|
import { hasBaguette, openInput } from "../screen/input.mjs";
|
|
28
29
|
import {
|
|
@@ -220,7 +221,7 @@ export async function buildScreen({ spec, script, root, out, tmp, recut = false,
|
|
|
220
221
|
: FONT_CANDIDATES.find(existsSync);
|
|
221
222
|
const rendered = renderCaptions(cards, {
|
|
222
223
|
simkitBin: bin, dir: join(tmp, "captions"), fontFile,
|
|
223
|
-
size: cfg.capSize, width: Math.
|
|
224
|
+
size: cfg.capSize, width: Math.floor(captionSafeRect(W, H, W / 45 + Math.abs(cfg.drift) * 1.6).width), pill: cfg.pill ?? 0,
|
|
224
225
|
...(cfg.stroke != null ? { stroke: cfg.stroke } : {}),
|
|
225
226
|
});
|
|
226
227
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface SafeZoneBand { readonly key: string; readonly left: number; readonly top: number; readonly width: number; readonly height: number; }
|
|
2
|
+
export interface VideoRect { x: number; y: number; width: number; height: number; }
|
|
3
|
+
export const SAFE_ZONE_BANDS: readonly SafeZoneBand[];
|
|
4
|
+
export function captionSafeRect(width: number, height: number, padding?: number): VideoRect;
|
|
5
|
+
export function overlapsSafeZones(rect: VideoRect, width: number, height: number): boolean;
|
|
6
|
+
export function captionPlacement(card: {w: number; h: number}, width: number, height: number, bottom: number, motion?: number): {x: number; y: number};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Approximate TikTok/Reels interface bands. Guides, never a crop or guarantee. */
|
|
2
|
+
export const SAFE_ZONE_BANDS = Object.freeze([
|
|
3
|
+
Object.freeze({ key: "caption", left: 0, top: 0.8, width: 1, height: 0.2 }),
|
|
4
|
+
Object.freeze({ key: "rail", left: 0.86, top: 0.38, width: 0.14, height: 0.42 }),
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
/** Conservative subtitle rectangle left of the rail and above the bottom band. */
|
|
8
|
+
export function captionSafeRect(width, height, padding = width / 45) {
|
|
9
|
+
if (![width, height, padding].every(Number.isFinite) || width <= 0 || height <= 0 || padding < 0) throw new Error("Invalid video dimensions");
|
|
10
|
+
const bottom = SAFE_ZONE_BANDS.find(b => b.key === "caption");
|
|
11
|
+
const rail = SAFE_ZONE_BANDS.find(b => b.key === "rail");
|
|
12
|
+
const rect = { x: padding, y: rail.top * height + padding, width: rail.left * width - 2 * padding, height: (bottom.top - rail.top) * height - 2 * padding };
|
|
13
|
+
if (rect.width <= 0 || rect.height <= 0) throw new Error("Safe zone has no room for captions");
|
|
14
|
+
return rect;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Rectangle includes every visible pixel, including text stroke or a caption box. */
|
|
18
|
+
export function overlapsSafeZones(rect, width, height) {
|
|
19
|
+
return SAFE_ZONE_BANDS.some(b => rect.x < (b.left + b.width) * width && rect.x + rect.width > b.left * width && rect.y < (b.top + b.height) * height && rect.y + rect.height > b.top * height);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Place a rendered caption card safely, reserving room for its rise animation. */
|
|
23
|
+
export function captionPlacement(card, width, height, bottom, motion = 0) {
|
|
24
|
+
const safe = captionSafeRect(width, height, width / 45 + motion);
|
|
25
|
+
if (![card.w, card.h, bottom, motion].every(Number.isFinite) || card.w <= 0 || card.h <= 0 || motion < 0 || card.w > safe.width || card.h > safe.height - 14) throw new Error("Caption exceeds safe zone; shorten the phrase or reduce its size");
|
|
26
|
+
return { x: safe.x + (safe.width - card.w) / 2, y: Math.max(safe.y, Math.min(bottom - card.h, safe.y + safe.height - card.h - 14)) };
|
|
27
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// Nothing in it is clever. The two places that cost a rebuild if you get them
|
|
15
15
|
// wrong are marked.
|
|
16
16
|
|
|
17
|
+
import { captionSafeRect, captionPlacement } from "../rules/video-safe-zones.mjs";
|
|
17
18
|
import { spawnSync } from "node:child_process";
|
|
18
19
|
import { existsSync, mkdirSync } from "node:fs";
|
|
19
20
|
import { dirname, join } from "node:path";
|
|
@@ -89,7 +90,7 @@ export function solveClock(plateaus, wallStartMs, wallEndMs) {
|
|
|
89
90
|
// the caption cards
|
|
90
91
|
// ---------------------------------------------------------------------------
|
|
91
92
|
|
|
92
|
-
export function renderCaptions(cards, { simkitBin, dir, fontFile, size = 82, width =
|
|
93
|
+
export function renderCaptions(cards, { simkitBin, dir, fontFile, size = 82, width = captionSafeRect(W, H).width, stroke = 5, pill = 0, color = "ffffff" }) {
|
|
93
94
|
mkdirSync(dir, { recursive: true });
|
|
94
95
|
return cards.map((c, i) => {
|
|
95
96
|
const out = join(dir, `cap-${String(i).padStart(3, "0")}.png`);
|
|
@@ -137,8 +138,9 @@ function wrap(text, max = 22) {
|
|
|
137
138
|
*/
|
|
138
139
|
function driftExpr(amp) {
|
|
139
140
|
const a = amp;
|
|
140
|
-
if (!a) return { x: "0", y: "0" };
|
|
141
|
+
if (!a) return { x: "0", y: "0", margin: 0 };
|
|
141
142
|
return {
|
|
143
|
+
margin: Math.abs(a) * 1.6,
|
|
142
144
|
x: `(${n(a * 1.0)}*sin(2*PI*t/7.3)+${n(a * 0.6)}*sin(2*PI*t/4.1+1.1))`,
|
|
143
145
|
y: `(${n(a * 0.8)}*sin(2*PI*t/5.9+0.4)+${n(a * 0.5)}*sin(2*PI*t/3.3))`,
|
|
144
146
|
};
|
|
@@ -224,8 +226,9 @@ export function buildGraph({ cards, gestures, geom, drift, videoOffsetMs, dotSiz
|
|
|
224
226
|
if (t1 <= 0) { input += 1; continue; }
|
|
225
227
|
// The pop: 14 pixels of rise over the first 90ms. A scale-up would be
|
|
226
228
|
// truer to the platform and costs a second scaled copy of every card.
|
|
229
|
+
const placement = captionPlacement(c, W, H, capY, drift.margin ?? 0);
|
|
227
230
|
const rise = `14*max(0,1-(t-${n(t0)})/0.09)`;
|
|
228
|
-
parts.push(`[${last}][${input}:v]overlay=x='(
|
|
231
|
+
parts.push(`[${last}][${input}:v]overlay=x='${n(placement.x, 1)}+${drift.x}':y='${n(placement.y, 1)}+${rise}+${drift.y}':enable='between(t,${n(Math.max(0, t0))},${n(t1)})'[v${input}]`);
|
|
229
232
|
last = `v${input}`;
|
|
230
233
|
input += 1;
|
|
231
234
|
}
|