@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,738 @@
1
+ "use client"
2
+ import {
3
+ CameraIcon,
4
+ DotsSixVerticalIcon,
5
+ DotsThreeVerticalIcon,
6
+ EyeIcon,
7
+ EyeSlashIcon,
8
+ FolderIcon,
9
+ ImageSquareIcon,
10
+ PlusIcon,
11
+ SidebarSimpleIcon,
12
+ SparkleIcon,
13
+ TextTIcon,
14
+ } from "@phosphor-icons/react"
15
+ import { Reorder, useDragControls } from "motion/react"
16
+ import {
17
+ type ChangeEvent,
18
+ type ReactNode,
19
+ type PointerEvent as ReactPointerEvent,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ } from "react"
24
+ import { GlassPanel } from "@/components/ui/glass-panel"
25
+ import { IconButton } from "@/components/ui/icon-button"
26
+ import { Select } from "@/components/ui/select"
27
+ import { Typography } from "@/components/ui/typography"
28
+ import { cn } from "@/lib/cn"
29
+ import { useAssetStore } from "@/store/asset-store"
30
+ import { useEditorStore } from "@/store/editor-store"
31
+ import { useLayerStore } from "@/store/layer-store"
32
+ import type { AssetKind, EditorAsset, EditorLayer } from "@/types/editor"
33
+
34
+ type AddLayerAction =
35
+ | "ascii"
36
+ | "crt"
37
+ | "custom-shader"
38
+ | "dithering"
39
+ | "gradient"
40
+ | "halftone"
41
+ | "image"
42
+ | "ink"
43
+ | "live"
44
+ | "particle-grid"
45
+ | "pattern"
46
+ | "pixel-sorting"
47
+ | "text"
48
+ | "video"
49
+ type LayerAction = "delete" | "reset"
50
+
51
+ const menuButtonClassName = "inline-flex items-center gap-[var(--ds-space-2)]"
52
+ const thumbnailBaseClassName =
53
+ "relative h-7 w-7 overflow-hidden rounded-[var(--ds-radius-thumb)] border border-white/6 bg-[linear-gradient(135deg,rgb(255_255_255_/_0.07),rgb(255_255_255_/_0.03))]"
54
+
55
+ const addLayerOptions = [
56
+ {
57
+ label: (
58
+ <span className={menuButtonClassName}>
59
+ <ImageSquareIcon size={14} weight="regular" />
60
+ Image
61
+ </span>
62
+ ),
63
+ value: "image",
64
+ },
65
+ {
66
+ label: (
67
+ <span className={menuButtonClassName}>
68
+ <ImageSquareIcon size={14} weight="regular" />
69
+ Video
70
+ </span>
71
+ ),
72
+ value: "video",
73
+ },
74
+ {
75
+ label: (
76
+ <span className={menuButtonClassName}>
77
+ <CameraIcon size={14} weight="regular" />
78
+ Camera
79
+ </span>
80
+ ),
81
+ value: "live",
82
+ },
83
+ {
84
+ label: (
85
+ <span className={menuButtonClassName}>
86
+ <TextTIcon size={14} weight="regular" />
87
+ Text
88
+ </span>
89
+ ),
90
+ value: "text",
91
+ },
92
+ {
93
+ label: (
94
+ <span className={menuButtonClassName}>
95
+ <SparkleIcon size={14} weight="regular" />
96
+ Mesh Gradient
97
+ </span>
98
+ ),
99
+ value: "gradient",
100
+ },
101
+ {
102
+ label: (
103
+ <span className={menuButtonClassName}>
104
+ <SparkleIcon size={14} weight="regular" />
105
+ Ink
106
+ </span>
107
+ ),
108
+ value: "ink",
109
+ },
110
+ {
111
+ label: (
112
+ <span className={menuButtonClassName}>
113
+ <SparkleIcon size={14} weight="regular" />
114
+ Custom Shader
115
+ </span>
116
+ ),
117
+ value: "custom-shader" as const,
118
+ },
119
+ {
120
+ label: (
121
+ <span className={menuButtonClassName}>
122
+ <SparkleIcon size={14} weight="regular" />
123
+ ASCII
124
+ </span>
125
+ ),
126
+ value: "ascii",
127
+ },
128
+ {
129
+ label: (
130
+ <span className={menuButtonClassName}>
131
+ <SparkleIcon size={14} weight="regular" />
132
+ Pattern
133
+ </span>
134
+ ),
135
+ value: "pattern",
136
+ },
137
+ {
138
+ label: (
139
+ <span className={menuButtonClassName}>
140
+ <SparkleIcon size={14} weight="regular" />
141
+ CRT
142
+ </span>
143
+ ),
144
+ value: "crt",
145
+ },
146
+ {
147
+ label: (
148
+ <span className={menuButtonClassName}>
149
+ <SparkleIcon size={14} weight="regular" />
150
+ Dithering
151
+ </span>
152
+ ),
153
+ value: "dithering",
154
+ },
155
+ {
156
+ label: (
157
+ <span className={menuButtonClassName}>
158
+ <SparkleIcon size={14} weight="regular" />
159
+ Halftone
160
+ </span>
161
+ ),
162
+ value: "halftone",
163
+ },
164
+ {
165
+ label: (
166
+ <span className={menuButtonClassName}>
167
+ <SparkleIcon size={14} weight="regular" />
168
+ Particle Grid
169
+ </span>
170
+ ),
171
+ value: "particle-grid",
172
+ },
173
+ {
174
+ label: (
175
+ <span className={menuButtonClassName}>
176
+ <SparkleIcon size={14} weight="regular" />
177
+ Pixel Sorting
178
+ </span>
179
+ ),
180
+ value: "pixel-sorting",
181
+ },
182
+ ] as const satisfies readonly { label: ReactNode; value: AddLayerAction }[]
183
+
184
+ function getLayerSecondaryText(
185
+ layer: EditorLayer,
186
+ asset: EditorAsset | null
187
+ ): string {
188
+ if (layer.runtimeError) {
189
+ return layer.runtimeError
190
+ }
191
+
192
+ if (
193
+ layer.type === "image" ||
194
+ layer.type === "video" ||
195
+ layer.type === "model"
196
+ ) {
197
+ return asset?.fileName ?? "No asset selected"
198
+ }
199
+
200
+ if (layer.type === "live") {
201
+ return "webcam"
202
+ }
203
+
204
+ if (layer.type === "custom-shader") {
205
+ return (
206
+ (typeof layer.params.sourceFileName === "string" &&
207
+ layer.params.sourceFileName) ||
208
+ "custom shader"
209
+ )
210
+ }
211
+
212
+ if (layer.type === "text") {
213
+ return (
214
+ (typeof layer.params.text === "string" && layer.params.text.trim()) ||
215
+ "text"
216
+ )
217
+ }
218
+
219
+ return layer.type.replaceAll("-", " ")
220
+ }
221
+
222
+ function getThumbnailClassName(
223
+ layer: EditorLayer,
224
+ asset: EditorAsset | null
225
+ ): string {
226
+ if (asset?.kind === "image" || asset?.kind === "video") {
227
+ return cn(thumbnailBaseClassName, "bg-center bg-cover")
228
+ }
229
+
230
+ if (layer.type === "model") {
231
+ return cn(
232
+ thumbnailBaseClassName,
233
+ "bg-[radial-gradient(circle_at_30%_30%,rgb(255_255_255_/_0.18),transparent_45%),linear-gradient(135deg,rgb(255_255_255_/_0.08),rgb(255_255_255_/_0.02))]"
234
+ )
235
+ }
236
+
237
+ return cn(
238
+ thumbnailBaseClassName,
239
+ "bg-[linear-gradient(135deg,rgb(255_255_255_/_0.1),rgb(255_255_255_/_0.03)),linear-gradient(180deg,rgb(255_255_255_/_0.05),transparent)] after:absolute after:inset-0 after:bg-[linear-gradient(90deg,transparent,rgb(255_255_255_/_0.18),transparent)] after:opacity-[0.35] after:content-['']"
240
+ )
241
+ }
242
+
243
+ function getExpectedAssetKind(layer: EditorLayer): AssetKind | null {
244
+ if (
245
+ layer.type === "image" ||
246
+ layer.type === "video" ||
247
+ layer.type === "model"
248
+ ) {
249
+ return layer.type
250
+ }
251
+
252
+ return null
253
+ }
254
+
255
+ function getAcceptForAssetKind(kind: AssetKind): string {
256
+ switch (kind) {
257
+ case "image":
258
+ return "image/png,image/jpeg,image/webp,image/gif"
259
+ case "video":
260
+ return "video/mp4,video/webm"
261
+ case "model":
262
+ return ".glb,.gltf,.obj,model/gltf-binary,model/gltf+json,model/obj,application/octet-stream"
263
+ }
264
+ }
265
+
266
+ function inferSelectedFileKind(file: File): AssetKind | null {
267
+ const mimeType = file.type.toLowerCase()
268
+ const fileName = file.name.toLowerCase()
269
+
270
+ if (mimeType.startsWith("image/")) {
271
+ return "image"
272
+ }
273
+
274
+ if (mimeType.startsWith("video/")) {
275
+ return "video"
276
+ }
277
+
278
+ if (
279
+ fileName.endsWith(".glb") ||
280
+ fileName.endsWith(".gltf") ||
281
+ fileName.endsWith(".obj") ||
282
+ mimeType === "model/gltf-binary" ||
283
+ mimeType === "model/gltf+json" ||
284
+ mimeType === "model/obj"
285
+ ) {
286
+ return "model"
287
+ }
288
+
289
+ return null
290
+ }
291
+
292
+ type LayerListItemProps = {
293
+ asset: EditorAsset | null
294
+ hasMissingAsset: boolean
295
+ isSelected: boolean
296
+ layer: EditorLayer
297
+ layerActionKey: number
298
+ onLayerAction: (layerId: string, action: LayerAction) => void
299
+ onRelinkPick: (layer: EditorLayer) => void
300
+ onSelectLayer: (layerId: string) => void
301
+ onSetLayerVisibility: (layerId: string, visible: boolean) => void
302
+ }
303
+
304
+ function LayerListItem({
305
+ asset,
306
+ hasMissingAsset,
307
+ isSelected,
308
+ layer,
309
+ layerActionKey,
310
+ onLayerAction,
311
+ onRelinkPick,
312
+ onSelectLayer,
313
+ onSetLayerVisibility,
314
+ }: LayerListItemProps) {
315
+ const dragControls = useDragControls()
316
+ const layerActionOptions = [
317
+ { label: "Reset properties", value: "reset" },
318
+ { label: "Delete layer", value: "delete" },
319
+ ] as const satisfies readonly {
320
+ label: ReactNode
321
+ value: LayerAction
322
+ }[]
323
+
324
+ function handlePointerDown(event: ReactPointerEvent<HTMLButtonElement>) {
325
+ if (layer.locked) {
326
+ return
327
+ }
328
+
329
+ dragControls.start(event)
330
+ }
331
+
332
+ return (
333
+ <Reorder.Item
334
+ as="li"
335
+ className={cn(
336
+ "relative grid min-h-11 grid-cols-[minmax(0,1fr)_28px_28px] items-center gap-[var(--ds-space-2)] rounded-[var(--ds-radius-control)] border border-transparent px-2 py-[6px] transition-[background-color,border-color,box-shadow] duration-160 ease-[var(--ease-out-cubic)]",
337
+ !layer.locked &&
338
+ "cursor-pointer hover:border-[var(--ds-border-subtle)] hover:bg-[var(--ds-color-surface-subtle)]",
339
+ isSelected &&
340
+ "border-[var(--ds-border-active)] bg-[var(--ds-color-surface-active)]"
341
+ )}
342
+ drag={layer.locked ? false : "y"}
343
+ dragControls={dragControls}
344
+ dragListener={false}
345
+ layout="position"
346
+ style={{ zIndex: 0 }}
347
+ value={layer}
348
+ >
349
+ <div className="grid min-w-0 grid-cols-[14px_minmax(0,1fr)] items-center gap-[var(--ds-space-2)]">
350
+ <button
351
+ aria-label={`Reorder ${layer.name}`}
352
+ className={cn(
353
+ "inline-flex h-[14px] w-[14px] touch-none items-center justify-center bg-transparent p-0 text-[var(--ds-color-text-muted)]",
354
+ !layer.locked && "cursor-grab active:cursor-grabbing",
355
+ layer.locked && "text-[var(--ds-color-text-disabled)]"
356
+ )}
357
+ onPointerDown={handlePointerDown}
358
+ type="button"
359
+ >
360
+ <DotsSixVerticalIcon size={14} weight="bold" />
361
+ </button>
362
+
363
+ <button
364
+ className="grid min-w-0 grid-cols-[28px_minmax(0,1fr)] items-center gap-[var(--ds-space-2)] bg-transparent p-0 text-left text-inherit"
365
+ onClick={() => onSelectLayer(layer.id)}
366
+ type="button"
367
+ >
368
+ <div
369
+ className={getThumbnailClassName(layer, asset)}
370
+ style={
371
+ asset?.kind === "image" || asset?.kind === "video"
372
+ ? { backgroundImage: `url("${asset.url}")` }
373
+ : undefined
374
+ }
375
+ />
376
+
377
+ <div className="flex min-w-0 flex-col gap-0.5">
378
+ <Typography
379
+ className="overflow-hidden text-ellipsis whitespace-nowrap"
380
+ variant="label"
381
+ >
382
+ {layer.name}
383
+ </Typography>
384
+ <Typography
385
+ className="overflow-hidden text-ellipsis whitespace-nowrap"
386
+ tone="muted"
387
+ variant="monoXs"
388
+ >
389
+ {getLayerSecondaryText(layer, asset)}
390
+ </Typography>
391
+ </div>
392
+ </button>
393
+ </div>
394
+
395
+ <Select
396
+ key={`${layer.id}:${layerActionKey}`}
397
+ onValueChange={(value) => onLayerAction(layer.id, value as LayerAction)}
398
+ options={layerActionOptions}
399
+ placeholder={<DotsThreeVerticalIcon size={14} weight="bold" />}
400
+ popupClassName="min-w-[152px]"
401
+ triggerAriaLabel={`Layer actions for ${layer.name}`}
402
+ triggerVariant="icon"
403
+ valueClassName="inline-flex items-center justify-center leading-none text-[var(--ds-color-text-tertiary)] [&_svg]:h-[14px] [&_svg]:w-[14px]"
404
+ />
405
+
406
+ {hasMissingAsset ? (
407
+ <IconButton
408
+ aria-label={`Relink missing asset for ${layer.name}`}
409
+ onClick={(event) => {
410
+ event.stopPropagation()
411
+ onRelinkPick(layer)
412
+ }}
413
+ variant="ghost"
414
+ >
415
+ <FolderIcon size={14} weight="regular" />
416
+ </IconButton>
417
+ ) : (
418
+ <IconButton
419
+ aria-label={layer.visible ? "Hide layer" : "Show layer"}
420
+ onClick={(event) => {
421
+ event.stopPropagation()
422
+ onSetLayerVisibility(layer.id, !layer.visible)
423
+ }}
424
+ variant="ghost"
425
+ >
426
+ {layer.visible ? (
427
+ <EyeIcon size={14} weight="regular" />
428
+ ) : (
429
+ <EyeSlashIcon size={14} weight="regular" />
430
+ )}
431
+ </IconButton>
432
+ )}
433
+ </Reorder.Item>
434
+ )
435
+ }
436
+
437
+ export function LayerSidebar() {
438
+ const imageInputRef = useRef<HTMLInputElement | null>(null)
439
+ const relinkInputRef = useRef<HTMLInputElement | null>(null)
440
+ const relinkTargetRef = useRef<{
441
+ expectedKind: AssetKind
442
+ layerId: string
443
+ } | null>(null)
444
+ const videoInputRef = useRef<HTMLInputElement | null>(null)
445
+ const [addLayerSelectKey, setAddLayerSelectKey] = useState(0)
446
+ const [layerActionSelectKeys, setLayerActionSelectKeys] = useState<
447
+ Record<string, number>
448
+ >({})
449
+
450
+ const layers = useLayerStore((state) => state.layers)
451
+ const hoveredLayerId = useLayerStore((state) => state.hoveredLayerId)
452
+ const selectedLayerId = useLayerStore((state) => state.selectedLayerId)
453
+ const addLayer = useLayerStore((state) => state.addLayer)
454
+ const removeLayer = useLayerStore((state) => state.removeLayer)
455
+ const replaceState = useLayerStore((state) => state.replaceState)
456
+ const resetLayerParams = useLayerStore((state) => state.resetLayerParams)
457
+ const selectLayer = useLayerStore((state) => state.selectLayer)
458
+ const setLayerAsset = useLayerStore((state) => state.setLayerAsset)
459
+ const setLayerRuntimeError = useLayerStore(
460
+ (state) => state.setLayerRuntimeError
461
+ )
462
+ const setLayerVisibility = useLayerStore((state) => state.setLayerVisibility)
463
+ const assets = useAssetStore((state) => state.assets)
464
+ const loadAsset = useAssetStore((state) => state.loadAsset)
465
+ const removeAsset = useAssetStore((state) => state.removeAsset)
466
+ const leftSidebarVisible = useEditorStore((state) => state.sidebars.left)
467
+ const enterImmersiveCanvas = useEditorStore(
468
+ (state) => state.enterImmersiveCanvas
469
+ )
470
+
471
+ const assetsById = useMemo(
472
+ () => new Map(assets.map((asset) => [asset.id, asset])),
473
+ [assets]
474
+ )
475
+
476
+ async function handleMediaFile(file: File, layerType: "image" | "video") {
477
+ try {
478
+ const asset = await loadAsset(file)
479
+ const layerId = addLayer(layerType)
480
+ setLayerAsset(layerId, asset.id)
481
+ } catch {
482
+ // No-op.
483
+ }
484
+ }
485
+
486
+ function handleImagePick() {
487
+ imageInputRef.current?.click()
488
+ }
489
+
490
+ function handleVideoPick() {
491
+ videoInputRef.current?.click()
492
+ }
493
+
494
+ function handleAddDithering() {
495
+ addLayer("dithering")
496
+ }
497
+
498
+ function handleAddAscii() {
499
+ addLayer("ascii")
500
+ }
501
+
502
+ function handleAddGradient() {
503
+ addLayer("gradient")
504
+ }
505
+
506
+ function handleAddCustomShader() {
507
+ addLayer("custom-shader")
508
+ }
509
+
510
+ function handleAddLayer(action: AddLayerAction) {
511
+ if (action === "image") {
512
+ handleImagePick()
513
+ } else if (action === "video") {
514
+ handleVideoPick()
515
+ } else if (action === "live") {
516
+ addLayer("live")
517
+ } else if (action === "text") {
518
+ addLayer("text")
519
+ } else if (action === "gradient") {
520
+ handleAddGradient()
521
+ } else if (action === "ink") {
522
+ addLayer("ink")
523
+ } else if (action === "custom-shader") {
524
+ handleAddCustomShader()
525
+ } else if (action === "ascii") {
526
+ handleAddAscii()
527
+ } else if (action === "pattern") {
528
+ addLayer("pattern")
529
+ } else if (action === "crt") {
530
+ addLayer("crt")
531
+ } else if (action === "halftone") {
532
+ addLayer("halftone")
533
+ } else if (action === "particle-grid") {
534
+ addLayer("particle-grid")
535
+ } else if (action === "pixel-sorting") {
536
+ addLayer("pixel-sorting")
537
+ } else {
538
+ handleAddDithering()
539
+ }
540
+
541
+ setAddLayerSelectKey((current) => current + 1)
542
+ }
543
+
544
+ function handleLayerAction(layerId: string, action: LayerAction) {
545
+ if (action === "delete") {
546
+ removeLayer(layerId)
547
+ } else {
548
+ resetLayerParams(layerId)
549
+ }
550
+
551
+ setLayerActionSelectKeys((current) => ({
552
+ ...current,
553
+ [layerId]: (current[layerId] ?? 0) + 1,
554
+ }))
555
+ }
556
+
557
+ function handleImageChange(event: ChangeEvent<HTMLInputElement>) {
558
+ const file = event.target.files?.[0]
559
+
560
+ event.currentTarget.value = ""
561
+
562
+ if (!file) {
563
+ return
564
+ }
565
+
566
+ void handleMediaFile(file, "image")
567
+ }
568
+
569
+ function handleVideoChange(event: ChangeEvent<HTMLInputElement>) {
570
+ const file = event.target.files?.[0]
571
+
572
+ event.currentTarget.value = ""
573
+
574
+ if (!file) {
575
+ return
576
+ }
577
+
578
+ void handleMediaFile(file, "video")
579
+ }
580
+
581
+ async function handleRelinkChange(event: ChangeEvent<HTMLInputElement>) {
582
+ const file = event.target.files?.[0]
583
+ const target = relinkTargetRef.current
584
+
585
+ event.currentTarget.value = ""
586
+ relinkTargetRef.current = null
587
+
588
+ if (!(file && target)) {
589
+ return
590
+ }
591
+
592
+ if (inferSelectedFileKind(file) !== target.expectedKind) {
593
+ setLayerRuntimeError(
594
+ target.layerId,
595
+ `Expected a ${target.expectedKind} file.`
596
+ )
597
+ return
598
+ }
599
+
600
+ try {
601
+ const asset = await loadAsset(file)
602
+
603
+ if (asset.kind !== target.expectedKind) {
604
+ removeAsset(asset.id)
605
+ setLayerRuntimeError(
606
+ target.layerId,
607
+ `Expected a ${target.expectedKind} file.`
608
+ )
609
+ return
610
+ }
611
+
612
+ setLayerAsset(target.layerId, asset.id)
613
+ } catch (error) {
614
+ setLayerRuntimeError(
615
+ target.layerId,
616
+ error instanceof Error ? error.message : "Failed to relink asset."
617
+ )
618
+ }
619
+ }
620
+
621
+ function handleRelinkPick(layer: EditorLayer) {
622
+ const expectedKind = getExpectedAssetKind(layer)
623
+
624
+ if (!expectedKind) {
625
+ return
626
+ }
627
+
628
+ relinkTargetRef.current = {
629
+ expectedKind,
630
+ layerId: layer.id,
631
+ }
632
+
633
+ if (relinkInputRef.current) {
634
+ relinkInputRef.current.accept = getAcceptForAssetKind(expectedKind)
635
+ relinkInputRef.current.click()
636
+ }
637
+ }
638
+
639
+ function handleReorder(nextLayers: EditorLayer[]) {
640
+ replaceState(nextLayers, selectedLayerId, hoveredLayerId)
641
+ }
642
+
643
+ return (
644
+ <aside
645
+ className={cn(
646
+ "pointer-events-none absolute top-[76px] left-4 z-20 w-[284px] translate-x-0 transition-[opacity,translate] duration-[220ms,260ms] ease-[ease-out,cubic-bezier(0.22,1,0.36,1)]",
647
+ !leftSidebarVisible && "-translate-x-[18px] opacity-0"
648
+ )}
649
+ >
650
+ <input
651
+ accept="image/png,image/jpeg,image/webp,image/gif"
652
+ className="hidden"
653
+ onChange={handleImageChange}
654
+ ref={imageInputRef}
655
+ type="file"
656
+ />
657
+ <input
658
+ className="hidden"
659
+ onChange={handleRelinkChange}
660
+ ref={relinkInputRef}
661
+ type="file"
662
+ />
663
+ <input
664
+ accept="video/mp4,video/webm"
665
+ className="hidden"
666
+ onChange={handleVideoChange}
667
+ ref={videoInputRef}
668
+ type="file"
669
+ />
670
+
671
+ <GlassPanel
672
+ className={cn(
673
+ "pointer-events-auto relative flex flex-col gap-[var(--ds-space-1)] p-0",
674
+ !leftSidebarVisible && "pointer-events-none"
675
+ )}
676
+ variant="panel"
677
+ >
678
+ <div className="flex min-h-11 items-center justify-between border-[var(--ds-border-divider)] border-b pr-3 pl-[var(--ds-space-4)]">
679
+ <Typography className="uppercase" tone="secondary" variant="overline">
680
+ Layers
681
+ </Typography>
682
+ <div className="inline-flex items-center gap-1.5">
683
+ <IconButton
684
+ aria-label="Enter immersive canvas mode"
685
+ className="pointer-events-auto"
686
+ onClick={enterImmersiveCanvas}
687
+ variant="ghost"
688
+ >
689
+ <SidebarSimpleIcon size={14} weight="regular" />
690
+ </IconButton>
691
+ <Select
692
+ key={addLayerSelectKey}
693
+ className="pointer-events-auto"
694
+ onValueChange={(value) => handleAddLayer(value as AddLayerAction)}
695
+ options={addLayerOptions}
696
+ placeholder={<PlusIcon size={14} weight="bold" />}
697
+ popupClassName="min-w-[152px]"
698
+ triggerAriaLabel="Add layer"
699
+ triggerVariant="icon"
700
+ valueClassName="inline-flex items-center justify-center leading-none [&_svg]:h-[14px] [&_svg]:w-[14px]"
701
+ />
702
+ </div>
703
+ </div>
704
+
705
+ <Reorder.Group
706
+ axis="y"
707
+ as="ul"
708
+ className="flex max-h-[min(52vh,480px)] flex-col gap-0.5 overflow-y-auto p-1"
709
+ onReorder={handleReorder}
710
+ values={layers}
711
+ >
712
+ {layers.map((layer) => {
713
+ const asset = layer.assetId
714
+ ? (assetsById.get(layer.assetId) ?? null)
715
+ : null
716
+ const hasMissingAsset = Boolean(layer.assetId && !asset)
717
+ const isSelected = selectedLayerId === layer.id
718
+
719
+ return (
720
+ <LayerListItem
721
+ asset={asset}
722
+ hasMissingAsset={hasMissingAsset}
723
+ isSelected={isSelected}
724
+ key={layer.id}
725
+ layer={layer}
726
+ layerActionKey={layerActionSelectKeys[layer.id] ?? 0}
727
+ onLayerAction={handleLayerAction}
728
+ onRelinkPick={handleRelinkPick}
729
+ onSelectLayer={selectLayer}
730
+ onSetLayerVisibility={setLayerVisibility}
731
+ />
732
+ )
733
+ })}
734
+ </Reorder.Group>
735
+ </GlassPanel>
736
+ </aside>
737
+ )
738
+ }