@yewdiel21/motion-kit 0.0.0-stage → 0.9.0

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 (391) hide show
  1. package/NOTICE +38 -0
  2. package/README.md +80 -2
  3. package/bin/motion-kit.mjs +242 -0
  4. package/css.d.ts +2 -0
  5. package/dist/auto/index.d.ts +63 -0
  6. package/dist/auto/index.js +207 -0
  7. package/dist/auto/index.js.map +1 -0
  8. package/dist/chunk-2BQGFOA7.js +177 -0
  9. package/dist/chunk-2BQGFOA7.js.map +1 -0
  10. package/dist/chunk-34XYOXHG.js +2374 -0
  11. package/dist/chunk-34XYOXHG.js.map +1 -0
  12. package/dist/chunk-6OJAP6ZL.js +832 -0
  13. package/dist/chunk-6OJAP6ZL.js.map +1 -0
  14. package/dist/chunk-ABNRDDO7.js +28 -0
  15. package/dist/chunk-ABNRDDO7.js.map +1 -0
  16. package/dist/chunk-FS2HJVVY.js +3320 -0
  17. package/dist/chunk-FS2HJVVY.js.map +1 -0
  18. package/dist/chunk-J27KH2A2.js +157 -0
  19. package/dist/chunk-J27KH2A2.js.map +1 -0
  20. package/dist/chunk-KLY5BRRI.js +18 -0
  21. package/dist/chunk-KLY5BRRI.js.map +1 -0
  22. package/dist/chunk-Q4LVFNLN.js +1617 -0
  23. package/dist/chunk-Q4LVFNLN.js.map +1 -0
  24. package/dist/chunk-RFDIZYFX.js +304 -0
  25. package/dist/chunk-RFDIZYFX.js.map +1 -0
  26. package/dist/chunk-TGV27ZWM.js +17 -0
  27. package/dist/chunk-TGV27ZWM.js.map +1 -0
  28. package/dist/confetti-BSX1CLLV.d.ts +41 -0
  29. package/dist/css/components/chat-background.css +87 -0
  30. package/dist/css/components/chat.css +237 -0
  31. package/dist/css/components/cursor.css +178 -0
  32. package/dist/css/components/dialog.css +259 -0
  33. package/dist/css/components/island.css +301 -0
  34. package/dist/css/components/menu.css +105 -0
  35. package/dist/css/components/nav.css +1054 -0
  36. package/dist/css/components/steps.css +50 -0
  37. package/dist/css/components/theme-picker.css +115 -0
  38. package/dist/css/components/toast.css +115 -0
  39. package/dist/css/controls.css +950 -0
  40. package/dist/css/effects.css +587 -0
  41. package/dist/css/fonts.css +3 -0
  42. package/dist/css/themes/black-green.css +126 -0
  43. package/dist/css/themes/black.css +126 -0
  44. package/dist/css/themes/blue.css +126 -0
  45. package/dist/css/themes/brown.css +126 -0
  46. package/dist/css/themes/cold-blue.css +126 -0
  47. package/dist/css/themes/funny.css +126 -0
  48. package/dist/css/themes/gray.css +126 -0
  49. package/dist/css/themes/green.css +126 -0
  50. package/dist/css/themes/modern-1.css +126 -0
  51. package/dist/css/themes/neogreen.css +126 -0
  52. package/dist/css/themes/new.css +126 -0
  53. package/dist/css/themes/new2.css +126 -0
  54. package/dist/css/themes/oled.css +126 -0
  55. package/dist/css/themes/orange.css +126 -0
  56. package/dist/css/themes/pastel-pink.css +126 -0
  57. package/dist/css/themes/pink.css +126 -0
  58. package/dist/css/themes/purple.css +126 -0
  59. package/dist/css/themes/red.css +126 -0
  60. package/dist/css/themes/super-blue.css +126 -0
  61. package/dist/css/themes.css +2431 -0
  62. package/dist/css/tokens.css +452 -0
  63. package/dist/elements/index.d.ts +183 -0
  64. package/dist/elements/index.js +472 -0
  65. package/dist/elements/index.js.map +1 -0
  66. package/dist/engine-CUr9QijM.d.ts +46 -0
  67. package/dist/gl/index.d.ts +61 -0
  68. package/dist/gl/index.js +18 -0
  69. package/dist/gl/index.js.map +1 -0
  70. package/dist/index.css +5296 -0
  71. package/dist/index.d.ts +440 -0
  72. package/dist/index.js +390 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/react/index.d.ts +476 -0
  75. package/dist/react/index.js +969 -0
  76. package/dist/react/index.js.map +1 -0
  77. package/dist/reduced-BM4Ls0g5.d.ts +566 -0
  78. package/dist/reveal-WFUWDrei.d.ts +416 -0
  79. package/dist/store-Bqy_obnD.d.ts +195 -0
  80. package/dist/theme/generate.d.ts +18 -0
  81. package/dist/theme/generate.js +64 -0
  82. package/dist/theme/generate.js.map +1 -0
  83. package/dist/theme/index.d.ts +17 -0
  84. package/dist/theme/index.js +32 -0
  85. package/dist/theme/index.js.map +1 -0
  86. package/dist/theme-BFMoCzi6.d.ts +158 -0
  87. package/dist/types-BkS_raL8.d.ts +7 -0
  88. package/package.json +114 -3
  89. package/registry.json +469 -0
  90. package/skills/motion-kit/SKILL.md +189 -0
  91. package/skills/motion-kit/reference/components.md +86 -0
  92. package/skills/motion-kit/reference/design.md +92 -0
  93. package/skills/motion-kit/reference/recipes.md +172 -0
  94. package/src/auto/auto-init.ts +151 -0
  95. package/src/auto/index.ts +3 -0
  96. package/src/auto/parse-options.ts +68 -0
  97. package/src/components/chat/README.md +90 -0
  98. package/src/components/chat/chat.css +236 -0
  99. package/src/components/chat/element.ts +98 -0
  100. package/src/components/chat/engine.ts +295 -0
  101. package/src/components/chat/react.tsx +196 -0
  102. package/src/components/chat-background/README.md +50 -0
  103. package/src/components/chat-background/audio.ts +56 -0
  104. package/src/components/chat-background/chat-background.css +86 -0
  105. package/src/components/chat-background/element.ts +31 -0
  106. package/src/components/chat-background/engine.ts +108 -0
  107. package/src/components/chat-background/react.tsx +36 -0
  108. package/src/components/cursor/README.md +53 -0
  109. package/src/components/cursor/cursor.css +177 -0
  110. package/src/components/cursor/element.ts +23 -0
  111. package/src/components/cursor/engine.ts +254 -0
  112. package/src/components/cursor/react.tsx +26 -0
  113. package/src/components/dialog/README.md +106 -0
  114. package/src/components/dialog/dialog.css +258 -0
  115. package/src/components/dialog/element.ts +118 -0
  116. package/src/components/dialog/engine.ts +432 -0
  117. package/src/components/dialog/react.tsx +200 -0
  118. package/src/components/icon.tsx +10 -0
  119. package/src/components/icons.ts +24 -0
  120. package/src/components/island/README.md +91 -0
  121. package/src/components/island/element.ts +45 -0
  122. package/src/components/island/engine.ts +200 -0
  123. package/src/components/island/island.css +300 -0
  124. package/src/components/island/react.tsx +115 -0
  125. package/src/components/menu/README.md +67 -0
  126. package/src/components/menu/element.ts +55 -0
  127. package/src/components/menu/engine.ts +142 -0
  128. package/src/components/menu/menu.css +104 -0
  129. package/src/components/menu/react.tsx +82 -0
  130. package/src/components/nav/README.md +85 -0
  131. package/src/components/nav/element.ts +39 -0
  132. package/src/components/nav/engine.ts +164 -0
  133. package/src/components/nav/nav.css +1053 -0
  134. package/src/components/nav/react.tsx +90 -0
  135. package/src/components/steps/README.md +70 -0
  136. package/src/components/steps/element.ts +43 -0
  137. package/src/components/steps/engine.ts +185 -0
  138. package/src/components/steps/react.tsx +91 -0
  139. package/src/components/steps/steps.css +49 -0
  140. package/src/components/theme-picker/README.md +57 -0
  141. package/src/components/theme-picker/element.ts +20 -0
  142. package/src/components/theme-picker/engine.ts +91 -0
  143. package/src/components/theme-picker/react.tsx +31 -0
  144. package/src/components/theme-picker/theme-picker.css +114 -0
  145. package/src/components/toast/README.md +60 -0
  146. package/src/components/toast/element.ts +25 -0
  147. package/src/components/toast/engine.ts +483 -0
  148. package/src/components/toast/react.tsx +22 -0
  149. package/src/components/toast/store.ts +122 -0
  150. package/src/components/toast/toast.css +114 -0
  151. package/src/core/channels.ts +165 -0
  152. package/src/core/curves.ts +35 -0
  153. package/src/core/frame.ts +12 -0
  154. package/src/core/gsap.ts +65 -0
  155. package/src/core/loop.ts +44 -0
  156. package/src/core/math.ts +9 -0
  157. package/src/core/physics-spring.ts +59 -0
  158. package/src/core/pointer.ts +63 -0
  159. package/src/core/reduced.ts +21 -0
  160. package/src/core/scope.ts +70 -0
  161. package/src/core/scroll-track.ts +106 -0
  162. package/src/core/split.ts +68 -0
  163. package/src/core/spring.ts +22 -0
  164. package/src/core/styles.ts +31 -0
  165. package/src/core/timing.ts +204 -0
  166. package/src/core/types.ts +20 -0
  167. package/src/core/visible.ts +19 -0
  168. package/src/css/base.css +142 -0
  169. package/src/css/controls/README.md +122 -0
  170. package/src/css/controls/burger.css +67 -0
  171. package/src/css/controls/button.css +226 -0
  172. package/src/css/controls/checkbox.css +53 -0
  173. package/src/css/controls/chip.css +85 -0
  174. package/src/css/controls/field.css +124 -0
  175. package/src/css/controls/glass.css +220 -0
  176. package/src/css/controls/icon.css +26 -0
  177. package/src/css/controls/segmented.css +60 -0
  178. package/src/css/controls/switch.css +72 -0
  179. package/src/css/default-theme.css +123 -0
  180. package/src/css/effects/README.md +134 -0
  181. package/src/css/effects/anim.css +224 -0
  182. package/src/css/effects/fade.css +15 -0
  183. package/src/css/effects/float.css +64 -0
  184. package/src/css/effects/frost.css +62 -0
  185. package/src/css/effects/grain.css +58 -0
  186. package/src/css/effects/loading.css +60 -0
  187. package/src/css/effects/success.css +22 -0
  188. package/src/css/effects/text.css +67 -0
  189. package/src/css/fonts.css +3 -0
  190. package/src/css/states.css +51 -0
  191. package/src/css/themes/black-green.css +126 -0
  192. package/src/css/themes/black.css +126 -0
  193. package/src/css/themes/blue.css +126 -0
  194. package/src/css/themes/brown.css +126 -0
  195. package/src/css/themes/cold-blue.css +126 -0
  196. package/src/css/themes/funny.css +126 -0
  197. package/src/css/themes/gray.css +126 -0
  198. package/src/css/themes/green.css +126 -0
  199. package/src/css/themes/modern-1.css +126 -0
  200. package/src/css/themes/neogreen.css +126 -0
  201. package/src/css/themes/new.css +126 -0
  202. package/src/css/themes/new2.css +126 -0
  203. package/src/css/themes/oled.css +126 -0
  204. package/src/css/themes/orange.css +126 -0
  205. package/src/css/themes/pastel-pink.css +126 -0
  206. package/src/css/themes/pink.css +126 -0
  207. package/src/css/themes/purple.css +126 -0
  208. package/src/css/themes/red.css +126 -0
  209. package/src/css/themes/super-blue.css +126 -0
  210. package/src/css/tokens.css +182 -0
  211. package/src/elements/base.ts +76 -0
  212. package/src/elements/index.ts +48 -0
  213. package/src/gl/index.ts +15 -0
  214. package/src/gl/liquid-blend/README.md +23 -0
  215. package/src/gl/liquid-blend/create-liquid-blend.ts +7 -0
  216. package/src/gl/liquid-blend/shader.ts +9 -0
  217. package/src/gl/matcap/README.md +23 -0
  218. package/src/gl/matcap/create-matcap.ts +7 -0
  219. package/src/gl/matcap/shader.ts +16 -0
  220. package/src/gl/particle-hero/README.md +41 -0
  221. package/src/gl/particle-hero/create-particle-hero.ts +544 -0
  222. package/src/gl/particle-hero/particle-hero.css +19 -0
  223. package/src/gl/particle-hero/shaders.ts +84 -0
  224. package/src/gl/shader-blur/README.md +23 -0
  225. package/src/gl/shader-blur/create-shader-blur.ts +7 -0
  226. package/src/gl/shader-blur/shader.ts +11 -0
  227. package/src/gl/shader-grain/README.md +23 -0
  228. package/src/gl/shader-grain/create-shader-grain.ts +7 -0
  229. package/src/gl/shader-grain/shader.ts +6 -0
  230. package/src/gl/shared/create-shader-effect.ts +186 -0
  231. package/src/gl/shared/shader.css +3 -0
  232. package/src/gl/shared/shaders.ts +30 -0
  233. package/src/index.ts +177 -0
  234. package/src/motion/anchor.ts +45 -0
  235. package/src/motion/chat.ts +97 -0
  236. package/src/motion/confetti.ts +130 -0
  237. package/src/motion/count.ts +95 -0
  238. package/src/motion/details.ts +70 -0
  239. package/src/motion/drag-dismiss.ts +231 -0
  240. package/src/motion/effects.ts +81 -0
  241. package/src/motion/entrances.ts +27 -0
  242. package/src/motion/floating.ts +389 -0
  243. package/src/motion/flow.ts +115 -0
  244. package/src/motion/indicator.ts +44 -0
  245. package/src/motion/layout.ts +39 -0
  246. package/src/motion/liquid.ts +227 -0
  247. package/src/motion/morph.ts +540 -0
  248. package/src/motion/reveal.ts +112 -0
  249. package/src/motion/sheet.ts +65 -0
  250. package/src/motion/surface.ts +108 -0
  251. package/src/react/animated-number.tsx +30 -0
  252. package/src/react/flip-group.tsx +44 -0
  253. package/src/react/frost.tsx +33 -0
  254. package/src/react/index.ts +80 -0
  255. package/src/react/slot-text.tsx +44 -0
  256. package/src/react/theme-provider.tsx +42 -0
  257. package/src/react/use-card-dock.ts +9 -0
  258. package/src/react/use-cascade.ts +9 -0
  259. package/src/react/use-contrast.ts +9 -0
  260. package/src/react/use-counter.ts +8 -0
  261. package/src/react/use-curved-carousel.ts +10 -0
  262. package/src/react/use-depth-grid.ts +9 -0
  263. package/src/react/use-details.ts +7 -0
  264. package/src/react/use-dock-pill.ts +10 -0
  265. package/src/react/use-drag-ruler.ts +10 -0
  266. package/src/react/use-fill-text.ts +9 -0
  267. package/src/react/use-fit-text.ts +8 -0
  268. package/src/react/use-flow.ts +8 -0
  269. package/src/react/use-frame-zoom.ts +9 -0
  270. package/src/react/use-infinite-reel.ts +8 -0
  271. package/src/react/use-letters.ts +9 -0
  272. package/src/react/use-liquid-blend.ts +8 -0
  273. package/src/react/use-marquee.ts +9 -0
  274. package/src/react/use-mask-text.ts +9 -0
  275. package/src/react/use-matcap.ts +8 -0
  276. package/src/react/use-parallax.ts +9 -0
  277. package/src/react/use-particle-hero.ts +9 -0
  278. package/src/react/use-pile.ts +8 -0
  279. package/src/react/use-pop.ts +8 -0
  280. package/src/react/use-radial-menu.ts +10 -0
  281. package/src/react/use-recipe.ts +38 -0
  282. package/src/react/use-reflow.ts +9 -0
  283. package/src/react/use-repel-bento.ts +8 -0
  284. package/src/react/use-reveal.ts +8 -0
  285. package/src/react/use-rotator.ts +10 -0
  286. package/src/react/use-shader-blur.ts +8 -0
  287. package/src/react/use-shader-grain.ts +8 -0
  288. package/src/react/use-slot-text.ts +17 -0
  289. package/src/react/use-snap-strip.ts +9 -0
  290. package/src/react/use-sortable.ts +14 -0
  291. package/src/react/use-spring-cards.ts +8 -0
  292. package/src/react/use-stretch-footer.ts +20 -0
  293. package/src/react/use-tabs.ts +10 -0
  294. package/src/react/use-text-roll.ts +9 -0
  295. package/src/react/use-ticket-menu.ts +15 -0
  296. package/src/react/use-words.ts +8 -0
  297. package/src/recipes/card-dock/README.md +36 -0
  298. package/src/recipes/card-dock/card-dock.css +178 -0
  299. package/src/recipes/card-dock/create-card-dock.ts +97 -0
  300. package/src/recipes/cascade/README.md +33 -0
  301. package/src/recipes/cascade/create-cascade.ts +111 -0
  302. package/src/recipes/contrast/README.md +29 -0
  303. package/src/recipes/contrast/create-contrast.ts +68 -0
  304. package/src/recipes/counter/README.md +42 -0
  305. package/src/recipes/counter/create-counter.ts +84 -0
  306. package/src/recipes/curved-carousel/README.md +50 -0
  307. package/src/recipes/curved-carousel/carousel-math.ts +20 -0
  308. package/src/recipes/curved-carousel/create-curved-carousel.ts +257 -0
  309. package/src/recipes/curved-carousel/curved-carousel.css +50 -0
  310. package/src/recipes/depth-grid/README.md +30 -0
  311. package/src/recipes/depth-grid/create-depth-grid.ts +120 -0
  312. package/src/recipes/details/README.md +31 -0
  313. package/src/recipes/details/create-details.ts +27 -0
  314. package/src/recipes/dock-pill/README.md +23 -0
  315. package/src/recipes/dock-pill/create-dock-pill.ts +68 -0
  316. package/src/recipes/dock-pill/dock-pill.css +5 -0
  317. package/src/recipes/drag-ruler/README.md +21 -0
  318. package/src/recipes/drag-ruler/create-drag-ruler.ts +95 -0
  319. package/src/recipes/drag-ruler/drag-ruler.css +16 -0
  320. package/src/recipes/fill-text/README.md +27 -0
  321. package/src/recipes/fill-text/create-fill-text.ts +93 -0
  322. package/src/recipes/fit-text/README.md +37 -0
  323. package/src/recipes/fit-text/create-fit-text.ts +55 -0
  324. package/src/recipes/fit-text/fit-text.css +6 -0
  325. package/src/recipes/flow/README.md +46 -0
  326. package/src/recipes/flow/create-flow.ts +55 -0
  327. package/src/recipes/frame-zoom/README.md +29 -0
  328. package/src/recipes/frame-zoom/create-frame-zoom.ts +91 -0
  329. package/src/recipes/infinite-reel/README.md +23 -0
  330. package/src/recipes/infinite-reel/create-infinite-reel.ts +140 -0
  331. package/src/recipes/infinite-reel/infinite-reel.css +8 -0
  332. package/src/recipes/letters/README.md +30 -0
  333. package/src/recipes/letters/create-letters.ts +113 -0
  334. package/src/recipes/marquee/README.md +32 -0
  335. package/src/recipes/marquee/create-marquee.ts +74 -0
  336. package/src/recipes/marquee/marquee.css +41 -0
  337. package/src/recipes/mask-text/README.md +39 -0
  338. package/src/recipes/mask-text/create-mask-text.ts +134 -0
  339. package/src/recipes/parallax/README.md +31 -0
  340. package/src/recipes/parallax/create-parallax.ts +74 -0
  341. package/src/recipes/pile/README.md +48 -0
  342. package/src/recipes/pile/create-pile.ts +50 -0
  343. package/src/recipes/pile/pile-math.ts +27 -0
  344. package/src/recipes/pile/pile.css +63 -0
  345. package/src/recipes/pill/README.md +30 -0
  346. package/src/recipes/pill/pill.css +48 -0
  347. package/src/recipes/pop/README.md +36 -0
  348. package/src/recipes/pop/create-pop.ts +52 -0
  349. package/src/recipes/radial-menu/README.md +44 -0
  350. package/src/recipes/radial-menu/create-radial-menu.ts +96 -0
  351. package/src/recipes/radial-menu/radial-menu.css +45 -0
  352. package/src/recipes/reflow/README.md +47 -0
  353. package/src/recipes/reflow/create-reflow.ts +49 -0
  354. package/src/recipes/repel-bento/README.md +21 -0
  355. package/src/recipes/repel-bento/create-repel-bento.ts +65 -0
  356. package/src/recipes/repel-bento/repel-bento.css +3 -0
  357. package/src/recipes/reveal/README.md +36 -0
  358. package/src/recipes/reveal/create-reveal.ts +48 -0
  359. package/src/recipes/rotator/README.md +21 -0
  360. package/src/recipes/rotator/create-rotator.ts +66 -0
  361. package/src/recipes/rotator/rotator.css +3 -0
  362. package/src/recipes/slot-text/README.md +44 -0
  363. package/src/recipes/slot-text/create-slot-text.ts +244 -0
  364. package/src/recipes/slot-text/slot-text.css +42 -0
  365. package/src/recipes/snap-strip/README.md +31 -0
  366. package/src/recipes/snap-strip/create-snap-strip.ts +133 -0
  367. package/src/recipes/snap-strip/snap-strip.css +23 -0
  368. package/src/recipes/sortable/README.md +88 -0
  369. package/src/recipes/sortable/create-sortable.ts +863 -0
  370. package/src/recipes/sortable/sortable.css +40 -0
  371. package/src/recipes/spring-cards/README.md +48 -0
  372. package/src/recipes/spring-cards/create-spring-cards.ts +140 -0
  373. package/src/recipes/spring-cards/spring-cards.css +23 -0
  374. package/src/recipes/stretch-footer/README.md +44 -0
  375. package/src/recipes/stretch-footer/create-stretch-footer.ts +69 -0
  376. package/src/recipes/stretch-footer/stretch-footer.css +28 -0
  377. package/src/recipes/tabs/README.md +39 -0
  378. package/src/recipes/tabs/create-tabs.ts +91 -0
  379. package/src/recipes/tabs/tabs.css +19 -0
  380. package/src/recipes/text-roll/README.md +35 -0
  381. package/src/recipes/text-roll/create-text-roll.ts +137 -0
  382. package/src/recipes/text-roll/text-roll.css +21 -0
  383. package/src/recipes/ticket-menu/README.md +24 -0
  384. package/src/recipes/ticket-menu/create-ticket-menu.ts +6 -0
  385. package/src/recipes/words/README.md +43 -0
  386. package/src/recipes/words/create-words.ts +114 -0
  387. package/src/theme/css-format.ts +32 -0
  388. package/src/theme/generate.ts +72 -0
  389. package/src/theme/index.ts +15 -0
  390. package/src/theme/presets.ts +79 -0
  391. package/src/theme/theme.ts +119 -0
