@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,28 @@
1
+ export const CUSTOM_SHADER_INTERNAL_VISIBILITY = {
2
+ equals: "__never__",
3
+ key: "__customShaderInternal",
4
+ } as const
5
+
6
+ export const CUSTOM_SHADER_ENTRY_EXPORT = "sketch"
7
+
8
+ export const CUSTOM_SHADER_INTERNAL_KEYS = new Set([
9
+ "entryExport",
10
+ "sourceCode",
11
+ "sourceFileName",
12
+ "sourceMode",
13
+ "sourceRevision",
14
+ ])
15
+
16
+ export const CUSTOM_SHADER_STARTER = `export const sketch = Fn(() => {
17
+ const uv0 = screenAspectUV(screenSize)
18
+ const color = vec3(
19
+ uv0.x.add(0.5),
20
+ uv0.y.add(0.5),
21
+ sin(time).mul(0.5).add(0.5)
22
+ ).toVar()
23
+
24
+ color.assign(technicolorTonemap(color))
25
+
26
+ return color
27
+ })
28
+ `
@@ -0,0 +1,420 @@
1
+ "use client"
2
+
3
+ import {
4
+ browserSupportsWebGPU,
5
+ createWebGPURenderer,
6
+ } from "@/renderer/create-webgpu-renderer"
7
+ import { buildRendererFrame } from "@/renderer/contracts"
8
+ import type { EditorAsset, EditorLayer, Size, TimelineStateSnapshot } from "@/types/editor"
9
+
10
+ export type ExportAspectPreset = "16:9" | "1:1" | "4:5" | "9:16" | "original"
11
+ export type ExportQualityPreset = "draft" | "high" | "standard" | "ultra"
12
+ export type VideoExportFormat = "mp4" | "webm"
13
+
14
+ export const EXPORT_QUALITY_SCALE: Record<ExportQualityPreset, number> = {
15
+ draft: 0.5,
16
+ high: 2,
17
+ standard: 1,
18
+ ultra: 4,
19
+ }
20
+
21
+ export const ASPECT_PRESET_LABELS: Record<ExportAspectPreset, string> = {
22
+ "16:9": "16:9",
23
+ "1:1": "1:1",
24
+ "4:5": "4:5",
25
+ "9:16": "9:16",
26
+ original: "Original",
27
+ }
28
+
29
+ type RenderProjectState = {
30
+ assets: EditorAsset[]
31
+ compositionSize: Size
32
+ layers: EditorLayer[]
33
+ timeline: TimelineStateSnapshot
34
+ }
35
+
36
+ type StillExportOptions = {
37
+ aspectPreset: ExportAspectPreset
38
+ qualityPreset: ExportQualityPreset
39
+ time: number
40
+ type?: string
41
+ width: number
42
+ height: number
43
+ }
44
+
45
+ type VideoExportOptions = {
46
+ aspectPreset: ExportAspectPreset
47
+ duration: number
48
+ format: VideoExportFormat
49
+ fps: number
50
+ qualityPreset: ExportQualityPreset
51
+ startTime: number
52
+ width: number
53
+ height: number
54
+ }
55
+
56
+ function clampDimension(value: number): number {
57
+ if (!Number.isFinite(value)) {
58
+ return 1
59
+ }
60
+
61
+ return Math.max(1, Math.round(value))
62
+ }
63
+
64
+ function getAspectRatio(compositionSize: Size, aspectPreset: ExportAspectPreset): number {
65
+ switch (aspectPreset) {
66
+ case "1:1":
67
+ return 1
68
+ case "4:5":
69
+ return 4 / 5
70
+ case "9:16":
71
+ return 9 / 16
72
+ case "16:9":
73
+ return 16 / 9
74
+ default:
75
+ return compositionSize.width / Math.max(compositionSize.height, 1)
76
+ }
77
+ }
78
+
79
+ export function getAspectRatioForPreset(
80
+ compositionSize: Size,
81
+ aspectPreset: ExportAspectPreset,
82
+ ): number {
83
+ return getAspectRatio(compositionSize, aspectPreset)
84
+ }
85
+
86
+ export function getDimensionsForPreset(
87
+ compositionSize: Size,
88
+ aspectPreset: ExportAspectPreset,
89
+ qualityPreset: ExportQualityPreset,
90
+ ): Size {
91
+ const ratio = getAspectRatio(compositionSize, aspectPreset)
92
+ const longEdge = Math.max(compositionSize.width, compositionSize.height)
93
+ const scaledLongEdge = clampDimension(longEdge * EXPORT_QUALITY_SCALE[qualityPreset])
94
+
95
+ if (ratio >= 1) {
96
+ return {
97
+ height: clampDimension(scaledLongEdge / ratio),
98
+ width: scaledLongEdge,
99
+ }
100
+ }
101
+
102
+ return {
103
+ height: scaledLongEdge,
104
+ width: clampDimension(scaledLongEdge * ratio),
105
+ }
106
+ }
107
+
108
+ export function getSupportedVideoMimeType(format: VideoExportFormat): string | null {
109
+ if (typeof MediaRecorder === "undefined") {
110
+ return null
111
+ }
112
+
113
+ const candidates =
114
+ format === "mp4"
115
+ ? ["video/mp4;codecs=h264", "video/mp4"]
116
+ : ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"]
117
+
118
+ return candidates.find((candidate) => MediaRecorder.isTypeSupported(candidate)) ?? null
119
+ }
120
+
121
+ export async function exportStillImage(
122
+ projectState: RenderProjectState,
123
+ options: StillExportOptions,
124
+ ): Promise<Blob> {
125
+ const renderScale = EXPORT_QUALITY_SCALE[options.qualityPreset]
126
+ const sourceRenderSize = {
127
+ height: clampDimension(projectState.compositionSize.height * renderScale),
128
+ width: clampDimension(projectState.compositionSize.width * renderScale),
129
+ }
130
+
131
+ const renderCanvas = createHiddenRenderCanvas()
132
+ const outputCanvas = document.createElement("canvas")
133
+ outputCanvas.width = clampDimension(options.width)
134
+ outputCanvas.height = clampDimension(options.height)
135
+
136
+ const renderer = await createExportRenderer(renderCanvas)
137
+
138
+ try {
139
+ await prewarmExportFrame(renderer, renderCanvas, projectState, {
140
+ logicalSize: projectState.compositionSize,
141
+ renderSize: sourceRenderSize,
142
+ time: options.time,
143
+ })
144
+
145
+ await renderFrameToCanvas(renderer, renderCanvas, projectState, {
146
+ logicalSize: projectState.compositionSize,
147
+ renderSize: sourceRenderSize,
148
+ time: options.time,
149
+ })
150
+ cropCanvasToAspect(renderCanvas, outputCanvas, options.aspectPreset, projectState.compositionSize)
151
+
152
+ const blob = await canvasToBlob(outputCanvas, options.type ?? "image/png")
153
+
154
+ if (!blob) {
155
+ throw new Error("Could not build the export image.")
156
+ }
157
+
158
+ return blob
159
+ } finally {
160
+ renderer.dispose()
161
+ destroyHiddenRenderCanvas(renderCanvas)
162
+ }
163
+ }
164
+
165
+ export async function exportVideo(
166
+ projectState: RenderProjectState,
167
+ options: VideoExportOptions,
168
+ ): Promise<Blob> {
169
+ const mimeType = getSupportedVideoMimeType(options.format)
170
+
171
+ if (!mimeType) {
172
+ throw new Error(`${options.format.toUpperCase()} export is not supported in this browser.`)
173
+ }
174
+
175
+ const renderScale = EXPORT_QUALITY_SCALE[options.qualityPreset]
176
+ const sourceRenderSize = {
177
+ height: clampDimension(projectState.compositionSize.height * renderScale),
178
+ width: clampDimension(projectState.compositionSize.width * renderScale),
179
+ }
180
+
181
+ const renderCanvas = createHiddenRenderCanvas()
182
+ const outputCanvas = document.createElement("canvas")
183
+ outputCanvas.width = clampDimension(options.width)
184
+ outputCanvas.height = clampDimension(options.height)
185
+
186
+ const renderer = await createExportRenderer(renderCanvas)
187
+ const stream = outputCanvas.captureStream(options.fps)
188
+ const recorder = new MediaRecorder(stream, {
189
+ mimeType,
190
+ videoBitsPerSecond: getVideoBitrate(options.qualityPreset),
191
+ })
192
+ const chunks: BlobPart[] = []
193
+
194
+ recorder.ondataavailable = (event) => {
195
+ if (event.data.size > 0) {
196
+ chunks.push(event.data)
197
+ }
198
+ }
199
+
200
+ const stopPromise = new Promise<Blob>((resolve, reject) => {
201
+ recorder.onerror = () => {
202
+ reject(new Error("The browser failed while encoding the export video."))
203
+ }
204
+ recorder.onstop = () => {
205
+ resolve(new Blob(chunks, { type: mimeType }))
206
+ }
207
+ })
208
+
209
+ try {
210
+ await prewarmExportFrame(renderer, renderCanvas, projectState, {
211
+ logicalSize: projectState.compositionSize,
212
+ renderSize: sourceRenderSize,
213
+ time: options.startTime,
214
+ })
215
+
216
+ recorder.start()
217
+
218
+ const totalFrames = Math.max(1, Math.ceil(options.duration * options.fps))
219
+
220
+ for (let frameIndex = 0; frameIndex < totalFrames; frameIndex += 1) {
221
+ const time = resolveExportTime(
222
+ options.startTime + frameIndex / options.fps,
223
+ projectState.timeline.duration,
224
+ projectState.timeline.loop,
225
+ )
226
+
227
+ await renderFrameToCanvas(renderer, renderCanvas, projectState, {
228
+ logicalSize: projectState.compositionSize,
229
+ renderSize: sourceRenderSize,
230
+ time,
231
+ })
232
+
233
+ cropCanvasToAspect(renderCanvas, outputCanvas, options.aspectPreset, projectState.compositionSize)
234
+ await wait(Math.max(4, 1000 / options.fps))
235
+ }
236
+
237
+ recorder.stop()
238
+ stream.getTracks().forEach((track) => {
239
+ track.stop()
240
+ })
241
+
242
+ return await stopPromise
243
+ } finally {
244
+ renderer.dispose()
245
+ destroyHiddenRenderCanvas(renderCanvas)
246
+ }
247
+ }
248
+
249
+ async function createExportRenderer(canvas: HTMLCanvasElement) {
250
+ if (!browserSupportsWebGPU()) {
251
+ throw new Error("WebGPU export is not available in this browser.")
252
+ }
253
+
254
+ const renderer = await createWebGPURenderer(canvas)
255
+ await renderer.initialize()
256
+ return renderer
257
+ }
258
+
259
+ async function renderFrameToCanvas(
260
+ renderer: Awaited<ReturnType<typeof createExportRenderer>>,
261
+ canvas: HTMLCanvasElement,
262
+ projectState: RenderProjectState,
263
+ options: {
264
+ logicalSize: Size
265
+ renderSize: Size
266
+ time: number
267
+ },
268
+ ): Promise<void> {
269
+ const timelineState = structuredClone(projectState.timeline)
270
+ timelineState.currentTime = resolveExportTime(
271
+ options.time,
272
+ timelineState.duration,
273
+ timelineState.loop,
274
+ )
275
+ timelineState.isPlaying = false
276
+
277
+ canvas.width = options.renderSize.width
278
+ canvas.height = options.renderSize.height
279
+ renderer.resize(options.renderSize, 1)
280
+ renderer.render(
281
+ buildRendererFrame({
282
+ assets: projectState.assets,
283
+ clockTime: timelineState.currentTime,
284
+ delta: 0,
285
+ layers: projectState.layers,
286
+ logicalSize: options.logicalSize,
287
+ outputSize: options.renderSize,
288
+ pixelRatio: 1,
289
+ timeline: timelineState,
290
+ viewportSize: options.renderSize,
291
+ }),
292
+ )
293
+
294
+ await waitForRenderedFrame()
295
+ }
296
+
297
+ async function prewarmExportFrame(
298
+ renderer: Awaited<ReturnType<typeof createExportRenderer>>,
299
+ canvas: HTMLCanvasElement,
300
+ projectState: RenderProjectState,
301
+ options: {
302
+ logicalSize: Size
303
+ renderSize: Size
304
+ time: number
305
+ },
306
+ ): Promise<void> {
307
+ await renderFrameToCanvas(renderer, canvas, projectState, options)
308
+ await wait(48)
309
+ }
310
+
311
+ function cropCanvasToAspect(
312
+ sourceCanvas: HTMLCanvasElement,
313
+ outputCanvas: HTMLCanvasElement,
314
+ aspectPreset: ExportAspectPreset,
315
+ compositionSize: Size,
316
+ ): void {
317
+ const context = outputCanvas.getContext("2d")
318
+
319
+ if (!context) {
320
+ throw new Error("Could not prepare the export canvas.")
321
+ }
322
+
323
+ const targetRatio = getAspectRatio(compositionSize, aspectPreset)
324
+ const sourceRatio = sourceCanvas.width / Math.max(sourceCanvas.height, 1)
325
+ let cropWidth = sourceCanvas.width
326
+ let cropHeight = sourceCanvas.height
327
+ let cropX = 0
328
+ let cropY = 0
329
+
330
+ if (Math.abs(targetRatio - sourceRatio) > 0.0001) {
331
+ if (targetRatio > sourceRatio) {
332
+ cropHeight = Math.round(sourceCanvas.width / targetRatio)
333
+ cropY = Math.round((sourceCanvas.height - cropHeight) / 2)
334
+ } else {
335
+ cropWidth = Math.round(sourceCanvas.height * targetRatio)
336
+ cropX = Math.round((sourceCanvas.width - cropWidth) / 2)
337
+ }
338
+ }
339
+
340
+ context.clearRect(0, 0, outputCanvas.width, outputCanvas.height)
341
+ context.drawImage(
342
+ sourceCanvas,
343
+ cropX,
344
+ cropY,
345
+ cropWidth,
346
+ cropHeight,
347
+ 0,
348
+ 0,
349
+ outputCanvas.width,
350
+ outputCanvas.height,
351
+ )
352
+ }
353
+
354
+ function canvasToBlob(canvas: HTMLCanvasElement, type: string): Promise<Blob | null> {
355
+ return new Promise((resolve) => {
356
+ canvas.toBlob((blob) => resolve(blob), type)
357
+ })
358
+ }
359
+
360
+ function getVideoBitrate(qualityPreset: ExportQualityPreset): number {
361
+ switch (qualityPreset) {
362
+ case "draft":
363
+ return 6_000_000
364
+ case "high":
365
+ return 16_000_000
366
+ case "ultra":
367
+ return 28_000_000
368
+ default:
369
+ return 10_000_000
370
+ }
371
+ }
372
+
373
+ function resolveExportTime(time: number, duration: number, loop: boolean): number {
374
+ if (!(Number.isFinite(time) && Number.isFinite(duration) && duration > 0)) {
375
+ return 0
376
+ }
377
+
378
+ if (loop) {
379
+ const remainder = time % duration
380
+ return remainder >= 0 ? remainder : duration + remainder
381
+ }
382
+
383
+ return Math.max(0, Math.min(duration, time))
384
+ }
385
+
386
+ function wait(durationMs: number): Promise<void> {
387
+ return new Promise((resolve) => {
388
+ window.setTimeout(resolve, durationMs)
389
+ })
390
+ }
391
+
392
+ function waitForRenderedFrame(): Promise<void> {
393
+ return new Promise((resolve) => {
394
+ window.requestAnimationFrame(() => {
395
+ window.requestAnimationFrame(() => {
396
+ resolve()
397
+ })
398
+ })
399
+ })
400
+ }
401
+
402
+ function createHiddenRenderCanvas(): HTMLCanvasElement {
403
+ const canvas = document.createElement("canvas")
404
+ canvas.setAttribute("aria-hidden", "true")
405
+ Object.assign(canvas.style, {
406
+ height: "1px",
407
+ left: "-99999px",
408
+ opacity: "0",
409
+ pointerEvents: "none",
410
+ position: "fixed",
411
+ top: "0",
412
+ width: "1px",
413
+ })
414
+ document.body.append(canvas)
415
+ return canvas
416
+ }
417
+
418
+ function destroyHiddenRenderCanvas(canvas: HTMLCanvasElement): void {
419
+ canvas.remove()
420
+ }
@@ -0,0 +1,71 @@
1
+ import type {
2
+ EditorHistorySnapshot,
3
+ TimelineStateSnapshot,
4
+ } from "@/types/editor"
5
+ import { useLayerStore } from "@/store/layer-store"
6
+ import { useTimelineStore } from "@/store/timeline-store"
7
+
8
+ type HistoryTimelineSnapshot = EditorHistorySnapshot["timeline"]
9
+
10
+ function cloneHistoryTimeline(
11
+ timeline: Pick<
12
+ TimelineStateSnapshot,
13
+ "currentTime" | "duration" | "loop" | "selectedKeyframeId" | "selectedTrackId" | "tracks"
14
+ >,
15
+ ): HistoryTimelineSnapshot {
16
+ return structuredClone({
17
+ currentTime: timeline.currentTime,
18
+ duration: timeline.duration,
19
+ loop: timeline.loop,
20
+ selectedKeyframeId: timeline.selectedKeyframeId,
21
+ selectedTrackId: timeline.selectedTrackId,
22
+ tracks: timeline.tracks,
23
+ })
24
+ }
25
+
26
+ export function buildEditorHistorySnapshotFromState(
27
+ layerState: Pick<
28
+ ReturnType<typeof useLayerStore.getState>,
29
+ "hoveredLayerId" | "layers" | "selectedLayerId"
30
+ >,
31
+ timelineState: Pick<
32
+ TimelineStateSnapshot,
33
+ "currentTime" | "duration" | "loop" | "selectedKeyframeId" | "selectedTrackId" | "tracks"
34
+ >,
35
+ ): EditorHistorySnapshot {
36
+ return {
37
+ hoveredLayerId: layerState.hoveredLayerId,
38
+ layers: structuredClone(layerState.layers),
39
+ selectedLayerId: layerState.selectedLayerId,
40
+ timeline: cloneHistoryTimeline(timelineState),
41
+ }
42
+ }
43
+
44
+ export function buildEditorHistorySnapshot(): EditorHistorySnapshot {
45
+ return buildEditorHistorySnapshotFromState(
46
+ useLayerStore.getState(),
47
+ useTimelineStore.getState(),
48
+ )
49
+ }
50
+
51
+ export function applyEditorHistorySnapshot(snapshot: EditorHistorySnapshot): void {
52
+ useLayerStore
53
+ .getState()
54
+ .replaceState(snapshot.layers, snapshot.selectedLayerId, snapshot.hoveredLayerId)
55
+ useTimelineStore.getState().replaceState({
56
+ currentTime: snapshot.timeline.currentTime,
57
+ duration: snapshot.timeline.duration,
58
+ isPlaying: false,
59
+ loop: snapshot.timeline.loop,
60
+ selectedKeyframeId: snapshot.timeline.selectedKeyframeId,
61
+ selectedTrackId: snapshot.timeline.selectedTrackId,
62
+ tracks: snapshot.timeline.tracks,
63
+ })
64
+ }
65
+
66
+ export function getHistorySnapshotSignature(snapshot: EditorHistorySnapshot): string {
67
+ return JSON.stringify({
68
+ layers: snapshot.layers,
69
+ timeline: snapshot.timeline,
70
+ })
71
+ }
@@ -0,0 +1,76 @@
1
+ import { getLayerDefinition } from "@/lib/editor/config/layer-registry"
2
+ import type {
3
+ EditorLayer,
4
+ LayerKind,
5
+ LayerParameterValues,
6
+ LayerType,
7
+ Size,
8
+ } from "@/types/editor"
9
+ import { buildParameterValues, cloneParameterValues } from "@/lib/editor/parameter-schema"
10
+
11
+ function clamp(value: number, min: number, max: number): number {
12
+ return Math.min(max, Math.max(min, value))
13
+ }
14
+
15
+ export const DEFAULT_CANVAS_SIZE: Size = {
16
+ height: 1080,
17
+ width: 1920,
18
+ }
19
+
20
+ export function getLayerKind(type: LayerType): LayerKind {
21
+ return getLayerDefinition(type).kind
22
+ }
23
+
24
+ export function getDefaultLayerName(type: LayerType, existingCount: number): string {
25
+ const definition = getLayerDefinition(type)
26
+
27
+ return existingCount > 0
28
+ ? `${definition.defaultName} ${existingCount + 1}`
29
+ : definition.defaultName
30
+ }
31
+
32
+ export function createLayer(type: LayerType, existingCount = 0): EditorLayer {
33
+ const definition = getLayerDefinition(type)
34
+
35
+ return {
36
+ assetId: null,
37
+ blendMode: "normal",
38
+ compositeMode: "filter",
39
+ expanded: true,
40
+ hue: 0,
41
+ id: crypto.randomUUID(),
42
+ kind: definition.kind,
43
+ locked: false,
44
+ name: getDefaultLayerName(type, existingCount),
45
+ opacity: 1,
46
+ params: buildParameterValues(definition.params),
47
+ runtimeError: null,
48
+ saturation: 1,
49
+ type,
50
+ visible: true,
51
+ } as EditorLayer
52
+ }
53
+
54
+ export function cloneLayer(layer: EditorLayer): EditorLayer {
55
+ return {
56
+ ...layer,
57
+ id: crypto.randomUUID(),
58
+ name: `${layer.name} Copy`,
59
+ params: cloneParameterValues(layer.params),
60
+ runtimeError: null,
61
+ }
62
+ }
63
+
64
+ export function clampLayerAdjustments(
65
+ params: Pick<EditorLayer, "hue" | "opacity" | "saturation">,
66
+ ): Pick<EditorLayer, "hue" | "opacity" | "saturation"> {
67
+ return {
68
+ hue: clamp(params.hue, -180, 180),
69
+ opacity: clamp(params.opacity, 0, 1),
70
+ saturation: clamp(params.saturation, 0, 2),
71
+ }
72
+ }
73
+
74
+ export function resetLayerParameters(type: LayerType): LayerParameterValues {
75
+ return buildParameterValues(getLayerDefinition(type).params)
76
+ }
@@ -0,0 +1,75 @@
1
+ import type {
2
+ LayerParameterValues,
3
+ ParameterDefinition,
4
+ ParameterDefinitions,
5
+ ParameterValue,
6
+ } from "@/types/editor"
7
+
8
+ export function cloneParameterValue(value: ParameterValue): ParameterValue {
9
+ if (Array.isArray(value)) {
10
+ return [...value] as ParameterValue
11
+ }
12
+
13
+ return value
14
+ }
15
+
16
+ export function cloneParameterValues(values: LayerParameterValues): LayerParameterValues {
17
+ const next: LayerParameterValues = {}
18
+
19
+ for (const [key, value] of Object.entries(values)) {
20
+ next[key] = cloneParameterValue(value)
21
+ }
22
+
23
+ return next
24
+ }
25
+
26
+ export function buildParameterValues(definitions: ParameterDefinitions): LayerParameterValues {
27
+ const values: LayerParameterValues = {}
28
+
29
+ for (const definition of definitions) {
30
+ values[definition.key] = cloneParameterValue(definition.defaultValue)
31
+ }
32
+
33
+ return values
34
+ }
35
+
36
+ export function getParameterDefinition(
37
+ definitions: ParameterDefinitions,
38
+ key: string,
39
+ ): ParameterDefinition | null {
40
+ return definitions.find((definition) => definition.key === key) ?? null
41
+ }
42
+
43
+ export function isParameterAnimatable(definition: ParameterDefinition): boolean {
44
+ if (definition.type === "text") {
45
+ return false
46
+ }
47
+
48
+ return definition.animatable ?? true
49
+ }
50
+
51
+ export function isParameterValueEqual(left: ParameterValue, right: ParameterValue): boolean {
52
+ if (Array.isArray(left) && Array.isArray(right)) {
53
+ return (
54
+ left.length === right.length &&
55
+ left.every((entry, index) => entry === right[index])
56
+ )
57
+ }
58
+
59
+ return left === right
60
+ }
61
+
62
+ export function valueSignature(value: ParameterValue): string {
63
+ if (Array.isArray(value)) {
64
+ return `[${value.join(",")}]`
65
+ }
66
+
67
+ return String(value)
68
+ }
69
+
70
+ export function parameterValuesSignature(values: LayerParameterValues): string {
71
+ return Object.entries(values)
72
+ .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
73
+ .map(([key, value]) => `${key}:${valueSignature(value)}`)
74
+ .join("|")
75
+ }