@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,572 @@
1
+ import { create } from "zustand"
2
+ import { advanceProjectTimeline } from "@/renderer/project-clock"
3
+ import type {
4
+ AnimatedPropertyBinding,
5
+ AnimatableValueType,
6
+ EditorLayer,
7
+ LayerAnimatableProperty,
8
+ ParameterType,
9
+ ParameterValue,
10
+ TimelineInterpolation,
11
+ TimelineKeyframe,
12
+ TimelineStateSnapshot,
13
+ TimelineTrack,
14
+ } from "@/types/editor"
15
+ import {
16
+ cloneParameterValue,
17
+ getParameterDefinition,
18
+ } from "@/lib/editor/parameter-schema"
19
+ import { getLayerDefinition } from "@/lib/editor/config/layer-registry"
20
+
21
+ export interface TimelineStoreState extends TimelineStateSnapshot {}
22
+
23
+ interface ToggleKeyframeInput {
24
+ binding: AnimatedPropertyBinding
25
+ layerId: string
26
+ time?: number
27
+ value: ParameterValue
28
+ }
29
+
30
+ interface UpsertKeyframeInput extends ToggleKeyframeInput {}
31
+
32
+ export interface TimelineStoreActions {
33
+ advance: (delta: number) => void
34
+ clearLayerTracks: (layerId: string) => void
35
+ getTrackForBinding: (
36
+ layerId: string,
37
+ binding: AnimatedPropertyBinding,
38
+ ) => TimelineTrack | null
39
+ pruneTracks: (layers: EditorLayer[]) => void
40
+ replaceState: (
41
+ nextState: Pick<
42
+ TimelineStateSnapshot,
43
+ "currentTime" | "duration" | "isPlaying" | "loop" | "selectedKeyframeId" | "selectedTrackId" | "tracks"
44
+ >,
45
+ ) => void
46
+ removeKeyframe: (trackId: string, keyframeId: string) => void
47
+ setCurrentTime: (time: number) => void
48
+ setDuration: (duration: number) => void
49
+ setLoop: (loop: boolean) => void
50
+ setPlaying: (playing: boolean) => void
51
+ setSelected: (trackId: string | null, keyframeId?: string | null) => void
52
+ setTrackEnabled: (trackId: string, enabled: boolean) => void
53
+ setTrackInterpolation: (trackId: string, interpolation: TimelineInterpolation) => void
54
+ setKeyframeTime: (trackId: string, keyframeId: string, time: number) => void
55
+ stop: () => void
56
+ toggleKeyframe: (input: ToggleKeyframeInput) => void
57
+ togglePlaying: () => void
58
+ upsertKeyframe: (input: UpsertKeyframeInput) => void
59
+ }
60
+
61
+ export type TimelineStore = TimelineStoreState & TimelineStoreActions
62
+
63
+ const DEFAULT_DURATION = 6
64
+ const MIN_DURATION = 0.25
65
+ const MAX_DURATION = 120
66
+ const TIME_EPSILON = 1 / 240
67
+
68
+ function clampDuration(duration: number): number {
69
+ if (!Number.isFinite(duration)) {
70
+ return DEFAULT_DURATION
71
+ }
72
+
73
+ return Math.min(MAX_DURATION, Math.max(MIN_DURATION, duration))
74
+ }
75
+
76
+ function clampTime(time: number, duration: number): number {
77
+ if (!Number.isFinite(time)) {
78
+ return 0
79
+ }
80
+
81
+ return Math.min(Math.max(duration, MIN_DURATION), Math.max(0, time))
82
+ }
83
+
84
+ function sortKeyframes(keyframes: TimelineKeyframe[]): TimelineKeyframe[] {
85
+ return [...keyframes].sort((left, right) => left.time - right.time)
86
+ }
87
+
88
+ function bindingEquals(left: AnimatedPropertyBinding, right: AnimatedPropertyBinding): boolean {
89
+ if (left.kind !== right.kind) {
90
+ return false
91
+ }
92
+
93
+ if (left.kind === "param" && right.kind === "param") {
94
+ return left.key === right.key
95
+ }
96
+
97
+ if (left.kind === "layer" && right.kind === "layer") {
98
+ return left.property === right.property
99
+ }
100
+
101
+ return false
102
+ }
103
+
104
+ function isAnimatableValueType(
105
+ valueType: ParameterType | AnimatableValueType,
106
+ ): valueType is AnimatableValueType {
107
+ return valueType !== "text"
108
+ }
109
+
110
+ function defaultInterpolationForBinding(binding: AnimatedPropertyBinding): TimelineInterpolation {
111
+ if (binding.valueType === "boolean" || binding.valueType === "select") {
112
+ return "step"
113
+ }
114
+
115
+ return "smooth"
116
+ }
117
+
118
+ function getLayerBindingValueType(property: LayerAnimatableProperty): "boolean" | "number" {
119
+ if (property === "visible") {
120
+ return "boolean"
121
+ }
122
+
123
+ return "number"
124
+ }
125
+
126
+ export function createLayerPropertyBinding(
127
+ property: LayerAnimatableProperty,
128
+ ): AnimatedPropertyBinding {
129
+ const labelByProperty: Record<LayerAnimatableProperty, string> = {
130
+ hue: "Hue",
131
+ opacity: "Opacity",
132
+ saturation: "Saturation",
133
+ visible: "Visible",
134
+ }
135
+
136
+ return {
137
+ kind: "layer",
138
+ label: labelByProperty[property],
139
+ property,
140
+ valueType: getLayerBindingValueType(property),
141
+ }
142
+ }
143
+
144
+ export function createParamBinding(
145
+ layer: EditorLayer,
146
+ key: string,
147
+ ): AnimatedPropertyBinding | null {
148
+ const definition = getParameterDefinition(getLayerDefinition(layer.type).params, key)
149
+
150
+ if (!(definition && isAnimatableValueType(definition.type))) {
151
+ return null
152
+ }
153
+
154
+ return {
155
+ key,
156
+ kind: "param",
157
+ label: definition.label,
158
+ valueType: definition.type,
159
+ }
160
+ }
161
+
162
+ function cloneTrack(track: TimelineTrack): TimelineTrack {
163
+ return {
164
+ ...track,
165
+ binding: { ...track.binding },
166
+ keyframes: track.keyframes.map((keyframe) => ({
167
+ ...keyframe,
168
+ value: cloneParameterValue(keyframe.value),
169
+ })),
170
+ }
171
+ }
172
+
173
+ function cloneTracks(tracks: TimelineTrack[]): TimelineTrack[] {
174
+ return tracks.map(cloneTrack)
175
+ }
176
+
177
+ export const useTimelineStore = create<TimelineStore>((set, get) => ({
178
+ currentTime: 0,
179
+ duration: DEFAULT_DURATION,
180
+ isPlaying: false,
181
+ loop: true,
182
+ selectedKeyframeId: null,
183
+ selectedTrackId: null,
184
+ tracks: [],
185
+
186
+ setPlaying: (isPlaying) => {
187
+ set({ isPlaying })
188
+ },
189
+
190
+ togglePlaying: () => {
191
+ set((state) => ({
192
+ isPlaying: !state.isPlaying,
193
+ }))
194
+ },
195
+
196
+ stop: () => {
197
+ set({
198
+ currentTime: 0,
199
+ isPlaying: false,
200
+ })
201
+ },
202
+
203
+ setLoop: (loop) => {
204
+ set({ loop })
205
+ },
206
+
207
+ setDuration: (duration) => {
208
+ set((state) => {
209
+ const nextDuration = clampDuration(duration)
210
+
211
+ return {
212
+ currentTime: clampTime(state.currentTime, nextDuration),
213
+ duration: nextDuration,
214
+ tracks: state.tracks.map((track) => ({
215
+ ...track,
216
+ keyframes: sortKeyframes(
217
+ track.keyframes.map((keyframe) => ({
218
+ ...keyframe,
219
+ time: clampTime(keyframe.time, nextDuration),
220
+ })),
221
+ ),
222
+ })),
223
+ }
224
+ })
225
+ },
226
+
227
+ setCurrentTime: (currentTime) => {
228
+ set((state) => ({
229
+ currentTime: clampTime(currentTime, state.duration),
230
+ }))
231
+ },
232
+
233
+ advance: (delta) => {
234
+ if (!Number.isFinite(delta) || delta <= 0) {
235
+ return
236
+ }
237
+
238
+ set((state) => {
239
+ const next = advanceProjectTimeline(state, delta)
240
+
241
+ return {
242
+ currentTime: next.currentTime,
243
+ isPlaying: next.isPlaying,
244
+ }
245
+ })
246
+ },
247
+
248
+ toggleKeyframe: ({ binding, layerId, time, value }) => {
249
+ if (!isAnimatableValueType(binding.valueType)) {
250
+ return
251
+ }
252
+
253
+ set((state) => {
254
+ const targetTime = clampTime(time ?? state.currentTime, state.duration)
255
+ const trackIndex = state.tracks.findIndex(
256
+ (track) => track.layerId === layerId && bindingEquals(track.binding, binding),
257
+ )
258
+
259
+ if (trackIndex === -1) {
260
+ const trackId = crypto.randomUUID()
261
+ const keyframeId = crypto.randomUUID()
262
+
263
+ return {
264
+ selectedKeyframeId: keyframeId,
265
+ selectedTrackId: trackId,
266
+ tracks: [
267
+ ...state.tracks,
268
+ {
269
+ binding: { ...binding },
270
+ enabled: true,
271
+ id: trackId,
272
+ interpolation: defaultInterpolationForBinding(binding),
273
+ keyframes: [
274
+ {
275
+ id: keyframeId,
276
+ time: targetTime,
277
+ value: cloneParameterValue(value),
278
+ },
279
+ ],
280
+ layerId,
281
+ },
282
+ ],
283
+ }
284
+ }
285
+
286
+ const track = state.tracks[trackIndex]
287
+
288
+ if (!track) {
289
+ return state
290
+ }
291
+
292
+ const existingKeyframe = track.keyframes.find(
293
+ (keyframe) => Math.abs(keyframe.time - targetTime) <= TIME_EPSILON,
294
+ )
295
+
296
+ if (existingKeyframe) {
297
+ const nextTracks = [...state.tracks]
298
+ const nextTrack = cloneTrack(track)
299
+ nextTrack.keyframes = nextTrack.keyframes.filter(
300
+ (keyframe) => keyframe.id !== existingKeyframe.id,
301
+ )
302
+
303
+ if (nextTrack.keyframes.length === 0) {
304
+ nextTracks.splice(trackIndex, 1)
305
+ } else {
306
+ nextTracks[trackIndex] = nextTrack
307
+ }
308
+
309
+ return {
310
+ selectedKeyframeId:
311
+ state.selectedKeyframeId === existingKeyframe.id
312
+ ? null
313
+ : state.selectedKeyframeId,
314
+ selectedTrackId:
315
+ nextTrack.keyframes.length === 0 && state.selectedTrackId === track.id
316
+ ? null
317
+ : state.selectedTrackId,
318
+ tracks: nextTracks,
319
+ }
320
+ }
321
+
322
+ const keyframeId = crypto.randomUUID()
323
+ const nextTrack = cloneTrack(track)
324
+ nextTrack.enabled = true
325
+ nextTrack.keyframes = sortKeyframes([
326
+ ...nextTrack.keyframes,
327
+ {
328
+ id: keyframeId,
329
+ time: targetTime,
330
+ value: cloneParameterValue(value),
331
+ },
332
+ ])
333
+
334
+ const nextTracks = [...state.tracks]
335
+ nextTracks[trackIndex] = nextTrack
336
+
337
+ return {
338
+ selectedKeyframeId: keyframeId,
339
+ selectedTrackId: nextTrack.id,
340
+ tracks: nextTracks,
341
+ }
342
+ })
343
+ },
344
+
345
+ upsertKeyframe: ({ binding, layerId, time, value }) => {
346
+ if (!isAnimatableValueType(binding.valueType)) {
347
+ return
348
+ }
349
+
350
+ set((state) => {
351
+ const targetTime = clampTime(time ?? state.currentTime, state.duration)
352
+ const trackIndex = state.tracks.findIndex(
353
+ (track) => track.layerId === layerId && bindingEquals(track.binding, binding),
354
+ )
355
+
356
+ if (trackIndex === -1) {
357
+ const trackId = crypto.randomUUID()
358
+ const keyframeId = crypto.randomUUID()
359
+
360
+ return {
361
+ selectedKeyframeId: keyframeId,
362
+ selectedTrackId: trackId,
363
+ tracks: [
364
+ ...state.tracks,
365
+ {
366
+ binding: { ...binding },
367
+ enabled: true,
368
+ id: trackId,
369
+ interpolation: defaultInterpolationForBinding(binding),
370
+ keyframes: [
371
+ {
372
+ id: keyframeId,
373
+ time: targetTime,
374
+ value: cloneParameterValue(value),
375
+ },
376
+ ],
377
+ layerId,
378
+ },
379
+ ],
380
+ }
381
+ }
382
+
383
+ const track = state.tracks[trackIndex]
384
+
385
+ if (!track) {
386
+ return state
387
+ }
388
+
389
+ const nextTrack = cloneTrack(track)
390
+ const existingKeyframeIndex = nextTrack.keyframes.findIndex(
391
+ (keyframe) => Math.abs(keyframe.time - targetTime) <= TIME_EPSILON,
392
+ )
393
+ let selectedKeyframeId = state.selectedKeyframeId
394
+
395
+ if (existingKeyframeIndex !== -1) {
396
+ const currentKeyframe = nextTrack.keyframes[existingKeyframeIndex]
397
+
398
+ if (!currentKeyframe) {
399
+ return state
400
+ }
401
+
402
+ nextTrack.keyframes[existingKeyframeIndex] = {
403
+ ...currentKeyframe,
404
+ value: cloneParameterValue(value),
405
+ }
406
+ selectedKeyframeId = currentKeyframe.id
407
+ } else {
408
+ const keyframeId = crypto.randomUUID()
409
+ nextTrack.keyframes = sortKeyframes([
410
+ ...nextTrack.keyframes,
411
+ {
412
+ id: keyframeId,
413
+ time: targetTime,
414
+ value: cloneParameterValue(value),
415
+ },
416
+ ])
417
+ selectedKeyframeId = keyframeId
418
+ }
419
+
420
+ const nextTracks = [...state.tracks]
421
+ nextTracks[trackIndex] = nextTrack
422
+
423
+ return {
424
+ selectedKeyframeId,
425
+ selectedTrackId: nextTrack.id,
426
+ tracks: nextTracks,
427
+ }
428
+ })
429
+ },
430
+
431
+ setTrackEnabled: (trackId, enabled) => {
432
+ set((state) => ({
433
+ tracks: state.tracks.map((track) =>
434
+ track.id === trackId ? { ...track, enabled } : track,
435
+ ),
436
+ }))
437
+ },
438
+
439
+ setTrackInterpolation: (trackId, interpolation) => {
440
+ set((state) => ({
441
+ tracks: state.tracks.map((track) =>
442
+ track.id === trackId ? { ...track, interpolation } : track,
443
+ ),
444
+ }))
445
+ },
446
+
447
+ setSelected: (selectedTrackId, selectedKeyframeId = null) => {
448
+ set({
449
+ selectedKeyframeId,
450
+ selectedTrackId,
451
+ })
452
+ },
453
+
454
+ setKeyframeTime: (trackId, keyframeId, time) => {
455
+ set((state) => ({
456
+ tracks: state.tracks.map((track) => {
457
+ if (track.id !== trackId) {
458
+ return track
459
+ }
460
+
461
+ return {
462
+ ...track,
463
+ keyframes: sortKeyframes(
464
+ track.keyframes.map((keyframe) =>
465
+ keyframe.id === keyframeId
466
+ ? {
467
+ ...keyframe,
468
+ time: clampTime(time, state.duration),
469
+ }
470
+ : keyframe,
471
+ ),
472
+ ),
473
+ }
474
+ }),
475
+ }))
476
+ },
477
+
478
+ removeKeyframe: (trackId, keyframeId) => {
479
+ set((state) => {
480
+ const nextTracks = state.tracks
481
+ .map((track) => {
482
+ if (track.id !== trackId) {
483
+ return track
484
+ }
485
+
486
+ return {
487
+ ...track,
488
+ keyframes: track.keyframes.filter((keyframe) => keyframe.id !== keyframeId),
489
+ }
490
+ })
491
+ .filter((track) => track.keyframes.length > 0)
492
+
493
+ return {
494
+ selectedKeyframeId:
495
+ state.selectedKeyframeId === keyframeId ? null : state.selectedKeyframeId,
496
+ selectedTrackId:
497
+ state.selectedTrackId === trackId &&
498
+ !nextTracks.some((track) => track.id === trackId)
499
+ ? null
500
+ : state.selectedTrackId,
501
+ tracks: nextTracks,
502
+ }
503
+ })
504
+ },
505
+
506
+ clearLayerTracks: (layerId) => {
507
+ set((state) => {
508
+ const nextTracks = state.tracks.filter((track) => track.layerId !== layerId)
509
+ const selectedTrackStillExists = nextTracks.some(
510
+ (track) => track.id === state.selectedTrackId,
511
+ )
512
+
513
+ return {
514
+ selectedKeyframeId: selectedTrackStillExists ? state.selectedKeyframeId : null,
515
+ selectedTrackId: selectedTrackStillExists ? state.selectedTrackId : null,
516
+ tracks: nextTracks,
517
+ }
518
+ })
519
+ },
520
+
521
+ pruneTracks: (layers) => {
522
+ const layerById = new Map(layers.map((layer) => [layer.id, layer]))
523
+
524
+ set((state) => {
525
+ const nextTracks = state.tracks.filter((track) => {
526
+ const layer = layerById.get(track.layerId)
527
+
528
+ if (!layer) {
529
+ return false
530
+ }
531
+
532
+ if (track.binding.kind === "layer") {
533
+ return true
534
+ }
535
+
536
+ const definition = getParameterDefinition(getLayerDefinition(layer.type).params, track.binding.key)
537
+
538
+ return Boolean(definition && isAnimatableValueType(definition.type))
539
+ })
540
+
541
+ const selectedTrackStillExists = nextTracks.some(
542
+ (track) => track.id === state.selectedTrackId,
543
+ )
544
+
545
+ return {
546
+ selectedKeyframeId: selectedTrackStillExists ? state.selectedKeyframeId : null,
547
+ selectedTrackId: selectedTrackStillExists ? state.selectedTrackId : null,
548
+ tracks: nextTracks,
549
+ }
550
+ })
551
+ },
552
+
553
+ getTrackForBinding: (layerId, binding) => {
554
+ return (
555
+ get().tracks.find(
556
+ (track) => track.layerId === layerId && bindingEquals(track.binding, binding),
557
+ ) ?? null
558
+ )
559
+ },
560
+
561
+ replaceState: (nextState) => {
562
+ set({
563
+ currentTime: clampTime(nextState.currentTime, nextState.duration),
564
+ duration: clampDuration(nextState.duration),
565
+ isPlaying: nextState.isPlaying,
566
+ loop: nextState.loop,
567
+ selectedKeyframeId: nextState.selectedKeyframeId,
568
+ selectedTrackId: nextState.selectedTrackId,
569
+ tracks: cloneTracks(nextState.tracks),
570
+ })
571
+ },
572
+ }))
@@ -0,0 +1,6 @@
1
+ declare module "*.svg" {
2
+ import type { FC, SVGProps } from "react"
3
+
4
+ const ReactComponent: FC<SVGProps<SVGSVGElement>>
5
+ export default ReactComponent
6
+ }
@@ -0,0 +1,21 @@
1
+ // CSS module type declarations (ambient - no imports allowed)
2
+
3
+ declare module "*.module.css" {
4
+ const classes: { [key: string]: string }
5
+ export default classes
6
+ }
7
+
8
+ declare module "*.module.scss" {
9
+ const classes: { [key: string]: string }
10
+ export default classes
11
+ }
12
+
13
+ declare module "*.module.sass" {
14
+ const classes: { [key: string]: string }
15
+ export default classes
16
+ }
17
+
18
+ // Regular CSS imports
19
+ declare module "*.css"
20
+ declare module "*.scss"
21
+ declare module "*.sass"