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,82 @@
1
+ import { clamp, emit, progress, toEase, type Ease, type EaseName, type Style, safeTime } from "@kasaframe/core";
2
+
3
+ export type TransitionName = "whip" | "zoomThrough" | "slide" | "matchCut" | "crossfade";
4
+
5
+ export const TRANSITIONS: readonly TransitionName[] = ["whip", "zoomThrough", "slide", "matchCut", "crossfade"];
6
+
7
+ export interface TransitionOpts {
8
+ /** 전환이 시작하는 시각. matchCut 은 이 순간 끊긴다 */
9
+ at: number;
10
+ dur?: number;
11
+ ease?: EaseName | Ease;
12
+ /** 화면 폭(px) — whip·slide 가 얼마나 밀지 */
13
+ width?: number;
14
+ dir?: "left" | "right" | "up" | "down";
15
+ source?: string;
16
+ scene?: string;
17
+ emit?: boolean;
18
+ }
19
+
20
+ export interface TransitionState {
21
+ /** 나가는 장면 */
22
+ out: Style;
23
+ /** 들어오는 장면 */
24
+ in: Style;
25
+ p: number;
26
+ /** 두 장면을 다 그려야 하는가 */
27
+ both: boolean;
28
+ }
29
+
30
+ const DUR: Record<TransitionName, number> = { whip: 0.32, zoomThrough: 0.5, slide: 0.45, matchCut: 0.25, crossfade: 0.4 };
31
+
32
+ /** 0.5 를 지나며 0→1 로 넘어가는 짧은 계단 — 휙 지나가는 순간에 갈아 끼운다 */
33
+ const swap = (p: number, w = 0.12): number => clamp((p - 0.5 + w / 2) / w);
34
+
35
+ export function transition(name: TransitionName, t: number, opts: TransitionOpts): TransitionState {
36
+ t = safeTime(t);
37
+ if (!(TRANSITIONS as readonly string[]).includes(name)) throw new Error(`모르는 전환: ${name} (있는 것: ${TRANSITIONS.join(" · ")})`);
38
+ const dur = opts.dur ?? DUR[name];
39
+ const raw = progress(t, opts.at, dur);
40
+ const p = toEase(opts.ease, name === "slide" ? toEase("inOutCubic") : toEase("inOutQuart"))(raw);
41
+ const w = opts.width ?? 1920;
42
+ const dir = opts.dir ?? "left";
43
+ const sx = dir === "left" ? -1 : dir === "right" ? 1 : 0;
44
+ const sy = dir === "up" ? -1 : dir === "down" ? 1 : 0;
45
+ if (opts.emit !== false && raw > 0 && raw < 1) emit({ kind: "move", source: opts.source ?? name, scene: opts.scene, name: `transition:${name}`, value: 1 });
46
+ const both = raw > 0 && raw < 1;
47
+ switch (name) {
48
+ case "whip": {
49
+ const blur = Math.sin(Math.PI * p) * 28;
50
+ const k = swap(p);
51
+ return {
52
+ p,
53
+ both,
54
+ out: { x: sx * w * 0.5 * p, y: sy * w * 0.5 * p, blur, opacity: 1 - k },
55
+ in: { x: -sx * w * 0.5 * (1 - p), y: -sy * w * 0.5 * (1 - p), blur, opacity: k },
56
+ };
57
+ }
58
+ case "zoomThrough": {
59
+ const k = swap(p, 0.3);
60
+ return {
61
+ p,
62
+ both,
63
+ out: { scale: 1 + 2.2 * p, blur: 12 * p, opacity: 1 - k },
64
+ in: { scale: 0.7 + 0.3 * p, blur: 10 * (1 - p), opacity: k },
65
+ };
66
+ }
67
+ case "slide":
68
+ return {
69
+ p,
70
+ both,
71
+ out: { x: sx * w * p, y: sy * w * p },
72
+ in: { x: -sx * w * (1 - p), y: -sy * w * (1 - p) },
73
+ };
74
+ case "matchCut": {
75
+ // 같은 자리·같은 크기로 이어 붙이고, 들어온 쪽만 살짝 가라앉혀 끊김을 감춘다
76
+ const cut = t >= opts.at;
77
+ return { p: raw, both: false, out: { opacity: cut ? 0 : 1 }, in: { opacity: cut ? 1 : 0, scale: cut ? 1.03 - 0.03 * p : 1.03 } };
78
+ }
79
+ case "crossfade":
80
+ return { p, both, out: { opacity: 1 - p }, in: { opacity: p } };
81
+ }
82
+ }
@@ -0,0 +1,65 @@
1
+ import * as THREE from "three";
2
+ import { cardSlots, cardsRig, hump, rr } from "./rig.ts";
3
+ import { aim, disposeTree, dust, frameRing, gridPlane, imageMaterial, raw, sky, type Kit } from "./world.ts";
4
+
5
+ export function cards(k: Kit) {
6
+ const { aspect, pal, seed } = k;
7
+ const env = { aspect, seed };
8
+ const scene = new THREE.Scene();
9
+ scene.add(sky({ top: pal.deep, bottom: "#03050b", glow: pal.accent, dir: [0.3, 0.1, -1], amt: 0.45, pow: 5 }));
10
+ scene.add(gridPlane({ color: pal.accent, y: -2.2, width: 30, depth: 30, z: -2, focus: -2 }));
11
+
12
+ const slots = cardSlots(env);
13
+ const decor: { mesh: THREE.Group; base: THREE.Vector3; yaw: number; i: number }[] = [];
14
+ for (const [i, s] of slots.entries()) {
15
+ const g = new THREE.Group();
16
+ const w = aspect * s.scale;
17
+ const h = s.scale;
18
+ let mat;
19
+ if (s.img < 2) mat = imageMaterial(s.img === 0 ? k.from.texture : k.to.texture);
20
+ else {
21
+ // 다른 장면들 — 앞·뒤 그림을 흐리고 눌러 꾸밈으로만
22
+ mat = imageMaterial((s.img === 2 ? k.from : k.to).soft(), { double: true, back: pal.deep });
23
+ mat.uniforms.bias.value = 4;
24
+ mat.uniforms.dim.value = 0.3;
25
+ mat.uniforms.tint.value = raw(pal.deep);
26
+ mat.uniforms.tintAmt.value = 0.35;
27
+ }
28
+ g.add(new THREE.Mesh(new THREE.PlaneGeometry(w, h), mat));
29
+ const ring = new THREE.Mesh(
30
+ frameRing(w, h, 0.008 * Math.max(0.6, s.scale)),
31
+ new THREE.MeshBasicMaterial({ color: raw(s.img < 2 ? pal.glow : pal.accent), transparent: true, opacity: s.img < 2 ? 0.9 : 0.55, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide }),
32
+ );
33
+ g.add(ring);
34
+ g.position.set(...s.pos);
35
+ g.rotation.y = s.yaw;
36
+ scene.add(g);
37
+ if (s.img >= 2) decor.push({ mesh: g, base: g.position.clone(), yaw: s.yaw, i });
38
+ }
39
+
40
+ const n = 600;
41
+ const pts = new Float32Array(n * 3);
42
+ for (let i = 0; i < n; i++) {
43
+ pts[i * 3] = rr(seed, 600 + i, -9, 9);
44
+ pts[i * 3 + 1] = rr(seed, 1600 + i, -3, 4);
45
+ pts[i * 3 + 2] = rr(seed, 2600 + i, -12, -4);
46
+ }
47
+ scene.add(dust(pts, pal.glow, 0.02));
48
+
49
+ return {
50
+ frame(u: number, t: number) {
51
+ aim(k.cam, cardsRig(u, env).cam);
52
+ // 꾸밈 판만 떠다닌다 — 앞·뒤 판은 경계 프레임을 지키려 제자리
53
+ const k2 = 0.4 + 0.6 * hump(u);
54
+ for (const d of decor) {
55
+ d.mesh.position.set(d.base.x, d.base.y + 0.07 * k2 * Math.sin(t * 1.1 + d.i * 1.7), d.base.z);
56
+ d.mesh.rotation.y = d.yaw + 0.06 * k2 * Math.sin(t * 0.8 + d.i);
57
+ }
58
+ k.renderer.clear(true, true, true);
59
+ k.renderer.render(scene, k.cam);
60
+ },
61
+ dispose() {
62
+ disposeTree(scene);
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,84 @@
1
+ import * as THREE from "three";
2
+ import { CHAIN_GAP, FIT, chainRig, rr, type ChainOpts } from "./rig.ts";
3
+ import { aim, disposeTree, dust, frameRing, gridPlane, raw, screenMaterial, sky, type Kit } from "./world.ts";
4
+
5
+ /**
6
+ * 화면 속 화면 — 화면들이 깊이 방향으로 늘어서 있고, 카메라가 화면마다 꽉 찬 순간(arrivals)을 지나며 가운데가 녹은 화면을 뚫고 다음 화면으로.
7
+ * 도착 순간은 그 화면 그림 그대로라 박마다 한 겹씩 맞춰도 경계가 이어진다.
8
+ */
9
+ export function chain(k: Kit, o: ChainOpts) {
10
+ const { aspect, pal, seed } = k;
11
+ const env = { aspect, seed };
12
+ const gap = o.gap ?? CHAIN_GAP;
13
+ const n = k.screens.length;
14
+ const scene = new THREE.Scene();
15
+ const skyMesh = sky({ top: "#0a1024", bottom: "#020308", glow: pal.accent, dir: [0, 0, -1], amt: 0.7, pow: 14 });
16
+ scene.add(skyMesh);
17
+ const len = (n - 1) * gap + 4;
18
+ const grids = [-1.25, 1.25].map((y) => gridPlane({ color: pal.accent, y, width: 12, depth: len, z: -len / 2 + 2, focus: 0 }));
19
+ scene.add(...grids);
20
+
21
+ const mats = k.screens.map((s, i) => {
22
+ const m = screenMaterial(s.texture, pal.glow, aspect, rr(seed, 300 + i, 0, 100));
23
+ const plane = new THREE.Mesh(new THREE.PlaneGeometry(aspect, 1), m);
24
+ plane.position.z = -i * gap;
25
+ scene.add(plane);
26
+ // 화면 테두리 — 떠 있는 모니터. 꽉 찬 순간에는 시야 밖이다
27
+ const bezel = new THREE.Mesh(frameRing(aspect, 1, 0.05), new THREE.MeshBasicMaterial({ color: raw("#0d1018") }));
28
+ bezel.position.z = -i * gap - 0.001;
29
+ scene.add(bezel);
30
+ const halo = new THREE.Mesh(frameRing(aspect + 0.1, 1.1, 0.012), new THREE.MeshBasicMaterial({ color: raw(pal.glow), transparent: true, opacity: 0.85, blending: THREE.AdditiveBlending, depthWrite: false }));
31
+ halo.position.z = -i * gap;
32
+ scene.add(halo);
33
+ return m;
34
+ });
35
+
36
+ // 화면 사이 빛 고리·점 — 도착 카메라와 그 화면 사이(꽉 찬 순간의 시야)는 비운다
37
+ for (let g = 0; g < n - 1; g++) {
38
+ const near = -g * gap - 0.4;
39
+ const far = -(g + 1) * gap + FIT + 0.4;
40
+ for (let j = 0; j < 4; j++) {
41
+ const i = g * 4 + j;
42
+ const s = rr(seed, 100 + i, 1.15, 2.2);
43
+ const ring = new THREE.Mesh(
44
+ frameRing(aspect * s, s, rr(seed, 120 + i, 0.005, 0.013)),
45
+ new THREE.MeshBasicMaterial({ color: raw(j % 2 ? pal.accent : pal.glow), transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending, depthWrite: false }),
46
+ );
47
+ ring.position.set(rr(seed, 140 + i, -0.15, 0.15), rr(seed, 160 + i, -0.1, 0.1), near + ((far - near) * (j + 0.5)) / 4);
48
+ ring.rotation.z = rr(seed, 180 + i, -0.4, 0.4);
49
+ scene.add(ring);
50
+ }
51
+ const m = 260;
52
+ const pts = new Float32Array(m * 3);
53
+ for (let i = 0; i < m; i++) {
54
+ let x = rr(seed, 400 + g * m + i, -4, 4);
55
+ const y = rr(seed, 2400 + g * m + i, -2.4, 2.4);
56
+ if (Math.abs(x) < 0.35 && Math.abs(y) < 0.3) x += x < 0 ? -0.4 : 0.4;
57
+ pts.set([x, y, rr(seed, 4400 + g * m + i, far, near)], i * 3);
58
+ }
59
+ scene.add(dust(pts, pal.glow, 0.014));
60
+ }
61
+
62
+ return {
63
+ frame(_u: number, t: number) {
64
+ const s = chainRig(t, env, o);
65
+ aim(k.cam, s.cam);
66
+ skyMesh.position.set(0, 0, s.cam.pos[2]);
67
+ for (const g of grids) (g.material as THREE.ShaderMaterial).uniforms.focus.value = s.cam.pos[2] - 4;
68
+ mats.forEach((m, i) => {
69
+ m.uniforms.dissolve.value = s.dissolve[i];
70
+ m.visible = s.dissolve[i] < 1;
71
+ });
72
+ k.renderer.clear(true, true, true);
73
+ k.renderer.render(scene, k.cam);
74
+ },
75
+ /** 이번 프레임에 보일 수 있는 화면 — 지나온 화면은 스냅숏하지 않는다 */
76
+ visible(t: number): boolean[] {
77
+ const s = chainRig(t, env, o);
78
+ return k.screens.map((_, i) => i >= s.index && i <= s.index + 2 && s.dissolve[i] < 1);
79
+ },
80
+ dispose() {
81
+ disposeTree(scene);
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,181 @@
1
+ import * as THREE from "three";
2
+ import { RoundedBoxGeometry } from "three/addons/geometries/RoundedBoxGeometry.js";
3
+ import { DIVE_DEPTH, FIT, diveRig, rr } from "./rig.ts";
4
+ import { aim, disposeTree, dust, frameRing, gridPlane, imageMaterial, raw, screenMaterial, sky, type Kit, type Palette } from "./world.ts";
5
+
6
+ function keyboardTexture(pal: Palette): THREE.CanvasTexture {
7
+ const c = document.createElement("canvas");
8
+ c.width = 1024;
9
+ c.height = 300;
10
+ const g = c.getContext("2d")!;
11
+ g.fillStyle = "#15171c";
12
+ g.fillRect(0, 0, c.width, c.height);
13
+ const rows = [14, 14, 13, 12, 9];
14
+ rows.forEach((n, r) => {
15
+ const kw = (c.width - 40) / 14.5;
16
+ let x = 20 + (14 - n) * kw * 0.5 * (r === 4 ? 0 : 1);
17
+ for (let i = 0; i < n; i++) {
18
+ const wide = r === 4 && i === 4 ? 5 : 1;
19
+ g.fillStyle = "#262a33";
20
+ g.beginPath();
21
+ g.roundRect(x + 3, 20 + r * 54 + 3, kw * wide - 6, 48, 6);
22
+ g.fill();
23
+ x += kw * wide;
24
+ }
25
+ });
26
+ g.fillStyle = pal.accent;
27
+ g.globalAlpha = 0.35;
28
+ g.fillRect(0, c.height - 4, c.width, 4);
29
+ const t = new THREE.CanvasTexture(c);
30
+ t.colorSpace = THREE.SRGBColorSpace;
31
+ return t;
32
+ }
33
+
34
+ export function dive(k: Kit) {
35
+ const { aspect, pal, seed } = k;
36
+ const env = { aspect, seed };
37
+
38
+ // 바깥 — 어두운 방, 책상 위 모니터
39
+ const outer = new THREE.Scene();
40
+ outer.add(sky({ top: pal.deep, bottom: "#05060a", glow: pal.accent, dir: [0, 0.2, -1], amt: 0.35 }));
41
+ const metal = new THREE.MeshStandardMaterial({ color: "#1b1e25", roughness: 0.42, metalness: 0.55 });
42
+ const bezel = new THREE.Mesh(new RoundedBoxGeometry(aspect + 0.12, 1.12, 0.08, 4, 0.03), metal);
43
+ bezel.position.z = -0.045;
44
+ outer.add(bezel);
45
+ const back = new THREE.Mesh(new RoundedBoxGeometry(aspect * 0.7, 0.62, 0.12, 4, 0.05), metal);
46
+ back.position.set(0, -0.04, -0.14);
47
+ outer.add(back);
48
+ const neck = new THREE.Mesh(new RoundedBoxGeometry(0.16, 0.5, 0.05, 2, 0.02), metal);
49
+ neck.position.set(0, -0.78, -0.16);
50
+ outer.add(neck);
51
+ const deskY = -1.03;
52
+ const foot = new THREE.Mesh(new RoundedBoxGeometry(0.74, 0.03, 0.4, 3, 0.012), metal);
53
+ foot.position.set(0, deskY + 0.015, -0.13);
54
+ outer.add(foot);
55
+ const desk = new THREE.Mesh(new THREE.PlaneGeometry(14, 8), new THREE.MeshStandardMaterial({ color: "#20232b", roughness: 0.7, metalness: 0.1 }));
56
+ desk.rotation.x = -Math.PI / 2;
57
+ desk.position.set(0, deskY, 1.5);
58
+ outer.add(desk);
59
+ const kbTex = keyboardTexture(pal);
60
+ const kb = new THREE.Mesh(
61
+ new RoundedBoxGeometry(1.25, 0.03, 0.38, 2, 0.01),
62
+ [metal, metal, new THREE.MeshStandardMaterial({ map: kbTex, roughness: 0.6 }), metal, metal, metal],
63
+ );
64
+ kb.position.set(-0.1, deskY + 0.015, 0.75);
65
+ kb.rotation.y = 0.04;
66
+ outer.add(kb);
67
+ outer.add(new THREE.HemisphereLight("#8fa4d8", "#08090c", 0.6));
68
+ const key = new THREE.DirectionalLight("#ffffff", 1.4);
69
+ key.position.set(-2, 3, 2.5);
70
+ outer.add(key);
71
+ const rim = new THREE.DirectionalLight(pal.accent, 1.2);
72
+ rim.position.set(2.5, 1.2, -2);
73
+ outer.add(rim);
74
+ const screenGlow = new THREE.PointLight(pal.glow, 2.2, 4, 1.4);
75
+ screenGlow.position.set(0, -0.2, 0.5);
76
+ outer.add(screenGlow);
77
+
78
+ // 화면 자리에 스텐실 1 — 안쪽 공간은 바깥에 있는 동안 여기로만 보인다
79
+ const mask = new THREE.Scene();
80
+ const maskMesh = new THREE.Mesh(
81
+ new THREE.PlaneGeometry(aspect, 1),
82
+ new THREE.MeshBasicMaterial({
83
+ colorWrite: false,
84
+ depthWrite: false,
85
+ depthTest: false,
86
+ stencilWrite: true,
87
+ stencilRef: 1,
88
+ stencilFunc: THREE.AlwaysStencilFunc,
89
+ stencilZPass: THREE.ReplaceStencilOp,
90
+ }),
91
+ );
92
+ mask.add(maskMesh);
93
+
94
+ // 안쪽 — 미지의 공간. 화면 모양 고리가 늘어선 통로 끝에 뒤 장면
95
+ const inner = new THREE.Scene();
96
+ inner.add(sky({ top: "#0a1024", bottom: "#020308", glow: pal.accent, dir: [0, 0, -1], amt: 0.7, pow: 14 }));
97
+ const rings: THREE.Mesh[] = [];
98
+ const nRing = 26;
99
+ for (let i = 0; i < nRing; i++) {
100
+ const z = -0.7 - (i / (nRing - 1)) * (DIVE_DEPTH - FIT - 1.6);
101
+ const s = rr(seed, 100 + i, 1.15, 2.2);
102
+ const m = new THREE.Mesh(
103
+ frameRing(aspect * s, s, rr(seed, 120 + i, 0.005, 0.013)),
104
+ new THREE.MeshBasicMaterial({ color: raw(i % 3 === 0 ? pal.glow : pal.accent), transparent: true, opacity: 0.75, blending: THREE.AdditiveBlending, depthWrite: false }),
105
+ );
106
+ m.position.set(rr(seed, 140 + i, -0.15, 0.15), rr(seed, 160 + i, -0.1, 0.1), z);
107
+ m.rotation.z = rr(seed, 180 + i, -0.35, 0.35);
108
+ inner.add(m);
109
+ rings.push(m);
110
+ }
111
+ for (const y of [-1.25, 1.25]) inner.add(gridPlane({ color: pal.accent, y, width: 12, depth: DIVE_DEPTH + 4, z: -DIVE_DEPTH / 2, focus: -4 }));
112
+ const n = 1800;
113
+ const pts = new Float32Array(n * 3);
114
+ for (let i = 0; i < n; i++) {
115
+ let x = rr(seed, 400 + i, -4, 4);
116
+ let y = rr(seed, 1400 + i, -2.4, 2.4);
117
+ // 카메라가 지나는 축 가까이는 비운다 — 코앞을 스치는 점이 큰 얼룩으로 찍힌다
118
+ if (Math.abs(x) < 0.35 && Math.abs(y) < 0.3) x += x < 0 ? -0.4 : 0.4;
119
+ pts[i * 3] = x;
120
+ pts[i * 3 + 1] = y;
121
+ pts[i * 3 + 2] = rr(seed, 2400 + i, -DIVE_DEPTH + FIT + 0.3, -0.2);
122
+ }
123
+ inner.add(dust(pts, pal.glow, 0.014));
124
+ const toMat = imageMaterial(k.to.texture);
125
+ const toPlane = new THREE.Mesh(new THREE.PlaneGeometry(aspect, 1), toMat);
126
+ toPlane.position.z = -DIVE_DEPTH;
127
+ inner.add(toPlane);
128
+ const halo = new THREE.Mesh(frameRing(aspect, 1, 0.03), new THREE.MeshBasicMaterial({ color: raw(pal.glow), transparent: true, blending: THREE.AdditiveBlending, depthWrite: false }));
129
+ halo.position.z = -DIVE_DEPTH;
130
+ inner.add(halo);
131
+ const innerMats: THREE.Material[] = [];
132
+ inner.traverse((o) => {
133
+ const m = (o as THREE.Mesh).material as THREE.Material | undefined;
134
+ if (m) innerMats.push(m);
135
+ });
136
+
137
+ // 위 — 앞 장면이 걸린 화면
138
+ const overlay = new THREE.Scene();
139
+ const screenMat = screenMaterial(k.from.texture, pal.glow, aspect, rr(seed, 0, 0, 100));
140
+ screenMat.depthTest = false;
141
+ screenMat.depthWrite = false;
142
+ overlay.add(new THREE.Mesh(new THREE.PlaneGeometry(aspect, 1), screenMat));
143
+
144
+ let wasInside: boolean | null = null;
145
+ return {
146
+ frame(u: number, t: number) {
147
+ const s = diveRig(u, env);
148
+ aim(k.cam, s.cam);
149
+ screenMat.uniforms.dissolve.value = s.dissolve;
150
+ screenMat.uniforms.glitch.value = s.glitch;
151
+ screenMat.uniforms.tick.value = Math.floor(t * 24);
152
+ screenMat.uniforms.sheen.value = 0.06 * Math.min(1, u / 0.3) * (1 - s.dissolve) * (u < 1 ? 1 : 0);
153
+ halo.material.opacity = 0.8 * (1 - Math.min(1, Math.max(0, (u - 0.8) / 0.18)));
154
+ if (wasInside !== s.inside) {
155
+ for (const m of innerMats) {
156
+ m.stencilWrite = !s.inside;
157
+ m.stencilRef = 1;
158
+ m.stencilFunc = THREE.EqualStencilFunc;
159
+ m.needsUpdate = true;
160
+ }
161
+ wasInside = s.inside;
162
+ }
163
+ const r = k.renderer;
164
+ r.clear(true, true, true);
165
+ if (s.inside) {
166
+ r.render(inner, k.cam);
167
+ return;
168
+ }
169
+ r.render(outer, k.cam);
170
+ r.clearDepth();
171
+ r.render(mask, k.cam);
172
+ r.render(inner, k.cam);
173
+ r.clearDepth();
174
+ r.render(overlay, k.cam);
175
+ },
176
+ dispose() {
177
+ for (const sc of [outer, mask, inner, overlay]) disposeTree(sc);
178
+ kbTex.dispose();
179
+ },
180
+ };
181
+ }
@@ -0,0 +1,110 @@
1
+ import * as THREE from "three";
2
+ import { foldRig, type FoldStyle } from "./rig.ts";
3
+ import { aim, disposeTree, imageMaterial, raw, sky, type Kit } from "./world.ts";
4
+
5
+ const STRIPS = 8;
6
+
7
+ // 아래에서 위로 말려 올라가는 종이. 말린 선(rollY) 아래쪽이 원통에 감기고, 감길수록 반지름이 조금씩 커져 겹이 안 겹친다
8
+ const ROLL_VERT = /* glsl */ `
9
+ uniform float rollY;
10
+ uniform float radius;
11
+ varying vec2 vUv;
12
+ varying float vShade;
13
+ void main() {
14
+ vUv = uv;
15
+ vec3 p = position;
16
+ float s = rollY - p.y;
17
+ vShade = 0.0;
18
+ if (s > 0.0) {
19
+ float r = radius + s * 0.012;
20
+ float a = s / r;
21
+ p.y = rollY - r * sin(a);
22
+ p.z = r - r * cos(a);
23
+ vShade = 0.5 - 0.5 * cos(a);
24
+ }
25
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
26
+ }`;
27
+
28
+ const ROLL_FRAG = /* glsl */ `
29
+ uniform sampler2D map;
30
+ uniform float opacity;
31
+ uniform vec3 backColor;
32
+ varying vec2 vUv;
33
+ varying float vShade;
34
+ void main() {
35
+ vec3 c = texture2D(map, vUv).rgb;
36
+ if (!gl_FrontFacing) c = mix(c * 0.3, backColor, 0.6);
37
+ c *= 1.0 - 0.45 * vShade;
38
+ gl_FragColor = vec4(c, opacity);
39
+ }`;
40
+
41
+ export function fold(k: Kit, o: { fold?: FoldStyle }) {
42
+ const { aspect, pal, seed } = k;
43
+ const env = { aspect, seed };
44
+ const style = o.fold ?? "accordion";
45
+ const scene = new THREE.Scene();
46
+ scene.add(sky({ top: pal.deep, bottom: "#020308", glow: pal.accent, dir: [0, 0, -1], amt: 0.4, pow: 4 }));
47
+ // 뒤 장면은 바닥에 깔고 깊이를 안 쓴다 — 같은 면에 놓인 앞 장면 판과 깜빡이며 다투지 않게
48
+ const bMat = imageMaterial(k.to.texture);
49
+ bMat.depthWrite = false;
50
+ const bPlane = new THREE.Mesh(new THREE.PlaneGeometry(aspect, 1), bMat);
51
+ bPlane.renderOrder = -1;
52
+ scene.add(bPlane);
53
+
54
+ let apply: (amount: number, slide: number, alpha: number) => void;
55
+ if (style === "roll") {
56
+ const mat = new THREE.ShaderMaterial({
57
+ vertexShader: ROLL_VERT,
58
+ fragmentShader: ROLL_FRAG,
59
+ uniforms: { map: { value: k.from.texture }, rollY: { value: -1 }, radius: { value: 0.06 }, opacity: { value: 1 }, backColor: { value: raw(pal.glow) } },
60
+ side: THREE.DoubleSide,
61
+ transparent: true,
62
+ });
63
+ const sheet = new THREE.Mesh(new THREE.PlaneGeometry(aspect, 1, 1, 320), mat);
64
+ scene.add(sheet);
65
+ apply = (amount, _slide, alpha) => {
66
+ mat.uniforms.rollY.value = amount;
67
+ mat.uniforms.opacity.value = alpha;
68
+ sheet.visible = alpha > 0;
69
+ };
70
+ } else {
71
+ const sw = aspect / STRIPS;
72
+ const strips = Array.from({ length: STRIPS }, (_, i) => {
73
+ const geo = new THREE.PlaneGeometry(sw, 1);
74
+ geo.translate(sw / 2, 0, 0);
75
+ const uv = geo.attributes.uv as THREE.BufferAttribute;
76
+ for (let j = 0; j < uv.count; j++) uv.setX(j, (i + uv.getX(j)) / STRIPS);
77
+ const mat = imageMaterial(k.from.texture, { double: true, back: pal.accent });
78
+ const m = new THREE.Mesh(geo, mat);
79
+ scene.add(m);
80
+ return { m, mat };
81
+ });
82
+ apply = (theta, slide, alpha) => {
83
+ const c = Math.cos(theta);
84
+ const s = Math.sin(theta);
85
+ strips.forEach(({ m, mat }, i) => {
86
+ // 짝수 띠는 바닥에 붙고 홀수 띠는 카메라 쪽으로 — 병풍처럼 왼쪽으로 접힌다
87
+ const x = -aspect / 2 + i * sw * c - slide;
88
+ const z = i % 2 ? sw * s : 0;
89
+ m.position.set(x, 0, z);
90
+ m.rotation.y = i % 2 ? theta : -theta;
91
+ mat.uniforms.dim.value = (i % 2 ? 0.42 : 0.12) * s;
92
+ mat.uniforms.opacity.value = alpha;
93
+ m.visible = alpha > 0;
94
+ });
95
+ };
96
+ }
97
+
98
+ return {
99
+ frame(u: number) {
100
+ const s = foldRig(u, env, style);
101
+ aim(k.cam, s.cam);
102
+ apply(s.amount, s.slide, s.alpha);
103
+ k.renderer.clear(true, true, true);
104
+ k.renderer.render(scene, k.cam);
105
+ },
106
+ dispose() {
107
+ disposeTree(scene);
108
+ },
109
+ };
110
+ }