@nous-research/ui 0.15.0 → 0.16.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 +227 -0
- package/README.md +24 -4
- package/dist/fonts.js +1 -0
- package/dist/hooks/use-capped-frame.js +1 -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/index.js +1 -0
- 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/button.js +2 -1
- package/dist/ui/components/checkbox.js +1 -0
- package/dist/ui/components/command-block.js +4 -3
- package/dist/ui/components/cursor.js +1 -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/leva-client.js +1 -0
- package/dist/ui/components/list-item.js +3 -2
- package/dist/ui/components/modal/index.js +1 -0
- package/dist/ui/components/modal/modal.css +1 -1
- 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/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/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 +33 -1
- 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 +4 -2
- 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-capped-frame.ts +18 -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/index.ts +109 -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/button.stories.tsx +68 -0
- package/src/ui/components/button.tsx +170 -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/cursor.tsx +115 -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/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/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/modal/index.stories.tsx +46 -0
- package/src/ui/components/modal/index.tsx +48 -0
- package/src/ui/components/modal/modal.css +36 -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/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/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 +383 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Leva } from 'leva'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
export function LevaClient() {
|
|
7
|
+
const [hidden, setHidden] = useState(true)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setHidden(!new URLSearchParams(window.location.search).has('dev'))
|
|
11
|
+
}, [])
|
|
12
|
+
|
|
13
|
+
return <Leva {...{ hidden }} />
|
|
14
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { ListItem } from './list-item'
|
|
5
|
+
|
|
6
|
+
const PROVIDERS = [
|
|
7
|
+
{ count: 412, name: 'OpenAI', slug: 'openai' },
|
|
8
|
+
{ count: 38, name: 'Anthropic', slug: 'anthropic' },
|
|
9
|
+
{ count: 124, name: 'Google', slug: 'google' },
|
|
10
|
+
{ count: 7, name: 'Mistral', slug: 'mistral' },
|
|
11
|
+
{ count: 4, name: 'xAI', slug: 'xai' }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
function Demo() {
|
|
15
|
+
const [active, setActive] = useState<string>('anthropic')
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="w-72 border border-midground/15 bg-background-base">
|
|
19
|
+
{PROVIDERS.map(p => (
|
|
20
|
+
<ListItem
|
|
21
|
+
active={p.slug === active}
|
|
22
|
+
key={p.slug}
|
|
23
|
+
onClick={() => setActive(p.slug)}
|
|
24
|
+
>
|
|
25
|
+
<span className="flex-1 truncate">{p.name}</span>
|
|
26
|
+
<span className="text-[0.65rem] tabular-nums text-midground/50">
|
|
27
|
+
{p.count}
|
|
28
|
+
</span>
|
|
29
|
+
</ListItem>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const meta: Meta<typeof ListItem> = {
|
|
36
|
+
component: ListItem,
|
|
37
|
+
title: 'Components/ListItem'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default meta
|
|
41
|
+
|
|
42
|
+
type Story = StoryObj<typeof ListItem>
|
|
43
|
+
|
|
44
|
+
export const Playground: Story = { render: () => <Demo /> }
|
|
45
|
+
|
|
46
|
+
export const WithSubtitle: Story = {
|
|
47
|
+
render: () => {
|
|
48
|
+
function MultiLineDemo() {
|
|
49
|
+
const [active, setActive] = useState<string>('anthropic')
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="w-80 border border-midground/15 bg-background-base">
|
|
53
|
+
{PROVIDERS.map(p => (
|
|
54
|
+
<ListItem
|
|
55
|
+
active={p.slug === active}
|
|
56
|
+
key={p.slug}
|
|
57
|
+
onClick={() => setActive(p.slug)}
|
|
58
|
+
>
|
|
59
|
+
<div className="flex-1 min-w-0">
|
|
60
|
+
<div className="truncate font-medium">{p.name}</div>
|
|
61
|
+
<div className="truncate text-[0.65rem] text-midground/60">
|
|
62
|
+
{p.slug} · {p.count} models
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</ListItem>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return <MultiLineDemo />
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const Disabled: Story = {
|
|
76
|
+
render: () => (
|
|
77
|
+
<div className="w-72 border border-midground/15 bg-background-base">
|
|
78
|
+
<ListItem>Enabled item</ListItem>
|
|
79
|
+
<ListItem disabled>Disabled item</ListItem>
|
|
80
|
+
<ListItem active>Active item</ListItem>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type ButtonHTMLAttributes } from 'react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../../utils'
|
|
6
|
+
|
|
7
|
+
export const ListItem = forwardRef<HTMLButtonElement, ListItemProps>(
|
|
8
|
+
function ListItem(
|
|
9
|
+
{ active = false, children, className, type = 'button', ...props },
|
|
10
|
+
ref
|
|
11
|
+
) {
|
|
12
|
+
return (
|
|
13
|
+
<button
|
|
14
|
+
className={cn(
|
|
15
|
+
'group relative flex w-full items-center gap-2 px-3 py-2 text-left',
|
|
16
|
+
'font-courier text-sm transition-colors cursor-pointer',
|
|
17
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/30',
|
|
18
|
+
'disabled:cursor-not-allowed disabled:text-text-disabled',
|
|
19
|
+
active
|
|
20
|
+
? 'bg-midground/10 text-midground'
|
|
21
|
+
: 'text-text-secondary hover:text-midground hover:bg-midground/5',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
data-active={active || undefined}
|
|
25
|
+
ref={ref}
|
|
26
|
+
type={type}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
</button>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
interface ListItemProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
36
|
+
active?: boolean
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { Button } from '../button'
|
|
4
|
+
import { EyeIcon } from '../icons'
|
|
5
|
+
import { Modal } from '../modal'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Modal> = {
|
|
8
|
+
component: Modal,
|
|
9
|
+
title: 'Components/Modal'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof Modal>
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Modal
|
|
19
|
+
trigger={({ open }) => (
|
|
20
|
+
<Button onClick={open} prefix={<EyeIcon />}>
|
|
21
|
+
Open Modal
|
|
22
|
+
</Button>
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<h2>Modal Content</h2>
|
|
26
|
+
<p>Click outside or press Escape to close.</p>
|
|
27
|
+
</Modal>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const WithBody: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<Modal
|
|
34
|
+
trigger={({ open }) => <Button onClick={open}>Open with body</Button>}
|
|
35
|
+
>
|
|
36
|
+
<h2>A bigger modal</h2>
|
|
37
|
+
|
|
38
|
+
<p>
|
|
39
|
+
Native <code><dialog></code> under the hood. It portals into{' '}
|
|
40
|
+
<code>document.body</code>, so stacking context isn't a concern.
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<p>Press Escape or click the backdrop to dismiss.</p>
|
|
44
|
+
</Modal>
|
|
45
|
+
)
|
|
46
|
+
}
|