@nous-research/ui 0.14.2 → 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.
Files changed (216) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +24 -4
  3. package/dist/fonts.js +1 -0
  4. package/dist/hooks/use-capped-frame.js +1 -0
  5. package/dist/hooks/use-css-var-dims.js +1 -0
  6. package/dist/hooks/use-gpu-tier.js +1 -0
  7. package/dist/hooks/use-render-loop.js +1 -0
  8. package/dist/hooks/use-smooth-controls.js +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/ui/basic-page.js +1 -0
  11. package/dist/ui/components/animated-count.js +1 -0
  12. package/dist/ui/components/ascii.js +1 -0
  13. package/dist/ui/components/badge.js +2 -1
  14. package/dist/ui/components/badges/nous-girl.js +1 -0
  15. package/dist/ui/components/blend-mode.js +1 -0
  16. package/dist/ui/components/blink.js +1 -0
  17. package/dist/ui/components/button.js +2 -1
  18. package/dist/ui/components/checkbox.js +1 -0
  19. package/dist/ui/components/command-block.js +4 -3
  20. package/dist/ui/components/cursor.js +1 -0
  21. package/dist/ui/components/dropdown-menu.js +1 -0
  22. package/dist/ui/components/fit-text/index.js +1 -0
  23. package/dist/ui/components/graphs/bar-chart.js +1 -0
  24. package/dist/ui/components/graphs/index.js +1 -0
  25. package/dist/ui/components/graphs/line-chart.js +1 -0
  26. package/dist/ui/components/graphs/utils.js +1 -0
  27. package/dist/ui/components/grid/index.js +1 -0
  28. package/dist/ui/components/hover-bg.js +1 -0
  29. package/dist/ui/components/icons/arrow.js +1 -0
  30. package/dist/ui/components/icons/check.js +1 -0
  31. package/dist/ui/components/icons/chevron.js +1 -0
  32. package/dist/ui/components/icons/discord.js +1 -0
  33. package/dist/ui/components/icons/eye.js +1 -0
  34. package/dist/ui/components/icons/gear.js +1 -0
  35. package/dist/ui/components/icons/github.js +1 -0
  36. package/dist/ui/components/icons/hamburger.js +1 -0
  37. package/dist/ui/components/icons/heart.js +1 -0
  38. package/dist/ui/components/icons/index.js +1 -0
  39. package/dist/ui/components/icons/link.js +1 -0
  40. package/dist/ui/components/icons/minus.js +1 -0
  41. package/dist/ui/components/icons/search.js +1 -0
  42. package/dist/ui/components/image-distortion.js +1 -0
  43. package/dist/ui/components/leva-client.js +1 -0
  44. package/dist/ui/components/list-item.js +3 -2
  45. package/dist/ui/components/modal/index.js +1 -0
  46. package/dist/ui/components/modal/modal.css +1 -1
  47. package/dist/ui/components/overlays/blend-modes.js +1 -0
  48. package/dist/ui/components/overlays/glitch.js +1 -0
  49. package/dist/ui/components/overlays/greys.js +1 -0
  50. package/dist/ui/components/overlays/index.js +1 -0
  51. package/dist/ui/components/overlays/lens-layers.js +1 -0
  52. package/dist/ui/components/overlays/lens.js +1 -0
  53. package/dist/ui/components/overlays/noise.js +1 -0
  54. package/dist/ui/components/overlays/vignette.js +1 -0
  55. package/dist/ui/components/poster.js +1 -0
  56. package/dist/ui/components/progress.js +1 -0
  57. package/dist/ui/components/scene-canvas.js +1 -0
  58. package/dist/ui/components/scramble.js +1 -0
  59. package/dist/ui/components/segmented.js +5 -4
  60. package/dist/ui/components/select.js +1 -0
  61. package/dist/ui/components/selection-switcher.js +1 -0
  62. package/dist/ui/components/shader.js +1 -0
  63. package/dist/ui/components/socials.js +1 -0
  64. package/dist/ui/components/spinner.js +1 -0
  65. package/dist/ui/components/stats.js +2 -1
  66. package/dist/ui/components/switch.js +1 -0
  67. package/dist/ui/components/tabs.js +4 -3
  68. package/dist/ui/components/terminal-demo.js +2 -1
  69. package/dist/ui/components/theme-toggle.js +1 -0
  70. package/dist/ui/components/tier-card.js +2 -1
  71. package/dist/ui/components/tv.js +1 -0
  72. package/dist/ui/components/typography/h1.js +1 -0
  73. package/dist/ui/components/typography/h2.js +1 -0
  74. package/dist/ui/components/typography/index.js +1 -0
  75. package/dist/ui/components/typography/legend.js +1 -0
  76. package/dist/ui/components/typography/small.js +1 -0
  77. package/dist/ui/components/watchlist.js +2 -1
  78. package/dist/ui/footer.js +1 -0
  79. package/dist/ui/globals.css +33 -1
  80. package/dist/ui/header.js +1 -0
  81. package/dist/ui/layout-wrapper.js +2 -1
  82. package/dist/utils/color.js +1 -0
  83. package/dist/utils/index.js +1 -0
  84. package/dist/utils/poly.js +1 -0
  85. package/package.json +4 -2
  86. package/src/assets/filler-bg0.webp +0 -0
  87. package/src/assets.d.ts +38 -0
  88. package/src/fonts/Collapse-Bold.woff2 +0 -0
  89. package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
  90. package/src/fonts/Collapse-Italic.woff2 +0 -0
  91. package/src/fonts/Collapse-Light.woff2 +0 -0
  92. package/src/fonts/Collapse-LightItalic.woff2 +0 -0
  93. package/src/fonts/Collapse-Regular.woff2 +0 -0
  94. package/src/fonts/Collapse-Thin.woff2 +0 -0
  95. package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
  96. package/src/fonts/Mondwest-Regular.woff2 +0 -0
  97. package/src/fonts/Neuebit-Bold.woff2 +0 -0
  98. package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
  99. package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
  100. package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
  101. package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
  102. package/src/fonts.ts +6 -0
  103. package/src/hooks/use-capped-frame.ts +18 -0
  104. package/src/hooks/use-css-var-dims.ts +39 -0
  105. package/src/hooks/use-gpu-tier.ts +165 -0
  106. package/src/hooks/use-render-loop.ts +121 -0
  107. package/src/hooks/use-smooth-controls.ts +318 -0
  108. package/src/index.ts +109 -0
  109. package/src/ui/basic-page.tsx +34 -0
  110. package/src/ui/build.css +4 -0
  111. package/src/ui/components/animated-count.stories.tsx +67 -0
  112. package/src/ui/components/animated-count.tsx +168 -0
  113. package/src/ui/components/ascii.stories.tsx +30 -0
  114. package/src/ui/components/ascii.tsx +110 -0
  115. package/src/ui/components/badge.stories.tsx +31 -0
  116. package/src/ui/components/badge.tsx +60 -0
  117. package/src/ui/components/badges/nous-girl.tsx +52 -0
  118. package/src/ui/components/blend-mode.stories.tsx +33 -0
  119. package/src/ui/components/blend-mode.tsx +129 -0
  120. package/src/ui/components/blink.stories.tsx +32 -0
  121. package/src/ui/components/blink.tsx +21 -0
  122. package/src/ui/components/button.stories.tsx +68 -0
  123. package/src/ui/components/button.tsx +170 -0
  124. package/src/ui/components/checkbox.stories.tsx +113 -0
  125. package/src/ui/components/checkbox.tsx +36 -0
  126. package/src/ui/components/command-block.stories.tsx +52 -0
  127. package/src/ui/components/command-block.tsx +86 -0
  128. package/src/ui/components/cursor.tsx +115 -0
  129. package/src/ui/components/dropdown-menu.stories.tsx +52 -0
  130. package/src/ui/components/dropdown-menu.tsx +117 -0
  131. package/src/ui/components/fit-text/fit-text.css +42 -0
  132. package/src/ui/components/fit-text/index.stories.tsx +33 -0
  133. package/src/ui/components/fit-text/index.tsx +45 -0
  134. package/src/ui/components/graphs/bar-chart.tsx +153 -0
  135. package/src/ui/components/graphs/index.stories.tsx +64 -0
  136. package/src/ui/components/graphs/index.tsx +4 -0
  137. package/src/ui/components/graphs/line-chart.tsx +213 -0
  138. package/src/ui/components/graphs/utils.tsx +265 -0
  139. package/src/ui/components/grid/grid.css +79 -0
  140. package/src/ui/components/grid/index.tsx +19 -0
  141. package/src/ui/components/hover-bg.stories.tsx +29 -0
  142. package/src/ui/components/hover-bg.tsx +15 -0
  143. package/src/ui/components/icons/arrow.tsx +42 -0
  144. package/src/ui/components/icons/check.tsx +14 -0
  145. package/src/ui/components/icons/chevron.tsx +45 -0
  146. package/src/ui/components/icons/discord.tsx +16 -0
  147. package/src/ui/components/icons/eye.tsx +12 -0
  148. package/src/ui/components/icons/gear.tsx +51 -0
  149. package/src/ui/components/icons/github.tsx +16 -0
  150. package/src/ui/components/icons/hamburger.tsx +52 -0
  151. package/src/ui/components/icons/heart.tsx +12 -0
  152. package/src/ui/components/icons/index.ts +12 -0
  153. package/src/ui/components/icons/link.tsx +14 -0
  154. package/src/ui/components/icons/minus.tsx +14 -0
  155. package/src/ui/components/icons/search.tsx +28 -0
  156. package/src/ui/components/image-distortion.stories.tsx +120 -0
  157. package/src/ui/components/image-distortion.tsx +498 -0
  158. package/src/ui/components/leva-client.tsx +14 -0
  159. package/src/ui/components/list-item.stories.tsx +83 -0
  160. package/src/ui/components/list-item.tsx +37 -0
  161. package/src/ui/components/modal/index.stories.tsx +46 -0
  162. package/src/ui/components/modal/index.tsx +48 -0
  163. package/src/ui/components/modal/modal.css +36 -0
  164. package/src/ui/components/overlays/blend-modes.ts +13 -0
  165. package/src/ui/components/overlays/glitch.tsx +243 -0
  166. package/src/ui/components/overlays/greys.tsx +386 -0
  167. package/src/ui/components/overlays/index.tsx +47 -0
  168. package/src/ui/components/overlays/lens-layers.tsx +119 -0
  169. package/src/ui/components/overlays/lens.ts +91 -0
  170. package/src/ui/components/overlays/noise.tsx +174 -0
  171. package/src/ui/components/overlays/vignette.tsx +60 -0
  172. package/src/ui/components/poster.stories.tsx +513 -0
  173. package/src/ui/components/poster.tsx +411 -0
  174. package/src/ui/components/progress.stories.tsx +48 -0
  175. package/src/ui/components/progress.tsx +56 -0
  176. package/src/ui/components/scene-canvas.tsx +254 -0
  177. package/src/ui/components/scramble.stories.tsx +49 -0
  178. package/src/ui/components/scramble.tsx +95 -0
  179. package/src/ui/components/segmented.stories.tsx +101 -0
  180. package/src/ui/components/segmented.tsx +81 -0
  181. package/src/ui/components/select.stories.tsx +88 -0
  182. package/src/ui/components/select.tsx +267 -0
  183. package/src/ui/components/selection-switcher.tsx +44 -0
  184. package/src/ui/components/shader.tsx +83 -0
  185. package/src/ui/components/socials.tsx +42 -0
  186. package/src/ui/components/spinner.stories.tsx +101 -0
  187. package/src/ui/components/spinner.tsx +60 -0
  188. package/src/ui/components/stats.stories.tsx +24 -0
  189. package/src/ui/components/stats.tsx +53 -0
  190. package/src/ui/components/switch.stories.tsx +77 -0
  191. package/src/ui/components/switch.tsx +48 -0
  192. package/src/ui/components/tabs.stories.tsx +101 -0
  193. package/src/ui/components/tabs.tsx +66 -0
  194. package/src/ui/components/terminal-demo.stories.tsx +67 -0
  195. package/src/ui/components/terminal-demo.tsx +189 -0
  196. package/src/ui/components/theme-toggle.stories.tsx +47 -0
  197. package/src/ui/components/theme-toggle.tsx +66 -0
  198. package/src/ui/components/tier-card.stories.tsx +217 -0
  199. package/src/ui/components/tier-card.tsx +190 -0
  200. package/src/ui/components/tv.stories.tsx +37 -0
  201. package/src/ui/components/tv.tsx +257 -0
  202. package/src/ui/components/typography/h1.tsx +18 -0
  203. package/src/ui/components/typography/h2.tsx +18 -0
  204. package/src/ui/components/typography/index.tsx +54 -0
  205. package/src/ui/components/typography/legend.tsx +24 -0
  206. package/src/ui/components/typography/small.tsx +11 -0
  207. package/src/ui/components/watchlist.stories.tsx +33 -0
  208. package/src/ui/components/watchlist.tsx +105 -0
  209. package/src/ui/fonts.css +63 -0
  210. package/src/ui/footer.tsx +111 -0
  211. package/src/ui/globals.css +383 -0
  212. package/src/ui/header.tsx +398 -0
  213. package/src/ui/layout-wrapper.tsx +11 -0
  214. package/src/utils/color.ts +21 -0
  215. package/src/utils/index.ts +62 -0
  216. package/src/utils/poly.ts +26 -0