@@ -0,0 +1,544 @@
1
+ import { whileVisible } from '../../core/loop';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { TIMING } from '../../core/timing';
5
+ import { warnOnce, type Instance } from '../../core/types';
6
+ import { fullscreenVertex, pointsFragment, pointsVertex, simulationFragment } from './shaders';
7
+
8
+ /*
9
+ * iyo.ai's hero, adapted: a cloud of points simulated on the GPU (two RGBA32F
10
+ * textures, position + velocity, ping-pong) that settles on the zero lines of a
11
+ * shifting interference pattern — Chladni modes from [3, 5, 7] every 6–12 s, a
12
+ * frequency of 20–45 every 5–10 s, a distortion tween of 1.2–3 s — while part of
13
+ * the points (40–80 %, eased) leave to draw a word, then return. The pointer
14
+ * pushes them away in proportion to its speed. It fades in over 1 s after 0.3 s.
15
+ *
16
+ * iyo runs 1 638 400 points on Three.js with its own word pictures; this is a
17
+ * smaller own implementation (size² points, 16 384 by default) with words drawn
18
+ * locally. Without WebGL2 float targets it falls back to ~1800 points on a 2D
19
+ * canvas; with no canvas at all it does nothing. Reduced motion draws one still
20
+ * frame. The canvas is decorative (aria-hidden): the hero's text lives in HTML.
21
+ */
22
+
23
+ export type ParticleHeroMode = 'webgl2' | 'canvas2d' | 'none';
24
+
25
+ export interface ParticleHeroOptions {
26
+ /** Words the points draw in turn. */
27
+ words?: string[];
28
+ /** The state texture's side: size² points (32–256). */
29
+ size?: number;
30
+ /** Point size in CSS pixels (1–8). */
31
+ pointSize?: number;
32
+ /** RGB, 0–1 each. */
33
+ color?: [number, number, number];
34
+ seed?: number;
35
+ /** Skip WebGL (to try the fallback). */
36
+ forceFallback?: boolean;
37
+ onMode?: (mode: ParticleHeroMode) => void;
38
+ }
39
+
40
+ export interface ParticleHeroInstance extends Instance {
41
+ readonly mode: ParticleHeroMode;
42
+ /** Draw this word next (the cycle carries on afterwards). */
43
+ setWord(word: string): void;
44
+ /** Share of points on the word, 0–1; null gives control back to the cycle. */
45
+ setTextureMix(value: number | null): void;
46
+ /** Base field: 0 circles, 1 grid, 2 squares. */
47
+ setPattern(value: number): void;
48
+ pause(): void;
49
+ resume(): void;
50
+ }
51
+
52
+ interface State {
53
+ time: number;
54
+ dt: number;
55
+ mix: number;
56
+ frequency: number;
57
+ distortion: number;
58
+ noise: number;
59
+ jitter: number;
60
+ m: number;
61
+ n: number;
62
+ mouseX: number;
63
+ mouseY: number;
64
+ mouseSpeed: number;
65
+ pattern: number;
66
+ aspect: number;
67
+ dpr: number;
68
+ opacity: number;
69
+ }
70
+
71
+ interface Renderer {
72
+ mode: Exclude<ParticleHeroMode, 'none'>;
73
+ render(state: State, steps?: number): void;
74
+ setMask(data: Float32Array): void;
75
+ destroy(): void;
76
+ }
77
+
78
+ const instances = new WeakMap<HTMLElement, ParticleHeroInstance>();
79
+ const clamp = (x: number, a: number, b: number) => Math.max(a, Math.min(b, x));
80
+ const mix = (a: number, b: number, t: number) => a + (b - a) * t;
81
+ /** power2.inOut-like cubic. */
82
+ const inOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
83
+
84
+ function random(seed: number) {
85
+ let s = seed >>> 0;
86
+ return () => {
87
+ s = (1664525 * s + 1013904223) >>> 0;
88
+ return s / 4294967296;
89
+ };
90
+ }
91
+
92
+ /** Points sampled from the dark pixels of a word drawn on a 640 × 320 canvas (iyo: red < 120). */
93
+ function wordMask(word: string, count: number, rng: () => number) {
94
+ const result = new Float32Array(count * 4);
95
+ const canvas = document.createElement('canvas');
96
+ canvas.width = 640;
97
+ canvas.height = 320;
98
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
99
+ const dark: Array<[number, number]> = [];
100
+ if (ctx) {
101
+ ctx.fillStyle = '#fff';
102
+ ctx.fillRect(0, 0, 640, 320);
103
+ ctx.fillStyle = '#000';
104
+ ctx.textAlign = 'center';
105
+ ctx.textBaseline = 'middle';
106
+ let size = 140;
107
+ ctx.font = `600 ${size}px system-ui, sans-serif`;
108
+ while (ctx.measureText(word).width > 590 && size > 16) {
109
+ size -= 4;
110
+ ctx.font = `600 ${size}px system-ui, sans-serif`;
111
+ }
112
+ ctx.fillText(word, 320, 160);
113
+ const pixels = ctx.getImageData(0, 0, 640, 320).data;
114
+ for (let y = 0; y < 320; y += 2) for (let x = 0; x < 640; x += 2) if (pixels[(y * 640 + x) * 4]! < 120) dark.push([x, y]);
115
+ }
116
+ for (let i = 0; i < count; i++) {
117
+ const [x, y] = dark.length ? dark[Math.floor(rng() * dark.length)]! : [320, 160];
118
+ result[i * 4] = (x / 640 - 0.5) * 1.75 + (rng() - 0.5) * 0.004;
119
+ result[i * 4 + 1] = (0.5 - y / 320) * 0.9 + (rng() - 0.5) * 0.004;
120
+ }
121
+ return result;
122
+ }
123
+
124
+ function gpuRenderer(
125
+ canvas: HTMLCanvasElement,
126
+ size: number,
127
+ pointSize: number,
128
+ color: number[],
129
+ initial: Float32Array,
130
+ mask: Float32Array,
131
+ ): Renderer {
132
+ const gl = canvas.getContext('webgl2', { alpha: true, antialias: false, powerPreference: 'high-performance', premultipliedAlpha: true });
133
+ if (!gl || !gl.getExtension('EXT_color_buffer_float')) throw new Error('WebGL2 float render targets unavailable');
134
+ const releases: Array<() => void> = [];
135
+ const release = () => {
136
+ for (const fn of releases.reverse()) fn();
137
+ releases.length = 0;
138
+ };
139
+ try {
140
+ const shader = (type: number, source: string) => {
141
+ const s = gl.createShader(type)!;
142
+ releases.push(() => gl.deleteShader(s));
143
+ gl.shaderSource(s, source);
144
+ gl.compileShader(s);
145
+ if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s) ?? 'shader');
146
+ return s;
147
+ };
148
+ const program = (vs: string, fs: string) => {
149
+ const p = gl.createProgram()!;
150
+ releases.push(() => gl.deleteProgram(p));
151
+ gl.attachShader(p, shader(gl.VERTEX_SHADER, vs));
152
+ gl.attachShader(p, shader(gl.FRAGMENT_SHADER, fs));
153
+ gl.linkProgram(p);
154
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p) ?? 'program');
155
+ return p;
156
+ };
157
+ const texture = (data: Float32Array) => {
158
+ const tex = gl.createTexture()!;
159
+ releases.push(() => gl.deleteTexture(tex));
160
+ gl.bindTexture(gl.TEXTURE_2D, tex);
161
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
162
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
163
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
164
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
165
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, size, size, 0, gl.RGBA, gl.FLOAT, data);
166
+ return tex;
167
+ };
168
+ const sim = program(fullscreenVertex, simulationFragment);
169
+ const draw = program(pointsVertex, pointsFragment);
170
+ const states = [texture(initial), texture(initial)];
171
+ const maskTexture = texture(mask);
172
+ const buffers = states.map((tex) => {
173
+ const fb = gl.createFramebuffer()!;
174
+ releases.push(() => gl.deleteFramebuffer(fb));
175
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
176
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
177
+ if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) throw new Error('incomplete float framebuffer');
178
+ return fb;
179
+ });
180
+ const vao = gl.createVertexArray()!;
181
+ releases.push(() => gl.deleteVertexArray(vao));
182
+ const uniforms = (p: WebGLProgram, names: string[]) => Object.fromEntries(names.map((name) => [name, gl.getUniformLocation(p, name)]));
183
+ const simU = uniforms(sim, [
184
+ 'uState',
185
+ 'uMask',
186
+ 'uTime',
187
+ 'uDt',
188
+ 'uMix',
189
+ 'uFrequency',
190
+ 'uDistortion',
191
+ 'uNoise',
192
+ 'uJitter',
193
+ 'uMouseSpeed',
194
+ 'uAspect',
195
+ 'uModes',
196
+ 'uMouse',
197
+ 'uPattern',
198
+ ]);
199
+ const drawU = uniforms(draw, ['uState', 'uSize', 'uAspect', 'uDpr', 'uPointSize', 'uColor', 'uOpacity']);
200
+ const bind = (unit: number, tex: WebGLTexture) => {
201
+ gl.activeTexture(gl.TEXTURE0 + unit);
202
+ gl.bindTexture(gl.TEXTURE_2D, tex);
203
+ };
204
+ let read = 0;
205
+ return {
206
+ mode: 'webgl2',
207
+ render(s, steps = 1) {
208
+ gl.bindVertexArray(vao);
209
+ gl.disable(gl.BLEND);
210
+ gl.useProgram(sim);
211
+ gl.uniform1i(simU.uState!, 0);
212
+ gl.uniform1i(simU.uMask!, 1);
213
+ gl.uniform1f(simU.uTime!, s.time);
214
+ gl.uniform1f(simU.uDt!, s.dt);
215
+ gl.uniform1f(simU.uMix!, s.mix);
216
+ gl.uniform1f(simU.uFrequency!, s.frequency);
217
+ gl.uniform1f(simU.uDistortion!, s.distortion);
218
+ gl.uniform1f(simU.uNoise!, s.noise);
219
+ gl.uniform1f(simU.uJitter!, s.jitter);
220
+ gl.uniform1f(simU.uMouseSpeed!, s.mouseSpeed);
221
+ gl.uniform1f(simU.uAspect!, s.aspect);
222
+ gl.uniform2f(simU.uModes!, s.m, s.n);
223
+ gl.uniform2f(simU.uMouse!, s.mouseX, s.mouseY);
224
+ gl.uniform1i(simU.uPattern!, s.pattern);
225
+ bind(1, maskTexture);
226
+ for (let i = 0; i < steps; i++) {
227
+ bind(0, states[read]!);
228
+ gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[1 - read]!);
229
+ gl.viewport(0, 0, size, size);
230
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
231
+ read = 1 - read;
232
+ }
233
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
234
+ gl.viewport(0, 0, canvas.width, canvas.height);
235
+ gl.clearColor(0, 0, 0, 0);
236
+ gl.clear(gl.COLOR_BUFFER_BIT);
237
+ gl.useProgram(draw);
238
+ bind(0, states[read]!);
239
+ gl.uniform1i(drawU.uState!, 0);
240
+ gl.uniform1i(drawU.uSize!, size);
241
+ gl.uniform1f(drawU.uAspect!, s.aspect);
242
+ gl.uniform1f(drawU.uDpr!, s.dpr);
243
+ gl.uniform1f(drawU.uPointSize!, pointSize);
244
+ gl.uniform3fv(drawU.uColor!, color);
245
+ gl.uniform1f(drawU.uOpacity!, s.opacity);
246
+ gl.enable(gl.BLEND);
247
+ // RGB premultiplied, coverage accumulated once, for the page's compositing.
248
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
249
+ gl.drawArrays(gl.POINTS, 0, size * size);
250
+ },
251
+ setMask(data) {
252
+ bind(1, maskTexture);
253
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, size, size, gl.RGBA, gl.FLOAT, data);
254
+ },
255
+ destroy: release,
256
+ };
257
+ } catch (error) {
258
+ release();
259
+ throw error;
260
+ }
261
+ }
262
+
263
+ /** ~1800 points eased toward a breathing circle or the word: no Chladni field. */
264
+ function canvasRenderer(
265
+ canvas: HTMLCanvasElement,
266
+ size: number,
267
+ pointSize: number,
268
+ color: number[],
269
+ initial: Float32Array,
270
+ mask: Float32Array,
271
+ ): Renderer {
272
+ const ctx = canvas.getContext('2d');
273
+ if (!ctx) throw new Error('no 2D context');
274
+ const stride = Math.max(1, Math.floor((size * size) / 1800));
275
+ const ids: number[] = [];
276
+ for (let i = 0; i < size * size; i += stride) ids.push(i);
277
+ const points = Float32Array.from(initial);
278
+ let target = mask;
279
+ const rgb = color.map((c) => Math.round(c * 255)).join(',');
280
+ return {
281
+ mode: 'canvas2d',
282
+ setMask(data) {
283
+ target = data;
284
+ },
285
+ render(s, steps = 1) {
286
+ for (let step = 0; step < steps; step++)
287
+ for (const i of ids) {
288
+ const o = i * 4;
289
+ const angle = i * 2.3999632297;
290
+ const radius = 0.3 + 0.52 * Math.sin(angle * 0.025 + s.time * 0.07);
291
+ let x = Math.cos(angle) * radius;
292
+ let y = Math.sin(angle) * radius;
293
+ if (((i * 137) % 1000) / 1000 < s.mix) {
294
+ x = target[o]!;
295
+ y = target[o + 1]!;
296
+ }
297
+ points[o] = points[o]! + (x - points[o]!) * 0.075;
298
+ points[o + 1] = points[o + 1]! + (y - points[o + 1]!) * 0.075;
299
+ }
300
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
301
+ ctx.fillStyle = `rgba(${rgb},${0.4 * s.opacity})`;
302
+ for (const i of ids) {
303
+ const x = (points[i * 4]! * 0.5 + 0.5) * canvas.width;
304
+ const y = (0.5 - points[i * 4 + 1]! * 0.5) * canvas.height;
305
+ ctx.beginPath();
306
+ ctx.arc(x, y, pointSize * s.dpr * 0.55, 0, Math.PI * 2);
307
+ ctx.fill();
308
+ }
309
+ },
310
+ destroy() {
311
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
312
+ },
313
+ };
314
+ }
315
+
316
+ const NONE: ParticleHeroInstance = {
317
+ mode: 'none',
318
+ setWord() {},
319
+ setTextureMix() {},
320
+ setPattern() {},
321
+ pause() {},
322
+ resume() {},
323
+ destroy() {},
324
+ };
325
+
326
+ export function createParticleHero(root: HTMLElement | null, options: ParticleHeroOptions = {}): ParticleHeroInstance {
327
+ if (!root) {
328
+ warnOnce('particle-hero: missing element');
329
+ return NONE;
330
+ }
331
+ const existing = instances.get(root);
332
+ if (existing) return existing;
333
+ const t = TIMING.particleHero;
334
+ const rng = random(options.seed ?? 1907);
335
+ const size = Math.round(clamp(options.size ?? t.size, 32, 256));
336
+ const pointSize = clamp(options.pointSize ?? t.pointSize, 1, 8);
337
+ const color = options.color ?? [1, 1, 1];
338
+ const words = options.words?.length ? options.words.map(String) : ['CREATE', 'NATURAL', 'IDEAS'];
339
+ const count = size * size;
340
+ const still = reduced();
341
+
342
+ const initial = new Float32Array(count * 4);
343
+ for (let i = 0; i < count; i++) {
344
+ initial[i * 4] = (rng() - 0.5) * 1.7;
345
+ initial[i * 4 + 1] = (rng() - 0.5) * 1.7;
346
+ }
347
+ let mask = wordMask(words[0]!, count, rng);
348
+
349
+ const makeCanvas = () => {
350
+ const canvas = document.createElement('canvas');
351
+ canvas.className = 'mk-particle-hero__canvas';
352
+ canvas.setAttribute('aria-hidden', 'true');
353
+ return canvas;
354
+ };
355
+ let canvas = makeCanvas();
356
+ let renderer: Renderer | null = null;
357
+ if (!options.forceFallback) {
358
+ try {
359
+ renderer = gpuRenderer(canvas, size, pointSize, color, initial, mask);
360
+ } catch {
361
+ canvas = makeCanvas();
362
+ }
363
+ }
364
+ if (!renderer) {
365
+ try {
366
+ renderer = canvasRenderer(canvas, size, pointSize, color, initial, mask);
367
+ } catch {
368
+ warnOnce('particle-hero: no WebGL2 and no 2D canvas — nothing drawn');
369
+ return NONE;
370
+ }
371
+ }
372
+ const draw = renderer;
373
+
374
+ const scope = createScope();
375
+ if (!root.classList.contains('mk-particle-hero')) {
376
+ root.classList.add('mk-particle-hero');
377
+ scope.add(() => {
378
+ root.classList.remove('mk-particle-hero');
379
+ if (root.getAttribute('class') === '') root.removeAttribute('class');
380
+ });
381
+ }
382
+ root.append(canvas);
383
+ scope.add(() => canvas.remove());
384
+ scope.attr(root, 'data-mk-mode', draw.mode);
385
+ scope.add(() => draw.destroy());
386
+
387
+ const state: State = {
388
+ time: 0,
389
+ dt: 1 / 60,
390
+ mix: still ? 0.8 : 0,
391
+ frequency: 35,
392
+ distortion: 2.4,
393
+ noise: 0.2,
394
+ jitter: 0.08,
395
+ m: 3,
396
+ n: 5,
397
+ mouseX: 9,
398
+ mouseY: 9,
399
+ mouseSpeed: 0,
400
+ pattern: 0,
401
+ aspect: 1,
402
+ dpr: 1,
403
+ opacity: still ? 1 : 0,
404
+ };
405
+ let paused = false;
406
+ let age = 0;
407
+ let wordIndex = 0;
408
+ let manualMix: number | null = null;
409
+ let nextFrequency = 5;
410
+ let nextModes = 6;
411
+ let distortion = { from: 2.4, to: rng() * 5, start: 0, end: 2.5 };
412
+ // Word cycle: points gather on the word, shift share, leave, next word.
413
+ let cycle = { stage: 0, start: 0, duration: 4.2, from: 0, to: 0.65 };
414
+ let pointer = { x: 9, y: 9 };
415
+ let previous = { x: 9, y: 9 };
416
+
417
+ const drawStill = () => {
418
+ state.opacity = 1;
419
+ state.mix = manualMix ?? 0.8;
420
+ state.mouseSpeed = 0;
421
+ state.dt = 1 / 60;
422
+ draw.render(state, t.stillSteps);
423
+ };
424
+ const isStill = () => still || paused;
425
+ const resize = () => {
426
+ const box = root.getBoundingClientRect();
427
+ const w = Math.max(1, box.width);
428
+ const h = Math.max(1, box.height);
429
+ state.dpr = Math.min(window.devicePixelRatio || 1, t.maxDpr);
430
+ state.aspect = w / h;
431
+ canvas.width = Math.round(w * state.dpr);
432
+ canvas.height = Math.round(h * state.dpr);
433
+ if (isStill()) drawStill();
434
+ };
435
+ const setWord = (word: string) => {
436
+ if (scope.disposed) return;
437
+ mask = wordMask(String(word), count, rng);
438
+ draw.setMask(mask);
439
+ if (isStill()) drawStill();
440
+ };
441
+
442
+ const tick = (frames: number) => {
443
+ if (paused || scope.disposed) return;
444
+ const dt = clamp(frames / 60, 0, 0.05);
445
+ age += dt;
446
+ state.time = age * 4;
447
+ state.dt = dt;
448
+ state.opacity = clamp((age - t.fadeDelay) / t.fadeIn, 0, 1);
449
+ state.mouseX = mix(state.mouseX, pointer.x, 0.1);
450
+ state.mouseY = mix(state.mouseY, pointer.y, 0.1);
451
+ const speed = Math.min(Math.hypot(state.mouseX - previous.x, state.mouseY - previous.y) * 5, 1);
452
+ state.mouseSpeed = mix(state.mouseSpeed, speed, 0.2);
453
+ previous = { x: state.mouseX, y: state.mouseY };
454
+ if (age >= nextFrequency) {
455
+ state.frequency = 20 + rng() * 25;
456
+ state.jitter = rng() * 0.3;
457
+ state.noise = rng() * 0.5;
458
+ nextFrequency = age + 5 + rng() * 5;
459
+ }
460
+ if (age >= nextModes) {
461
+ const modes = [3, 5, 7];
462
+ state.m = modes[Math.floor(rng() * 3)]!;
463
+ state.n = modes[Math.floor(rng() * 3)]!;
464
+ nextModes = age + 6 + rng() * 6;
465
+ }
466
+ if (age >= distortion.end) distortion = { from: state.distortion, to: rng() * 5, start: age, end: age + 1.2 + rng() * 1.8 };
467
+ state.distortion = mix(
468
+ distortion.from,
469
+ distortion.to,
470
+ inOut(clamp((age - distortion.start) / (distortion.end - distortion.start), 0, 1)),
471
+ );
472
+ if (age - cycle.start >= cycle.duration) {
473
+ const stage = (cycle.stage + 1) % 3;
474
+ cycle = { stage, start: age, from: cycle.to, to: 0, duration: 2 + rng() };
475
+ if (stage === 0) {
476
+ wordIndex = (wordIndex + 1) % words.length;
477
+ setWord(words[wordIndex]!);
478
+ }
479
+ if (stage !== 2) {
480
+ cycle.to = 0.4 + rng() * 0.4;
481
+ cycle.duration = 2 + rng() * 2;
482
+ }
483
+ }
484
+ state.mix = manualMix ?? mix(cycle.from, cycle.to, inOut(clamp((age - cycle.start) / cycle.duration, 0, 1)));
485
+ draw.render(state);
486
+ };
487
+
488
+ scope.on(
489
+ root,
490
+ 'pointermove',
491
+ (event) => {
492
+ const box = root.getBoundingClientRect();
493
+ const e = event as PointerEvent;
494
+ pointer = {
495
+ x: (((e.clientX - box.left) / Math.max(box.width, 1)) * 2 - 1) * Math.max(state.aspect, 1),
496
+ y: 1 - ((e.clientY - box.top) / Math.max(box.height, 1)) * 2,
497
+ };
498
+ },
499
+ { passive: true },
500
+ );
501
+ scope.on(root, 'pointerleave', () => (pointer = { x: 9, y: 9 }), { passive: true });
502
+ scope.on(canvas, 'webglcontextlost', (event) => {
503
+ event.preventDefault();
504
+ paused = true;
505
+ scope.attr(root, 'data-mk-mode', 'context-lost');
506
+ });
507
+ if (typeof ResizeObserver === 'function') scope.observe(new ResizeObserver(resize)).observe(root);
508
+ else scope.on(window, 'resize', resize);
509
+ // The shared ticker runs while the hero is near the screen and the tab is visible.
510
+ if (!still) scope.add(whileVisible(root, tick, '100px'));
511
+
512
+ const instance: ParticleHeroInstance = {
513
+ mode: draw.mode,
514
+ setWord,
515
+ setTextureMix(value) {
516
+ if (scope.disposed) return;
517
+ manualMix = value === null ? null : clamp(Number(value) || 0, 0, 1);
518
+ if (isStill()) drawStill();
519
+ },
520
+ setPattern(value) {
521
+ if (scope.disposed) return;
522
+ state.pattern = clamp(Math.round(Number(value) || 0), 0, 2);
523
+ if (isStill()) drawStill();
524
+ },
525
+ pause() {
526
+ if (!scope.disposed) paused = true;
527
+ },
528
+ resume() {
529
+ if (scope.disposed) return;
530
+ paused = false;
531
+ if (still) drawStill();
532
+ },
533
+ destroy() {
534
+ if (scope.disposed) return;
535
+ scope.dispose();
536
+ instances.delete(root);
537
+ },
538
+ };
539
+ instances.set(root, instance);
540
+ resize();
541
+ if (still) drawStill();
542
+ options.onMode?.(draw.mode);
543
+ return instance;
544
+ }
@@ -0,0 +1,19 @@
1
+ /* The particle hero's canvas fills its host, under the host's own content. The host sets the height. */
2
+ .mk-particle-hero {
3
+ position: relative;
4
+ isolation: isolate;
5
+ overflow: hidden;
6
+ }
7
+ .mk-particle-hero__canvas {
8
+ position: absolute;
9
+ inset: 0;
10
+ z-index: 0;
11
+ display: block;
12
+ width: 100%;
13
+ height: 100%;
14
+ pointer-events: none;
15
+ }
16
+ .mk-particle-hero > :not(.mk-particle-hero__canvas) {
17
+ position: relative;
18
+ z-index: 1;
19
+ }
@@ -0,0 +1,84 @@
1
+ /*
2
+ * Shaders for the particle hero. Own GLSL (not iyo's): state texture XY = position,
3
+ * ZW = velocity; the simulation pulls each point toward the zero lines of a
4
+ * radial / grid / square field mixed with a Chladni plate (four Newton-like
5
+ * projections, step .4) or toward a word, and pushes it away from the pointer.
6
+ */
7
+
8
+ export const fullscreenVertex = `#version 300 es
9
+ precision highp float;
10
+ out vec2 vUv;
11
+ void main() {
12
+ vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
13
+ vUv = p;
14
+ gl_Position = vec4(p * 2. - 1., 0., 1.);
15
+ }`;
16
+
17
+ export const simulationFragment = `#version 300 es
18
+ precision highp float;
19
+ in vec2 vUv;
20
+ uniform sampler2D uState, uMask;
21
+ uniform float uTime, uDt, uMix, uFrequency, uDistortion, uNoise, uJitter, uMouseSpeed, uAspect;
22
+ uniform vec2 uModes, uMouse;
23
+ uniform int uPattern;
24
+ out vec4 outState;
25
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
26
+ float field(vec2 p) {
27
+ float base = sin(length(p) * uFrequency * .35);
28
+ if (uPattern == 1) base = sin(p.x * uFrequency * .35) * sin(p.y * uFrequency * .35);
29
+ if (uPattern == 2) base = sin(max(abs(p.x), abs(p.y)) * uFrequency * .35);
30
+ float chladni = sin(uModes.x * 3.14159265 * (p.x + 1.) * .5) * sin(uModes.y * 3.14159265 * (p.y + 1.) * .5);
31
+ float spots = (hash(floor(p * 12.)) - .5) * uNoise * .08;
32
+ float jitter = sin(uTime * 42. + hash(p * 30.) * 6.283) * uJitter * .015;
33
+ return mix(base, chladni, clamp(uDistortion * .2, 0., .85)) + spots + jitter;
34
+ }
35
+ void main() {
36
+ vec4 state = texture(uState, vUv);
37
+ vec2 pos = state.xy;
38
+ vec2 vel = state.zw;
39
+ vec2 p = (vUv - .5) * 2.;
40
+ for (int i = 0; i < 4; i++) {
41
+ float f = field(p);
42
+ float e = .003;
43
+ vec2 g = vec2(field(p + vec2(e, 0.)) - field(p - vec2(e, 0.)), field(p + vec2(0., e)) - field(p - vec2(0., e))) / (2. * e);
44
+ float q = dot(g, g);
45
+ if (q > .00001) p -= f * g / q * .4;
46
+ }
47
+ vec2 word = texture(uMask, vUv).xy;
48
+ float text = step(hash(vUv), uMix);
49
+ vec2 target = mix(p * .88, word, text);
50
+ target.x *= max(uAspect, 1.);
51
+ float dt60 = uDt * 60.;
52
+ vel *= pow(.91, dt60);
53
+ vec2 delta = target - pos;
54
+ float dist = length(delta);
55
+ if (dist > .004 * (1. - text)) vel += delta / max(dist, .00001) * mix(.0016, .0009, text) * dt60;
56
+ vec2 away = pos - uMouse;
57
+ float r = length(away);
58
+ vel += away / max(r, .0001) * (1. - smoothstep(0., .28, r)) * uMouseSpeed * .017 * dt60;
59
+ vel += vec2(sin(uTime + vUv.x * 400.), cos(uTime + vUv.y * 400.)) * .00003 * dt60;
60
+ pos += vel * dt60;
61
+ outState = vec4(pos, vel);
62
+ }`;
63
+
64
+ export const pointsVertex = `#version 300 es
65
+ precision highp float;
66
+ uniform sampler2D uState;
67
+ uniform int uSize;
68
+ uniform float uAspect, uDpr, uPointSize;
69
+ void main() {
70
+ ivec2 uv = ivec2(gl_VertexID % uSize, gl_VertexID / uSize);
71
+ vec2 p = texelFetch(uState, uv, 0).xy;
72
+ gl_Position = vec4(p.x / max(uAspect, 1.), p.y, 0., 1.);
73
+ gl_PointSize = uPointSize * uDpr;
74
+ }`;
75
+
76
+ export const pointsFragment = `#version 300 es
77
+ precision highp float;
78
+ uniform vec3 uColor;
79
+ uniform float uOpacity;
80
+ out vec4 outColor;
81
+ void main() {
82
+ float a = (1. - smoothstep(.12, .5, length(gl_PointCoord - .5))) * .44 * uOpacity;
83
+ outColor = vec4(uColor, a);
84
+ }`;
@@ -0,0 +1,23 @@
1
+ # shader-blur
2
+
3
+ Desenfoque direccional de siete muestras por shader (Siena).
4
+
5
+ ```ts
6
+ import { createShaderBlur } from '@yewdiel21/motion-kit/gl';
7
+ import { useShaderBlur } from '@yewdiel21/motion-kit/react';
8
+ import '@yewdiel21/motion-kit/css';
9
+ const effect = createShaderBlur(root);
10
+ effect.setProgress(.5);
11
+ effect.setAmount(5);
12
+ effect.destroy();
13
+ ```
14
+
15
+ Opciones compartidas: `images?: HTMLImageElement[]` (por defecto las dos primeras imágenes de root; una se repite), `progress` 0–1, `amount`, `maxDpr` 1.5. Las imágenes pueden terminar de cargar después de crear la instancia. Recursos del mismo origen o con CORS configurado antes de cargar la imagen. El kit no descarga imágenes. Root debe tener dimensiones; el canvas decorativo cubre su caja y deja el HTML accesible debajo.
16
+
17
+ Controles: `setProgress(0..1)`, `setAmount(0..100)`, `pause()`, `resume()`, `mode: 'webgl' | 'none'`, `destroy()`. React: `useShaderBlur(ref, options, deps)`, ref a la instancia. No usa `autoInit`: importar explícitamente desde `/gl` conserva la separación de GPU del paquete principal.
18
+
19
+ Pausa fuera de pantalla y con pestaña oculta. Movimiento reducido: frame fijo, con controles manuales. Sin WebGL, error de textura o contexto perdido: canvas oculto, HTML original disponible. Contexto restaurado: recompila y vuelve a cargar texturas. Destroy libera texturas, buffer, shaders y programa, desconecta observers y restaura la clase original.
20
+
21
+ Siena `siena.webgl.work`: siete muestras, pesos .19648255/.29690696/.09447040/.01038136 y offsets 1.41176471/3.29411765/5.17647059. Amount 5 en píxeles de la imagen; progreso rota la dirección de horizontal a vertical. Amount 0 conserva la imagen.
22
+
23
+ Desde 0.9.0. Fuentes de parámetros: `REA - Dousan OFFFICE Siena/`. Shaders originales, sin redistribuir código ni assets de terceros.
@@ -0,0 +1,7 @@
1
+ import { createShaderEffect, type ShaderEffectInstance, type ShaderEffectOptions } from '../shared/create-shader-effect';
2
+ import { blurFragment } from './shader';
3
+
4
+ export type ShaderBlurOptions = ShaderEffectOptions;
5
+ export type ShaderBlurInstance = ShaderEffectInstance;
6
+ export const createShaderBlur = (root: HTMLElement | null, options: ShaderBlurOptions = {}): ShaderBlurInstance =>
7
+ createShaderEffect(root, blurFragment, { amount: 5, ...options });
@@ -0,0 +1,11 @@
1
+ export const blurFragment = `
2
+ void main() {
3
+ vec2 uv = cover(vUv, uImageSize0);
4
+ float angle = uProgress * 1.5707963;
5
+ vec2 delta = vec2(cos(angle), sin(angle)) * uAmount / uImageSize0;
6
+ vec4 color = texture2D(uImage0, uv) * .1964825501511404;
7
+ color += (texture2D(uImage0, uv + delta * 1.411764705882353) + texture2D(uImage0, uv - delta * 1.411764705882353)) * .2969069646728344;
8
+ color += (texture2D(uImage0, uv + delta * 3.2941176470588234) + texture2D(uImage0, uv - delta * 3.2941176470588234)) * .09447039785044732;
9
+ color += (texture2D(uImage0, uv + delta * 5.176470588235294) + texture2D(uImage0, uv - delta * 5.176470588235294)) * .010381362401148057;
10
+ gl_FragColor = color;
11
+ }`;