hyperframes 0.1.7 → 0.1.9
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.9" : "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
|
|
@@ -24397,8 +24463,16 @@ async function handleVideoFile(videoPath, destDir, interactive) {
|
|
|
24397
24463
|
const transcode = await Jt({
|
|
24398
24464
|
message: "Transcode to H.264 MP4 for browser playback?",
|
|
24399
24465
|
options: [
|
|
24400
|
-
{
|
|
24401
|
-
|
|
24466
|
+
{
|
|
24467
|
+
value: "yes",
|
|
24468
|
+
label: "Yes, transcode",
|
|
24469
|
+
hint: "converts to H.264 MP4"
|
|
24470
|
+
},
|
|
24471
|
+
{
|
|
24472
|
+
value: "no",
|
|
24473
|
+
label: "No, keep original",
|
|
24474
|
+
hint: "video won't play in browser"
|
|
24475
|
+
}
|
|
24402
24476
|
]
|
|
24403
24477
|
});
|
|
24404
24478
|
if (Ct(transcode)) {
|
|
@@ -24484,7 +24558,11 @@ async function nextStepLoop(destDir) {
|
|
|
24484
24558
|
const next = await Jt({
|
|
24485
24559
|
message: "What do you want to do?",
|
|
24486
24560
|
options: [
|
|
24487
|
-
{
|
|
24561
|
+
{
|
|
24562
|
+
value: "dev",
|
|
24563
|
+
label: "Open in studio",
|
|
24564
|
+
hint: "full editor with timeline"
|
|
24565
|
+
},
|
|
24488
24566
|
{ value: "render", label: "Render to MP4", hint: "export video now" },
|
|
24489
24567
|
{ value: "done", label: "Done for now" }
|
|
24490
24568
|
]
|
|
@@ -24533,10 +24611,10 @@ var init_init = __esm({
|
|
|
24533
24611
|
description: `Scaffold a new composition project
|
|
24534
24612
|
|
|
24535
24613
|
Examples:
|
|
24536
|
-
hyperframes init my-video
|
|
24537
|
-
hyperframes init
|
|
24538
|
-
hyperframes init my-video --
|
|
24539
|
-
hyperframes init --
|
|
24614
|
+
hyperframes init my-video # interactive wizard
|
|
24615
|
+
hyperframes init my-video --template warm-grain # pick a template
|
|
24616
|
+
hyperframes init my-video --video video.mp4 # with video file
|
|
24617
|
+
hyperframes init my-video --non-interactive # skip prompts (CI/agents)`
|
|
24540
24618
|
},
|
|
24541
24619
|
args: {
|
|
24542
24620
|
name: { type: "positional", description: "Project name", required: false },
|
|
@@ -24545,11 +24623,28 @@ Examples:
|
|
|
24545
24623
|
description: `Template (${ALL_TEMPLATE_IDS.join(", ")})`,
|
|
24546
24624
|
alias: "t"
|
|
24547
24625
|
},
|
|
24548
|
-
video: {
|
|
24549
|
-
|
|
24550
|
-
|
|
24551
|
-
|
|
24552
|
-
|
|
24626
|
+
video: {
|
|
24627
|
+
type: "string",
|
|
24628
|
+
description: "Path to a video file (MP4, WebM, MOV)",
|
|
24629
|
+
alias: "V"
|
|
24630
|
+
},
|
|
24631
|
+
audio: {
|
|
24632
|
+
type: "string",
|
|
24633
|
+
description: "Path to an audio file (MP3, WAV, M4A)",
|
|
24634
|
+
alias: "a"
|
|
24635
|
+
},
|
|
24636
|
+
"skip-skills": {
|
|
24637
|
+
type: "boolean",
|
|
24638
|
+
description: "Skip AI coding skills installation"
|
|
24639
|
+
},
|
|
24640
|
+
"skip-transcribe": {
|
|
24641
|
+
type: "boolean",
|
|
24642
|
+
description: "Skip whisper transcription"
|
|
24643
|
+
},
|
|
24644
|
+
"non-interactive": {
|
|
24645
|
+
type: "boolean",
|
|
24646
|
+
description: "Disable interactive prompts (for CI/agents)"
|
|
24647
|
+
}
|
|
24553
24648
|
},
|
|
24554
24649
|
async run({ args }) {
|
|
24555
24650
|
const templateFlag = args.template;
|
|
@@ -24557,21 +24652,16 @@ Examples:
|
|
|
24557
24652
|
const audioFlag = args.audio;
|
|
24558
24653
|
const skipSkills = args["skip-skills"] === true;
|
|
24559
24654
|
const skipTranscribe = args["skip-transcribe"] === true;
|
|
24560
|
-
const
|
|
24561
|
-
|
|
24562
|
-
|
|
24563
|
-
|
|
24564
|
-
|
|
24565
|
-
console.error(`
|
|
24566
|
-
Example: hyperframes init my-video --template blank --video video.mp4`);
|
|
24567
|
-
process.exit(1);
|
|
24568
|
-
}
|
|
24569
|
-
if (!ALL_TEMPLATE_IDS.includes(templateFlag)) {
|
|
24570
|
-
console.error(c.error(`Unknown template: ${templateFlag}`));
|
|
24655
|
+
const nonInteractive = args["non-interactive"] === true;
|
|
24656
|
+
const interactive = !nonInteractive && process.stdout.isTTY === true;
|
|
24657
|
+
if (!interactive) {
|
|
24658
|
+
const resolvedTemplate = templateFlag ?? "blank";
|
|
24659
|
+
if (!ALL_TEMPLATE_IDS.includes(resolvedTemplate)) {
|
|
24660
|
+
console.error(c.error(`Unknown template: ${resolvedTemplate}`));
|
|
24571
24661
|
console.error(`Available: ${ALL_TEMPLATE_IDS.join(", ")}`);
|
|
24572
24662
|
process.exit(1);
|
|
24573
24663
|
}
|
|
24574
|
-
const templateId2 =
|
|
24664
|
+
const templateId2 = resolvedTemplate;
|
|
24575
24665
|
const name2 = args.name ?? "my-video";
|
|
24576
24666
|
const destDir2 = resolve14(name2);
|
|
24577
24667
|
if (existsSync26(destDir2) && readdirSync7(destDir2).length > 0) {
|
|
@@ -24706,7 +24796,11 @@ Example: hyperframes init my-video --template blank --video video.mp4`);
|
|
|
24706
24796
|
options: [
|
|
24707
24797
|
{ value: "video", label: "Video", hint: "MP4, WebM, MOV" },
|
|
24708
24798
|
{ value: "audio", label: "Audio only", hint: "MP3, WAV, M4A" },
|
|
24709
|
-
{
|
|
24799
|
+
{
|
|
24800
|
+
value: "no",
|
|
24801
|
+
label: "No",
|
|
24802
|
+
hint: "Start with motion graphics or text"
|
|
24803
|
+
}
|
|
24710
24804
|
],
|
|
24711
24805
|
initialValue: "no"
|
|
24712
24806
|
});
|
|
@@ -24763,7 +24857,9 @@ Example: hyperframes init my-video --template blank --video video.mp4`);
|
|
|
24763
24857
|
await ensureWhisper2({
|
|
24764
24858
|
onProgress: (msg) => spin.message(msg)
|
|
24765
24859
|
});
|
|
24766
|
-
await ensureModel2(void 0, {
|
|
24860
|
+
await ensureModel2(void 0, {
|
|
24861
|
+
onProgress: (msg) => spin.message(msg)
|
|
24862
|
+
});
|
|
24767
24863
|
spin.message("Transcribing audio...");
|
|
24768
24864
|
const { transcribe: runTranscribe } = await Promise.resolve().then(() => (init_transcribe(), transcribe_exports));
|
|
24769
24865
|
const transcribeResult = await runTranscribe(sourceFilePath, destDir, {
|
|
@@ -24779,21 +24875,29 @@ Example: hyperframes init my-video --template blank --video video.mp4`);
|
|
|
24779
24875
|
}
|
|
24780
24876
|
}
|
|
24781
24877
|
}
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24878
|
+
let templateId;
|
|
24879
|
+
if (templateFlag && ALL_TEMPLATE_IDS.includes(templateFlag)) {
|
|
24880
|
+
templateId = templateFlag;
|
|
24881
|
+
} else {
|
|
24882
|
+
if (templateFlag) {
|
|
24883
|
+
R2.warn(`Unknown template "${templateFlag}" \u2014 pick from the list below`);
|
|
24884
|
+
}
|
|
24885
|
+
const defaultTemplate = isAudioOnly ? "warm-grain" : "blank";
|
|
24886
|
+
const templateResult = await Jt({
|
|
24887
|
+
message: "Pick a template",
|
|
24888
|
+
options: TEMPLATES.map((t2) => ({
|
|
24889
|
+
value: t2.id,
|
|
24890
|
+
label: t2.label,
|
|
24891
|
+
hint: t2.hint
|
|
24892
|
+
})),
|
|
24893
|
+
initialValue: defaultTemplate
|
|
24894
|
+
});
|
|
24895
|
+
if (Ct(templateResult)) {
|
|
24896
|
+
Nt("Setup cancelled.");
|
|
24897
|
+
process.exit(0);
|
|
24898
|
+
}
|
|
24899
|
+
templateId = templateResult;
|
|
24795
24900
|
}
|
|
24796
|
-
const templateId = templateResult;
|
|
24797
24901
|
scaffoldProject(destDir, name, templateId, localVideoName, videoDuration);
|
|
24798
24902
|
trackInitTemplate(templateId);
|
|
24799
24903
|
const transcriptFile = resolve14(destDir, "transcript.json");
|
|
@@ -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.
|