@basementstudio/shader-lab 0.1.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 (261) hide show
  1. package/.biome/plugins/README.md +21 -0
  2. package/.biome/plugins/no-anchor-element.grit +12 -0
  3. package/.biome/plugins/no-relative-parent-imports.grit +10 -0
  4. package/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  5. package/.changeset/README.md +17 -0
  6. package/.changeset/config.json +11 -0
  7. package/.editorconfig +40 -0
  8. package/.env.example +81 -0
  9. package/.gitattributes +19 -0
  10. package/.github/workflows/canary.yml +80 -0
  11. package/.github/workflows/ci.yml +37 -0
  12. package/.github/workflows/release.yml +56 -0
  13. package/.tldrignore +84 -0
  14. package/.vscode/extensions.json +20 -0
  15. package/.vscode/settings.json +105 -0
  16. package/README.md +119 -0
  17. package/biome.json +249 -0
  18. package/bun.lock +1224 -0
  19. package/next.config.ts +131 -0
  20. package/package.json +73 -0
  21. package/packages/shader-lab-react/CHANGELOG.md +9 -0
  22. package/packages/shader-lab-react/README.md +119 -0
  23. package/packages/shader-lab-react/assets/patterns/bars/1.svg +3 -0
  24. package/packages/shader-lab-react/assets/patterns/bars/2.svg +3 -0
  25. package/packages/shader-lab-react/assets/patterns/bars/3.svg +3 -0
  26. package/packages/shader-lab-react/assets/patterns/bars/4.svg +3 -0
  27. package/packages/shader-lab-react/assets/patterns/bars/5.svg +3 -0
  28. package/packages/shader-lab-react/assets/patterns/bars/6.svg +3 -0
  29. package/packages/shader-lab-react/assets/patterns/candles/1.svg +3 -0
  30. package/packages/shader-lab-react/assets/patterns/candles/2.svg +3 -0
  31. package/packages/shader-lab-react/assets/patterns/candles/3.svg +3 -0
  32. package/packages/shader-lab-react/assets/patterns/candles/4.svg +3 -0
  33. package/packages/shader-lab-react/assets/patterns/shapes/1.svg +3 -0
  34. package/packages/shader-lab-react/assets/patterns/shapes/2.svg +3 -0
  35. package/packages/shader-lab-react/assets/patterns/shapes/3.svg +3 -0
  36. package/packages/shader-lab-react/assets/patterns/shapes/4.svg +4 -0
  37. package/packages/shader-lab-react/assets/patterns/shapes/5.svg +3 -0
  38. package/packages/shader-lab-react/assets/patterns/shapes/6.svg +4 -0
  39. package/packages/shader-lab-react/assets/textures/blue-noise.png +0 -0
  40. package/packages/shader-lab-react/package.json +36 -0
  41. package/packages/shader-lab-react/scripts/fix-esm-specifiers.mjs +57 -0
  42. package/packages/shader-lab-react/scripts/prepare-dist.mjs +4 -0
  43. package/packages/shader-lab-react/src/ambient/three-tsl.d.ts +146 -0
  44. package/packages/shader-lab-react/src/ambient/three-webgpu.d.ts +51 -0
  45. package/packages/shader-lab-react/src/easings.ts +4 -0
  46. package/packages/shader-lab-react/src/index.ts +35 -0
  47. package/packages/shader-lab-react/src/lib/editor/custom-shader/shared.ts +2 -0
  48. package/packages/shader-lab-react/src/renderer/ascii-atlas.ts +83 -0
  49. package/packages/shader-lab-react/src/renderer/ascii-pass.ts +416 -0
  50. package/packages/shader-lab-react/src/renderer/asset-url.ts +3 -0
  51. package/packages/shader-lab-react/src/renderer/blend-modes.ts +229 -0
  52. package/packages/shader-lab-react/src/renderer/contracts.ts +54 -0
  53. package/packages/shader-lab-react/src/renderer/create-webgpu-renderer.ts +48 -0
  54. package/packages/shader-lab-react/src/renderer/crt-pass.ts +1040 -0
  55. package/packages/shader-lab-react/src/renderer/custom-shader-pass.ts +108 -0
  56. package/packages/shader-lab-react/src/renderer/custom-shader-runtime.ts +309 -0
  57. package/packages/shader-lab-react/src/renderer/dither-textures.ts +99 -0
  58. package/packages/shader-lab-react/src/renderer/dithering-pass.ts +322 -0
  59. package/packages/shader-lab-react/src/renderer/gradient-pass.ts +521 -0
  60. package/packages/shader-lab-react/src/renderer/halftone-pass.ts +932 -0
  61. package/packages/shader-lab-react/src/renderer/ink-pass.ts +802 -0
  62. package/packages/shader-lab-react/src/renderer/live-pass.ts +194 -0
  63. package/packages/shader-lab-react/src/renderer/media-pass.ts +187 -0
  64. package/packages/shader-lab-react/src/renderer/media-texture.ts +66 -0
  65. package/packages/shader-lab-react/src/renderer/particle-grid-pass.ts +389 -0
  66. package/packages/shader-lab-react/src/renderer/pass-node.ts +209 -0
  67. package/packages/shader-lab-react/src/renderer/pattern-atlas.ts +133 -0
  68. package/packages/shader-lab-react/src/renderer/pattern-pass.ts +552 -0
  69. package/packages/shader-lab-react/src/renderer/pipeline-manager.ts +369 -0
  70. package/packages/shader-lab-react/src/renderer/pixel-sorting-pass.ts +277 -0
  71. package/packages/shader-lab-react/src/renderer/shaders/tsl/color/tonemapping.ts +87 -0
  72. package/packages/shader-lab-react/src/renderer/shaders/tsl/cosine-palette.ts +9 -0
  73. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/common.ts +31 -0
  74. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/curl-noise-3d.ts +36 -0
  75. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/curl-noise-4d.ts +36 -0
  76. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/fbm.ts +13 -0
  77. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/perlin-noise-3d.ts +96 -0
  78. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/ridge-noise.ts +24 -0
  79. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/simplex-noise-3d.ts +79 -0
  80. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/simplex-noise-4d.ts +89 -0
  81. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/turbulence.ts +56 -0
  82. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/value-noise-3d.ts +32 -0
  83. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/voronoi-noise-3d.ts +60 -0
  84. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/bloom-edge-pattern.ts +15 -0
  85. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/bloom.ts +11 -0
  86. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/canvas-weave-pattern.ts +24 -0
  87. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/grain-texture-pattern.ts +9 -0
  88. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/repeating-pattern.ts +11 -0
  89. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/atan2.ts +9 -0
  90. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-conj.ts +9 -0
  91. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-cos.ts +10 -0
  92. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-div.ts +11 -0
  93. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-log.ts +7 -0
  94. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-mobius.ts +12 -0
  95. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-mul.ts +9 -0
  96. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-pow.ts +16 -0
  97. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-sin.ts +10 -0
  98. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-sqrt.ts +18 -0
  99. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-tan.ts +12 -0
  100. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-to-polar.ts +10 -0
  101. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/hyperbolic.ts +20 -0
  102. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/index.ts +48 -0
  103. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/rotate.ts +15 -0
  104. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/screen-aspect-uv.ts +15 -0
  105. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-box-2d.ts +6 -0
  106. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-diamond.ts +6 -0
  107. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-rhombus.ts +27 -0
  108. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-sphere.ts +6 -0
  109. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/smax.ts +7 -0
  110. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/smin.ts +7 -0
  111. package/packages/shader-lab-react/src/renderer/text-pass.ts +176 -0
  112. package/packages/shader-lab-react/src/runtime-clock.ts +42 -0
  113. package/packages/shader-lab-react/src/runtime-frame.ts +29 -0
  114. package/packages/shader-lab-react/src/shader-lab-composition.tsx +163 -0
  115. package/packages/shader-lab-react/src/timeline.ts +283 -0
  116. package/packages/shader-lab-react/src/types/editor.ts +5 -0
  117. package/packages/shader-lab-react/src/types.ts +141 -0
  118. package/packages/shader-lab-react/tsconfig.build.json +8 -0
  119. package/packages/shader-lab-react/tsconfig.json +21 -0
  120. package/postcss.config.mjs +5 -0
  121. package/public/assets/fonts/msdf/geist-mono/GeistMono-Regular-msdf-atlas.png +0 -0
  122. package/public/assets/fonts/msdf/geist-mono/GeistMono-Regular-msdf.json +1412 -0
  123. package/public/assets/patterns/bars/1.svg +3 -0
  124. package/public/assets/patterns/bars/2.svg +3 -0
  125. package/public/assets/patterns/bars/3.svg +3 -0
  126. package/public/assets/patterns/bars/4.svg +3 -0
  127. package/public/assets/patterns/bars/5.svg +3 -0
  128. package/public/assets/patterns/bars/6.svg +3 -0
  129. package/public/assets/patterns/candles/1.svg +3 -0
  130. package/public/assets/patterns/candles/2.svg +3 -0
  131. package/public/assets/patterns/candles/3.svg +3 -0
  132. package/public/assets/patterns/candles/4.svg +3 -0
  133. package/public/assets/patterns/shapes/1.svg +3 -0
  134. package/public/assets/patterns/shapes/2.svg +3 -0
  135. package/public/assets/patterns/shapes/3.svg +3 -0
  136. package/public/assets/patterns/shapes/4.svg +4 -0
  137. package/public/assets/patterns/shapes/5.svg +3 -0
  138. package/public/assets/patterns/shapes/6.svg +4 -0
  139. package/public/fonts/geist/Geist-Mono.woff2 +0 -0
  140. package/public/textures/blue-noise.png +0 -0
  141. package/public/textures/crt-mask.png +0 -0
  142. package/src/app/design/page.tsx +398 -0
  143. package/src/app/favicon.ico +0 -0
  144. package/src/app/globals.css +280 -0
  145. package/src/app/layout.tsx +89 -0
  146. package/src/app/page.tsx +20 -0
  147. package/src/app/robots.ts +13 -0
  148. package/src/app/sitemap.ts +13 -0
  149. package/src/components/editor/editor-canvas-viewport.tsx +116 -0
  150. package/src/components/editor/editor-export-dialog.tsx +1177 -0
  151. package/src/components/editor/editor-timeline-overlay.tsx +983 -0
  152. package/src/components/editor/editor-topbar.tsx +287 -0
  153. package/src/components/editor/layer-sidebar.tsx +738 -0
  154. package/src/components/editor/properties-sidebar-content.tsx +574 -0
  155. package/src/components/editor/properties-sidebar-fields.tsx +389 -0
  156. package/src/components/editor/properties-sidebar-utils.ts +178 -0
  157. package/src/components/editor/properties-sidebar.tsx +421 -0
  158. package/src/components/ui/button/index.tsx +57 -0
  159. package/src/components/ui/color-picker/index.tsx +358 -0
  160. package/src/components/ui/glass-panel/index.tsx +45 -0
  161. package/src/components/ui/icon-button/index.tsx +46 -0
  162. package/src/components/ui/select/index.tsx +136 -0
  163. package/src/components/ui/slider/index.tsx +192 -0
  164. package/src/components/ui/toggle/index.tsx +34 -0
  165. package/src/components/ui/typography/index.tsx +61 -0
  166. package/src/components/ui/xy-pad/index.tsx +160 -0
  167. package/src/features/editor/components/editor-export-dialog.module.css +271 -0
  168. package/src/hooks/use-editor-renderer.ts +182 -0
  169. package/src/lib/app.ts +6 -0
  170. package/src/lib/cn.ts +7 -0
  171. package/src/lib/easings.ts +240 -0
  172. package/src/lib/editor/config/layer-registry.ts +2434 -0
  173. package/src/lib/editor/custom-shader/shared.ts +28 -0
  174. package/src/lib/editor/export.ts +420 -0
  175. package/src/lib/editor/history.ts +71 -0
  176. package/src/lib/editor/layers.ts +76 -0
  177. package/src/lib/editor/parameter-schema.ts +75 -0
  178. package/src/lib/editor/project-file.ts +145 -0
  179. package/src/lib/editor/shader-export-snippet.ts +37 -0
  180. package/src/lib/editor/shader-export.ts +315 -0
  181. package/src/lib/editor/timeline/evaluate.ts +252 -0
  182. package/src/lib/editor/view-transform.ts +58 -0
  183. package/src/lib/fonts.ts +28 -0
  184. package/src/renderer/ascii-atlas.ts +83 -0
  185. package/src/renderer/ascii-pass.ts +416 -0
  186. package/src/renderer/blend-modes.ts +229 -0
  187. package/src/renderer/contracts.ts +161 -0
  188. package/src/renderer/create-webgpu-renderer.ts +48 -0
  189. package/src/renderer/crt-pass.ts +1040 -0
  190. package/src/renderer/custom-shader-pass.ts +117 -0
  191. package/src/renderer/custom-shader-runtime.ts +309 -0
  192. package/src/renderer/dither-textures.ts +99 -0
  193. package/src/renderer/dithering-pass.ts +322 -0
  194. package/src/renderer/gradient-pass.ts +520 -0
  195. package/src/renderer/halftone-pass.ts +932 -0
  196. package/src/renderer/ink-pass.ts +683 -0
  197. package/src/renderer/live-pass.ts +194 -0
  198. package/src/renderer/media-pass.ts +187 -0
  199. package/src/renderer/media-texture.ts +66 -0
  200. package/src/renderer/particle-grid-pass.ts +389 -0
  201. package/src/renderer/pass-node-factory.ts +33 -0
  202. package/src/renderer/pass-node.ts +209 -0
  203. package/src/renderer/pattern-atlas.ts +97 -0
  204. package/src/renderer/pattern-pass.ts +552 -0
  205. package/src/renderer/pipeline-manager.ts +343 -0
  206. package/src/renderer/pixel-sorting-pass.ts +277 -0
  207. package/src/renderer/project-clock.ts +57 -0
  208. package/src/renderer/shaders/tsl/color/tonemapping.ts +86 -0
  209. package/src/renderer/shaders/tsl/cosine-palette.ts +8 -0
  210. package/src/renderer/shaders/tsl/noise/common.ts +30 -0
  211. package/src/renderer/shaders/tsl/noise/curl-noise-3d.ts +35 -0
  212. package/src/renderer/shaders/tsl/noise/curl-noise-4d.ts +35 -0
  213. package/src/renderer/shaders/tsl/noise/fbm.ts +12 -0
  214. package/src/renderer/shaders/tsl/noise/perlin-noise-3d.ts +97 -0
  215. package/src/renderer/shaders/tsl/noise/ridge-noise.ts +23 -0
  216. package/src/renderer/shaders/tsl/noise/simplex-noise-3d.ts +78 -0
  217. package/src/renderer/shaders/tsl/noise/simplex-noise-4d.ts +88 -0
  218. package/src/renderer/shaders/tsl/noise/turbulence.ts +55 -0
  219. package/src/renderer/shaders/tsl/noise/value-noise-3d.ts +31 -0
  220. package/src/renderer/shaders/tsl/noise/voronoi-noise-3d.ts +59 -0
  221. package/src/renderer/shaders/tsl/patterns/bloom-edge-pattern.ts +14 -0
  222. package/src/renderer/shaders/tsl/patterns/bloom.ts +10 -0
  223. package/src/renderer/shaders/tsl/patterns/canvas-weave-pattern.ts +23 -0
  224. package/src/renderer/shaders/tsl/patterns/grain-texture-pattern.ts +8 -0
  225. package/src/renderer/shaders/tsl/patterns/repeating-pattern.ts +10 -0
  226. package/src/renderer/shaders/tsl/utils/atan2.ts +8 -0
  227. package/src/renderer/shaders/tsl/utils/complex-conj.ts +8 -0
  228. package/src/renderer/shaders/tsl/utils/complex-cos.ts +9 -0
  229. package/src/renderer/shaders/tsl/utils/complex-div.ts +10 -0
  230. package/src/renderer/shaders/tsl/utils/complex-log.ts +6 -0
  231. package/src/renderer/shaders/tsl/utils/complex-mobius.ts +11 -0
  232. package/src/renderer/shaders/tsl/utils/complex-mul.ts +8 -0
  233. package/src/renderer/shaders/tsl/utils/complex-pow.ts +15 -0
  234. package/src/renderer/shaders/tsl/utils/complex-sin.ts +9 -0
  235. package/src/renderer/shaders/tsl/utils/complex-sqrt.ts +17 -0
  236. package/src/renderer/shaders/tsl/utils/complex-tan.ts +11 -0
  237. package/src/renderer/shaders/tsl/utils/complex-to-polar.ts +9 -0
  238. package/src/renderer/shaders/tsl/utils/hyperbolic.ts +19 -0
  239. package/src/renderer/shaders/tsl/utils/index.ts +47 -0
  240. package/src/renderer/shaders/tsl/utils/rotate.ts +14 -0
  241. package/src/renderer/shaders/tsl/utils/screen-aspect-uv.ts +14 -0
  242. package/src/renderer/shaders/tsl/utils/sd-box-2d.ts +5 -0
  243. package/src/renderer/shaders/tsl/utils/sd-diamond.ts +5 -0
  244. package/src/renderer/shaders/tsl/utils/sd-rhombus.ts +26 -0
  245. package/src/renderer/shaders/tsl/utils/sd-sphere.ts +5 -0
  246. package/src/renderer/shaders/tsl/utils/smax.ts +7 -0
  247. package/src/renderer/shaders/tsl/utils/smin.ts +6 -0
  248. package/src/renderer/text-pass.ts +176 -0
  249. package/src/store/asset-store.ts +193 -0
  250. package/src/store/editor-store.ts +223 -0
  251. package/src/store/history-store.ts +172 -0
  252. package/src/store/index.ts +31 -0
  253. package/src/store/layer-store.ts +675 -0
  254. package/src/store/timeline-store.ts +572 -0
  255. package/src/types/assets.d.ts +6 -0
  256. package/src/types/css.d.ts +21 -0
  257. package/src/types/editor.ts +357 -0
  258. package/src/types/react.d.ts +15 -0
  259. package/src/types/three-tsl.d.ts +146 -0
  260. package/src/types/three-webgpu.d.ts +51 -0
  261. package/tsconfig.json +49 -0
