kasaframe 0.0.0-stage → 0.1.1

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.
Files changed (338) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +146 -0
  3. package/README.md +77 -2
  4. package/app/package.json +12 -0
  5. package/app/server/engine.ts +415 -0
  6. package/app/server/main.ts +205 -0
  7. package/app/web/env.d.ts +2 -0
  8. package/app/web/icons.ts +31 -0
  9. package/app/web/index.html +21 -0
  10. package/app/web/main.ts +1013 -0
  11. package/app/web/style.css +430 -0
  12. package/bin/hooks.mjs +51 -0
  13. package/bin/kf.mjs +14 -0
  14. package/docs/AGENTS.md +238 -0
  15. package/music/README.md +13 -0
  16. package/package.json +44 -4
  17. package/packages/adapters/package.json +36 -0
  18. package/packages/adapters/src/css.ts +11 -0
  19. package/packages/adapters/src/gsap.ts +26 -0
  20. package/packages/adapters/src/index.ts +9 -0
  21. package/packages/adapters/src/lottie.ts +22 -0
  22. package/packages/adapters/src/media.ts +59 -0
  23. package/packages/adapters/src/page.ts +229 -0
  24. package/packages/adapters/src/project.ts +214 -0
  25. package/packages/adapters/src/react.ts +36 -0
  26. package/packages/adapters/src/svelte.ts +22 -0
  27. package/packages/adapters/src/three.ts +18 -0
  28. package/packages/adapters/src/vue.ts +27 -0
  29. package/packages/check/package.json +19 -0
  30. package/packages/check/src/check.ts +273 -0
  31. package/packages/check/src/cli.ts +70 -0
  32. package/packages/check/src/image.ts +146 -0
  33. package/packages/check/src/index.ts +9 -0
  34. package/packages/check/src/ocr.swift +37 -0
  35. package/packages/check/src/probe.ts +328 -0
  36. package/packages/check/src/report.ts +44 -0
  37. package/packages/check/src/rules/assets.ts +42 -0
  38. package/packages/check/src/rules/beats.ts +212 -0
  39. package/packages/check/src/rules/camera.ts +217 -0
  40. package/packages/check/src/rules/captions.ts +49 -0
  41. package/packages/check/src/rules/flash.ts +235 -0
  42. package/packages/check/src/rules/frames.ts +135 -0
  43. package/packages/check/src/rules/layout.ts +257 -0
  44. package/packages/check/src/rules/page.ts +87 -0
  45. package/packages/check/src/rules/runs.ts +20 -0
  46. package/packages/check/src/rules/text.ts +326 -0
  47. package/packages/check/src/scan.ts +240 -0
  48. package/packages/check/src/serve.ts +67 -0
  49. package/packages/check/src/types.ts +80 -0
  50. package/packages/check/src/video.ts +137 -0
  51. package/packages/cli/package.json +29 -0
  52. package/packages/cli/src/blur.ts +68 -0
  53. package/packages/cli/src/browser.ts +203 -0
  54. package/packages/cli/src/command.ts +44 -0
  55. package/packages/cli/src/commands/app.ts +25 -0
  56. package/packages/cli/src/commands/beats.ts +36 -0
  57. package/packages/cli/src/commands/blender.ts +270 -0
  58. package/packages/cli/src/commands/check.ts +25 -0
  59. package/packages/cli/src/commands/doctor.ts +22 -0
  60. package/packages/cli/src/commands/gen.ts +173 -0
  61. package/packages/cli/src/commands/look.ts +39 -0
  62. package/packages/cli/src/commands/lyrics.ts +111 -0
  63. package/packages/cli/src/commands/make.ts +542 -0
  64. package/packages/cli/src/commands/new.ts +176 -0
  65. package/packages/cli/src/commands/preview.ts +43 -0
  66. package/packages/cli/src/commands/render.ts +216 -0
  67. package/packages/cli/src/commands/setup.ts +63 -0
  68. package/packages/cli/src/commands/sheet.ts +114 -0
  69. package/packages/cli/src/commands/skill.ts +43 -0
  70. package/packages/cli/src/commands/still.ts +40 -0
  71. package/packages/cli/src/commands/theme.ts +85 -0
  72. package/packages/cli/src/commands/transitions.ts +40 -0
  73. package/packages/cli/src/deps.ts +122 -0
  74. package/packages/cli/src/ffmpeg.ts +81 -0
  75. package/packages/cli/src/index.ts +8 -0
  76. package/packages/cli/src/kf.ts +53 -0
  77. package/packages/cli/src/lock.ts +72 -0
  78. package/packages/cli/src/out.ts +13 -0
  79. package/packages/cli/src/serve.ts +124 -0
  80. package/packages/clock/package.json +14 -0
  81. package/packages/clock/src/index.ts +25 -0
  82. package/packages/clock/src/install.ts +351 -0
  83. package/packages/clock/src/types.ts +45 -0
  84. package/packages/core/package.json +9 -0
  85. package/packages/core/src/beats.ts +283 -0
  86. package/packages/core/src/ease.ts +87 -0
  87. package/packages/core/src/frame.ts +99 -0
  88. package/packages/core/src/index.ts +27 -0
  89. package/packages/core/src/project.ts +146 -0
  90. package/packages/core/src/rand.ts +56 -0
  91. package/packages/core/src/spring.ts +52 -0
  92. package/packages/core/src/style.ts +100 -0
  93. package/packages/core/src/time.ts +88 -0
  94. package/packages/gen/PROVIDERS.md +55 -0
  95. package/packages/gen/package.json +9 -0
  96. package/packages/gen/src/generate.ts +204 -0
  97. package/packages/gen/src/image/og.ts +133 -0
  98. package/packages/gen/src/index.ts +18 -0
  99. package/packages/gen/src/keys.ts +60 -0
  100. package/packages/gen/src/media.ts +150 -0
  101. package/packages/gen/src/plan.ts +106 -0
  102. package/packages/gen/src/registry.ts +51 -0
  103. package/packages/gen/src/seam.ts +103 -0
  104. package/packages/gen/src/types.ts +86 -0
  105. package/packages/gen/src/video/fal.ts +140 -0
  106. package/packages/gen/src/video/h3.ts +107 -0
  107. package/packages/gen/src/video/http.ts +44 -0
  108. package/packages/gen/src/video/ltx.ts +60 -0
  109. package/packages/gen/src/video/minimax.ts +55 -0
  110. package/packages/gen/src/video/veo.ts +71 -0
  111. package/packages/jizura/package.json +16 -0
  112. package/packages/jizura/src/deny.ts +73 -0
  113. package/packages/jizura/src/index.ts +7 -0
  114. package/packages/jizura/src/install.ts +226 -0
  115. package/packages/jizura/src/layer.ts +282 -0
  116. package/packages/jizura/src/lyrics.ts +81 -0
  117. package/packages/jizura/src/pin.ts +5 -0
  118. package/packages/jizura/src/vite.ts +36 -0
  119. package/packages/motion/README.md +142 -0
  120. package/packages/motion/package.json +29 -0
  121. package/packages/motion/src/camera.ts +97 -0
  122. package/packages/motion/src/captions.ts +479 -0
  123. package/packages/motion/src/enter.ts +128 -0
  124. package/packages/motion/src/hangul.ts +129 -0
  125. package/packages/motion/src/impact.ts +100 -0
  126. package/packages/motion/src/index.ts +17 -0
  127. package/packages/motion/src/moves/brushWipe.ts +90 -0
  128. package/packages/motion/src/moves/cloneArray.ts +111 -0
  129. package/packages/motion/src/moves/curtains.ts +81 -0
  130. package/packages/motion/src/moves/echoStack.ts +70 -0
  131. package/packages/motion/src/moves/fit.ts +34 -0
  132. package/packages/motion/src/moves/flood.ts +61 -0
  133. package/packages/motion/src/moves/ghostStack.ts +79 -0
  134. package/packages/motion/src/moves/gridFloor.ts +100 -0
  135. package/packages/motion/src/moves/imageSwarm.ts +109 -0
  136. package/packages/motion/src/moves/index.ts +19 -0
  137. package/packages/motion/src/moves/irisBlades.ts +113 -0
  138. package/packages/motion/src/moves/karaokeLine.ts +94 -0
  139. package/packages/motion/src/moves/ledBoard.ts +94 -0
  140. package/packages/motion/src/moves/liquidTab.ts +72 -0
  141. package/packages/motion/src/moves/markerWrite.ts +63 -0
  142. package/packages/motion/src/moves/morphBox.ts +96 -0
  143. package/packages/motion/src/moves/neonSign.ts +77 -0
  144. package/packages/motion/src/moves/panelPop.ts +78 -0
  145. package/packages/motion/src/moves/paperTear.ts +75 -0
  146. package/packages/motion/src/moves/path.ts +64 -0
  147. package/packages/motion/src/moves/pathText.ts +107 -0
  148. package/packages/motion/src/moves/pointSphere.ts +109 -0
  149. package/packages/motion/src/moves/ransomLetters.ts +73 -0
  150. package/packages/motion/src/moves/shapeMorph.ts +143 -0
  151. package/packages/motion/src/moves/slamWords.ts +89 -0
  152. package/packages/motion/src/moves/sparkTrail.ts +114 -0
  153. package/packages/motion/src/moves/speedLines.ts +91 -0
  154. package/packages/motion/src/moves/splitFlap.ts +147 -0
  155. package/packages/motion/src/moves/stepWords.ts +117 -0
  156. package/packages/motion/src/moves/strikeOut.ts +98 -0
  157. package/packages/motion/src/moves/sunburst.ts +58 -0
  158. package/packages/motion/src/moves/tickerRows.ts +87 -0
  159. package/packages/motion/src/moves/tokenPicker.ts +114 -0
  160. package/packages/motion/src/moves/truchet.ts +85 -0
  161. package/packages/motion/src/moves/wordWall.ts +76 -0
  162. package/packages/motion/src/moves/worldRoll.ts +31 -0
  163. package/packages/motion/src/particles.ts +102 -0
  164. package/packages/motion/src/react/index.tsx +21 -0
  165. package/packages/motion/src/react/parts.tsx +431 -0
  166. package/packages/motion/src/react/time.tsx +97 -0
  167. package/packages/motion/src/text.ts +103 -0
  168. package/packages/motion/src/transition.ts +82 -0
  169. package/packages/motion/src/transition3d/cards.ts +65 -0
  170. package/packages/motion/src/transition3d/chain.ts +84 -0
  171. package/packages/motion/src/transition3d/dive.ts +181 -0
  172. package/packages/motion/src/transition3d/fold.ts +110 -0
  173. package/packages/motion/src/transition3d/gl.ts +219 -0
  174. package/packages/motion/src/transition3d/index.ts +228 -0
  175. package/packages/motion/src/transition3d/live.ts +70 -0
  176. package/packages/motion/src/transition3d/portal.ts +115 -0
  177. package/packages/motion/src/transition3d/react.tsx +29 -0
  178. package/packages/motion/src/transition3d/rig.ts +502 -0
  179. package/packages/motion/src/transition3d/shatter.ts +101 -0
  180. package/packages/motion/src/transition3d/whip.ts +65 -0
  181. package/packages/motion/src/transition3d/world.ts +683 -0
  182. package/packages/motion/src/transition3d/zoomOut.ts +114 -0
  183. package/packages/motion/src/transitions2d/catalog.ts +127 -0
  184. package/packages/motion/src/transitions2d/css.d.ts +2 -0
  185. package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
  186. package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
  187. package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
  188. package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
  189. package/packages/motion/src/transitions2d/index.ts +2 -0
  190. package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
  191. package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
  192. package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
  193. package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
  194. package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
  195. package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
  196. package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
  197. package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
  198. package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
  199. package/packages/motion/src/transitions2d/react.ts +13 -0
  200. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
  201. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
  202. package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
  203. package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
  204. package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
  205. package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
  206. package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
  207. package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
  208. package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
  209. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
  210. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
  211. package/packages/motion/src/type.ts +131 -0
  212. package/packages/motion/src/ui.ts +172 -0
  213. package/packages/plan/package.json +15 -0
  214. package/packages/plan/src/brief.ts +254 -0
  215. package/packages/plan/src/captions.ts +75 -0
  216. package/packages/plan/src/detect.ts +93 -0
  217. package/packages/plan/src/index.ts +23 -0
  218. package/packages/plan/src/layout.ts +176 -0
  219. package/packages/plan/src/llm.ts +110 -0
  220. package/packages/plan/src/look.ts +220 -0
  221. package/packages/plan/src/plan.ts +125 -0
  222. package/packages/plan/src/request.ts +55 -0
  223. package/packages/plan/src/studio.ts +53 -0
  224. package/packages/plan/src/templates.ts +42 -0
  225. package/packages/plan/src/themes.ts +48 -0
  226. package/packages/plan/src/types.ts +106 -0
  227. package/packages/studio/package.json +12 -0
  228. package/packages/studio/src/index.ts +1 -0
  229. package/packages/studio/src/plugin.ts +236 -0
  230. package/packages/studio/src/ui/main.ts +503 -0
  231. package/packages/studio/src/ui/studio.css +165 -0
  232. package/packages/studio/src/ui/timeline.ts +317 -0
  233. package/packages/themes/kit/THEME-MAKE.md +68 -0
  234. package/packages/themes/kit/THEME.md +30 -0
  235. package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
  236. package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
  237. package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
  238. package/packages/themes/kit/effects.ts +5 -0
  239. package/packages/themes/kit/sample/index.html +11 -0
  240. package/packages/themes/kit/sample/main.ts +9 -0
  241. package/packages/themes/kit/sample/scenes/scene.ts +15 -0
  242. package/packages/themes/package.json +16 -0
  243. package/packages/themes/src/factory.ts +296 -0
  244. package/packages/themes/src/fx.ts +14 -0
  245. package/packages/themes/src/index.ts +105 -0
  246. package/packages/themes/src/stage3d.ts +177 -0
  247. package/packages/themes/src/types.ts +58 -0
  248. package/presets/kawaii-pastel.json +94 -0
  249. package/presets/neutral-dark.json +82 -0
  250. package/presets/og.json +98 -0
  251. package/skills/kasaframe/SKILL.md +32 -0
  252. package/templates/_shared/project/index.html +11 -0
  253. package/templates/_shared/project/lib/react.ts +22 -0
  254. package/templates/_shared/project/lib/stage.ts +272 -0
  255. package/templates/_shared/project/main.ts +9 -0
  256. package/templates/_shared/scenes/default.ts +18 -0
  257. package/templates/_shared/scenes/drop.ts +30 -0
  258. package/templates/_shared/scenes/end.ts +23 -0
  259. package/templates/_shared/scenes/hook.ts +16 -0
  260. package/templates/_shared/scenes/seam.ts +41 -0
  261. package/templates/_shared/scenes/step.ts +40 -0
  262. package/templates/ad/preview.png +0 -0
  263. package/templates/ad/template.json +132 -0
  264. package/templates/demo/preview.png +0 -0
  265. package/templates/demo/template.json +130 -0
  266. package/templates/explainer/preview.png +0 -0
  267. package/templates/explainer/template.json +142 -0
  268. package/templates/launch/preview.png +0 -0
  269. package/templates/launch/template.json +130 -0
  270. package/templates/mv/preview.png +0 -0
  271. package/templates/mv/scenes/default.ts +95 -0
  272. package/templates/mv/template.json +176 -0
  273. package/templates/promo/preview.png +0 -0
  274. package/templates/promo/template.json +144 -0
  275. package/templates/shorts/preview.png +0 -0
  276. package/templates/shorts/scenes/hook.ts +34 -0
  277. package/templates/shorts/template.json +127 -0
  278. package/templates/square/preview.png +0 -0
  279. package/templates/square/template.json +113 -0
  280. package/themes/debug-hud/THEME.md +39 -0
  281. package/themes/debug-hud/effects.ts +389 -0
  282. package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
  283. package/themes/debug-hud/sample/beats.json +1 -0
  284. package/themes/debug-hud/sample/index.html +11 -0
  285. package/themes/debug-hud/sample/kf.json +59 -0
  286. package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
  287. package/themes/debug-hud/sample/lib/og.ts +29 -0
  288. package/themes/debug-hud/sample/main.ts +9 -0
  289. package/themes/debug-hud/sample/scenes/end.ts +62 -0
  290. package/themes/debug-hud/sample/scenes/gather.ts +67 -0
  291. package/themes/debug-hud/sample/scenes/models.ts +70 -0
  292. package/themes/debug-hud/sample/scenes/price.ts +79 -0
  293. package/themes/debug-hud/sample/style.json +91 -0
  294. package/themes/debug-hud/still.png +0 -0
  295. package/themes/debug-hud/style.json +91 -0
  296. package/themes/debug-hud/theme.json +68 -0
  297. package/themes/og-measure/THEME.md +47 -0
  298. package/themes/og-measure/effects.ts +432 -0
  299. package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
  300. package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
  301. package/themes/og-measure/sample/assets/data/measure.json +353 -0
  302. package/themes/og-measure/sample/beats.json +1 -0
  303. package/themes/og-measure/sample/index.html +11 -0
  304. package/themes/og-measure/sample/kf.json +66 -0
  305. package/themes/og-measure/sample/lib/data.ts +62 -0
  306. package/themes/og-measure/sample/main.ts +9 -0
  307. package/themes/og-measure/sample/measure.ts +78 -0
  308. package/themes/og-measure/sample/scenes/end.ts +47 -0
  309. package/themes/og-measure/sample/scenes/gather.ts +51 -0
  310. package/themes/og-measure/sample/scenes/models.ts +34 -0
  311. package/themes/og-measure/sample/scenes/one.ts +39 -0
  312. package/themes/og-measure/sample/scenes/price.ts +56 -0
  313. package/themes/og-measure/sample/style.json +97 -0
  314. package/themes/og-measure/still.png +0 -0
  315. package/themes/og-measure/style.json +97 -0
  316. package/themes/og-measure/theme.json +79 -0
  317. package/themes/riso-zine/THEME.md +43 -0
  318. package/themes/riso-zine/effects.ts +321 -0
  319. package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
  320. package/themes/riso-zine/sample/beats.json +1 -0
  321. package/themes/riso-zine/sample/index.html +11 -0
  322. package/themes/riso-zine/sample/kf.json +59 -0
  323. package/themes/riso-zine/sample/lib/og.ts +29 -0
  324. package/themes/riso-zine/sample/lib/shot.ts +86 -0
  325. package/themes/riso-zine/sample/lib/zine.ts +42 -0
  326. package/themes/riso-zine/sample/main.ts +9 -0
  327. package/themes/riso-zine/sample/scenes/end.ts +61 -0
  328. package/themes/riso-zine/sample/scenes/gather.ts +62 -0
  329. package/themes/riso-zine/sample/scenes/models.ts +52 -0
  330. package/themes/riso-zine/sample/scenes/price.ts +82 -0
  331. package/themes/riso-zine/sample/style.json +99 -0
  332. package/themes/riso-zine/still.png +0 -0
  333. package/themes/riso-zine/style.json +99 -0
  334. package/themes/riso-zine/theme.json +75 -0
  335. package/tools/beats/beats.py +372 -0
  336. package/tools/beats/requirements.txt +3 -0
  337. package/tools/lyrics/align.py +164 -0
  338. package/tools/lyrics/requirements.txt +9 -0
