@litfamily/lithermes 1.0.28 → 1.0.30
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/README.md +12 -9
- package/README_Ko-KR.md +12 -9
- package/assets/lithermes-plugin/README.md +15 -2
- package/assets/lithermes-plugin/__init__.py +49 -10
- package/assets/lithermes-plugin/adhd_stack.py +228 -0
- package/assets/lithermes-plugin/core.py +30 -4
- package/assets/lithermes-plugin/core_commands.py +99 -0
- package/assets/lithermes-plugin/core_contexts.py +54 -4
- package/assets/lithermes-plugin/core_contract.py +2 -2
- package/assets/lithermes-plugin/core_routing.py +126 -0
- package/assets/lithermes-plugin/diagnostics.py +3 -2
- package/assets/lithermes-plugin/lit_mark.py +75 -0
- package/assets/lithermes-plugin/payload-version.json +472 -60
- package/assets/lithermes-plugin/plugin.yaml +1 -1
- package/assets/lithermes-plugin/scientific_visualization.py +23 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +7 -7
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/components.css +414 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/fonts.json +42 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/README.md +2 -4
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +4 -4
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/craft-floor.md +10 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe-rules.json +6 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe.mjs +76 -4
- package/assets/lithermes-plugin/skills/lit-adhd/SKILL.md +194 -0
- package/assets/lithermes-plugin/skills/lit-adhd/references/brief-contract.md +55 -0
- package/assets/lithermes-plugin/skills/lit-adhd/references/card-examples.md +86 -0
- package/assets/lithermes-plugin/skills/lit-adhd/references/status-taxonomy.md +40 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/adhd.mjs +11 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/collect.mjs +554 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/gauge.mjs +89 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/hermes-sessions.mjs +219 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/next-step.mjs +101 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/plan-task-rows.mjs +32 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/redact.mjs +42 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/secret-shapes.mjs +59 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/SKILL.md +7 -4
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/verifier-guide.md +5 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_quality.py +4 -4
- package/assets/lithermes-plugin/skills/lit-docx/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/lit-hijack/SKILL.md +239 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/clean-room-build.md +128 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/parity-and-report.md +131 -0
- package/assets/lithermes-plugin/skills/lit-hijack/scripts/hijack.mjs +995 -0
- package/assets/lithermes-plugin/skills/lit-plan/SKILL.md +1 -0
- package/assets/lithermes-plugin/skills/lit-pptx/SKILL.md +11 -6
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/provenance.json +1 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/anti-slop-checklist.md +3 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/authoring-guide.md +1 -1
- package/assets/lithermes-plugin/skills/lit-pptx/references/consensus-protocol.md +1 -1
- package/assets/lithermes-plugin/skills/lit-pptx/references/direction-step.md +8 -2
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +175 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +156 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +141 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +160 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +183 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/layout-families.md +1 -1
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/bauhaus.md +105 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +111 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +114 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/newsroom.md +106 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/riso.md +101 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/craft_extras.py +49 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/deck_output.py +4 -2
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/render-pack.js +96 -10
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/template-registry.js +50 -6
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +103 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/assets/lithermes-plugin/skills/lit-recap/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/lit-schematic/SKILL.md +144 -0
- package/assets/lithermes-plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/assets/lithermes-plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/assets/lithermes-plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/assets/lithermes-plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/journal-figures.md +72 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/kit-api.md +134 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/verification.md +100 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/fonts.mjs +27 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/render.mjs +233 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/assets/lithermes-plugin/skills/lit-scientific-visualization/SKILL.md +19 -1
- package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +12 -11
- package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/motion.mjs +56 -2
- package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/runtime.mjs +129 -4
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +9 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/blender.mjs +154 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/board.mjs +132 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/completion.mjs +48 -5
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/gate.mjs +13 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/house.mjs +44 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/scaffold.mjs +238 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/score.mjs +643 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound-dsp.mjs +21 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound.mjs +171 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-qa.mjs +23 -3
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +124 -17
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/styles.mjs +240 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/treatment.mjs +152 -10
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +67 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/depth.md +74 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/studio.md +87 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/style-bibles.md +9 -2
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/treatment.md +24 -8
- package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
- package/assets/lithermes-plugin/skills/litresearch/SKILL.md +4 -0
- package/assets/lithermes-plugin/skills/readme-studio/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
- package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/assets/lithermes-plugin/skills/review-work/SKILL.md +2 -0
- package/assets/lithermes-plugin/skills/start-work/SKILL.md +2 -1
- package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +3 -3
- package/package.json +1 -1
- package/readme-assets/badge-version.svg +1 -1
- package/src/lib/skillPayload.js +1 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// House styles as data: palette, faces, type scale, layout zones, motion tokens, transitions
|
|
2
|
+
// and sound for one look, so every film in a style shares them. A style lives in
|
|
3
|
+
// `styles/<id>.json`; the treatment names one, the validator checks it against the rules
|
|
4
|
+
// (named in the request, the project's house style, or the fixed keyword auto-pick), the
|
|
5
|
+
// renderer serves it to the stage page as /lit/style.css and /lit/style.js, and the text QA
|
|
6
|
+
// judges copy against it. Zones are one grid per layout, named per shot and mapped per
|
|
7
|
+
// format, so a story re-lays itself per format instead of cropping. Four styles ship;
|
|
8
|
+
// `paper` is stage-only (the type engine has no preset for it, so the type path skips it).
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { contrastRatio, relativeLuminance } from './util.mjs';
|
|
12
|
+
|
|
13
|
+
export const STYLE_SCHEMA = 'litfamily.motion-style/v1';
|
|
14
|
+
export const STYLE_IDS = Object.freeze(['swiss-signal', 'tidal', 'terminalcore', 'paper']);
|
|
15
|
+
export const DEFAULT_STYLE = 'swiss-signal';
|
|
16
|
+
export const TYPE_ROLES = Object.freeze(['display', 'headline', 'body', 'label']);
|
|
17
|
+
export const TRANSITIONS = Object.freeze(['cut', 'match', 'wipe', 'push', 'iris', 'dissolve', 'morph']);
|
|
18
|
+
// Copy may sit this far (a share of the frame side) outside its zone, and this far off a scale step.
|
|
19
|
+
export const ZONE_TOLERANCE = 0.012;
|
|
20
|
+
export const SIZE_TOLERANCE = 0.04;
|
|
21
|
+
|
|
22
|
+
// [x, y, w, h] as frame fractions. `type` bounds where copy may sit; `subject` bounds where the
|
|
23
|
+
// drawing leads (a `title` or `full` subject fills the frame as backdrop). Every type zone sits
|
|
24
|
+
// inside the central 90 % title-safe area.
|
|
25
|
+
export const LAYOUTS = Object.freeze({
|
|
26
|
+
'16:9': Object.freeze({
|
|
27
|
+
title: { type: [0.08, 0.2, 0.84, 0.6], subject: [0, 0, 1, 1] },
|
|
28
|
+
split: { type: [0.08, 0.16, 0.36, 0.68], subject: [0.48, 0.08, 0.46, 0.84] },
|
|
29
|
+
'split-alt': { type: [0.56, 0.16, 0.36, 0.68], subject: [0.06, 0.08, 0.46, 0.84] },
|
|
30
|
+
stage: { type: [0.08, 0.75, 0.84, 0.18], subject: [0.06, 0.07, 0.88, 0.64] },
|
|
31
|
+
hero: { type: [0.08, 0.76, 0.84, 0.17], subject: [0.03, 0.03, 0.94, 0.94] },
|
|
32
|
+
full: { type: null, subject: [0, 0, 1, 1] },
|
|
33
|
+
}),
|
|
34
|
+
'9:16': Object.freeze({
|
|
35
|
+
title: { type: [0.09, 0.26, 0.82, 0.48], subject: [0, 0, 1, 1] },
|
|
36
|
+
split: { type: [0.09, 0.08, 0.82, 0.26], subject: [0.06, 0.36, 0.88, 0.56] },
|
|
37
|
+
'split-alt': { type: [0.09, 0.66, 0.82, 0.26], subject: [0.06, 0.06, 0.88, 0.56] },
|
|
38
|
+
stage: { type: [0.09, 0.79, 0.82, 0.13], subject: [0.06, 0.06, 0.88, 0.68] },
|
|
39
|
+
hero: { type: [0.09, 0.8, 0.82, 0.12], subject: [0.03, 0.03, 0.94, 0.94] },
|
|
40
|
+
full: { type: null, subject: [0, 0, 1, 1] },
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
export const LAYOUT_IDS = Object.freeze(Object.keys(LAYOUTS['16:9']));
|
|
44
|
+
const FRAME = Object.freeze({ '16:9': [1920, 1080], '9:16': [1080, 1920] });
|
|
45
|
+
|
|
46
|
+
// With no name, house file or keyword, the genre chooses the style that reads best: an
|
|
47
|
+
// explainer on the warm, legible paper ground (stage only), a brand mood on the tidal flow.
|
|
48
|
+
export const GENRE_STYLE = Object.freeze({ explainer: 'paper', 'brand-mood': 'tidal' });
|
|
49
|
+
|
|
50
|
+
// Checked in order; the first row with a whole-word hit wins. `paper` has no type-engine preset,
|
|
51
|
+
// so the type path skips its row.
|
|
52
|
+
export const STYLE_PICK = Object.freeze([
|
|
53
|
+
{ style: 'terminalcore', korean: ['터미널', 'CRT', '해커'], latin: ['terminal', 'hacker', 'crt'] },
|
|
54
|
+
{ style: 'tidal', korean: ['물결', '파도', '잔잔한', '흐름'], latin: ['gradient', 'wave', 'tide', 'calm'] },
|
|
55
|
+
{ style: 'paper', korean: ['따뜻한', '밝은', '종이', '손그림', '포근한', '아기자기'], latin: ['warm', 'cozy', 'bright', 'paper', 'handmade', 'hand-drawn'] },
|
|
56
|
+
]);
|
|
57
|
+
const NAMED = Object.freeze({
|
|
58
|
+
'swiss-signal': ['swiss-signal', 'swiss signal', '스위스 시그널'],
|
|
59
|
+
tidal: ['tidal', '타이달'],
|
|
60
|
+
terminalcore: ['terminalcore', '터미널코어'],
|
|
61
|
+
paper: ['paper style', 'paper look', '페이퍼 스타일', '페이퍼'],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export class StyleError extends Error {}
|
|
65
|
+
|
|
66
|
+
const HEX = /^#[0-9a-f]{6}$/iu;
|
|
67
|
+
const isText = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
68
|
+
const escape = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
69
|
+
|
|
70
|
+
export const matchesKeyword = (text, keyword, korean) => {
|
|
71
|
+
const pattern = korean
|
|
72
|
+
? new RegExp(`(?<![\\p{L}\\p{N}])${escape(keyword)}`, 'u')
|
|
73
|
+
: new RegExp(`(?<![\\p{L}\\p{N}])${escape(keyword)}(?![\\p{L}\\p{N}])`, 'iu');
|
|
74
|
+
return pattern.test(String(text ?? ''));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Validate a style document; returns it, 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 key of ['name', 'summary']) if (!isText(style[key])) fail(key, 'must be a non-empty string');
|
|
86
|
+
const palette = style.palette ?? {};
|
|
87
|
+
for (const role of ['ground', 'surface', 'ink', 'muted', 'signal', 'line']) {
|
|
88
|
+
if (!HEX.test(palette[role] ?? '')) fail(`palette.${role}`, 'must be #rrggbb');
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(style.textRoles) || style.textRoles.some((role) => !HEX.test(palette[role] ?? ''))) {
|
|
91
|
+
fail('textRoles', 'every text role names a palette colour');
|
|
92
|
+
}
|
|
93
|
+
const ground = relativeLuminance(palette.ground);
|
|
94
|
+
if (contrastRatio(relativeLuminance(palette.ink), ground) < 7) fail('palette.ink', 'ink on ground needs 7:1');
|
|
95
|
+
if (contrastRatio(relativeLuminance(palette.muted), ground) < 4.5) fail('palette.muted', 'muted on ground needs 4.5:1');
|
|
96
|
+
for (const role of ['signal', 'accent']) {
|
|
97
|
+
if (palette[role] && contrastRatio(relativeLuminance(palette[role]), ground) < 3) {
|
|
98
|
+
fail(`palette.${role}`, `${role} on ground needs 3:1 (large type)`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(style.accents) || style.accents.length < 3) fail('accents', 'names at least three role accents a rich film may add');
|
|
102
|
+
style.accents.forEach((accent, index) => {
|
|
103
|
+
if (!isText(accent?.role)) fail(`accents[${index}].role`, 'says what the colour marks');
|
|
104
|
+
if (!HEX.test(accent?.color ?? '')) fail(`accents[${index}].color`, 'must be #rrggbb');
|
|
105
|
+
if (contrastRatio(relativeLuminance(accent.color), ground) < 3) fail(`accents[${index}].color`, 'an accent on ground needs 3:1 (large type)');
|
|
106
|
+
});
|
|
107
|
+
for (const role of TYPE_ROLES) if (!isText(style.faces?.[role]?.family)) fail(`faces.${role}`, 'needs a family');
|
|
108
|
+
for (const format of Object.keys(FRAME)) {
|
|
109
|
+
const steps = style.scale?.[format];
|
|
110
|
+
if (!steps || TYPE_ROLES.some((role) => !(steps[role] > 0))) fail(`scale.${format}`, 'needs a size per type role');
|
|
111
|
+
if (!(steps.display > steps.headline && steps.headline > steps.body && steps.body > steps.label)) {
|
|
112
|
+
fail(`scale.${format}`, 'steps must descend display > headline > body > label');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const motion = style.motion ?? {};
|
|
116
|
+
for (const key of ['enter', 'move', 'exit']) {
|
|
117
|
+
if (!Array.isArray(motion.ease?.[key]) || motion.ease[key].length !== 4) fail(`motion.ease.${key}`, 'is a cubic-bezier [x1, y1, x2, y2]');
|
|
118
|
+
}
|
|
119
|
+
for (const key of ['micro', 'move', 'hero', 'exit']) if (!(motion.dur?.[key] > 0)) fail(`motion.dur.${key}`, 'seconds > 0');
|
|
120
|
+
if (!(motion.dur.exit <= motion.dur.hero)) fail('motion.dur.exit', 'an exit is never longer than the hero move');
|
|
121
|
+
if (!Array.isArray(style.transitions) || !style.transitions.length || style.transitions.some((name) => !TRANSITIONS.includes(name))) {
|
|
122
|
+
fail('transitions', `a subset of ${TRANSITIONS.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
if (!isText(style.score?.genre)) fail('score.genre', 'names the genre a film\u2019s score takes by default');
|
|
125
|
+
if (!style.sound || !isText(style.sound.timbre)) fail('sound', 'needs the generated bed\u2019s palette');
|
|
126
|
+
if (!Array.isArray(style.beds) || style.beds.length < 2 || style.beds.some((bed) => !isText(bed?.timbre) || !isText(bed?.key) || !(bed?.tempo > 0))) {
|
|
127
|
+
fail('beds', 'lists at least two beds a film may pick, each { timbre, key, mode, tempo }');
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(style.bans) || style.bans.length < 3) fail('bans', 'names at least three defaults this style never uses');
|
|
130
|
+
return style;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const cache = new Map();
|
|
134
|
+
/** Load a shipped style by id (validated, frozen). */
|
|
135
|
+
export function readStyle(id) {
|
|
136
|
+
if (!STYLE_IDS.includes(id)) throw new StyleError(`unknown style: ${id} (choose ${STYLE_IDS.join(', ')})`);
|
|
137
|
+
if (!cache.has(id)) {
|
|
138
|
+
const file = fileURLToPath(new URL(`../styles/${id}.json`, import.meta.url));
|
|
139
|
+
cache.set(id, Object.freeze(validateStyle(JSON.parse(readFileSync(file, 'utf8')))));
|
|
140
|
+
}
|
|
141
|
+
return cache.get(id);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Every font family copy may use in this style (the role faces, the Hangul face, Pretendard). */
|
|
145
|
+
export const styleFamilies = (style) => new Set([...TYPE_ROLES.map((role) => style.faces[role].family), style.hangul?.family ?? 'Pretendard', 'Pretendard']);
|
|
146
|
+
/** The hex colours copy may use. */
|
|
147
|
+
export const styleTextColors = (style) => style.textRoles.map((role) => style.palette[role].toLowerCase());
|
|
148
|
+
/** The beds a film in this style may pick from, the default first. */
|
|
149
|
+
export const styleBeds = (style) => style.beds;
|
|
150
|
+
/** The role accents a rich film may add to the core palette, each { role, color }. */
|
|
151
|
+
export const styleAccents = (style) => style.accents;
|
|
152
|
+
/** All palette colours of the style (what a stage treatment's palette may list); rich adds the accents. */
|
|
153
|
+
export const styleColors = (style, { rich = false } = {}) => [...Object.values(style.palette), ...(rich ? style.accents.map((accent) => accent.color) : [])].map((color) => color.toLowerCase());
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Which style a film takes. A style named in the request wins; then the project's house style;
|
|
157
|
+
* then the keyword auto-pick; then the genre's style; then the default. Returns { id, source, reason }.
|
|
158
|
+
*/
|
|
159
|
+
export function pickStyle({ request = '', copy = [], path = 'stage', house = null, genre = null } = {}) {
|
|
160
|
+
const text = String(request);
|
|
161
|
+
for (const [id, names] of Object.entries(NAMED)) {
|
|
162
|
+
if (path === 'type' && !readStyle(id).typePreset) continue;
|
|
163
|
+
const hit = names.find((name) => matchesKeyword(text, name, /[가-힣]/u.test(name)));
|
|
164
|
+
if (hit) return { id, source: 'request', reason: `the request names the ${id} style ("${hit}")` };
|
|
165
|
+
}
|
|
166
|
+
if (house?.style && STYLE_IDS.includes(house.style) && (path !== 'type' || readStyle(house.style).typePreset)) {
|
|
167
|
+
return { id: house.style, source: 'house', reason: `the project's house style is ${house.style} (motion-house.json)` };
|
|
168
|
+
}
|
|
169
|
+
const haystack = [text, ...copy].join('\n');
|
|
170
|
+
for (const row of STYLE_PICK) {
|
|
171
|
+
if (path === 'type' && !readStyle(row.style).typePreset) continue;
|
|
172
|
+
for (const keyword of row.korean) if (matchesKeyword(haystack, keyword, true)) return { id: row.style, source: 'auto', reason: `the request mentions "${keyword}"` };
|
|
173
|
+
for (const keyword of row.latin) if (matchesKeyword(haystack, keyword, false)) return { id: row.style, source: 'auto', reason: `the request mentions "${keyword}"` };
|
|
174
|
+
}
|
|
175
|
+
const byGenre = GENRE_STYLE[genre];
|
|
176
|
+
if (byGenre && (path !== 'type' || readStyle(byGenre).typePreset)) {
|
|
177
|
+
return { id: byGenre, source: 'genre', reason: `no style is named and no keyword picks one, and a ${genre} film takes ${byGenre}` };
|
|
178
|
+
}
|
|
179
|
+
return { id: DEFAULT_STYLE, source: 'default', reason: `no style is named and no keyword picks one, so the default ${DEFAULT_STYLE}` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A layout's zones in pixels for a format: { type: {x, y, w, h} | null, subject: {...} }. */
|
|
183
|
+
export function zonesPx(format) {
|
|
184
|
+
const [frameWidth, frameHeight] = FRAME[format];
|
|
185
|
+
const px = (rect) => (rect ? { x: Math.round(rect[0] * frameWidth), y: Math.round(rect[1] * frameHeight), w: Math.round(rect[2] * frameWidth), h: Math.round(rect[3] * frameHeight) } : null);
|
|
186
|
+
return Object.fromEntries(Object.entries(LAYOUTS[format]).map(([id, zones]) => [id, { type: px(zones.type), subject: px(zones.subject) }]));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The type step a rendered size sits on, or null when it is off the style's scale. */
|
|
190
|
+
export function scaleStep(style, format, sizePx) {
|
|
191
|
+
for (const role of TYPE_ROLES) {
|
|
192
|
+
const step = style.scale[format][role];
|
|
193
|
+
if (Math.abs(sizePx - step) <= step * SIZE_TOLERANCE) return role;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** /lit/style.js: window.LitStyle (tokens and pixel zones) and window.LitTimeline (the treatment's shots). */
|
|
199
|
+
export function styleScript(style, format, treatment) {
|
|
200
|
+
const timeline = {
|
|
201
|
+
format, fps: treatment.fps ?? 60, duration: treatment.durationSec,
|
|
202
|
+
beats: (treatment.beats ?? []).map((beat, index) => ({ index, t0: beat.t0, t1: beat.t1, layout: beat.layout ?? null, transition: beat.transition ?? null })),
|
|
203
|
+
};
|
|
204
|
+
const data = {
|
|
205
|
+
id: style.id, name: style.name, format, palette: style.palette, textRoles: style.textRoles,
|
|
206
|
+
faces: style.faces, hangul: style.hangul ?? null, scale: style.scale[format], leading: style.leading ?? {},
|
|
207
|
+
motion: style.motion, transitions: style.transitions, ground: style.ground, zones: zonesPx(format),
|
|
208
|
+
};
|
|
209
|
+
return `window.LitStyle = Object.freeze(${JSON.stringify(data)});\nwindow.LitTimeline = Object.freeze(${JSON.stringify(timeline)});\n`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const familyStack = (style, role) => {
|
|
213
|
+
const face = style.faces[role];
|
|
214
|
+
const hangul = style.hangul?.family ?? 'Pretendard';
|
|
215
|
+
return [face.family, hangul, 'Pretendard'].filter((family, index, all) => all.indexOf(family) === index).map((family) => `"${family}"`).join(', ');
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** /lit/style.css for one style and format: custom properties, type-role classes, colour classes. */
|
|
219
|
+
export function styleCss(style, format) {
|
|
220
|
+
const steps = style.scale[format];
|
|
221
|
+
const vars = [
|
|
222
|
+
...Object.entries(style.palette).map(([key, value]) => ` --lit-${key}: ${value};`),
|
|
223
|
+
...TYPE_ROLES.map((role) => ` --lit-size-${role}: ${steps[role]}px;`),
|
|
224
|
+
];
|
|
225
|
+
const roles = TYPE_ROLES.map((role) => {
|
|
226
|
+
const face = style.faces[role];
|
|
227
|
+
return `.lit-${role} { font-family: ${familyStack(style, role)}; font-size: ${steps[role]}px; font-weight: ${face.weight}; line-height: ${style.leading?.[role] ?? 1.3}; letter-spacing: 0; margin: 0; }`;
|
|
228
|
+
});
|
|
229
|
+
const colours = style.textRoles.map((role) => `.lit-${role === 'ground' ? 'knockout' : role} { color: var(--lit-${role}); }`);
|
|
230
|
+
return [
|
|
231
|
+
`/* lit-typographic-motion style: ${style.id} (${format}) */`,
|
|
232
|
+
`:root {\n${vars.join('\n')}\n}`,
|
|
233
|
+
// Korean breaks between eojeol, never inside a word (type-craft.md).
|
|
234
|
+
`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; }`,
|
|
235
|
+
...roles,
|
|
236
|
+
...colours,
|
|
237
|
+
'.lit-zone { position: absolute; box-sizing: border-box; }',
|
|
238
|
+
'',
|
|
239
|
+
].join('\n');
|
|
240
|
+
}
|
|
@@ -5,19 +5,36 @@
|
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { LAYOUT_IDS, pickStyle, readStyle, styleColors, styleFamilies } from './styles.mjs';
|
|
9
|
+
import { readHouse } from './house.mjs';
|
|
10
|
+
import { EVENT_SOUNDS } from './sound.mjs';
|
|
11
|
+
import { SCORE_GENRES } from './score.mjs';
|
|
8
12
|
|
|
9
13
|
export const GENRES = Object.freeze(['announcement', 'brand-mood', 'event', 'explainer', 'motion-graphics', 'type-led', 'other']);
|
|
10
14
|
export const ARC_STAGES = Object.freeze({ announcement: 5, 'brand-mood': 4, event: 4, explainer: 4, 'motion-graphics': 5, other: 3 });
|
|
11
15
|
export const DEVICE_KINDS = Object.freeze(['illustration', 'diagram', 'chart', 'icon', 'shape', 'path', 'mask', 'depth3d', 'particles', 'grid', 'gradient', 'photo-texture']);
|
|
12
16
|
export const TEXTURE_KINDS = Object.freeze(['grid', 'gradient', 'particles', 'photo-texture']);
|
|
13
17
|
export const FORMATS = Object.freeze({ '16:9': [1920, 1080], '9:16': [1080, 1920] });
|
|
14
|
-
export const SOUND_MODES = Object.freeze(['generated', 'supplied', 'authored', 'none']);
|
|
18
|
+
export const SOUND_MODES = Object.freeze(['score', 'generated', 'supplied', 'authored', 'none']);
|
|
19
|
+
export const EVENT_OFF_CUT_SEC = 0.1;
|
|
15
20
|
// The families fonts.css serves; the type engine draws from the same set.
|
|
16
21
|
export const FACES = Object.freeze(['Pretendard', 'Archivo', 'Galmuri9', 'VT323', 'Silkscreen', 'MesloLGS NF']);
|
|
17
22
|
export const SOUND_PALETTES = Object.freeze(['soft-mallet', 'warm-keys', 'glass-pulse', 'low-strings']);
|
|
23
|
+
export const PROTAGONIST_KINDS = Object.freeze(['character', 'object']);
|
|
24
|
+
// A rich stage film plans its density before any page exists: the depth layers
|
|
25
|
+
// of its world, the labelled parts the viewer reads and the camera moves.
|
|
26
|
+
export const RICH_WORLD = Object.freeze({ layers: 3, labels: 6, labelWords: 4, cameraShare: 0.5 });
|
|
27
|
+
const LOCKED_CAMERA = /^\s*(?:none|locked|static|still|hold|fixed|no move)\s*\.?\s*$/iu;
|
|
28
|
+
// A bare deferred-work marker with no value: the global placeholder sweep only
|
|
29
|
+
// sees <angle> markers, so story, protagonist and world fields check this too.
|
|
30
|
+
const BARE_PLACEHOLDER = /^\s*(?:todo|tbd|tk|xxx|\.\.\.|…)\s*$/iu;
|
|
18
31
|
const FLOOR_GENRES = new Set(['announcement', 'event', 'explainer', 'motion-graphics']);
|
|
19
32
|
const MIN_BEAT_SEC = 1.2;
|
|
20
33
|
const MAX_GAP_SEC = 0.25;
|
|
34
|
+
const REGISTERS = Object.freeze(['rich', 'minimal']);
|
|
35
|
+
// The request asks for a calm, quiet or minimal film: the one case the minimal register is for.
|
|
36
|
+
const MINIMAL_ASKED = /미니멀|잔잔|차분|조용|심플|절제|단순하게|고요|\bminimal(?:ist|ism)?\b|\bcalm\b|\bquiet\b|\bsimple\b|\bunderstated\b|\brestrained\b|\bsparse\b|\bserene\b/iu;
|
|
37
|
+
const PREVIEW_ASKED = /미리\s?보기|스토리보드|콘티|\bpreview\b|\bstoryboard\b/iu;
|
|
21
38
|
|
|
22
39
|
const skillRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
40
|
|
|
@@ -57,7 +74,10 @@ export function sharesSubstring(text, request, limit = restatementLimit(request)
|
|
|
57
74
|
// Free-text leaves the copiedExample rule compares.
|
|
58
75
|
export function freeTextLeaves(t) {
|
|
59
76
|
const leaves = [t?.idea, t?.audience, t?.channel, t?.ambition];
|
|
60
|
-
for (const beat of Array.isArray(t?.beats) ? t.beats : []) leaves.push(beat?.purpose, beat?.onScreen, beat?.motion, beat?.sound);
|
|
77
|
+
for (const beat of Array.isArray(t?.beats) ? t.beats : []) leaves.push(beat?.purpose, beat?.onScreen, beat?.motion, beat?.sound, beat?.hero);
|
|
78
|
+
for (const key of ['expect', 'turn', 'payoff']) leaves.push(t?.story?.[key]);
|
|
79
|
+
leaves.push(t?.protagonist?.look);
|
|
80
|
+
for (const act of Array.isArray(t?.protagonist?.acts) ? t.protagonist.acts : []) leaves.push(act?.action);
|
|
61
81
|
for (const line of Array.isArray(t?.copy?.lines) ? t.copy.lines : []) leaves.push(line);
|
|
62
82
|
for (const entry of Array.isArray(t?.palette) ? t.palette : []) leaves.push(entry?.role);
|
|
63
83
|
return leaves.filter((v) => typeof v === 'string').map(normalizeText).filter(Boolean);
|
|
@@ -104,7 +124,7 @@ const need = (cond, field, message) => { if (!cond) throw new Invalid(field, mes
|
|
|
104
124
|
function check(t, { outDir, examples }) {
|
|
105
125
|
need(t && typeof t === 'object' && !Array.isArray(t), 'treatment', 'treatment.json must be a JSON object');
|
|
106
126
|
need(t.mode !== 'cover', 'mode', 'this is a cover treatment; render it with `cover --out DIR` (references/cover.md)');
|
|
107
|
-
for (const field of ['request', 'genre', 'path', 'pathReason', 'idea', 'audience', 'channel', 'format', 'durationSec', 'beats', 'subject', 'visualDevices', 'typePlan', 'palette', 'sound', 'copy', 'ambition']) {
|
|
127
|
+
for (const field of ['request', 'genre', 'path', 'pathReason', 'style', 'idea', 'audience', 'channel', 'format', 'durationSec', 'beats', 'subject', 'visualDevices', 'typePlan', 'palette', 'sound', 'copy', 'ambition']) {
|
|
108
128
|
need(t[field] !== undefined && t[field] !== null && t[field] !== '', field, `${field} is required`);
|
|
109
129
|
}
|
|
110
130
|
const mine = freeTextLeaves(t);
|
|
@@ -120,6 +140,17 @@ function check(t, { outDir, examples }) {
|
|
|
120
140
|
need(GENRES.includes(t.genre), 'genre', `genre must be one of ${GENRES.join(', ')}`);
|
|
121
141
|
need(['type', 'stage'].includes(t.path), 'path', 'path must be type or stage');
|
|
122
142
|
need(text(t.pathReason), 'pathReason', 'pathReason must say why this path fits');
|
|
143
|
+
const house = outDir ? readHouse(outDir) : null;
|
|
144
|
+
const pick = pickStyle({ request: t.request, copy: Array.isArray(t.copy?.lines) ? t.copy.lines : [], path: t.path, house, genre: t.genre });
|
|
145
|
+
need('style' in t, 'style', 'missing: one of swiss-signal, tidal, terminalcore, paper (references/studio.md)');
|
|
146
|
+
need(['swiss-signal', 'tidal', 'terminalcore', 'paper'].includes(t.style), 'style', 'must be one of swiss-signal, tidal, terminalcore, paper');
|
|
147
|
+
if (t.path === 'type' && t.style === 'paper') need(false, 'style', 'paper is stage-only; the type path takes swiss-signal, tidal or terminalcore');
|
|
148
|
+
need(t.style === pick.id, 'style', `${pick.reason}; this film takes the ${pick.id} style`);
|
|
149
|
+
const register = t.register ?? 'rich';
|
|
150
|
+
need(REGISTERS.includes(register), 'register', 'rich (the default) or minimal');
|
|
151
|
+
need(!(register === 'minimal' && !MINIMAL_ASKED.test(t.request)), 'register', 'minimal only when the request asks for a calm, quiet or minimal film; the default is rich (references/studio.md, Register)');
|
|
152
|
+
need(!(register === 'rich' && MINIMAL_ASKED.test(t.request)), 'register', 'the request asks for a calm or minimal film: register minimal');
|
|
153
|
+
if (t.preview === true) need(PREVIEW_ASKED.test(t.request), 'preview', 'true only when the request asks for a preview or storyboard first');
|
|
123
154
|
need(Object.hasOwn(FORMATS, t.format), 'format', 'format must be 16:9 or 9:16');
|
|
124
155
|
need(text(t.formatReason), 'formatReason', 'formatReason must tie the format to the channel');
|
|
125
156
|
need(text(t.audience), 'audience', 'audience must say who watches');
|
|
@@ -157,11 +188,58 @@ function check(t, { outDir, examples }) {
|
|
|
157
188
|
const at = `beats[${i}]`;
|
|
158
189
|
need(beat && typeof beat.t0 === 'number' && typeof beat.t1 === 'number', at, `${at} needs numeric t0 and t1`);
|
|
159
190
|
for (const key of ['purpose', 'onScreen', 'motion', 'sound']) need(text(beat[key]), `${at}.${key}`, `${at}.${key} is required`);
|
|
191
|
+
if (t.path === 'stage') {
|
|
192
|
+
const style = readStyle(t.style);
|
|
193
|
+
need(LAYOUT_IDS.includes(beat.layout), `${at}.layout`, `${at}.layout must be one of ${LAYOUT_IDS.join(', ')}`);
|
|
194
|
+
need(text(beat.hero) && !PLACEHOLDER.test(String(beat.hero)), `${at}.hero`, `${at}.hero names the one move the eye follows in this shot`);
|
|
195
|
+
if (i === beats.length - 1) need(beat.transition === 'end', `${at}.transition`, `${at}.transition: the last shot's transition is end`);
|
|
196
|
+
else need(style.transitions.includes(beat.transition), `${at}.transition`, `${at}.transition: the ${t.style} style hands off with ${style.transitions.join(', ')}`);
|
|
197
|
+
if ('lines' in beat) {
|
|
198
|
+
const count = Array.isArray(t.copy?.lines) ? t.copy.lines.length : 0;
|
|
199
|
+
need(Array.isArray(beat.lines) && beat.lines.every((key) => Number.isInteger(key) && key >= 0 && key < count), `${at}.lines`, `${at}.lines lists the indices of the copy lines this shot shows`);
|
|
200
|
+
need(!(beat.layout === 'full' && beat.lines.length), `${at}.lines`, `${at}.lines: a full shot has no type zone, so it shows no copy`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
160
203
|
need(beat.t1 - beat.t0 >= MIN_BEAT_SEC - 1e-9, at, `${at} lasts ${(beat.t1 - beat.t0).toFixed(2)} s; every beat is at least ${MIN_BEAT_SEC} s`);
|
|
161
204
|
need(beat.t0 - cursor <= MAX_GAP_SEC + 1e-9, at, `${at} leaves a ${(beat.t0 - cursor).toFixed(2)} s gap; beats cover the film with no gap over ${MAX_GAP_SEC} s`);
|
|
162
205
|
cursor = Math.max(cursor, beat.t1);
|
|
163
206
|
});
|
|
164
207
|
need(d - cursor <= MAX_GAP_SEC + 1e-9 && cursor <= d + MAX_GAP_SEC, 'beats', `beats end at ${cursor} s but the film runs ${d} s`);
|
|
208
|
+
if (t.path === 'stage') {
|
|
209
|
+
for (let i = 1; i < beats.length; i++) {
|
|
210
|
+
need(beats[i].layout !== beats[i - 1].layout, `beats[${i}].layout`, `shots ${i} and ${i + 1} both use ${beats[i].layout}; adjacent shots change layout (references/studio.md)`);
|
|
211
|
+
}
|
|
212
|
+
const used = new Set(beats.map((beat) => beat.layout));
|
|
213
|
+
need(beats.length < 4 || used.size >= 3, 'beats', `${beats.length} shots use only ${[...used].join(' and ')}; a film of four or more shots uses at least three layouts`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const story = t.story;
|
|
217
|
+
need(story && typeof story === 'object', 'story', 'story is required: { expect, turn, beat, payoff }: what the viewer expects, what happens instead, the shot where it lands, what the ending gives');
|
|
218
|
+
for (const key of ['expect', 'turn', 'payoff']) {
|
|
219
|
+
need(text(story[key]) && !BARE_PLACEHOLDER.test(story[key]), `story.${key}`, `story.${key} must be a non-empty string`);
|
|
220
|
+
need(sentences(story[key]).length <= 2, `story.${key}`, `story.${key} is at most 2 sentences`);
|
|
221
|
+
}
|
|
222
|
+
need(normalizeText(story.turn) !== normalizeText(story.expect), 'story.turn', 'the turn is what the viewer does not expect; it cannot equal story.expect');
|
|
223
|
+
need(Number.isInteger(story.beat) && story.beat >= 1 && story.beat < beats.length, 'story.beat', `story.beat is the index of the shot where the turn lands: after the opening, before the end (1-${beats.length - 1})`);
|
|
224
|
+
|
|
225
|
+
if (t.path === 'stage') {
|
|
226
|
+
const protagonist = t.protagonist;
|
|
227
|
+
need(protagonist && typeof protagonist === 'object', 'protagonist', 'protagonist is required: { name, kind, look, acts[] }: the character or hero object that acts across the film');
|
|
228
|
+
need(text(protagonist.name) && !BARE_PLACEHOLDER.test(protagonist.name), 'protagonist.name', 'protagonist.name must name it');
|
|
229
|
+
need(PROTAGONIST_KINDS.includes(protagonist.kind), 'protagonist.kind', `protagonist.kind must be ${PROTAGONIST_KINDS.join(' or ')}`);
|
|
230
|
+
need(text(protagonist.look) && !BARE_PLACEHOLDER.test(protagonist.look), 'protagonist.look', 'protagonist.look says what it looks like on screen: shape, material, colour role, scale in the frame');
|
|
231
|
+
need(Array.isArray(protagonist.acts) && protagonist.acts.length > 0, 'protagonist.acts', 'protagonist.acts lists what it does, shot by shot: [{ beat, action }]');
|
|
232
|
+
const seen = new Set();
|
|
233
|
+
protagonist.acts.forEach((act, i) => {
|
|
234
|
+
need(Number.isInteger(act?.beat) && act.beat >= 0 && act.beat < beats.length, `protagonist.acts[${i}].beat`, 'protagonist.acts[].beat must be a beat index');
|
|
235
|
+
need(!seen.has(act.beat), `protagonist.acts[${i}].beat`, 'one act per shot');
|
|
236
|
+
seen.add(act.beat);
|
|
237
|
+
need(text(act.action) && !BARE_PLACEHOLDER.test(act.action), `protagonist.acts[${i}].action`, 'say what it does in this shot, as a viewer would see it');
|
|
238
|
+
});
|
|
239
|
+
const covered = [...seen].reduce((sum, b) => sum + (beats[b].t1 - beats[b].t0), 0);
|
|
240
|
+
need(covered >= 0.5 * d - 1e-9, 'protagonist.acts', `it acts for ${covered.toFixed(1)} s, under half the film; it carries the film, not one shot`);
|
|
241
|
+
need(seen.has(story.beat), 'protagonist.acts', `the turn lands at shot ${story.beat + 1}, and the protagonist has no act there`);
|
|
242
|
+
}
|
|
165
243
|
|
|
166
244
|
const subject = t.subject;
|
|
167
245
|
need(subject && text(subject.name), 'subject.name', 'subject.name is required');
|
|
@@ -188,31 +266,95 @@ function check(t, { outDir, examples }) {
|
|
|
188
266
|
need(kinds.size >= 2, 'visualDevices', `the stage path needs at least 2 distinct non-texture device kinds; this has ${kinds.size}`);
|
|
189
267
|
}
|
|
190
268
|
|
|
269
|
+
if ('depth' in t) need(['2d', '3d'].includes(t.depth), 'depth', 'depth is 2d (the default) or 3d');
|
|
270
|
+
const solid = Array.isArray(devices) && devices.some((device) => device?.kind === 'depth3d' && device?.role === 'subject');
|
|
271
|
+
if (t.path === 'stage') {
|
|
272
|
+
need(!(solid && t.depth !== '3d'), 'depth', 'the subject is a depth3d device: set depth to 3d, so the film takes the 3D route (references/depth.md)');
|
|
273
|
+
need(!(t.depth === '3d' && !solid), 'depth', 'depth 3d needs the subject drawn as a depth3d device (role subject)');
|
|
274
|
+
if ('render3d' in t) need(['three', 'blender'].includes(t.render3d), 'render3d', 'render3d is three (the GPU rung, the default) or blender (the offline Cycles rung)');
|
|
275
|
+
need(t.render3d !== 'blender' || t.depth === '3d', 'render3d', 'the Blender rung renders a 3D world: set depth to 3d with a depth3d subject');
|
|
276
|
+
} else {
|
|
277
|
+
need(t.depth !== '3d', 'depth', 'depth 3d is a stage-path route');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (t.path === 'stage' && register === 'rich') {
|
|
281
|
+
const world = t.world;
|
|
282
|
+
need(world && typeof world === 'object', 'world', 'world is required: { layers, labels, camera }: the layered world, its labelled parts and its camera moves');
|
|
283
|
+
need(Array.isArray(world.layers) && world.layers.filter(text).length >= RICH_WORLD.layers, 'world.layers', `world.layers needs at least ${RICH_WORLD.layers} depth layers, back to front, each saying what lives there`);
|
|
284
|
+
need(Array.isArray(world.labels) && world.labels.length >= RICH_WORLD.labels, 'world.labels', `world.labels needs at least ${RICH_WORLD.labels} labelled parts the viewer reads on screen`);
|
|
285
|
+
world.labels.forEach((label, i) => {
|
|
286
|
+
need(text(label) && !BARE_PLACEHOLDER.test(label), `world.labels[${i}]`, 'the label as it appears on screen');
|
|
287
|
+
need(String(label).trim().split(/\s+/u).length <= RICH_WORLD.labelWords, `world.labels[${i}]`, `a label is at most ${RICH_WORLD.labelWords} words; longer text is copy`);
|
|
288
|
+
});
|
|
289
|
+
need(Array.isArray(world.camera), 'world.camera', 'world.camera lists the camera moves: [{ beat, move }]');
|
|
290
|
+
const moving = new Set();
|
|
291
|
+
world.camera.forEach((entry, i) => {
|
|
292
|
+
need(Number.isInteger(entry?.beat) && entry.beat >= 0 && entry.beat < beats.length, `world.camera[${i}].beat`, 'world.camera[].beat must be a beat index');
|
|
293
|
+
need(text(entry.move) && !BARE_PLACEHOLDER.test(entry.move) && !LOCKED_CAMERA.test(entry.move), `world.camera[${i}].move`, 'name the move (push-in, truck, crane, orbit, rack focus); a locked shot is not listed');
|
|
294
|
+
moving.add(entry.beat);
|
|
295
|
+
});
|
|
296
|
+
need(moving.size >= RICH_WORLD.cameraShare * beats.length - 1e-9, 'world.camera', `the camera moves in ${moving.size} of ${beats.length} shots; a rich film moves it in at least half`);
|
|
297
|
+
}
|
|
298
|
+
|
|
191
299
|
const plan = t.typePlan;
|
|
192
300
|
need(plan && Array.isArray(plan.faces) && plan.faces.length > 0, 'typePlan.faces', 'typePlan.faces must list faces');
|
|
193
301
|
plan.faces.forEach((face, i) => need(FACES.some((f) => f.toLowerCase() === String(face).toLowerCase()), `typePlan.faces[${i}]`, `${face} is not a verified face; use ${FACES.join(', ')}`));
|
|
302
|
+
if (t.path === 'stage') {
|
|
303
|
+
const families = styleFamilies(readStyle(t.style));
|
|
304
|
+
const off = plan.faces.find((face) => ![...families].some((family) => family.toLowerCase() === String(face).toLowerCase()));
|
|
305
|
+
need(!off, 'typePlan.faces', `${off} is not a face of the ${t.style} style (${[...families].join(', ')})`);
|
|
306
|
+
}
|
|
194
307
|
need(text(plan.hierarchy), 'typePlan.hierarchy', 'typePlan.hierarchy is required');
|
|
195
308
|
need(Number.isInteger(plan.maxWordsOnScreen) && plan.maxWordsOnScreen > 0, 'typePlan.maxWordsOnScreen', 'typePlan.maxWordsOnScreen must be a positive integer');
|
|
196
309
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
need(
|
|
201
|
-
|
|
310
|
+
if (t.path === 'stage') {
|
|
311
|
+
const rich = register === 'rich';
|
|
312
|
+
const allowed = new Set(styleColors(readStyle(t.style), { rich }));
|
|
313
|
+
need(Array.isArray(t.palette) && t.palette.length <= (rich ? 14 : 9), 'palette', 'palette lists the style\u2019s colours with their roles');
|
|
314
|
+
t.palette.forEach((entry, i) => {
|
|
315
|
+
need(allowed.has(String(entry?.color ?? '').toLowerCase()), `palette[${i}].color`, `${entry?.color} is not a colour of the ${t.style} style${rich ? ' or one of its role accents' : ' (a minimal film keeps to the core palette)'}; the stage palette comes from the style`);
|
|
316
|
+
need(text(entry.role), `palette[${i}].role`, `palette[${i}].role is required`);
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
need(Array.isArray(t.palette) && t.palette.length >= 3 && t.palette.length <= 6, 'palette', 'palette must hold 3 to 6 colours');
|
|
320
|
+
t.palette.forEach((entry, i) => {
|
|
321
|
+
need(/^#[0-9a-f]{6}$/iu.test(entry?.color || ''), `palette[${i}].color`, `palette[${i}].color must be #rrggbb`);
|
|
322
|
+
need(text(entry.role), `palette[${i}].role`, `palette[${i}].role is required`);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
202
325
|
|
|
203
326
|
const sound = t.sound;
|
|
204
327
|
need(sound && SOUND_MODES.includes(sound.mode), 'sound.mode', `sound.mode must be one of ${SOUND_MODES.join(', ')}`);
|
|
205
328
|
if (sound.mode === 'none') {
|
|
206
329
|
need(SILENT_REQUEST.test(t.request) || /무음|소리\s*없이|(?<![A-Za-z])muted?(?![A-Za-z])/iu.test(t.channel), 'sound.mode',
|
|
207
|
-
'sound none is allowed only when the user asked for silence or the channel plays muted by design');
|
|
330
|
+
'sound none is allowed only when the user asked for silence or the channel plays muted by design; the default is a score');
|
|
208
331
|
} else {
|
|
209
332
|
need(text(sound.plan), 'sound.plan', 'sound.plan is required');
|
|
333
|
+
if (sound.mode === 'score' && register === 'minimal') need(false, 'sound.mode', 'a minimal film keeps the generated bed; the score is for rich films');
|
|
334
|
+
if (sound.mode === 'generated' && register === 'rich') need(false, 'sound.mode', 'a rich film scores its own music (mode score); the generated bed is for minimal films');
|
|
335
|
+
if ('genre' in sound) need(SCORE_GENRES.includes(sound.genre), 'sound.genre', `sound.genre must be one of ${SCORE_GENRES.join(', ')}`);
|
|
210
336
|
if (sound.mode === 'generated') need(SOUND_PALETTES.includes(sound.palette), 'sound.palette', `sound.palette must be one of ${SOUND_PALETTES.join(', ')}`);
|
|
211
|
-
else {
|
|
337
|
+
else if (sound.mode !== 'score') {
|
|
212
338
|
need(text(sound.file), 'sound.file', `a ${sound.mode} track names its WAV in sound.file`);
|
|
213
339
|
if (sound.mode === 'authored') need(/^stage\//u.test(sound.file), 'sound.file', 'an authored track lives in the stage dir (stage/*.wav)');
|
|
214
340
|
if (outDir) need(existsSync(resolve(outDir, sound.file)), 'sound.file', `${sound.file} does not exist in the output dir`);
|
|
215
341
|
}
|
|
342
|
+
if ((sound.mode === 'generated' || sound.mode === 'score') && t.path === 'stage') {
|
|
343
|
+
need('events' in sound, 'sound.events', 'list the on-screen events that make a sound: [{ t, on, kind }], at least 2, half of them away from the cuts');
|
|
344
|
+
}
|
|
345
|
+
if ('events' in sound) {
|
|
346
|
+
const events = sound.events;
|
|
347
|
+
need(Array.isArray(events) && events.length >= 2, 'sound.events', 'at least 2 on-screen events that make a sound: [{ t, on, kind }]');
|
|
348
|
+
const cuts = beats.slice(1).map((beat) => beat.t0);
|
|
349
|
+
let offCut = 0;
|
|
350
|
+
events.forEach((entry, i) => {
|
|
351
|
+
need(typeof entry?.t === 'number' && entry.t > 0 && entry.t < d, `sound.events[${i}].t`, `the second the event happens on screen, inside the film (0-${d})`);
|
|
352
|
+
need(text(entry.on), `sound.events[${i}].on`, 'what happens on screen at that moment, as a viewer would see it');
|
|
353
|
+
need(EVENT_SOUNDS.includes(entry.kind), `sound.events[${i}].kind`, `must be one of ${EVENT_SOUNDS.join(', ')}`);
|
|
354
|
+
if (cuts.every((cut) => Math.abs(cut - entry.t) >= EVENT_OFF_CUT_SEC - 1e-9)) offCut += 1;
|
|
355
|
+
});
|
|
356
|
+
need(offCut * 2 >= events.length, 'sound.events', `${events.length - offCut} of ${events.length} events sit on a cut; at least half sync to something moving inside a shot`);
|
|
357
|
+
}
|
|
216
358
|
}
|
|
217
359
|
|
|
218
360
|
const inventedSubject = subject.source === 'invented';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2010-2024 three.js authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BackSide,
|
|
7
|
+
BoxGeometry,
|
|
8
|
+
Mesh,
|
|
9
|
+
MeshBasicMaterial,
|
|
10
|
+
MeshStandardMaterial,
|
|
11
|
+
PointLight,
|
|
12
|
+
Scene,
|
|
13
|
+
} from 'three';
|
|
14
|
+
|
|
15
|
+
class RoomEnvironment extends Scene {
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
|
|
19
|
+
super();
|
|
20
|
+
|
|
21
|
+
const geometry = new BoxGeometry();
|
|
22
|
+
geometry.deleteAttribute( 'uv' );
|
|
23
|
+
|
|
24
|
+
const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
|
|
25
|
+
const boxMaterial = new MeshStandardMaterial();
|
|
26
|
+
|
|
27
|
+
const mainLight = new PointLight( 0xffffff, 900, 28, 2 );
|
|
28
|
+
mainLight.position.set( 0.418, 16.199, 0.300 );
|
|
29
|
+
this.add( mainLight );
|
|
30
|
+
|
|
31
|
+
const room = new Mesh( geometry, roomMaterial );
|
|
32
|
+
room.position.set( - 0.757, 13.219, 0.717 );
|
|
33
|
+
room.scale.set( 31.713, 28.305, 28.591 );
|
|
34
|
+
this.add( room );
|
|
35
|
+
|
|
36
|
+
const box1 = new Mesh( geometry, boxMaterial );
|
|
37
|
+
box1.position.set( - 10.906, 2.009, 1.846 );
|
|
38
|
+
box1.rotation.set( 0, - 0.195, 0 );
|
|
39
|
+
box1.scale.set( 2.328, 7.905, 4.651 );
|
|
40
|
+
this.add( box1 );
|
|
41
|
+
|
|
42
|
+
const box2 = new Mesh( geometry, boxMaterial );
|
|
43
|
+
box2.position.set( - 5.607, - 0.754, - 0.758 );
|
|
44
|
+
box2.rotation.set( 0, 0.994, 0 );
|
|
45
|
+
box2.scale.set( 1.970, 1.534, 3.955 );
|
|
46
|
+
this.add( box2 );
|
|
47
|
+
|
|
48
|
+
const box3 = new Mesh( geometry, boxMaterial );
|
|
49
|
+
box3.position.set( 6.167, 0.857, 7.803 );
|
|
50
|
+
box3.rotation.set( 0, 0.561, 0 );
|
|
51
|
+
box3.scale.set( 3.927, 6.285, 3.687 );
|
|
52
|
+
this.add( box3 );
|
|
53
|
+
|
|
54
|
+
const box4 = new Mesh( geometry, boxMaterial );
|
|
55
|
+
box4.position.set( - 2.017, 0.018, 6.124 );
|
|
56
|
+
box4.rotation.set( 0, 0.333, 0 );
|
|
57
|
+
box4.scale.set( 2.002, 4.566, 2.064 );
|
|
58
|
+
this.add( box4 );
|
|
59
|
+
|
|
60
|
+
const box5 = new Mesh( geometry, boxMaterial );
|
|
61
|
+
box5.position.set( 2.291, - 0.756, - 2.621 );
|
|
62
|
+
box5.rotation.set( 0, - 0.286, 0 );
|
|
63
|
+
box5.scale.set( 1.546, 1.552, 1.496 );
|
|
64
|
+
this.add( box5 );
|
|
65
|
+
|
|
66
|
+
const box6 = new Mesh( geometry, boxMaterial );
|
|
67
|
+
box6.position.set( - 2.193, - 0.369, - 5.547 );
|
|
68
|
+
box6.rotation.set( 0, 0.516, 0 );
|
|
69
|
+
box6.scale.set( 3.875, 3.487, 2.986 );
|
|
70
|
+
this.add( box6 );
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
// -x right
|
|
74
|
+
const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
|
|
75
|
+
light1.position.set( - 16.116, 14.37, 8.208 );
|
|
76
|
+
light1.scale.set( 0.1, 2.428, 2.739 );
|
|
77
|
+
this.add( light1 );
|
|
78
|
+
|
|
79
|
+
// -x left
|
|
80
|
+
const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
|
|
81
|
+
light2.position.set( - 16.109, 18.021, - 8.207 );
|
|
82
|
+
light2.scale.set( 0.1, 2.425, 2.751 );
|
|
83
|
+
this.add( light2 );
|
|
84
|
+
|
|
85
|
+
// +x
|
|
86
|
+
const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );
|
|
87
|
+
light3.position.set( 14.904, 12.198, - 1.832 );
|
|
88
|
+
light3.scale.set( 0.15, 4.265, 6.331 );
|
|
89
|
+
this.add( light3 );
|
|
90
|
+
|
|
91
|
+
// +z
|
|
92
|
+
const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );
|
|
93
|
+
light4.position.set( - 0.462, 8.89, 14.520 );
|
|
94
|
+
light4.scale.set( 4.38, 5.441, 0.088 );
|
|
95
|
+
this.add( light4 );
|
|
96
|
+
|
|
97
|
+
// -z
|
|
98
|
+
const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );
|
|
99
|
+
light5.position.set( 3.235, 11.486, - 12.541 );
|
|
100
|
+
light5.scale.set( 2.5, 2.0, 0.1 );
|
|
101
|
+
this.add( light5 );
|
|
102
|
+
|
|
103
|
+
// +y
|
|
104
|
+
const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );
|
|
105
|
+
light6.position.set( 0.0, 20.0, 0.0 );
|
|
106
|
+
light6.scale.set( 1.0, 0.1, 1.0 );
|
|
107
|
+
this.add( light6 );
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dispose() {
|
|
112
|
+
|
|
113
|
+
const resources = new Set();
|
|
114
|
+
|
|
115
|
+
this.traverse( ( object ) => {
|
|
116
|
+
|
|
117
|
+
if ( object.isMesh ) {
|
|
118
|
+
|
|
119
|
+
resources.add( object.geometry );
|
|
120
|
+
resources.add( object.material );
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
for ( const resource of resources ) {
|
|
127
|
+
|
|
128
|
+
resource.dispose();
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createAreaLightMaterial( intensity ) {
|
|
137
|
+
|
|
138
|
+
const material = new MeshBasicMaterial();
|
|
139
|
+
material.color.setScalar( intensity );
|
|
140
|
+
return material;
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { RoomEnvironment };
|