@@ -0,0 +1,133 @@
1
+ import * as THREE from "three/webgpu"
2
+
3
+ export const PATTERN_PRESET_SOURCES = {
4
+ bars: [
5
+ new URL("../../../assets/patterns/bars/1.svg", import.meta.url).toString(),
6
+ new URL("../../../assets/patterns/bars/2.svg", import.meta.url).toString(),
7
+ new URL("../../../assets/patterns/bars/3.svg", import.meta.url).toString(),
8
+ new URL("../../../assets/patterns/bars/4.svg", import.meta.url).toString(),
9
+ new URL("../../../assets/patterns/bars/5.svg", import.meta.url).toString(),
10
+ new URL("../../../assets/patterns/bars/6.svg", import.meta.url).toString(),
11
+ ],
12
+ candles: [
13
+ new URL(
14
+ "../../../assets/patterns/candles/1.svg",
15
+ import.meta.url
16
+ ).toString(),
17
+ new URL(
18
+ "../../../assets/patterns/candles/2.svg",
19
+ import.meta.url
20
+ ).toString(),
21
+ new URL(
22
+ "../../../assets/patterns/candles/3.svg",
23
+ import.meta.url
24
+ ).toString(),
25
+ new URL(
26
+ "../../../assets/patterns/candles/4.svg",
27
+ import.meta.url
28
+ ).toString(),
29
+ ],
30
+ shapes: [
31
+ new URL(
32
+ "../../../assets/patterns/shapes/1.svg",
33
+ import.meta.url
34
+ ).toString(),
35
+ new URL(
36
+ "../../../assets/patterns/shapes/2.svg",
37
+ import.meta.url
38
+ ).toString(),
39
+ new URL(
40
+ "../../../assets/patterns/shapes/3.svg",
41
+ import.meta.url
42
+ ).toString(),
43
+ new URL(
44
+ "../../../assets/patterns/shapes/4.svg",
45
+ import.meta.url
46
+ ).toString(),
47
+ new URL(
48
+ "../../../assets/patterns/shapes/5.svg",
49
+ import.meta.url
50
+ ).toString(),
51
+ new URL(
52
+ "../../../assets/patterns/shapes/6.svg",
53
+ import.meta.url
54
+ ).toString(),
55
+ ],
56
+ } as const
57
+
58
+ export type PatternPreset = keyof typeof PATTERN_PRESET_SOURCES
59
+
60
+ function loadSvg(url: string): Promise<HTMLImageElement> {
61
+ return new Promise((resolve, reject) => {
62
+ const image = new Image()
63
+ image.decoding = "async"
64
+ image.onload = () => resolve(image)
65
+ image.onerror = () =>
66
+ reject(new Error(`Unable to load SVG pattern: ${url}`))
67
+ image.src = url
68
+ })
69
+ }
70
+
71
+ export async function buildPatternAtlas(
72
+ preset: PatternPreset,
73
+ cellPx = 16
74
+ ): Promise<THREE.CanvasTexture> {
75
+ const urls = PATTERN_PRESET_SOURCES[preset]
76
+ const cellSize = Math.max(4, Math.round(cellPx))
77
+ const images = await Promise.all(urls.map((url) => loadSvg(url)))
78
+ const canvas = document.createElement("canvas")
79
+ canvas.width = urls.length * cellSize
80
+ canvas.height = cellSize
81
+
82
+ const context = canvas.getContext("2d")
83
+
84
+ if (!context) {
85
+ throw new Error("Unable to create 2D context for pattern atlas")
86
+ }
87
+
88
+ context.clearRect(0, 0, canvas.width, canvas.height)
89
+ context.fillStyle = "#000"
90
+ context.fillRect(0, 0, canvas.width, canvas.height)
91
+ context.imageSmoothingEnabled = true
92
+
93
+ for (const [index, image] of images.entries()) {
94
+ const aspect = image.naturalWidth / Math.max(image.naturalHeight, 1)
95
+ const drawWidth = aspect >= 1 ? cellSize : cellSize * aspect
96
+ const drawHeight =
97
+ aspect >= 1 ? cellSize / Math.max(aspect, 0.0001) : cellSize
98
+ const x = index * cellSize + (cellSize - drawWidth) * 0.5
99
+ const y = (cellSize - drawHeight) * 0.5
100
+
101
+ context.drawImage(image, x, y, drawWidth, drawHeight)
102
+ }
103
+
104
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height)
105
+ const { data } = imageData
106
+
107
+ for (let index = 0; index < data.length; index += 4) {
108
+ const alpha = data[index + 3] ?? 0
109
+ const luminance = Math.max(
110
+ data[index] ?? 0,
111
+ data[index + 1] ?? 0,
112
+ data[index + 2] ?? 0
113
+ )
114
+ const mask = alpha > 0 && luminance > 32 ? 255 : 0
115
+ data[index] = mask
116
+ data[index + 1] = mask
117
+ data[index + 2] = mask
118
+ data[index + 3] = 255
119
+ }
120
+
121
+ context.putImageData(imageData, 0, 0)
122
+
123
+ const texture = new THREE.CanvasTexture(canvas)
124
+ texture.flipY = false
125
+ texture.generateMipmaps = false
126
+ texture.magFilter = THREE.NearestFilter
127
+ texture.minFilter = THREE.NearestFilter
128
+ texture.wrapS = THREE.ClampToEdgeWrapping
129
+ texture.wrapT = THREE.ClampToEdgeWrapping
130
+ texture.needsUpdate = true
131
+
132
+ return texture
133
+ }
@@ -0,0 +1,552 @@
1
+ import * as THREE from "three/webgpu"
2
+ import { bloom } from "three/examples/jsm/tsl/display/BloomNode.js"
3
+ import {
4
+ clamp,
5
+ float,
6
+ floor,
7
+ mix,
8
+ mod,
9
+ select,
10
+ texture as tslTexture,
11
+ type TSLNode,
12
+ uniform,
13
+ uv,
14
+ vec2,
15
+ vec3,
16
+ vec4,
17
+ } from "three/tsl"
18
+ import {
19
+ buildPatternAtlas,
20
+ type PatternPreset,
21
+ } from "./pattern-atlas"
22
+ import { PassNode } from "./pass-node"
23
+ import type { LayerParameterValues } from "../types/editor"
24
+
25
+ type Node = TSLNode
26
+ type PatternColorMode = "custom" | "monochrome" | "quantized" | "source"
27
+
28
+ function clamp01(value: number): number {
29
+ return Math.max(0, Math.min(1, value))
30
+ }
31
+
32
+ function parseCssColorRgb(value: string): [number, number, number] {
33
+ const rgba = value.match(
34
+ /rgba?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*[\d.]+)?\s*\)/i,
35
+ )
36
+
37
+ if (rgba) {
38
+ return [
39
+ clamp01(Number.parseFloat(rgba[1] ?? "0") / 255),
40
+ clamp01(Number.parseFloat(rgba[2] ?? "0") / 255),
41
+ clamp01(Number.parseFloat(rgba[3] ?? "0") / 255),
42
+ ]
43
+ }
44
+
45
+ const hex = value.trim().replace("#", "")
46
+
47
+ if (hex.length === 6) {
48
+ return [
49
+ Number.parseInt(hex.slice(0, 2), 16) / 255,
50
+ Number.parseInt(hex.slice(2, 4), 16) / 255,
51
+ Number.parseInt(hex.slice(4, 6), 16) / 255,
52
+ ]
53
+ }
54
+
55
+ if (hex.length === 3) {
56
+ return [
57
+ Number.parseInt(`${hex[0] ?? "0"}${hex[0] ?? "0"}`, 16) / 255,
58
+ Number.parseInt(`${hex[1] ?? "0"}${hex[1] ?? "0"}`, 16) / 255,
59
+ Number.parseInt(`${hex[2] ?? "0"}${hex[2] ?? "0"}`, 16) / 255,
60
+ ]
61
+ }
62
+
63
+ return [1, 1, 1]
64
+ }
65
+
66
+ export class PatternPass extends PassNode {
67
+ private atlasTexture: THREE.CanvasTexture | null = null
68
+ private atlasTextureNodes: Node[] = []
69
+ private bloomEnabled = false
70
+ private bloomNode: ReturnType<typeof bloom> | null = null
71
+ private readonly bloomIntensityUniform: Node
72
+ private readonly bloomRadiusUniform: Node
73
+ private readonly bloomSoftnessUniform: Node
74
+ private readonly bloomThresholdUniform: Node
75
+ private sourceTextureNodes: Node[] = []
76
+
77
+ private readonly bgOpacityUniform: Node
78
+ private readonly cellSizeUniform: Node
79
+ private readonly colorModeUniform: Node
80
+ private readonly customBgColorUniform: Node
81
+ private readonly customColor1Uniform: Node
82
+ private readonly customColor2Uniform: Node
83
+ private readonly customColor3Uniform: Node
84
+ private readonly customColor4Uniform: Node
85
+ private readonly customColorCountUniform: Node
86
+ private readonly customLuminanceBiasUniform: Node
87
+ private readonly invertUniform: Node
88
+ private readonly logicalHeightUniform: Node
89
+ private readonly logicalWidthUniform: Node
90
+ private readonly monoBlueUniform: Node
91
+ private readonly monoGreenUniform: Node
92
+ private readonly monoRedUniform: Node
93
+ private readonly numPatternsUniform: Node
94
+ private readonly placeholder: THREE.Texture
95
+
96
+ private atlasBuildRequestId = 0
97
+ private atlasPending = false
98
+ private currentCellSize = 12
99
+ private currentPreset: PatternPreset = "bars"
100
+ private needsRefresh = false
101
+
102
+ constructor(layerId: string) {
103
+ super(layerId)
104
+ this.placeholder = new THREE.Texture()
105
+ this.bloomIntensityUniform = uniform(1.25)
106
+ this.bloomRadiusUniform = uniform(6)
107
+ this.bloomSoftnessUniform = uniform(0.35)
108
+ this.bloomThresholdUniform = uniform(0.6)
109
+ this.bgOpacityUniform = uniform(0)
110
+ this.cellSizeUniform = uniform(12)
111
+ this.colorModeUniform = uniform(0)
112
+ this.customColorCountUniform = uniform(4)
113
+ this.customLuminanceBiasUniform = uniform(0)
114
+ this.customBgColorUniform = uniform(new THREE.Vector3(0.96, 0.96, 0.94))
115
+ this.customColor1Uniform = uniform(new THREE.Vector3(0.05, 0.063, 0.078))
116
+ this.customColor2Uniform = uniform(new THREE.Vector3(0.302, 0.314, 0.341))
117
+ this.customColor3Uniform = uniform(new THREE.Vector3(0.588, 0.604, 0.635))
118
+ this.customColor4Uniform = uniform(new THREE.Vector3(0.882, 0.886, 0.871))
119
+ this.invertUniform = uniform(0)
120
+ this.logicalWidthUniform = uniform(1)
121
+ this.logicalHeightUniform = uniform(1)
122
+ this.monoRedUniform = uniform(0.96)
123
+ this.monoGreenUniform = uniform(0.96)
124
+ this.monoBlueUniform = uniform(0.94)
125
+ this.numPatternsUniform = uniform(1)
126
+ this.rebuildAtlas()
127
+ this.rebuildEffectNode()
128
+ }
129
+
130
+ override render(
131
+ renderer: THREE.WebGPURenderer,
132
+ inputTexture: THREE.Texture,
133
+ outputTarget: THREE.WebGLRenderTarget,
134
+ time: number,
135
+ delta: number,
136
+ ): void {
137
+ for (const sourceTextureNode of this.sourceTextureNodes) {
138
+ sourceTextureNode.value = inputTexture
139
+ }
140
+
141
+ if (this.atlasTexture) {
142
+ for (const atlasTextureNode of this.atlasTextureNodes) {
143
+ atlasTextureNode.value = this.atlasTexture
144
+ }
145
+ }
146
+
147
+ super.render(renderer, inputTexture, outputTarget, time, delta)
148
+ this.needsRefresh = false
149
+ }
150
+
151
+ override updateParams(params: LayerParameterValues): void {
152
+ const nextCellSize =
153
+ typeof params.cellSize === "number" ? Math.max(4, Math.round(params.cellSize)) : 12
154
+ const nextPreset = this.resolvePreset(params.preset)
155
+ const nextColorMode = this.resolveColorMode(params.colorMode)
156
+ const nextBgOpacity =
157
+ typeof params.bgOpacity === "number" ? clamp01(params.bgOpacity) : 0
158
+ const nextCustomColorCount =
159
+ typeof params.customColorCount === "number"
160
+ ? Math.min(4, Math.max(2, Math.round(params.customColorCount)))
161
+ : 4
162
+ const nextCustomLuminanceBias =
163
+ typeof params.customLuminanceBias === "number"
164
+ ? Math.min(1, Math.max(-1, params.customLuminanceBias))
165
+ : 0
166
+ const nextBloomEnabled = params.bloomEnabled === true
167
+ const nextBloomIntensity =
168
+ typeof params.bloomIntensity === "number" ? Math.max(0, params.bloomIntensity) : 1.25
169
+ const nextBloomThreshold =
170
+ typeof params.bloomThreshold === "number" ? clamp01(params.bloomThreshold) : 0.6
171
+ const nextBloomRadius =
172
+ typeof params.bloomRadius === "number" ? Math.max(0, params.bloomRadius) : 6
173
+ const nextBloomSoftness =
174
+ typeof params.bloomSoftness === "number" ? clamp01(params.bloomSoftness) : 0.35
175
+ const [red, green, blue] = parseCssColorRgb(
176
+ typeof params.monoColor === "string" ? params.monoColor : "#f5f5f0",
177
+ )
178
+
179
+ this.bgOpacityUniform.value = nextBgOpacity
180
+ this.bloomIntensityUniform.value = nextBloomIntensity
181
+ this.bloomRadiusUniform.value = nextBloomRadius
182
+ this.bloomSoftnessUniform.value = nextBloomSoftness
183
+ this.bloomThresholdUniform.value = nextBloomThreshold
184
+ this.cellSizeUniform.value = nextCellSize
185
+ this.colorModeUniform.value = this.getColorModeValue(nextColorMode)
186
+ this.customColorCountUniform.value = nextCustomColorCount
187
+ this.customLuminanceBiasUniform.value = nextCustomLuminanceBias
188
+ this.invertUniform.value = params.invert === true ? 1 : 0
189
+ this.monoRedUniform.value = red
190
+ this.monoGreenUniform.value = green
191
+ this.monoBlueUniform.value = blue
192
+ this.setCustomColorUniform(
193
+ this.customBgColorUniform,
194
+ typeof params.customBgColor === "string" ? params.customBgColor : "#F5F5F0",
195
+ )
196
+ this.setCustomColorUniform(
197
+ this.customColor1Uniform,
198
+ typeof params.customColor1 === "string" ? params.customColor1 : "#0d1014",
199
+ )
200
+ this.setCustomColorUniform(
201
+ this.customColor2Uniform,
202
+ typeof params.customColor2 === "string" ? params.customColor2 : "#4d5057",
203
+ )
204
+ this.setCustomColorUniform(
205
+ this.customColor3Uniform,
206
+ typeof params.customColor3 === "string" ? params.customColor3 : "#969aa2",
207
+ )
208
+ this.setCustomColorUniform(
209
+ this.customColor4Uniform,
210
+ typeof params.customColor4 === "string" ? params.customColor4 : "#e1e2de",
211
+ )
212
+
213
+ const needsAtlasRebuild =
214
+ nextCellSize !== this.currentCellSize || nextPreset !== this.currentPreset
215
+
216
+ this.currentCellSize = nextCellSize
217
+ this.currentPreset = nextPreset
218
+
219
+ if (nextBloomEnabled !== this.bloomEnabled) {
220
+ this.bloomEnabled = nextBloomEnabled
221
+ this.rebuildEffectNode()
222
+ return
223
+ }
224
+
225
+ if (this.bloomNode) {
226
+ this.bloomNode.strength.value = nextBloomIntensity
227
+ this.bloomNode.radius.value = this.normalizeBloomRadius(nextBloomRadius)
228
+ this.bloomNode.threshold.value = nextBloomThreshold
229
+ this.bloomNode.smoothWidth.value = this.normalizeBloomSoftness(nextBloomSoftness)
230
+ }
231
+
232
+ if (needsAtlasRebuild) {
233
+ this.rebuildAtlas()
234
+ }
235
+ }
236
+
237
+ override updateLogicalSize(width: number, height: number): void {
238
+ this.logicalWidthUniform.value = Math.max(1, width)
239
+ this.logicalHeightUniform.value = Math.max(1, height)
240
+ }
241
+
242
+ override needsContinuousRender(): boolean {
243
+ return this.atlasPending || this.needsRefresh
244
+ }
245
+
246
+ override dispose(): void {
247
+ this.disposeBloomNode()
248
+ this.placeholder.dispose()
249
+ this.atlasTexture?.dispose()
250
+ super.dispose()
251
+ }
252
+
253
+ protected override buildEffectNode(): Node {
254
+ if (!(this.cellSizeUniform && this.numPatternsUniform && this.placeholder)) {
255
+ return this.inputNode
256
+ }
257
+
258
+ this.disposeBloomNode()
259
+ this.bloomNode = null
260
+ this.sourceTextureNodes = []
261
+ this.atlasTextureNodes = []
262
+
263
+ const renderTargetUv = vec2(uv().x, float(1).sub(uv().y))
264
+ const logicalScreenSize = vec2(this.logicalWidthUniform, this.logicalHeightUniform)
265
+ const normalizedCellSize = vec2(this.cellSizeUniform, this.cellSizeUniform).div(
266
+ logicalScreenSize,
267
+ )
268
+ const quantizeLevels = vec3(float(7), float(7), float(3))
269
+ const inverseQuantizeLevels = vec3(float(1 / 7), float(1 / 7), float(1 / 3))
270
+ const customBgVec = vec3(
271
+ float(this.customBgColorUniform.x),
272
+ float(this.customBgColorUniform.y),
273
+ float(this.customBgColorUniform.z),
274
+ )
275
+ const customColor1Vec = vec3(
276
+ float(this.customColor1Uniform.x),
277
+ float(this.customColor1Uniform.y),
278
+ float(this.customColor1Uniform.z),
279
+ )
280
+ const customColor2Vec = vec3(
281
+ float(this.customColor2Uniform.x),
282
+ float(this.customColor2Uniform.y),
283
+ float(this.customColor2Uniform.z),
284
+ )
285
+ const customColor3Vec = vec3(
286
+ float(this.customColor3Uniform.x),
287
+ float(this.customColor3Uniform.y),
288
+ float(this.customColor3Uniform.z),
289
+ )
290
+ const customColor4Vec = vec3(
291
+ float(this.customColor4Uniform.x),
292
+ float(this.customColor4Uniform.y),
293
+ float(this.customColor4Uniform.z),
294
+ )
295
+ const customColorCount = clamp(
296
+ float(this.customColorCountUniform),
297
+ float(2),
298
+ float(4),
299
+ )
300
+
301
+ const samplePattern = (sampleUv: Node) => {
302
+ const safeUv = clamp(sampleUv, vec2(float(0), float(0)), vec2(float(1), float(1)))
303
+ const screenPixel = floor(safeUv.mul(logicalScreenSize))
304
+ const tileCenterUv = floor(safeUv.div(normalizedCellSize))
305
+ .add(vec2(0.5, 0.5))
306
+ .mul(normalizedCellSize)
307
+ const localCellPixel = vec2(
308
+ mod(screenPixel.x, this.cellSizeUniform),
309
+ mod(screenPixel.y, this.cellSizeUniform),
310
+ )
311
+ const sampledColor = this.trackSourceTextureNode(tileCenterUv)
312
+ const sourceColor = vec3(
313
+ float(sampledColor.r),
314
+ float(sampledColor.g),
315
+ float(sampledColor.b),
316
+ )
317
+ const luma = float(sampledColor.r)
318
+ .mul(float(0.299))
319
+ .add(float(sampledColor.g).mul(float(0.45)))
320
+ .add(float(sampledColor.b).mul(float(0.114)))
321
+ const adjustedLuma = select(
322
+ this.invertUniform.greaterThan(float(0.5)),
323
+ float(1).sub(luma),
324
+ luma,
325
+ )
326
+ const patternIndex = floor(
327
+ clamp(
328
+ adjustedLuma.mul(this.numPatternsUniform.sub(float(1))),
329
+ float(0),
330
+ this.numPatternsUniform.sub(float(1)),
331
+ ),
332
+ )
333
+ const atlasUv = vec2(
334
+ patternIndex
335
+ .mul(this.cellSizeUniform)
336
+ .add(localCellPixel.x)
337
+ .add(float(0.5))
338
+ .div(this.numPatternsUniform.mul(this.cellSizeUniform)),
339
+ localCellPixel.y.add(float(0.5)).div(this.cellSizeUniform),
340
+ )
341
+ const patternMask = float(this.trackAtlasTextureNode(atlasUv).r)
342
+ const quantized = floor(sourceColor.mul(quantizeLevels).add(vec3(0.5, 0.5, 0.5))).mul(
343
+ inverseQuantizeLevels,
344
+ )
345
+ const quantizedLuma = float(quantized.x)
346
+ .mul(float(0.299))
347
+ .add(float(quantized.y).mul(float(0.45)))
348
+ .add(float(quantized.z).mul(float(0.114)))
349
+ const quantizedColor = clamp(
350
+ mix(vec3(quantizedLuma, quantizedLuma, quantizedLuma), quantized, float(1.2)),
351
+ vec3(float(0), float(0), float(0)),
352
+ vec3(float(1), float(1), float(1)),
353
+ )
354
+ const monochromeColor = vec3(
355
+ this.monoRedUniform,
356
+ this.monoGreenUniform,
357
+ this.monoBlueUniform,
358
+ ).mul(adjustedLuma)
359
+ const customLuminance = clamp(
360
+ adjustedLuma.add(float(this.customLuminanceBiasUniform).mul(float(0.35))),
361
+ float(0),
362
+ float(1),
363
+ )
364
+ const customColor = select(
365
+ customColorCount.lessThan(float(2.5)),
366
+ select(
367
+ customLuminance.lessThan(float(0.5)),
368
+ customColor1Vec,
369
+ customColor2Vec,
370
+ ),
371
+ select(
372
+ customColorCount.lessThan(float(3.5)),
373
+ select(
374
+ customLuminance.lessThan(float(1 / 3)),
375
+ customColor1Vec,
376
+ select(
377
+ customLuminance.lessThan(float(2 / 3)),
378
+ customColor2Vec,
379
+ customColor3Vec,
380
+ ),
381
+ ),
382
+ select(
383
+ customLuminance.lessThan(float(0.25)),
384
+ customColor1Vec,
385
+ select(
386
+ customLuminance.lessThan(float(0.5)),
387
+ customColor2Vec,
388
+ select(
389
+ customLuminance.lessThan(float(0.75)),
390
+ customColor3Vec,
391
+ customColor4Vec,
392
+ ),
393
+ ),
394
+ ),
395
+ ),
396
+ )
397
+ const patternColor = select(
398
+ this.colorModeUniform.lessThan(float(0.5)),
399
+ sourceColor,
400
+ select(
401
+ this.colorModeUniform.lessThan(float(1.5)),
402
+ quantizedColor,
403
+ select(
404
+ this.colorModeUniform.lessThan(float(2.5)),
405
+ monochromeColor,
406
+ customColor,
407
+ ),
408
+ ),
409
+ )
410
+ const sourceBackground = sourceColor.mul(this.bgOpacityUniform)
411
+ const backgroundColor = select(
412
+ this.colorModeUniform.lessThan(float(0.5)),
413
+ sourceBackground,
414
+ select(
415
+ this.colorModeUniform.lessThan(float(2.5)),
416
+ vec3(float(0), float(0), float(0)),
417
+ customBgVec,
418
+ ),
419
+ )
420
+
421
+ return vec4(mix(backgroundColor, patternColor, patternMask), float(1))
422
+ }
423
+
424
+ const baseSample = samplePattern(renderTargetUv)
425
+
426
+ if (!this.bloomEnabled) {
427
+ return baseSample
428
+ }
429
+
430
+ this.bloomNode = bloom(
431
+ vec4(baseSample.rgb, float(1)),
432
+ this.bloomIntensityUniform.value as number,
433
+ this.normalizeBloomRadius(this.bloomRadiusUniform.value as number),
434
+ this.bloomThresholdUniform.value as number,
435
+ )
436
+ this.bloomNode.smoothWidth.value = this.normalizeBloomSoftness(
437
+ this.bloomSoftnessUniform.value as number,
438
+ )
439
+
440
+ return vec4(
441
+ clamp(
442
+ baseSample.rgb.add(this.getBloomTextureNode().rgb),
443
+ vec3(float(0), float(0), float(0)),
444
+ vec3(float(1), float(1), float(1)),
445
+ ),
446
+ float(1),
447
+ )
448
+ }
449
+
450
+ private getColorModeValue(colorMode: PatternColorMode): number {
451
+ switch (colorMode) {
452
+ case "quantized":
453
+ return 1
454
+ case "monochrome":
455
+ return 2
456
+ case "custom":
457
+ return 3
458
+ default:
459
+ return 0
460
+ }
461
+ }
462
+
463
+ private rebuildAtlas(): void {
464
+ const requestId = ++this.atlasBuildRequestId
465
+ this.atlasPending = true
466
+
467
+ void buildPatternAtlas(this.currentPreset, this.currentCellSize)
468
+ .then((atlasTexture) => {
469
+ if (requestId !== this.atlasBuildRequestId) {
470
+ atlasTexture.dispose()
471
+ return
472
+ }
473
+
474
+ this.atlasTexture?.dispose()
475
+ this.atlasTexture = atlasTexture
476
+ this.numPatternsUniform.value = atlasTexture.image.width / this.currentCellSize
477
+ this.atlasPending = false
478
+ this.needsRefresh = true
479
+ this.rebuildEffectNode()
480
+ })
481
+ .catch(() => {
482
+ if (requestId !== this.atlasBuildRequestId) {
483
+ return
484
+ }
485
+
486
+ this.atlasPending = false
487
+ this.needsRefresh = true
488
+ })
489
+ }
490
+
491
+ private resolveColorMode(value: unknown): PatternColorMode {
492
+ return value === "quantized" || value === "monochrome" || value === "custom"
493
+ ? value
494
+ : "source"
495
+ }
496
+
497
+ private resolvePreset(value: unknown): PatternPreset {
498
+ return value === "candles" || value === "shapes" ? value : "bars"
499
+ }
500
+
501
+ private normalizeBloomRadius(value: number): number {
502
+ return clamp01(value / 24)
503
+ }
504
+
505
+ private normalizeBloomSoftness(value: number): number {
506
+ return Math.max(0.001, value * 0.25)
507
+ }
508
+
509
+ private disposeBloomNode(): void {
510
+ ;(this.bloomNode as { dispose?: () => void } | null)?.dispose?.()
511
+ }
512
+
513
+ private getBloomTextureNode(): Node {
514
+ const bloomNode = this.bloomNode as
515
+ | ({
516
+ getTexture?: () => Node
517
+ getTextureNode?: () => Node
518
+ } & object)
519
+ | null
520
+
521
+ if (!bloomNode) {
522
+ throw new Error("Bloom node is not initialized")
523
+ }
524
+
525
+ if ("getTextureNode" in bloomNode && typeof bloomNode.getTextureNode === "function") {
526
+ return bloomNode.getTextureNode()
527
+ }
528
+
529
+ if ("getTexture" in bloomNode && typeof bloomNode.getTexture === "function") {
530
+ return bloomNode.getTexture()
531
+ }
532
+
533
+ throw new Error("Bloom node does not expose a texture getter")
534
+ }
535
+
536
+ private trackAtlasTextureNode(uvNode: Node): Node {
537
+ const atlasTextureNode = tslTexture(this.atlasTexture ?? new THREE.Texture(), uvNode)
538
+ this.atlasTextureNodes.push(atlasTextureNode)
539
+ return atlasTextureNode
540
+ }
541
+
542
+ private trackSourceTextureNode(uvNode: Node): Node {
543
+ const sourceTextureNode = tslTexture(this.placeholder, uvNode)
544
+ this.sourceTextureNodes.push(sourceTextureNode)
545
+ return sourceTextureNode
546
+ }
547
+
548
+ private setCustomColorUniform(target: Node, value: string): void {
549
+ const [r, g, b] = parseCssColorRgb(value)
550
+ ;(target.value as THREE.Vector3).set(r, g, b)
551
+ }
552
+ }