@nous-research/ui 0.15.0 → 0.17.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 (258) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/README.md +24 -4
  3. package/dist/fonts.js +1 -0
  4. package/dist/hooks/use-below-breakpoint.d.ts +2 -0
  5. package/dist/hooks/use-below-breakpoint.js +17 -0
  6. package/dist/hooks/use-capped-frame.js +1 -0
  7. package/dist/hooks/use-confirm-delete.d.ts +10 -0
  8. package/dist/hooks/use-confirm-delete.js +35 -0
  9. package/dist/hooks/use-css-var-dims.js +1 -0
  10. package/dist/hooks/use-gpu-tier.js +1 -0
  11. package/dist/hooks/use-render-loop.js +1 -0
  12. package/dist/hooks/use-smooth-controls.js +1 -0
  13. package/dist/hooks/use-toast.d.ts +7 -0
  14. package/dist/hooks/use-toast.js +21 -0
  15. package/dist/index.d.ts +11 -1
  16. package/dist/index.js +23 -1
  17. package/dist/ui/basic-page.js +1 -0
  18. package/dist/ui/components/animated-count.js +1 -0
  19. package/dist/ui/components/ascii.js +1 -0
  20. package/dist/ui/components/badge.js +2 -1
  21. package/dist/ui/components/badges/nous-girl.js +1 -0
  22. package/dist/ui/components/blend-mode.js +1 -0
  23. package/dist/ui/components/blink.js +1 -0
  24. package/dist/ui/components/bottom-sheet.d.ts +15 -0
  25. package/dist/ui/components/bottom-sheet.js +192 -0
  26. package/dist/ui/components/button.js +2 -1
  27. package/dist/ui/components/card.d.ts +5 -0
  28. package/dist/ui/components/card.js +74 -0
  29. package/dist/ui/components/checkbox.d.ts +1 -1
  30. package/dist/ui/components/checkbox.js +2 -1
  31. package/dist/ui/components/command-block.js +4 -3
  32. package/dist/ui/components/confirm-dialog.d.ts +13 -0
  33. package/dist/ui/components/confirm-dialog.js +113 -0
  34. package/dist/ui/components/cursor.js +1 -0
  35. package/dist/ui/components/dialog.d.ts +15 -0
  36. package/dist/ui/components/dialog.js +171 -0
  37. package/dist/ui/components/dropdown-menu.js +1 -0
  38. package/dist/ui/components/fit-text/index.js +1 -0
  39. package/dist/ui/components/graphs/bar-chart.js +1 -0
  40. package/dist/ui/components/graphs/index.js +1 -0
  41. package/dist/ui/components/graphs/line-chart.js +1 -0
  42. package/dist/ui/components/graphs/utils.js +1 -0
  43. package/dist/ui/components/grid/index.js +1 -0
  44. package/dist/ui/components/hover-bg.js +1 -0
  45. package/dist/ui/components/icons/arrow.js +1 -0
  46. package/dist/ui/components/icons/check.js +1 -0
  47. package/dist/ui/components/icons/chevron.js +1 -0
  48. package/dist/ui/components/icons/discord.js +1 -0
  49. package/dist/ui/components/icons/eye.js +1 -0
  50. package/dist/ui/components/icons/gear.js +1 -0
  51. package/dist/ui/components/icons/github.js +1 -0
  52. package/dist/ui/components/icons/hamburger.js +1 -0
  53. package/dist/ui/components/icons/heart.js +1 -0
  54. package/dist/ui/components/icons/index.js +1 -0
  55. package/dist/ui/components/icons/link.js +1 -0
  56. package/dist/ui/components/icons/minus.js +1 -0
  57. package/dist/ui/components/icons/search.js +1 -0
  58. package/dist/ui/components/image-distortion.js +1 -0
  59. package/dist/ui/components/input.d.ts +1 -0
  60. package/dist/ui/components/input.js +21 -0
  61. package/dist/ui/components/label.d.ts +1 -0
  62. package/dist/ui/components/label.js +18 -0
  63. package/dist/ui/components/leva-client.js +1 -0
  64. package/dist/ui/components/list-item.js +3 -2
  65. package/dist/ui/components/overlays/blend-modes.js +1 -0
  66. package/dist/ui/components/overlays/glitch.js +1 -0
  67. package/dist/ui/components/overlays/greys.js +1 -0
  68. package/dist/ui/components/overlays/index.js +1 -0
  69. package/dist/ui/components/overlays/lens-layers.js +1 -0
  70. package/dist/ui/components/overlays/lens.js +1 -0
  71. package/dist/ui/components/overlays/noise.js +1 -0
  72. package/dist/ui/components/overlays/vignette.js +1 -0
  73. package/dist/ui/components/poster.js +1 -0
  74. package/dist/ui/components/progress.js +1 -0
  75. package/dist/ui/components/scene-canvas.js +1 -0
  76. package/dist/ui/components/scramble.js +1 -0
  77. package/dist/ui/components/segmented.js +5 -4
  78. package/dist/ui/components/select.js +1 -0
  79. package/dist/ui/components/selection-switcher.js +1 -0
  80. package/dist/ui/components/separator.d.ts +5 -0
  81. package/dist/ui/components/separator.js +22 -0
  82. package/dist/ui/components/shader.js +1 -0
  83. package/dist/ui/components/socials.js +1 -0
  84. package/dist/ui/components/spinner.js +1 -0
  85. package/dist/ui/components/stats.js +2 -1
  86. package/dist/ui/components/switch.js +1 -0
  87. package/dist/ui/components/tabs.js +4 -3
  88. package/dist/ui/components/terminal-demo.js +2 -1
  89. package/dist/ui/components/theme-toggle.js +1 -0
  90. package/dist/ui/components/tier-card.js +2 -1
  91. package/dist/ui/components/toast.d.ts +8 -0
  92. package/dist/ui/components/toast.js +39 -0
  93. package/dist/ui/components/tv.js +1 -0
  94. package/dist/ui/components/typography/h1.js +1 -0
  95. package/dist/ui/components/typography/h2.js +1 -0
  96. package/dist/ui/components/typography/index.js +1 -0
  97. package/dist/ui/components/typography/legend.js +1 -0
  98. package/dist/ui/components/typography/small.js +1 -0
  99. package/dist/ui/components/watchlist.js +2 -1
  100. package/dist/ui/footer.js +1 -0
  101. package/dist/ui/globals.css +47 -3
  102. package/dist/ui/header.js +1 -0
  103. package/dist/ui/layout-wrapper.js +2 -1
  104. package/dist/utils/color.js +1 -0
  105. package/dist/utils/index.js +1 -0
  106. package/dist/utils/poly.js +1 -0
  107. package/package.json +5 -3
  108. package/src/assets/filler-bg0.webp +0 -0
  109. package/src/assets.d.ts +38 -0
  110. package/src/fonts/Collapse-Bold.woff2 +0 -0
  111. package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
  112. package/src/fonts/Collapse-Italic.woff2 +0 -0
  113. package/src/fonts/Collapse-Light.woff2 +0 -0
  114. package/src/fonts/Collapse-LightItalic.woff2 +0 -0
  115. package/src/fonts/Collapse-Regular.woff2 +0 -0
  116. package/src/fonts/Collapse-Thin.woff2 +0 -0
  117. package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
  118. package/src/fonts/Mondwest-Regular.woff2 +0 -0
  119. package/src/fonts/Neuebit-Bold.woff2 +0 -0
  120. package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
  121. package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
  122. package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
  123. package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
  124. package/src/fonts.ts +6 -0
  125. package/src/hooks/use-below-breakpoint.ts +21 -0
  126. package/src/hooks/use-capped-frame.ts +18 -0
  127. package/src/hooks/use-confirm-delete.ts +43 -0
  128. package/src/hooks/use-css-var-dims.ts +39 -0
  129. package/src/hooks/use-gpu-tier.ts +165 -0
  130. package/src/hooks/use-render-loop.ts +121 -0
  131. package/src/hooks/use-smooth-controls.ts +318 -0
  132. package/src/hooks/use-toast.ts +29 -0
  133. package/src/index.ts +130 -0
  134. package/src/ui/basic-page.tsx +34 -0
  135. package/src/ui/build.css +4 -0
  136. package/src/ui/components/animated-count.stories.tsx +67 -0
  137. package/src/ui/components/animated-count.tsx +168 -0
  138. package/src/ui/components/ascii.stories.tsx +30 -0
  139. package/src/ui/components/ascii.tsx +110 -0
  140. package/src/ui/components/badge.stories.tsx +31 -0
  141. package/src/ui/components/badge.tsx +60 -0
  142. package/src/ui/components/badges/nous-girl.tsx +52 -0
  143. package/src/ui/components/blend-mode.stories.tsx +33 -0
  144. package/src/ui/components/blend-mode.tsx +129 -0
  145. package/src/ui/components/blink.stories.tsx +32 -0
  146. package/src/ui/components/blink.tsx +21 -0
  147. package/src/ui/components/bottom-sheet.stories.tsx +43 -0
  148. package/src/ui/components/bottom-sheet.tsx +227 -0
  149. package/src/ui/components/button.stories.tsx +68 -0
  150. package/src/ui/components/button.tsx +170 -0
  151. package/src/ui/components/card.stories.tsx +63 -0
  152. package/src/ui/components/card.tsx +85 -0
  153. package/src/ui/components/checkbox.stories.tsx +113 -0
  154. package/src/ui/components/checkbox.tsx +36 -0
  155. package/src/ui/components/command-block.stories.tsx +52 -0
  156. package/src/ui/components/command-block.tsx +86 -0
  157. package/src/ui/components/confirm-dialog.stories.tsx +91 -0
  158. package/src/ui/components/confirm-dialog.tsx +130 -0
  159. package/src/ui/components/cursor.tsx +115 -0
  160. package/src/ui/components/dialog.stories.tsx +169 -0
  161. package/src/ui/components/dialog.tsx +177 -0
  162. package/src/ui/components/dropdown-menu.stories.tsx +52 -0
  163. package/src/ui/components/dropdown-menu.tsx +117 -0
  164. package/src/ui/components/fit-text/fit-text.css +42 -0
  165. package/src/ui/components/fit-text/index.stories.tsx +33 -0
  166. package/src/ui/components/fit-text/index.tsx +45 -0
  167. package/src/ui/components/forms.stories.tsx +173 -0
  168. package/src/ui/components/graphs/bar-chart.tsx +153 -0
  169. package/src/ui/components/graphs/index.stories.tsx +64 -0
  170. package/src/ui/components/graphs/index.tsx +4 -0
  171. package/src/ui/components/graphs/line-chart.tsx +213 -0
  172. package/src/ui/components/graphs/utils.tsx +265 -0
  173. package/src/ui/components/grid/grid.css +79 -0
  174. package/src/ui/components/grid/index.tsx +19 -0
  175. package/src/ui/components/hover-bg.stories.tsx +29 -0
  176. package/src/ui/components/hover-bg.tsx +15 -0
  177. package/src/ui/components/icons/arrow.tsx +42 -0
  178. package/src/ui/components/icons/check.tsx +14 -0
  179. package/src/ui/components/icons/chevron.tsx +45 -0
  180. package/src/ui/components/icons/discord.tsx +16 -0
  181. package/src/ui/components/icons/eye.tsx +12 -0
  182. package/src/ui/components/icons/gear.tsx +51 -0
  183. package/src/ui/components/icons/github.tsx +16 -0
  184. package/src/ui/components/icons/hamburger.tsx +52 -0
  185. package/src/ui/components/icons/heart.tsx +12 -0
  186. package/src/ui/components/icons/index.ts +12 -0
  187. package/src/ui/components/icons/link.tsx +14 -0
  188. package/src/ui/components/icons/minus.tsx +14 -0
  189. package/src/ui/components/icons/search.tsx +28 -0
  190. package/src/ui/components/image-distortion.stories.tsx +120 -0
  191. package/src/ui/components/image-distortion.tsx +498 -0
  192. package/src/ui/components/input.stories.tsx +39 -0
  193. package/src/ui/components/input.tsx +20 -0
  194. package/src/ui/components/label.stories.tsx +26 -0
  195. package/src/ui/components/label.tsx +16 -0
  196. package/src/ui/components/leva-client.tsx +14 -0
  197. package/src/ui/components/list-item.stories.tsx +83 -0
  198. package/src/ui/components/list-item.tsx +37 -0
  199. package/src/ui/components/overlays/blend-modes.ts +13 -0
  200. package/src/ui/components/overlays/glitch.tsx +243 -0
  201. package/src/ui/components/overlays/greys.tsx +386 -0
  202. package/src/ui/components/overlays/index.tsx +47 -0
  203. package/src/ui/components/overlays/lens-layers.tsx +119 -0
  204. package/src/ui/components/overlays/lens.ts +91 -0
  205. package/src/ui/components/overlays/noise.tsx +174 -0
  206. package/src/ui/components/overlays/vignette.tsx +60 -0
  207. package/src/ui/components/poster.stories.tsx +513 -0
  208. package/src/ui/components/poster.tsx +411 -0
  209. package/src/ui/components/progress.stories.tsx +48 -0
  210. package/src/ui/components/progress.tsx +56 -0
  211. package/src/ui/components/scene-canvas.tsx +254 -0
  212. package/src/ui/components/scramble.stories.tsx +49 -0
  213. package/src/ui/components/scramble.tsx +95 -0
  214. package/src/ui/components/segmented.stories.tsx +101 -0
  215. package/src/ui/components/segmented.tsx +81 -0
  216. package/src/ui/components/select.stories.tsx +88 -0
  217. package/src/ui/components/select.tsx +267 -0
  218. package/src/ui/components/selection-switcher.tsx +44 -0
  219. package/src/ui/components/separator.stories.tsx +33 -0
  220. package/src/ui/components/separator.tsx +24 -0
  221. package/src/ui/components/shader.tsx +83 -0
  222. package/src/ui/components/socials.tsx +42 -0
  223. package/src/ui/components/spinner.stories.tsx +101 -0
  224. package/src/ui/components/spinner.tsx +60 -0
  225. package/src/ui/components/stats.stories.tsx +24 -0
  226. package/src/ui/components/stats.tsx +53 -0
  227. package/src/ui/components/switch.stories.tsx +77 -0
  228. package/src/ui/components/switch.tsx +48 -0
  229. package/src/ui/components/tabs.stories.tsx +101 -0
  230. package/src/ui/components/tabs.tsx +66 -0
  231. package/src/ui/components/terminal-demo.stories.tsx +67 -0
  232. package/src/ui/components/terminal-demo.tsx +189 -0
  233. package/src/ui/components/theme-toggle.stories.tsx +47 -0
  234. package/src/ui/components/theme-toggle.tsx +66 -0
  235. package/src/ui/components/tier-card.stories.tsx +217 -0
  236. package/src/ui/components/tier-card.tsx +190 -0
  237. package/src/ui/components/toast.stories.tsx +55 -0
  238. package/src/ui/components/toast.tsx +49 -0
  239. package/src/ui/components/tv.stories.tsx +37 -0
  240. package/src/ui/components/tv.tsx +257 -0
  241. package/src/ui/components/typography/h1.tsx +18 -0
  242. package/src/ui/components/typography/h2.tsx +18 -0
  243. package/src/ui/components/typography/index.tsx +54 -0
  244. package/src/ui/components/typography/legend.tsx +24 -0
  245. package/src/ui/components/typography/small.tsx +11 -0
  246. package/src/ui/components/watchlist.stories.tsx +33 -0
  247. package/src/ui/components/watchlist.tsx +105 -0
  248. package/src/ui/fonts.css +63 -0
  249. package/src/ui/footer.tsx +111 -0
  250. package/src/ui/globals.css +395 -0
  251. package/src/ui/header.tsx +398 -0
  252. package/src/ui/layout-wrapper.tsx +11 -0
  253. package/src/utils/color.ts +21 -0
  254. package/src/utils/index.ts +62 -0
  255. package/src/utils/poly.ts +26 -0
  256. package/dist/ui/components/modal/index.d.ts +0 -8
  257. package/dist/ui/components/modal/index.js +0 -34
  258. package/dist/ui/components/modal/modal.css +0 -36
