@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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// The storyboard: `stills/board.png`, one page a person can read before the full render.
|
|
2
|
+
// One tile per beat (the beat still) with the shot's number, time, layout and hand-off under it
|
|
3
|
+
// and the shot's type zone outlined on the frame when the beat names one; a header with the house
|
|
4
|
+
// style (name, palette, type) and the film's format, length and sound; a footer with the layout
|
|
5
|
+
// check. Drawn as a page in the same headless Chrome the stage uses, so its type is product faces.
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { LAYOUTS, readStyle } from "./styles.mjs";
|
|
10
|
+
import { resolveChrome } from "./chrome.mjs";
|
|
11
|
+
import { openStage } from "./stage-session.mjs";
|
|
12
|
+
import { stageFonts } from "./stage.mjs";
|
|
13
|
+
|
|
14
|
+
const SKILL_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
15
|
+
|
|
16
|
+
export const BOARD = Object.freeze({ width: 1920, height: 1080, margin: 48, gap: 20, header: 150, footer: 56, caption: 104 });
|
|
17
|
+
|
|
18
|
+
const esc = (text) => String(text ?? "").replace(/[&<>"]/gu, (c) => ({ "&": "&", "<": ">", '"': """ }[c] ?? c));
|
|
19
|
+
const secs = (v) => `${Number(v).toFixed(1)}`;
|
|
20
|
+
|
|
21
|
+
/** Tile geometry: columns and thumbnail size for n shots of a film of size width x height. */
|
|
22
|
+
export function boardGrid(n, width, height) {
|
|
23
|
+
const usableW = BOARD.width - 2 * BOARD.margin;
|
|
24
|
+
const usableH = BOARD.height - BOARD.header - BOARD.footer - BOARD.margin;
|
|
25
|
+
const aspect = width / height;
|
|
26
|
+
const options = [];
|
|
27
|
+
for (let cols = 1; cols <= Math.max(1, n); cols++) {
|
|
28
|
+
const rows = Math.ceil(n / cols);
|
|
29
|
+
const tw = Math.floor((usableW - (cols - 1) * BOARD.gap) / cols);
|
|
30
|
+
const byHeight = Math.floor((usableH - (rows - 1) * BOARD.gap) / rows) - BOARD.caption;
|
|
31
|
+
const th = Math.min(Math.round(tw / aspect), byHeight);
|
|
32
|
+
if (th <= 0) continue;
|
|
33
|
+
options.push({ cols, rows, tw: Math.round(th * aspect), th, empty: rows * cols - n });
|
|
34
|
+
}
|
|
35
|
+
// The largest tiles, unless a grid with fewer empty slots keeps 85 % of their area.
|
|
36
|
+
const largest = Math.max(...options.map((o) => o.tw * o.th));
|
|
37
|
+
return options.filter((o) => o.tw * o.th >= 0.85 * largest).sort((a, b) => a.empty - b.empty || b.tw * b.th - a.tw * a.th)[0];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The board page's HTML for a treatment and its beat stills (file names relative to the page). */
|
|
41
|
+
export function boardHtml({ treatment, stills, check = null }) {
|
|
42
|
+
const style = treatment.style ? readStyle(treatment.style) : null;
|
|
43
|
+
const [width, height] = treatment.format === "9:16" ? [1080, 1920] : [1920, 1080];
|
|
44
|
+
const grid = boardGrid(stills.length, width, height);
|
|
45
|
+
const zones = LAYOUTS[treatment.format === "9:16" ? "9:16" : "16:9"];
|
|
46
|
+
const pal = style?.palette ?? {};
|
|
47
|
+
const chips = Object.entries(pal).filter(([k]) => ["ground", "ink", "muted", "signal", "accent"].includes(k))
|
|
48
|
+
.map(([k, v]) => `<span class="chip"><i style="background:${esc(v)}"></i>${esc(k)} ${esc(v)}</span>`).join("");
|
|
49
|
+
const tiles = stills.map((file, i) => {
|
|
50
|
+
const b = treatment.beats[i] ?? {};
|
|
51
|
+
const z = b.layout && zones[b.layout]?.type;
|
|
52
|
+
const guide = z ? `<b class="zone" style="left:${(z[0] * 100).toFixed(2)}%;top:${(z[1] * 100).toFixed(2)}%;width:${(z[2] * 100).toFixed(2)}%;height:${(z[3] * 100).toFixed(2)}%"></b>` : "";
|
|
53
|
+
const handoff = b.transition && b.transition !== "end" ? `→ ${b.transition}` : b.transition;
|
|
54
|
+
const meta = [String(i + 1).padStart(2, "0"), `${secs(b.t0)}–${secs(b.t1)} s`, b.layout, handoff].filter(Boolean).map(esc).join(" · ");
|
|
55
|
+
return `<figure style="width:${grid.tw}px"><div class="frame" style="width:${grid.tw}px;height:${grid.th}px"><img src="${esc(file)}">${guide}</div>`
|
|
56
|
+
+ `<figcaption><span class="meta">${meta}</span><span class="hero">${esc(b.hero ?? b.onScreen ?? "")}</span></figcaption></figure>`;
|
|
57
|
+
}).join("");
|
|
58
|
+
const failed = (check?.rules ?? []).filter((x) => x.status === "FAIL");
|
|
59
|
+
const verdict = !check ? "layout check: not run on this render"
|
|
60
|
+
: failed.length ? `layout check: FAIL — ${failed.map((x) => x.id).join(", ")}` : "layout check: PASS";
|
|
61
|
+
const sound = treatment.sound?.mode === "generated" && typeof treatment.sound.palette === "object"
|
|
62
|
+
? `${treatment.sound.palette.timbre}, ${treatment.sound.palette.key} ${treatment.sound.palette.mode}, ${treatment.sound.palette.tempo} BPM`
|
|
63
|
+
: treatment.sound?.mode === "score" ? `score (${treatment.sound.genre ?? style?.score?.genre ?? "house"})` : treatment.sound?.mode ?? "none";
|
|
64
|
+
const ground = pal.ground ?? "#101216";
|
|
65
|
+
return `<!doctype html>
|
|
66
|
+
<meta charset="utf-8">
|
|
67
|
+
<link rel="stylesheet" href="/lit/fonts.css">
|
|
68
|
+
<link rel="stylesheet" href="/lit/style.css">
|
|
69
|
+
<script src="/lit/stage-kit.js"></script>
|
|
70
|
+
<style>
|
|
71
|
+
html, body { margin: 0; width: ${BOARD.width}px; height: ${BOARD.height}px; overflow: hidden; background: #15171b; color: #e8e9e6; font-family: "Pretendard"; }
|
|
72
|
+
header { position: absolute; left: ${BOARD.margin}px; right: ${BOARD.margin}px; top: 36px; height: ${BOARD.header - 48}px; display: flex; justify-content: space-between; align-items: flex-start; }
|
|
73
|
+
h1 { margin: 0; font: 700 40px/1.1 "Pretendard"; }
|
|
74
|
+
.sub { margin-top: 10px; font: 400 20px/1.3 "MesloLGS NF"; color: #a8aba6; }
|
|
75
|
+
.style { text-align: right; }
|
|
76
|
+
.specimen { display: inline-block; padding: 6px 14px; margin-bottom: 10px; background: ${esc(ground)}; color: ${esc(pal.ink ?? "#fff")}; font: ${style?.faces.display.weight ?? 700} 34px/1.1 "${esc(style?.faces.display.family ?? "Pretendard")}", "Pretendard"; }
|
|
77
|
+
.chips { display: flex; gap: 14px; justify-content: flex-end; font: 400 16px/1 "MesloLGS NF"; color: #a8aba6; }
|
|
78
|
+
.chip i { display: inline-block; width: 16px; height: 16px; margin-right: 6px; vertical-align: -3px; border: 1px solid #555; }
|
|
79
|
+
main { position: absolute; left: ${BOARD.margin}px; right: ${BOARD.margin}px; top: ${BOARD.header}px; display: flex; flex-wrap: wrap; gap: ${BOARD.gap}px; }
|
|
80
|
+
figure { margin: 0; }
|
|
81
|
+
.frame { position: relative; overflow: hidden; background: #000; }
|
|
82
|
+
.frame img { display: block; width: 100%; height: 100%; }
|
|
83
|
+
.zone { position: absolute; box-sizing: border-box; border: 2px dashed rgba(255, 196, 0, 0.75); }
|
|
84
|
+
figcaption { display: block; height: ${BOARD.caption - 8}px; padding-top: 8px; overflow: hidden; }
|
|
85
|
+
.meta { display: block; font: 400 16px/1.3 "MesloLGS NF"; color: #c9cbc6; }
|
|
86
|
+
.hero { display: block; margin-top: 4px; font: 400 18px/1.35 "Pretendard"; color: #e8e9e6; }
|
|
87
|
+
footer { position: absolute; left: ${BOARD.margin}px; right: ${BOARD.margin}px; bottom: 18px; font: 400 18px/1.2 "MesloLGS NF"; color: ${failed.length ? "#ff8a7a" : "#9fd3a4"}; }
|
|
88
|
+
</style>
|
|
89
|
+
<body>
|
|
90
|
+
<header>
|
|
91
|
+
<div><h1>Storyboard · ${esc(treatment.subject?.name ?? "")}</h1>
|
|
92
|
+
<div class="sub">${esc(treatment.format)} · ${esc(treatment.durationSec)} s · ${treatment.beats.length} shots · ${esc(treatment.path)} path · sound: ${esc(sound)}</div></div>
|
|
93
|
+
<div class="style"><div class="specimen">${esc(style?.name ?? treatment.style ?? "")} Aa 가</div><div class="chips">${chips}</div></div>
|
|
94
|
+
</header>
|
|
95
|
+
<main>${tiles}</main>
|
|
96
|
+
<footer>${esc(verdict)} · dashed box: the shot's type zone</footer>
|
|
97
|
+
<script>LitStage.define({ width: ${BOARD.width}, height: ${BOARD.height}, fps: 30, duration: 1, render() {} });</script>
|
|
98
|
+
</body>
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Render `<out>/stills/board.png` from the beat stills already written. Returns the file path.
|
|
104
|
+
* `check` is the layout check ({ rules }) when one ran.
|
|
105
|
+
*/
|
|
106
|
+
export async function writeBoard({ out, treatment, beatFiles, check = null, env = process.env, log = () => {} }) {
|
|
107
|
+
const executable = resolveChrome(env);
|
|
108
|
+
if (!executable) throw new Error("writeBoard needs Chrome: set CHROME_PATH");
|
|
109
|
+
const dir = path.join(out, ".run", "board");
|
|
110
|
+
rmSync(dir, { recursive: true, force: true });
|
|
111
|
+
mkdirSync(dir, { recursive: true });
|
|
112
|
+
const names = beatFiles.map((f, i) => {
|
|
113
|
+
const name = `s${String(i + 1).padStart(2, "0")}.png`;
|
|
114
|
+
copyFileSync(f, path.join(dir, name));
|
|
115
|
+
return name;
|
|
116
|
+
});
|
|
117
|
+
writeFileSync(path.join(dir, "index.html"), boardHtml({ treatment, stills: names, check }));
|
|
118
|
+
const faces = [...new Set([...(treatment.typePlan?.faces ?? ["Pretendard"]), "Pretendard"])];
|
|
119
|
+
let style = null;
|
|
120
|
+
if (treatment.style) {
|
|
121
|
+
const { readStyle, styleCss, styleScript } = await import("./styles.mjs");
|
|
122
|
+
const declared = readStyle(treatment.style);
|
|
123
|
+
style = { css: styleCss(declared, treatment.format), js: styleScript(declared, treatment.format, treatment) };
|
|
124
|
+
}
|
|
125
|
+
const session = await openStage({
|
|
126
|
+
executable, outDir: out, stageDir: dir, width: BOARD.width, height: BOARD.height,
|
|
127
|
+
fps: 30, seed: 1, fonts: stageFonts(faces, env), style, label: "b", log,
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
await session.load();
|
|
131
|
+
await session.step(0, 30);
|
|
132
|
+
const png = await session.capture();
|
|
133
|
+
const file = path.join(out, "stills", "board.png");
|
|
134
|
+
mkdirSync(path.join(out, "stills"), { recursive: true });
|
|
135
|
+
writeFileSync(file, png);
|
|
136
|
+
return file;
|
|
137
|
+
} finally {
|
|
138
|
+
await session.close();
|
|
139
|
+
rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Add a renderer-written file to stills/index.json; returns the new index SHA-256. */
|
|
144
|
+
export function addToStills(out, entry, { sha256 }) {
|
|
145
|
+
const indexFile = path.join(out, "stills", "index.json");
|
|
146
|
+
const index = JSON.parse(readFileSync(indexFile, "utf8"));
|
|
147
|
+
const file = path.join(out, entry.file);
|
|
148
|
+
if (!existsSync(file)) throw new Error(`addToStills: ${entry.file} was not written`);
|
|
149
|
+
index.stills = [...(index.stills ?? []).filter((f) => f.file !== entry.file), { ...entry, sha256: sha256(readFileSync(file)) }];
|
|
150
|
+
const bytes = Buffer.from(`${JSON.stringify(index, null, 2)}\n`);
|
|
151
|
+
writeFileSync(indexFile, bytes);
|
|
152
|
+
return sha256(bytes);
|
|
153
|
+
}
|
|
@@ -194,13 +194,18 @@ export async function openRenderer({ executable, profileDir, softwareOnly = fals
|
|
|
194
194
|
export const STAGE_FLAGS = Object.freeze(['--run-all-compositor-stages-before-draw', '--disable-checker-imaging', '--disable-new-content-rendering-timeout', '--disable-threaded-animation', '--disable-threaded-scrolling', '--disable-image-animation-resync', '--disable-lcd-text', '--force-color-profile=srgb', '--hide-scrollbars', '--mute-audio', '--force-device-scale-factor=1', '--disable-background-networking', '--disable-component-update', '--disable-sync', '--no-pings', '--metrics-recording-only']);
|
|
195
195
|
|
|
196
196
|
// `scale` is the device scale: 1 for a film; a cover is captured at 2 and shrunk afterwards.
|
|
197
|
-
// `extra` flags ride after the stage set (a cover adds COVER_FLAGS).
|
|
198
|
-
|
|
197
|
+
// `extra` flags ride after the stage set (a cover adds COVER_FLAGS). The gpu rung uses the
|
|
198
|
+
// platform's GL flags with page raster on the CPU; anything else is the SwiftShader rung.
|
|
199
|
+
export function stageArgs({ profileDir, width, height, scale = 1, extra = [], rung = 'software', platform = osPlatform() }) {
|
|
199
200
|
const flags = STAGE_FLAGS.map((flag) => (flag === '--force-device-scale-factor=1' ? `--force-device-scale-factor=${scale}` : flag));
|
|
200
|
-
const
|
|
201
|
+
const rungFlags = rung === 'gpu' ? (gpuRungs(platform)[0] ?? []).filter((flag) => flag !== '--enable-gpu-rasterization') : SWIFTSHADER_RUNG;
|
|
202
|
+
const args = [...HOUSEKEEPING, `--user-data-dir=${profileDir}`, ...rungFlags, ...COMMON_FLAGS, ...flags, `--window-size=${width},${height}`, `--host-resolver-rules=MAP * ~NOTFOUND , EXCLUDE ${STAGE.host}`, ...extra];
|
|
201
203
|
return [...new Set(args), 'about:blank'];
|
|
202
204
|
}
|
|
203
205
|
|
|
206
|
+
// The rung counts only when the page gets a WebGL2 context; the renderer string is recorded.
|
|
207
|
+
const WEBGL_PROBE = `(() => { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl2'); if (!gl) return { webgl2: false }; const info = gl.getExtension('WEBGL_debug_renderer_info'); return { webgl2: true, renderer: info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : ${JSON.stringify(UNKNOWN_RENDERER)} }; })()`;
|
|
208
|
+
|
|
204
209
|
// Launch one stage Chrome and attach a page target. Exit 10 when Chrome is missing or never
|
|
205
210
|
// answers; WebGL is not probed here (exit 11 applies only when the page asks for it).
|
|
206
211
|
// A cover's frames are compared across fresh browsers and sought out of order. With GPU raster on
|
|
@@ -209,39 +214,63 @@ export function stageArgs({ profileDir, width, height, scale = 1, extra = [] })
|
|
|
209
214
|
// frame identically in 5 of 5 pairs.
|
|
210
215
|
export const COVER_FLAGS = Object.freeze(['--disable-gpu-rasterization']);
|
|
211
216
|
|
|
212
|
-
|
|
217
|
+
// A 3D film asks for the gpu rung; when it cannot start or gives the page no WebGL2, the launch
|
|
218
|
+
// falls back to the software rung and records why. Either way the launch reports the rung the page
|
|
219
|
+
// got and its renderer string; without WebGL2 on any rung it is exit 11.
|
|
220
|
+
export async function launchStage({ executable, profileDir, width, height, scale = 1, extra = [], timeoutMs = 30000, rung = 'software', platform = osPlatform() }) {
|
|
213
221
|
if (!executable) throw new BlockedError(EXIT.NO_CHROME, 'BLOCKED_NO_CHROME: Chrome/Chromium not found; set CHROME_PATH to a Chrome executable');
|
|
214
222
|
mkdirSync(profileDir, { recursive: true });
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
// Strict only for the gpu rung: a 3D page needs its WebGL context, while a 2D page renders
|
|
224
|
+
// without one (the renderer is then unknown, and the software label still holds).
|
|
225
|
+
const attempt = async (wanted, strict) => {
|
|
226
|
+
const args = stageArgs({ profileDir, width, height, scale, extra, rung: wanted, platform });
|
|
227
|
+
let stderr = '';
|
|
228
|
+
let proc;
|
|
229
|
+
try { proc = spawn(executable, args, { stdio: ['ignore', 'ignore', 'pipe', 'pipe', 'pipe'] }); } catch (error) { throw new BlockedError(EXIT.NO_CHROME, `BLOCKED_NO_CHROME: ${error.message}`); }
|
|
230
|
+
proc.stderr.setEncoding('utf8');
|
|
231
|
+
proc.stderr.on('data', (chunk) => { if (stderr.length < 16384) stderr += chunk; });
|
|
232
|
+
const spawnError = new Promise((_, reject) => proc.once('error', (error) => reject(error)));
|
|
233
|
+
const session = new PipeSession(proc);
|
|
234
|
+
const close = async () => {
|
|
235
|
+
if (proc.exitCode === null && proc.signalCode === null) {
|
|
236
|
+
try { await session.send('Browser.close', {}, undefined, 5000); } catch {}
|
|
237
|
+
await new Promise((resolve) => {
|
|
238
|
+
const timer = setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} resolve(); }, 5000);
|
|
239
|
+
proc.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
240
|
+
if (proc.exitCode !== null || proc.signalCode !== null) { clearTimeout(timer); resolve(); }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// Chrome's helpers can still be flushing the profile for a moment after the browser exits.
|
|
244
|
+
for (let retry = 0; retry < 20; retry += 1) {
|
|
245
|
+
try { rmSync(profileDir, { recursive: true, force: true }); break; } catch { await new Promise((resolve) => setTimeout(resolve, 100)); }
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
try {
|
|
249
|
+
await Promise.race([spawnError, session.send('Browser.getVersion', {}, undefined, timeoutMs)]);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
await close();
|
|
252
|
+
throw new BlockedError(EXIT.NO_CHROME, `BLOCKED_NO_CHROME: Chrome failed to launch headless: ${firstLine(stderr) || error.message}`);
|
|
231
253
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
254
|
+
const { targetId } = await session.send('Target.createTarget', { url: 'about:blank' });
|
|
255
|
+
const { sessionId } = await session.send('Target.attachToTarget', { targetId, flatten: true });
|
|
256
|
+
const send = (method, params = {}, timeout) => session.send(method, params, sessionId, timeout);
|
|
257
|
+
await send('Runtime.enable').catch(() => {});
|
|
258
|
+
const probe = await send('Runtime.evaluate', { expression: WEBGL_PROBE, returnByValue: true }, timeoutMs).catch(() => null);
|
|
259
|
+
const found = probe?.result?.value;
|
|
260
|
+
if (!found?.webgl2) {
|
|
261
|
+
if (!strict) return { browser: { proc, session, sessionId, send, close, args, on: (listener) => session.listeners.add(listener) }, renderer: UNKNOWN_RENDERER };
|
|
262
|
+
await close();
|
|
263
|
+
return { webgl2: false, detail: firstLine(stderr) };
|
|
235
264
|
}
|
|
265
|
+
return { browser: { proc, session, sessionId, send, close, args, on: (listener) => session.listeners.add(listener) }, renderer: found.renderer ?? UNKNOWN_RENDERER };
|
|
236
266
|
};
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
await
|
|
241
|
-
|
|
267
|
+
const first = await attempt(rung, rung === 'gpu');
|
|
268
|
+
if (first.browser) return { ...first.browser, rung, renderer: first.renderer, notice: null };
|
|
269
|
+
if (rung === 'gpu') {
|
|
270
|
+
const fallback = await attempt('software', false);
|
|
271
|
+
if (fallback.browser) {
|
|
272
|
+
return { ...fallback.browser, rung: 'software', renderer: fallback.renderer, notice: `the 3D route asked for the GPU rung and it gave the page no WebGL there${first.detail ? ` (${first.detail})` : ''}; rendered on the software rung (SwiftShader), slower, same frames` };
|
|
273
|
+
}
|
|
242
274
|
}
|
|
243
|
-
|
|
244
|
-
const { sessionId } = await session.send('Target.attachToTarget', { targetId, flatten: true });
|
|
245
|
-
const send = (method, params = {}, timeout) => session.send(method, params, sessionId, timeout);
|
|
246
|
-
return { proc, session, sessionId, send, close, args, on: (listener) => session.listeners.add(listener) };
|
|
275
|
+
throw new BlockedError(EXIT.NO_WEBGL2, `BLOCKED_NO_WEBGL2: Chrome ran but no rung produced a WebGL2 context${first.detail ? ` (${first.detail})` : ''}`);
|
|
247
276
|
}
|
|
@@ -171,11 +171,13 @@ export const DIRECTOR = Object.freeze({
|
|
|
171
171
|
durationRange: Object.freeze([4, 90]),
|
|
172
172
|
minBeatSec: 1.2,
|
|
173
173
|
maxGapSec: 0.25,
|
|
174
|
-
deviceKinds: Object.freeze(['illustration', 'diagram', 'chart', 'icon', 'shape', 'path', 'mask', 'depth3d', 'particles', 'grid', 'gradient', 'photo-texture']),
|
|
174
|
+
deviceKinds: Object.freeze(['illustration', 'diagram', 'chart', 'icon', 'shape', 'path', 'mask', 'depth3d', 'screen', 'particles', 'grid', 'gradient', 'photo-texture']),
|
|
175
|
+
protagonistKinds: Object.freeze(['character', 'object']),
|
|
176
|
+
assetRoles: Object.freeze(['logo', 'wordmark', 'icon', 'photo', 'screenshot', 'ui', 'readme-image']),
|
|
175
177
|
textureKinds: Object.freeze(['grid', 'gradient', 'particles', 'photo-texture']),
|
|
176
178
|
subjectCoverage: 0.5,
|
|
177
179
|
faces: Object.freeze(['Archivo', 'Pretendard', 'VT323', 'Silkscreen', 'Galmuri9', 'Meslo']),
|
|
178
|
-
soundModes: Object.freeze(['generated', 'supplied', 'authored', 'none']),
|
|
180
|
+
soundModes: Object.freeze(['score', 'generated', 'supplied', 'authored', 'none']),
|
|
179
181
|
timbres: Object.freeze(['glass', 'felt', 'pulse']),
|
|
180
182
|
});
|
|
181
183
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// Cover capture: one stage Chrome (the
|
|
2
|
-
// the page's doing) laid out at the cover's CSS size and captured at device scale 2. The
|
|
3
|
-
// served from <out>/cover/ on the synthetic stage origin through CDP Fetch interception,
|
|
4
|
-
// /lit/ routes (fonts, the loop kit), and the cover init script
|
|
5
|
-
// renderer SEEKS: it calls window.renderFrame(t, { mask }) for any
|
|
6
|
-
// master pass, the replay in a fresh browser, the seek-order pass
|
|
7
|
-
// their own page here.
|
|
1
|
+
// Cover capture: one stage Chrome (the rung the recipe asks for, every stage flag, so a mismatch is
|
|
2
|
+
// always the page's doing) laid out at the cover's CSS size and captured at device scale 2. The
|
|
3
|
+
// page is served from <out>/cover/ on the synthetic stage origin through CDP Fetch interception,
|
|
4
|
+
// beside the /lit/ routes (fonts, the loop kit, the vendored three.js), and the cover init script
|
|
5
|
+
// runs before any page script. The renderer SEEKS: it calls window.renderFrame(t, { mask }) for any
|
|
6
|
+
// t in [0, 1) in any order, so the master pass, the replay in a fresh browser, the seek-order pass
|
|
7
|
+
// and a window re-render each open their own page here.
|
|
8
8
|
import { randomBytes } from 'node:crypto';
|
|
9
9
|
import { readFileSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { BlockedError, EXIT, STAGE } from './constants.mjs';
|
|
12
12
|
import { COVER_FLAGS, launchStage, resolveChrome } from './chrome.mjs';
|
|
13
13
|
import { SKILL_ROOT } from './fonts.mjs';
|
|
14
|
-
import { CONTENT_TYPES, resolveStagePath } from './stage-session.mjs';
|
|
14
|
+
import { CONTENT_TYPES, resolveStagePath, resolveThree } from './stage-session.mjs';
|
|
15
15
|
|
|
16
16
|
export const LOOP_KIT_PATH = join(SKILL_ROOT, 'scripts', 'stage', 'loop-kit.js');
|
|
17
17
|
export const COVER_INIT_PATH = join(SKILL_ROOT, 'scripts', 'stage', 'cover-init.js');
|
|
@@ -19,11 +19,12 @@ export const COVER_INIT_PATH = join(SKILL_ROOT, 'scripts', 'stage', 'cover-init.
|
|
|
19
19
|
const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
20
20
|
|
|
21
21
|
// Open one cover page and wait for window.ready. `dir` is the folder served as the site root
|
|
22
|
-
// (<out>/cover for a render; the sheet's own folder for the contact sheet).
|
|
23
|
-
|
|
22
|
+
// (<out>/cover for a render; the sheet's own folder for the contact sheet). A hero-3d cover asks
|
|
23
|
+
// for the GPU rung; anything else renders on software, where a mismatch is always the page's doing.
|
|
24
|
+
export async function openCover({ dir, fonts, width, height, scale, profileRoot, env = process.env, label = 'c', rung = 'software', log = () => {} }) {
|
|
24
25
|
const executable = resolveChrome(env);
|
|
25
26
|
if (!executable) throw new BlockedError(EXIT.NO_CHROME, 'BLOCKED_NO_CHROME: Chrome/Chromium not found; set CHROME_PATH to a Chrome executable');
|
|
26
|
-
const browser = await launchStage({ executable, profileDir: join(profileRoot, `${label}-${randomBytes(3).toString('hex')}`), width, height, scale, extra: COVER_FLAGS });
|
|
27
|
+
const browser = await launchStage({ executable, profileDir: join(profileRoot, `${label}-${randomBytes(3).toString('hex')}`), width, height, scale, extra: COVER_FLAGS, rung });
|
|
27
28
|
const network = [];
|
|
28
29
|
const refused = [];
|
|
29
30
|
const errors = [];
|
|
@@ -46,6 +47,13 @@ export async function openCover({ dir, fonts, width, height, scale, profileRoot,
|
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
if (url.pathname === '/lit/loop-kit.js') { await respond(requestId, 200, kit, CONTENT_TYPES['.js']); return; }
|
|
50
|
+
if (url.pathname.startsWith('/lit/three/')) {
|
|
51
|
+
const resolved = resolveThree(url.pathname);
|
|
52
|
+
if (resolved.refused) { refused.push(resolved.refused); await respond(requestId, 403, 'refused'); return; }
|
|
53
|
+
if (resolved.missing) { await respond(requestId, 404, 'not a vendored three.js file'); return; }
|
|
54
|
+
await respond(requestId, 200, readFileSync(resolved.file), resolved.type);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
49
57
|
if (url.pathname === '/lit/fonts.css') { await respond(requestId, 200, fonts.css, CONTENT_TYPES['.css']); return; }
|
|
50
58
|
if (url.pathname.startsWith('/lit/fonts/')) {
|
|
51
59
|
const route = fonts.routes.get(url.pathname);
|
|
@@ -103,6 +111,7 @@ export async function openCover({ dir, fonts, width, height, scale, profileRoot,
|
|
|
103
111
|
|
|
104
112
|
return {
|
|
105
113
|
width, height, scale,
|
|
114
|
+
rung: browser.rung, renderer: browser.renderer, notice: browser.notice,
|
|
106
115
|
// Draw loop time t (or its title mask), then wait for fonts and two native frames.
|
|
107
116
|
async frame(t, { mask = false } = {}) {
|
|
108
117
|
try {
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { BlockedError, COVER, EXIT } from './constants.mjs';
|
|
10
|
-
import { normalize, restateLimit, sentenceCount, sharedRun, stripQuoted } from './treatment.mjs';
|
|
10
|
+
import { normalize, normUrl, requestUrls, restateLimit, sentenceCount, sharedRun, stripQuoted } from './treatment.mjs';
|
|
11
11
|
|
|
12
|
-
export const RECIPES = Object.freeze(['painted-light', 'weight-tide', 'orbit-loop', 'own']);
|
|
12
|
+
export const RECIPES = Object.freeze(['hero-3d', 'painted-light', 'weight-tide', 'orbit-loop', 'own']);
|
|
13
13
|
export const LAYERS = Object.freeze(['slow', 'mid', 'beat']);
|
|
14
14
|
|
|
15
15
|
const PLACEHOLDER = /<[^<>\n]{1,80}>|^\s*(?:todo|tbd|tk|xxx|\.\.\.|…)\s*$/iu;
|
|
@@ -76,6 +76,19 @@ export function validateCoverTreatment(t) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
if ('seed' in t && !Number.isInteger(t.seed)) return bad('seed', 'seed must be an integer');
|
|
79
|
+
// A link in the request names the product: the cover is built from its real assets, fetched first.
|
|
80
|
+
{
|
|
81
|
+
const urls = requestUrls(t.request);
|
|
82
|
+
if (!('product' in t) || t.product === null) {
|
|
83
|
+
if (urls.length) return bad('product', `product is missing: the request links ${urls[0]}; fetch its real assets first with motion.mjs assets --out <dir> --url <link> --cover, look at them, and set product { name, url }`);
|
|
84
|
+
} else {
|
|
85
|
+
const product = t.product;
|
|
86
|
+
if (!product || typeof product !== 'object') return bad('product', 'product must be an object { name, url }');
|
|
87
|
+
if (typeof product.name !== 'string' || !product.name.trim() || PLACEHOLDER.test(product.name)) return bad('product.name', 'product.name names the product');
|
|
88
|
+
if (urls.length && !urls.includes(normUrl(product.url ?? ''))) return bad('product.url', `product.url must be the link the request gives (${urls.join(', ')})`);
|
|
89
|
+
if ('unreachable' in product && product.unreachable !== undefined && product.unreachable !== null && (typeof product.unreachable !== 'string' || !product.unreachable.trim())) return bad('product.unreachable', 'product.unreachable says what could not be reached and why');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
79
92
|
const ambition = prose(t, 'ambition', 2);
|
|
80
93
|
if (ambition) return ambition;
|
|
81
94
|
const { page, out } = COVER.aspects[t.aspect];
|
|
@@ -95,6 +108,17 @@ export function loadCoverTreatment(outDir) {
|
|
|
95
108
|
if (parsed?.mode !== 'cover') throw new BlockedError(EXIT.TREATMENT_INVALID, 'BLOCKED_TREATMENT_INVALID field=mode: this treatment is a film; render it with stage or make (references/treatment.md); a cover sets mode: cover');
|
|
96
109
|
const result = validateCoverTreatment(parsed);
|
|
97
110
|
if (!result.ok) throw new BlockedError(EXIT.TREATMENT_INVALID, `BLOCKED_TREATMENT_INVALID field=${result.field}: ${result.message}`);
|
|
111
|
+
// A linked cover is built only from fetched assets: product.json must exist, list cover files,
|
|
112
|
+
// and an unreachable product stops with exit 22 until the user supplies the art.
|
|
113
|
+
const coverProduct = result.treatment.product;
|
|
114
|
+
if (coverProduct?.unreachable) throw new BlockedError(EXIT.ASSET_MISSING, `BLOCKED_ASSET_MISSING: ${coverProduct.name}'s real assets could not be reached (${coverProduct.unreachable}); ask the user for its logo or README images, put them in ${join(outDir, 'cover', 'assets')} and list them in assets.json`);
|
|
115
|
+
if (coverProduct && !existsSync(join(outDir, 'product.json'))) throw new BlockedError(EXIT.TREATMENT_INVALID, `BLOCKED_TREATMENT_INVALID field=product: no ${join(outDir, 'product.json')}: run motion.mjs assets --out ${outDir} --url ${coverProduct.url} --cover first`);
|
|
116
|
+
if (coverProduct) {
|
|
117
|
+
let fetched;
|
|
118
|
+
try { fetched = JSON.parse(readFileSync(join(outDir, 'product.json'), 'utf8'))?.product?.assets ?? []; } catch (error) { throw new BlockedError(EXIT.TREATMENT_INVALID, `BLOCKED_TREATMENT_INVALID field=product: ${join(outDir, 'product.json')} is not valid JSON (${error.message}); run motion.mjs assets --out ${outDir} --url ${coverProduct.url} --cover again`); }
|
|
119
|
+
const astray = fetched.filter((asset) => !String(asset?.path ?? '').startsWith('cover/assets/'));
|
|
120
|
+
if (astray.length) throw new BlockedError(EXIT.TREATMENT_INVALID, `BLOCKED_TREATMENT_INVALID field=product: ${astray[0].path} sits outside cover/assets/, where the cover page cannot load it; run motion.mjs assets --out ${outDir} --url ${coverProduct.url} --cover again`);
|
|
121
|
+
}
|
|
98
122
|
const first = join(outDir, '.run', 'treatment-first.json');
|
|
99
123
|
if (!existsSync(first)) {
|
|
100
124
|
mkdirSync(join(outDir, '.run'), { recursive: true });
|
|
@@ -210,17 +210,24 @@ export async function renderCover({ out, round = 1, env = process.env, log = (li
|
|
|
210
210
|
const order = seekOrder(times.length, treatment.seed);
|
|
211
211
|
const pool = new CoverPool();
|
|
212
212
|
let master, replay, sought;
|
|
213
|
+
// A 3D hero turns on the GPU rung; when it cannot start there it falls back to software, and
|
|
214
|
+
// the report says so. The replay and the seek share the master's rung.
|
|
215
|
+
const rung = treatment.recipe === 'hero-3d' ? 'gpu' : 'software';
|
|
216
|
+
let rungUsed = rung, rendererUsed = null;
|
|
213
217
|
try {
|
|
214
|
-
const open = (label) => openCover({ dir, fonts, width: pageW, height: pageH, scale: COVER.captureScale, profileRoot: runDir, env, label, log });
|
|
218
|
+
const open = (label, useRung) => openCover({ dir, fonts, width: pageW, height: pageH, scale: COVER.captureScale, profileRoot: runDir, env, label, rung: useRung ?? rung, log });
|
|
215
219
|
const a = await open('master');
|
|
216
220
|
try {
|
|
217
221
|
master = await capturePass({ page: a, pool, times, dir: dirs.frames, prefix: 'f', width: w, height: h });
|
|
218
222
|
log(`cover: master ${times.length} frames (${((Date.now() - started) / 1000).toFixed(1)} s)`);
|
|
219
223
|
await capturePass({ page: a, pool, times, dir: dirs.masks, prefix: 'm', mask: true, width: w, height: h });
|
|
224
|
+
rungUsed = a.rung;
|
|
225
|
+
rendererUsed = a.renderer;
|
|
226
|
+
if (a.notice) { warnings.push(a.notice); log(`warning: ${a.notice}`); }
|
|
220
227
|
} finally { await a.close(); }
|
|
221
|
-
const b = await open('replay');
|
|
228
|
+
const b = await open('replay', rungUsed);
|
|
222
229
|
try { replay = await capturePass({ page: b, pool, times, dir: dirs.frames2, prefix: 'f', width: w, height: h }); } finally { await b.close(); }
|
|
223
|
-
const c = await open('seek');
|
|
230
|
+
const c = await open('seek', rungUsed);
|
|
224
231
|
try { sought = await capturePass({ page: c, pool, times: order.map((i) => times[i]), dir: dirs.seek, prefix: 's', width: w, height: h }); } finally { await c.close(); }
|
|
225
232
|
} finally { await pool.close(); }
|
|
226
233
|
const captureSec = (Date.now() - started) / 1000;
|
|
@@ -279,7 +286,7 @@ export async function renderCover({ out, round = 1, env = process.env, log = (li
|
|
|
279
286
|
const summary = { pass: gate.pass, target: gate.target, medianD: gate.medianD, movingFrac: gate.movingFrac, longestStillSec: gate.longestStillSec, spike: gate.spike, seamLocal: gate.seamLocal, gates: gate.gates };
|
|
280
287
|
writeJson(join(out, 'manifest.json'), {
|
|
281
288
|
schemaVersion: 1, path: 'cover', mode: 'cover', round, recipe: treatment.recipe, treatmentSha256, stillsIndexSha256: indexed.sha256,
|
|
282
|
-
page: treatment.page, size: treatment.out, scale: COVER.captureScale, fps: treatment.fps, frames: times.length, frameMs: ms, seed: treatment.seed,
|
|
289
|
+
page: treatment.page, size: treatment.out, scale: COVER.captureScale, fps: treatment.fps, frames: times.length, frameMs: ms, seed: treatment.seed, rung: rungUsed, renderer: rendererUsed ? `${rendererUsed} on the ${rungUsed} rung (cover path)` : null,
|
|
283
290
|
encoder: encoded.encoder, encode: treatment.encode, file: encoded.file.split('/').at(-1), bytes: statSync(encoded.file).size, captureSec, warnings, gate: summary,
|
|
284
291
|
});
|
|
285
292
|
if (!flash.pass) {
|
|
@@ -326,7 +333,7 @@ export async function renderCoverWindow({ out, round = 1, window: [t0, t1], env
|
|
|
326
333
|
mkdirSync(profileRoot, { recursive: true });
|
|
327
334
|
const pool = new CoverPool();
|
|
328
335
|
try {
|
|
329
|
-
const page = await openCover({ dir: coverDir, fonts, width: treatment.page[0], height: treatment.page[1], scale: COVER.captureScale, profileRoot, env, label: 'window', log });
|
|
336
|
+
const page = await openCover({ dir: coverDir, fonts, width: treatment.page[0], height: treatment.page[1], scale: COVER.captureScale, profileRoot, env, label: 'window', rung: treatment.recipe === 'hero-3d' ? 'gpu' : 'software', log });
|
|
330
337
|
try { await capturePass({ page, pool, times: [...new Set(picks.map((p) => p.index))].map((i) => [i, i / treatment.frames]), dir, prefix: 'f', width: w, height: h }); } finally { await page.close(); }
|
|
331
338
|
} finally { await pool.close(); }
|
|
332
339
|
const file = (i) => join(dir, `f${pad4(i)}.png`);
|
|
@@ -33,12 +33,89 @@ export function luminanceP995(rgba) {
|
|
|
33
33
|
return 1;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const luma8 = (rgba, i) => Math.round((0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2]));
|
|
37
|
+
|
|
38
|
+
/** Gamma-light luminance percentile (0..1) of an opaque RGBA frame. */
|
|
39
|
+
export function luminancePercentile(rgba, q) {
|
|
40
|
+
const bins = new Uint32Array(256);
|
|
41
|
+
const total = rgba.length / 4;
|
|
42
|
+
for (let i = 0; i < rgba.length; i += 4) bins[luma8(rgba, i)] += 1;
|
|
43
|
+
const target = total * q;
|
|
44
|
+
let acc = 0;
|
|
45
|
+
for (let i = 0; i < 256; i += 1) { acc += bins[i]; if (acc >= target) return i / 255; }
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The frame cut into 4 x 3 regions for the rich-fill rule.
|
|
50
|
+
export const FILL_GRID = Object.freeze({ cols: 4, rows: 3 });
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Share of 48 px cells that carry something: a cell is busy when its luma varies (sd > 6) or its
|
|
54
|
+
* mean sits more than 16 levels off the frame's ground, the most common luma smoothed over +-3.
|
|
55
|
+
* Sampled on a 3 px lattice; an opaque RGBA frame in, a 0..1 share out.
|
|
56
|
+
*/
|
|
57
|
+
export function busyShare(rgba, width, height, { cell = 48, step = 3 } = {}) {
|
|
58
|
+
const hist = new Uint32Array(256);
|
|
59
|
+
for (let y = 0; y < height; y += step) for (let x = 0; x < width; x += step) hist[luma8(rgba, (y * width + x) * 4)] += 1;
|
|
60
|
+
let ground = 0;
|
|
61
|
+
let best = -1;
|
|
62
|
+
for (let i = 0; i < 256; i += 1) {
|
|
63
|
+
let s = 0;
|
|
64
|
+
for (let k = Math.max(0, i - 3); k <= Math.min(255, i + 3); k += 1) s += hist[k];
|
|
65
|
+
if (s > best) { best = s; ground = i; }
|
|
66
|
+
}
|
|
67
|
+
let busy = 0;
|
|
68
|
+
let cells = 0;
|
|
69
|
+
for (let cy = 0; cy + cell <= height; cy += cell) {
|
|
70
|
+
for (let cx = 0; cx + cell <= width; cx += cell) {
|
|
71
|
+
let s = 0;
|
|
72
|
+
let s2 = 0;
|
|
73
|
+
let n = 0;
|
|
74
|
+
let mean = 0;
|
|
75
|
+
for (let y = cy; y < cy + cell; y += step) for (let x = cx; x < cx + cell; x += step) {
|
|
76
|
+
const l = luma8(rgba, (y * width + x) * 4);
|
|
77
|
+
s += l; s2 += l * l; n += 1;
|
|
78
|
+
}
|
|
79
|
+
if (!n) continue;
|
|
80
|
+
mean = s / n;
|
|
81
|
+
const sd = Math.sqrt(Math.max(0, s2 / n - mean * mean));
|
|
82
|
+
if (sd > 6 || Math.abs(mean - ground) > 16) busy += 1;
|
|
83
|
+
cells += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return cells ? busy / cells : 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The busy share of each region of the FILL_GRID, row by row. */
|
|
90
|
+
export function regionFill(rgba, width, height) {
|
|
91
|
+
const out = [];
|
|
92
|
+
const rw = Math.floor(width / FILL_GRID.cols);
|
|
93
|
+
const rh = Math.floor(height / FILL_GRID.rows);
|
|
94
|
+
for (let r = 0; r < FILL_GRID.rows; r += 1) {
|
|
95
|
+
for (let c = 0; c < FILL_GRID.cols; c += 1) {
|
|
96
|
+
const sub = new Uint8Array(rw * rh * 4);
|
|
97
|
+
for (let y = 0; y < rh; y += 1) {
|
|
98
|
+
const from = ((r * rh + y) * width + c * rw) * 4;
|
|
99
|
+
sub.set(rgba.subarray(from, from + rw * 4), y * rw * 4);
|
|
100
|
+
}
|
|
101
|
+
out.push(busyShare(sub, rw, rh));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
36
107
|
export function measureFrame(png, { flash = true } = {}) {
|
|
37
108
|
const decoded = decodePng(png);
|
|
38
109
|
const rgba = toRgba(decoded);
|
|
39
110
|
const sha256 = createHash('sha256').update(rgba).digest('hex');
|
|
40
111
|
const grid = flash ? cellGrid(rgba, decoded.width, decoded.height, 4) : null;
|
|
41
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
width: decoded.width, height: decoded.height, rgba, sha256, grid,
|
|
114
|
+
lumP995: flash ? luminanceP995(rgba) : null,
|
|
115
|
+
lumP005: luminancePercentile(rgba, 0.005),
|
|
116
|
+
busy: busyShare(rgba, decoded.width, decoded.height),
|
|
117
|
+
regions: regionFill(rgba, decoded.width, decoded.height),
|
|
118
|
+
};
|
|
42
119
|
}
|
|
43
120
|
|
|
44
121
|
if (!isMainThread && parentPort) {
|