@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,389 @@
1
+ "use client"
2
+
3
+ import { motion } from "motion/react"
4
+ import type {
5
+ AnimatedPropertyBinding,
6
+ ParameterDefinition,
7
+ ParameterValue,
8
+ SelectParameterDefinition,
9
+ TextParameterDefinition,
10
+ } from "@/types/editor"
11
+ import { cn } from "@/lib/cn"
12
+ import { ColorPicker } from "@/components/ui/color-picker"
13
+ import { IconButton } from "@/components/ui/icon-button"
14
+ import { Select } from "@/components/ui/select"
15
+ import { Slider } from "@/components/ui/slider"
16
+ import { Toggle } from "@/components/ui/toggle"
17
+ import { Typography } from "@/components/ui/typography"
18
+ import { XYPad } from "@/components/ui/xy-pad"
19
+ import { useLayerStore } from "@/store/layer-store"
20
+ import { useTimelineStore } from "@/store/timeline-store"
21
+ import {
22
+ hasTrackForBinding,
23
+ toBooleanValue,
24
+ toColorValue,
25
+ toNumberValue,
26
+ toTextValue,
27
+ toVec2Value,
28
+ } from "./properties-sidebar-utils"
29
+
30
+ export type TimelineKeyframeControl = {
31
+ binding: AnimatedPropertyBinding | null
32
+ hasTrack: boolean
33
+ layerId: string
34
+ onKeyframe: (
35
+ binding: AnimatedPropertyBinding,
36
+ layerId: string,
37
+ value: ParameterValue
38
+ ) => void
39
+ reduceMotion: boolean
40
+ timelinePanelOpen: boolean
41
+ value: ParameterValue
42
+ }
43
+
44
+ function RhombusIcon() {
45
+ return (
46
+ <svg aria-hidden="true" fill="none" viewBox="0 0 14 14">
47
+ <path
48
+ d="M7 1.8L12.2 7L7 12.2L1.8 7L7 1.8Z"
49
+ fill="currentColor"
50
+ fillOpacity="0.18"
51
+ stroke="currentColor"
52
+ strokeWidth="1.2"
53
+ />
54
+ </svg>
55
+ )
56
+ }
57
+
58
+ function TimelineKeyframeButton({
59
+ control,
60
+ }: {
61
+ control: TimelineKeyframeControl | null
62
+ }) {
63
+ if (!control?.binding) {
64
+ return null
65
+ }
66
+
67
+ let animation: { opacity: number; scale?: number }
68
+
69
+ if (control.timelinePanelOpen) {
70
+ animation = control.reduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }
71
+ } else {
72
+ animation = control.reduceMotion
73
+ ? { opacity: 0 }
74
+ : { opacity: 0, scale: 0.82 }
75
+ }
76
+
77
+ return (
78
+ <span className="inline-flex h-6 w-6 shrink-0 items-center justify-center">
79
+ <motion.span
80
+ animate={animation}
81
+ className="inline-flex shrink-0"
82
+ initial={false}
83
+ transition={
84
+ control.reduceMotion
85
+ ? { duration: 0.12, ease: "easeOut" }
86
+ : { damping: 20, mass: 0.5, stiffness: 420, type: "spring" }
87
+ }
88
+ >
89
+ <IconButton
90
+ aria-hidden={!control.timelinePanelOpen}
91
+ aria-label={`Create keyframe for ${control.binding.label}`}
92
+ className={cn(
93
+ "h-6 w-6 [&_svg]:h-3 [&_svg]:w-3",
94
+ control.hasTrack && "text-[rgb(200_220_255_/_0.92)]"
95
+ )}
96
+ disabled={!control.timelinePanelOpen}
97
+ onClick={() =>
98
+ control.onKeyframe(
99
+ control.binding as AnimatedPropertyBinding,
100
+ control.layerId,
101
+ control.value
102
+ )
103
+ }
104
+ tabIndex={control.timelinePanelOpen ? 0 : -1}
105
+ variant="ghost"
106
+ >
107
+ <RhombusIcon />
108
+ </IconButton>
109
+ </motion.span>
110
+ </span>
111
+ )
112
+ }
113
+
114
+ function renderFieldLabelStack(
115
+ label: string,
116
+ description: string | undefined,
117
+ control: TimelineKeyframeControl | null
118
+ ) {
119
+ return (
120
+ <span
121
+ style={{
122
+ display: "flex",
123
+ flexDirection: "column",
124
+ gap: "2px",
125
+ minWidth: 0,
126
+ }}
127
+ >
128
+ <Typography className="min-w-0" tone="secondary" variant="label">
129
+ {renderFieldLabel(label, control)}
130
+ </Typography>
131
+ {description ? (
132
+ <Typography tone="muted" variant="caption">
133
+ {description}
134
+ </Typography>
135
+ ) : null}
136
+ </span>
137
+ )
138
+ }
139
+
140
+ function getCustomPaletteFieldLabel(
141
+ definition: ParameterDefinition,
142
+ layerParams: Record<string, ParameterValue> | null
143
+ ): string {
144
+ if (!layerParams) {
145
+ return definition.label
146
+ }
147
+
148
+ const colorCount =
149
+ typeof layerParams.customColorCount === "number"
150
+ ? layerParams.customColorCount
151
+ : 4
152
+
153
+ switch (definition.key) {
154
+ case "customColor1":
155
+ return "Shadows"
156
+ case "customColor2":
157
+ return colorCount <= 2 ? "Highlights" : "Midtones"
158
+ case "customColor3":
159
+ return colorCount === 3 ? "Highlights" : "High Mids"
160
+ case "customColor4":
161
+ return "Highlights"
162
+ default:
163
+ return definition.label
164
+ }
165
+ }
166
+
167
+ function shouldRenderCustomPaletteField(
168
+ definition: ParameterDefinition,
169
+ layerParams: Record<string, ParameterValue> | null
170
+ ): boolean {
171
+ if (!layerParams) {
172
+ return true
173
+ }
174
+
175
+ if (
176
+ definition.key === "customBgColor" ||
177
+ definition.key === "customColorCount" ||
178
+ definition.key === "customLuminanceBias" ||
179
+ definition.key === "customColor1" ||
180
+ definition.key === "customColor2"
181
+ ) {
182
+ return layerParams.colorMode === "custom"
183
+ }
184
+
185
+ if (definition.key === "customColor3") {
186
+ return (
187
+ layerParams.colorMode === "custom" &&
188
+ typeof layerParams.customColorCount === "number" &&
189
+ layerParams.customColorCount >= 3
190
+ )
191
+ }
192
+
193
+ if (definition.key === "customColor4") {
194
+ return (
195
+ layerParams.colorMode === "custom" &&
196
+ typeof layerParams.customColorCount === "number" &&
197
+ layerParams.customColorCount >= 4
198
+ )
199
+ }
200
+
201
+ return true
202
+ }
203
+
204
+ export function renderFieldLabel(
205
+ label: string,
206
+ control: TimelineKeyframeControl | null
207
+ ) {
208
+ return (
209
+ <span className="inline-flex min-w-0 w-full items-center justify-between gap-2">
210
+ <span>{label}</span>
211
+ <TimelineKeyframeButton control={control} />
212
+ </span>
213
+ )
214
+ }
215
+
216
+ export function ParameterField({
217
+ definition,
218
+ layerId,
219
+ onChange,
220
+ onTimelineKeyframe,
221
+ reduceMotion,
222
+ timelineBinding,
223
+ timelinePanelOpen,
224
+ value,
225
+ }: {
226
+ definition: ParameterDefinition
227
+ layerId: string
228
+ onChange: (id: string, key: string, value: ParameterValue) => void
229
+ onTimelineKeyframe: (
230
+ binding: AnimatedPropertyBinding,
231
+ layerId: string,
232
+ value: ParameterValue
233
+ ) => void
234
+ reduceMotion: boolean
235
+ timelineBinding: AnimatedPropertyBinding | null
236
+ timelinePanelOpen: boolean
237
+ value: ParameterValue
238
+ }) {
239
+ const layerParams = useLayerStore(
240
+ (state) =>
241
+ state.layers.find((layer) => layer.id === layerId)?.params ?? null
242
+ )
243
+ const timelineTracks = useTimelineStore((state) => state.tracks)
244
+ const timelineControl: TimelineKeyframeControl | null = timelineBinding
245
+ ? {
246
+ binding: timelineBinding,
247
+ hasTrack: hasTrackForBinding(timelineTracks, layerId, timelineBinding),
248
+ layerId,
249
+ onKeyframe: onTimelineKeyframe,
250
+ reduceMotion,
251
+ timelinePanelOpen,
252
+ value,
253
+ }
254
+ : null
255
+
256
+ if (!shouldRenderCustomPaletteField(definition, layerParams)) {
257
+ return null
258
+ }
259
+
260
+ const fieldLabel = getCustomPaletteFieldLabel(definition, layerParams)
261
+
262
+ switch (definition.type) {
263
+ case "number":
264
+ return (
265
+ <Slider
266
+ label={renderFieldLabelStack(
267
+ fieldLabel,
268
+ definition.description,
269
+ timelineControl
270
+ )}
271
+ max={definition.max ?? 100}
272
+ min={definition.min ?? 0}
273
+ onValueChange={(nextValue) =>
274
+ onChange(layerId, definition.key, nextValue)
275
+ }
276
+ step={definition.step ?? 0.01}
277
+ value={toNumberValue(value, definition.defaultValue)}
278
+ valueFormatOptions={{
279
+ maximumFractionDigits: 2,
280
+ minimumFractionDigits: 0,
281
+ }}
282
+ />
283
+ )
284
+
285
+ case "select":
286
+ return (
287
+ <div
288
+ className="grid items-center gap-[10px] [grid-template-columns:minmax(0,1fr)_132px]"
289
+ style={definition.description ? { alignItems: "start" } : undefined}
290
+ >
291
+ {renderFieldLabelStack(
292
+ fieldLabel,
293
+ definition.description,
294
+ timelineControl
295
+ )}
296
+ <Select
297
+ className="w-[132px]"
298
+ onValueChange={(nextValue) => {
299
+ if (nextValue) {
300
+ onChange(layerId, definition.key, nextValue)
301
+ }
302
+ }}
303
+ options={(definition as SelectParameterDefinition).options}
304
+ triggerClassName="w-[132px]"
305
+ value={typeof value === "string" ? value : definition.defaultValue}
306
+ />
307
+ </div>
308
+ )
309
+
310
+ case "boolean":
311
+ return (
312
+ <div
313
+ className="grid items-center gap-[10px] [grid-template-columns:minmax(0,1fr)_auto]"
314
+ style={definition.description ? { alignItems: "start" } : undefined}
315
+ >
316
+ {renderFieldLabelStack(
317
+ fieldLabel,
318
+ definition.description,
319
+ timelineControl
320
+ )}
321
+ <Toggle
322
+ checked={toBooleanValue(value)}
323
+ className="justify-self-end"
324
+ onCheckedChange={(nextValue) =>
325
+ onChange(layerId, definition.key, nextValue)
326
+ }
327
+ />
328
+ </div>
329
+ )
330
+
331
+ case "color":
332
+ return (
333
+ <div
334
+ className="grid items-center gap-[10px] [grid-template-columns:minmax(0,1fr)_132px]"
335
+ style={definition.description ? { alignItems: "start" } : undefined}
336
+ >
337
+ {renderFieldLabelStack(
338
+ fieldLabel,
339
+ definition.description,
340
+ timelineControl
341
+ )}
342
+ <ColorPicker
343
+ onValueChange={(nextValue) =>
344
+ onChange(layerId, definition.key, nextValue)
345
+ }
346
+ value={toColorValue(value)}
347
+ />
348
+ </div>
349
+ )
350
+
351
+ case "vec2":
352
+ return (
353
+ <XYPad
354
+ label={renderFieldLabel(fieldLabel, timelineControl)}
355
+ max={definition.max ?? 1}
356
+ min={definition.min ?? -1}
357
+ onValueChange={(nextValue) =>
358
+ onChange(layerId, definition.key, nextValue)
359
+ }
360
+ step={definition.step ?? 0.01}
361
+ value={toVec2Value(value)}
362
+ />
363
+ )
364
+
365
+ case "text":
366
+ return (
367
+ <label className="flex flex-col gap-2">
368
+ {renderFieldLabelStack(
369
+ fieldLabel,
370
+ definition.description,
371
+ timelineControl
372
+ )}
373
+ <input
374
+ 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)]"
375
+ maxLength={(definition as TextParameterDefinition).maxLength}
376
+ onChange={(event) =>
377
+ onChange(layerId, definition.key, event.currentTarget.value)
378
+ }
379
+ spellCheck={false}
380
+ type="text"
381
+ value={toTextValue(value, definition.defaultValue)}
382
+ />
383
+ </label>
384
+ )
385
+
386
+ default:
387
+ return null
388
+ }
389
+ }
@@ -0,0 +1,178 @@
1
+ "use client"
2
+
3
+ import type {
4
+ AnimatedPropertyBinding,
5
+ EditorAsset,
6
+ ParameterDefinition,
7
+ ParameterValue,
8
+ } from "@/types/editor"
9
+ import type { useTimelineStore } from "@/store/timeline-store"
10
+
11
+ export const blendModeOptions = [
12
+ { label: "Normal", value: "normal" },
13
+ { label: "Multiply", value: "multiply" },
14
+ { label: "Screen", value: "screen" },
15
+ { label: "Overlay", value: "overlay" },
16
+ { label: "Darken", value: "darken" },
17
+ { label: "Lighten", value: "lighten" },
18
+ ] as const
19
+
20
+ export const compositeModeOptions = [
21
+ { label: "Filter", value: "filter" },
22
+ { label: "Mask", value: "mask" },
23
+ ] as const
24
+
25
+ const COLLAPSIBLE_PARAM_GROUPS = new Set(["Points", "Effects"])
26
+ export const DEFAULT_PARAM_GROUP = "Settings"
27
+
28
+ export type ParamGroup = {
29
+ collapsible: boolean
30
+ id: string
31
+ label: string
32
+ params: ParameterDefinition[]
33
+ }
34
+
35
+ export function getBindingKey(binding: AnimatedPropertyBinding): string {
36
+ if (binding.kind === "layer") {
37
+ return `layer:${binding.property}`
38
+ }
39
+
40
+ return `param:${binding.key}`
41
+ }
42
+
43
+ export function getSelectedAsset(
44
+ assetById: Map<string, EditorAsset>,
45
+ assetId: string | null
46
+ ): EditorAsset | null {
47
+ if (!assetId) {
48
+ return null
49
+ }
50
+
51
+ return assetById.get(assetId) ?? null
52
+ }
53
+
54
+ export function formatLayerKind(kind: string): string {
55
+ switch (kind) {
56
+ case "effect":
57
+ return "Effect"
58
+ case "model":
59
+ return "3D Model"
60
+ case "source":
61
+ return "Source"
62
+ default:
63
+ return kind
64
+ }
65
+ }
66
+
67
+ export function toColorValue(value: ParameterValue): string {
68
+ return typeof value === "string" ? value : "#ffffff"
69
+ }
70
+
71
+ export function toVec2Value(value: ParameterValue): [number, number] {
72
+ return Array.isArray(value) && value.length === 2
73
+ ? [value[0] ?? 0, value[1] ?? 0]
74
+ : [0, 0]
75
+ }
76
+
77
+ export function toNumberValue(value: ParameterValue, fallback = 0): number {
78
+ return typeof value === "number" ? value : fallback
79
+ }
80
+
81
+ export function toBooleanValue(value: ParameterValue): boolean {
82
+ return value === true
83
+ }
84
+
85
+ export function toTextValue(value: ParameterValue, fallback: string): string {
86
+ return typeof value === "string" ? value : fallback
87
+ }
88
+
89
+ export function resolveParamValue(
90
+ params: Record<string, ParameterValue>,
91
+ definitions: ParameterDefinition[],
92
+ key: string
93
+ ): ParameterValue | undefined {
94
+ const explicitValue = params[key]
95
+ if (explicitValue !== undefined) {
96
+ return explicitValue
97
+ }
98
+
99
+ const definition = definitions.find((entry) => entry.key === key)
100
+ return definition?.defaultValue
101
+ }
102
+
103
+ export function isParamVisible(
104
+ definition: ParameterDefinition,
105
+ params: Record<string, ParameterValue>,
106
+ definitions: ParameterDefinition[]
107
+ ): boolean {
108
+ if (!definition.visibleWhen) {
109
+ return true
110
+ }
111
+
112
+ const controllingValue = resolveParamValue(
113
+ params,
114
+ definitions,
115
+ definition.visibleWhen.key
116
+ )
117
+
118
+ if ("equals" in definition.visibleWhen) {
119
+ return controllingValue === definition.visibleWhen.equals
120
+ }
121
+
122
+ return (
123
+ typeof controllingValue === "number" &&
124
+ controllingValue >= definition.visibleWhen.gte
125
+ )
126
+ }
127
+
128
+ export function groupVisibleParams(params: ParameterDefinition[]): ParamGroup[] {
129
+ const groups = new Map<string, ParamGroup>()
130
+
131
+ for (const param of params) {
132
+ const label = param.group ?? DEFAULT_PARAM_GROUP
133
+ const id = label.toLowerCase().replace(/\s+/g, "-")
134
+ const existing = groups.get(id)
135
+
136
+ if (existing) {
137
+ existing.params.push(param)
138
+ continue
139
+ }
140
+
141
+ groups.set(id, {
142
+ collapsible: COLLAPSIBLE_PARAM_GROUPS.has(label),
143
+ id,
144
+ label,
145
+ params: [param],
146
+ })
147
+ }
148
+
149
+ return [...groups.values()]
150
+ }
151
+
152
+ export function createParamTimelineBinding(
153
+ definition: ParameterDefinition
154
+ ): AnimatedPropertyBinding | null {
155
+ if (definition.type === "text") {
156
+ return null
157
+ }
158
+
159
+ return {
160
+ key: definition.key,
161
+ kind: "param",
162
+ label: definition.label,
163
+ valueType: definition.type === "boolean" ? "boolean" : definition.type,
164
+ }
165
+ }
166
+
167
+ export function hasTrackForBinding(
168
+ tracks: ReturnType<typeof useTimelineStore.getState>["tracks"],
169
+ layerId: string,
170
+ binding: AnimatedPropertyBinding
171
+ ): boolean {
172
+ const bindingKey = getBindingKey(binding)
173
+
174
+ return tracks.some(
175
+ (track) =>
176
+ track.layerId === layerId && getBindingKey(track.binding) === bindingKey
177
+ )
178
+ }