@@ -0,0 +1,683 @@
1
+ import * as THREE from "three";
2
+ import { FIT, FOV, type Pose } from "./rig.ts";
3
+
4
+ /** 장면 화면을 t 로 그리는 함수 — 같은 t 면 같은 그림이어야 한다 */
5
+ export type Painter = (g: CanvasRenderingContext2D, t: number, w: number, h: number) => void;
6
+
7
+ /** 시각마다 텍스처로 올리는 살아 있는 DOM. build 는 장면 build 와 같은 꼴(root 에 한 번 짓고 t 로 그리는 함수를 돌려준다) */
8
+ export interface LiveDom {
9
+ dom(root: HTMLElement): ((t: number) => void) | void;
10
+ /** 전환 안 시각 → 이 화면의 시각. 기본은 그대로 */
11
+ time?: (t: number) => number;
12
+ /** HTML-in-Canvas 가 없는 브라우저(플래그 없는 크롬)에서 대신 쓸 화면 */
13
+ still?: Source;
14
+ }
15
+
16
+ /** <video> — data-kf-from 처럼 영상 전체 기준 from 초에 0 초부터 튼다. 엔진 media 어댑터가 시각을 맞춘다 */
17
+ export interface VideoSource {
18
+ video: string;
19
+ from?: number;
20
+ rate?: number;
21
+ loop?: boolean;
22
+ }
23
+
24
+ /** 앞·뒤 장면 화면. 주소(kf still PNG 등)·그림·캔버스·그리기 함수·영상·살아 있는 DOM */
25
+ export type Source = string | HTMLImageElement | HTMLCanvasElement | Painter | VideoSource | LiveDom;
26
+
27
+ export interface Palette {
28
+ bg: string;
29
+ deep: string;
30
+ accent: string;
31
+ glow: string;
32
+ ink: string;
33
+ }
34
+
35
+ export interface Screen {
36
+ texture: THREE.Texture;
37
+ /** dom·video 는 seek 의 그리기 단계 뒤(영상 맞춤·페인트 뒤)에 텍스처를 다시 올려야 한다 */
38
+ live: "dom" | "video" | null;
39
+ /** 받는 중인 그림이 있으면 다 받을 때까지 */
40
+ loaded(): Promise<void> | null;
41
+ /** 흐린 꾸밈용 — 밉맵을 단 같은 그림 */
42
+ soft(): THREE.Texture;
43
+ /** 그리기 함수·살아 있는 DOM 을 t 로 */
44
+ update(t: number): void;
45
+ /** 영상 프레임·DOM 스냅숏을 텍스처로. DOM 은 페인트 이벤트 안에서 불러야 이번 프레임 그림이다 */
46
+ refresh(): void;
47
+ /** 살아 있는 DOM 을 이번 프레임에 스냅숏할지 */
48
+ show(on: boolean): void;
49
+ /** 살아 있는 DOM 화면의 상자(공유 캔버스 자식) */
50
+ box?: HTMLElement;
51
+ dispose(): void;
52
+ }
53
+
54
+ let shared: THREE.WebGLRenderer | null = null;
55
+
56
+ // 크롬은 한 페이지에 WebGL 문맥을 16 개쯤만 살려 둔다. 전환이 여럿인 영상에서도 하나를 돌려 쓰고, 그리는 장면으로 캔버스를 옮긴다.
57
+ // 살아 있는 DOM 화면은 이 캔버스의 자식이어야 텍스처로 올릴 수 있다(layoutSubtree).
58
+ export function sharedRenderer(w: number, h: number): THREE.WebGLRenderer {
59
+ if (!shared) {
60
+ shared = new THREE.WebGLRenderer({ antialias: true, stencil: true, preserveDrawingBuffer: true, alpha: false, powerPreference: "high-performance" });
61
+ shared.setPixelRatio(1);
62
+ shared.outputColorSpace = THREE.SRGBColorSpace;
63
+ shared.autoClear = false;
64
+ shared.domElement.dataset.kfTransition3d = "";
65
+ shared.domElement.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block";
66
+ if (elementTextures()) (shared.domElement as HTMLCanvasElement & { layoutSubtree: boolean }).layoutSubtree = true;
67
+ }
68
+ const size = shared.getSize(new THREE.Vector2());
69
+ if (size.x !== w || size.y !== h) shared.setSize(w, h, false);
70
+ return shared;
71
+ }
72
+
73
+ /** HTML-in-Canvas(크롬 --enable-features=CanvasDrawElement) — DOM 을 그대로 WebGL 텍스처로 */
74
+ export function elementTextures(): boolean {
75
+ return typeof (globalThis.WebGL2RenderingContext?.prototype as { texElementImage2D?: unknown } | undefined)?.texElementImage2D === "function" && "requestPaint" in HTMLCanvasElement.prototype;
76
+ }
77
+
78
+ type PaintCanvas = HTMLCanvasElement & { requestPaint(): void };
79
+
80
+ /** 다음 렌더링 갱신의 paint 이벤트 안에서 fn — 자식 DOM 스냅숏이 이번 DOM 상태로 찍힌 뒤다. 가상 시계와 무관한 진짜 타이머로 끝을 막는다 */
81
+ export function inPaint(canvas: HTMLCanvasElement, fn: () => void): Promise<void> {
82
+ const realTimeout = (window as { __kfClock?: { real: { setTimeout: typeof setTimeout } } }).__kfClock?.real.setTimeout ?? setTimeout;
83
+ return new Promise((resolve) => {
84
+ let done = false;
85
+ const finish = () => {
86
+ if (done) return;
87
+ done = true;
88
+ canvas.removeEventListener("paint", run);
89
+ fn();
90
+ resolve();
91
+ };
92
+ const run = () => finish();
93
+ canvas.addEventListener("paint", run);
94
+ (canvas as PaintCanvas).requestPaint();
95
+ realTimeout(() => {
96
+ if (!done) console.warn("[kf] 3D 전환: paint 이벤트가 오지 않아 지난 DOM 스냅숏으로 그린다");
97
+ finish();
98
+ }, 3000);
99
+ });
100
+ }
101
+
102
+ // 페이지 ready 는 문서 안 <img> 를 기다린다. 없는 파일을 <img> 로 부르면 404 가 콘솔 오류로 찍혀 검사가 실패로 친다 —
103
+ // 개발 서버는 없는 경로에 index.html 을 200 으로 주므로 content-type 으로 가린다.
104
+ function urlExists(url: string, kind: "image/" | "video/"): boolean {
105
+ if (url.startsWith("data:") || url.startsWith("blob:")) return true;
106
+ const x = new XMLHttpRequest();
107
+ x.open("HEAD", url, false);
108
+ try {
109
+ x.send();
110
+ } catch {
111
+ return false;
112
+ }
113
+ return x.status === 200 && (x.getResponseHeader("content-type") ?? "").startsWith(kind);
114
+ }
115
+
116
+ function setup(tex: THREE.Texture): THREE.Texture {
117
+ // 텍셀을 그대로 내보낸다 — 색 공간을 오가면 경계 프레임이 앞·뒤 장면과 한두 단계 어긋난다
118
+ tex.colorSpace = THREE.NoColorSpace;
119
+ tex.minFilter = THREE.LinearFilter;
120
+ tex.magFilter = THREE.LinearFilter;
121
+ tex.generateMipmaps = false;
122
+ tex.wrapS = tex.wrapT = THREE.ClampToEdgeWrapping;
123
+ return tex;
124
+ }
125
+
126
+ function canvasOf(w: number, h: number): { canvas: HTMLCanvasElement; g: CanvasRenderingContext2D } {
127
+ const canvas = document.createElement("canvas");
128
+ canvas.width = w;
129
+ canvas.height = h;
130
+ return { canvas, g: canvas.getContext("2d")! };
131
+ }
132
+
133
+ export interface ScreenEnv {
134
+ /** 그림·영상 요소를 둘 곳(장면 안) */
135
+ host: HTMLElement;
136
+ w: number;
137
+ h: number;
138
+ renderer: THREE.WebGLRenderer;
139
+ /** 영상 from 기본값 — 이 장면이 시작하는 영상 전체 기준 초 */
140
+ sceneFrom: number;
141
+ }
142
+
143
+ const isVideo = (s: unknown): s is VideoSource => !!s && typeof s === "object" && typeof (s as VideoSource).video === "string";
144
+ const isLive = (s: unknown): s is LiveDom => !!s && typeof s === "object" && typeof (s as LiveDom).dom === "function";
145
+
146
+ // 같은 주소의 그림은 전환이 몇 개든 텍스처 하나 — 전환 백여 개가 그림 두 장을 함께 쓰면 GPU 메모리가 기가바이트로 불어난다
147
+ const imageScreens = new Map<string, { screen: Screen; refs: number }>();
148
+
149
+ export function makeScreen(src: Source | undefined, fallback: Painter, env: ScreenEnv): Screen {
150
+ if (isLive(src)) return elementTextures() ? domScreen(src, env) : makeScreen(src.still, fallback, env);
151
+ if (isVideo(src)) return urlExists(src.video, "video/") ? videoScreen(src, env) : makeScreen(undefined, fallback, env);
152
+ if (typeof src === "string" && urlExists(src, "image/")) {
153
+ const key = `${src}|${env.w}x${env.h}`;
154
+ let hit = imageScreens.get(key);
155
+ if (!hit) {
156
+ const screen = stillScreen(src, fallback, env);
157
+ const dispose = screen.dispose;
158
+ hit = { screen: { ...screen, dispose: () => {} }, refs: 0 };
159
+ const entry = hit;
160
+ entry.screen.dispose = () => {
161
+ if (--entry.refs > 0) return;
162
+ imageScreens.delete(key);
163
+ dispose();
164
+ };
165
+ imageScreens.set(key, hit);
166
+ }
167
+ hit.refs++;
168
+ return hit.screen;
169
+ }
170
+ // 없는 그림 주소는 부르지 않는다(대체 화면)
171
+ return stillScreen(typeof src === "string" ? undefined : src, fallback, env);
172
+ }
173
+
174
+ function stillScreen(src: Source | undefined, fallback: Painter, env: ScreenEnv): Screen {
175
+ const { host, w, h } = env;
176
+ let image: TexImageSource | HTMLCanvasElement;
177
+ let painter: Painter | null = null;
178
+ let pending: HTMLImageElement | null = null;
179
+ let live = false;
180
+ let g: CanvasRenderingContext2D | null = null;
181
+ const useFallback = () => {
182
+ const c = canvasOf(w, h);
183
+ painter = fallback;
184
+ g = c.g;
185
+ return c.canvas;
186
+ };
187
+ if (typeof src === "function") {
188
+ const c = canvasOf(w, h);
189
+ painter = src;
190
+ g = c.g;
191
+ image = c.canvas;
192
+ } else if (typeof src === "string") {
193
+ const img = document.createElement("img");
194
+ img.src = src;
195
+ img.decoding = "sync";
196
+ img.style.display = "none";
197
+ host.append(img);
198
+ pending = img;
199
+ image = useFallback();
200
+ } else if (src instanceof HTMLImageElement) {
201
+ pending = src;
202
+ image = useFallback();
203
+ } else if (src instanceof HTMLCanvasElement) {
204
+ image = src;
205
+ live = true;
206
+ } else image = useFallback();
207
+
208
+ const texture = setup(new THREE.Texture(image as HTMLCanvasElement));
209
+ texture.needsUpdate = true;
210
+ let softTex: THREE.Texture | null = null;
211
+ const touch = () => {
212
+ texture.needsUpdate = true;
213
+ if (softTex) {
214
+ softTex.image = texture.image;
215
+ softTex.needsUpdate = true;
216
+ }
217
+ };
218
+ return {
219
+ texture,
220
+ live: null,
221
+ loaded() {
222
+ return pending && !(pending.complete && pending.naturalWidth > 0) ? pending.decode().catch(() => {}) : null;
223
+ },
224
+ soft() {
225
+ if (!softTex) {
226
+ softTex = texture.clone();
227
+ softTex.generateMipmaps = true;
228
+ softTex.minFilter = THREE.LinearMipmapLinearFilter;
229
+ softTex.needsUpdate = true;
230
+ }
231
+ return softTex;
232
+ },
233
+ update(t) {
234
+ if (pending && pending.complete && pending.naturalWidth > 0) {
235
+ texture.image = pending;
236
+ painter = null;
237
+ pending = null;
238
+ touch();
239
+ }
240
+ if (painter && g) {
241
+ g.save();
242
+ g.clearRect(0, 0, w, h);
243
+ painter(g, t, w, h);
244
+ g.restore();
245
+ touch();
246
+ } else if (live) touch();
247
+ },
248
+ refresh() {},
249
+ show() {},
250
+ dispose() {
251
+ texture.dispose();
252
+ softTex?.dispose();
253
+ },
254
+ };
255
+ }
256
+
257
+ function videoScreen(src: VideoSource, env: ScreenEnv): Screen {
258
+ const v = document.createElement("video");
259
+ v.src = src.video;
260
+ v.muted = true;
261
+ v.playsInline = true;
262
+ v.preload = "auto";
263
+ v.loop = !!src.loop;
264
+ v.dataset.kfFrom = String(src.from ?? env.sceneFrom);
265
+ if (src.rate !== undefined) v.dataset.kfRate = String(src.rate);
266
+ // display:none 이면 디코더가 프레임을 안 내놓는 경우가 있어 화면 밖 작은 칸에 둔다
267
+ v.style.cssText = "position:absolute;left:0;top:0;width:2px;height:2px;opacity:0;pointer-events:none";
268
+ env.host.append(v);
269
+ const texture = setup(new THREE.Texture(v as unknown as HTMLCanvasElement));
270
+ return {
271
+ texture,
272
+ live: "video",
273
+ loaded: () => null,
274
+ soft: () => texture,
275
+ update() {},
276
+ refresh() {
277
+ if (v.readyState < 2) return;
278
+ // three 는 <video> 크기를 width·height 속성에서 읽는다(기본 0 이면 텍스처를 0 크기로 잡는다). 화면 배치는 CSS 가 쥔다
279
+ if (v.width !== v.videoWidth) v.width = v.videoWidth;
280
+ if (v.height !== v.videoHeight) v.height = v.videoHeight;
281
+ texture.needsUpdate = true;
282
+ },
283
+ show() {},
284
+ dispose() {
285
+ texture.dispose();
286
+ v.remove();
287
+ },
288
+ };
289
+ }
290
+
291
+ // 살아 있는 DOM 화면들 — 공유 캔버스 자식으로 두고, 이번 프레임에 쓰는 것만 보이게 해 스냅숏 비용을 줄인다
292
+ function domScreen(src: LiveDom, env: ScreenEnv): Screen {
293
+ const { w, h, renderer } = env;
294
+ const canvas = renderer.domElement;
295
+ const box = document.createElement("div");
296
+ box.dataset.kfLive = "";
297
+ box.setAttribute("aria-hidden", "true");
298
+ box.style.cssText = `position:relative;width:${w}px;height:${h}px;overflow:hidden;background:#000;display:none`;
299
+ canvas.append(box);
300
+ const draw = src.dom(box) ?? (() => {});
301
+ const gl = renderer.getContext() as WebGL2RenderingContext & { texElementImage2D(target: number, internalformat: number, el: Element): void };
302
+ const glTex = gl.createTexture()!;
303
+ gl.bindTexture(gl.TEXTURE_2D, glTex);
304
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
305
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
306
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
307
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
308
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
309
+ renderer.resetState();
310
+ const texture = new THREE.ExternalTexture(glTex);
311
+ return {
312
+ texture,
313
+ box,
314
+ live: "dom",
315
+ loaded: () => null,
316
+ soft: () => texture,
317
+ update(t) {
318
+ if (box.parentNode !== canvas) canvas.append(box);
319
+ draw(src.time ? src.time(t) : t);
320
+ },
321
+ refresh() {
322
+ if (box.style.display === "none") return;
323
+ gl.bindTexture(gl.TEXTURE_2D, glTex);
324
+ // three 의 그림 텍스처와 같은 방향(위가 v=1)
325
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
326
+ try {
327
+ gl.texElementImage2D(gl.TEXTURE_2D, gl.RGBA8, box);
328
+ } catch {
329
+ // 아직 첫 스냅숏 전 — 다음 페인트에서 올린다
330
+ }
331
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
332
+ renderer.resetState();
333
+ },
334
+ show(on) {
335
+ const d = on ? "block" : "none";
336
+ if (box.style.display !== d) box.style.display = d;
337
+ },
338
+ dispose() {
339
+ gl.deleteTexture(glTex);
340
+ box.remove();
341
+ },
342
+ };
343
+ }
344
+
345
+ /** 받은 그림이 아직 없을 때(kf still 전) 서는 화면 — 스타일 색으로 앞·뒤를 갈라 그린다 */
346
+ export function placeholder(pal: Palette, side: 0 | 1): Painter {
347
+ return (g, _t, w, h) => {
348
+ const bg = g.createLinearGradient(0, 0, w, h);
349
+ bg.addColorStop(0, side ? pal.deep : pal.bg);
350
+ bg.addColorStop(1, side ? pal.bg : pal.deep);
351
+ g.fillStyle = bg;
352
+ g.fillRect(0, 0, w, h);
353
+ const k = h / 1080;
354
+ g.globalAlpha = 0.9;
355
+ g.fillStyle = pal.accent;
356
+ if (side === 0) {
357
+ g.beginPath();
358
+ g.arc(w * 0.68, h * 0.46, 210 * k, 0, Math.PI * 2);
359
+ g.fill();
360
+ g.globalAlpha = 0.5;
361
+ g.fillStyle = pal.glow;
362
+ g.fillRect(w * 0.14, h * 0.62, w * 0.3, 26 * k);
363
+ g.fillRect(w * 0.14, h * 0.68, w * 0.2, 26 * k);
364
+ } else {
365
+ g.save();
366
+ g.translate(w * 0.32, h * 0.5);
367
+ g.rotate(-0.2);
368
+ g.fillRect(-170 * k, -170 * k, 340 * k, 340 * k);
369
+ g.restore();
370
+ g.globalAlpha = 0.5;
371
+ g.fillStyle = pal.glow;
372
+ g.fillRect(w * 0.56, h * 0.36, w * 0.28, 26 * k);
373
+ g.fillRect(w * 0.56, h * 0.42, w * 0.18, 26 * k);
374
+ }
375
+ g.globalAlpha = 1;
376
+ };
377
+ }
378
+
379
+ // 화면 판 — 녹아 사라지는(dissolve) 칸과 지직거림(glitch). 두 값이 0 이면 텍셀 그대로
380
+ export const SCREEN_FRAG = /* glsl */ `
381
+ uniform sampler2D map;
382
+ uniform float dissolve;
383
+ uniform float glitch;
384
+ uniform float sheen;
385
+ uniform float tick;
386
+ uniform float aspect;
387
+ uniform float seed;
388
+ uniform vec3 edge;
389
+ varying vec2 vUv;
390
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7)) + seed * 17.13) * 43758.5453); }
391
+ void main() {
392
+ vec2 uv = vUv;
393
+ float band = floor(uv.y * 36.0);
394
+ float h = hash(vec2(band, tick));
395
+ if (glitch > 0.0 && h > 1.0 - glitch * 0.3) uv.x += (hash(vec2(tick, band)) - 0.5) * 0.05 * glitch;
396
+ vec3 c = texture2D(map, uv).rgb;
397
+ if (glitch > 0.0) {
398
+ c.r = mix(c.r, texture2D(map, uv + vec2(0.004 * glitch, 0.0)).r, glitch);
399
+ c *= 1.0 - glitch * 0.1 * step(0.5, fract(vUv.y * 240.0));
400
+ }
401
+ vec2 g = vec2(vUv.x * aspect, vUv.y) * 30.0;
402
+ vec2 cell = floor(g);
403
+ vec2 cc = (cell + 0.5) / 30.0 - vec2(aspect * 0.5, 0.5);
404
+ float d = length(cc) / length(vec2(aspect * 0.5, 0.5));
405
+ float th = hash(cell) * 0.45 + d * 0.55;
406
+ float r = dissolve * 1.25 - 0.12;
407
+ if (th < r) discard;
408
+ if (th < r + 0.07) c = mix(c, edge, 0.85);
409
+ c += sheen * smoothstep(0.3, 1.0, 1.0 - abs(vUv.x - vUv.y * 0.6 - 0.1) * 2.5);
410
+ gl_FragColor = vec4(c, 1.0);
411
+ }`;
412
+
413
+ /** 녹아 사라지는 화면 판. dissolve·glitch·sheen 이 0 이면 텍셀 그대로 */
414
+ export function screenMaterial(map: THREE.Texture, edge: string, aspect: number, seed: number): THREE.ShaderMaterial {
415
+ return new THREE.ShaderMaterial({
416
+ vertexShader: /* glsl */ `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
417
+ fragmentShader: SCREEN_FRAG,
418
+ uniforms: {
419
+ map: { value: map },
420
+ dissolve: { value: 0 },
421
+ glitch: { value: 0 },
422
+ sheen: { value: 0 },
423
+ tick: { value: 0 },
424
+ aspect: { value: aspect },
425
+ seed: { value: seed },
426
+ edge: { value: raw(edge) },
427
+ },
428
+ });
429
+ }
430
+
431
+ /** CSS 색을 셰이더에 날것 그대로 — 우리 셰이더는 출력 색 공간 변환을 거치지 않는다 */
432
+ export const raw = (css: string): THREE.Color => new THREE.Color().setStyle(css, THREE.LinearSRGBColorSpace);
433
+
434
+ /** 종류와 상관없이 카메라에 얹는 효과 — 진입점이 프레임마다 정한다. 전환 양 끝에서는 모두 0 이라 경계 프레임이 그대로다 */
435
+ export interface CamFx {
436
+ /** 시선 방향으로 다가가는 거리(착지 넘침) */
437
+ dolly: number;
438
+ /** 화면 평면 흔들림(충격) */
439
+ shake: [number, number];
440
+ /** 더하는 기울기(라디안) */
441
+ roll: number;
442
+ /** 더하는 화각(도) — 음수면 당겨 들어간다(펀치) */
443
+ fov: number;
444
+ /** 조리개 위 자리 — 초점(시선 점)은 그대로 두고 카메라만 옮겨, 하위 프레임 평균이 피사계 심도가 된다 */
445
+ jitter: [number, number];
446
+ }
447
+
448
+ export const NO_FX: CamFx = { dolly: 0, shake: [0, 0], roll: 0, fov: 0, jitter: [0, 0] };
449
+
450
+ export function aim(cam: THREE.PerspectiveCamera, pose: Pose) {
451
+ const fx = (cam.userData.fx as CamFx | undefined) ?? NO_FX;
452
+ cam.position.set(pose.pos[0], pose.pos[1], pose.pos[2]);
453
+ cam.up.set(0, 1, 0);
454
+ cam.lookAt(pose.look[0], pose.look[1], pose.look[2]);
455
+ if (fx.jitter[0] || fx.jitter[1]) {
456
+ cam.translateX(fx.jitter[0]);
457
+ cam.translateY(fx.jitter[1]);
458
+ cam.lookAt(pose.look[0], pose.look[1], pose.look[2]);
459
+ }
460
+ const roll = (pose.roll ?? 0) + fx.roll;
461
+ if (roll) cam.rotateZ(roll);
462
+ if (fx.dolly) cam.translateZ(-fx.dolly);
463
+ if (fx.shake[0] || fx.shake[1]) {
464
+ cam.translateX(fx.shake[0]);
465
+ cam.translateY(fx.shake[1]);
466
+ }
467
+ const fov = FOV + (pose.fov ?? 0) + fx.fov;
468
+ if (cam.fov !== fov) {
469
+ cam.fov = fov;
470
+ cam.updateProjectionMatrix();
471
+ }
472
+ cam.updateMatrixWorld(true);
473
+ }
474
+
475
+ export function camera(aspect: number): THREE.PerspectiveCamera {
476
+ const cam = new THREE.PerspectiveCamera(FOV, aspect, 0.01, 200);
477
+ cam.position.set(0, 0, FIT);
478
+ cam.updateProjectionMatrix();
479
+ return cam;
480
+ }
481
+
482
+ const IMAGE_VERT = /* glsl */ `
483
+ varying vec2 vUv;
484
+ void main() {
485
+ vUv = uv;
486
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
487
+ }`;
488
+
489
+ // 장면 화면 판. 꾸밈 값이 모두 0 이면 텍셀을 그대로 낸다(경계 프레임 = 앞·뒤 장면).
490
+ const IMAGE_FRAG = /* glsl */ `
491
+ uniform sampler2D map;
492
+ uniform vec4 uvRect;
493
+ uniform float opacity;
494
+ uniform float dim;
495
+ uniform float bias;
496
+ uniform vec3 tint;
497
+ uniform float tintAmt;
498
+ uniform vec3 backColor;
499
+ varying vec2 vUv;
500
+ void main() {
501
+ vec2 uv = uvRect.xy + vUv * uvRect.zw;
502
+ vec3 c = texture2D(map, uv, bias).rgb;
503
+ c = mix(c, tint, tintAmt);
504
+ c *= 1.0 - dim;
505
+ if (!gl_FrontFacing) c = mix(c * 0.35, backColor, 0.55);
506
+ gl_FragColor = vec4(c, opacity);
507
+ }`;
508
+
509
+ export type ImageMaterial = THREE.ShaderMaterial & {
510
+ uniforms: {
511
+ map: { value: THREE.Texture };
512
+ uvRect: { value: THREE.Vector4 };
513
+ opacity: { value: number };
514
+ dim: { value: number };
515
+ bias: { value: number };
516
+ tint: { value: THREE.Color };
517
+ tintAmt: { value: number };
518
+ backColor: { value: THREE.Color };
519
+ };
520
+ };
521
+
522
+ export function imageMaterial(map: THREE.Texture, o: { back?: string; double?: boolean } = {}): ImageMaterial {
523
+ return new THREE.ShaderMaterial({
524
+ vertexShader: IMAGE_VERT,
525
+ fragmentShader: IMAGE_FRAG,
526
+ uniforms: {
527
+ map: { value: map },
528
+ uvRect: { value: new THREE.Vector4(0, 0, 1, 1) },
529
+ opacity: { value: 1 },
530
+ dim: { value: 0 },
531
+ bias: { value: 0 },
532
+ tint: { value: new THREE.Color(0, 0, 0) },
533
+ tintAmt: { value: 0 },
534
+ backColor: { value: raw(o.back ?? "#223") },
535
+ },
536
+ transparent: true,
537
+ side: o.double ? THREE.DoubleSide : THREE.FrontSide,
538
+ }) as ImageMaterial;
539
+ }
540
+
541
+ // 둘레 바탕 — 위아래 그러데이션 + 한 점에 모인 빛. 큰 구 안쪽에 칠한다
542
+ const SKY_FRAG = /* glsl */ `
543
+ uniform vec3 top;
544
+ uniform vec3 bottom;
545
+ uniform vec3 glow;
546
+ uniform vec3 glowDir;
547
+ uniform float glowAmt;
548
+ uniform float glowPow;
549
+ varying vec3 vDir;
550
+ void main() {
551
+ vec3 d = normalize(vDir);
552
+ vec3 c = mix(bottom, top, smoothstep(-0.6, 0.8, d.y));
553
+ float g = pow(max(dot(d, normalize(glowDir)), 0.0), glowPow);
554
+ c += glow * g * glowAmt;
555
+ gl_FragColor = vec4(c, 1.0);
556
+ }`;
557
+
558
+ const SKY_VERT = /* glsl */ `
559
+ varying vec3 vDir;
560
+ void main() {
561
+ vDir = position;
562
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
563
+ }`;
564
+
565
+ export function sky(o: { top: string; bottom: string; glow: string; dir?: [number, number, number]; amt?: number; pow?: number; radius?: number }): THREE.Mesh<THREE.SphereGeometry, THREE.ShaderMaterial> {
566
+ const m = new THREE.Mesh(
567
+ new THREE.SphereGeometry(o.radius ?? 80, 48, 24),
568
+ new THREE.ShaderMaterial({
569
+ vertexShader: SKY_VERT,
570
+ fragmentShader: SKY_FRAG,
571
+ uniforms: {
572
+ top: { value: raw(o.top) },
573
+ bottom: { value: raw(o.bottom) },
574
+ glow: { value: raw(o.glow) },
575
+ glowDir: { value: new THREE.Vector3(...(o.dir ?? [0, 0, -1])) },
576
+ glowAmt: { value: o.amt ?? 0.6 },
577
+ glowPow: { value: o.pow ?? 6 },
578
+ },
579
+ side: THREE.BackSide,
580
+ depthWrite: false,
581
+ }),
582
+ );
583
+ m.renderOrder = -10;
584
+ return m;
585
+ }
586
+
587
+ /** 시드로 흩은 빛 점들 — 지나가는 속도감. 같은 시드면 같은 자리 */
588
+ export function dust(positions: Float32Array, color: string, size: number): THREE.Points {
589
+ const geo = new THREE.BufferGeometry();
590
+ geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
591
+ const mat = new THREE.PointsMaterial({ color: raw(color), size, sizeAttenuation: true, transparent: true, opacity: 0.85, depthWrite: false, blending: THREE.AdditiveBlending });
592
+ return new THREE.Points(geo, mat);
593
+ }
594
+
595
+ const GRID_VERT = /* glsl */ `
596
+ varying vec3 vPos;
597
+ void main() {
598
+ vec4 wp = modelMatrix * vec4(position, 1.0);
599
+ vPos = wp.xyz;
600
+ gl_Position = projectionMatrix * viewMatrix * wp;
601
+ }`;
602
+
603
+ const GRID_FRAG = /* glsl */ `
604
+ uniform vec3 color;
605
+ uniform float focus;
606
+ uniform float spread;
607
+ varying vec3 vPos;
608
+ void main() {
609
+ vec2 p = vPos.xz * 2.0;
610
+ vec2 w = fwidth(p);
611
+ vec2 l = abs(fract(p - 0.5) - 0.5) / max(w, 1e-4);
612
+ float line = 1.0 - min(min(l.x, l.y), 1.0);
613
+ float f = exp(-abs(vPos.z - focus) * 0.18) * smoothstep(spread, spread * 0.2, abs(vPos.x));
614
+ gl_FragColor = vec4(color * line * f * 0.8, 1.0);
615
+ }`;
616
+
617
+ /** 바닥·천장 빛 격자(더해 그린다) */
618
+ export function gridPlane(o: { color: string; y: number; width: number; depth: number; z: number; focus: number }): THREE.Mesh {
619
+ const m = new THREE.Mesh(
620
+ new THREE.PlaneGeometry(o.width, o.depth),
621
+ new THREE.ShaderMaterial({
622
+ vertexShader: GRID_VERT,
623
+ fragmentShader: GRID_FRAG,
624
+ uniforms: { color: { value: raw(o.color) }, focus: { value: o.focus }, spread: { value: o.width * 0.45 } },
625
+ transparent: true,
626
+ blending: THREE.AdditiveBlending,
627
+ depthWrite: false,
628
+ side: THREE.DoubleSide,
629
+ }),
630
+ );
631
+ m.rotation.x = -Math.PI / 2;
632
+ m.position.set(0, o.y, o.z);
633
+ return m;
634
+ }
635
+
636
+ /** 판 테두리 고리(가운데가 빈 사각형) */
637
+ export function frameRing(w: number, h: number, t: number): THREE.ShapeGeometry {
638
+ const s = new THREE.Shape();
639
+ s.moveTo(-w / 2 - t, -h / 2 - t);
640
+ s.lineTo(w / 2 + t, -h / 2 - t);
641
+ s.lineTo(w / 2 + t, h / 2 + t);
642
+ s.lineTo(-w / 2 - t, h / 2 + t);
643
+ s.lineTo(-w / 2 - t, -h / 2 - t);
644
+ const hole = new THREE.Path();
645
+ hole.moveTo(-w / 2, -h / 2);
646
+ hole.lineTo(-w / 2, h / 2);
647
+ hole.lineTo(w / 2, h / 2);
648
+ hole.lineTo(w / 2, -h / 2);
649
+ hole.lineTo(-w / 2, -h / 2);
650
+ s.holes.push(hole);
651
+ return new THREE.ShapeGeometry(s);
652
+ }
653
+
654
+ /** 씬 아래 모든 것을 놓아준다 */
655
+ export function disposeTree(o: THREE.Object3D) {
656
+ o.traverse((x) => {
657
+ const m = x as THREE.Mesh;
658
+ m.geometry?.dispose();
659
+ const mats = Array.isArray(m.material) ? m.material : m.material ? [m.material] : [];
660
+ for (const mat of mats) mat.dispose();
661
+ });
662
+ }
663
+
664
+ /** 종류마다 받는 재료 */
665
+ export interface Kit {
666
+ renderer: THREE.WebGLRenderer;
667
+ cam: THREE.PerspectiveCamera;
668
+ aspect: number;
669
+ w: number;
670
+ h: number;
671
+ seed: string;
672
+ pal: Palette;
673
+ from: Screen;
674
+ to: Screen;
675
+ /** 화면 모두(diveChain). 두 장면 전환이면 [from, to] */
676
+ screens: Screen[];
677
+ }
678
+
679
+ export interface KindRun {
680
+ /** u = 전환 진행도 0..1, t = 장면 안 초(지직거림 같은 결에만) */
681
+ frame(u: number, t: number): void;
682
+ dispose(): void;
683
+ }