@litfamily/litgrok 1.0.26 → 1.0.28
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/.grok/hooks/lit-mark.mjs +42 -0
- package/.grok/hooks/lit-status-line.mjs +174 -21
- package/.grok/hooks/plan-gate.mjs +13 -3
- package/.grok/hooks/record-passive-event.mjs +40 -6
- package/.grok/hooks/session-end.json +15 -0
- package/.grok/hooks/session-end.mjs +6 -0
- package/.grok/hooks/session-liveness.mjs +102 -0
- package/.grok/hooks/session-rows.mjs +57 -0
- package/.grok/hooks/session-start.mjs +17 -0
- package/.grok/hooks/workflow-stage.mjs +5 -2
- package/.grok/rules/00-litgrok.md +14 -0
- package/.grok/skills/frontend-ui-ux/ATTRIBUTION.md +23 -0
- package/.grok/skills/frontend-ui-ux/SKILL.md +4 -1
- package/.grok/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +9 -0
- package/.grok/skills/frontend-ui-ux/kit/components.css +414 -0
- package/.grok/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/.grok/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/.grok/skills/frontend-ui-ux/references/adaptive-layout.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/brand-and-imagery.md +11 -7
- package/.grok/skills/frontend-ui-ux/references/complete-contract.md +21 -0
- package/.grok/skills/frontend-ui-ux/references/composition.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/craft-floor.md +256 -93
- package/.grok/skills/frontend-ui-ux/references/creative-directions.md +9 -6
- package/.grok/skills/frontend-ui-ux/references/default-editorial-pixel.json +28 -0
- package/.grok/skills/frontend-ui-ux/references/house-styles.md +234 -0
- package/.grok/skills/frontend-ui-ux/references/implementation-platforms.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/inclusive-interface.md +11 -5
- package/.grok/skills/frontend-ui-ux/references/interaction-motion.md +34 -78
- package/.grok/skills/frontend-ui-ux/references/motion-guide.md +10 -7
- package/.grok/skills/frontend-ui-ux/references/performance-delivery.md +11 -8
- package/.grok/skills/frontend-ui-ux/references/probe-loop.md +1 -1
- package/.grok/skills/frontend-ui-ux/references/product-direction.md +9 -4
- package/.grok/skills/frontend-ui-ux/references/production-interview.md +17 -0
- package/.grok/skills/frontend-ui-ux/references/redesign-playbook.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/slop-register.md +148 -73
- package/.grok/skills/frontend-ui-ux/references/system-foundations.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/visual-language.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/visual-reconstruction.md +39 -72
- package/.grok/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-driver.mjs +33 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +1555 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +153 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-server.mjs +57 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +139 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe.mjs +363 -0
- package/.grok/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/.grok/skills/lit-adhd/SKILL.md +155 -0
- package/.grok/skills/lit-adhd/references/card-examples.md +131 -0
- package/.grok/skills/lit-adhd/references/status-taxonomy.md +50 -0
- package/.grok/skills/lit-adhd/scripts/lib/brief.mjs +421 -0
- package/.grok/skills/lit-adhd/scripts/lib/collect.mjs +395 -0
- package/.grok/skills/lit-adhd/scripts/lib/gauge.mjs +87 -0
- package/.grok/skills/lit-adhd/scripts/lib/next-step.mjs +22 -0
- package/.grok/skills/lit-adhd/scripts/lib/plan-task-rows.mjs +32 -0
- package/.grok/skills/lit-adhd/scripts/lib/project-state-root.mjs +55 -0
- package/.grok/skills/lit-adhd/scripts/lib/redact.mjs +64 -0
- package/.grok/skills/lit-adhd/scripts/lib/suggest.mjs +140 -0
- package/.grok/skills/lit-adhd/scripts/lib/transcripts.mjs +232 -0
- package/.grok/skills/lit-adhd/scripts/rewind.mjs +6 -0
- package/.grok/skills/lit-diagram-drawer/SKILL.md +17 -5
- package/.grok/skills/lit-diagram-drawer/references/output-spec.md +2 -2
- package/.grok/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/.grok/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/.grok/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/.grok/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/.grok/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/.grok/skills/lit-diagram-drawer/references/verifier-guide.md +4 -0
- package/.grok/skills/lit-diagram-drawer/scripts/visual-quality.mjs +2 -2
- package/.grok/skills/lit-handoff/SKILL.md +2 -0
- package/.grok/skills/lit-hijack/SKILL.md +459 -0
- package/.grok/skills/lit-hijack/references/capture-and-provenance.md +124 -0
- package/.grok/skills/lit-hijack/references/clean-room-build.md +130 -0
- package/.grok/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/.grok/skills/lit-hijack/references/gap-and-port-design.md +150 -0
- package/.grok/skills/lit-hijack/references/parity-and-report.md +130 -0
- package/.grok/skills/lit-hijack/scripts/hijack.mjs +890 -0
- package/.grok/skills/lit-hijack/scripts/lib/safe-fetch.mjs +213 -0
- package/.grok/skills/lit-plan/SKILL.md +3 -1
- package/.grok/skills/lit-plan/references/plan-schema.md +4 -0
- package/.grok/skills/lit-plan/scripts/scaffold-plan.mjs +9 -2
- package/.grok/skills/lit-plan/scripts/scope-roots.mjs +83 -0
- package/.grok/skills/lit-pptx/SKILL.md +10 -5
- package/.grok/skills/lit-pptx/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/.grok/skills/lit-pptx/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/.grok/skills/lit-pptx/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/.grok/skills/lit-pptx/examples/16-community-programme-riso-ko.md +158 -0
- package/.grok/skills/lit-pptx/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/provenance.json +1 -0
- package/.grok/skills/lit-pptx/references/direction-step.md +34 -8
- package/.grok/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
- package/.grok/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
- package/.grok/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
- package/.grok/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
- package/.grok/skills/lit-pptx/references/tonalities/riso.md +137 -0
- package/.grok/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/.grok/skills/lit-pptx/scripts/craft_extras.py +61 -3
- package/.grok/skills/lit-pptx/scripts/deck_output.py +76 -62
- package/.grok/skills/lit-pptx/scripts/embed_fonts.py +14 -1
- package/.grok/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
- package/.grok/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/.grok/skills/lit-pptx/scripts/lib/render-pack.js +389 -582
- package/.grok/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/.grok/skills/lit-pptx/scripts/lib/template-registry.js +60 -8
- package/.grok/skills/lit-pptx/scripts/qa_deck.py +4 -4
- package/.grok/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/.grok/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/.grok/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/night/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/paper/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/signal/pack.yaml +3 -0
- package/.grok/skills/lit-pptx/templates/tonalities/studio/pack.yaml +2 -0
- package/.grok/skills/lit-recap/SKILL.md +1 -1
- package/.grok/skills/lit-schematic/SKILL.md +140 -0
- package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/.grok/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/.grok/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/.grok/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/.grok/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/.grok/skills/lit-schematic/references/journal-figures.md +72 -0
- package/.grok/skills/lit-schematic/references/kit-api.md +134 -0
- package/.grok/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/.grok/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/.grok/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/.grok/skills/lit-schematic/references/verification.md +100 -0
- package/.grok/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/.grok/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/.grok/skills/lit-schematic/scripts/lib/fonts.mjs +27 -0
- package/.grok/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/.grok/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/.grok/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/.grok/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/.grok/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/.grok/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/.grok/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/.grok/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/.grok/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/.grok/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/.grok/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/.grok/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/.grok/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/.grok/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/.grok/skills/lit-schematic/scripts/render.mjs +233 -0
- package/.grok/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/.grok/skills/lit-scientific-visualization/SKILL.md +36 -12
- package/.grok/skills/lit-typographic-motion/SKILL.md +7 -4
- package/.grok/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +11 -0
- package/.grok/skills/lit-typographic-motion/recipes/hero-3d/expected.json +62 -0
- package/.grok/skills/lit-typographic-motion/recipes/hero-3d/index.html +82 -0
- package/.grok/skills/lit-typographic-motion/references/choice-first.md +30 -0
- package/.grok/skills/lit-typographic-motion/references/cover.md +10 -1
- package/.grok/skills/lit-typographic-motion/references/depth.md +119 -0
- package/.grok/skills/lit-typographic-motion/references/treatment.md +9 -1
- package/.grok/skills/lit-typographic-motion/scripts/blender/runner.py +218 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/assets.mjs +230 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/blender.mjs +136 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/board.mjs +153 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/chrome.mjs +61 -32
- package/.grok/skills/lit-typographic-motion/scripts/lib/constants.mjs +4 -2
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-capture.mjs +20 -11
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-treatment.mjs +26 -2
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover.mjs +12 -5
- package/.grok/skills/lit-typographic-motion/scripts/lib/frame-pool.mjs +78 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/gallery.mjs +160 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/house.mjs +43 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/look.mjs +39 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/scaffold.mjs +174 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/score.mjs +640 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/sound.mjs +143 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/soundtrack.mjs +12 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-gate.mjs +44 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-qa.mjs +132 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-session.mjs +51 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage.mjs +74 -9
- package/.grok/skills/lit-typographic-motion/scripts/lib/styles.mjs +224 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/treatment.mjs +198 -4
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/LICENSE +21 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/three.module.js +54571 -0
- package/.grok/skills/lit-typographic-motion/scripts/motion.mjs +56 -6
- package/.grok/skills/lit-typographic-motion/scripts/stage/cover-init.js +20 -8
- package/.grok/skills/lit-typographic-motion/scripts/stage/loop-kit.js +2 -2
- package/.grok/skills/lit-typographic-motion/scripts/stage/stage-kit.js +25 -0
- package/.grok/skills/lit-typographic-motion/styles/paper.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/swiss-signal.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/terminalcore.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/tidal.json +1 -0
- package/.grok/skills/litgrok/SKILL.md +1 -1
- package/.grok/skills/litresearch/SKILL.md +2 -0
- package/.grok/skills/readme-studio/SKILL.md +14 -12
- package/.grok/skills/readme-studio/references/choice-first.md +24 -0
- package/.grok/skills/start-work/SKILL.md +8 -1
- package/.grok-plugin/plugin.json +4 -1
- package/CHANGELOG.md +18 -0
- package/README.md +11 -11
- package/README_ko-KR.md +11 -11
- package/TRADEMARK.md +12 -0
- package/bin/litgrok.mjs +2 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/showcase/brief.webp +0 -0
- package/docs/assets/showcase/deck.webp +0 -0
- package/docs/assets/showcase/page.webp +0 -0
- package/docs/assets/showcase/pid.webp +0 -0
- package/docs/assets/skills/lit-adhd.webp +0 -0
- package/docs/assets/skills/lit-hijack.webp +0 -0
- package/docs/assets/skills/lit-schematic.webp +0 -0
- package/docs/reference.md +11 -7
- package/docs/reference_ko-KR.md +11 -7
- package/hooks/hooks.json +11 -0
- package/package.json +6 -1
- package/plugin.json +1 -1
- package/.grok/skills/frontend-ui-ux/scripts/page-probe.mjs +0 -105
- package/.grok/skills/frontend-ui-ux/scripts/probe.mjs +0 -91
- package/.grok/skills/frontend-ui-ux/scripts/rule-data.mjs +0 -56
- package/.grok/skills/frontend-ui-ux/scripts/static-probe.mjs +0 -27
- package/.grok/skills/frontend-ui-ux/scripts/static-server.mjs +0 -19
|
@@ -12,6 +12,7 @@ import { FlashDetector } from './flash.mjs';
|
|
|
12
12
|
import { FramePool, measureFrame } from './frame-pool.mjs';
|
|
13
13
|
import { encodePng } from './png.mjs';
|
|
14
14
|
import { CONTENT_TYPES, openStage } from './stage-session.mjs';
|
|
15
|
+
import { readPlate } from './blender.mjs';
|
|
15
16
|
import { stillsPlan, writeStills } from './stills.mjs';
|
|
16
17
|
import { startEncode } from './encode.mjs';
|
|
17
18
|
import { encodePreview, findTool, previewRung, promoteOrWithhold, clearDeliverables } from './render.mjs';
|
|
@@ -73,6 +74,10 @@ export function scanStage(stageDir) {
|
|
|
73
74
|
const warnings = [];
|
|
74
75
|
if (!existsSync(join(stageDir, 'index.html'))) return { contract: ['stage/index.html is missing'], network, warnings, files: [], rasters: [] };
|
|
75
76
|
const files = walk(stageDir);
|
|
77
|
+
// A Blender plate and its preview stills are frames the runner wrote and plate.json lists by name
|
|
78
|
+
// and size; they are not page rasters. Anything not listed is held to the raster limits.
|
|
79
|
+
const plates = [readPlate(stageDir, 'plate'), readPlate(stageDir, 'plate-preview')].filter(Boolean);
|
|
80
|
+
const listed = new Set(plates.flatMap((p) => p.frames.map((f) => `${p.dir}/${String(f.name)}`)));
|
|
76
81
|
const rasters = [];
|
|
77
82
|
for (const file of files) {
|
|
78
83
|
if (file.symlink) continue;
|
|
@@ -93,7 +98,7 @@ export function scanStage(stageDir) {
|
|
|
93
98
|
const hint = text.match(/<link[^>]+rel\s*=\s*["']?(preconnect|prefetch|dns-prefetch|prerender)\b/i);
|
|
94
99
|
if (hint) network.push(`stage/${file.rel}: <link rel=${hint[1]}>`);
|
|
95
100
|
}
|
|
96
|
-
if (RASTER_EXT.has(ext)) {
|
|
101
|
+
if (RASTER_EXT.has(ext) && !listed.has(file.rel)) {
|
|
97
102
|
const bytes = readFileSync(file.path);
|
|
98
103
|
let info;
|
|
99
104
|
try { info = rasterInfo(bytes, ext === '.jpeg' ? '.jpg' : ext); } catch { info = { width: 0, height: 0, animated: false }; }
|
|
@@ -244,18 +249,31 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
|
|
|
244
249
|
if (scan.contract.length) fail(new BlockedError(EXIT.STAGE_CONTRACT, `STAGE_CONTRACT_ERROR: ${scan.contract[0]}`));
|
|
245
250
|
let fonts;
|
|
246
251
|
try { fonts = stageFonts(treatment.typePlan.faces, env); } catch (error) { fail(error); }
|
|
252
|
+
let style = null;
|
|
253
|
+
if (treatment.style) {
|
|
254
|
+
const { readStyle, styleCss, styleScript } = await import('./styles.mjs');
|
|
255
|
+
const declared = readStyle(treatment.style);
|
|
256
|
+
style = { css: styleCss(declared, treatment.format), js: styleScript(declared, treatment.format, treatment) };
|
|
257
|
+
}
|
|
247
258
|
const executable = resolveChrome(env);
|
|
248
259
|
if (!executable) fail(new BlockedError(EXIT.NO_CHROME, 'BLOCKED_NO_CHROME: Chrome/Chromium not found; set CHROME_PATH to a Chrome executable'));
|
|
260
|
+
// The browser rung: a three.js world asks for the GPU; a Blender film shows its path-traced
|
|
261
|
+
// plate under HTML type, so its page needs only the software rung.
|
|
262
|
+
const rung = stageRung(treatment);
|
|
249
263
|
const ffmpeg = findTool('ffmpeg', env);
|
|
250
264
|
const seed = Number.isInteger(treatment.seed) ? treatment.seed : fnv1a32(treatment.idea);
|
|
251
265
|
const warnings = [...scan.warnings];
|
|
252
266
|
|
|
253
267
|
const pool = new FramePool();
|
|
254
268
|
let master = null;
|
|
269
|
+
// The rung the master page actually got (a GPU ask can fall back to software); the replay, the
|
|
270
|
+
// QA and the manifest reuse it, not the requested rung.
|
|
271
|
+
let masterRung = null, masterRenderer = null, masterArgs = null;
|
|
255
272
|
let encoder = null;
|
|
256
273
|
let abortReplays = () => {};
|
|
257
274
|
try {
|
|
258
|
-
master = await openStage({ executable, outDir: out, stageDir, width, height, fps: 60, seed, fonts, label: 'm', log });
|
|
275
|
+
master = await openStage({ executable, outDir: out, stageDir, width, height, fps: 60, seed, fonts, style, label: 'm', rung, log });
|
|
276
|
+
if (master.notice) { warnings.push(master.notice); log(master.notice); }
|
|
259
277
|
const prepared = await master.load();
|
|
260
278
|
const contractError = contractCheck(prepared.contract, treatment, width, height);
|
|
261
279
|
if (contractError) throw new BlockedError(EXIT.STAGE_CONTRACT, `STAGE_CONTRACT_ERROR: ${contractError}`);
|
|
@@ -286,7 +304,8 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
|
|
|
286
304
|
// The QA replay only reads layout between samples and settles before its own captures.
|
|
287
305
|
const stop = { aborted: false };
|
|
288
306
|
const replay = async () => {
|
|
289
|
-
|
|
307
|
+
// The replay shares the master's rung: a replay on another rung would flag the rung, not the page.
|
|
308
|
+
const session = await openStage({ executable, outDir: out, stageDir, width, height, fps, seed, fonts, style, label: 'd', rung: masterRung ?? rung, log });
|
|
290
309
|
try {
|
|
291
310
|
await session.load();
|
|
292
311
|
const hashes = {};
|
|
@@ -309,7 +328,7 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
|
|
|
309
328
|
const result = await job;
|
|
310
329
|
if (!stillsOnly) {
|
|
311
330
|
const flash = detector.push(result.grid);
|
|
312
|
-
frames.push({ frame: f, ink: 0, lumP995: Math.round(result.lumP995 * 10000) / 10000, flash: { general: flash.general, red: flash.red, ...(flash.general2 ? { general2: flash.general2 } : {}), ...(flash.red2 ? { red2: flash.red2 } : {}), stepArea: flash.stepArea }, rgbaSha256: result.sha256 });
|
|
331
|
+
frames.push({ frame: f, ink: 0, lumP995: Math.round(result.lumP995 * 10000) / 10000, lumP005: Math.round(result.lumP005 * 10000) / 10000, busy: Math.round(result.busy * 10000) / 10000, flash: { general: flash.general, red: flash.red, ...(flash.general2 ? { general2: flash.general2 } : {}), ...(flash.red2 ? { red2: flash.red2 } : {}), stepArea: flash.stepArea }, rgbaSha256: result.sha256 });
|
|
313
332
|
await encoder?.write(result.rgba);
|
|
314
333
|
}
|
|
315
334
|
if (keep.has(f)) kept.get(f).rgba = result.rgba;
|
|
@@ -330,6 +349,9 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
|
|
|
330
349
|
while (queue.length) await drain();
|
|
331
350
|
const masterDone = Date.now();
|
|
332
351
|
const pageReport = await master.report();
|
|
352
|
+
masterRung = master.rung;
|
|
353
|
+
masterRenderer = master.renderer;
|
|
354
|
+
masterArgs = master.args;
|
|
333
355
|
await master.close();
|
|
334
356
|
master = null;
|
|
335
357
|
if (pageReport.errors.length) warnings.push(`the page logged ${pageReport.errors.length} error(s); first: ${pageReport.errors[0]}`);
|
|
@@ -340,8 +362,27 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
|
|
|
340
362
|
record({ exitCode: EXIT.NO_FFMPEG_FOR_VIDEO, state: 'blocked', reason: 'no ffmpeg', stillsIndexSha256: stills.sha256 });
|
|
341
363
|
throw new BlockedError(EXIT.NO_FFMPEG_FOR_VIDEO, 'BLOCKED_NO_FFMPEG_FOR_VIDEO: ffmpeg not found on PATH; the stills and the contact sheet were written, install ffmpeg for the film');
|
|
342
364
|
}
|
|
365
|
+
// The board round: judge the page's text on a QA replay, draw the storyboard, and stop when
|
|
366
|
+
// copy overlaps, leaves the style, or leaves its zone — fix the page, not the gate.
|
|
367
|
+
const boardQa = await runStageQa({ executable, out, stageDir, width, height, fps, seed, fonts, treatment, totalFrames, log, stepOrThrow, captureChecked, rung: masterRung, stop });
|
|
368
|
+
const { writeBoard, addToStills } = await import('./board.mjs');
|
|
369
|
+
const { assetUsed } = await import('./assets.mjs');
|
|
370
|
+
const { createHash } = await import('node:crypto');
|
|
371
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
372
|
+
const beatFiles = plan.mids.map((m) => join(out, 'stills', `beat-${pad2(m.beat + 1)}-mid.png`));
|
|
373
|
+
const assetRule = assetUsed(treatment, stageDir);
|
|
374
|
+
const check = { schema: 1, path: 'stage', round, rules: [...boardQa.results.filter((r) => /^(TEXT-|RICH-FILL)/u.test(r.id)), ...(assetRule ? [assetRule] : [])] };
|
|
375
|
+
writeFileSync(join(stageDir, 'check.json'), `${JSON.stringify(check, null, 2)}\n`);
|
|
376
|
+
const boardFile = await writeBoard({ out, treatment, beatFiles, check, env, log });
|
|
377
|
+
const sha = (b) => createHash('sha256').update(b).digest('hex');
|
|
378
|
+
addToStills(out, { file: 'stills/board.png', kind: 'board', check: 'stills' }, { sha256: sha });
|
|
379
|
+
const failed = check.rules.filter((x) => x.status === 'FAIL');
|
|
380
|
+
if (failed.length) {
|
|
381
|
+
record({ exitCode: EXIT.GATE_FAIL_QA, state: 'check-fail', stillsIndexSha256: stills.sha256, frames: lastNeeded + 1, perf: { p50Ms: pct(times, 0.5), p95Ms: pct(times, 0.95), totalSec: (Date.now() - started) / 1000 } });
|
|
382
|
+
throw new BlockedError(EXIT.GATE_FAIL_QA, `BOARD_CHECK_FAIL: ${failed.map((x) => `${x.id} ${x.detail}`).join('; ')}. Fix the page, render --stills-only again, then look at ${boardFile ?? join(out, 'stills', 'board.png')}`);
|
|
383
|
+
}
|
|
343
384
|
record({ exitCode: EXIT.OK, state: 'stills-only', stillsIndexSha256: stills.sha256, frames: lastNeeded + 1, perf: { p50Ms: pct(times, 0.5), p95Ms: pct(times, 0.95), totalSec: (Date.now() - started) / 1000 } });
|
|
344
|
-
log(`stills-only: ${stills.
|
|
385
|
+
log(`stills-only: board check PASS; open the storyboard ${boardFile ?? join(out, 'stills', 'board.png')} and every file in ${join(out, 'stills', 'index.json')}, record the look with look --round ${round}, make the change, then render the film with --round ${round + 1} (no --stills-only)`);
|
|
345
386
|
return { exitCode: EXIT.OK, stillsOnly: true, stills };
|
|
346
387
|
}
|
|
347
388
|
|
|
@@ -358,7 +399,7 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
|
|
|
358
399
|
previewJob.catch(() => {});
|
|
359
400
|
const [rerun, qa] = await Promise.all([
|
|
360
401
|
replay(),
|
|
361
|
-
runStageQa({ executable, out, stageDir, width, height, fps, seed, fonts, treatment, totalFrames, log, stepOrThrow, captureChecked, stop }),
|
|
402
|
+
runStageQa({ executable, out, stageDir, width, height, fps, seed, fonts, treatment, totalFrames, log, stepOrThrow, captureChecked, rung: masterRung, stop }),
|
|
362
403
|
]);
|
|
363
404
|
timings.replaysSec = (Date.now() - replayStart) / 1000;
|
|
364
405
|
const masterHash = new Map(frames.map((frame) => [frame.frame, frame.rgbaSha256]));
|
|
@@ -371,13 +412,29 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
|
|
|
371
412
|
if (qa.minCopyFontPx && qa.minCopyFontPx < 32) preview = await encodePreview({ runDir, master: join(runDir, 'film.mp4'), encoder: previewRung(env), minFontPx: qa.minCopyFontPx, env, log, frame: { width, height } });
|
|
372
413
|
const previewRecords = preview.records ? preview.records.map((r) => ({ preview: { frame: r.frame, fps: preview.fps }, flash: { general: r.general, red: r.red, ...(r.general2 ? { general2: r.general2 } : {}), ...(r.red2 ? { red2: r.red2 } : {}) } })) : [];
|
|
373
414
|
const sound = muxGate({ mp4: join(runDir, 'film.mp4'), durationSec: totalFrames / fps, treatment, env, track });
|
|
415
|
+
// A film about a named product shows its real assets; without a product there is no verdict.
|
|
416
|
+
const { assetUsed } = await import('./assets.mjs');
|
|
417
|
+
const assetRule = assetUsed(treatment, stageDir);
|
|
418
|
+
if (assetRule) qa.results.push(assetRule);
|
|
374
419
|
|
|
420
|
+
// The manifest names the rung the master page got and its renderer string, so the report and
|
|
421
|
+
// the determinism replay can be read against the same hardware.
|
|
422
|
+
const { isSoftwareRenderer } = await import('./chrome.mjs');
|
|
375
423
|
const manifest = {
|
|
376
424
|
schemaVersion: 1, path: 'stage', format: treatment.format, resolution: [width, height], fps, durationSec: totalFrames / fps, targetDurationSec: treatment.durationSec,
|
|
377
|
-
frames: totalFrames, seed, renderer:
|
|
425
|
+
frames: totalFrames, seed, rung: masterRung, renderer: `${masterRenderer} on the ${masterRung} rung (stage path)`, softwareRenderer: masterRung === 'software' || isSoftwareRenderer(masterRenderer), chromeFlags: masterArgs.filter((arg) => !arg.startsWith('--user-data-dir') && !arg.startsWith('--window-size') && arg !== 'about:blank'), previewEncoder: preview.encoder,
|
|
378
426
|
sound: sound.summary, round, warnings,
|
|
379
427
|
};
|
|
380
428
|
const stills = writeStills(out, { plan, frames: kept, width, height, path: 'stage', round, mode: 'full', poster: { bytes: posterBytes, frame: posterFrame }, treatmentSha256 });
|
|
429
|
+
// The final render ships its storyboard too: the done-check wants the board viewed on the final set.
|
|
430
|
+
const { writeBoard, addToStills } = await import('./board.mjs');
|
|
431
|
+
const { createHash } = await import('node:crypto');
|
|
432
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
433
|
+
const beatFiles = plan.mids.map((m) => join(out, 'stills', `beat-${pad2(m.beat + 1)}-mid.png`));
|
|
434
|
+
const boardCheck = { schema: 1, path: 'stage', round, rules: [...qa.results.filter((r) => /^(TEXT-|RICH-FILL)/u.test(r.id)), ...(assetRule ? [assetRule] : [])] };
|
|
435
|
+
await writeBoard({ out, treatment, beatFiles, check: boardCheck, env, log });
|
|
436
|
+
const sha = (b) => createHash('sha256').update(b).digest('hex');
|
|
437
|
+
stills.sha256 = addToStills(out, { file: 'stills/board.png', kind: 'board', check: 'stills' }, { sha256: sha });
|
|
381
438
|
manifest.stillsIndexSha256 = stills.sha256;
|
|
382
439
|
clearDeliverables(out);
|
|
383
440
|
writeFileSync(join(out, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
@@ -415,8 +472,16 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
|
|
|
415
472
|
}
|
|
416
473
|
}
|
|
417
474
|
|
|
418
|
-
|
|
419
|
-
|
|
475
|
+
// The browser rung for a treatment: a three.js world asks for the platform GPU rung, a Blender
|
|
476
|
+
// film shows its path-traced plate under HTML type on the software rung, and anything else renders
|
|
477
|
+
// on the software rung where a determinism mismatch is always a page leak.
|
|
478
|
+
export function stageRung(treatment) {
|
|
479
|
+
if (treatment.render3d === 'blender') return 'software';
|
|
480
|
+
return treatment.depth === '3d' ? 'gpu' : 'software';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function stageFlagsUsed(rung = 'software') {
|
|
484
|
+
return stageArgs({ profileDir: '<profile>', width: 0, height: 0, rung }).filter((arg) => !arg.startsWith('--user-data-dir') && !arg.startsWith('--window-size') && arg !== 'about:blank');
|
|
420
485
|
}
|
|
421
486
|
|
|
422
487
|
// `gate --out <dir>` on a stage render: re-evaluate the recorded facts against the exports where they
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// House styles as data. Four JSON documents under styles/ carry each style's palette, type
|
|
2
|
+
// scale, motion tokens, transitions, ground, default bed, bed list and bans; this module loads,
|
|
3
|
+
// validates and applies them. treatments name a style id; pickStyle() resolves one by rule.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
export const STYLE_SCHEMA = "litgrok.motion-style/v1";
|
|
8
|
+
export const STYLE_IDS = Object.freeze(["swiss-signal", "tidal", "terminalcore", "paper"]);
|
|
9
|
+
export const DEFAULT_STYLE = "swiss-signal";
|
|
10
|
+
export const TYPE_ROLES = Object.freeze(["display", "headline", "body", "label"]);
|
|
11
|
+
export const TRANSITIONS = Object.freeze(["cut", "match", "wipe", "push", "iris", "dissolve", "morph"]);
|
|
12
|
+
// Copy may sit this far (share of frame side) outside its zone, and this far off a scale step.
|
|
13
|
+
export const ZONE_TOLERANCE = 0.012;
|
|
14
|
+
export const SIZE_TOLERANCE = 0.04;
|
|
15
|
+
|
|
16
|
+
// [x, y, w, h] as frame fractions. `type` bounds copy; `subject` bounds the drawing.
|
|
17
|
+
export const LAYOUTS = Object.freeze({
|
|
18
|
+
"16:9": Object.freeze({
|
|
19
|
+
title: { type: [0.08, 0.2, 0.84, 0.6], subject: [0, 0, 1, 1] },
|
|
20
|
+
split: { type: [0.08, 0.16, 0.36, 0.68], subject: [0.48, 0.08, 0.46, 0.84] },
|
|
21
|
+
"split-alt": { type: [0.56, 0.16, 0.36, 0.68], subject: [0.06, 0.08, 0.46, 0.84] },
|
|
22
|
+
stage: { type: [0.08, 0.75, 0.84, 0.18], subject: [0.06, 0.07, 0.88, 0.64] },
|
|
23
|
+
hero: { type: [0.08, 0.76, 0.84, 0.17], subject: [0.03, 0.03, 0.94, 0.94] },
|
|
24
|
+
full: { type: null, subject: [0, 0, 1, 1] },
|
|
25
|
+
}),
|
|
26
|
+
"9:16": Object.freeze({
|
|
27
|
+
title: { type: [0.09, 0.26, 0.82, 0.48], subject: [0, 0, 1, 1] },
|
|
28
|
+
split: { type: [0.09, 0.08, 0.82, 0.26], subject: [0.06, 0.36, 0.88, 0.56] },
|
|
29
|
+
"split-alt": { type: [0.09, 0.66, 0.82, 0.26], subject: [0.06, 0.06, 0.88, 0.56] },
|
|
30
|
+
stage: { type: [0.09, 0.79, 0.82, 0.13], subject: [0.06, 0.06, 0.88, 0.68] },
|
|
31
|
+
hero: { type: [0.09, 0.8, 0.82, 0.12], subject: [0.03, 0.03, 0.94, 0.94] },
|
|
32
|
+
full: { type: null, subject: [0, 0, 1, 1] },
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
export const LAYOUT_IDS = Object.freeze(Object.keys(LAYOUTS["16:9"]));
|
|
36
|
+
const FRAME = Object.freeze({ "16:9": [1920, 1080], "9:16": [1080, 1920] });
|
|
37
|
+
|
|
38
|
+
// With no name, house file or keyword, the genre picks where one reads better: a warm legible
|
|
39
|
+
// ground for explainers (stage only), the tidal flow for brand moods.
|
|
40
|
+
export const GENRE_STYLE = Object.freeze({ explainer: "paper", "brand-mood": "tidal" });
|
|
41
|
+
|
|
42
|
+
// Checked in order; the first row with a hit wins. A style the request names outright wins over
|
|
43
|
+
// every row. Paper has no type-engine preset, so the type path skips its row.
|
|
44
|
+
export const STYLE_PICK = Object.freeze([
|
|
45
|
+
{ style: "terminalcore", korean: ["터미널", "CRT", "해커"], latin: ["terminal", "hacker", "crt"] },
|
|
46
|
+
{ style: "tidal", korean: ["물결", "파도", "잔잔한", "흐름"], latin: ["gradient", "wave", "tide", "calm"] },
|
|
47
|
+
{ style: "paper", korean: ["따뜻한", "밝은", "종이", "손그림", "포근한", "아기자기"], latin: ["warm", "cozy", "bright", "paper", "handmade", "hand-drawn"] },
|
|
48
|
+
]);
|
|
49
|
+
const NAMED = Object.freeze({
|
|
50
|
+
"swiss-signal": ["swiss-signal", "swiss signal", "스위스 시그널"],
|
|
51
|
+
tidal: ["tidal", "타이달"],
|
|
52
|
+
terminalcore: ["terminalcore", "터미널코어"],
|
|
53
|
+
paper: ["paper style", "paper look", "페이퍼 스타일", "페이퍼"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export class StyleError extends Error {}
|
|
57
|
+
|
|
58
|
+
const HEX = /^#[0-9a-f]{6}$/iu;
|
|
59
|
+
const isText = (v) => typeof v === "string" && v.trim().length > 0;
|
|
60
|
+
const linear = (hex) => [1, 3, 5].map((i) => {
|
|
61
|
+
const s = parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
62
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
63
|
+
});
|
|
64
|
+
const luminance = (hex) => {
|
|
65
|
+
const [r, g, b] = linear(hex);
|
|
66
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
67
|
+
};
|
|
68
|
+
const ratio = (la, lb) => (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
69
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
70
|
+
const keywordHit = (text, keyword, korean) => {
|
|
71
|
+
const pattern = korean
|
|
72
|
+
? new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(keyword)}`, "u")
|
|
73
|
+
: new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(keyword)}(?![\\p{L}\\p{N}])`, "iu");
|
|
74
|
+
return pattern.test(text);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** A style document is valid, or throws StyleError naming the first bad field. */
|
|
78
|
+
export function validateStyle(style) {
|
|
79
|
+
const fail = (field, message) => {
|
|
80
|
+
throw new StyleError(`style ${style?.id ?? "?"}: ${field}: ${message}`);
|
|
81
|
+
};
|
|
82
|
+
if (!style || typeof style !== "object") fail("style", "must be an object");
|
|
83
|
+
if (style.schema !== STYLE_SCHEMA) fail("schema", `must be ${STYLE_SCHEMA}`);
|
|
84
|
+
if (!STYLE_IDS.includes(style.id)) fail("id", `must be one of ${STYLE_IDS.join(", ")}`);
|
|
85
|
+
for (const k of ["name", "summary"]) if (!isText(style[k])) fail(k, "must be a non-empty string");
|
|
86
|
+
const p = style.palette ?? {};
|
|
87
|
+
for (const role of ["ground", "surface", "ink", "muted", "signal", "line"]) if (!HEX.test(p[role] ?? "")) fail(`palette.${role}`, "must be #rrggbb");
|
|
88
|
+
if (!Array.isArray(style.textRoles) || style.textRoles.some((r) => !HEX.test(p[r] ?? ""))) fail("textRoles", "every text role names a palette colour");
|
|
89
|
+
const ground = luminance(p.ground);
|
|
90
|
+
if (ratio(luminance(p.ink), ground) < 7) fail("palette.ink", "ink on ground needs 7:1");
|
|
91
|
+
if (ratio(luminance(p.muted), ground) < 4.5) fail("palette.muted", "muted on ground needs 4.5:1");
|
|
92
|
+
for (const role of ["signal", "accent"]) if (p[role] && ratio(luminance(p[role]), ground) < 3) fail(`palette.${role}`, `${role} on ground needs 3:1 (large type)`);
|
|
93
|
+
if (!Array.isArray(style.accents) || style.accents.length < 3) fail("accents", "names at least three role accents a rich film may add");
|
|
94
|
+
style.accents.forEach((a, i) => {
|
|
95
|
+
if (!isText(a?.role)) fail(`accents[${i}].role`, "says what the colour marks");
|
|
96
|
+
if (!HEX.test(a?.color ?? "")) fail(`accents[${i}].color`, "must be #rrggbb");
|
|
97
|
+
if (ratio(luminance(a.color), ground) < 3) fail(`accents[${i}].color`, "an accent on ground needs 3:1 (large type)");
|
|
98
|
+
});
|
|
99
|
+
for (const role of TYPE_ROLES) if (!isText(style.faces?.[role]?.family)) fail(`faces.${role}`, "needs a family");
|
|
100
|
+
for (const format of Object.keys(FRAME)) {
|
|
101
|
+
const s = style.scale?.[format];
|
|
102
|
+
if (!s || TYPE_ROLES.some((r) => !(s[r] > 0))) fail(`scale.${format}`, "needs a size per type role");
|
|
103
|
+
if (!(s.display > s.headline && s.headline > s.body && s.body > s.label)) fail(`scale.${format}`, "steps must descend display > headline > body > label");
|
|
104
|
+
}
|
|
105
|
+
const m = style.motion ?? {};
|
|
106
|
+
for (const k of ["enter", "move", "exit"]) if (!Array.isArray(m.ease?.[k]) || m.ease[k].length !== 4) fail(`motion.ease.${k}`, "is a cubic-bezier [x1, y1, x2, y2]");
|
|
107
|
+
for (const k of ["micro", "move", "hero", "exit"]) if (!(m.dur?.[k] > 0)) fail(`motion.dur.${k}`, "seconds > 0");
|
|
108
|
+
if (!(m.dur.exit <= m.dur.hero)) fail("motion.dur.exit", "an exit is never longer than the hero move");
|
|
109
|
+
if (!Array.isArray(style.transitions) || !style.transitions.length || style.transitions.some((x) => !TRANSITIONS.includes(x))) fail("transitions", `a subset of ${TRANSITIONS.join(", ")}`);
|
|
110
|
+
if (!isText(style.score?.genre)) fail("score.genre", "names the genre a film's score takes by default");
|
|
111
|
+
if (!style.sound || !isText(style.sound.timbre)) fail("sound", "needs the generated bed's palette");
|
|
112
|
+
if (!Array.isArray(style.beds) || style.beds.length < 2 || style.beds.some((b) => !isText(b?.timbre) || !isText(b?.key) || !(b?.tempo > 0))) fail("beds", "lists at least two beds a film may pick, each { timbre, key, mode, tempo }");
|
|
113
|
+
const same = (a, b) => ["timbre", "key", "mode", "tempo"].every((k) => String(a[k]).toLowerCase() === String(b[k]).toLowerCase());
|
|
114
|
+
if (!style.beds.some((b) => same(b, style.sound))) fail("beds", "must include the default bed (sound)");
|
|
115
|
+
if (!Array.isArray(style.bans) || style.bans.length < 3) fail("bans", "names at least three defaults this style never uses");
|
|
116
|
+
return style;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cache = new Map();
|
|
120
|
+
/** A shipped style by id, validated and frozen. */
|
|
121
|
+
export function readStyle(id) {
|
|
122
|
+
if (!STYLE_IDS.includes(id)) throw new StyleError(`unknown style: ${id} (choose ${STYLE_IDS.join(", ")})`);
|
|
123
|
+
if (!cache.has(id)) {
|
|
124
|
+
const url = new URL(`../../styles/${id}.json`, import.meta.url);
|
|
125
|
+
cache.set(id, Object.freeze(validateStyle(JSON.parse(readFileSync(fileURLToPath(url), "utf8")))));
|
|
126
|
+
}
|
|
127
|
+
return cache.get(id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Every font family copy may use in this style (the role faces, the Hangul face, Pretendard). */
|
|
131
|
+
export const styleFamilies = (style) => new Set([...TYPE_ROLES.map((r) => style.faces[r].family), style.hangul?.family ?? "Pretendard", "Pretendard"]);
|
|
132
|
+
/** The hex colours copy may use. */
|
|
133
|
+
export const styleTextColors = (style) => style.textRoles.map((r) => style.palette[r].toLowerCase());
|
|
134
|
+
/** The beds a film in this style may pick from, the default first. */
|
|
135
|
+
export const styleBeds = (style) => style.beds;
|
|
136
|
+
/** The role accents a rich film may add to the core palette, each { role, color }. */
|
|
137
|
+
export const styleAccents = (style) => style.accents;
|
|
138
|
+
/** All palette colours of the style (what a stage treatment's palette may list); rich adds the accents. */
|
|
139
|
+
export const styleColors = (style, { rich = false } = {}) => [...Object.values(style.palette), ...(rich ? style.accents.map((a) => a.color) : [])].map((c) => c.toLowerCase());
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Which style a film takes. A style the request names wins; then the project's house style; then
|
|
143
|
+
* the keyword auto-pick; then the genre's style; then the default. Returns { id, source, reason }.
|
|
144
|
+
*/
|
|
145
|
+
export function pickStyle({ request = "", copy = [], path = "stage", house = null, genre = null } = {}) {
|
|
146
|
+
const text = String(request);
|
|
147
|
+
for (const [id, names] of Object.entries(NAMED)) {
|
|
148
|
+
if (path === "type" && !readStyle(id).typePreset) continue;
|
|
149
|
+
const hit = names.find((n) => keywordHit(text, n, /[가-힣]/u.test(n)));
|
|
150
|
+
if (hit) return { id, source: "request", reason: `the request names the ${id} style ("${hit}")` };
|
|
151
|
+
}
|
|
152
|
+
if (house?.style && STYLE_IDS.includes(house.style) && (path !== "type" || readStyle(house.style).typePreset)) {
|
|
153
|
+
return { id: house.style, source: "house", reason: `the project's house style is ${house.style} (motion-house.json)` };
|
|
154
|
+
}
|
|
155
|
+
const haystack = [text, ...copy].join("\n");
|
|
156
|
+
for (const row of STYLE_PICK) {
|
|
157
|
+
if (path === "type" && !readStyle(row.style).typePreset) continue;
|
|
158
|
+
for (const k of row.korean) if (keywordHit(haystack, k, true)) return { id: row.style, source: "auto", reason: `the request mentions "${k}"` };
|
|
159
|
+
for (const k of row.latin) if (keywordHit(haystack, k, false)) return { id: row.style, source: "auto", reason: `the request mentions "${k}"` };
|
|
160
|
+
}
|
|
161
|
+
const byGenre = GENRE_STYLE[genre];
|
|
162
|
+
if (byGenre && (path !== "type" || readStyle(byGenre).typePreset)) return { id: byGenre, source: "genre", reason: `no style is named and no keyword picks one, and a ${genre} film takes ${byGenre}` };
|
|
163
|
+
return { id: DEFAULT_STYLE, source: "default", reason: `no style is named and no keyword picks one, so the default ${DEFAULT_STYLE}` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A layout's zones in pixels for a format: { type: {x, y, w, h} | null, subject: {...} }. */
|
|
167
|
+
export function zonesPx(format) {
|
|
168
|
+
const [W, H] = FRAME[format];
|
|
169
|
+
const px = (r) => (r ? { x: Math.round(r[0] * W), y: Math.round(r[1] * H), w: Math.round(r[2] * W), h: Math.round(r[3] * H) } : null);
|
|
170
|
+
return Object.fromEntries(Object.entries(LAYOUTS[format]).map(([id, z]) => [id, { type: px(z.type), subject: px(z.subject) }]));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The type step a rendered size sits on, or null when it is off the style's scale. */
|
|
174
|
+
export function scaleStep(style, format, sizePx) {
|
|
175
|
+
for (const role of TYPE_ROLES) {
|
|
176
|
+
const step = style.scale[format][role];
|
|
177
|
+
if (Math.abs(sizePx - step) <= step * SIZE_TOLERANCE) return role;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const familyStack = (style, role) => {
|
|
183
|
+
const f = style.faces[role];
|
|
184
|
+
const hangul = style.hangul?.family ?? "Pretendard";
|
|
185
|
+
return [f.family, hangul, "Pretendard"].filter((x, i, a) => a.indexOf(x) === i).map((x) => `"${x}"`).join(", ");
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** /lit/style.css for one style and format: custom properties, type-role classes, colour classes. */
|
|
189
|
+
export function styleCss(style, format) {
|
|
190
|
+
const s = style.scale[format];
|
|
191
|
+
const vars = [
|
|
192
|
+
...Object.entries(style.palette).map(([k, v]) => ` --lit-${k}: ${v};`),
|
|
193
|
+
...TYPE_ROLES.map((r) => ` --lit-size-${r}: ${s[r]}px;`),
|
|
194
|
+
];
|
|
195
|
+
const roles = TYPE_ROLES.map((r) => {
|
|
196
|
+
const f = style.faces[r];
|
|
197
|
+
return `.lit-${r} { font-family: ${familyStack(style, r)}; font-size: ${s[r]}px; font-weight: ${f.weight}; ${f.stretch ? `font-stretch: ${f.stretch}; ` : ""}line-height: ${style.leading?.[r] ?? 1.3}; letter-spacing: 0; margin: 0; }`;
|
|
198
|
+
});
|
|
199
|
+
const colours = style.textRoles.map((r) => `.lit-${r === "ground" ? "knockout" : r} { color: var(--lit-${r}); }`);
|
|
200
|
+
return [
|
|
201
|
+
`/* lit-typographic-motion style: ${style.id} (${format}) */`,
|
|
202
|
+
`:root {\n${vars.join("\n")}\n}`,
|
|
203
|
+
// Korean breaks only between eojeol, never inside a word (type-craft.md, MO-A-13).
|
|
204
|
+
`html, body { margin: 0; overflow: hidden; background: var(--lit-ground); color: var(--lit-ink); font-family: ${familyStack(style, "body")}; word-break: keep-all; overflow-wrap: break-word; }`,
|
|
205
|
+
...roles,
|
|
206
|
+
...colours,
|
|
207
|
+
".lit-zone { position: absolute; box-sizing: border-box; }",
|
|
208
|
+
"",
|
|
209
|
+
].join("\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** /lit/style.js: window.LitStyle (tokens and pixel zones) and window.LitTimeline (the treatment's shots). */
|
|
213
|
+
export function styleScript(style, format, treatment) {
|
|
214
|
+
const timeline = {
|
|
215
|
+
format, fps: treatment.fps ?? 60, duration: treatment.durationSec,
|
|
216
|
+
beats: (treatment.beats ?? []).map((b, i) => ({ index: i, t0: b.t0, t1: b.t1, layout: b.layout ?? null, transition: b.transition ?? null })),
|
|
217
|
+
};
|
|
218
|
+
const data = {
|
|
219
|
+
id: style.id, name: style.name, format, palette: style.palette, textRoles: style.textRoles,
|
|
220
|
+
faces: style.faces, hangul: style.hangul ?? null, scale: style.scale[format], leading: style.leading ?? {},
|
|
221
|
+
motion: style.motion, transitions: style.transitions, ground: style.ground, zones: zonesPx(format),
|
|
222
|
+
};
|
|
223
|
+
return `window.LitStyle = Object.freeze(${JSON.stringify(data)});\nwindow.LitTimeline = Object.freeze(${JSON.stringify(timeline)});\n`;
|
|
224
|
+
}
|