hyperframes 0.1.7 → 0.1.8
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/dist/cli.js
CHANGED
|
@@ -422,7 +422,7 @@ var VERSION;
|
|
|
422
422
|
var init_version = __esm({
|
|
423
423
|
"src/version.ts"() {
|
|
424
424
|
"use strict";
|
|
425
|
-
VERSION = true ? "0.1.
|
|
425
|
+
VERSION = true ? "0.1.8" : "0.0.0-dev";
|
|
426
426
|
}
|
|
427
427
|
});
|
|
428
428
|
|
|
@@ -3499,6 +3499,37 @@ function lintHyperframeHtml(html, options = {}) {
|
|
|
3499
3499
|
fixHint: "Register each composition timeline on `window.__timelines[compositionId]`."
|
|
3500
3500
|
});
|
|
3501
3501
|
}
|
|
3502
|
+
if (TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) && !TIMELINE_REGISTRY_INIT_PATTERN.test(source)) {
|
|
3503
|
+
pushFinding({
|
|
3504
|
+
code: "timeline_registry_missing_init",
|
|
3505
|
+
severity: "error",
|
|
3506
|
+
message: "`window.__timelines[\u2026] = \u2026` is used without initializing `window.__timelines` first.",
|
|
3507
|
+
fixHint: "Add `window.__timelines = window.__timelines || {};` before any timeline assignment."
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
{
|
|
3511
|
+
const htmlCompIds = /* @__PURE__ */ new Set();
|
|
3512
|
+
const timelineRegKeys = /* @__PURE__ */ new Set();
|
|
3513
|
+
const compIdRe = /data-composition-id\s*=\s*["']([^"']+)["']/gi;
|
|
3514
|
+
const tlKeyRe = /window\.__timelines\[\s*["']([^"']+)["']\s*\]/g;
|
|
3515
|
+
let m;
|
|
3516
|
+
while ((m = compIdRe.exec(source)) !== null) {
|
|
3517
|
+
if (m[1]) htmlCompIds.add(m[1]);
|
|
3518
|
+
}
|
|
3519
|
+
while ((m = tlKeyRe.exec(source)) !== null) {
|
|
3520
|
+
if (m[1]) timelineRegKeys.add(m[1]);
|
|
3521
|
+
}
|
|
3522
|
+
for (const key2 of timelineRegKeys) {
|
|
3523
|
+
if (!htmlCompIds.has(key2)) {
|
|
3524
|
+
pushFinding({
|
|
3525
|
+
code: "timeline_id_mismatch",
|
|
3526
|
+
severity: "error",
|
|
3527
|
+
message: `Timeline registered as "${key2}" but no element has data-composition-id="${key2}". The runtime cannot auto-nest this timeline.`,
|
|
3528
|
+
fixHint: `Change window.__timelines["${key2}"] to match the data-composition-id attribute, or vice versa.`
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3502
3533
|
if (INVALID_SCRIPT_CLOSE_PATTERN.test(source)) {
|
|
3503
3534
|
pushFinding({
|
|
3504
3535
|
code: "invalid_inline_script_syntax",
|
|
@@ -23796,7 +23827,11 @@ __export(render_exports, {
|
|
|
23796
23827
|
default: () => render_default
|
|
23797
23828
|
});
|
|
23798
23829
|
import { existsSync as existsSync24, mkdirSync as mkdirSync16, statSync as statSync9 } from "fs";
|
|
23830
|
+
import { cpus as cpus2 } from "os";
|
|
23799
23831
|
import { resolve as resolve13, dirname as dirname12, join as join22 } from "path";
|
|
23832
|
+
function defaultWorkerCount() {
|
|
23833
|
+
return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT / 2), 4));
|
|
23834
|
+
}
|
|
23800
23835
|
async function renderDocker(projectDir, outputPath, options) {
|
|
23801
23836
|
const producer = await loadProducer();
|
|
23802
23837
|
const startTime = Date.now();
|
|
@@ -23810,7 +23845,11 @@ async function renderDocker(projectDir, outputPath, options) {
|
|
|
23810
23845
|
});
|
|
23811
23846
|
await producer.executeRenderJob(job, projectDir, outputPath);
|
|
23812
23847
|
} catch (error) {
|
|
23813
|
-
trackRenderError({
|
|
23848
|
+
trackRenderError({
|
|
23849
|
+
fps: options.fps,
|
|
23850
|
+
quality: options.quality,
|
|
23851
|
+
docker: true
|
|
23852
|
+
});
|
|
23814
23853
|
const message = error instanceof Error ? error.message : String(error);
|
|
23815
23854
|
errorBox("Render failed", message, "Check Docker is running: docker info");
|
|
23816
23855
|
process.exit(1);
|
|
@@ -23820,7 +23859,7 @@ async function renderDocker(projectDir, outputPath, options) {
|
|
|
23820
23859
|
durationMs: elapsed,
|
|
23821
23860
|
fps: options.fps,
|
|
23822
23861
|
quality: options.quality,
|
|
23823
|
-
workers: options.workers
|
|
23862
|
+
workers: options.workers,
|
|
23824
23863
|
docker: true,
|
|
23825
23864
|
gpu: options.gpu
|
|
23826
23865
|
});
|
|
@@ -23845,7 +23884,11 @@ async function renderLocal(projectDir, outputPath, options) {
|
|
|
23845
23884
|
try {
|
|
23846
23885
|
await producer.executeRenderJob(job, projectDir, outputPath, onProgress);
|
|
23847
23886
|
} catch (error) {
|
|
23848
|
-
trackRenderError({
|
|
23887
|
+
trackRenderError({
|
|
23888
|
+
fps: options.fps,
|
|
23889
|
+
quality: options.quality,
|
|
23890
|
+
docker: false
|
|
23891
|
+
});
|
|
23849
23892
|
const message = error instanceof Error ? error.message : String(error);
|
|
23850
23893
|
errorBox("Render failed", message, "Try --docker for containerized rendering");
|
|
23851
23894
|
process.exit(1);
|
|
@@ -23855,7 +23898,7 @@ async function renderLocal(projectDir, outputPath, options) {
|
|
|
23855
23898
|
durationMs: elapsed,
|
|
23856
23899
|
fps: options.fps,
|
|
23857
23900
|
quality: options.quality,
|
|
23858
|
-
workers: options.workers
|
|
23901
|
+
workers: options.workers,
|
|
23859
23902
|
docker: false,
|
|
23860
23903
|
gpu: options.gpu
|
|
23861
23904
|
});
|
|
@@ -23873,7 +23916,7 @@ function printRenderComplete(outputPath, elapsedMs, quiet) {
|
|
|
23873
23916
|
console.log(c.success("\u25C7") + " " + c.accent(outputPath));
|
|
23874
23917
|
console.log(" " + c.bold(fileSize) + c.dim(" \xB7 " + duration + " \xB7 completed"));
|
|
23875
23918
|
}
|
|
23876
|
-
var VALID_FPS, VALID_QUALITY, VALID_FORMAT, render_default;
|
|
23919
|
+
var VALID_FPS, VALID_QUALITY, VALID_FORMAT, CPU_CORE_COUNT, render_default;
|
|
23877
23920
|
var init_render = __esm({
|
|
23878
23921
|
"src/commands/render.ts"() {
|
|
23879
23922
|
"use strict";
|
|
@@ -23887,6 +23930,7 @@ var init_render = __esm({
|
|
|
23887
23930
|
VALID_FPS = /* @__PURE__ */ new Set([24, 30, 60]);
|
|
23888
23931
|
VALID_QUALITY = /* @__PURE__ */ new Set(["draft", "standard", "high"]);
|
|
23889
23932
|
VALID_FORMAT = /* @__PURE__ */ new Set(["mp4", "webm"]);
|
|
23933
|
+
CPU_CORE_COUNT = cpus2().length;
|
|
23890
23934
|
render_default = defineCommand({
|
|
23891
23935
|
meta: {
|
|
23892
23936
|
name: "render",
|
|
@@ -23899,19 +23943,45 @@ Examples:
|
|
|
23899
23943
|
hyperframes render --docker --output deterministic.mp4`
|
|
23900
23944
|
},
|
|
23901
23945
|
args: {
|
|
23902
|
-
dir: {
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
|
|
23946
|
+
dir: {
|
|
23947
|
+
type: "positional",
|
|
23948
|
+
description: "Project directory",
|
|
23949
|
+
required: false
|
|
23950
|
+
},
|
|
23951
|
+
output: {
|
|
23952
|
+
type: "string",
|
|
23953
|
+
description: "Output path (default: renders/<name>.mp4)"
|
|
23954
|
+
},
|
|
23955
|
+
fps: {
|
|
23956
|
+
type: "string",
|
|
23957
|
+
description: "Frame rate: 24, 30, 60",
|
|
23958
|
+
default: "30"
|
|
23959
|
+
},
|
|
23960
|
+
quality: {
|
|
23961
|
+
type: "string",
|
|
23962
|
+
description: "Quality: draft, standard, high",
|
|
23963
|
+
default: "standard"
|
|
23964
|
+
},
|
|
23906
23965
|
format: {
|
|
23907
23966
|
type: "string",
|
|
23908
23967
|
description: "Output format: mp4, webm (WebM renders with transparency)",
|
|
23909
23968
|
default: "mp4"
|
|
23910
23969
|
},
|
|
23911
|
-
workers: {
|
|
23912
|
-
|
|
23970
|
+
workers: {
|
|
23971
|
+
type: "string",
|
|
23972
|
+
description: "Parallel render workers (1-8 or 'auto'). Default: half your CPU cores, max 4. Each worker launches a separate Chrome process."
|
|
23973
|
+
},
|
|
23974
|
+
docker: {
|
|
23975
|
+
type: "boolean",
|
|
23976
|
+
description: "Use Docker for deterministic render",
|
|
23977
|
+
default: false
|
|
23978
|
+
},
|
|
23913
23979
|
gpu: { type: "boolean", description: "Use GPU encoding", default: false },
|
|
23914
|
-
quiet: {
|
|
23980
|
+
quiet: {
|
|
23981
|
+
type: "boolean",
|
|
23982
|
+
description: "Suppress verbose output",
|
|
23983
|
+
default: false
|
|
23984
|
+
}
|
|
23915
23985
|
},
|
|
23916
23986
|
async run({ args }) {
|
|
23917
23987
|
const project = resolveProject(args.dir);
|
|
@@ -23934,10 +24004,10 @@ Examples:
|
|
|
23934
24004
|
}
|
|
23935
24005
|
const format = formatRaw;
|
|
23936
24006
|
let workers;
|
|
23937
|
-
if (args.workers != null) {
|
|
24007
|
+
if (args.workers != null && args.workers !== "auto") {
|
|
23938
24008
|
const parsed = parseInt(args.workers, 10);
|
|
23939
24009
|
if (isNaN(parsed) || parsed < 1 || parsed > 8) {
|
|
23940
|
-
errorBox("Invalid workers", `Got "${args.workers}". Must be
|
|
24010
|
+
errorBox("Invalid workers", `Got "${args.workers}". Must be 1-8 or "auto".`);
|
|
23941
24011
|
process.exit(1);
|
|
23942
24012
|
}
|
|
23943
24013
|
workers = parsed;
|
|
@@ -23945,22 +24015,18 @@ Examples:
|
|
|
23945
24015
|
const rendersDir = resolve13("renders");
|
|
23946
24016
|
const ext = format === "webm" ? ".webm" : ".mp4";
|
|
23947
24017
|
const outputPath = args.output ? resolve13(args.output) : join22(rendersDir, `${project.name}${ext}`);
|
|
23948
|
-
|
|
23949
|
-
if (!existsSync24(outputDir)) {
|
|
23950
|
-
mkdirSync16(outputDir, { recursive: true });
|
|
23951
|
-
}
|
|
24018
|
+
mkdirSync16(dirname12(outputPath), { recursive: true });
|
|
23952
24019
|
const useDocker = args.docker ?? false;
|
|
23953
24020
|
const useGpu = args.gpu ?? false;
|
|
23954
24021
|
const quiet = args.quiet ?? false;
|
|
23955
|
-
const workerCount = workers ??
|
|
24022
|
+
const workerCount = workers ?? defaultWorkerCount();
|
|
23956
24023
|
if (!quiet) {
|
|
24024
|
+
const workerLabel = args.workers != null ? `${workerCount} workers` : `${workerCount} workers (auto \u2014 half of ${CPU_CORE_COUNT} cores)`;
|
|
23957
24025
|
console.log("");
|
|
23958
24026
|
console.log(
|
|
23959
24027
|
c.accent("\u25C6") + " Rendering " + c.accent(project.name) + c.dim(" \u2192 " + outputPath)
|
|
23960
24028
|
);
|
|
23961
|
-
console.log(
|
|
23962
|
-
c.dim(" " + fps + "fps \xB7 " + quality + " \xB7 " + workerCount + " workers")
|
|
23963
|
-
);
|
|
24029
|
+
console.log(c.dim(" " + fps + "fps \xB7 " + quality + " \xB7 " + workerLabel));
|
|
23964
24030
|
console.log("");
|
|
23965
24031
|
}
|
|
23966
24032
|
if (!useDocker) {
|
|
@@ -24007,7 +24073,7 @@ Examples:
|
|
|
24007
24073
|
fps,
|
|
24008
24074
|
quality,
|
|
24009
24075
|
format,
|
|
24010
|
-
workers,
|
|
24076
|
+
workers: workerCount,
|
|
24011
24077
|
gpu: useGpu,
|
|
24012
24078
|
quiet
|
|
24013
24079
|
});
|
|
@@ -24016,7 +24082,7 @@ Examples:
|
|
|
24016
24082
|
fps,
|
|
24017
24083
|
quality,
|
|
24018
24084
|
format,
|
|
24019
|
-
workers,
|
|
24085
|
+
workers: workerCount,
|
|
24020
24086
|
gpu: useGpu,
|
|
24021
24087
|
quiet,
|
|
24022
24088
|
browserPath
|
|
@@ -137,6 +137,7 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
|
|
|
137
137
|
- From sub-compositions, use `../` to reference root files
|
|
138
138
|
|
|
139
139
|
For PiP, title cards, and slide show patterns, see [patterns.md](./patterns.md).
|
|
140
|
+
For data, stats, and infographics, see [data-in-motion.md](./data-in-motion.md).
|
|
140
141
|
|
|
141
142
|
## Output Checklist
|
|
142
143
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Data in Motion
|
|
2
|
+
|
|
3
|
+
Light guidance for data and stats in video compositions. The [house style](./house-style.md) handles aesthetics — this just addresses data-specific pitfalls.
|
|
4
|
+
|
|
5
|
+
## Visual Continuity
|
|
6
|
+
|
|
7
|
+
When successive stats belong to the same concept (Q1 → Q2 → Q3 → Q4, or three metrics for the same product), keep them in the same visual space with the same aesthetic. Only the VALUE changes. An aesthetic change should signal a new concept, not just a new number.
|
|
8
|
+
|
|
9
|
+
## Numbers Need Visual Weight
|
|
10
|
+
|
|
11
|
+
A number on its own floats in empty space. Pair every metric with a visual element that gives it presence — a proportional fill bar, a background color shift, a shape that represents the value, a progress ring. The visual doesn't need to be a chart — it just needs to fill the frame and make the data feel tangible rather than just text on a background.
|
|
12
|
+
|
|
13
|
+
## Avoid Web Patterns
|
|
14
|
+
|
|
15
|
+
- **No pie charts** — hard to compare, looks like PowerPoint
|
|
16
|
+
- **No multi-axis charts** — viewer can't study intersections in a 3-second window
|
|
17
|
+
- **No 6-panel dashboards** — 2-3 related metrics side-by-side is fine, 6+ is a web pattern
|
|
18
|
+
- **No gridlines, tick marks, or legends** — visual noise that adds nothing in motion
|
|
19
|
+
- **No chart library output** — build with GSAP + SVG/CSS, not D3 or Chart.js
|
|
@@ -7,7 +7,8 @@ Defaults when no `visual-style.md` or animation direction is provided. These rai
|
|
|
7
7
|
1. **Interpret the prompt.** Generate real content for the topic — don't use the prompt text as body copy. A recipe lists real ingredients. A stats dashboard shows the actual numbers given. A product showcase names real features and specs. A sci-fi HUD has actual crosshairs and readouts, not a heading that says "sci-fi HUD."
|
|
8
8
|
2. **Pick a palette.** First decide: does this content call for a light or dark canvas? Food, weddings, children, wellness, education, lifestyle, nature, and celebrations → light palette (Warm/Editorial, Clean/Corporate, Nature/Earth, Pastel/Soft). Tech, finance, cinema, nightlife, horror, gaming, and premium → dark palette. Then load the file and pick one palette. Declare your bg, fg, and accent colors before writing any code.
|
|
9
9
|
3. **Pick a typeface.** Don't reach for Sora, Space Grotesk, Outfit, Playfair Display, Cormorant Garamond, or Bodoni Moda — they're overused. Explore the full range of Google Fonts. Serif for editorial, mono for technical, display for impact, handwritten for personal.
|
|
10
|
-
4. **Pick
|
|
10
|
+
4. **Pick a layout approach.** Don't default to the same structure every time. Options: full-bleed centered hero, left-aligned editorial column, split-frame (content left / visual right or vice versa), scattered/asymmetric positioning, grid-based with uneven cells, stacked vertical sections. Vary this across compositions.
|
|
11
|
+
5. **Pick your entrance patterns.** Plan how elements enter — never use the same entrance pattern twice in a composition.
|
|
11
12
|
|
|
12
13
|
## Motion
|
|
13
14
|
|
|
@@ -85,7 +86,7 @@ Flat single-color backgrounds look digital. Avoid pure solid backgrounds — add
|
|
|
85
86
|
|
|
86
87
|
Beyond choosing a typeface:
|
|
87
88
|
|
|
88
|
-
- **Weight contrast** — pair a heavy weight (700-900) headline with a light weight (300-400) body.
|
|
89
|
+
- **Weight contrast** — pair a heavy weight (700-900) headline with a light weight (300-400) body. Always use at least two explicit font-weight values — even with display fonts that look bold by default, set labels or secondary text to a lighter weight.
|
|
89
90
|
- **Case deliberately** — ALL CAPS for labels and short text (under 5 words). Sentence case for longer text. Don't uppercase paragraphs.
|
|
90
91
|
- **Tracking** — tight tracking (-0.02em) on large headlines. Normal or wide tracking on small labels.
|
|
91
92
|
- **One typeface, two weights** — don't mix typefaces unless you have a reason. One family at two weights creates more hierarchy than two families at one weight each.
|