kasaframe 0.0.0-stage → 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/LICENSE +21 -0
- package/NOTICE +148 -0
- package/README.md +92 -2
- package/app/package.json +12 -0
- package/app/server/engine.ts +533 -0
- package/app/server/main.ts +209 -0
- package/app/web/env.d.ts +2 -0
- package/app/web/icons.ts +31 -0
- package/app/web/index.html +21 -0
- package/app/web/main.ts +1195 -0
- package/app/web/style.css +463 -0
- package/bin/hooks.mjs +64 -0
- package/bin/kf.mjs +32 -0
- package/docs/AGENTS.md +286 -0
- package/music/README.md +37 -0
- package/package.json +44 -4
- package/packages/adapters/package.json +36 -0
- package/packages/adapters/src/css.ts +11 -0
- package/packages/adapters/src/gsap.ts +26 -0
- package/packages/adapters/src/index.ts +9 -0
- package/packages/adapters/src/lottie.ts +22 -0
- package/packages/adapters/src/media.ts +59 -0
- package/packages/adapters/src/page.ts +229 -0
- package/packages/adapters/src/project.ts +214 -0
- package/packages/adapters/src/react.ts +36 -0
- package/packages/adapters/src/svelte.ts +22 -0
- package/packages/adapters/src/three.ts +18 -0
- package/packages/adapters/src/vue.ts +27 -0
- package/packages/check/package.json +19 -0
- package/packages/check/src/check.ts +280 -0
- package/packages/check/src/cli.ts +70 -0
- package/packages/check/src/image.ts +146 -0
- package/packages/check/src/index.ts +9 -0
- package/packages/check/src/ocr.swift +37 -0
- package/packages/check/src/probe.ts +328 -0
- package/packages/check/src/report.ts +44 -0
- package/packages/check/src/rules/assets.ts +45 -0
- package/packages/check/src/rules/beats.ts +212 -0
- package/packages/check/src/rules/camera.ts +217 -0
- package/packages/check/src/rules/captions.ts +49 -0
- package/packages/check/src/rules/flash.ts +235 -0
- package/packages/check/src/rules/frames.ts +136 -0
- package/packages/check/src/rules/layout.ts +281 -0
- package/packages/check/src/rules/page.ts +87 -0
- package/packages/check/src/rules/runs.ts +20 -0
- package/packages/check/src/rules/text.ts +326 -0
- package/packages/check/src/scan.ts +240 -0
- package/packages/check/src/serve.ts +67 -0
- package/packages/check/src/types.ts +80 -0
- package/packages/check/src/video.ts +137 -0
- package/packages/cli/package.json +30 -0
- package/packages/cli/src/blur.ts +80 -0
- package/packages/cli/src/browser.ts +203 -0
- package/packages/cli/src/command.ts +44 -0
- package/packages/cli/src/commands/app.ts +25 -0
- package/packages/cli/src/commands/beats.ts +36 -0
- package/packages/cli/src/commands/bench.ts +123 -0
- package/packages/cli/src/commands/blender.ts +293 -0
- package/packages/cli/src/commands/check.ts +25 -0
- package/packages/cli/src/commands/direct.ts +210 -0
- package/packages/cli/src/commands/doctor.ts +30 -0
- package/packages/cli/src/commands/gen.ts +173 -0
- package/packages/cli/src/commands/look.ts +46 -0
- package/packages/cli/src/commands/lyrics.ts +157 -0
- package/packages/cli/src/commands/make.ts +1121 -0
- 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 +185 -0
- package/packages/cli/src/commands/preview.ts +43 -0
- package/packages/cli/src/commands/render.ts +250 -0
- package/packages/cli/src/commands/series.ts +110 -0
- package/packages/cli/src/commands/setup.ts +102 -0
- package/packages/cli/src/commands/sheet.ts +114 -0
- package/packages/cli/src/commands/skill.ts +43 -0
- package/packages/cli/src/commands/still.ts +40 -0
- package/packages/cli/src/commands/theme.ts +172 -0
- package/packages/cli/src/commands/transitions.ts +40 -0
- package/packages/cli/src/deps.ts +122 -0
- package/packages/cli/src/ffmpeg.ts +81 -0
- package/packages/cli/src/index.ts +8 -0
- package/packages/cli/src/kf.ts +64 -0
- package/packages/cli/src/lock.ts +72 -0
- package/packages/cli/src/mix.ts +131 -0
- package/packages/cli/src/out.ts +13 -0
- package/packages/cli/src/pool.ts +34 -0
- package/packages/cli/src/segments.ts +402 -0
- package/packages/cli/src/serve.ts +124 -0
- package/packages/clock/package.json +14 -0
- package/packages/clock/src/index.ts +25 -0
- package/packages/clock/src/install.ts +351 -0
- package/packages/clock/src/types.ts +45 -0
- package/packages/core/package.json +9 -0
- package/packages/core/src/beats.ts +283 -0
- package/packages/core/src/ease.ts +87 -0
- package/packages/core/src/frame.ts +99 -0
- package/packages/core/src/index.ts +27 -0
- package/packages/core/src/project.ts +157 -0
- package/packages/core/src/rand.ts +56 -0
- package/packages/core/src/spring.ts +52 -0
- package/packages/core/src/style.ts +100 -0
- package/packages/core/src/time.ts +88 -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 +90 -0
- package/packages/gen/package.json +9 -0
- 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 +206 -0
- package/packages/gen/src/image/codex.ts +117 -0
- package/packages/gen/src/image/og.ts +133 -0
- package/packages/gen/src/index.ts +26 -0
- package/packages/gen/src/keys.ts +86 -0
- package/packages/gen/src/media.ts +150 -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/plan.ts +106 -0
- package/packages/gen/src/registry.ts +54 -0
- package/packages/gen/src/seam.ts +103 -0
- 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 +90 -0
- package/packages/gen/src/video/fal.ts +140 -0
- package/packages/gen/src/video/h3.ts +112 -0
- package/packages/gen/src/video/http.ts +44 -0
- package/packages/gen/src/video/ltx.ts +60 -0
- package/packages/gen/src/video/minimax.ts +55 -0
- package/packages/gen/src/video/veo.ts +71 -0
- package/packages/jizura/package.json +16 -0
- package/packages/jizura/src/deny.ts +73 -0
- package/packages/jizura/src/index.ts +7 -0
- package/packages/jizura/src/install.ts +269 -0
- package/packages/jizura/src/layer.ts +325 -0
- package/packages/jizura/src/lyrics.ts +81 -0
- package/packages/jizura/src/pin.ts +8 -0
- package/packages/jizura/src/vendor.ts +140 -0
- package/packages/jizura/src/vite.ts +63 -0
- 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 +153 -0
- package/packages/motion/package.json +30 -0
- package/packages/motion/src/camera.ts +97 -0
- package/packages/motion/src/captions.ts +487 -0
- package/packages/motion/src/enter.ts +128 -0
- package/packages/motion/src/hangul.ts +129 -0
- package/packages/motion/src/impact.ts +100 -0
- package/packages/motion/src/index.ts +17 -0
- package/packages/motion/src/moves/brushWipe.ts +103 -0
- package/packages/motion/src/moves/cloneArray.ts +111 -0
- package/packages/motion/src/moves/curtains.ts +81 -0
- package/packages/motion/src/moves/dress.ts +191 -0
- package/packages/motion/src/moves/echoStack.ts +70 -0
- package/packages/motion/src/moves/fit.ts +70 -0
- package/packages/motion/src/moves/flood.ts +61 -0
- package/packages/motion/src/moves/ghostStack.ts +88 -0
- package/packages/motion/src/moves/gridFloor.ts +100 -0
- package/packages/motion/src/moves/imageSwarm.ts +113 -0
- package/packages/motion/src/moves/index.ts +38 -0
- package/packages/motion/src/moves/irisBlades.ts +113 -0
- package/packages/motion/src/moves/karaokeLine.ts +108 -0
- package/packages/motion/src/moves/ledBoard.ts +94 -0
- package/packages/motion/src/moves/liquidTab.ts +72 -0
- package/packages/motion/src/moves/markerWrite.ts +63 -0
- package/packages/motion/src/moves/menu.ts +138 -0
- package/packages/motion/src/moves/morphBox.ts +96 -0
- package/packages/motion/src/moves/neonSign.ts +78 -0
- package/packages/motion/src/moves/panelPop.ts +78 -0
- package/packages/motion/src/moves/paperTear.ts +75 -0
- package/packages/motion/src/moves/path.ts +64 -0
- package/packages/motion/src/moves/pathText.ts +107 -0
- package/packages/motion/src/moves/pointSphere.ts +109 -0
- package/packages/motion/src/moves/ransomLetters.ts +73 -0
- package/packages/motion/src/moves/shapeMorph.ts +143 -0
- package/packages/motion/src/moves/slamWords.ts +96 -0
- package/packages/motion/src/moves/sparkTrail.ts +130 -0
- package/packages/motion/src/moves/speedLines.ts +91 -0
- package/packages/motion/src/moves/splitFlap.ts +149 -0
- package/packages/motion/src/moves/stepWords.ts +128 -0
- package/packages/motion/src/moves/strikeOut.ts +98 -0
- package/packages/motion/src/moves/sunburst.ts +58 -0
- package/packages/motion/src/moves/tickerRows.ts +91 -0
- package/packages/motion/src/moves/tokenPicker.ts +115 -0
- package/packages/motion/src/moves/truchet.ts +85 -0
- package/packages/motion/src/moves/wordWall.ts +78 -0
- package/packages/motion/src/moves/worldRoll.ts +32 -0
- package/packages/motion/src/particles.ts +102 -0
- package/packages/motion/src/react/index.tsx +21 -0
- package/packages/motion/src/react/parts.tsx +431 -0
- package/packages/motion/src/react/time.tsx +97 -0
- package/packages/motion/src/text.ts +103 -0
- package/packages/motion/src/transition.ts +82 -0
- package/packages/motion/src/transition3d/cards.ts +65 -0
- package/packages/motion/src/transition3d/chain.ts +84 -0
- package/packages/motion/src/transition3d/dive.ts +181 -0
- package/packages/motion/src/transition3d/fold.ts +110 -0
- package/packages/motion/src/transition3d/gl.ts +219 -0
- package/packages/motion/src/transition3d/index.ts +235 -0
- package/packages/motion/src/transition3d/live.ts +70 -0
- package/packages/motion/src/transition3d/portal.ts +115 -0
- package/packages/motion/src/transition3d/react.tsx +29 -0
- package/packages/motion/src/transition3d/rig.ts +600 -0
- package/packages/motion/src/transition3d/shatter.ts +104 -0
- package/packages/motion/src/transition3d/space.ts +173 -0
- package/packages/motion/src/transition3d/whip.ts +78 -0
- package/packages/motion/src/transition3d/world.ts +737 -0
- package/packages/motion/src/transition3d/zoomOut.ts +114 -0
- package/packages/motion/src/transitions2d/catalog.ts +127 -0
- package/packages/motion/src/transitions2d/css.d.ts +2 -0
- package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
- package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
- package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
- package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
- package/packages/motion/src/transitions2d/index.ts +2 -0
- package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
- package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
- package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
- package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
- package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
- package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
- package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
- package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
- package/packages/motion/src/transitions2d/react.ts +13 -0
- package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
- package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
- package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
- package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
- package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
- package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
- package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
- package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
- package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
- package/packages/motion/src/type.ts +131 -0
- package/packages/motion/src/ui.ts +172 -0
- 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/package.json +15 -0
- package/packages/plan/src/asks.ts +256 -0
- package/packages/plan/src/brief.ts +383 -0
- package/packages/plan/src/captions.ts +78 -0
- package/packages/plan/src/detect.ts +93 -0
- package/packages/plan/src/index.ts +27 -0
- package/packages/plan/src/layout.ts +176 -0
- package/packages/plan/src/llm.ts +172 -0
- package/packages/plan/src/look.ts +222 -0
- package/packages/plan/src/plan.ts +179 -0
- package/packages/plan/src/request.ts +55 -0
- package/packages/plan/src/series.ts +76 -0
- package/packages/plan/src/studio.ts +53 -0
- package/packages/plan/src/templates.ts +42 -0
- package/packages/plan/src/themes.ts +48 -0
- package/packages/plan/src/types.ts +157 -0
- package/packages/studio/package.json +12 -0
- package/packages/studio/src/index.ts +1 -0
- package/packages/studio/src/plugin.ts +236 -0
- package/packages/studio/src/ui/main.ts +503 -0
- package/packages/studio/src/ui/studio.css +165 -0
- package/packages/studio/src/ui/timeline.ts +317 -0
- package/packages/themes/kit/THEME-MAKE.md +68 -0
- package/packages/themes/kit/THEME.md +30 -0
- package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
- package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
- package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
- package/packages/themes/kit/effects.ts +5 -0
- package/packages/themes/kit/sample/index.html +11 -0
- package/packages/themes/kit/sample/main.ts +9 -0
- package/packages/themes/kit/sample/scenes/scene.ts +15 -0
- package/packages/themes/package.json +19 -0
- package/packages/themes/src/factory.ts +296 -0
- package/packages/themes/src/fx.ts +14 -0
- package/packages/themes/src/index.ts +114 -0
- package/packages/themes/src/slotkit.ts +79 -0
- package/packages/themes/src/slots.ts +46 -0
- package/packages/themes/src/stage3d.ts +197 -0
- package/packages/themes/src/types.ts +58 -0
- package/presets/kawaii-pastel.json +94 -0
- package/presets/neutral-dark.json +82 -0
- package/presets/og.json +98 -0
- package/series/og-shorts.json +14 -0
- package/skills/kasaframe/SKILL.md +33 -0
- package/templates/_shared/project/index.html +11 -0
- package/templates/_shared/project/lib/react.ts +22 -0
- package/templates/_shared/project/lib/stage.ts +292 -0
- package/templates/_shared/project/main.ts +9 -0
- package/templates/_shared/scenes/default.ts +21 -0
- package/templates/_shared/scenes/drop.ts +33 -0
- package/templates/_shared/scenes/end.ts +26 -0
- package/templates/_shared/scenes/hook.ts +18 -0
- package/templates/_shared/scenes/seam.ts +41 -0
- package/templates/_shared/scenes/step.ts +49 -0
- package/templates/ad/preview.png +0 -0
- package/templates/ad/template.json +151 -0
- package/templates/demo/preview.png +0 -0
- package/templates/demo/template.json +141 -0
- package/templates/explainer/preview.png +0 -0
- package/templates/explainer/template.json +193 -0
- package/templates/launch/preview.png +0 -0
- package/templates/launch/template.json +163 -0
- package/templates/mv/preview.png +0 -0
- package/templates/mv/scenes/default.ts +98 -0
- package/templates/mv/template.json +178 -0
- package/templates/promo/preview.png +0 -0
- package/templates/promo/template.json +185 -0
- package/templates/shorts/preview.png +0 -0
- package/templates/shorts/scenes/hook.ts +37 -0
- package/templates/shorts/template.json +196 -0
- package/templates/square/preview.png +0 -0
- package/templates/square/template.json +133 -0
- package/themes/debug-hud/THEME.md +39 -0
- package/themes/debug-hud/effects.ts +389 -0
- package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
- package/themes/debug-hud/sample/beats.json +1 -0
- package/themes/debug-hud/sample/index.html +11 -0
- package/themes/debug-hud/sample/kf.json +59 -0
- package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
- package/themes/debug-hud/sample/lib/og.ts +29 -0
- package/themes/debug-hud/sample/main.ts +9 -0
- package/themes/debug-hud/sample/scenes/end.ts +62 -0
- package/themes/debug-hud/sample/scenes/gather.ts +67 -0
- package/themes/debug-hud/sample/scenes/models.ts +70 -0
- package/themes/debug-hud/sample/scenes/price.ts +79 -0
- package/themes/debug-hud/sample/style.json +91 -0
- package/themes/debug-hud/slots.ts +99 -0
- package/themes/debug-hud/still.png +0 -0
- package/themes/debug-hud/style.json +91 -0
- package/themes/debug-hud/theme.json +68 -0
- package/themes/og-measure/THEME.md +47 -0
- package/themes/og-measure/effects.ts +432 -0
- package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
- package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
- package/themes/og-measure/sample/assets/data/measure.json +353 -0
- package/themes/og-measure/sample/beats.json +1 -0
- package/themes/og-measure/sample/index.html +11 -0
- package/themes/og-measure/sample/kf.json +66 -0
- package/themes/og-measure/sample/lib/data.ts +62 -0
- package/themes/og-measure/sample/main.ts +9 -0
- package/themes/og-measure/sample/measure.ts +78 -0
- package/themes/og-measure/sample/scenes/end.ts +47 -0
- package/themes/og-measure/sample/scenes/gather.ts +51 -0
- package/themes/og-measure/sample/scenes/models.ts +34 -0
- package/themes/og-measure/sample/scenes/one.ts +39 -0
- package/themes/og-measure/sample/scenes/price.ts +56 -0
- package/themes/og-measure/sample/style.json +97 -0
- package/themes/og-measure/slots.ts +88 -0
- package/themes/og-measure/still.png +0 -0
- package/themes/og-measure/style.json +97 -0
- package/themes/og-measure/theme.json +79 -0
- package/themes/riso-zine/THEME.md +43 -0
- package/themes/riso-zine/effects.ts +321 -0
- package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
- package/themes/riso-zine/sample/beats.json +1 -0
- package/themes/riso-zine/sample/index.html +11 -0
- package/themes/riso-zine/sample/kf.json +59 -0
- package/themes/riso-zine/sample/lib/og.ts +29 -0
- package/themes/riso-zine/sample/lib/shot.ts +86 -0
- package/themes/riso-zine/sample/lib/zine.ts +42 -0
- package/themes/riso-zine/sample/main.ts +9 -0
- package/themes/riso-zine/sample/scenes/end.ts +61 -0
- package/themes/riso-zine/sample/scenes/gather.ts +62 -0
- package/themes/riso-zine/sample/scenes/models.ts +52 -0
- package/themes/riso-zine/sample/scenes/price.ts +82 -0
- package/themes/riso-zine/sample/style.json +99 -0
- package/themes/riso-zine/slots.ts +81 -0
- package/themes/riso-zine/still.png +0 -0
- package/themes/riso-zine/style.json +99 -0
- package/themes/riso-zine/theme.json +75 -0
- package/tools/beats/beats.py +372 -0
- package/tools/beats/requirements.txt +3 -0
- package/tools/lyrics/align.py +164 -0
- package/tools/lyrics/requirements.txt +9 -0
|
@@ -0,0 +1,269 @@
|
|
|
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 — 그 언어의 나머지 얼굴을 미리 받는다
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { GOOGLE_FONTS_COMMIT, JIZURA_COMMIT } from "./pin.ts";
|
|
13
|
+
|
|
14
|
+
export const vendorDir = fileURLToPath(new URL("../vendor", import.meta.url));
|
|
15
|
+
export const builtinFontDir = join(vendorDir, "fonts");
|
|
16
|
+
export const jizuraHome = (): string => process.env.KF_JIZURA_HOME ?? join(homedir(), ".cache", "kasaframe", "jizura");
|
|
17
|
+
export const fontDir = (): string => join(jizuraHome(), "fonts");
|
|
18
|
+
|
|
19
|
+
export interface EngineInfo {
|
|
20
|
+
commit: string;
|
|
21
|
+
version: string;
|
|
22
|
+
sha256: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
files: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
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")] },
|
|
76
|
+
];
|
|
77
|
+
export const FONT_LANGS = ["ko", "ja"];
|
|
78
|
+
|
|
79
|
+
export const localName = (file: string): string => file.replace(/\[.*\]/, "-VF");
|
|
80
|
+
|
|
81
|
+
export interface BuiltinFont {
|
|
82
|
+
family: string;
|
|
83
|
+
weight: string;
|
|
84
|
+
file: string;
|
|
85
|
+
/** 서브셋을 뜬 원본(google/fonts 파일 이름) */
|
|
86
|
+
from: string;
|
|
87
|
+
/** 서브셋이면 남긴 글자 범위 — 원본을 그대로 실은 것은 없다 */
|
|
88
|
+
unicodes?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
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;
|
|
97
|
+
}
|
|
98
|
+
|
|
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];
|
|
109
|
+
}
|
|
110
|
+
|
|
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;
|
|
114
|
+
}
|
|
115
|
+
|
|
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++) {
|
|
125
|
+
try {
|
|
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
|
+
renameSync(`${to}.part`, to);
|
|
134
|
+
return;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
last = why(err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`${url} — ${last}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
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);
|
|
161
|
+
}
|
|
162
|
+
})();
|
|
163
|
+
jobs.set(fam.family, job);
|
|
164
|
+
job.catch(() => jobs.delete(fam.family));
|
|
165
|
+
}
|
|
166
|
+
return job;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface FaceSrc {
|
|
170
|
+
family: string;
|
|
171
|
+
weight: string;
|
|
172
|
+
/** /@kf-jizura/ 아래 경로 */
|
|
173
|
+
url: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
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 };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** kf doctor 한 줄 — cli/deps.ts 의 Check 꼴 */
|
|
229
|
+
export function jizuraCheck() {
|
|
230
|
+
const info = engineInfo();
|
|
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));
|
|
235
|
+
return {
|
|
236
|
+
name: "JIZURA 가사 층",
|
|
237
|
+
ok,
|
|
238
|
+
required: false,
|
|
239
|
+
detail: info
|
|
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 를 다시 깐다",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** kf setup jizura — 그 언어에서 내장에 없는 얼굴을 미리 받는다(오프라인으로 굽기 전에) */
|
|
247
|
+
export async function install(langs: string[], log = console.log): Promise<void> {
|
|
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("·")})`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
255
|
+
const i = process.argv.indexOf("--lang");
|
|
256
|
+
const langs = (i > 0 ? process.argv[i + 1] : "ko").split(",").filter(Boolean);
|
|
257
|
+
const bad = langs.filter((l) => !FONT_LANGS.includes(l));
|
|
258
|
+
if (bad.length) {
|
|
259
|
+
console.error(`모르는 언어 ${bad.join(" ")} — ${FONT_LANGS.join(" ")}`);
|
|
260
|
+
process.exit(2);
|
|
261
|
+
}
|
|
262
|
+
install(langs).then(
|
|
263
|
+
() => process.exit(0),
|
|
264
|
+
(err) => {
|
|
265
|
+
console.error(`jizura 글꼴 받기 실패: ${why(err)}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// JIZURA 字面(MIT, (c) 2026 hakoniwa) 엔진을 결정적인 가사 층으로 — 장면 위 캔버스 한 장에 시각 t 의 가사를 그린다.
|
|
2
|
+
// 플랜 하나 = JIZURA 프로젝트 하나(곡 전체 가사·박). 같은 설정을 쓰는 장면끼리는 한 플랜을 나눠 쓰고, 장면마다 다른 플랜을 고른다.
|
|
3
|
+
// 엔진은 패키지에 실려 있고(vendor/) 서버가 /@kf-jizura/ 로 낸다(vite.ts). 글꼴도 서버가 고른다 — 내장 서브셋·받아 둔 원본·처음 쓰면 받기.
|
|
4
|
+
// 빌려 온 것: 플랜 만들기·색 구성 고르기(schemes)는 vocaloid-style-mv-pipeline(MIT, NikusonP) 의 jizura_adapter.js 를 옮겼다.
|
|
5
|
+
import { onSeek } from "@kasaframe/adapters";
|
|
6
|
+
import type { KfMeta, SceneCtx } from "@kasaframe/core";
|
|
7
|
+
import { partPool, type JizuraRegistry, type PoolOptions } from "./deny.ts";
|
|
8
|
+
import type { FaceSrc, Missing } from "./install.ts";
|
|
9
|
+
import { parseLyrics, toLrc, type LyricsJson } from "./lyrics.ts";
|
|
10
|
+
|
|
11
|
+
const URL_BASE = "/@kf-jizura/";
|
|
12
|
+
|
|
13
|
+
export type DrawMode = "transparent" | "front" | "back" | "normal";
|
|
14
|
+
|
|
15
|
+
export interface PlanSettings extends PoolOptions {
|
|
16
|
+
/** JIZURA 스타일 — ocean·gold·noir·sakura·sunset·mono·paper·sumi … */
|
|
17
|
+
style?: string;
|
|
18
|
+
/** 'light' = 밝은 바탕 위 어두운 글(낮 장면) · 'dark' = 어두운 바탕 위 밝은 글(밤 장면) · [번호…] = 그 스타일의 색 구성 */
|
|
19
|
+
schemes?: "light" | "dark" | number[];
|
|
20
|
+
/** 가운데를 비우고 양옆(세로 화면은 위아래) 띠에만 — 주인공 얼굴·몸짓이 요점인 장면 */
|
|
21
|
+
centerFree?: boolean;
|
|
22
|
+
centerDir?: "tb" | "lr";
|
|
23
|
+
seed?: number;
|
|
24
|
+
fx?: Record<string, unknown>;
|
|
25
|
+
colors?: Record<string, unknown>;
|
|
26
|
+
/** 그 밖의 JIZURA 프로젝트 칸은 그대로 넘긴다 */
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LayerOptions {
|
|
31
|
+
ctx: SceneCtx;
|
|
32
|
+
/** jizuraPlans 로 등록한 이름, 또는 설정 그대로(같은 설정이면 한 플랜을 나눠 쓴다) */
|
|
33
|
+
plan?: string | PlanSettings;
|
|
34
|
+
/** 기본 transparent — JIZURA 바탕 없이 글·장식만 */
|
|
35
|
+
mode?: DrawMode;
|
|
36
|
+
/** 기본은 프로젝트 뿌리의 lyrics.json */
|
|
37
|
+
lyrics?: LyricsJson;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type J = JizuraRegistry & Record<string, any>;
|
|
41
|
+
|
|
42
|
+
interface PlanEntry {
|
|
43
|
+
plan: any;
|
|
44
|
+
R: any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare global {
|
|
48
|
+
interface Window {
|
|
49
|
+
J?: J;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// mulberry32 — 렌더러가 만들 때 쓰는 Math.random(종이·입자 질감)을 플랜 seed 에 묶는다. 가상 시계가 seek 마다 난수를 다시 심어도 질감이 같다
|
|
54
|
+
function withSeed<T>(seed: number, fn: () => T): T {
|
|
55
|
+
let a = seed >>> 0;
|
|
56
|
+
const rnd = () => {
|
|
57
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
58
|
+
let t = a;
|
|
59
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
60
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
61
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
62
|
+
};
|
|
63
|
+
const real = Math.random;
|
|
64
|
+
Math.random = rnd;
|
|
65
|
+
try {
|
|
66
|
+
return fn();
|
|
67
|
+
} finally {
|
|
68
|
+
Math.random = real;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadScript(src: string): Promise<void> {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const s = document.createElement("script");
|
|
75
|
+
s.src = src;
|
|
76
|
+
s.onload = () => resolve();
|
|
77
|
+
s.onerror = () => reject(new Error(`${src} 를 못 받았다`));
|
|
78
|
+
document.head.append(s);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 없는 파일을 Vite 가 SPA 대체로 index.html(200)로 돌려준다 — JSON 이 아니면 없는 것으로 본다. 가사 없는 뮤비 프로젝트가 렌더 중에 던지지 않게
|
|
83
|
+
async function getJson<T>(url: string | URL): Promise<T | null> {
|
|
84
|
+
try {
|
|
85
|
+
const r = await fetch(url);
|
|
86
|
+
if (!r.ok || !(r.headers.get("content-type") ?? "").includes("json")) return null;
|
|
87
|
+
return (await r.json()) as T;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** jizura — 가사 층 · builtin — 가사 층인데 못 받은 얼굴은 내장 글꼴로 · words — 가사 층 없이 낱말(틀 장면이 DOM 으로). 사람이 고른다 */
|
|
94
|
+
export type LyricsMode = "jizura" | "builtin" | "words";
|
|
95
|
+
|
|
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 {
|
|
131
|
+
const upstream = J.ensureFonts;
|
|
132
|
+
J.ensureFonts = async (text: string, keys: string[] | null) => {
|
|
133
|
+
if (!LOCAL.has(J.lang)) return upstream(text, keys);
|
|
134
|
+
const uniq = [...new Set([...text])].join("") || "가";
|
|
135
|
+
const list = (keys ?? Object.keys(J.FONTS)).filter((k: string) => J.FONTS[k]);
|
|
136
|
+
const faces: { family: string; weight: number }[] = list.map((k: string) => J.faceOf(k));
|
|
137
|
+
for (const b of J.langBaseFaces(list)) faces.push({ family: `"${b.family}"`, weight: b.weight });
|
|
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)));
|
|
149
|
+
J.glyphs.clear();
|
|
150
|
+
J.metrics.clear();
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let engine: Promise<J> | null = null;
|
|
155
|
+
|
|
156
|
+
/** 실린 엔진을 한 번만 싣는다. 못 실으면 던진다 — 깨진 설치를 낱말 화면으로 덮지 않는다 */
|
|
157
|
+
export function loadJizura(): Promise<J> {
|
|
158
|
+
engine ??= (async () => {
|
|
159
|
+
if (window.J?.plan) return window.J;
|
|
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;
|
|
165
|
+
})();
|
|
166
|
+
return engine;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const named = new Map<string, PlanSettings>();
|
|
170
|
+
|
|
171
|
+
/** 장면들이 이름으로 고를 플랜을 등록한다 — 낮·밤·후렴처럼 */
|
|
172
|
+
export function jizuraPlans(plans: Record<string, PlanSettings>): void {
|
|
173
|
+
for (const [k, v] of Object.entries(plans)) named.set(k, v);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let lyricsJob: Promise<LyricsJson | null> | null = null;
|
|
177
|
+
function projectLyrics(): Promise<LyricsJson | null> {
|
|
178
|
+
lyricsJob ??= getJson(new URL("lyrics.json", document.baseURI)).then((raw) => {
|
|
179
|
+
if (raw === null) return null;
|
|
180
|
+
try {
|
|
181
|
+
return parseLyrics(raw);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.warn(`[kf] lyrics.json 을 못 읽었다 — 가사 층 없이 그린다: ${err instanceof Error ? err.message : err}`);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return lyricsJob;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const ASPECTS: [string, number][] = [["16:9", 16 / 9], ["9:16", 9 / 16], ["1:1", 1], ["4:5", 0.8], ["4:3", 4 / 3], ["3:4", 0.75], ["21:9", 21 / 9]];
|
|
191
|
+
const aspectOf = (m: KfMeta) => ASPECTS.reduce((b, a) => (Math.abs(a[1] - m.width / m.height) < Math.abs(b[1] - m.width / m.height) ? a : b))[0];
|
|
192
|
+
|
|
193
|
+
// 플랜을 짓는 동안 J.lang·J.typeset 같은 전역이 바뀐다 — 한 번에 하나씩
|
|
194
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
195
|
+
const plans = new Map<string, Promise<PlanEntry | null>>();
|
|
196
|
+
|
|
197
|
+
function buildPlan(J: J, s: PlanSettings, lyrics: LyricsJson, ctx: SceneCtx): Promise<PlanEntry> {
|
|
198
|
+
const base = J.defaultProject();
|
|
199
|
+
const { deny, denyOnly, moods, schemes, fx, ...rest } = s;
|
|
200
|
+
const seed = s.seed ?? 2609;
|
|
201
|
+
const project = {
|
|
202
|
+
...base,
|
|
203
|
+
lyrics: toLrc(lyrics),
|
|
204
|
+
lang: lyrics.lang ?? "auto",
|
|
205
|
+
fps: ctx.meta.fps,
|
|
206
|
+
res: Math.min(ctx.meta.width, ctx.meta.height),
|
|
207
|
+
aspect: aspectOf(ctx.meta),
|
|
208
|
+
style: "ocean",
|
|
209
|
+
seed,
|
|
210
|
+
// 統一感·文字整列 켬, 공포·和風 세트 끔. 화면 전체 섬광은 kasaframe(dropBurst·섬광 검사)이 한 곳에서 쥔다
|
|
211
|
+
unify: true,
|
|
212
|
+
typeset: true,
|
|
213
|
+
extra: true,
|
|
214
|
+
wa: false,
|
|
215
|
+
horror: false,
|
|
216
|
+
...rest,
|
|
217
|
+
fx: { ...base.fx, flash: false, hud: "off", glitch: 0.3, chroma: 0.45, ...fx },
|
|
218
|
+
timing: { ...base.timing, ...((rest.timing as object) ?? {}) },
|
|
219
|
+
enabled: partPool(J, { deny, denyOnly, moods }),
|
|
220
|
+
};
|
|
221
|
+
const data = ctx.beats.data;
|
|
222
|
+
const audio = { duration: data.duration, beats: data.beats.slice(), energy: null, energyRate: 0, bpm: data.bpm };
|
|
223
|
+
const job = queue.then(async () => {
|
|
224
|
+
const plan = J.plan(project, audio);
|
|
225
|
+
if (schemes) {
|
|
226
|
+
const pick = (test: (l: number) => boolean) => plan.style.schemes.map((x: { bg: string }, i: number) => (test(J.lum(x.bg)) ? i : -1)).filter((i: number) => i >= 0);
|
|
227
|
+
const ok: number[] = Array.isArray(schemes) ? schemes : schemes === "dark" ? pick((l) => l < 0.25) : pick((l) => l > 0.6);
|
|
228
|
+
if (ok.length) for (const c of plan.cuts) c.scheme = ok[(c.scheme | 0) % ok.length];
|
|
229
|
+
}
|
|
230
|
+
const R = withSeed(seed, () => new J.Renderer());
|
|
231
|
+
withSeed(seed + 1, () => R.paper(plan.W, plan.H));
|
|
232
|
+
await J.ensureFonts(project.lyrics + "0123456789:./-", J.fontsOfPlan(plan));
|
|
233
|
+
J.glyphs.maxRes = plan.H >= 1000 ? 768 : 512;
|
|
234
|
+
return { plan, R };
|
|
235
|
+
});
|
|
236
|
+
queue = job.catch(() => null);
|
|
237
|
+
return job;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function planFor(o: LayerOptions): Promise<PlanEntry | null> {
|
|
241
|
+
const settings = typeof o.plan === "string" ? named.get(o.plan) : (o.plan ?? {});
|
|
242
|
+
if (!settings) return Promise.reject(new Error(`jizuraPlans 에 없는 플랜 "${o.plan}"`));
|
|
243
|
+
const key = JSON.stringify([settings, o.lyrics ? "own" : "project", o.ctx.meta.width, o.ctx.meta.height]);
|
|
244
|
+
let job = plans.get(key);
|
|
245
|
+
if (!job) {
|
|
246
|
+
job = (async () => {
|
|
247
|
+
if ((await lyricsMode()) === "words") return null;
|
|
248
|
+
const [J, lyrics] = await Promise.all([loadJizura(), o.lyrics ? Promise.resolve(o.lyrics) : projectLyrics()]);
|
|
249
|
+
if (!lyrics?.lines.length) return null;
|
|
250
|
+
return buildPlan(J, settings, lyrics, o.ctx);
|
|
251
|
+
})();
|
|
252
|
+
plans.set(key, job);
|
|
253
|
+
}
|
|
254
|
+
return job;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 이 층이 그릴지 — 틀 장면이 낱말 화면을 고를 때. 가사가 없거나 사람이 「낱말로」를 골랐으면 false, 엔진을 못 실으면 던진다 */
|
|
258
|
+
export async function jizuraReady(): Promise<boolean> {
|
|
259
|
+
if ((await lyricsMode()) === "words") return false;
|
|
260
|
+
const [, lyrics] = await Promise.all([loadJizura(), projectLyrics()]);
|
|
261
|
+
return !!lyrics?.lines.length;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 컷 목록 — 어느 컷이 화면을 덮는지 찾을 때(layout·enter·hold·exit·decor·cam·trans) */
|
|
265
|
+
export async function jizuraCuts(o: LayerOptions): Promise<object[]> {
|
|
266
|
+
const e = await planFor(o);
|
|
267
|
+
return (e?.plan.cuts ?? []).map((c: any) => ({
|
|
268
|
+
i: c.index, start: +c.start.toFixed(3), end: +c.end.toFixed(3), text: c.text,
|
|
269
|
+
layout: c.layout, enter: c.enter, hold: c.hold, exit: c.exit, cam: c.cam, trans: c.trans ?? null,
|
|
270
|
+
decor: (c.decor ?? []).map((d: any) => d.id ?? d.type ?? d), scheme: c.scheme,
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const drawOpts = (mode: DrawMode, scale: number) =>
|
|
275
|
+
mode === "front" || mode === "back" ? { scale, transparent: true, layer: mode } : mode === "transparent" ? { scale, transparent: true } : { scale };
|
|
276
|
+
|
|
277
|
+
export type JizuraPaint = ((t: number) => void) & { canvas: HTMLCanvasElement };
|
|
278
|
+
|
|
279
|
+
const told = new Set<string>();
|
|
280
|
+
|
|
281
|
+
/** 장면 build 에서 부른다 — 장면을 덮는 캔버스를 짓고 (t) => 칠하기를 돌려준다. t 는 장면 안 시각.
|
|
282
|
+
* 한 장면에서 플랜을 바꿔 끼우려면(드롭부터 큰 플랜) 층을 둘 짓고 안 쓰는 쪽 .canvas 를 숨긴다 */
|
|
283
|
+
export function jizuraLayer(el: HTMLElement, o: LayerOptions): JizuraPaint {
|
|
284
|
+
const { ctx } = o;
|
|
285
|
+
const mode = o.mode ?? "transparent";
|
|
286
|
+
const canvas = document.createElement("canvas");
|
|
287
|
+
canvas.width = ctx.meta.width;
|
|
288
|
+
canvas.height = ctx.meta.height;
|
|
289
|
+
canvas.dataset.kfLyrics = "jizura";
|
|
290
|
+
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none";
|
|
291
|
+
el.append(canvas);
|
|
292
|
+
const g = canvas.getContext("2d")!;
|
|
293
|
+
let entry: PlanEntry | null = null;
|
|
294
|
+
let failed: unknown = null;
|
|
295
|
+
const ready = planFor(o).then(
|
|
296
|
+
(e) => (entry = e),
|
|
297
|
+
(err) => {
|
|
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);
|
|
303
|
+
return null;
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
let want: number | null = null;
|
|
307
|
+
let drawn: number | null = null;
|
|
308
|
+
const paint = (T: number) => {
|
|
309
|
+
if (!entry) return;
|
|
310
|
+
entry.R.frame(g, entry.plan, T, drawOpts(mode, canvas.width / entry.plan.W));
|
|
311
|
+
drawn = T;
|
|
312
|
+
};
|
|
313
|
+
// 플랜이 아직이면 그 seek 을 플랜이 설 때까지 붙든다 — 첫 프레임부터 가사가 찍힌다. 플랜을 못 세웠으면 그 프레임을 가사 없이 찍지 않고 던진다
|
|
314
|
+
onSeek(async () => {
|
|
315
|
+
if (want === null || drawn === want) return;
|
|
316
|
+
await ready;
|
|
317
|
+
if (failed) throw failed;
|
|
318
|
+
paint(want);
|
|
319
|
+
});
|
|
320
|
+
const draw = (t: number) => {
|
|
321
|
+
want = ctx.from + t;
|
|
322
|
+
paint(want);
|
|
323
|
+
};
|
|
324
|
+
return Object.assign(draw, { canvas });
|
|
325
|
+
}
|