@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,574 @@
1
+ "use client"
2
+
3
+ import { TextAlignRightIcon } from "@phosphor-icons/react"
4
+ import { AnimatePresence, motion } from "motion/react"
5
+ import { useCallback, useEffect, useMemo, useState } from "react"
6
+ import { CUSTOM_SHADER_ENTRY_EXPORT } from "@/lib/editor/custom-shader/shared"
7
+ import { formatCustomShaderSource } from "@/renderer/custom-shader-runtime"
8
+ import type {
9
+ AnimatedPropertyBinding,
10
+ BlendMode,
11
+ LayerCompositeMode,
12
+ LayerType,
13
+ ParameterDefinition,
14
+ ParameterValue,
15
+ } from "@/types/editor"
16
+ import { cn } from "@/lib/cn"
17
+ import { Button } from "@/components/ui/button"
18
+ import { IconButton } from "@/components/ui/icon-button"
19
+ import { Select } from "@/components/ui/select"
20
+ import { Slider } from "@/components/ui/slider"
21
+ import { Typography } from "@/components/ui/typography"
22
+ import { useTimelineStore } from "@/store/timeline-store"
23
+ import {
24
+ ParameterField,
25
+ renderFieldLabel,
26
+ type TimelineKeyframeControl,
27
+ } from "./properties-sidebar-fields"
28
+ import {
29
+ blendModeOptions,
30
+ compositeModeOptions,
31
+ createParamTimelineBinding,
32
+ DEFAULT_PARAM_GROUP,
33
+ formatLayerKind,
34
+ groupVisibleParams,
35
+ hasTrackForBinding,
36
+ } from "./properties-sidebar-utils"
37
+
38
+ export function EmptyPropertiesContent() {
39
+ return (
40
+ <div className="flex flex-col gap-1.5 p-4">
41
+ <Typography tone="secondary" variant="overline">
42
+ Properties
43
+ </Typography>
44
+ <Typography variant="body">Select a layer to edit it.</Typography>
45
+ <Typography tone="muted" variant="caption">
46
+ Nothing to edit yet. Create a new layer in the left panel.
47
+ </Typography>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ function CustomShaderSection({
53
+ layerId,
54
+ updateLayerParam,
55
+ values,
56
+ }: {
57
+ layerId: string
58
+ updateLayerParam: (id: string, key: string, value: ParameterValue) => void
59
+ values: Record<string, ParameterValue>
60
+ }) {
61
+ const persistedSource =
62
+ typeof values.sourceCode === "string" ? values.sourceCode : ""
63
+ const persistedEntryExport =
64
+ typeof values.entryExport === "string" && values.entryExport.trim()
65
+ ? values.entryExport
66
+ : CUSTOM_SHADER_ENTRY_EXPORT
67
+ const persistedRevision =
68
+ typeof values.sourceRevision === "number" ? values.sourceRevision : 0
69
+ const [draftSource, setDraftSource] = useState(persistedSource)
70
+ const [draftEntryExport, setDraftEntryExport] = useState(persistedEntryExport)
71
+ const [formatError, setFormatError] = useState<string | null>(null)
72
+
73
+ useEffect(() => {
74
+ setDraftSource(persistedSource)
75
+ }, [persistedSource])
76
+
77
+ useEffect(() => {
78
+ setDraftEntryExport(persistedEntryExport)
79
+ }, [persistedEntryExport])
80
+
81
+ const isDirty =
82
+ draftSource !== persistedSource || draftEntryExport !== persistedEntryExport
83
+
84
+ const commitShader = useCallback(
85
+ (next: { entryExport?: string; sourceCode?: string } = {}) => {
86
+ const nextEntryExport =
87
+ (next.entryExport ?? draftEntryExport).trim() ||
88
+ CUSTOM_SHADER_ENTRY_EXPORT
89
+ const nextSourceCode = next.sourceCode ?? draftSource
90
+
91
+ updateLayerParam(layerId, "sourceMode", "paste")
92
+ updateLayerParam(layerId, "entryExport", nextEntryExport)
93
+ updateLayerParam(layerId, "sourceFileName", "")
94
+ updateLayerParam(layerId, "sourceCode", nextSourceCode)
95
+ updateLayerParam(layerId, "sourceRevision", persistedRevision + 1)
96
+ },
97
+ [
98
+ draftEntryExport,
99
+ draftSource,
100
+ layerId,
101
+ persistedRevision,
102
+ updateLayerParam,
103
+ ]
104
+ )
105
+
106
+ return (
107
+ <section className="flex flex-col gap-3 border-t border-[var(--ds-border-divider)] px-4 pt-[14px] pb-4 first:border-t-0">
108
+ <Typography
109
+ className="uppercase"
110
+ tone="secondary"
111
+ variant="overline"
112
+ >
113
+ Shader
114
+ </Typography>
115
+
116
+ <div className="flex flex-col gap-[10px]">
117
+ <label className="flex flex-col gap-2">
118
+ <Typography className="min-w-0" tone="secondary" variant="label">
119
+ Entry Export
120
+ </Typography>
121
+ <input
122
+ className="min-h-9 appearance-none rounded-[var(--ds-radius-control)] border border-[var(--ds-border-divider)] bg-[var(--ds-color-surface-control)] px-[10px] py-2 font-[var(--ds-font-mono)] text-[12px] leading-4 text-[var(--ds-color-text-primary)] outline-none transition-[border-color,background-color] duration-120 ease-[ease] focus:border-[var(--ds-color-text-secondary)] placeholder:text-[var(--ds-color-text-muted)]"
123
+ onChange={(event) => {
124
+ setDraftEntryExport(event.currentTarget.value)
125
+ setFormatError(null)
126
+ }}
127
+ spellCheck={false}
128
+ type="text"
129
+ value={draftEntryExport}
130
+ />
131
+ </label>
132
+
133
+ <label className="flex flex-col gap-2">
134
+ <Typography className="min-w-0" tone="secondary" variant="label">
135
+ Sketch Source
136
+ </Typography>
137
+ <textarea
138
+ className="min-h-[280px] w-full resize-y appearance-none rounded-[var(--ds-radius-control)] border border-[var(--ds-border-divider)] bg-[var(--ds-color-surface-control)] px-3 py-[10px] font-[var(--ds-font-mono)] text-[12px] leading-[18px] text-[var(--ds-color-text-primary)] outline-none transition-[border-color,background-color] duration-120 ease-[ease] focus:border-[var(--ds-color-text-secondary)]"
139
+ onChange={(event) => {
140
+ setDraftSource(event.currentTarget.value)
141
+ setFormatError(null)
142
+ }}
143
+ spellCheck={false}
144
+ value={draftSource}
145
+ />
146
+ </label>
147
+
148
+ <div className="flex flex-wrap items-center justify-between gap-2">
149
+ <div className="inline-flex items-center gap-2">
150
+ <Button
151
+ disabled={!isDirty}
152
+ onClick={() => commitShader()}
153
+ size="compact"
154
+ variant="primary"
155
+ >
156
+ Apply
157
+ </Button>
158
+ </div>
159
+
160
+ <IconButton
161
+ aria-label="Format sketch source"
162
+ className="shrink-0"
163
+ onClick={() => {
164
+ void formatCustomShaderSource({
165
+ fileName: "custom-shader.ts",
166
+ sourceCode: draftSource,
167
+ })
168
+ .then((formatted) => {
169
+ setDraftSource(formatted)
170
+ setFormatError(null)
171
+ })
172
+ .catch((error) => {
173
+ setFormatError(
174
+ error instanceof Error
175
+ ? error.message
176
+ : "Could not format sketch source."
177
+ )
178
+ })
179
+ }}
180
+ title="Format sketch source"
181
+ variant="ghost"
182
+ >
183
+ <TextAlignRightIcon size={14} weight="regular" />
184
+ </IconButton>
185
+ </div>
186
+
187
+ <Typography tone="muted" variant="caption">
188
+ {`⌘V export const sketch = Fn(() => { ...`}
189
+ </Typography>
190
+ {formatError ? (
191
+ <Typography tone="muted" variant="caption">
192
+ {formatError}
193
+ </Typography>
194
+ ) : null}
195
+ </div>
196
+ </section>
197
+ )
198
+ }
199
+
200
+ export function SelectedLayerPropertiesContent({
201
+ blendMode,
202
+ compositeMode,
203
+ definitionName,
204
+ expandedParamGroups,
205
+ hue,
206
+ layerId,
207
+ layerKind,
208
+ layerName,
209
+ layerRuntimeError,
210
+ layerSubtitle,
211
+ layerType,
212
+ onToggleParamGroup,
213
+ onTimelineKeyframe,
214
+ opacity,
215
+ reduceMotion,
216
+ saturation,
217
+ setLayerBlendMode,
218
+ setLayerCompositeMode,
219
+ setLayerHue,
220
+ setLayerOpacity,
221
+ setLayerSaturation,
222
+ timelinePanelOpen,
223
+ updateLayerParam,
224
+ values,
225
+ visibleParams,
226
+ }: {
227
+ blendMode: BlendMode
228
+ compositeMode: LayerCompositeMode
229
+ definitionName: string
230
+ expandedParamGroups: Record<string, boolean>
231
+ hue: number
232
+ layerId: string
233
+ layerKind: string
234
+ layerName: string
235
+ layerRuntimeError: string | null
236
+ layerSubtitle: string
237
+ layerType: LayerType
238
+ onToggleParamGroup: (groupId: string) => void
239
+ onTimelineKeyframe: (
240
+ binding: AnimatedPropertyBinding,
241
+ layerId: string,
242
+ value: ParameterValue
243
+ ) => void
244
+ opacity: number
245
+ reduceMotion: boolean
246
+ saturation: number
247
+ setLayerBlendMode: (id: string, value: BlendMode) => void
248
+ setLayerCompositeMode: (id: string, value: LayerCompositeMode) => void
249
+ setLayerHue: (id: string, value: number) => void
250
+ setLayerOpacity: (id: string, value: number) => void
251
+ setLayerSaturation: (id: string, value: number) => void
252
+ timelinePanelOpen: boolean
253
+ updateLayerParam: (id: string, key: string, value: ParameterValue) => void
254
+ values: Record<string, ParameterValue>
255
+ visibleParams: ParameterDefinition[]
256
+ }) {
257
+ const groupedParams = useMemo(
258
+ () => groupVisibleParams(visibleParams),
259
+ [visibleParams]
260
+ )
261
+ const showGroupedParams =
262
+ groupedParams.length > 1 || groupedParams[0]?.label !== DEFAULT_PARAM_GROUP
263
+
264
+ const opacityBinding = useMemo(
265
+ () => ({
266
+ kind: "layer" as const,
267
+ label: "Opacity",
268
+ property: "opacity" as const,
269
+ valueType: "number" as const,
270
+ }),
271
+ []
272
+ )
273
+ const hueBinding = useMemo(
274
+ () => ({
275
+ kind: "layer" as const,
276
+ label: "Hue",
277
+ property: "hue" as const,
278
+ valueType: "number" as const,
279
+ }),
280
+ []
281
+ )
282
+ const saturationBinding = useMemo(
283
+ () => ({
284
+ kind: "layer" as const,
285
+ label: "Saturation",
286
+ property: "saturation" as const,
287
+ valueType: "number" as const,
288
+ }),
289
+ []
290
+ )
291
+ const timelineTracks = useTimelineStore((state) => state.tracks)
292
+
293
+ const hasTrack = useCallback(
294
+ (binding: AnimatedPropertyBinding) =>
295
+ hasTrackForBinding(timelineTracks, layerId, binding),
296
+ [layerId, timelineTracks]
297
+ )
298
+
299
+ const buildTimelineControl = useCallback(
300
+ (
301
+ binding: AnimatedPropertyBinding | null,
302
+ value: ParameterValue
303
+ ): TimelineKeyframeControl | null => {
304
+ if (!binding) {
305
+ return null
306
+ }
307
+
308
+ return {
309
+ binding,
310
+ hasTrack: hasTrack(binding),
311
+ layerId,
312
+ onKeyframe: onTimelineKeyframe,
313
+ reduceMotion,
314
+ timelinePanelOpen,
315
+ value,
316
+ }
317
+ },
318
+ [hasTrack, layerId, onTimelineKeyframe, reduceMotion, timelinePanelOpen]
319
+ )
320
+
321
+ return (
322
+ <>
323
+ <div className="flex flex-col gap-1.5 border-b border-[var(--ds-border-divider)] px-4 pt-[14px] pb-3">
324
+ <div className="flex items-center justify-between gap-2">
325
+ <Typography tone="secondary" variant="overline">
326
+ Properties
327
+ </Typography>
328
+ <span className="inline-flex min-h-5 items-center rounded-[var(--ds-radius-icon)] border border-[var(--ds-border-divider)] bg-[var(--ds-color-surface-active)] px-[7px] font-[var(--ds-font-mono)] text-[10px] leading-3 text-[var(--ds-color-text-secondary)] capitalize">
329
+ {formatLayerKind(layerKind)}
330
+ </span>
331
+ </div>
332
+ <Typography variant="title">{layerName}</Typography>
333
+ {layerSubtitle ? (
334
+ <Typography tone="muted" variant="monoXs">
335
+ {layerSubtitle}
336
+ </Typography>
337
+ ) : null}
338
+ {layerRuntimeError ? (
339
+ <Typography tone="muted" variant="caption">
340
+ {layerRuntimeError}
341
+ </Typography>
342
+ ) : null}
343
+ </div>
344
+
345
+ <div className="flex min-h-0 max-h-[min(62vh,620px)] flex-col gap-0 overflow-y-auto">
346
+ <section className="flex flex-col gap-3 border-t border-[var(--ds-border-divider)] px-4 pt-[14px] pb-4 first:border-t-0">
347
+ <Typography
348
+ className="uppercase"
349
+ tone="secondary"
350
+ variant="overline"
351
+ >
352
+ General
353
+ </Typography>
354
+
355
+ <div className="flex flex-col gap-[10px]">
356
+ <Slider
357
+ label={renderFieldLabel(
358
+ "Opacity",
359
+ buildTimelineControl(opacityBinding, opacity)
360
+ )}
361
+ max={100}
362
+ min={0}
363
+ onValueChange={(value) => setLayerOpacity(layerId, value / 100)}
364
+ value={opacity * 100}
365
+ valueSuffix="%"
366
+ />
367
+
368
+ <div className="grid items-center gap-[10px] [grid-template-columns:minmax(0,1fr)_132px]">
369
+ <Typography
370
+ className="min-w-0"
371
+ tone="secondary"
372
+ variant="label"
373
+ >
374
+ Blend
375
+ </Typography>
376
+ <Select
377
+ className="w-[132px]"
378
+ onValueChange={(value) => {
379
+ if (value) {
380
+ setLayerBlendMode(layerId, value as BlendMode)
381
+ }
382
+ }}
383
+ options={blendModeOptions}
384
+ triggerClassName="w-[132px]"
385
+ value={blendMode}
386
+ />
387
+ </div>
388
+
389
+ <div className="grid items-center gap-[10px] [grid-template-columns:minmax(0,1fr)_132px]">
390
+ <Typography
391
+ className="min-w-0"
392
+ tone="secondary"
393
+ variant="label"
394
+ >
395
+ Mode
396
+ </Typography>
397
+ <Select
398
+ className="w-[132px]"
399
+ onValueChange={(value) => {
400
+ if (value) {
401
+ setLayerCompositeMode(layerId, value as LayerCompositeMode)
402
+ }
403
+ }}
404
+ options={compositeModeOptions}
405
+ triggerClassName="w-[132px]"
406
+ value={compositeMode}
407
+ />
408
+ </div>
409
+
410
+ <Slider
411
+ label={renderFieldLabel(
412
+ "Hue",
413
+ buildTimelineControl(hueBinding, hue)
414
+ )}
415
+ max={180}
416
+ min={-180}
417
+ onValueChange={(value) => setLayerHue(layerId, value)}
418
+ value={hue}
419
+ />
420
+
421
+ <Slider
422
+ label={renderFieldLabel(
423
+ "Saturation",
424
+ buildTimelineControl(saturationBinding, saturation)
425
+ )}
426
+ max={2}
427
+ min={0}
428
+ onValueChange={(value) => setLayerSaturation(layerId, value)}
429
+ step={0.01}
430
+ value={saturation}
431
+ valueFormatOptions={{
432
+ maximumFractionDigits: 2,
433
+ minimumFractionDigits: 2,
434
+ }}
435
+ />
436
+ </div>
437
+ </section>
438
+
439
+ {layerType === "custom-shader" ? (
440
+ <CustomShaderSection
441
+ layerId={layerId}
442
+ updateLayerParam={updateLayerParam}
443
+ values={values}
444
+ />
445
+ ) : null}
446
+
447
+ {visibleParams.length > 0 ? (
448
+ <section className="flex flex-col gap-3 border-t border-[var(--ds-border-divider)] px-4 pt-[14px] pb-4 first:border-t-0">
449
+ {!showGroupedParams && (
450
+ <Typography
451
+ className="uppercase"
452
+ tone="secondary"
453
+ variant="overline"
454
+ >
455
+ {definitionName}
456
+ </Typography>
457
+ )}
458
+
459
+ {showGroupedParams ? (
460
+ <div className="flex flex-col gap-3">
461
+ {groupedParams.map((group) => {
462
+ const groupKey = `${layerId}:${group.id}`
463
+ const isExpanded = expandedParamGroups[groupKey] ?? true
464
+
465
+ return (
466
+ <div className="flex flex-col gap-[10px]" key={group.id}>
467
+ {group.collapsible ? (
468
+ <button
469
+ aria-expanded={isExpanded}
470
+ className="inline-flex min-h-0 cursor-pointer items-center bg-transparent p-0 text-left text-inherit transition-[background-color,color,transform] duration-120 ease-[ease] hover:text-[var(--ds-color-text-primary)] active:scale-[0.99]"
471
+ onClick={() => onToggleParamGroup(groupKey)}
472
+ type="button"
473
+ >
474
+ <div className="inline-flex min-w-0 items-center gap-2">
475
+ <span
476
+ aria-hidden="true"
477
+ className={cn(
478
+ "inline-block h-[7px] w-[7px] shrink-0 border-r-[1.5px] border-b-[1.5px] border-[var(--ds-color-text-secondary)] transition-transform duration-180 ease-[cubic-bezier(0.34,1.56,0.64,1)]",
479
+ isExpanded
480
+ ? "translate-x-[-1px] translate-y-[-1px] rotate-[-135deg]"
481
+ : "translate-y-[-1px] rotate-45"
482
+ )}
483
+ />
484
+ <Typography tone="secondary" variant="overline">
485
+ {group.label}
486
+ </Typography>
487
+ </div>
488
+ </button>
489
+ ) : (
490
+ <div className="inline-flex min-w-0 items-center gap-2 px-[2px]">
491
+ <Typography tone="secondary" variant="overline">
492
+ {group.label}
493
+ </Typography>
494
+ </div>
495
+ )}
496
+
497
+ <AnimatePresence initial={false}>
498
+ {isExpanded ? (
499
+ <motion.div
500
+ animate={
501
+ reduceMotion
502
+ ? { opacity: 1 }
503
+ : { height: "auto", opacity: 1 }
504
+ }
505
+ exit={
506
+ reduceMotion
507
+ ? { opacity: 0 }
508
+ : { height: 0, opacity: 0 }
509
+ }
510
+ initial={
511
+ reduceMotion
512
+ ? { opacity: 0 }
513
+ : { height: 0, opacity: 0 }
514
+ }
515
+ transition={
516
+ reduceMotion
517
+ ? { duration: 0.12, ease: "easeOut" }
518
+ : {
519
+ damping: 34,
520
+ mass: 0.85,
521
+ stiffness: 360,
522
+ type: "spring",
523
+ }
524
+ }
525
+ >
526
+ <div className="flex flex-col gap-[10px]">
527
+ {group.params.map((param) => (
528
+ <ParameterField
529
+ definition={param}
530
+ key={param.key}
531
+ layerId={layerId}
532
+ onChange={updateLayerParam}
533
+ onTimelineKeyframe={onTimelineKeyframe}
534
+ reduceMotion={reduceMotion}
535
+ timelineBinding={createParamTimelineBinding(
536
+ param
537
+ )}
538
+ timelinePanelOpen={timelinePanelOpen}
539
+ value={
540
+ values[param.key] ?? param.defaultValue
541
+ }
542
+ />
543
+ ))}
544
+ </div>
545
+ </motion.div>
546
+ ) : null}
547
+ </AnimatePresence>
548
+ </div>
549
+ )
550
+ })}
551
+ </div>
552
+ ) : (
553
+ <div className="flex flex-col gap-[10px]">
554
+ {visibleParams.map((param) => (
555
+ <ParameterField
556
+ definition={param}
557
+ key={param.key}
558
+ layerId={layerId}
559
+ onChange={updateLayerParam}
560
+ onTimelineKeyframe={onTimelineKeyframe}
561
+ reduceMotion={reduceMotion}
562
+ timelineBinding={createParamTimelineBinding(param)}
563
+ timelinePanelOpen={timelinePanelOpen}
564
+ value={values[param.key] ?? param.defaultValue}
565
+ />
566
+ ))}
567
+ </div>
568
+ )}
569
+ </section>
570
+ ) : null}
571
+ </div>
572
+ </>
573
+ )
574
+ }