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,90 @@
1
+ 'use client';
2
+
3
+ // SmoothUI Text Morph(MIT, vendor/transitions/smoothui/text-morph.tsx)를 Tailwind 없이 옮긴 판. 원본과 같은 동작.
4
+ // 글자(또는 낱말)끼리 비교해 같은 것은 자리만 미끄러지고, 빠진 것은 줄며 사라지고, 새 것은 튀어나온다.
5
+
6
+ import { AnimatePresence, motion, useReducedMotion } from 'motion/react';
7
+ import { useMemo, type ElementType } from 'react';
8
+
9
+ export type TextMorphMode = 'characters' | 'words';
10
+ export type TextMorphAlign = 'start' | 'center' | 'end';
11
+
12
+ type TextMorphProps = {
13
+ align?: TextMorphAlign;
14
+ as?: 'span' | 'div' | 'p' | 'h1' | 'h2' | 'h3' | 'h4';
15
+ className?: string;
16
+ /** 낱개마다 스프링 길이(ms) */
17
+ duration?: number;
18
+ mode?: TextMorphMode;
19
+ /** 낱개 사이 지연(ms). 긴 글도 늘어지지 않게 위를 막는다 */
20
+ stagger?: number;
21
+ text: string;
22
+ };
23
+
24
+ type Token = { key: string; value: string };
25
+
26
+ const DEFAULT_DURATION = 250;
27
+ const DEFAULT_STAGGER = 16;
28
+ const MS = 1000;
29
+ const POP_SCALE = 0.72;
30
+ const POP_OFFSET = 6;
31
+ const MAX_STAGGER_MS = 260;
32
+ const EXIT_DURATION_S = 0.15;
33
+ const EASE_OUT = [0.23, 1, 0.32, 1] as const;
34
+
35
+ const segmentGraphemes = (value: string): string[] => {
36
+ if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {
37
+ const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
38
+ return Array.from(segmenter.segment(value), (entry) => entry.segment);
39
+ }
40
+ return [...value];
41
+ };
42
+
43
+ const tokenizeText = (text: string, mode: TextMorphMode): string[] =>
44
+ mode === 'words' ? text.split(/(\s+)/).filter((token) => token.length > 0) : segmentGraphemes(text);
45
+
46
+ const buildTokens = (text: string, mode: TextMorphMode): Token[] => {
47
+ const occurrences = new Map<string, number>();
48
+ return tokenizeText(text, mode).map((value) => {
49
+ const count = occurrences.get(value) ?? 0;
50
+ occurrences.set(value, count + 1);
51
+ return { key: `${value}-${count}`, value };
52
+ });
53
+ };
54
+
55
+ export function TextMorph({ text, align = 'start', as = 'span', className, duration = DEFAULT_DURATION, mode = 'characters', stagger = DEFAULT_STAGGER }: TextMorphProps) {
56
+ const shouldReduceMotion = useReducedMotion();
57
+ const tokens = useMemo(() => buildTokens(text, mode), [text, mode]);
58
+ const Tag = as as ElementType;
59
+
60
+ if (shouldReduceMotion) return <Tag className={className}>{text}</Tag>;
61
+
62
+ return (
63
+ <Tag className={className}>
64
+ <span className="sr-only">{text}</span>
65
+ {/* 빠지는 글자는 popLayout 이 absolute 로 뽑아내니 줄은 position: relative 여야 제자리에서 사라진다. */}
66
+ <motion.span
67
+ style={{ position: 'relative', display: 'inline-flex', flexWrap: 'wrap', justifyContent: align === 'center' ? 'center' : align === 'end' ? 'flex-end' : 'flex-start' }}
68
+ layout
69
+ transition={{ bounce: 0.1, duration: duration / MS, type: 'spring' }}
70
+ >
71
+ <AnimatePresence initial={false} mode="popLayout">
72
+ {tokens.map((token, index) => (
73
+ <motion.span
74
+ animate={{ opacity: 1, scale: 1, y: 0 }}
75
+ aria-hidden="true"
76
+ exit={{ opacity: 0, scale: POP_SCALE, transition: { duration: EXIT_DURATION_S, ease: EASE_OUT }, y: -POP_OFFSET }}
77
+ initial={{ opacity: 0, scale: POP_SCALE, y: POP_OFFSET }}
78
+ key={token.key}
79
+ layout
80
+ style={{ display: 'inline-block', whiteSpace: 'pre' }}
81
+ transition={{ bounce: 0.1, delay: Math.min(index * stagger, MAX_STAGGER_MS) / MS, duration: duration / MS, type: 'spring' }}
82
+ >
83
+ {token.value}
84
+ </motion.span>
85
+ ))}
86
+ </AnimatePresence>
87
+ </motion.span>
88
+ </Tag>
89
+ );
90
+ }
@@ -0,0 +1,223 @@
1
+ // SmoothUI(MIT, educlopez/smoothui)의 셰이더 전환 두 벌. 조각 셰이더는 원본 그대로다.
2
+ // 원본: packages/smoothui/components/shader-reveal-transition · sdf-blob-transition (vendor/transitions/smoothui)
3
+
4
+ // noise · zoom · circle · wipe · luma · planetary · stripes · push 순서가 uVariant 0..7 이다.
5
+ export const REVEAL_VARIANTS = ['noise', 'zoom', 'circle', 'wipe', 'luma', 'planetary', 'stripes', 'push'] as const;
6
+ export const REVEAL_FRAGMENT = `
7
+ precision mediump float;
8
+ uniform vec2 uRes;
9
+ uniform float uTime;
10
+ uniform float uProgress;
11
+ uniform float uAlpha;
12
+ uniform float uVariant;
13
+ #define PI 3.14159265359
14
+
15
+ float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
16
+ float noise(vec2 p){
17
+ vec2 i = floor(p);
18
+ vec2 f = fract(p);
19
+ float a = hash(i);
20
+ float b = hash(i + vec2(1.0, 0.0));
21
+ float c = hash(i + vec2(0.0, 1.0));
22
+ float d = hash(i + vec2(1.0, 1.0));
23
+ vec2 u = f * f * (3.0 - 2.0 * f);
24
+ return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
25
+ }
26
+ float parabola(float x, float k){ return pow(4.0 * x * (1.0 - x), k); }
27
+ mat2 rot(float a){ float s = sin(a); float c = cos(a); return mat2(c, -s, s, c); }
28
+
29
+ vec4 paint(vec2 uv, float mask, vec3 color, float glow){
30
+ float exitFade = smoothstep(1.0, 0.76, uProgress);
31
+ float entryFade = smoothstep(0.0, 0.10, uProgress);
32
+ float alpha = clamp((mask * 0.76 + glow * 0.24) * entryFade * exitFade * uAlpha, 0.0, 0.92);
33
+ return vec4(color * alpha + vec3(1.0) * glow * 0.16 * uAlpha, alpha);
34
+ }
35
+
36
+ void main(){
37
+ vec2 uv = gl_FragCoord.xy / uRes;
38
+ vec2 aspect = vec2(uRes.x / uRes.y, 1.0);
39
+ vec2 p = (uv - 0.5) * aspect;
40
+ float progress = smoothstep(0.0, 1.0, uProgress);
41
+ vec3 pink = vec3(1.0, 0.20, 0.58);
42
+ vec3 cyan = vec3(0.12, 0.86, 1.0);
43
+ vec3 gold = vec3(1.0, 0.72, 0.28);
44
+ vec3 mint = vec3(0.45, 1.0, 0.72);
45
+
46
+ float mask = 0.0;
47
+ float glow = 0.0;
48
+ vec3 color = mix(pink, cyan, uv.x);
49
+
50
+ if (uVariant < 0.5) {
51
+ // Demo 1 — turbulent noise threshold. Chunky, organic gate.
52
+ float n1 = noise(uv * vec2(18.0, 12.0) + uTime * 0.42);
53
+ float n2 = noise(uv * vec2(52.0, 44.0) - uTime * 0.18);
54
+ float threshold = progress * 1.35 - 0.18;
55
+ float gate = smoothstep(threshold - 0.18, threshold + 0.18, uv.x * 0.62 + uv.y * 0.28 + n1 * 0.34 + n2 * 0.08);
56
+ mask = pow(gate * (1.0 - gate) * 4.0, 0.74) + step(0.74, n2) * gate * 0.22;
57
+ glow = exp(-pow(uv.x + n1 * 0.26 - progress, 2.0) * 26.0);
58
+ color = mix(vec3(1.0, 0.10, 0.55), vec3(0.18, 1.0, 0.78), n1);
59
+ } else if (uVariant < 1.5) {
60
+ // Demo 2 — zoom mix. Large central lens, not a sweep.
61
+ float yGate = smoothstep(0.0, 1.0, progress * 2.15 + uv.y - 1.08);
62
+ vec2 z = (uv - 0.5) * mix(1.75, 0.35, yGate) + 0.5;
63
+ float lens = 1.0 - smoothstep(0.12, 0.78, length((z - 0.5) * aspect));
64
+ float seam = exp(-pow(yGate - 0.5, 2.0) * 20.0);
65
+ mask = (lens * 0.72 + seam * 0.42) * parabola(progress, 0.42);
66
+ glow = seam * 0.9 + lens * 0.25;
67
+ color = mix(vec3(1.0, 0.80, 0.34), vec3(0.70, 0.92, 1.0), yGate);
68
+ } else if (uVariant < 2.5) {
69
+ // Demo 3 — noisy circular reveal.
70
+ float n = noise(uv * 9.0 + uTime * 0.25);
71
+ float radius = progress * 1.08 + n * 0.045;
72
+ float d = length(p) - radius * 0.72;
73
+ mask = exp(-d * d * 42.0) + smoothstep(0.08, -0.12, d) * 0.18;
74
+ glow = exp(-d * d * 140.0);
75
+ color = mix(vec3(1.0, 0.22, 0.58), vec3(1.0, 0.75, 0.32), atan(p.y, p.x) / PI * 0.5 + 0.5);
76
+ } else if (uVariant < 3.5) {
77
+ // Demo 4 — displacement wipe. Narrow noisy blade.
78
+ float n = noise(uv * vec2(12.0, 26.0) + vec2(uTime * 0.26, -uTime * 0.12));
79
+ float blade = progress * 1.32 - 0.16 + n * 0.11;
80
+ float edge = blade - uv.x;
81
+ float front = exp(-edge * edge * 120.0);
82
+ float fillSide = smoothstep(-0.03, 0.16, edge) * 0.18;
83
+ mask = front * 0.92 + fillSide;
84
+ glow = front;
85
+ color = mix(vec3(0.12, 0.92, 1.0), vec3(1.0, 1.0, 1.0), front * 0.38);
86
+ } else if (uVariant < 4.5) {
87
+ // Demo 5 — luminance displacement. Horizontal ribbons pulled vertically.
88
+ float lum = noise(uv * 5.0 + vec2(0.0, uTime * 0.22));
89
+ float displacedY = uv.y + progress * (lum - 0.5) * 0.55;
90
+ float ribbons = sin(displacedY * 58.0 + uTime * 1.25) * 0.5 + 0.5;
91
+ float fine = sin(displacedY * 132.0 - uTime * 1.8) * 0.5 + 0.5;
92
+ float window = smoothstep(0.03, 0.5, progress) * smoothstep(1.0, 0.72, progress);
93
+ mask = (smoothstep(0.58, 0.92, ribbons) * 0.74 + smoothstep(0.76, 0.98, fine) * 0.22) * window;
94
+ glow = smoothstep(0.84, 1.0, ribbons) * window;
95
+ color = mix(vec3(1.0, 0.68, 0.22), vec3(1.0, 0.20, 0.62), lum);
96
+ } else if (uVariant < 5.5) {
97
+ // Demo 6 — planetary rotated displacement. Spiral vortex.
98
+ vec2 disp = vec2(noise(uv * 5.5 + uTime * 0.12), noise(uv * 5.5 + 4.2 - uTime * 0.10)) - 0.5;
99
+ vec2 q = rot(PI * 0.25) * (p + disp * 0.44 * parabola(progress, 0.55));
100
+ float ang = atan(q.y, q.x);
101
+ float rr = length(q);
102
+ float arms = sin(ang * 5.0 + rr * 18.0 - uTime * 1.8);
103
+ float core = 1.0 - smoothstep(0.0, 0.42, rr);
104
+ mask = (smoothstep(0.08, 0.92, arms) * 0.72 + core * 0.38) * parabola(progress, 0.58);
105
+ glow = exp(-abs(arms) * 1.5) * parabola(progress, 0.85);
106
+ color = mix(vec3(0.10, 0.88, 1.0), vec3(1.0, 0.66, 0.18), rr + core * 0.35);
107
+ } else if (uVariant < 6.5) {
108
+ // Demo 7 — divided UV stripe displacement. Barcode shutter.
109
+ vec2 divided = fract(uv * vec2(42.0, 1.0));
110
+ float stripeCore = step(0.18, divided.x) * step(divided.x, 0.62);
111
+ float diagonal = uv.x + uv.y * 0.18 + divided.x * 0.06;
112
+ float sweep = smoothstep(-0.10, 1.12, progress * 1.32 - diagonal + 0.18);
113
+ float shutter = stripeCore * sweep * smoothstep(0.04, 0.56, progress) * smoothstep(1.0, 0.78, progress);
114
+ mask = shutter * 0.92;
115
+ glow = stripeCore * exp(-pow(progress - uv.x, 2.0) * 30.0) * 0.86;
116
+ color = mix(vec3(1.0, 0.15, 0.55), vec3(0.12, 0.88, 1.0), divided.x);
117
+ } else {
118
+ // Demo 8 — noise push/pull. Vertical flow with directional pressure.
119
+ float hn = noise(uv * uRes / 115.0 + uTime * 0.12);
120
+ vec2 centerDir = normalize(vec2(0.5) - uv + 0.001);
121
+ float pull = centerDir.y * (1.0 + hn * 0.7) * parabola(progress, 0.62);
122
+ float bands = sin((uv.y + pull * 0.52) * 28.0 + hn * 5.0);
123
+ float pressure = smoothstep(-0.22, 0.96, bands) * parabola(progress, 0.62);
124
+ float verticalGlow = exp(-pow(uv.y - (0.5 + pull * 0.22), 2.0) * 18.0);
125
+ mask = pressure * 0.78 + verticalGlow * 0.22;
126
+ glow = verticalGlow * pressure;
127
+ color = mix(vec3(0.48, 1.0, 0.74), vec3(1.0, 0.22, 0.58), hn);
128
+ }
129
+
130
+ gl_FragColor = paint(uv, mask, color, glow);
131
+ }
132
+ `;
133
+
134
+ // circle · warped · merge · radial · final 순서가 uVariant 0..4 다.
135
+ export const SDF_VARIANTS = ['circle', 'warped', 'merge', 'radial', 'final'] as const;
136
+ export const SDF_FRAGMENT = `
137
+ precision mediump float;
138
+ uniform vec2 uRes;
139
+ uniform float uTime;
140
+ uniform float uProgress;
141
+ uniform float uAlpha;
142
+ uniform float uVariant;
143
+ #define PI 3.14159265359
144
+
145
+ float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
146
+ float noise(vec2 p){
147
+ vec2 i = floor(p);
148
+ vec2 f = fract(p);
149
+ float a = hash(i);
150
+ float b = hash(i + vec2(1.0, 0.0));
151
+ float c = hash(i + vec2(0.0, 1.0));
152
+ float d = hash(i + vec2(1.0, 1.0));
153
+ vec2 u = f * f * (3.0 - 2.0 * f);
154
+ return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
155
+ }
156
+
157
+ float circleSdf(vec2 p, vec2 c, float r){ return length(p - c) - r; }
158
+ float smin(float a, float b, float k){
159
+ float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
160
+ return mix(b, a, h) - k * h * (1.0 - h);
161
+ }
162
+
163
+ void main(){
164
+ vec2 uv = gl_FragCoord.xy / uRes;
165
+ vec2 aspect = vec2(uRes.x / uRes.y, 1.0);
166
+ vec2 p = (uv - 0.5) * aspect;
167
+ float progress = smoothstep(0.0, 1.0, uProgress);
168
+ float grow = smoothstep(0.0, 0.86, progress);
169
+ float exitFade = smoothstep(1.0, 0.72, progress);
170
+
171
+ float wobble = noise(uv * 7.0 + uTime * 0.18) * 0.035;
172
+ float ringNoise = noise(vec2(atan(p.y, p.x) * 2.0, uTime * 0.18)) * 0.075;
173
+ float dCircle = circleSdf(p, vec2(0.0), 0.04 + grow * 0.64);
174
+ float dWarped = circleSdf(p, vec2(0.0), 0.04 + grow * 0.62 + ringNoise * smoothstep(0.08, 0.96, progress));
175
+
176
+ float m0 = circleSdf(p, vec2(-0.26, -0.08), 0.06 + grow * 0.42 + wobble);
177
+ float m1 = circleSdf(p, vec2(0.24, 0.12), 0.06 + grow * 0.40 - wobble * 0.5);
178
+ float dMerge = smin(m0, m1, 0.18);
179
+
180
+ float angle = atan(p.y, p.x);
181
+ float sector3 = 2.0 * PI / 3.0;
182
+ float sector5 = 2.0 * PI / 5.0;
183
+ float a3 = floor((angle + PI) / sector3 + 0.5) * sector3 - PI;
184
+ float a5 = floor((angle + PI) / sector5 + 0.5) * sector5 - PI;
185
+ float r0 = 0.17 + grow * 0.24;
186
+ float r1 = 0.30 + grow * 0.18;
187
+ float dR0 = circleSdf(p, vec2(cos(a3), sin(a3)) * r0, 0.04 + grow * 0.22 + wobble * 0.4);
188
+ float dR1 = circleSdf(p, vec2(cos(a5), sin(a5)) * r1, 0.035 + grow * 0.18);
189
+ float dRadial = smin(dR0, dR1, 0.14);
190
+
191
+ float f0 = circleSdf(p, vec2(-0.28, -0.10), 0.08 + grow * 0.46 + wobble);
192
+ float f1 = circleSdf(p, vec2(0.00, 0.16), 0.06 + grow * 0.42 - wobble * 0.5);
193
+ float f2 = circleSdf(p, vec2(0.30, -0.06), 0.05 + grow * 0.40 + wobble * 0.6);
194
+ float f3 = circleSdf(p, vec2(-0.02, -0.30), 0.04 + grow * 0.34);
195
+ float dFinal = smin(smin(f0, f1, 0.18), smin(f2, f3, 0.16), 0.20);
196
+ dFinal = smin(dFinal, dRadial, 0.16);
197
+
198
+ float d = dFinal;
199
+ if (uVariant < 0.5) {
200
+ d = dCircle;
201
+ } else if (uVariant < 1.5) {
202
+ d = dWarped;
203
+ } else if (uVariant < 2.5) {
204
+ d = dMerge;
205
+ } else if (uVariant < 3.5) {
206
+ d = dRadial;
207
+ }
208
+
209
+ float edge = exp(-d * d * 160.0) * exitFade;
210
+ float fill = smoothstep(0.08, -0.10, d) * smoothstep(0.0, 0.18, progress) * exitFade;
211
+ float cell = hash(floor((uv + noise(uv * 11.0) * 0.01) * uRes / 7.0) + floor(uTime * 10.0));
212
+ float flecks = step(0.78, cell) * edge * 0.22 * smoothstep(1.5, 4.0, uVariant);
213
+
214
+ vec3 rose = vec3(1.0, 0.23, 0.58);
215
+ vec3 apricot = vec3(1.0, 0.70, 0.38);
216
+ vec3 mint = vec3(0.42, 0.94, 0.78);
217
+ vec3 color = mix(rose, apricot, smoothstep(-0.35, 0.35, p.x + noise(uv * 5.0) * 0.18));
218
+ color = mix(color, mint, smoothstep(0.12, 0.7, p.y + grow * 0.16));
219
+
220
+ float alpha = (fill * 0.24 + edge * 0.72 + flecks) * uAlpha;
221
+ gl_FragColor = vec4(color * alpha + vec3(1.0) * edge * 0.18 * uAlpha, clamp(alpha, 0.0, 0.9));
222
+ }
223
+ `;
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Léo Wicki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ // unlumen UI Scramble Text(MIT, vendor/transitions/unlumen/scramble-text.json)를 Tailwind 없이 옮긴 판. 원본과 같은 동작.
4
+ // 앞에서부터 한 글자씩 드러나고, 드러난 글자 바로 뒤에 섞인 글자 몇 개가 앞서 달린다.
5
+
6
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
7
+
8
+ type ScrambleTextProps = {
9
+ text: string;
10
+ scrambleSpeed?: number;
11
+ /** 드러난 글자 앞에서 달리는 섞인 글자 수 */
12
+ scrambledLetterCount?: number;
13
+ characters?: string;
14
+ className?: string;
15
+ /** 아직 안 드러난(섞인) 쪽 */
16
+ scrambledClassName?: string;
17
+ autoStart?: boolean;
18
+ delay?: number;
19
+ onStart?: () => void;
20
+ onComplete?: () => void;
21
+ };
22
+
23
+ export type ScrambleTextHandle = { start: () => void; reset: () => void };
24
+
25
+ export const ScrambleText = forwardRef<ScrambleTextHandle, ScrambleTextProps>(
26
+ (
27
+ {
28
+ text,
29
+ scrambleSpeed = 50,
30
+ scrambledLetterCount = 2,
31
+ characters = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',
32
+ className = '',
33
+ scrambledClassName = '',
34
+ autoStart = true,
35
+ delay = 0,
36
+ onStart,
37
+ onComplete,
38
+ },
39
+ ref,
40
+ ) => {
41
+ const [displayText, setDisplayText] = useState('');
42
+ const [isAnimating, setIsAnimating] = useState(false);
43
+ const [visibleLetterCount, setVisibleLetterCount] = useState(0);
44
+ const [scrambleOffset, setScrambleOffset] = useState(0);
45
+
46
+ const startAnimation = useCallback(() => {
47
+ setIsAnimating(true);
48
+ setVisibleLetterCount(0);
49
+ setScrambleOffset(0);
50
+ onStart?.();
51
+ }, [onStart]);
52
+
53
+ const reset = useCallback(() => {
54
+ setIsAnimating(false);
55
+ setVisibleLetterCount(0);
56
+ setScrambleOffset(0);
57
+ setDisplayText('');
58
+ }, []);
59
+
60
+ useImperativeHandle(ref, () => ({ start: startAnimation, reset }));
61
+
62
+ useEffect(() => {
63
+ if (!autoStart) return;
64
+ if (delay > 0) {
65
+ const t = setTimeout(startAnimation, delay);
66
+ return () => clearTimeout(t);
67
+ }
68
+ startAnimation();
69
+ }, [autoStart, delay, startAnimation]);
70
+
71
+ useEffect(() => {
72
+ let interval: ReturnType<typeof setInterval> | undefined;
73
+
74
+ if (isAnimating) {
75
+ interval = setInterval(() => {
76
+ if (visibleLetterCount < text.length) {
77
+ setVisibleLetterCount((prev) => prev + 1);
78
+ } else if (scrambleOffset < scrambledLetterCount) {
79
+ setScrambleOffset((prev) => prev + 1);
80
+ } else {
81
+ clearInterval(interval);
82
+ setIsAnimating(false);
83
+ onComplete?.();
84
+ }
85
+
86
+ const remainingSpace = Math.max(0, text.length - visibleLetterCount);
87
+ const currentScrambleCount = Math.min(remainingSpace, scrambledLetterCount);
88
+ const scrambledPart = Array(currentScrambleCount)
89
+ .fill(0)
90
+ .map(() => characters[Math.floor(Math.random() * characters.length)])
91
+ .join('');
92
+
93
+ setDisplayText(text.slice(0, visibleLetterCount) + scrambledPart);
94
+ }, scrambleSpeed);
95
+ }
96
+
97
+ return () => {
98
+ if (interval) clearInterval(interval);
99
+ };
100
+ }, [isAnimating, text, visibleLetterCount, scrambleOffset, scrambledLetterCount, characters, scrambleSpeed, onComplete]);
101
+
102
+ return (
103
+ <>
104
+ <span className="sr-only">{text}</span>
105
+ <span style={{ display: 'inline-block', whiteSpace: 'pre-wrap' }} aria-hidden="true">
106
+ <span className={className}>{displayText.slice(0, visibleLetterCount)}</span>
107
+ <span className={scrambledClassName}>{displayText.slice(visibleLetterCount)}</span>
108
+ </span>
109
+ </>
110
+ );
111
+ },
112
+ );
113
+
114
+ ScrambleText.displayName = 'ScrambleText';
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Ashutoshx7
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,69 @@
1
+ .morph-text-root {
2
+ position: relative;
3
+ display: flex;
4
+ flex-direction: column;
5
+ align-items: center;
6
+ }
7
+
8
+ .morph-text-defs {
9
+ position: absolute;
10
+ width: 0;
11
+ height: 0;
12
+ pointer-events: none;
13
+ }
14
+
15
+ .morph-text-container {
16
+ position: relative;
17
+ font-weight: 700;
18
+ user-select: none;
19
+ }
20
+
21
+ .morph-word-rotator {
22
+ position: relative;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ height: 1.2em;
27
+ min-width: 14ch;
28
+ }
29
+
30
+ .morph-word {
31
+ position: absolute;
32
+ top: 50%;
33
+ left: 50%;
34
+ transform: translate(-50%, -50%);
35
+ opacity: 0;
36
+ white-space: nowrap;
37
+ animation-name: morph-word-rotate;
38
+ animation-timing-function: ease-in-out;
39
+ animation-iteration-count: infinite;
40
+ animation-fill-mode: both;
41
+ }
42
+
43
+ @keyframes morph-word-rotate {
44
+ 0% {
45
+ opacity: 0;
46
+ filter: blur(20px);
47
+ transform: translate(-50%, -50%) scale(0.8);
48
+ }
49
+ 5% {
50
+ opacity: 0.5;
51
+ filter: blur(10px);
52
+ }
53
+ 15%,
54
+ 35% {
55
+ opacity: 1;
56
+ filter: blur(0px);
57
+ transform: translate(-50%, -50%) scale(1);
58
+ }
59
+ 45% {
60
+ opacity: 0.5;
61
+ filter: blur(10px);
62
+ }
63
+ 50%,
64
+ 100% {
65
+ opacity: 0;
66
+ filter: blur(20px);
67
+ transform: translate(-50%, -50%) scale(1.2);
68
+ }
69
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ // Vengeance UI Morph Text(MIT, vendor/transitions/vengeance-ui/morph-text.json)를 Tailwind 없이 옮긴 판.
4
+ // 낱말마다 같은 CSS 애니메이션을 시차만 두고 무한히 돌리고, 위에 끈적한 문턱 필터를 걸어 녹아 바뀌는 것처럼 보인다.
5
+ // 움직임이 전부 CSS animation 이라 영상 시계(getAnimations)로 바로 맞출 수 있다.
6
+ // 원본과 다른 곳: Google Fonts(Space Grotesk) @import 와 아래 작은 글(subtext)을 뺐다 — 글꼴은 허브 글꼴을 따른다. 키프레임은 MorphText.css 로.
7
+
8
+ import { useId } from 'react';
9
+ import './MorphText.css';
10
+
11
+ type MorphTextProps = {
12
+ words?: string[];
13
+ /** 낱말 하나가 머무는 ms */
14
+ interval?: number;
15
+ fontSize?: string;
16
+ className?: string;
17
+ };
18
+
19
+ export function MorphText({ words = ['CREATE', 'DESIGN', 'DEVELOP'], interval = 3000, fontSize = 'clamp(3rem, 15vw, 10rem)', className }: MorphTextProps) {
20
+ const filterId = `morph-threshold-${useId().replace(/:/g, '')}`;
21
+ const totalDuration = (interval / 1000) * words.length;
22
+ const wordDuration = interval / 1000;
23
+
24
+ return (
25
+ <div className={className ? `morph-text-root ${className}` : 'morph-text-root'}>
26
+ <svg aria-hidden="true" focusable="false" className="morph-text-defs">
27
+ <defs>
28
+ <filter id={filterId}>
29
+ <feColorMatrix in="SourceGraphic" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 25 -9" result="goo" />
30
+ <feComposite in="SourceGraphic" in2="goo" operator="atop" />
31
+ </filter>
32
+ </defs>
33
+ </svg>
34
+ <div className="morph-text-container" style={{ fontSize, filter: `url(#${filterId})` }}>
35
+ <div className="morph-word-rotator">
36
+ {words.map((word, i) => (
37
+ <span key={`${word}-${i}`} className="morph-word" style={{ animationDelay: `${i * wordDuration}s`, animationDuration: `${totalDuration}s` }}>
38
+ {word}
39
+ </span>
40
+ ))}
41
+ </div>
42
+ </div>
43
+ </div>
44
+ );
45
+ }