@@ -0,0 +1,14 @@
1
+ import type { SVGProps } from 'react'
2
+
3
+ export function LinkIcon(props: SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg fill="none" viewBox="0 0 17 7" {...props}>
6
+ <path
7
+ d="M.264191.25061 6.27068.265071V2.26334h3.96512v1.99578l4.0238.00091.043-2.06986-4.0381.07302-.0142-2.01254L16.271.250649l-.0144 6.006381h-5.992l-.0287-1.9981-3.96528-.0002v1.99826l-6.02085-.0001zM6.24063 2.26197l-3.97944-.01436v1.99827l3.97954.01446z"
8
+ fill="currentColor"
9
+ stroke="currentColor"
10
+ strokeWidth=".5"
11
+ />
12
+ </svg>
13
+ )
14
+ }
@@ -0,0 +1,14 @@
1
+ import type { SVGProps } from 'react'
2
+
3
+ export function MinusIcon(props: SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg fill="none" viewBox="0 0 12 3" {...props}>
6
+ <path
7
+ clipRule="evenodd"
8
+ d="M12 0 0-5.2e-7-1e-7 2.50075H12z"
9
+ fill="currentColor"
10
+ fillRule="evenodd"
11
+ />
12
+ </svg>
13
+ )
14
+ }
@@ -0,0 +1,28 @@
1
+ import type { SVGProps } from 'react'
2
+
3
+ export function SearchIcon(props: SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg fill="none" viewBox="0 0 20 21" {...props}>
6
+ <path
7
+ clipRule="evenodd"
8
+ d="M7.49773 1.6664h6.66637V0H7.49773zM14.1641 15.0001H7.49773v1.6664h6.66637z"
9
+ fill="currentColor"
10
+ fillRule="evenodd"
11
+ />
12
+
13
+ <path
14
+ clipRule="evenodd"
15
+ d="M5.8336 3.33278H7.5v-1.6664H5.8336zM15.8359 13.3329h-1.6671v1.6672h1.6671zM4.16877 5.00017h1.66717V3.33301H4.16877zM17.5 11.6665h-1.6664v1.6664H17.5zM4.16406 11.6665V5.00012h-1.6664v6.66638zM17.4977 5.00012v6.66638h1.6664V5.00012z"
16
+ fill="currentColor"
17
+ fillRule="evenodd"
18
+ />
19
+
20
+ <path
21
+ clipRule="evenodd"
22
+ d="M17.5 5.00017V3.33301h-1.6664v1.66716zM15.8359 3.33278v-1.6664h-1.6671v1.6664zM15.8359 10.0002V6.6665h-1.6671v3.3337zM14.1641 6.6664V5h-1.6664v1.6664zM5.8335 13.3328v-1.6664H4.16633V15H7.5v-1.6671zM4.16406 16.6664V15h-1.6664v1.6664zM2.5 18.3331v-1.6664H.8336v1.6664z"
23
+ fill="currentColor"
24
+ fillRule="evenodd"
25
+ />
26
+ </svg>
27
+ )
28
+ }
@@ -0,0 +1,120 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import fillerBg from '../../assets/filler-bg0.webp'
5
+ import { Button } from './button'
6
+ import { ImageDistortion } from './image-distortion'
7
+
8
+ const meta: Meta<typeof ImageDistortion> = {
9
+ args: { active: true, src: fillerBg.src ?? (fillerBg as unknown as string) },
10
+ component: ImageDistortion,
11
+ title: 'Components/Effects/ImageDistortion'
12
+ }
13
+
14
+ export default meta
15
+
16
+ type Story = StoryObj<typeof ImageDistortion>
17
+
18
+ function Frame({ children }: { children: React.ReactNode }) {
19
+ return (
20
+ <div
21
+ className="bg-background-base relative h-[420px] w-[560px] overflow-hidden border border-current/20"
22
+ style={{ backgroundColor: 'var(--background)' }}
23
+ >
24
+ {children}
25
+ </div>
26
+ )
27
+ }
28
+
29
+ export const Default: Story = {
30
+ render: args => (
31
+ <Frame>
32
+ <ImageDistortion {...args} />
33
+ </Frame>
34
+ )
35
+ }
36
+
37
+ export const Tinted: Story = {
38
+ args: { tint: '#88ccaa' },
39
+ render: args => (
40
+ <Frame>
41
+ <ImageDistortion {...args} />
42
+ </Frame>
43
+ )
44
+ }
45
+
46
+ export const TintStrength: Story = {
47
+ render: () => {
48
+ const src = fillerBg.src ?? (fillerBg as unknown as string)
49
+
50
+ return (
51
+ <div className="grid grid-cols-3 gap-4">
52
+ {[
53
+ ['#88ccaa', 'mint'],
54
+ ['#ccaa88', 'amber'],
55
+ ['#ff4444', 'fatal']
56
+ ].map(([tint, label]) => (
57
+ <div className="flex flex-col gap-2" key={label}>
58
+ <span className="text-xs uppercase tracking-widest opacity-50">
59
+ {label}
60
+ </span>
61
+
62
+ <Frame>
63
+ <ImageDistortion
64
+ src={src}
65
+ tint={tint}
66
+ tintStrength={{ active: 0.55, inactive: 0.25 }}
67
+ />
68
+ </Frame>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ )
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Runs the haptic-distortion effect on a choreographed motion pattern so
78
+ * the image looks alive without needing a real pointer. Perfect for
79
+ * screen recordings, posters, and social cuts.
80
+ */
81
+ export const AutoPlay: Story = {
82
+ render: () => {
83
+ const src = fillerBg.src ?? (fillerBg as unknown as string)
84
+
85
+ return (
86
+ <div className="grid grid-cols-3 gap-4">
87
+ {(['slash', 'gentle', 'aggressive'] as const).map(pattern => (
88
+ <div className="flex flex-col gap-2" key={pattern}>
89
+ <span className="text-xs uppercase tracking-widest opacity-50">
90
+ {pattern}
91
+ </span>
92
+
93
+ <Frame>
94
+ <ImageDistortion autoPlay={pattern} src={src} tint="#ccaa88" />
95
+ </Frame>
96
+ </div>
97
+ ))}
98
+ </div>
99
+ )
100
+ }
101
+ }
102
+
103
+ export const ToggleActive: Story = {
104
+ render: () => {
105
+ const [active, setActive] = useState(true)
106
+ const src = fillerBg.src ?? (fillerBg as unknown as string)
107
+
108
+ return (
109
+ <div className="flex flex-col gap-3">
110
+ <Frame>
111
+ <ImageDistortion active={active} src={src} tint="#ff4444" />
112
+ </Frame>
113
+
114
+ <Button onClick={() => setActive(v => !v)}>
115
+ {active ? 'Active' : 'Inactive'}
116
+ </Button>
117
+ </div>
118
+ )
119
+ }
120
+ }
@@ -0,0 +1,498 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+
5
+ import { useGpuTier } from '../../hooks/use-gpu-tier'
6
+ import { cn, hexToRgb } from '../../utils'
7
+
8
+ const NUM_BANDS = 12
9
+
10
+ const VERT = `attribute vec2 a;varying vec2 vUv;void main(){vUv=vec2(a.x*.5+.5,.5-a.y*.5);gl_Position=vec4(a,0,1);}`
11
+
12
+ const FRAG = `precision highp float;
13
+ uniform float t;
14
+ uniform vec2 r,imgSize,vel;
15
+ uniform sampler2D tex;
16
+ uniform float bands[${NUM_BANDS}];
17
+ uniform vec3 tint;
18
+ uniform float tintStrength;
19
+ varying vec2 vUv;
20
+
21
+ float h(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
22
+
23
+ // cover-style UV: crops the image to fill the canvas, centered
24
+ vec2 coverUV(vec2 uv){
25
+ float canvasAspect=r.x/r.y;
26
+ float imgAspect=imgSize.x/imgSize.y;
27
+ vec2 scale=canvasAspect>imgAspect
28
+ ?vec2(1.0,imgAspect/canvasAspect)
29
+ :vec2(canvasAspect/imgAspect,1.0);
30
+ return(uv-0.5)*scale+0.5;
31
+ }
32
+
33
+ void main(){
34
+ vec2 uv=coverUV(vUv);
35
+ float scanY=floor(vUv.y*r.y);
36
+
37
+ float bandF=vUv.y*${NUM_BANDS}.0;
38
+ int bandIdx=int(floor(bandF));
39
+ float bandFrac=fract(bandF);
40
+
41
+ float strength=0.0;
42
+ for(int i=0;i<${NUM_BANDS};i++){
43
+ if(i==bandIdx) strength=bands[i];
44
+ }
45
+
46
+ float neighborStr=0.0;
47
+ int neighborIdx=bandFrac>.5?bandIdx+1:bandIdx-1;
48
+ for(int i=0;i<${NUM_BANDS};i++){
49
+ if(i==neighborIdx) neighborStr=bands[i];
50
+ }
51
+ float edgeBlend=abs(bandFrac-.5)*2.0;
52
+ edgeBlend*=edgeBlend;
53
+ strength=mix(strength,neighborStr,edgeBlend*.3);
54
+
55
+ float speed=length(vel);
56
+ float dirBlend=smoothstep(0.0,0.02,speed);
57
+ vec2 dir=speed>.0001?vel/speed:vec2(0);
58
+ dir*=dirBlend;
59
+
60
+ float rowSeed=h(vec2(scanY,floor(t*3.)+float(bandIdx)*7.));
61
+ float rowVar=mix(.4,1.0,rowSeed);
62
+
63
+ float ySmooth=vUv.y*6.0+t*0.7;
64
+ float yNoise=mix(h(vec2(floor(ySmooth),13.)),h(vec2(floor(ySmooth)+1.0,13.)),smoothstep(0.0,1.0,fract(ySmooth)));
65
+ float colVar=mix(.4,1.0,yNoise);
66
+
67
+ float tearShiftX=dir.x*strength*rowVar*0.15;
68
+ float tearShiftY=dir.y*strength*colVar*0.10;
69
+
70
+ float bandSeed=h(vec2(float(bandIdx),42.));
71
+ tearShiftX+=strength*(.5-bandSeed)*0.05;
72
+
73
+ float yJitter=mix(h(vec2(floor(ySmooth),73.)),h(vec2(floor(ySmooth)+1.0,73.)),smoothstep(0.0,1.0,fract(ySmooth)));
74
+ tearShiftY+=strength*(.5-yJitter)*0.035;
75
+
76
+ uv.x+=tearShiftX;
77
+ uv.y+=tearShiftY;
78
+
79
+ float sortGate=step(.5,strength)*step(.4,rowSeed);
80
+ uv.x+=dir.x*sortGate*strength*0.03;
81
+ uv.y+=dir.y*sortGate*strength*0.02;
82
+
83
+ float caX=abs(tearShiftX)*2.5+sortGate*strength*0.01;
84
+ float caY=abs(tearShiftY)*2.5+sortGate*strength*0.01;
85
+ float cr=texture2D(tex,vec2(uv.x+caX,uv.y+caY)).r;
86
+ float cg=texture2D(tex,uv).g;
87
+ float cb=texture2D(tex,vec2(uv.x-caX,uv.y-caY)).b;
88
+
89
+ vec3 col=vec3(cr,cg,cb);
90
+
91
+ col*=.97+.03*sin(vUv.y*r.y*3.14159);
92
+
93
+ float bandEdge=smoothstep(.02,.0,min(bandFrac,1.0-bandFrac));
94
+ col+=vec3(bandEdge*strength*.1);
95
+
96
+ col=mix(col,col*tint,tintStrength);
97
+
98
+ gl_FragColor=vec4(col,1.0);
99
+ }`
100
+
101
+ /**
102
+ * Choreographed motion patterns used when `autoPlay` is set. Each pattern
103
+ * returns a synthetic pointer position in [0,1] and a hover intensity in
104
+ * [0,1] for the current time (seconds). They drive the shader without
105
+ * requiring a real pointer, which is what lets us record the distortion
106
+ * as a GIF / screenshot / poster.
107
+ */
108
+ const AUTOPLAY_PATTERNS: Record<
109
+ AutoPlayPattern,
110
+ (t: number) => { hover: number; mx: number; my: number }
111
+ > = {
112
+ aggressive: t => {
113
+ const cycle = 1.4
114
+ const phase = (t % cycle) / cycle
115
+ const stab = Math.exp(-((phase - 0.15) ** 2) * 260)
116
+ const angle = Math.floor(t / cycle) * 1.37
117
+ const mx = 0.5 + Math.cos(angle) * 0.42 * (stab + 0.15)
118
+ const my = 0.5 + Math.sin(angle) * 0.38 * (stab + 0.15)
119
+
120
+ return { hover: 0.55 + stab * 0.45, mx, my }
121
+ },
122
+ gentle: t => ({
123
+ hover: 0.45 + Math.sin(t * 0.9) * 0.1,
124
+ mx: 0.5 + Math.sin(t * 0.5) * 0.28,
125
+ my: 0.5 + Math.cos(t * 0.37) * 0.22
126
+ }),
127
+ slash: t => {
128
+ // Long breath -> sword slash -> recoil twitch, repeating.
129
+ const cycle = 3.6
130
+ const phase = (t % cycle) / cycle
131
+ const slash = Math.exp(-((phase - 0.28) ** 2) * 180)
132
+ const micro = Math.exp(-((phase - 0.7) ** 2) * 340)
133
+
134
+ const driftX = 0.5 + Math.sin(t * 0.7) * 0.16
135
+ const driftY = 0.55 + Math.cos(t * 0.5) * 0.14
136
+
137
+ // Slash trajectory: bottom-left up into the giant's chest (top-right).
138
+ const slashX = -0.15 + phase * 1.55
139
+ const slashY = 0.95 - phase * 1.35
140
+
141
+ const mx = driftX * (1 - slash) + slashX * slash
142
+ const my = driftY * (1 - slash) + slashY * slash
143
+
144
+ return { hover: 0.5 + slash * 0.5 + micro * 0.35, mx, my }
145
+ }
146
+ }
147
+
148
+ export function ImageDistortion({
149
+ active = true,
150
+ autoPlay,
151
+ className,
152
+ fallbackClassName,
153
+ src,
154
+ style,
155
+ tint,
156
+ tintStrength
157
+ }: ImageDistortionProps) {
158
+ const canvasRef = useRef<HTMLCanvasElement>(null)
159
+ const tier = useGpuTier()
160
+ const [loaded, setLoaded] = useState(false)
161
+
162
+ const activeRef = useRef(active)
163
+ activeRef.current = active
164
+ const tintStrengthRef = useRef(tintStrength)
165
+ tintStrengthRef.current = tintStrength
166
+ const autoPlayRef = useRef(autoPlay)
167
+ autoPlayRef.current = autoPlay
168
+
169
+ const state = useRef({
170
+ bandTargets: new Float32Array(NUM_BANDS),
171
+ bands: new Float32Array(NUM_BANDS),
172
+ hoverTarget: 0,
173
+ imgH: 1,
174
+ imgW: 1,
175
+ mx: 0.5,
176
+ my: 0.5,
177
+ prevMx: 0.5,
178
+ prevMy: 0.5,
179
+ vx: 0,
180
+ vy: 0
181
+ })
182
+
183
+ useEffect(() => {
184
+ if (tier === 0) {
185
+ return
186
+ }
187
+
188
+ const c = canvasRef.current
189
+
190
+ if (!c) {
191
+ return
192
+ }
193
+
194
+ const gl = c.getContext('webgl')
195
+
196
+ if (!gl) {
197
+ return
198
+ }
199
+
200
+ const compile = (type: number, source: string) => {
201
+ const s = gl.createShader(type)!
202
+ gl.shaderSource(s, source)
203
+ gl.compileShader(s)
204
+
205
+ return s
206
+ }
207
+
208
+ const prog = gl.createProgram()!
209
+ gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT))
210
+ gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG))
211
+ gl.linkProgram(prog)
212
+ gl.useProgram(prog)
213
+
214
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
215
+ gl.bufferData(
216
+ gl.ARRAY_BUFFER,
217
+ new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
218
+ gl.STATIC_DRAW
219
+ )
220
+
221
+ const a = gl.getAttribLocation(prog, 'a')
222
+ gl.enableVertexAttribArray(a)
223
+ gl.vertexAttribPointer(a, 2, gl.FLOAT, false, 0, 0)
224
+
225
+ const uT = gl.getUniformLocation(prog, 't')
226
+ const uR = gl.getUniformLocation(prog, 'r')
227
+ const uImgSize = gl.getUniformLocation(prog, 'imgSize')
228
+ const uVel = gl.getUniformLocation(prog, 'vel')
229
+ const uTex = gl.getUniformLocation(prog, 'tex')
230
+ const uTint = gl.getUniformLocation(prog, 'tint')
231
+ const uTintStrength = gl.getUniformLocation(prog, 'tintStrength')
232
+ const uBands: (null | WebGLUniformLocation)[] = []
233
+
234
+ for (let i = 0; i < NUM_BANDS; i++) {
235
+ uBands.push(gl.getUniformLocation(prog, `bands[${i}]`))
236
+ }
237
+
238
+ const texture = gl.createTexture()!
239
+ gl.bindTexture(gl.TEXTURE_2D, texture)
240
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
241
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
242
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
243
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
244
+ gl.texImage2D(
245
+ gl.TEXTURE_2D,
246
+ 0,
247
+ gl.RGBA,
248
+ 1,
249
+ 1,
250
+ 0,
251
+ gl.RGBA,
252
+ gl.UNSIGNED_BYTE,
253
+ new Uint8Array([0, 0, 0, 255])
254
+ )
255
+
256
+ const img = new Image()
257
+ img.crossOrigin = 'anonymous'
258
+
259
+ img.onload = () => {
260
+ state.current.imgW = img.naturalWidth
261
+ state.current.imgH = img.naturalHeight
262
+ gl.bindTexture(gl.TEXTURE_2D, texture)
263
+ gl.texImage2D(
264
+ gl.TEXTURE_2D,
265
+ 0,
266
+ gl.RGBA,
267
+ gl.RGBA,
268
+ gl.UNSIGNED_BYTE,
269
+ img
270
+ )
271
+ setLoaded(true)
272
+ }
273
+
274
+ img.src = src
275
+
276
+ gl.activeTexture(gl.TEXTURE0)
277
+ gl.bindTexture(gl.TEXTURE_2D, texture)
278
+ gl.uniform1i(uTex, 0)
279
+
280
+ const resize = () => {
281
+ const rect = c.getBoundingClientRect()
282
+ const dpr = Math.min(devicePixelRatio, 2)
283
+ c.width = rect.width * dpr
284
+ c.height = rect.height * dpr
285
+ gl.viewport(0, 0, c.width, c.height)
286
+ }
287
+
288
+ resize()
289
+ const ro = new ResizeObserver(resize)
290
+ ro.observe(c)
291
+
292
+ const onMove = (e: PointerEvent) => {
293
+ const rect = c.getBoundingClientRect()
294
+ state.current.mx = (e.clientX - rect.left) / rect.width
295
+ state.current.my = (e.clientY - rect.top) / rect.height
296
+ }
297
+
298
+ const onEnter = () => {
299
+ state.current.hoverTarget = 1
300
+ }
301
+
302
+ const onLeave = () => {
303
+ state.current.hoverTarget = 0
304
+ }
305
+
306
+ // When autoPlay drives the distortion we want the poster to look
307
+ // alive regardless of whether a pointer is near the canvas, so we
308
+ // skip the real pointer listeners entirely.
309
+ if (!autoPlayRef.current) {
310
+ c.addEventListener('pointermove', onMove)
311
+ c.addEventListener('pointerenter', onEnter)
312
+ c.addEventListener('pointerleave', onLeave)
313
+ }
314
+
315
+ const bandEaseRates = new Float32Array(NUM_BANDS)
316
+
317
+ for (let i = 0; i < NUM_BANDS; i++) {
318
+ bandEaseRates[i] = 0.02 + Math.random() * 0.06
319
+ }
320
+
321
+ const tintVec: readonly [number, number, number] = tint
322
+ ? (() => {
323
+ const [tr, tg, tb] = hexToRgb(tint)
324
+
325
+ return [tr / 255, tg / 255, tb / 255] as const
326
+ })()
327
+ : ([1, 1, 1] as const)
328
+
329
+ const t0 = performance.now()
330
+ let raf = 0
331
+ let visible = !document.hidden
332
+ let inView = true
333
+
334
+ const loop = () => {
335
+ raf = requestAnimationFrame(loop)
336
+ const s = state.current
337
+
338
+ const pattern = autoPlayRef.current
339
+ ? AUTOPLAY_PATTERNS[autoPlayRef.current]
340
+ : null
341
+
342
+ if (pattern) {
343
+ const driven = pattern((performance.now() - t0) / 1e3)
344
+ s.mx = driven.mx
345
+ s.my = driven.my
346
+ s.hoverTarget = driven.hover
347
+ }
348
+
349
+ const dvx = s.mx - s.prevMx
350
+ const dvy = s.my - s.prevMy
351
+ s.vx += (dvx * 8 - s.vx) * 0.1
352
+ s.vy += (dvy * 8 - s.vy) * 0.1
353
+ s.prevMx = s.mx
354
+ s.prevMy = s.my
355
+
356
+ const speed = Math.sqrt(s.vx * s.vx + s.vy * s.vy)
357
+
358
+ for (let i = 0; i < NUM_BANDS; i++) {
359
+ const bandCenter = (i + 0.5) / NUM_BANDS
360
+ const dist = Math.abs(s.my - bandCenter)
361
+ const proximity = Math.max(0, 1 - dist / 0.3)
362
+ const activation =
363
+ s.hoverTarget * proximity * (0.4 + Math.min(speed, 1) * 0.6)
364
+ s.bandTargets[i] = activation
365
+ }
366
+
367
+ for (let i = 0; i < NUM_BANDS; i++) {
368
+ const rate = bandEaseRates[i]!
369
+ const current = s.bands[i] ?? 0
370
+ const target = s.bandTargets[i] ?? 0
371
+ s.bands[i] = current + (target - current) * rate
372
+
373
+ if (s.bands[i]! < 0.001) {
374
+ s.bands[i] = 0
375
+ }
376
+ }
377
+
378
+ gl.uniform1f(uT, (performance.now() - t0) / 1e3)
379
+ gl.uniform2f(uR, c.width, c.height)
380
+ gl.uniform2f(uImgSize, s.imgW, s.imgH)
381
+ gl.uniform2f(uVel, s.vx, s.vy)
382
+ gl.uniform3f(uTint, tintVec[0], tintVec[1], tintVec[2])
383
+
384
+ const ts = tintStrengthRef.current
385
+ const defaultStrength = tint ? 0.35 : 0
386
+ const defaultInactive = tint ? 0.15 : 0
387
+ gl.uniform1f(
388
+ uTintStrength,
389
+ activeRef.current
390
+ ? (ts?.active ?? defaultStrength)
391
+ : (ts?.inactive ?? defaultInactive)
392
+ )
393
+
394
+ for (let i = 0; i < NUM_BANDS; i++) {
395
+ gl.uniform1f(uBands[i]!, s.bands[i]!)
396
+ }
397
+
398
+ gl.bindTexture(gl.TEXTURE_2D, texture)
399
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
400
+ }
401
+
402
+ const start = () => {
403
+ if (visible && inView && !raf) {
404
+ raf = requestAnimationFrame(loop)
405
+ }
406
+ }
407
+
408
+ const stop = () => {
409
+ if (raf) {
410
+ cancelAnimationFrame(raf)
411
+ raf = 0
412
+ }
413
+ }
414
+
415
+ const onVisibility = () => {
416
+ visible = !document.hidden
417
+ visible ? start() : stop()
418
+ }
419
+
420
+ const io = new IntersectionObserver(
421
+ entries => {
422
+ inView = entries.some(e => e.isIntersecting)
423
+ inView ? start() : stop()
424
+ },
425
+ { threshold: 0 }
426
+ )
427
+
428
+ io.observe(c)
429
+ document.addEventListener('visibilitychange', onVisibility)
430
+
431
+ start()
432
+
433
+ return () => {
434
+ stop()
435
+ io.disconnect()
436
+ document.removeEventListener('visibilitychange', onVisibility)
437
+ ro.disconnect()
438
+ c.removeEventListener('pointermove', onMove)
439
+ c.removeEventListener('pointerenter', onEnter)
440
+ c.removeEventListener('pointerleave', onLeave)
441
+ gl.deleteTexture(texture)
442
+ gl.deleteProgram(prog)
443
+ setLoaded(false)
444
+ }
445
+ // autoPlay is intentionally omitted so toggling it at runtime doesn't
446
+ // tear down the shader pipeline. The ref-driven loop reads the live
447
+ // value each frame, so listener attach/detach is handled once on mount.
448
+ // eslint-disable-next-line react-hooks/exhaustive-deps
449
+ }, [src, tier, tint])
450
+
451
+ if (tier === 0) {
452
+ return (
453
+ // eslint-disable-next-line @next/next/no-img-element
454
+ <img
455
+ alt=""
456
+ className={cn(
457
+ 'absolute inset-0 h-full w-full object-cover',
458
+ fallbackClassName ?? className
459
+ )}
460
+ src={src}
461
+ style={{ mixBlendMode: 'overlay', ...style }}
462
+ />
463
+ )
464
+ }
465
+
466
+ return (
467
+ <canvas
468
+ className={cn(
469
+ 'absolute inset-0 h-full w-full transition-opacity duration-500',
470
+ className
471
+ )}
472
+ ref={canvasRef}
473
+ style={{
474
+ mixBlendMode: 'overlay',
475
+ opacity: loaded ? 1 : 0,
476
+ ...style
477
+ }}
478
+ />
479
+ )
480
+ }
481
+
482
+ export type AutoPlayPattern = 'aggressive' | 'gentle' | 'slash'
483
+
484
+ interface ImageDistortionProps {
485
+ active?: boolean
486
+ /**
487
+ * Drive the distortion with a choreographed motion pattern instead of
488
+ * waiting for a real pointer. Useful for posters, social clips, and any
489
+ * context where the image needs to feel alive on its own.
490
+ */
491
+ autoPlay?: AutoPlayPattern
492
+ className?: string
493
+ fallbackClassName?: string
494
+ src: string
495
+ style?: React.CSSProperties
496
+ tint?: string
497
+ tintStrength?: { active: number; inactive: number }
498
+ }