@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.
- package/CHANGELOG.md +266 -0
- package/README.md +24 -4
- package/dist/fonts.js +1 -0
- package/dist/hooks/use-below-breakpoint.d.ts +2 -0
- package/dist/hooks/use-below-breakpoint.js +17 -0
- package/dist/hooks/use-capped-frame.js +1 -0
- package/dist/hooks/use-confirm-delete.d.ts +10 -0
- package/dist/hooks/use-confirm-delete.js +35 -0
- package/dist/hooks/use-css-var-dims.js +1 -0
- package/dist/hooks/use-gpu-tier.js +1 -0
- package/dist/hooks/use-render-loop.js +1 -0
- package/dist/hooks/use-smooth-controls.js +1 -0
- package/dist/hooks/use-toast.d.ts +7 -0
- package/dist/hooks/use-toast.js +21 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +23 -1
- package/dist/ui/basic-page.js +1 -0
- package/dist/ui/components/animated-count.js +1 -0
- package/dist/ui/components/ascii.js +1 -0
- package/dist/ui/components/badge.js +2 -1
- package/dist/ui/components/badges/nous-girl.js +1 -0
- package/dist/ui/components/blend-mode.js +1 -0
- package/dist/ui/components/blink.js +1 -0
- package/dist/ui/components/bottom-sheet.d.ts +15 -0
- package/dist/ui/components/bottom-sheet.js +192 -0
- package/dist/ui/components/button.js +2 -1
- package/dist/ui/components/card.d.ts +5 -0
- package/dist/ui/components/card.js +74 -0
- package/dist/ui/components/checkbox.d.ts +1 -1
- package/dist/ui/components/checkbox.js +2 -1
- package/dist/ui/components/command-block.js +4 -3
- package/dist/ui/components/confirm-dialog.d.ts +13 -0
- package/dist/ui/components/confirm-dialog.js +113 -0
- package/dist/ui/components/cursor.js +1 -0
- package/dist/ui/components/dialog.d.ts +15 -0
- package/dist/ui/components/dialog.js +171 -0
- package/dist/ui/components/dropdown-menu.js +1 -0
- package/dist/ui/components/fit-text/index.js +1 -0
- package/dist/ui/components/graphs/bar-chart.js +1 -0
- package/dist/ui/components/graphs/index.js +1 -0
- package/dist/ui/components/graphs/line-chart.js +1 -0
- package/dist/ui/components/graphs/utils.js +1 -0
- package/dist/ui/components/grid/index.js +1 -0
- package/dist/ui/components/hover-bg.js +1 -0
- package/dist/ui/components/icons/arrow.js +1 -0
- package/dist/ui/components/icons/check.js +1 -0
- package/dist/ui/components/icons/chevron.js +1 -0
- package/dist/ui/components/icons/discord.js +1 -0
- package/dist/ui/components/icons/eye.js +1 -0
- package/dist/ui/components/icons/gear.js +1 -0
- package/dist/ui/components/icons/github.js +1 -0
- package/dist/ui/components/icons/hamburger.js +1 -0
- package/dist/ui/components/icons/heart.js +1 -0
- package/dist/ui/components/icons/index.js +1 -0
- package/dist/ui/components/icons/link.js +1 -0
- package/dist/ui/components/icons/minus.js +1 -0
- package/dist/ui/components/icons/search.js +1 -0
- package/dist/ui/components/image-distortion.js +1 -0
- package/dist/ui/components/input.d.ts +1 -0
- package/dist/ui/components/input.js +21 -0
- package/dist/ui/components/label.d.ts +1 -0
- package/dist/ui/components/label.js +18 -0
- package/dist/ui/components/leva-client.js +1 -0
- package/dist/ui/components/list-item.js +3 -2
- package/dist/ui/components/overlays/blend-modes.js +1 -0
- package/dist/ui/components/overlays/glitch.js +1 -0
- package/dist/ui/components/overlays/greys.js +1 -0
- package/dist/ui/components/overlays/index.js +1 -0
- package/dist/ui/components/overlays/lens-layers.js +1 -0
- package/dist/ui/components/overlays/lens.js +1 -0
- package/dist/ui/components/overlays/noise.js +1 -0
- package/dist/ui/components/overlays/vignette.js +1 -0
- package/dist/ui/components/poster.js +1 -0
- package/dist/ui/components/progress.js +1 -0
- package/dist/ui/components/scene-canvas.js +1 -0
- package/dist/ui/components/scramble.js +1 -0
- package/dist/ui/components/segmented.js +5 -4
- package/dist/ui/components/select.js +1 -0
- package/dist/ui/components/selection-switcher.js +1 -0
- package/dist/ui/components/separator.d.ts +5 -0
- package/dist/ui/components/separator.js +22 -0
- package/dist/ui/components/shader.js +1 -0
- package/dist/ui/components/socials.js +1 -0
- package/dist/ui/components/spinner.js +1 -0
- package/dist/ui/components/stats.js +2 -1
- package/dist/ui/components/switch.js +1 -0
- package/dist/ui/components/tabs.js +4 -3
- package/dist/ui/components/terminal-demo.js +2 -1
- package/dist/ui/components/theme-toggle.js +1 -0
- package/dist/ui/components/tier-card.js +2 -1
- package/dist/ui/components/toast.d.ts +8 -0
- package/dist/ui/components/toast.js +39 -0
- package/dist/ui/components/tv.js +1 -0
- package/dist/ui/components/typography/h1.js +1 -0
- package/dist/ui/components/typography/h2.js +1 -0
- package/dist/ui/components/typography/index.js +1 -0
- package/dist/ui/components/typography/legend.js +1 -0
- package/dist/ui/components/typography/small.js +1 -0
- package/dist/ui/components/watchlist.js +2 -1
- package/dist/ui/footer.js +1 -0
- package/dist/ui/globals.css +47 -3
- package/dist/ui/header.js +1 -0
- package/dist/ui/layout-wrapper.js +2 -1
- package/dist/utils/color.js +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/poly.js +1 -0
- package/package.json +5 -3
- package/src/assets/filler-bg0.webp +0 -0
- package/src/assets.d.ts +38 -0
- package/src/fonts/Collapse-Bold.woff2 +0 -0
- package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
- package/src/fonts/Collapse-Italic.woff2 +0 -0
- package/src/fonts/Collapse-Light.woff2 +0 -0
- package/src/fonts/Collapse-LightItalic.woff2 +0 -0
- package/src/fonts/Collapse-Regular.woff2 +0 -0
- package/src/fonts/Collapse-Thin.woff2 +0 -0
- package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
- package/src/fonts/Mondwest-Regular.woff2 +0 -0
- package/src/fonts/Neuebit-Bold.woff2 +0 -0
- package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
- package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
- package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
- package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
- package/src/fonts.ts +6 -0
- package/src/hooks/use-below-breakpoint.ts +21 -0
- package/src/hooks/use-capped-frame.ts +18 -0
- package/src/hooks/use-confirm-delete.ts +43 -0
- package/src/hooks/use-css-var-dims.ts +39 -0
- package/src/hooks/use-gpu-tier.ts +165 -0
- package/src/hooks/use-render-loop.ts +121 -0
- package/src/hooks/use-smooth-controls.ts +318 -0
- package/src/hooks/use-toast.ts +29 -0
- package/src/index.ts +130 -0
- package/src/ui/basic-page.tsx +34 -0
- package/src/ui/build.css +4 -0
- package/src/ui/components/animated-count.stories.tsx +67 -0
- package/src/ui/components/animated-count.tsx +168 -0
- package/src/ui/components/ascii.stories.tsx +30 -0
- package/src/ui/components/ascii.tsx +110 -0
- package/src/ui/components/badge.stories.tsx +31 -0
- package/src/ui/components/badge.tsx +60 -0
- package/src/ui/components/badges/nous-girl.tsx +52 -0
- package/src/ui/components/blend-mode.stories.tsx +33 -0
- package/src/ui/components/blend-mode.tsx +129 -0
- package/src/ui/components/blink.stories.tsx +32 -0
- package/src/ui/components/blink.tsx +21 -0
- package/src/ui/components/bottom-sheet.stories.tsx +43 -0
- package/src/ui/components/bottom-sheet.tsx +227 -0
- package/src/ui/components/button.stories.tsx +68 -0
- package/src/ui/components/button.tsx +170 -0
- package/src/ui/components/card.stories.tsx +63 -0
- package/src/ui/components/card.tsx +85 -0
- package/src/ui/components/checkbox.stories.tsx +113 -0
- package/src/ui/components/checkbox.tsx +36 -0
- package/src/ui/components/command-block.stories.tsx +52 -0
- package/src/ui/components/command-block.tsx +86 -0
- package/src/ui/components/confirm-dialog.stories.tsx +91 -0
- package/src/ui/components/confirm-dialog.tsx +130 -0
- package/src/ui/components/cursor.tsx +115 -0
- package/src/ui/components/dialog.stories.tsx +169 -0
- package/src/ui/components/dialog.tsx +177 -0
- package/src/ui/components/dropdown-menu.stories.tsx +52 -0
- package/src/ui/components/dropdown-menu.tsx +117 -0
- package/src/ui/components/fit-text/fit-text.css +42 -0
- package/src/ui/components/fit-text/index.stories.tsx +33 -0
- package/src/ui/components/fit-text/index.tsx +45 -0
- package/src/ui/components/forms.stories.tsx +173 -0
- package/src/ui/components/graphs/bar-chart.tsx +153 -0
- package/src/ui/components/graphs/index.stories.tsx +64 -0
- package/src/ui/components/graphs/index.tsx +4 -0
- package/src/ui/components/graphs/line-chart.tsx +213 -0
- package/src/ui/components/graphs/utils.tsx +265 -0
- package/src/ui/components/grid/grid.css +79 -0
- package/src/ui/components/grid/index.tsx +19 -0
- package/src/ui/components/hover-bg.stories.tsx +29 -0
- package/src/ui/components/hover-bg.tsx +15 -0
- package/src/ui/components/icons/arrow.tsx +42 -0
- package/src/ui/components/icons/check.tsx +14 -0
- package/src/ui/components/icons/chevron.tsx +45 -0
- package/src/ui/components/icons/discord.tsx +16 -0
- package/src/ui/components/icons/eye.tsx +12 -0
- package/src/ui/components/icons/gear.tsx +51 -0
- package/src/ui/components/icons/github.tsx +16 -0
- package/src/ui/components/icons/hamburger.tsx +52 -0
- package/src/ui/components/icons/heart.tsx +12 -0
- package/src/ui/components/icons/index.ts +12 -0
- package/src/ui/components/icons/link.tsx +14 -0
- package/src/ui/components/icons/minus.tsx +14 -0
- package/src/ui/components/icons/search.tsx +28 -0
- package/src/ui/components/image-distortion.stories.tsx +120 -0
- package/src/ui/components/image-distortion.tsx +498 -0
- package/src/ui/components/input.stories.tsx +39 -0
- package/src/ui/components/input.tsx +20 -0
- package/src/ui/components/label.stories.tsx +26 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/leva-client.tsx +14 -0
- package/src/ui/components/list-item.stories.tsx +83 -0
- package/src/ui/components/list-item.tsx +37 -0
- package/src/ui/components/overlays/blend-modes.ts +13 -0
- package/src/ui/components/overlays/glitch.tsx +243 -0
- package/src/ui/components/overlays/greys.tsx +386 -0
- package/src/ui/components/overlays/index.tsx +47 -0
- package/src/ui/components/overlays/lens-layers.tsx +119 -0
- package/src/ui/components/overlays/lens.ts +91 -0
- package/src/ui/components/overlays/noise.tsx +174 -0
- package/src/ui/components/overlays/vignette.tsx +60 -0
- package/src/ui/components/poster.stories.tsx +513 -0
- package/src/ui/components/poster.tsx +411 -0
- package/src/ui/components/progress.stories.tsx +48 -0
- package/src/ui/components/progress.tsx +56 -0
- package/src/ui/components/scene-canvas.tsx +254 -0
- package/src/ui/components/scramble.stories.tsx +49 -0
- package/src/ui/components/scramble.tsx +95 -0
- package/src/ui/components/segmented.stories.tsx +101 -0
- package/src/ui/components/segmented.tsx +81 -0
- package/src/ui/components/select.stories.tsx +88 -0
- package/src/ui/components/select.tsx +267 -0
- package/src/ui/components/selection-switcher.tsx +44 -0
- package/src/ui/components/separator.stories.tsx +33 -0
- package/src/ui/components/separator.tsx +24 -0
- package/src/ui/components/shader.tsx +83 -0
- package/src/ui/components/socials.tsx +42 -0
- package/src/ui/components/spinner.stories.tsx +101 -0
- package/src/ui/components/spinner.tsx +60 -0
- package/src/ui/components/stats.stories.tsx +24 -0
- package/src/ui/components/stats.tsx +53 -0
- package/src/ui/components/switch.stories.tsx +77 -0
- package/src/ui/components/switch.tsx +48 -0
- package/src/ui/components/tabs.stories.tsx +101 -0
- package/src/ui/components/tabs.tsx +66 -0
- package/src/ui/components/terminal-demo.stories.tsx +67 -0
- package/src/ui/components/terminal-demo.tsx +189 -0
- package/src/ui/components/theme-toggle.stories.tsx +47 -0
- package/src/ui/components/theme-toggle.tsx +66 -0
- package/src/ui/components/tier-card.stories.tsx +217 -0
- package/src/ui/components/tier-card.tsx +190 -0
- package/src/ui/components/toast.stories.tsx +55 -0
- package/src/ui/components/toast.tsx +49 -0
- package/src/ui/components/tv.stories.tsx +37 -0
- package/src/ui/components/tv.tsx +257 -0
- package/src/ui/components/typography/h1.tsx +18 -0
- package/src/ui/components/typography/h2.tsx +18 -0
- package/src/ui/components/typography/index.tsx +54 -0
- package/src/ui/components/typography/legend.tsx +24 -0
- package/src/ui/components/typography/small.tsx +11 -0
- package/src/ui/components/watchlist.stories.tsx +33 -0
- package/src/ui/components/watchlist.tsx +105 -0
- package/src/ui/fonts.css +63 -0
- package/src/ui/footer.tsx +111 -0
- package/src/ui/globals.css +395 -0
- package/src/ui/header.tsx +398 -0
- package/src/ui/layout-wrapper.tsx +11 -0
- package/src/utils/color.ts +21 -0
- package/src/utils/index.ts +62 -0
- package/src/utils/poly.ts +26 -0
- package/dist/ui/components/modal/index.d.ts +0 -8
- package/dist/ui/components/modal/index.js +0 -34
- 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
|
+
}
|