@@ -0,0 +1,386 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import * as THREE from 'three'
5
+
6
+ import { $gpuTier, useGpuTier } from '../../../hooks/use-gpu-tier'
7
+ import { runRenderLoop } from '../../../hooks/use-render-loop'
8
+ import { useSmoothControls } from '../../../hooks/use-smooth-controls'
9
+ import { cn } from '../../../utils'
10
+
11
+ import { BLEND_MODES } from './blend-modes'
12
+
13
+ const vert = /*glsl*/ `
14
+ varying vec2 vUv;
15
+ void main() {
16
+ vUv = uv;
17
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
18
+ }
19
+ `
20
+
21
+ const sourceFrag = /*glsl*/ `
22
+ uniform sampler2D uTex0, uTex1, uTex2, uTex3;
23
+ uniform float uTime, uZoom, uSpeed, uRotate, uFolds, uDrift;
24
+ varying vec2 vUv;
25
+
26
+ vec3 gray(vec3 c) { return vec3(dot(c, vec3(.299, .587, .114))); }
27
+ vec2 rot(vec2 p, float a) { return vec2(p.x * cos(a) - p.y * sin(a), p.x * sin(a) + p.y * cos(a)); }
28
+
29
+ vec2 kaleid(vec2 p, float n) {
30
+ float a = mod(atan(p.y, p.x), 6.28318 / n) - 3.14159 / n;
31
+ return length(p) * vec2(cos(a), sin(a));
32
+ }
33
+
34
+ vec4 tex(int i, vec2 uv) {
35
+ if (i == 0) return texture2D(uTex0, uv);
36
+ if (i == 1) return texture2D(uTex1, uv);
37
+ if (i == 2) return texture2D(uTex2, uv);
38
+ return texture2D(uTex3, uv);
39
+ }
40
+
41
+ void main() {
42
+ vec2 uv = rot(vUv - .5, uTime * uRotate * .05);
43
+ if (uFolds > 1.) uv = kaleid(uv, uFolds);
44
+
45
+ float dt = uTime * uDrift * .1;
46
+ uv = uv / uZoom + .5 + vec2(sin(dt * .7) * cos(dt * .3), cos(dt * .5) * sin(dt * .9)) * .15 * uDrift;
47
+
48
+ float cycle = mod(uTime * uSpeed * .01, 4.);
49
+ int i0 = int(floor(cycle)), i1 = int(mod(float(i0) + 1., 4.));
50
+ float t = smoothstep(0., 1., fract(cycle));
51
+
52
+ vec3 base = mix(gray(vec3(1.) - tex(i0, uv).rgb), gray(vec3(1.) - tex(i1, uv).rgb), t);
53
+ vec2 uvF = vec2(1. - uv.x, uv.y);
54
+ vec3 flip = mix(gray(vec3(1.) - tex(i0, uvF).rgb), gray(vec3(1.) - tex(i1, uvF).rgb), t);
55
+
56
+ gl_FragColor = vec4(mix(base, flip, .3 + sin(uTime * .2) * .2), 1.);
57
+ }
58
+ `
59
+
60
+ const moshFrag = /*glsl*/ `
61
+ uniform sampler2D uCurrent, uPrev, uTex0, uTex1, uTex2, uTex3;
62
+ uniform float uTime, uIntensity, uMotion, uZoom, uSpeed;
63
+ uniform vec2 uRes;
64
+ varying vec2 vUv;
65
+
66
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
67
+ vec2 hash2(vec2 p) { return fract(sin(vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)))) * 43758.5453); }
68
+
69
+ float noise(vec2 p) {
70
+ vec2 i = floor(p), f = fract(p) * fract(p) * (3. - 2. * fract(p));
71
+ return mix(mix(hash(i), hash(i + vec2(1., 0.)), f.x), mix(hash(i + vec2(0., 1.)), hash(i + vec2(1., 1.)), f.x), f.y);
72
+ }
73
+
74
+ vec3 gray(vec3 c) { return vec3(dot(c, vec3(.299, .587, .114))); }
75
+
76
+ vec2 distort(vec2 uv, float k, float t) {
77
+ float n1 = noise(uv * 8. + t * .5), n2 = noise(uv * 12. + t * .7), flow = noise(uv * 4. + t * .3);
78
+ return uv + vec2(cos(n1 * 6.28 + t * 1.2), sin(n2 * 6.28 + t * .9)) * .02 * k
79
+ + vec2(cos(flow * 6.28 + uv.y * 10.), sin(flow * 6.28 + uv.x * 10.)) * .015 * k;
80
+ }
81
+
82
+ vec3 tex(int i, vec2 uv) {
83
+ vec2 zuv = (uv - .5) / uZoom + .5;
84
+ if (i == 0) return gray(vec3(1.) - texture2D(uTex0, zuv).rgb);
85
+ if (i == 1) return gray(vec3(1.) - texture2D(uTex1, zuv).rgb);
86
+ if (i == 2) return gray(vec3(1.) - texture2D(uTex2, zuv).rgb);
87
+ return gray(vec3(1.) - texture2D(uTex3, zuv).rgb);
88
+ }
89
+
90
+ void main() {
91
+ vec2 uv = vUv;
92
+ float t = uTime * uSpeed, tS = floor(t * .1), pS = 80.;
93
+ float amt = uIntensity * uMotion * .8 * (.7 + (sin(t * .5) * .5 + .5) * .3);
94
+
95
+ vec2 mUV = distort(uv, uIntensity * .4, t);
96
+
97
+ float hS = floor(uv.y * pS), hA = smoothstep(0., .8, hash(vec2(hS, tS)));
98
+ float hO = (hash(vec2(hS, tS + 50.)) - .5) * .25 * hA * amt;
99
+ float vS = floor(uv.x * pS), vA = smoothstep(0., .8, hash(vec2(vS, tS + 100.)));
100
+ float vO = (hash(vec2(vS, tS + 150.)) - .5) * .25 * vA * amt;
101
+ mUV += vec2(hO, vO);
102
+
103
+ float bS = pS * .25;
104
+ float hBA = step(.5, hash(vec2(floor(uv.y * bS), tS + 200.)));
105
+ float hBO = (hash(vec2(floor(uv.y * bS), 200.)) - .5) * .35 * hBA * amt;
106
+ float vBA = step(.5, hash(vec2(floor(uv.x * bS), tS + 300.)));
107
+ float vBO = (hash(vec2(floor(uv.x * bS), 250.)) - .5) * .35 * vBA * amt;
108
+ mUV += vec2(hBO, vBO);
109
+
110
+ vec2 blk = floor(uv * pS * .15);
111
+ mUV += (hash2(vec2(blk.x, blk.y + 500.)) - .5) * .4 * step(.7, hash(vec2(blk.x, blk.y + tS))) * amt;
112
+ mUV = clamp(mUV, 0., 1.);
113
+
114
+ vec3 prev = texture2D(uPrev, mUV).rgb;
115
+ prev = mix(prev, texture2D(uPrev, clamp(uv + vec2(hBO, vBO), 0., 1.)).rgb, max(hBA, vBA) * .9);
116
+
117
+ float tY = floor(uv.y * pS * .4);
118
+ if (hash(vec2(tY, tS + 400.)) > .75) {
119
+ prev = mix(prev, texture2D(uPrev, clamp(vec2(uv.x + (hash(vec2(tY, 400.)) - .5) * .5 * amt, uv.y), 0., 1.)).rgb, .85);
120
+ }
121
+
122
+ if (hA > 0. && amt > .01) {
123
+ prev = mix(prev, gray(texture2D(uPrev, clamp(vec2(uv.x + (gray(prev).r - uv.x) * amt + hO, uv.y), 0., 1.)).rgb), hA);
124
+ }
125
+
126
+ float d = mix(mix(.97, .99, noise(uv * 8. + t * .2)), 0., step(.994, hash(vec2(tS, 0.))));
127
+ gl_FragColor = vec4(mix(texture2D(uCurrent, uv).rgb, prev, d), 1.);
128
+ }
129
+ `
130
+
131
+ const outputFrag = /*glsl*/ `
132
+ uniform sampler2D uInput;
133
+ uniform float uTime, uAlpha, uHue;
134
+ uniform vec3 uColor;
135
+ varying vec2 vUv;
136
+
137
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
138
+
139
+ vec3 hueShift(vec3 c, float h) {
140
+ float a = h * 6.28318, s = sin(a), co = cos(a);
141
+ vec3 w = vec3(.299, .587, .114);
142
+ return clamp(vec3(
143
+ dot(c, w) + dot(c, vec3(.701, -.587, -.114) * co + vec3(.168, .330, -.497) * s),
144
+ dot(c, w) + dot(c, vec3(-.299, .413, -.114) * co + vec3(.328, .035, -.363) * s),
145
+ dot(c, w) + dot(c, vec3(-.299, -.587, .886) * co + vec3(-.497, .330, .168) * s)
146
+ ), 0., 1.);
147
+ }
148
+
149
+ void main() {
150
+ vec3 m = texture2D(uInput, vUv).rgb;
151
+ m *= 1. - step(.5, fract(vUv.y * 200.)) * .06 * step(.97, hash(vec2(floor(vUv.y * 30.), floor(uTime * .5))));
152
+
153
+ float lum = dot(m, vec3(.299, .587, .114));
154
+ gl_FragColor = vec4(hueShift(mix(vec3(lum), uColor * lum * 2., length(uColor)), uHue) * uAlpha, smoothstep(.08, .18, lum * uAlpha));
155
+ }
156
+ `
157
+
158
+ const TEXTURES = [
159
+ '/anatomy/grays-0.jpg',
160
+ '/anatomy/grays-3.jpg',
161
+ '/anatomy/grays-6.jpg',
162
+ '/anatomy/grays-9.jpg'
163
+ ]
164
+
165
+ export function Greys({ className, style }: GreysProps) {
166
+ const gpuTier = useGpuTier()
167
+ const [blendOverride, setBlendOverride] = useState<string | null>(null)
168
+ const canvasRef = useRef<HTMLCanvasElement>(null)
169
+
170
+ const c = useSmoothControls(
171
+ 'Effects/Greys',
172
+ {
173
+ alpha: { max: 1, min: 0, step: 0.01, value: 0.19 },
174
+ blend: { options: BLEND_MODES, value: 'color-burn' },
175
+ color: { value: '#ffac02' },
176
+ drift: { max: 2, min: 0, step: 0.1, value: 0.5 },
177
+ enabled: { value: false },
178
+ folds: { max: 12, min: 1, step: 1, value: 1 },
179
+ hue: { max: 1, min: 0, step: 0.01, value: 0.37 },
180
+ intensity: { max: 3, min: 0, step: 0.1, value: 0.1 },
181
+ motion: { max: 2, min: 0, step: 0.1, value: 0.1 },
182
+ rotate: { max: 2, min: -2, step: 0.1, value: 0.3 },
183
+ speed: { max: 1, min: 0.01, step: 0.01, value: 0.21 },
184
+ zoom: { max: 4, min: 0.5, step: 0.1, value: 0.7 }
185
+ },
186
+ { collapsed: true }
187
+ )
188
+
189
+ const cRef = useRef(c)
190
+ cRef.current = c
191
+
192
+ useEffect(() => {
193
+ const onKey = (e: KeyboardEvent) =>
194
+ e.key.toLowerCase() === 'x' &&
195
+ setBlendOverride(p => (p === 'screen' ? null : 'screen'))
196
+
197
+ window.addEventListener('keydown', onKey)
198
+
199
+ return () => window.removeEventListener('keydown', onKey)
200
+ }, [])
201
+
202
+ const enabled = c.enabled && gpuTier === 2
203
+
204
+ useEffect(() => {
205
+ if (!canvasRef.current || !enabled) {
206
+ return
207
+ }
208
+
209
+ let renderer: THREE.WebGLRenderer
210
+
211
+ try {
212
+ renderer = new THREE.WebGLRenderer({
213
+ alpha: true,
214
+ canvas: canvasRef.current
215
+ })
216
+ } catch {
217
+ // See note in noise.tsx — eager gpu-tier detection should keep us
218
+ // out of here, but if the driver fails the renderer constructor
219
+ // anyway, downgrade so other overlays stop trying too.
220
+ $gpuTier.set(0)
221
+
222
+ return
223
+ }
224
+
225
+ const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
226
+ const geo = new THREE.PlaneGeometry(2, 2)
227
+
228
+ const [rtSource, rtA, rtB] = [0, 1, 2].map(
229
+ () =>
230
+ new THREE.WebGLRenderTarget(innerWidth, innerHeight, {
231
+ magFilter: THREE.NearestFilter,
232
+ minFilter: THREE.NearestFilter
233
+ })
234
+ )
235
+
236
+ const textures = TEXTURES.map(p => {
237
+ const t = new THREE.TextureLoader().load(p)
238
+ t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping
239
+ t.minFilter = t.magFilter = THREE.LinearFilter
240
+
241
+ return t
242
+ })
243
+
244
+ const texU = Object.fromEntries(
245
+ textures.map((t, i) => [`uTex${i}`, { value: t }])
246
+ )
247
+
248
+ const srcU = {
249
+ ...texU,
250
+ uDrift: { value: 0 },
251
+ uFolds: { value: 0 },
252
+ uRotate: { value: 0 },
253
+ uSpeed: { value: 0 },
254
+ uTime: { value: 0 },
255
+ uZoom: { value: 0 }
256
+ }
257
+
258
+ const moshU = {
259
+ ...texU,
260
+ uCurrent: { value: rtSource.texture },
261
+ uIntensity: { value: 0 },
262
+ uMotion: { value: 0 },
263
+ uPrev: { value: rtA.texture },
264
+ uRes: { value: new THREE.Vector2(innerWidth, innerHeight) },
265
+ uSpeed: { value: 0 },
266
+ uTime: { value: 0 },
267
+ uZoom: { value: 0 }
268
+ }
269
+
270
+ const outU = {
271
+ uAlpha: { value: 0 },
272
+ uColor: { value: new THREE.Color() },
273
+ uHue: { value: 0 },
274
+ uInput: { value: rtB.texture },
275
+ uTime: { value: 0 }
276
+ }
277
+
278
+ const mkScene = (frag: string, uniforms: object, transparent = false) => {
279
+ const s = new THREE.Scene()
280
+ s.add(
281
+ new THREE.Mesh(
282
+ geo.clone(),
283
+ new THREE.ShaderMaterial({
284
+ fragmentShader: frag,
285
+ transparent,
286
+ uniforms: uniforms as Record<string, THREE.IUniform<any>>,
287
+ vertexShader: vert
288
+ })
289
+ )
290
+ )
291
+
292
+ return s
293
+ }
294
+
295
+ const srcScene = mkScene(sourceFrag, srcU)
296
+ const moshScene = mkScene(moshFrag, moshU)
297
+ const outScene = mkScene(outputFrag, outU, true)
298
+
299
+ const resize = () => {
300
+ renderer.setSize(innerWidth, innerHeight)
301
+ // Cap at 1.5x — Greys does triple-buffered ping-pong rendering at
302
+ // every frame, so retina x2 is brutal on fillrate.
303
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5))
304
+ ;[rtSource, rtA, rtB].forEach(rt => rt.setSize(innerWidth, innerHeight))
305
+ moshU.uRes.value.set(innerWidth, innerHeight)
306
+ }
307
+
308
+ resize()
309
+ window.addEventListener('resize', resize)
310
+
311
+ let ping = true,
312
+ time = 0
313
+
314
+ // 30fps cap — feedback effect, no perceptual loss vs 60fps but
315
+ // halves the cost of the heaviest overlay we ship.
316
+ const dispose = runRenderLoop({
317
+ el: canvasRef.current,
318
+ minIntervalMs: 33,
319
+ onFrame: deltaSeconds => {
320
+ time += deltaSeconds
321
+
322
+ const v = cRef.current
323
+
324
+ srcU.uTime.value = time
325
+ srcU.uSpeed.value = v.speed
326
+ srcU.uZoom.value = v.zoom
327
+ srcU.uRotate.value = v.rotate
328
+ srcU.uFolds.value = v.folds
329
+ srcU.uDrift.value = v.drift
330
+
331
+ moshU.uTime.value = time
332
+ moshU.uIntensity.value = v.intensity
333
+ moshU.uMotion.value = v.motion
334
+ moshU.uSpeed.value = v.speed
335
+ moshU.uZoom.value = v.zoom
336
+
337
+ outU.uTime.value = time
338
+ outU.uAlpha.value = v.alpha
339
+ outU.uHue.value = v.hue
340
+ outU.uColor.value.set(typeof v.color === 'string' ? v.color : '#fff')
341
+
342
+ renderer.setRenderTarget(rtSource)
343
+ renderer.render(srcScene, camera)
344
+
345
+ const [read, write] = ping ? [rtA, rtB] : [rtB, rtA]
346
+ moshU.uPrev.value = read.texture
347
+ renderer.setRenderTarget(write)
348
+ renderer.render(moshScene, camera)
349
+
350
+ outU.uInput.value = write.texture
351
+ renderer.setRenderTarget(null)
352
+ renderer.render(outScene, camera)
353
+
354
+ ping = !ping
355
+ }
356
+ })
357
+
358
+ return () => {
359
+ window.removeEventListener('resize', resize)
360
+ dispose()
361
+ textures.forEach(t => t.dispose())
362
+ ;[geo, rtSource, rtA, rtB, renderer].forEach(x => x.dispose())
363
+ }
364
+ }, [enabled])
365
+
366
+ if (!enabled) {
367
+ return null
368
+ }
369
+
370
+ return (
371
+ <canvas
372
+ className={cn('h-full w-full', className)}
373
+ ref={canvasRef}
374
+ style={{
375
+ mixBlendMode: (blendOverride ??
376
+ c.blend) as React.CSSProperties['mixBlendMode'],
377
+ ...style
378
+ }}
379
+ />
380
+ )
381
+ }
382
+
383
+ interface GreysProps {
384
+ className?: string
385
+ style?: React.CSSProperties
386
+ }
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+
3
+ import { Glitch } from './glitch'
4
+ import { Greys } from './greys'
5
+ import { Lens } from './lens-layers'
6
+ import { Noise } from './noise'
7
+ import { Vignette } from './vignette'
8
+
9
+ import type { LensPreset } from './lens'
10
+
11
+ export { BLEND_MODES } from './blend-modes'
12
+ export { Glitch } from './glitch'
13
+ export { Greys } from './greys'
14
+ export { Lens } from './lens-layers'
15
+ export { Noise } from './noise'
16
+ export { Vignette } from './vignette'
17
+ export {
18
+ $lightMode,
19
+ applyLens,
20
+ lens0,
21
+ lens5i,
22
+ LENS_0,
23
+ LENS_5I,
24
+ LENSES,
25
+ toggleLens
26
+ } from './lens'
27
+ export type { LensPreset } from './lens'
28
+
29
+ const LAYER = 'pointer-events-none fixed inset-0'
30
+
31
+ export function Overlays({ dark, initial }: OverlaysProps) {
32
+ return (
33
+ <>
34
+ <Lens dark={dark} initial={initial} />
35
+
36
+ <Noise className={LAYER} style={{ zIndex: 101 }} />
37
+ <Vignette className={LAYER} style={{ zIndex: 99 }} />
38
+ <Greys className={LAYER} style={{ zIndex: 200 }} />
39
+ <Glitch className={LAYER} style={{ zIndex: 201 }} />
40
+ </>
41
+ )
42
+ }
43
+
44
+ interface OverlaysProps {
45
+ dark?: boolean
46
+ initial?: LensPreset
47
+ }
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ import { useSmoothControls } from '../../../hooks/use-smooth-controls'
6
+ import { colorMix } from '../../../utils/color'
7
+
8
+ import fillerBg from '../../../assets/filler-bg0.webp'
9
+
10
+ import { BLEND_MODES } from './blend-modes'
11
+ import { $lightMode, LENS_0, LENS_5I, type LensPreset, toggleLens } from './lens'
12
+
13
+ const LAYER = 'pointer-events-none fixed inset-0'
14
+
15
+ export function Lens({ dark, initial }: LensProps) {
16
+ // `initial` lets the host (e.g. Storybook) seed the Leva/atom state with
17
+ // the *exact* lens preset the user selected, avoiding a one-cycle lag
18
+ // where useSmoothControls emits old colors for the first paint (and, on
19
+ // Storybook's fast iframe reload, sometimes never catches up because
20
+ // useControls' ready-gate swallows the instant color writes).
21
+ const base = initial?.Lens ?? (dark ? LENS_0.Lens : LENS_5I.Lens)
22
+
23
+ const lens = useSmoothControls(
24
+ 'Lens',
25
+ {
26
+ bgBlend: { options: BLEND_MODES, value: base.bgBlend as 'multiply' },
27
+ bgColor: { value: base.bgColor },
28
+ bgOpacity: { max: 1, min: 0, step: 0.01, value: base.bgOpacity },
29
+ fgBlend: { options: BLEND_MODES, value: 'difference' as const },
30
+ fgColor: { value: base.fgColor },
31
+ fgOpacity: { max: 1, min: 0, step: 0.01, value: base.fgOpacity },
32
+ fillerBlend: { options: BLEND_MODES, value: 'difference' as const },
33
+ fillerOpacity: { max: 1, min: 0, step: 0.01, value: base.fillerOpacity },
34
+ mgColor: { value: base.mgColor },
35
+ mgOpacity: { max: 1, min: 0, step: 0.01, value: base.mgOpacity }
36
+ },
37
+ { collapsed: false }
38
+ )
39
+
40
+ useEffect(() => {
41
+ $lightMode.set(!dark)
42
+ }, [dark])
43
+
44
+ useEffect(() => {
45
+ const s = document.documentElement.style
46
+
47
+ for (const [name, color, alpha] of [
48
+ ['foreground', lens.fgColor, lens.fgOpacity],
49
+ ['midground', lens.mgColor, lens.mgOpacity],
50
+ ['background', lens.bgColor, lens.bgOpacity]
51
+ ] as [string, string, number][]) {
52
+ s.setProperty(`--${name}`, colorMix(color, alpha))
53
+ s.setProperty(`--${name}-base`, color)
54
+ s.setProperty(`--${name}-alpha`, `${alpha}`)
55
+ }
56
+ }, [lens])
57
+
58
+ useEffect(() => {
59
+ const handle = (e: KeyboardEvent) => e.key === 'x' && toggleLens()
60
+ window.addEventListener('keydown', handle)
61
+ return () => window.removeEventListener('keydown', handle)
62
+ }, [])
63
+
64
+ // NOTE: z-index is inlined because Tailwind's JIT sometimes doesn't emit
65
+ // these non-default utilities (e.g. in Storybook's isolated content
66
+ // scan), which silently collapses the overlay stack to DOM order and
67
+ // breaks the mix-blend-mode inversion — producing a muddy warm wash
68
+ // instead of the intended clean black/white inversion.
69
+ return (
70
+ <>
71
+ <div
72
+ className={LAYER}
73
+ style={{
74
+ backgroundColor: colorMix(lens.fgColor, lens.fgOpacity),
75
+ mixBlendMode: lens.fgBlend,
76
+ zIndex: 100
77
+ }}
78
+ />
79
+
80
+ <div
81
+ className={LAYER}
82
+ style={{
83
+ mixBlendMode: lens.fillerBlend,
84
+ opacity: lens.fillerOpacity,
85
+ zIndex: 2
86
+ }}
87
+ >
88
+ {/* eslint-disable-next-line @next/next/no-img-element */}
89
+ <img
90
+ alt=""
91
+ className="h-[150dvh] w-auto min-w-dvw object-cover object-top-left invert"
92
+ fetchPriority="low"
93
+ src={fillerBg.src}
94
+ />
95
+ </div>
96
+
97
+ <div
98
+ className={LAYER}
99
+ style={{
100
+ backgroundColor: colorMix(lens.bgColor, lens.bgOpacity),
101
+ mixBlendMode: lens.bgBlend,
102
+ zIndex: 1
103
+ }}
104
+ />
105
+ </>
106
+ )
107
+ }
108
+
109
+ interface LensProps {
110
+ dark?: boolean
111
+ /**
112
+ * Exact preset to seed the internal Leva controls with. When omitted the
113
+ * component falls back to `LENS_0` / `LENS_5I` based on `dark`. Pass the
114
+ * actual preset from a host (e.g. Storybook toolbar) to guarantee the
115
+ * first-paint colors match the selected lens without needing a followup
116
+ * `applyLens` that can be lost in useSmoothControls' startup window.
117
+ */
118
+ initial?: LensPreset
119
+ }
@@ -0,0 +1,91 @@
1
+ 'use client'
2
+
3
+ import { atom } from 'nanostores'
4
+
5
+ import { setControlValue } from '../../../hooks/use-smooth-controls'
6
+
7
+ export const LENS_0 = {
8
+ Globe: { innerColor: '#170d02', innerOpacity: 0.1, outerColor: '#FFAC02' },
9
+ Lens: {
10
+ bgBlend: 'difference',
11
+ bgColor: '#041C1C',
12
+ bgOpacity: 1,
13
+ fgColor: '#FFFFFF',
14
+ fgOpacity: 0,
15
+ fillerOpacity: 0.033,
16
+ mgColor: '#ffe6cb',
17
+ mgOpacity: 1
18
+ }
19
+ }
20
+
21
+ export const LENS_5I = {
22
+ Globe: { innerColor: '#170d02', innerOpacity: 0.3, outerColor: '#FFAC02' },
23
+ Lens: {
24
+ bgBlend: 'multiply',
25
+ bgColor: '#170d02',
26
+ bgOpacity: 1,
27
+ fgColor: '#FFFFFF',
28
+ fgOpacity: 1,
29
+ fillerOpacity: 0.06,
30
+ mgColor: '#FFAC02',
31
+ mgOpacity: 1
32
+ }
33
+ }
34
+
35
+ export const lens0 = (
36
+ l?: Partial<typeof LENS_0.Lens>,
37
+ g?: Partial<typeof LENS_0.Globe>
38
+ ): LensPreset => ({
39
+ Globe: { ...LENS_0.Globe, ...g },
40
+ Lens: { ...LENS_0.Lens, ...l }
41
+ })
42
+
43
+ // The Hermes light-mode look is produced by a fullscreen opaque-white
44
+ // `mix-blend-mode: difference` foreground layer that inverts everything.
45
+ // Colored lenses that want a "white + accent" look MUST be built from
46
+ // LENS_5I, not LENS_0 — otherwise `bgBlend: 'difference'` + an opaque
47
+ // colored bg + active fg inversion land halfway between dark and light
48
+ // mode and produce a muddy warm wash instead of a clean inversion.
49
+ export const lens5i = (
50
+ l?: Partial<typeof LENS_5I.Lens>,
51
+ g?: Partial<typeof LENS_5I.Globe>
52
+ ): LensPreset => ({
53
+ Globe: { ...LENS_5I.Globe, ...g },
54
+ Lens: { ...LENS_5I.Lens, ...l }
55
+ })
56
+
57
+ // Accent colors are the *pre-inversion* source; after the difference FG
58
+ // layer they read as their visual complement. e.g. `#FFAC02` (orange)
59
+ // renders as blue #0053FD on screen — that's the default LENS_5I accent.
60
+ export const LENSES: [string, LensPreset][] = [
61
+ ['0', LENS_0],
62
+ ['1', lens0({ bgColor: '#0A1F1F' })],
63
+ ['2', lens0({ bgColor: '#0E0313', mgColor: '#e6cbff' })],
64
+ ['3', lens5i({ mgColor: '#FFAC02' })],
65
+ ['4', lens5i({ bgColor: '#0E0313', mgColor: '#FF5500' })],
66
+ ['5', lens0({ bgColor: '#1540B1', bgOpacity: 0.7 })],
67
+ ['5i', LENS_5I],
68
+ ['6', lens5i({ bgColor: '#170D02', mgColor: '#00E5FF' })]
69
+ ]
70
+
71
+ export const applyLens = (preset: LensPreset, animate = false) =>
72
+ Object.entries(preset).forEach(([g, v]) =>
73
+ Object.entries(v).forEach(([k, val]) =>
74
+ setControlValue(g, k, val, { animate })
75
+ )
76
+ )
77
+
78
+ export const $lightMode = atom(true)
79
+
80
+ export const toggleLens = () => {
81
+ const isLight = $lightMode.get()
82
+ const next = isLight ? LENS_0 : LENS_5I
83
+
84
+ $lightMode.set(!isLight)
85
+ applyLens(next, true)
86
+ }
87
+
88
+ export interface LensPreset {
89
+ Globe: typeof LENS_0.Globe
90
+ Lens: typeof LENS_0.Lens
91
+ }