kasaframe 0.1.1 → 0.1.3
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/NOTICE +10 -8
- package/README.md +16 -1
- package/app/server/engine.ts +133 -15
- package/app/server/main.ts +7 -3
- package/app/web/main.ts +218 -36
- package/app/web/style.css +37 -4
- package/bin/hooks.mjs +14 -1
- package/bin/kf.mjs +20 -2
- package/docs/AGENTS.md +64 -16
- package/music/README.md +31 -7
- package/package.json +2 -2
- package/packages/adapters/src/project.ts +1 -1
- package/packages/check/src/check.ts +10 -3
- package/packages/check/src/rules/assets.ts +4 -1
- package/packages/check/src/rules/frames.ts +2 -1
- package/packages/check/src/rules/layout.ts +30 -6
- package/packages/cli/package.json +2 -1
- package/packages/cli/src/blur.ts +15 -3
- package/packages/cli/src/commands/bench.ts +123 -0
- package/packages/cli/src/commands/blender.ts +72 -49
- package/packages/cli/src/commands/direct.ts +210 -0
- package/packages/cli/src/commands/doctor.ts +9 -1
- package/packages/cli/src/commands/look.ts +9 -2
- package/packages/cli/src/commands/lyrics.ts +47 -1
- package/packages/cli/src/commands/make.ts +729 -150
- package/packages/cli/src/commands/music.ts +78 -0
- package/packages/cli/src/commands/narrate.ts +238 -0
- package/packages/cli/src/commands/new.ts +14 -5
- package/packages/cli/src/commands/render.ts +94 -60
- package/packages/cli/src/commands/series.ts +110 -0
- package/packages/cli/src/commands/setup.ts +42 -3
- package/packages/cli/src/commands/theme.ts +89 -2
- package/packages/cli/src/deps.ts +6 -6
- package/packages/cli/src/kf.ts +14 -3
- package/packages/cli/src/mix.ts +131 -0
- package/packages/cli/src/pool.ts +34 -0
- package/packages/cli/src/segments.ts +402 -0
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/project.ts +11 -0
- package/packages/director/package.json +23 -0
- package/packages/director/src/assemble.ts +361 -0
- package/packages/director/src/brief.ts +122 -0
- package/packages/director/src/catalog.ts +49 -0
- package/packages/director/src/content.ts +343 -0
- package/packages/director/src/fallback.ts +58 -0
- package/packages/director/src/index.ts +13 -0
- package/packages/director/src/pattern.ts +90 -0
- package/packages/director/src/patterns/fill/cells.ts +115 -0
- package/packages/director/src/patterns/formula/stack.ts +93 -0
- package/packages/director/src/patterns/util.ts +58 -0
- package/packages/director/src/scene.ts +62 -0
- package/packages/director/src/select.ts +163 -0
- package/packages/director/src/transitions.ts +115 -0
- package/packages/gen/PROVIDERS.md +39 -4
- package/packages/gen/src/ask.ts +132 -0
- package/packages/gen/src/audio.ts +91 -0
- package/packages/gen/src/blender.ts +54 -0
- package/packages/gen/src/capabilities.ts +151 -0
- package/packages/gen/src/generate.ts +4 -2
- package/packages/gen/src/image/codex.ts +117 -0
- package/packages/gen/src/index.ts +10 -2
- package/packages/gen/src/keys.ts +26 -0
- package/packages/gen/src/music/elevenlabs.ts +41 -0
- package/packages/gen/src/music/index.ts +120 -0
- package/packages/gen/src/music/library.ts +73 -0
- package/packages/gen/src/music/lyria.ts +52 -0
- package/packages/gen/src/music/types.ts +28 -0
- package/packages/gen/src/registry.ts +6 -3
- package/packages/gen/src/tts/dashscope.ts +64 -0
- package/packages/gen/src/tts/elevenlabs.ts +84 -0
- package/packages/gen/src/tts/gemini.ts +73 -0
- package/packages/gen/src/tts/index.ts +241 -0
- package/packages/gen/src/tts/local.ts +42 -0
- package/packages/gen/src/tts/types.ts +44 -0
- package/packages/gen/src/types.ts +4 -0
- package/packages/gen/src/video/h3.ts +5 -0
- package/packages/gen/src/video/http.ts +1 -1
- package/packages/jizura/src/index.ts +2 -2
- package/packages/jizura/src/install.ts +198 -155
- package/packages/jizura/src/layer.ts +75 -32
- package/packages/jizura/src/pin.ts +4 -1
- package/packages/jizura/src/vendor.ts +140 -0
- package/packages/jizura/src/vite.ts +34 -7
- package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
- package/packages/jizura/vendor/NOTICE +43 -0
- package/packages/jizura/vendor/engine.json +47 -0
- package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
- package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
- package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
- package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
- package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
- package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
- package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
- package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
- package/packages/jizura/vendor/fonts/fonts.json +52 -0
- package/packages/jizura/vendor/jizura_engine.js +35024 -0
- package/packages/motion/README.md +11 -0
- package/packages/motion/package.json +1 -0
- package/packages/motion/src/captions.ts +32 -24
- package/packages/motion/src/moves/brushWipe.ts +14 -1
- package/packages/motion/src/moves/dress.ts +191 -0
- package/packages/motion/src/moves/echoStack.ts +3 -3
- package/packages/motion/src/moves/fit.ts +36 -0
- package/packages/motion/src/moves/ghostStack.ts +14 -5
- package/packages/motion/src/moves/imageSwarm.ts +8 -4
- package/packages/motion/src/moves/index.ts +23 -4
- package/packages/motion/src/moves/karaokeLine.ts +17 -3
- package/packages/motion/src/moves/menu.ts +138 -0
- package/packages/motion/src/moves/neonSign.ts +2 -1
- package/packages/motion/src/moves/slamWords.ts +14 -7
- package/packages/motion/src/moves/sparkTrail.ts +16 -0
- package/packages/motion/src/moves/splitFlap.ts +4 -2
- package/packages/motion/src/moves/stepWords.ts +19 -8
- package/packages/motion/src/moves/tickerRows.ts +13 -9
- package/packages/motion/src/moves/tokenPicker.ts +4 -3
- package/packages/motion/src/moves/wordWall.ts +23 -21
- package/packages/motion/src/moves/worldRoll.ts +2 -1
- package/packages/motion/src/transition3d/index.ts +10 -3
- package/packages/motion/src/transition3d/rig.ts +107 -9
- package/packages/motion/src/transition3d/shatter.ts +5 -2
- package/packages/motion/src/transition3d/space.ts +173 -0
- package/packages/motion/src/transition3d/whip.ts +19 -6
- package/packages/motion/src/transition3d/world.ts +54 -0
- package/packages/motion/src/transitions2d/catalog.ts +1 -1
- package/packages/patterns/package.json +15 -0
- package/packages/patterns/src/bars/rank.ts +119 -0
- package/packages/patterns/src/before-after/shrink.ts +127 -0
- package/packages/patterns/src/before-after/strike.ts +125 -0
- package/packages/patterns/src/code/terminal.ts +133 -0
- package/packages/patterns/src/compare/bars.ts +142 -0
- package/packages/patterns/src/compare/duel.ts +103 -0
- package/packages/patterns/src/end/lockup.ts +85 -0
- package/packages/patterns/src/end/slam.ts +91 -0
- package/packages/patterns/src/flow/graph.ts +250 -0
- package/packages/patterns/src/hook-number/counter.ts +101 -0
- package/packages/patterns/src/hook-number/roll.ts +179 -0
- package/packages/patterns/src/hook-number/slam.ts +91 -0
- package/packages/patterns/src/index.ts +38 -0
- package/packages/patterns/src/kit.ts +449 -0
- package/packages/patterns/src/limit/gauge.ts +117 -0
- package/packages/patterns/src/list/cards.ts +131 -0
- package/packages/patterns/src/list/cycle.ts +22 -0
- package/packages/patterns/src/media/panels.ts +100 -0
- package/packages/patterns/src/objects.ts +234 -0
- package/packages/patterns/src/quote/karaoke.ts +126 -0
- package/packages/patterns/src/quote/slam.ts +22 -0
- package/packages/patterns/src/screen/focus.ts +85 -0
- package/packages/patterns/src/steps/path.ts +137 -0
- package/packages/patterns/src/steps/stack.ts +111 -0
- package/packages/patterns/src/term-reveal/chips.ts +92 -0
- package/packages/patterns/src/term-reveal/expand.ts +127 -0
- package/packages/patterns/src/timeline/axis.ts +144 -0
- package/packages/patterns/src/title/mask.ts +85 -0
- package/packages/patterns/src/title/slam.ts +23 -0
- package/packages/patterns/src/toggle-sim/switch.ts +92 -0
- package/packages/patterns/src/trend/columns.ts +108 -0
- package/packages/patterns/src/trend/draw.ts +140 -0
- package/packages/patterns/src/types.ts +67 -0
- package/packages/patterns/src/words/moves.ts +42 -0
- package/packages/patterns/src/words/slam.ts +4 -0
- package/packages/plan/src/asks.ts +256 -0
- package/packages/plan/src/brief.ts +136 -7
- package/packages/plan/src/captions.ts +5 -2
- package/packages/plan/src/index.ts +6 -2
- package/packages/plan/src/llm.ts +64 -2
- package/packages/plan/src/look.ts +3 -1
- package/packages/plan/src/plan.ts +67 -13
- package/packages/plan/src/series.ts +76 -0
- package/packages/plan/src/types.ts +53 -2
- package/packages/themes/package.json +5 -2
- package/packages/themes/src/index.ts +9 -0
- package/packages/themes/src/slotkit.ts +79 -0
- package/packages/themes/src/slots.ts +46 -0
- package/packages/themes/src/stage3d.ts +20 -0
- package/series/og-shorts.json +14 -0
- package/skills/kasaframe/SKILL.md +4 -3
- package/templates/_shared/project/lib/stage.ts +70 -50
- package/templates/_shared/scenes/default.ts +10 -7
- package/templates/_shared/scenes/drop.ts +11 -8
- package/templates/_shared/scenes/end.ts +9 -6
- package/templates/_shared/scenes/hook.ts +8 -6
- package/templates/_shared/scenes/step.ts +13 -4
- package/templates/ad/template.json +20 -1
- package/templates/demo/template.json +11 -0
- package/templates/explainer/template.json +54 -3
- package/templates/launch/template.json +34 -1
- package/templates/mv/scenes/default.ts +11 -8
- package/templates/mv/template.json +4 -2
- package/templates/promo/template.json +42 -1
- package/templates/shorts/scenes/hook.ts +8 -5
- package/templates/shorts/template.json +72 -3
- package/templates/square/template.json +21 -1
- package/themes/debug-hud/slots.ts +99 -0
- package/themes/og-measure/slots.ts +88 -0
- package/themes/riso-zine/slots.ts +81 -0
|
@@ -1,27 +1,20 @@
|
|
|
1
|
-
// JIZURA
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
1
|
+
// JIZURA 가사 층의 엔진·글꼴 자리(노드 쪽). 엔진은 패키지에 실려 있다(vendor/ — 다시 만들기는 vendor.ts).
|
|
2
|
+
// 한국어 가사에 자주 쓰는 얼굴은 vendor/fonts 에 실려 있고(한글·영숫자·문장부호 서브셋), 나머지 얼굴과 서브셋 밖 글자(한자·가나)는
|
|
3
|
+
// 처음 쓸 때 google/fonts 고정 커밋에서 원본을 받아 ~/.cache/kasaframe/jizura/fonts 에 둔다 — 설치본(node_modules 안)에는 못 쓸 수 있다.
|
|
4
|
+
// 못 받으면 다른 글꼴로 대신 그리지 않고 fontsMissing() 에 남긴다. 어떻게 할지는 사람이 고른다(kf 가 끝에 묻는다).
|
|
5
|
+
// 렌더는 오프라인이어도 돌아야 해서 CDN 이 아니라 여기서 낸다(vite.ts 가 /@kf-jizura/ 로 낸다).
|
|
6
|
+
// node packages/jizura/src/install.ts [--lang ko,ja] kf setup jizura — 그 언어의 나머지 얼굴을 미리 받는다
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
|
-
import {
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import {
|
|
12
|
+
import { GOOGLE_FONTS_COMMIT, JIZURA_COMMIT } from "./pin.ts";
|
|
12
13
|
|
|
14
|
+
export const vendorDir = fileURLToPath(new URL("../vendor", import.meta.url));
|
|
15
|
+
export const builtinFontDir = join(vendorDir, "fonts");
|
|
13
16
|
export const jizuraHome = (): string => process.env.KF_JIZURA_HOME ?? join(homedir(), ".cache", "kasaframe", "jizura");
|
|
14
|
-
export const engineDir = (): string => join(jizuraHome(), "engine");
|
|
15
17
|
export const fontDir = (): string => join(jizuraHome(), "fonts");
|
|
16
|
-
const repoDir = (): string => join(jizuraHome(), "repo");
|
|
17
|
-
|
|
18
|
-
export interface FontFile {
|
|
19
|
-
family: string;
|
|
20
|
-
/** 가변 글꼴은 "100 900" */
|
|
21
|
-
weight: string;
|
|
22
|
-
file: string;
|
|
23
|
-
lang: string;
|
|
24
|
-
}
|
|
25
18
|
|
|
26
19
|
export interface EngineInfo {
|
|
27
20
|
commit: string;
|
|
@@ -31,181 +24,231 @@ export interface EngineInfo {
|
|
|
31
24
|
files: string[];
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
export interface Upstream {
|
|
28
|
+
/** google/fonts ofl/<dir>/ 의 파일 이름 */
|
|
29
|
+
file: string;
|
|
30
|
+
/** 가변 글꼴은 "100 900" */
|
|
31
|
+
weight: string;
|
|
32
|
+
bytes: number;
|
|
33
|
+
sha256: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Family {
|
|
37
|
+
family: string;
|
|
38
|
+
dir: string;
|
|
39
|
+
lang: "common" | "ko" | "ja";
|
|
40
|
+
files: Upstream[];
|
|
41
|
+
/** 사람이 「내장 글꼴로」를 고르면 이 얼굴 대신 그릴 내장 얼굴 */
|
|
42
|
+
stand?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const up = (file: string, weight: string, bytes: number, sha256: string): Upstream => ({ file, weight, bytes, sha256 });
|
|
46
|
+
|
|
47
|
+
// JIZURA 의 J.FONTS·J.LANG_FACES 가 그 언어에서 그리는 얼굴. 한국어 매핑에 없는 키(dot·mono)는 일본어 얼굴을 그대로 쓰고 한글은 그 뒤 Noto Sans KR 로 떨어진다
|
|
48
|
+
export const FAMILIES: Family[] = [
|
|
49
|
+
{ family: "IBM Plex Mono", dir: "ibmplexmono", lang: "common", files: [up("IBMPlexMono-Medium.ttf", "500", 136704, "a9b4c49bb299e05b5f6c481e7fb5e78943d2793249a0c8874ab574a2d1ea6755"), up("IBMPlexMono-SemiBold.ttf", "600", 140216, "d3c38e55c78f5b0f28009fddba4834ec503278936a5986032424c9bd2d23aa46")] },
|
|
50
|
+
{ family: "DotGothic16", dir: "dotgothic16", lang: "common", files: [up("DotGothic16-Regular.ttf", "400", 2069236, "3ad9af88726d42b40f7f365f0dcac785af73cf20ea6f1d5b44e57cc21150b8f1")] },
|
|
51
|
+
{ family: "Noto Sans KR", dir: "notosanskr", lang: "ko", files: [up("NotoSansKR[wght].ttf", "100 900", 10414588, "194018e6b2b293a7964f037b25c0249ce1418bc9ab3c971060a03aa57861e252")] },
|
|
52
|
+
{ family: "Noto Serif KR", dir: "notoserifkr", lang: "ko", files: [up("NotoSerifKR[wght].ttf", "200 900", 23795420, "11f8d5de6f1b79195efba3828aaa2ec95c1178f5ae976fb23c8d53250a9938f3")] },
|
|
53
|
+
{ family: "Black Han Sans", dir: "blackhansans", lang: "ko", files: [up("BlackHanSans-Regular.ttf", "400", 998424, "31960809284026681774a8e52dc19ebcad26cf69b0ad9d560f288296fbb52739")] },
|
|
54
|
+
{ family: "Gowun Batang", dir: "gowunbatang", lang: "ko", files: [up("GowunBatang-Bold.ttf", "700", 8178712, "dbfcaa646e5831e7478524924f02906f550285a5050699b4e38c9950b3ec4b94")] },
|
|
55
|
+
{ family: "Jua", dir: "jua", lang: "ko", stand: "Black Han Sans", files: [up("Jua-Regular.ttf", "400", 2119352, "769677aef240bfc3b9965f2b50748075bff885e6c6992fc591a3fb268279f898")] },
|
|
56
|
+
{ family: "Do Hyeon", dir: "dohyeon", lang: "ko", stand: "Black Han Sans", files: [up("DoHyeon-Regular.ttf", "400", 879764, "35644be7f28e0a68a447b1f7af351dcde5674b870f24f7b5f43e26d00b4ab653")] },
|
|
57
|
+
{ family: "Gowun Dodum", dir: "gowundodum", lang: "ko", stand: "Noto Sans KR", files: [up("GowunDodum-Regular.ttf", "400", 7229088, "a6e457933227483a11758fd0947bc74422a106d46f0bf057fdaa5af94a30067d")] },
|
|
58
|
+
{ family: "Nanum Brush Script", dir: "nanumbrushscript", lang: "ko", stand: "Gowun Batang", files: [up("NanumBrushScript-Regular.ttf", "400", 3564804, "27ceaf578c96f594cdf07fe0181b251790acbb746a164e45c1f6473f89911a31")] },
|
|
59
|
+
{ family: "IBM Plex Sans KR", dir: "ibmplexsanskr", lang: "ko", stand: "Noto Sans KR", files: [up("IBMPlexSansKR-Medium.ttf", "500", 2866736, "4a7130f56ce50bf10f9d383f624e50bad2c80bebc63e51f4269ece2c7d919166")] },
|
|
60
|
+
{ family: "Noto Sans JP", dir: "notosansjp", lang: "ja", files: [up("NotoSansJP[wght].ttf", "100 900", 9589900, "c2f3b4d463500a2ddcd3849cded1fceeb9fd6d1c32e6cbecd568453ba50fc68f")] },
|
|
61
|
+
{ family: "Noto Serif JP", dir: "notoserifjp", lang: "ja", files: [up("NotoSerifJP[wght].ttf", "200 900", 13574352, "2fd527ba12b6a44ec30d796d633360da0aeba6c5d4af1304ce12bb4dc15a7dfc")] },
|
|
62
|
+
{ family: "Dela Gothic One", dir: "delagothicone", lang: "ja", files: [up("DelaGothicOne-Regular.ttf", "400", 2508848, "4ff87a0965f1b0505e5a2c58424bc6ad3cff27e56a82f21c2fc9d6b0e3857ee2")] },
|
|
63
|
+
{ family: "Zen Kaku Gothic New", dir: "zenkakugothicnew", lang: "ja", files: [up("ZenKakuGothicNew-Black.ttf", "900", 2300600, "795819a979184981842994d8f4eb9e14ce443d687bd5e731d6ca67ded8f92261")] },
|
|
64
|
+
{ family: "Zen Old Mincho", dir: "zenoldmincho", lang: "ja", files: [up("ZenOldMincho-Black.ttf", "900", 5416348, "84a80d8bca79d7d9478935045b216ed003ad40fdea5fd9116d524eb26e872cdc")] },
|
|
65
|
+
{ family: "Kaisei Tokumin", dir: "kaiseitokumin", lang: "ja", files: [up("KaiseiTokumin-ExtraBold.ttf", "800", 4564000, "bf44bb3e23cc703bfb19111833f78e651998d0a2b1863eff9e88cecb38a8bc53")] },
|
|
66
|
+
{ family: "M PLUS Rounded 1c", dir: "mplusrounded1c", lang: "ja", files: [up("MPLUSRounded1c-ExtraBold.ttf", "800", 3628512, "8e7c15901dca87f1451b356dda594f7d092ba252a5dcc47da74523a242493c36")] },
|
|
67
|
+
{ family: "Mochiy Pop One", dir: "mochiypopone", lang: "ja", files: [up("MochiyPopOne-Regular.ttf", "400", 5163948, "9e009430e1316c271a5f34759c6b65fc343c4e806f193042528887e7235a92c6")] },
|
|
68
|
+
{ family: "Yuji Syuku", dir: "yujisyuku", lang: "ja", files: [up("YujiSyuku-Regular.ttf", "400", 8430348, "82728ebafc8c97391e2dab633414a806f344b8e4e2227d307179f07b548fca61")] },
|
|
69
|
+
{ family: "Reggae One", dir: "reggaeone", lang: "ja", files: [up("ReggaeOne-Regular.ttf", "400", 2153256, "aebe62598732d76036f30ec11bb0ec5f68938e373a06d1b4ceb6b9cf1abf3db2")] },
|
|
70
|
+
{ family: "Rampart One", dir: "rampartone", lang: "ja", files: [up("RampartOne-Regular.ttf", "400", 3722352, "a4c60ee835175fa147eb483fac0dd8ff9007c37884ef77af86b0a868fa554d49")] },
|
|
71
|
+
{ family: "Potta One", dir: "pottaone", lang: "ja", files: [up("PottaOne-Regular.ttf", "400", 4918516, "bde843868b56e429e1c863da2efa76df533162b226d990efcb96d533b0ca2a93")] },
|
|
72
|
+
{ family: "Kiwi Maru", dir: "kiwimaru", lang: "ja", files: [up("KiwiMaru-Medium.ttf", "500", 5123612, "b2659f300a7d48c3f29eb273ffc5e1b26cc416ac8c37ff6bb2f3e43c2f4d235a")] },
|
|
73
|
+
{ family: "Klee One", dir: "kleeone", lang: "ja", files: [up("KleeOne-SemiBold.ttf", "600", 8905128, "b031ec426c23ca1143ef1f7d58bee7a79efe119ed654152f121c922202b303fd")] },
|
|
74
|
+
{ family: "Shippori Mincho B1", dir: "shipporiminchob1", lang: "ja", files: [up("ShipporiMinchoB1-ExtraBold.ttf", "800", 15088060, "bee99a242f32128e8d6a4acff2b3f1742cd42ea90748758e6e10456871887e76")] },
|
|
75
|
+
{ family: "IBM Plex Sans JP", dir: "ibmplexsansjp", lang: "ja", files: [up("IBMPlexSansJP-Medium.ttf", "500", 2389060, "1d09f5e25b19e54b2c55da70de58f9962512c3e07ed7c3e1c9bad77ace5c22e6")] },
|
|
39
76
|
];
|
|
40
|
-
const
|
|
41
|
-
ko: [
|
|
42
|
-
{ family: "Noto Sans KR", dir: "notosanskr" },
|
|
43
|
-
{ family: "Noto Serif KR", dir: "notoserifkr" },
|
|
44
|
-
{ family: "Black Han Sans", dir: "blackhansans" },
|
|
45
|
-
{ family: "Jua", dir: "jua" },
|
|
46
|
-
{ family: "Do Hyeon", dir: "dohyeon" },
|
|
47
|
-
{ family: "Gowun Dodum", dir: "gowundodum" },
|
|
48
|
-
{ family: "Gowun Batang", dir: "gowunbatang", weights: [700] },
|
|
49
|
-
{ family: "Nanum Brush Script", dir: "nanumbrushscript" },
|
|
50
|
-
{ family: "IBM Plex Sans KR", dir: "ibmplexsanskr", weights: [500] },
|
|
51
|
-
],
|
|
52
|
-
ja: [
|
|
53
|
-
{ family: "Noto Sans JP", dir: "notosansjp" },
|
|
54
|
-
{ family: "Noto Serif JP", dir: "notoserifjp" },
|
|
55
|
-
{ family: "Dela Gothic One", dir: "delagothicone" },
|
|
56
|
-
{ family: "Zen Kaku Gothic New", dir: "zenkakugothicnew", weights: [900] },
|
|
57
|
-
{ family: "Zen Old Mincho", dir: "zenoldmincho", weights: [900] },
|
|
58
|
-
{ family: "Kaisei Tokumin", dir: "kaiseitokumin", weights: [800] },
|
|
59
|
-
{ family: "M PLUS Rounded 1c", dir: "mplusrounded1c", weights: [800] },
|
|
60
|
-
{ family: "Mochiy Pop One", dir: "mochiypopone" },
|
|
61
|
-
{ family: "Yuji Syuku", dir: "yujisyuku" },
|
|
62
|
-
{ family: "Reggae One", dir: "reggaeone" },
|
|
63
|
-
{ family: "Rampart One", dir: "rampartone" },
|
|
64
|
-
{ family: "Potta One", dir: "pottaone" },
|
|
65
|
-
{ family: "Kiwi Maru", dir: "kiwimaru", weights: [500] },
|
|
66
|
-
{ family: "Klee One", dir: "kleeone", weights: [600] },
|
|
67
|
-
{ family: "Shippori Mincho B1", dir: "shipporiminchob1", weights: [800] },
|
|
68
|
-
{ family: "IBM Plex Sans JP", dir: "ibmplexsansjp", weights: [500] },
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
export const FONT_LANGS = Object.keys(FAMILIES);
|
|
77
|
+
export const FONT_LANGS = ["ko", "ja"];
|
|
72
78
|
|
|
73
|
-
const
|
|
79
|
+
export const localName = (file: string): string => file.replace(/\[.*\]/, "-VF");
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
export interface BuiltinFont {
|
|
82
|
+
family: string;
|
|
83
|
+
weight: string;
|
|
84
|
+
file: string;
|
|
85
|
+
/** 서브셋을 뜬 원본(google/fonts 파일 이름) */
|
|
86
|
+
from: string;
|
|
87
|
+
/** 서브셋이면 남긴 글자 범위 — 원본을 그대로 실은 것은 없다 */
|
|
88
|
+
unicodes?: string;
|
|
89
|
+
}
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
export function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return given;
|
|
84
|
-
}
|
|
85
|
-
const dir = repoDir();
|
|
86
|
-
if (existsSync(join(dir, ".git")) && git(["rev-parse", "HEAD"], dir) === JIZURA_COMMIT) return dir;
|
|
87
|
-
mkdirSync(dir, { recursive: true });
|
|
88
|
-
if (!existsSync(join(dir, ".git"))) {
|
|
89
|
-
git(["init", "-q"], dir);
|
|
90
|
-
git(["remote", "add", "origin", JIZURA_REPO], dir);
|
|
91
|
-
}
|
|
92
|
-
log(` ${JIZURA_REPO} @ ${JIZURA_COMMIT.slice(0, 7)} 받는 중`);
|
|
93
|
-
git(["fetch", "-q", "--depth", "1", "origin", JIZURA_COMMIT], dir);
|
|
94
|
-
git(["checkout", "-q", "--detach", "FETCH_HEAD"], dir);
|
|
95
|
-
return dir;
|
|
91
|
+
let builtinCache: BuiltinFont[] | null = null;
|
|
92
|
+
export function builtinFonts(): BuiltinFont[] {
|
|
93
|
+
if (builtinCache) return builtinCache;
|
|
94
|
+
const p = join(builtinFontDir, "fonts.json");
|
|
95
|
+
builtinCache = existsSync(p) ? (JSON.parse(readFileSync(p, "utf8")).fonts as BuiltinFont[]) : [];
|
|
96
|
+
return builtinCache;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
/**
|
|
99
|
-
export function
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const info: EngineInfo = {
|
|
109
|
-
commit: git(["rev-parse", "HEAD"], src),
|
|
110
|
-
version,
|
|
111
|
-
sha256: createHash("sha256").update(js).digest("hex"),
|
|
112
|
-
bytes: Buffer.byteLength(js),
|
|
113
|
-
files,
|
|
114
|
-
};
|
|
115
|
-
writeFileSync(join(out, "engine.json"), JSON.stringify(info, null, 1));
|
|
116
|
-
return info;
|
|
99
|
+
/** text 에서 그 범위(U+AC00-D7A3,…) 밖 글자 — 서브셋에 없으니 원본이 있어야 그 얼굴로 그린다 */
|
|
100
|
+
export function outside(text: string, unicodes: string): string[] {
|
|
101
|
+
const ranges = unicodes.split(",").map((r) => r.trim().replace(/^U\+/i, "").split("-").map((h) => parseInt(h, 16)));
|
|
102
|
+
const out = new Set<string>();
|
|
103
|
+
for (const ch of text) {
|
|
104
|
+
const c = ch.codePointAt(0)!;
|
|
105
|
+
if (c < 0x20) continue;
|
|
106
|
+
if (!ranges.some(([a, b = a]) => c >= a && c <= b)) out.add(ch);
|
|
107
|
+
}
|
|
108
|
+
return [...out];
|
|
117
109
|
}
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const r = await fetch(url, { headers });
|
|
123
|
-
if (!r.ok) throw new Error(`${url} → ${r.status}${r.status === 403 ? " (GitHub API 한도 — GITHUB_TOKEN 을 주면 늘어난다)" : ""}`);
|
|
124
|
-
return r.json();
|
|
111
|
+
export function engineInfo(): EngineInfo | null {
|
|
112
|
+
const p = join(vendorDir, "engine.json");
|
|
113
|
+
return existsSync(p) ? (JSON.parse(readFileSync(p, "utf8")) as EngineInfo) : null;
|
|
125
114
|
}
|
|
126
115
|
|
|
127
|
-
|
|
128
|
-
|
|
116
|
+
// 막힌 망에서는 google/fonts 거울을 줄 수 있다 — ofl/<dir>/<파일> 꼴만 맞으면 된다. 받은 파일은 크기·sha256 으로 고정본인지 본다
|
|
117
|
+
const fontBase = (): string => process.env.KF_JIZURA_FONTS_URL ?? `https://raw.githubusercontent.com/google/fonts/${GOOGLE_FONTS_COMMIT}`;
|
|
118
|
+
const fontUrl = (dir: string, file: string): string => `${fontBase()}/ofl/${dir}/${encodeURIComponent(file)}`;
|
|
119
|
+
|
|
120
|
+
const why = (err: unknown): string => (err instanceof Error ? (err.cause instanceof Error ? `${err.message}(${err.cause.message})` : err.message) : String(err));
|
|
121
|
+
|
|
122
|
+
async function download(url: string, to: string, want?: Upstream): Promise<void> {
|
|
123
|
+
let last = "";
|
|
124
|
+
for (let i = 0; i < 3; i++) {
|
|
129
125
|
try {
|
|
130
|
-
const r = await fetch(url);
|
|
131
|
-
if (!r.ok) throw new Error(
|
|
132
|
-
|
|
126
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(120_000) });
|
|
127
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
128
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
129
|
+
if (want && (buf.length !== want.bytes || createHash("sha256").update(buf).digest("hex") !== want.sha256)) {
|
|
130
|
+
throw new Error(`고정본(${GOOGLE_FONTS_COMMIT.slice(0, 7)})과 다른 파일이 왔다`);
|
|
131
|
+
}
|
|
132
|
+
writeFileSync(`${to}.part`, buf);
|
|
133
133
|
renameSync(`${to}.part`, to);
|
|
134
134
|
return;
|
|
135
135
|
} catch (err) {
|
|
136
|
-
|
|
136
|
+
last = why(err);
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
throw new Error(`${url} — ${last}`);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
for (const p of picks) {
|
|
161
|
-
const file = p.src.name.replace(/\[.*\]/, "-VF");
|
|
162
|
-
const to = join(dir, file);
|
|
163
|
-
if (!existsSync(to) || statSync(to).size !== p.src.size) {
|
|
164
|
-
log(` ${fam.family} ${p.weight} — ${(p.src.size / 1048576).toFixed(1)}MB`);
|
|
165
|
-
await download(p.src.download_url, to);
|
|
142
|
+
export const cached = (f: Upstream): boolean => {
|
|
143
|
+
const p = join(fontDir(), localName(f.file));
|
|
144
|
+
return existsSync(p) && statSync(p).size === f.bytes;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const jobs = new Map<string, Promise<void>>();
|
|
148
|
+
|
|
149
|
+
/** 그 얼굴의 원본을 받아 둔다(같은 얼굴은 한 번만, OFL 원문도 옆에) */
|
|
150
|
+
export function fetchFamily(fam: Family, log: (s: string) => void = console.log): Promise<void> {
|
|
151
|
+
let job = jobs.get(fam.family);
|
|
152
|
+
if (!job) {
|
|
153
|
+
job = (async () => {
|
|
154
|
+
mkdirSync(fontDir(), { recursive: true });
|
|
155
|
+
const ofl = join(fontDir(), `OFL-${fam.dir}.txt`);
|
|
156
|
+
if (!existsSync(ofl)) await download(fontUrl(fam.dir, "OFL.txt"), ofl);
|
|
157
|
+
for (const f of fam.files) {
|
|
158
|
+
if (cached(f)) continue;
|
|
159
|
+
log(` ${fam.family} ${f.weight} — ${(f.bytes / 1048576).toFixed(1)}MB 받는 중`);
|
|
160
|
+
await download(fontUrl(fam.dir, f.file), join(fontDir(), localName(f.file)), f);
|
|
166
161
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const oflTo = join(dir, `OFL-${fam.dir}.txt`);
|
|
171
|
-
if (ofl && !existsSync(oflTo)) await download(ofl.download_url, oflTo);
|
|
162
|
+
})();
|
|
163
|
+
jobs.set(fam.family, job);
|
|
164
|
+
job.catch(() => jobs.delete(fam.family));
|
|
172
165
|
}
|
|
173
|
-
|
|
174
|
-
return out;
|
|
166
|
+
return job;
|
|
175
167
|
}
|
|
176
168
|
|
|
177
|
-
export
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
export interface FaceSrc {
|
|
170
|
+
family: string;
|
|
171
|
+
weight: string;
|
|
172
|
+
/** /@kf-jizura/ 아래 경로 */
|
|
173
|
+
url: string;
|
|
180
174
|
}
|
|
181
175
|
|
|
182
|
-
export
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
export interface Missing {
|
|
177
|
+
family: string;
|
|
178
|
+
reason: string;
|
|
179
|
+
/** 「내장 글꼴로」를 고르면 대신 그릴 내장 얼굴 — 없으면 그 길로도 못 그린다(서브셋 밖 글자) */
|
|
180
|
+
stand?: string;
|
|
181
|
+
/** 그 얼굴을 쓰려던 프로젝트 폴더 — 답을 넣을 곳 */
|
|
182
|
+
project?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const missing = new Map<string, Missing>();
|
|
186
|
+
/** 이 프로세스에서 못 받아 그리지 못한 얼굴 — kf 가 끝에 이걸 보고 묻는다 */
|
|
187
|
+
export const fontsMissing = (): Missing[] => [...missing.values()];
|
|
188
|
+
|
|
189
|
+
/** 페이지가 그릴 얼굴의 파일을 고른다 — 받아 둔 원본 → 내장 글꼴(서브셋이면 글자가 다 들 때만) → 처음 쓰는 얼굴이면 받기.
|
|
190
|
+
* 못 받으면 사람이 「내장 글꼴로」를 골랐을 때(builtin)만 stand 로 그리고, 아니면 missing 으로 돌려준다 */
|
|
191
|
+
export async function provideFonts(families: string[], text: string, o: { builtin?: boolean; project?: string; log?: (s: string) => void } = {}): Promise<{ faces: FaceSrc[]; missing: Missing[] }> {
|
|
192
|
+
const packed = builtinFonts();
|
|
193
|
+
const fits = (b: BuiltinFont) => !b.unicodes || !outside(text, b.unicodes).length;
|
|
194
|
+
const inBox = (name: string) => {
|
|
195
|
+
const list = packed.filter((b) => b.family === name);
|
|
196
|
+
return list.length && list.every(fits) ? list : null;
|
|
197
|
+
};
|
|
198
|
+
const faces: FaceSrc[] = [];
|
|
199
|
+
const miss: Missing[] = [];
|
|
200
|
+
await Promise.all(
|
|
201
|
+
[...new Set(families)].map(async (name) => {
|
|
202
|
+
const fam = FAMILIES.find((f) => f.family === name);
|
|
203
|
+
if (!fam) return;
|
|
204
|
+
const own = () => faces.push(...fam.files.map((f) => ({ family: name, weight: f.weight, url: `fonts/${localName(f.file)}` })));
|
|
205
|
+
if (fam.files.every(cached)) return void own();
|
|
206
|
+
const box = inBox(name);
|
|
207
|
+
if (box) return void faces.push(...box.map((b) => ({ family: name, weight: b.weight, url: `fonts/${b.file}` })));
|
|
208
|
+
try {
|
|
209
|
+
await fetchFamily(fam, o.log);
|
|
210
|
+
own();
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const standName = fam.stand ?? name;
|
|
213
|
+
const stand = inBox(standName);
|
|
214
|
+
if (o.builtin && stand) {
|
|
215
|
+
// 그 얼굴 이름으로 내장 파일을 건다 — 플랜·컷은 그대로 두고 글자 모양만 바뀐다
|
|
216
|
+
for (const f of fam.files) faces.push({ family: name, weight: f.weight, url: `fonts/${stand[0].file}` });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const m: Missing = { family: name, reason: why(err), ...(stand ? { stand: standName } : {}), ...(o.project ? { project: o.project } : {}) };
|
|
220
|
+
missing.set(name, m);
|
|
221
|
+
miss.push(m);
|
|
222
|
+
}
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
return { faces, missing: miss };
|
|
185
226
|
}
|
|
186
227
|
|
|
187
228
|
/** kf doctor 한 줄 — cli/deps.ts 의 Check 꼴 */
|
|
188
229
|
export function jizuraCheck() {
|
|
189
230
|
const info = engineInfo();
|
|
190
|
-
const
|
|
191
|
-
const
|
|
231
|
+
const ok = !!info && info.commit === JIZURA_COMMIT && builtinFonts().length > 0;
|
|
232
|
+
const packed = [...new Set(builtinFonts().map((f) => f.family))];
|
|
233
|
+
const rest = FAMILIES.filter((f) => !packed.includes(f.family) && f.lang !== "ja");
|
|
234
|
+
const have = rest.filter((f) => f.files.every(cached));
|
|
192
235
|
return {
|
|
193
236
|
name: "JIZURA 가사 층",
|
|
194
237
|
ok,
|
|
195
238
|
required: false,
|
|
196
239
|
detail: info
|
|
197
|
-
? `v${info.version} @${info.commit.slice(0, 7)}${info.commit === JIZURA_COMMIT ? "" : ` (고정 ${JIZURA_COMMIT.slice(0, 7)} 이 아니다)`} · 글꼴 ${
|
|
198
|
-
: "
|
|
199
|
-
fix: ok ? undefined : "
|
|
240
|
+
? `v${info.version} @${info.commit.slice(0, 7)}${info.commit === JIZURA_COMMIT ? "" : ` (고정 ${JIZURA_COMMIT.slice(0, 7)} 이 아니다)`} 내장 · 글꼴 내장 ${packed.length}·받아 둔 한국어 ${have.length}/${rest.length} — 나머지는 처음 쓸 때 받는다`
|
|
241
|
+
: "엔진이 패키지에 없다 — 깨진 설치",
|
|
242
|
+
fix: ok ? undefined : "레포면 node packages/jizura/src/vendor.ts, 설치본이면 kasaframe 를 다시 깐다",
|
|
200
243
|
};
|
|
201
244
|
}
|
|
202
245
|
|
|
246
|
+
/** kf setup jizura — 그 언어에서 내장에 없는 얼굴을 미리 받는다(오프라인으로 굽기 전에) */
|
|
203
247
|
export async function install(langs: string[], log = console.log): Promise<void> {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
log(` 글꼴 ${fonts.length}개(${langs.join("·")}) → ${fontDir()}`);
|
|
248
|
+
const packed = new Set(builtinFonts().map((f) => f.family));
|
|
249
|
+
const want = FAMILIES.filter((f) => (f.lang === "common" || langs.includes(f.lang)) && !packed.has(f.family));
|
|
250
|
+
for (const fam of want) await fetchFamily(fam, log);
|
|
251
|
+
log(` 글꼴 ${want.length}개(${langs.join("·")}) → ${fontDir()} · 내장 ${packed.size}개는 패키지에(${[...packed].join("·")})`);
|
|
209
252
|
}
|
|
210
253
|
|
|
211
254
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
@@ -219,7 +262,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
219
262
|
install(langs).then(
|
|
220
263
|
() => process.exit(0),
|
|
221
264
|
(err) => {
|
|
222
|
-
console.error(`jizura
|
|
265
|
+
console.error(`jizura 글꼴 받기 실패: ${why(err)}`);
|
|
223
266
|
process.exit(1);
|
|
224
267
|
},
|
|
225
268
|
);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// JIZURA 字面(MIT, (c) 2026 hakoniwa) 엔진을 결정적인 가사 층으로 — 장면 위 캔버스 한 장에 시각 t 의 가사를 그린다.
|
|
2
2
|
// 플랜 하나 = JIZURA 프로젝트 하나(곡 전체 가사·박). 같은 설정을 쓰는 장면끼리는 한 플랜을 나눠 쓰고, 장면마다 다른 플랜을 고른다.
|
|
3
|
-
//
|
|
3
|
+
// 엔진은 패키지에 실려 있고(vendor/) 서버가 /@kf-jizura/ 로 낸다(vite.ts). 글꼴도 서버가 고른다 — 내장 서브셋·받아 둔 원본·처음 쓰면 받기.
|
|
4
4
|
// 빌려 온 것: 플랜 만들기·색 구성 고르기(schemes)는 vocaloid-style-mv-pipeline(MIT, NikusonP) 의 jizura_adapter.js 를 옮겼다.
|
|
5
5
|
import { onSeek } from "@kasaframe/adapters";
|
|
6
6
|
import type { KfMeta, SceneCtx } from "@kasaframe/core";
|
|
7
7
|
import { partPool, type JizuraRegistry, type PoolOptions } from "./deny.ts";
|
|
8
|
+
import type { FaceSrc, Missing } from "./install.ts";
|
|
8
9
|
import { parseLyrics, toLrc, type LyricsJson } from "./lyrics.ts";
|
|
9
10
|
|
|
10
11
|
const URL_BASE = "/@kf-jizura/";
|
|
@@ -89,46 +90,78 @@ async function getJson<T>(url: string | URL): Promise<T | null> {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
/** jizura — 가사 층 · builtin — 가사 층인데 못 받은 얼굴은 내장 글꼴로 · words — 가사 층 없이 낱말(틀 장면이 DOM 으로). 사람이 고른다 */
|
|
94
|
+
export type LyricsMode = "jizura" | "builtin" | "words";
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
let modeJob: Promise<LyricsMode> | null = null;
|
|
97
|
+
/** kf.json "lyrics"(kf lyrics use 가 적는다)를 페이지 주소 ?lyrics= 가 덮는다 */
|
|
98
|
+
export function lyricsMode(): Promise<LyricsMode> {
|
|
99
|
+
modeJob ??= (async () => {
|
|
100
|
+
const q = new URLSearchParams(location.search).get("lyrics");
|
|
101
|
+
const v = q ?? (await getJson<{ lyrics?: unknown }>(new URL("kf.json", document.baseURI)))?.lyrics;
|
|
102
|
+
return v === "builtin" || v === "words" ? v : "jizura";
|
|
103
|
+
})();
|
|
104
|
+
return modeJob;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const registered = new Set<string>();
|
|
108
|
+
|
|
109
|
+
// 그릴 얼굴의 파일을 서버가 골라 준다(vite.ts) — 받아 둔 원본 · 내장 서브셋 · 처음 쓰는 얼굴이면 받기.
|
|
110
|
+
// 못 받은 얼굴이 있으면 던진다: CSS 대체 목록의 다른 글꼴로 조용히 그리지 않는다
|
|
111
|
+
async function provide(families: string[], text: string): Promise<FaceSrc[]> {
|
|
112
|
+
const builtin = (await lyricsMode()) === "builtin";
|
|
113
|
+
const r = await fetch(`${URL_BASE}fonts`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ families, text, builtin }) });
|
|
114
|
+
if (!r.ok) throw new Error(`글꼴 서버 ${r.status}: ${await r.text()}`);
|
|
115
|
+
const { faces, missing } = (await r.json()) as { faces: FaceSrc[]; missing: Missing[] };
|
|
116
|
+
if (missing.length) throw new Error(`JIZURA 글꼴 ${missing.map((m) => m.family).join("·")} 을 못 받았다 — 대신 그리지 않는다(kf 가 끝에 고를 길을 찍는다)`);
|
|
117
|
+
return faces;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function addFace(family: string, f: FaceSrc): void {
|
|
121
|
+
const key = `${family}|${f.weight}|${f.url}`;
|
|
122
|
+
if (registered.has(key)) return;
|
|
123
|
+
registered.add(key);
|
|
124
|
+
document.fonts.add(new FontFace(family, `url(${URL_BASE}${f.url.split("/").map(encodeURIComponent).join("/")})`, { weight: f.weight, display: "block" }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 한국어·일본어는 CDN 에 묻지 않는다 — 렌더가 오프라인이어도, 늦게 온 조각 글꼴로 첫 프레임이 대체 글꼴이 되는 일도 없다
|
|
128
|
+
const LOCAL = new Set(["ko", "ja"]);
|
|
129
|
+
|
|
130
|
+
function localFonts(J: J): void {
|
|
99
131
|
const upstream = J.ensureFonts;
|
|
100
132
|
J.ensureFonts = async (text: string, keys: string[] | null) => {
|
|
101
|
-
if (!
|
|
133
|
+
if (!LOCAL.has(J.lang)) return upstream(text, keys);
|
|
102
134
|
const uniq = [...new Set([...text])].join("") || "가";
|
|
103
135
|
const list = (keys ?? Object.keys(J.FONTS)).filter((k: string) => J.FONTS[k]);
|
|
104
136
|
const faces: { family: string; weight: number }[] = list.map((k: string) => J.faceOf(k));
|
|
105
137
|
for (const b of J.langBaseFaces(list)) faces.push({ family: `"${b.family}"`, weight: b.weight });
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
// 太さ(@var)는 이름이 Noto Sans·Serif JP 인 가변 글꼴로 그린다 — 한국어면 그 이름에 가변 Noto Sans·Serif KR 을 건다(안 걸면 한글이 시스템 글꼴로 떨어진다)
|
|
139
|
+
const vary = !!keys?.includes("@var");
|
|
140
|
+
const varFaces = [["Noto Sans JP", "Noto Sans KR", 100, 900], ["Noto Serif JP", "Noto Serif KR", 200, 900]] as const;
|
|
141
|
+
const want = new Set(faces.map((f) => f.family.replace(/"/g, "")));
|
|
142
|
+
if (vary) for (const [jp, kr] of varFaces) want.add(J.lang === "ko" ? kr : jp);
|
|
143
|
+
const got = await provide([...want], text);
|
|
144
|
+
for (const f of got) addFace(f.family, f);
|
|
145
|
+
if (vary && J.lang === "ko") for (const [jp, kr] of varFaces) for (const f of got.filter((x) => x.family === kr)) addFace(jp, f);
|
|
146
|
+
const specs = new Set(faces.map((f) => `${f.weight} 64px ${f.family}`));
|
|
147
|
+
if (vary) for (const [jp, , lo, hi] of varFaces) specs.add(`${lo} 64px "${jp}"`).add(`${hi} 64px "${jp}"`);
|
|
148
|
+
await Promise.all([...specs].map((s) => document.fonts.load(s, uniq)));
|
|
108
149
|
J.glyphs.clear();
|
|
109
150
|
J.metrics.clear();
|
|
110
151
|
};
|
|
111
152
|
}
|
|
112
153
|
|
|
113
|
-
let engine: Promise<J
|
|
154
|
+
let engine: Promise<J> | null = null;
|
|
114
155
|
|
|
115
|
-
/** 엔진을 한 번만 싣는다.
|
|
116
|
-
export function loadJizura(): Promise<J
|
|
156
|
+
/** 실린 엔진을 한 번만 싣는다. 못 실으면 던진다 — 깨진 설치를 낱말 화면으로 덮지 않는다 */
|
|
157
|
+
export function loadJizura(): Promise<J> {
|
|
117
158
|
engine ??= (async () => {
|
|
118
159
|
if (window.J?.plan) return window.J;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await loadScript(`${URL_BASE}engine/jizura_engine.js`);
|
|
125
|
-
const J = window.J as J;
|
|
126
|
-
await localFonts(J);
|
|
127
|
-
return J;
|
|
128
|
-
} catch (err) {
|
|
129
|
-
console.warn(`[kf] JIZURA 엔진을 못 실었다 — 가사 층 없이 그린다: ${err instanceof Error ? err.message : err}`);
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
160
|
+
await loadScript(`${URL_BASE}engine/jizura_engine.js`);
|
|
161
|
+
const J = window.J;
|
|
162
|
+
if (!J?.plan) throw new Error("JIZURA 엔진을 실었는데 window.J 가 없다");
|
|
163
|
+
localFonts(J);
|
|
164
|
+
return J;
|
|
132
165
|
})();
|
|
133
166
|
return engine;
|
|
134
167
|
}
|
|
@@ -211,8 +244,9 @@ function planFor(o: LayerOptions): Promise<PlanEntry | null> {
|
|
|
211
244
|
let job = plans.get(key);
|
|
212
245
|
if (!job) {
|
|
213
246
|
job = (async () => {
|
|
247
|
+
if ((await lyricsMode()) === "words") return null;
|
|
214
248
|
const [J, lyrics] = await Promise.all([loadJizura(), o.lyrics ? Promise.resolve(o.lyrics) : projectLyrics()]);
|
|
215
|
-
if (!
|
|
249
|
+
if (!lyrics?.lines.length) return null;
|
|
216
250
|
return buildPlan(J, settings, lyrics, o.ctx);
|
|
217
251
|
})();
|
|
218
252
|
plans.set(key, job);
|
|
@@ -220,10 +254,11 @@ function planFor(o: LayerOptions): Promise<PlanEntry | null> {
|
|
|
220
254
|
return job;
|
|
221
255
|
}
|
|
222
256
|
|
|
223
|
-
/**
|
|
257
|
+
/** 이 층이 그릴지 — 틀 장면이 낱말 화면을 고를 때. 가사가 없거나 사람이 「낱말로」를 골랐으면 false, 엔진을 못 실으면 던진다 */
|
|
224
258
|
export async function jizuraReady(): Promise<boolean> {
|
|
225
|
-
|
|
226
|
-
|
|
259
|
+
if ((await lyricsMode()) === "words") return false;
|
|
260
|
+
const [, lyrics] = await Promise.all([loadJizura(), projectLyrics()]);
|
|
261
|
+
return !!lyrics?.lines.length;
|
|
227
262
|
}
|
|
228
263
|
|
|
229
264
|
/** 컷 목록 — 어느 컷이 화면을 덮는지 찾을 때(layout·enter·hold·exit·decor·cam·trans) */
|
|
@@ -241,6 +276,8 @@ const drawOpts = (mode: DrawMode, scale: number) =>
|
|
|
241
276
|
|
|
242
277
|
export type JizuraPaint = ((t: number) => void) & { canvas: HTMLCanvasElement };
|
|
243
278
|
|
|
279
|
+
const told = new Set<string>();
|
|
280
|
+
|
|
244
281
|
/** 장면 build 에서 부른다 — 장면을 덮는 캔버스를 짓고 (t) => 칠하기를 돌려준다. t 는 장면 안 시각.
|
|
245
282
|
* 한 장면에서 플랜을 바꿔 끼우려면(드롭부터 큰 플랜) 층을 둘 짓고 안 쓰는 쪽 .canvas 를 숨긴다 */
|
|
246
283
|
export function jizuraLayer(el: HTMLElement, o: LayerOptions): JizuraPaint {
|
|
@@ -254,10 +291,15 @@ export function jizuraLayer(el: HTMLElement, o: LayerOptions): JizuraPaint {
|
|
|
254
291
|
el.append(canvas);
|
|
255
292
|
const g = canvas.getContext("2d")!;
|
|
256
293
|
let entry: PlanEntry | null = null;
|
|
294
|
+
let failed: unknown = null;
|
|
257
295
|
const ready = planFor(o).then(
|
|
258
296
|
(e) => (entry = e),
|
|
259
297
|
(err) => {
|
|
260
|
-
|
|
298
|
+
failed = err;
|
|
299
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
300
|
+
// 플랜은 장면끼리 나눠 쓴다 — 같은 실패를 장면마다 찍지 않는다
|
|
301
|
+
if (!told.has(msg)) console.error(`[kf] 가사 층(${ctx.id}): ${msg}`);
|
|
302
|
+
told.add(msg);
|
|
261
303
|
return null;
|
|
262
304
|
},
|
|
263
305
|
);
|
|
@@ -268,10 +310,11 @@ export function jizuraLayer(el: HTMLElement, o: LayerOptions): JizuraPaint {
|
|
|
268
310
|
entry.R.frame(g, entry.plan, T, drawOpts(mode, canvas.width / entry.plan.W));
|
|
269
311
|
drawn = T;
|
|
270
312
|
};
|
|
271
|
-
// 플랜이 아직이면 그 seek 을 플랜이 설 때까지 붙든다 — 첫 프레임부터 가사가
|
|
313
|
+
// 플랜이 아직이면 그 seek 을 플랜이 설 때까지 붙든다 — 첫 프레임부터 가사가 찍힌다. 플랜을 못 세웠으면 그 프레임을 가사 없이 찍지 않고 던진다
|
|
272
314
|
onSeek(async () => {
|
|
273
315
|
if (want === null || drawn === want) return;
|
|
274
316
|
await ready;
|
|
317
|
+
if (failed) throw failed;
|
|
275
318
|
paint(want);
|
|
276
319
|
});
|
|
277
320
|
const draw = (t: number) => {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
// 부품 이름·스타일의 색 구성 번호·거부 목록이 이 판에 맞춰져 있다. 판을 올리면 deny.ts 를 다시 보고 견본 스틸을 다시 찍는다 — 같은 seed 라도 컷이 다시
|
|
1
|
+
// 부품 이름·스타일의 색 구성 번호·거부 목록이 이 판에 맞춰져 있다. 판을 올리면 deny.ts 를 다시 보고 견본 스틸을 다시 찍는다 — 같은 seed 라도 컷이 다시 굴려진다.
|
|
2
|
+
// 올린 뒤에는 node packages/jizura/src/vendor.ts 로 vendor/ 를 다시 만든다
|
|
2
3
|
export const JIZURA_REPO = "https://github.com/852wa/JIZURA.git";
|
|
3
4
|
export const JIZURA_COMMIT = "fc16bfe43ea4a6c25a21f1caf04d17326de14f00";
|
|
4
5
|
export const JIZURA_VERSION = "0.10.1";
|
|
5
6
|
export const JIZURA_CREDIT = "JIZURA 字面 (c) 2026 hakoniwa (github.com/852wa/JIZURA), MIT";
|
|
7
|
+
// 글꼴은 google/fonts 의 이 커밋에서 받는다 — 같은 파일이라야 어느 기계에서든 같은 프레임이 나온다(install.ts 가 크기·sha256 을 맞춰 본다)
|
|
8
|
+
export const GOOGLE_FONTS_COMMIT = "51303ca9e8ac9dcea7b12d307ba568fd0e6fcfca";
|