@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,343 @@
1
+ import { float, type TSLNode, texture as tslTexture, uv, vec2 } from "three/tsl"
2
+ import * as THREE from "three/webgpu"
3
+ import type { RenderableLayerPass } from "@/renderer/contracts"
4
+ import { CustomShaderPass } from "@/renderer/custom-shader-pass"
5
+ import { GradientPass } from "@/renderer/gradient-pass"
6
+ import { LivePass } from "@/renderer/live-pass"
7
+ import { MediaPass } from "@/renderer/media-pass"
8
+ import type { PassNode } from "@/renderer/pass-node"
9
+ import { createPassNode } from "@/renderer/pass-node-factory"
10
+ import { TextPass } from "@/renderer/text-pass"
11
+ import type { EditorLayer, Size } from "@/types/editor"
12
+ import { parameterValuesSignature } from "@/lib/editor/parameter-schema"
13
+
14
+ type LayerPassNode = LivePass | MediaPass | PassNode
15
+
16
+ const RENDER_TARGET_OPTIONS = {
17
+ depthBuffer: false,
18
+ format: THREE.RGBAFormat,
19
+ generateMipmaps: false,
20
+ magFilter: THREE.NearestFilter,
21
+ minFilter: THREE.NearestFilter,
22
+ stencilBuffer: false,
23
+ type: THREE.HalfFloatType,
24
+ } as const
25
+
26
+ function clampUnit(value: number): number {
27
+ return Math.max(0, Math.min(1, value))
28
+ }
29
+
30
+ function createLayerSignature(layer: RenderableLayerPass): string {
31
+ if (layer.layer.type === "custom-shader") {
32
+ return [
33
+ layer.layer.id,
34
+ layer.layer.kind,
35
+ layer.layer.type,
36
+ layer.layer.visible ? "1" : "0",
37
+ layer.layer.opacity.toFixed(4),
38
+ layer.layer.hue.toFixed(4),
39
+ layer.layer.saturation.toFixed(4),
40
+ layer.layer.blendMode,
41
+ layer.layer.compositeMode,
42
+ typeof layer.params.sourceRevision === "number"
43
+ ? String(layer.params.sourceRevision)
44
+ : "0",
45
+ typeof layer.params.sourceMode === "string"
46
+ ? layer.params.sourceMode
47
+ : "paste",
48
+ typeof layer.params.entryExport === "string"
49
+ ? layer.params.entryExport
50
+ : "sketch",
51
+ typeof layer.params.sourceFileName === "string"
52
+ ? layer.params.sourceFileName
53
+ : "",
54
+ ].join("|")
55
+ }
56
+
57
+ return [
58
+ layer.layer.id,
59
+ layer.layer.kind,
60
+ layer.layer.type,
61
+ layer.asset?.id ?? "no-asset",
62
+ layer.asset?.url ?? "no-url",
63
+ layer.layer.visible ? "1" : "0",
64
+ layer.layer.opacity.toFixed(4),
65
+ layer.layer.hue.toFixed(4),
66
+ layer.layer.saturation.toFixed(4),
67
+ layer.layer.blendMode,
68
+ layer.layer.compositeMode,
69
+ parameterValuesSignature(layer.params),
70
+ ].join("|")
71
+ }
72
+
73
+ export class PipelineManager {
74
+ private readonly renderer: THREE.WebGPURenderer
75
+ private readonly baseScene: THREE.Scene
76
+ private readonly baseCamera: THREE.OrthographicCamera
77
+ private readonly blitScene: THREE.Scene
78
+ private readonly blitCamera: THREE.OrthographicCamera
79
+ private readonly blitInputNode: TSLNode
80
+ private readonly blitMaterial: THREE.MeshBasicNodeMaterial
81
+
82
+ private passMap = new Map<string, LayerPassNode>()
83
+ private passes: LayerPassNode[] = []
84
+ private layerSignatures = new Map<string, string>()
85
+ private dirty = true
86
+ private width: number
87
+ private height: number
88
+ private logicalWidth: number
89
+ private logicalHeight: number
90
+ private rtA: THREE.WebGLRenderTarget
91
+ private rtB: THREE.WebGLRenderTarget
92
+
93
+ constructor(renderer: THREE.WebGPURenderer, size: Size) {
94
+ this.renderer = renderer
95
+ this.width = Math.max(1, size.width)
96
+ this.height = Math.max(1, size.height)
97
+ this.logicalWidth = this.width
98
+ this.logicalHeight = this.height
99
+
100
+ this.baseScene = new THREE.Scene()
101
+ this.baseCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
102
+ const baseMaterial = new THREE.MeshBasicMaterial({ color: "#080808" })
103
+ const baseMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), baseMaterial)
104
+ baseMesh.frustumCulled = false
105
+ this.baseScene.add(baseMesh)
106
+
107
+ this.rtA = new THREE.WebGLRenderTarget(
108
+ this.width,
109
+ this.height,
110
+ RENDER_TARGET_OPTIONS
111
+ )
112
+ this.rtB = new THREE.WebGLRenderTarget(
113
+ this.width,
114
+ this.height,
115
+ RENDER_TARGET_OPTIONS
116
+ )
117
+
118
+ this.blitScene = new THREE.Scene()
119
+ this.blitCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
120
+ const blitUv = vec2(uv().x, float(1).sub(uv().y))
121
+ this.blitInputNode = tslTexture(new THREE.Texture(), blitUv)
122
+ this.blitMaterial = new THREE.MeshBasicNodeMaterial()
123
+ this.blitMaterial.colorNode = this.blitInputNode
124
+ const blitMesh = new THREE.Mesh(
125
+ new THREE.PlaneGeometry(2, 2),
126
+ this.blitMaterial
127
+ )
128
+ blitMesh.frustumCulled = false
129
+ this.blitScene.add(blitMesh)
130
+ }
131
+
132
+ syncLayers(layers: RenderableLayerPass[]): void {
133
+ const incomingIds = new Set(layers.map((layer) => layer.layer.id))
134
+
135
+ for (const [layerId, pass] of this.passMap) {
136
+ if (incomingIds.has(layerId)) {
137
+ continue
138
+ }
139
+
140
+ pass.dispose()
141
+ this.passMap.delete(layerId)
142
+ this.layerSignatures.delete(layerId)
143
+ this.dirty = true
144
+ }
145
+
146
+ const orderedPasses: LayerPassNode[] = []
147
+
148
+ for (const renderableLayer of layers) {
149
+ const layerId = renderableLayer.layer.id
150
+ const signature = createLayerSignature(renderableLayer)
151
+ let pass = this.passMap.get(layerId)
152
+
153
+ if (!pass) {
154
+ pass = this.createPass(renderableLayer.layer)
155
+ pass.resize(this.width, this.height)
156
+ pass.updateLogicalSize(this.logicalWidth, this.logicalHeight)
157
+ this.passMap.set(layerId, pass)
158
+ this.dirty = true
159
+ }
160
+
161
+ if (this.layerSignatures.get(layerId) !== signature) {
162
+ this.layerSignatures.set(layerId, signature)
163
+ this.applyLayerState(pass, renderableLayer)
164
+ this.dirty = true
165
+ }
166
+
167
+ orderedPasses.push(pass)
168
+ }
169
+
170
+ if (
171
+ orderedPasses.length !== this.passes.length ||
172
+ orderedPasses.some((pass, index) => this.passes[index] !== pass)
173
+ ) {
174
+ this.passes = orderedPasses
175
+ this.dirty = true
176
+ }
177
+ }
178
+
179
+ render(time: number, delta: number): boolean {
180
+ const activePasses = this.passes.filter((pass) => pass.enabled)
181
+ const needsContinuousRender = activePasses.some((pass) =>
182
+ pass.needsContinuousRender()
183
+ )
184
+
185
+ if (!(this.dirty || needsContinuousRender)) {
186
+ return false
187
+ }
188
+
189
+ if (activePasses.length === 0) {
190
+ this.renderer.setRenderTarget(null)
191
+ this.renderer.render(this.baseScene, this.baseCamera)
192
+ this.dirty = false
193
+ return true
194
+ }
195
+
196
+ this.renderer.setRenderTarget(this.rtA)
197
+ this.renderer.render(this.baseScene, this.baseCamera)
198
+
199
+ let readTarget = this.rtA
200
+ let writeTarget = this.rtB
201
+
202
+ for (const pass of activePasses) {
203
+ pass.render(this.renderer, readTarget.texture, writeTarget, time, delta)
204
+ const previousRead = readTarget
205
+ readTarget = writeTarget
206
+ writeTarget = previousRead
207
+ }
208
+
209
+ this.blitInputNode.value = readTarget.texture
210
+ this.renderer.setRenderTarget(null)
211
+ this.renderer.render(this.blitScene, this.blitCamera)
212
+ this.dirty = false
213
+ return true
214
+ }
215
+
216
+ resize(size: Size): void {
217
+ this.width = Math.max(1, size.width)
218
+ this.height = Math.max(1, size.height)
219
+ this.rtA.setSize(this.width, this.height)
220
+ this.rtB.setSize(this.width, this.height)
221
+
222
+ for (const pass of this.passMap.values()) {
223
+ pass.resize(this.width, this.height)
224
+ }
225
+
226
+ this.dirty = true
227
+ }
228
+
229
+ updateLogicalSize(size: Size): void {
230
+ const nextWidth = Math.max(1, size.width)
231
+ const nextHeight = Math.max(1, size.height)
232
+
233
+ if (nextWidth === this.logicalWidth && nextHeight === this.logicalHeight) {
234
+ return
235
+ }
236
+
237
+ this.logicalWidth = nextWidth
238
+ this.logicalHeight = nextHeight
239
+
240
+ for (const pass of this.passMap.values()) {
241
+ pass.updateLogicalSize(this.logicalWidth, this.logicalHeight)
242
+ }
243
+
244
+ this.dirty = true
245
+ }
246
+
247
+ dispose(): void {
248
+ this.rtA.dispose()
249
+ this.rtB.dispose()
250
+ this.blitMaterial.dispose()
251
+
252
+ for (const pass of this.passMap.values()) {
253
+ pass.dispose()
254
+ }
255
+
256
+ this.passMap.clear()
257
+ this.passes = []
258
+ this.layerSignatures.clear()
259
+ }
260
+
261
+ private applyLayerState(
262
+ pass: LayerPassNode,
263
+ renderableLayer: RenderableLayerPass
264
+ ): void {
265
+ pass.enabled = renderableLayer.layer.visible
266
+ pass.updateOpacity(clampUnit(renderableLayer.layer.opacity))
267
+ pass.updateBlendMode(renderableLayer.layer.blendMode)
268
+ pass.updateCompositeMode(renderableLayer.layer.compositeMode)
269
+ pass.updateLayerColorAdjustments(
270
+ renderableLayer.layer.hue,
271
+ renderableLayer.layer.saturation
272
+ )
273
+ pass.updateParams(renderableLayer.params)
274
+
275
+ if (pass instanceof MediaPass) {
276
+ const asset = renderableLayer.asset
277
+ if (asset?.kind === "image" || asset?.kind === "video") {
278
+ void pass
279
+ .setMedia(asset.url, asset.kind)
280
+ .then(() => {
281
+ this.dirty = true
282
+ })
283
+ .catch(() => {
284
+ this.dirty = true
285
+ })
286
+ } else {
287
+ pass.clearMedia()
288
+ }
289
+ }
290
+
291
+ if (pass instanceof LivePass) {
292
+ const facingMode =
293
+ typeof renderableLayer.params.facingMode === "string"
294
+ ? renderableLayer.params.facingMode
295
+ : "user"
296
+
297
+ if (
298
+ facingMode !== pass.getFacingMode() ||
299
+ !pass.needsContinuousRender()
300
+ ) {
301
+ void pass
302
+ .startCamera(facingMode)
303
+ .then(() => {
304
+ this.dirty = true
305
+ })
306
+ .catch(() => {
307
+ this.dirty = true
308
+ })
309
+ }
310
+ }
311
+ }
312
+
313
+ private createPass(layer: EditorLayer): LayerPassNode {
314
+ if (layer.kind === "effect") {
315
+ return createPassNode(layer.id, layer.type)
316
+ }
317
+
318
+ if (
319
+ layer.kind === "source" &&
320
+ (layer.type === "image" || layer.type === "video")
321
+ ) {
322
+ return new MediaPass(layer.id)
323
+ }
324
+
325
+ if (layer.kind === "source" && layer.type === "gradient") {
326
+ return new GradientPass(layer.id)
327
+ }
328
+
329
+ if (layer.kind === "source" && layer.type === "text") {
330
+ return new TextPass(layer.id)
331
+ }
332
+
333
+ if (layer.kind === "source" && layer.type === "custom-shader") {
334
+ return new CustomShaderPass(layer.id)
335
+ }
336
+
337
+ if (layer.kind === "source" && layer.type === "live") {
338
+ return new LivePass(layer.id)
339
+ }
340
+
341
+ throw new Error(`Unsupported layer type in current scope: ${layer.type}`)
342
+ }
343
+ }
@@ -0,0 +1,277 @@
1
+ import * as THREE from "three/webgpu"
2
+ import {
3
+ dot,
4
+ float,
5
+ floor,
6
+ max,
7
+ min,
8
+ mod,
9
+ select,
10
+ texture as tslTexture,
11
+ type TSLNode,
12
+ uniform,
13
+ uv,
14
+ vec2,
15
+ vec3,
16
+ vec4,
17
+ } from "three/tsl"
18
+ import { PassNode } from "@/renderer/pass-node"
19
+ import type { LayerParameterValues } from "@/types/editor"
20
+
21
+ type Node = TSLNode
22
+
23
+ export class PixelSortingPass extends PassNode {
24
+ private readonly sortScene: THREE.Scene
25
+ private readonly sortCamera: THREE.OrthographicCamera
26
+ private readonly sortMaterial: THREE.MeshBasicNodeMaterial
27
+ private sortRtA: THREE.WebGLRenderTarget
28
+ private sortRtB: THREE.WebGLRenderTarget
29
+
30
+ private readonly blitInputNode: Node
31
+ private readonly sortTexNodeA: Node
32
+ private readonly sortTexNodeB: Node
33
+
34
+ private readonly passOffsetUniform: Node
35
+ private readonly widthUniform: Node
36
+ private readonly heightUniform: Node
37
+ private readonly thresholdUniform: Node
38
+ private readonly upperThresholdUniform: Node
39
+ private readonly directionUniform: Node
40
+ private readonly modeUniform: Node
41
+ private readonly reverseUniform: Node
42
+
43
+ private passCount = 150
44
+ private width = 1
45
+ private height = 1
46
+
47
+ constructor(layerId: string) {
48
+ super(layerId)
49
+
50
+ this.sortScene = new THREE.Scene()
51
+ this.sortCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
52
+
53
+ this.passOffsetUniform = uniform(0)
54
+ this.widthUniform = uniform(1)
55
+ this.heightUniform = uniform(1)
56
+ this.thresholdUniform = uniform(0.25)
57
+ this.upperThresholdUniform = uniform(1)
58
+ this.directionUniform = uniform(0)
59
+ this.modeUniform = uniform(0)
60
+ this.reverseUniform = uniform(0)
61
+
62
+ const placeholder = new THREE.Texture()
63
+
64
+ // Compute pixel coordinates from flipped UVs
65
+ const texUv = vec2(uv().x, float(1).sub(uv().y))
66
+ const dims = vec2(this.widthUniform, this.heightUniform)
67
+ const pixelCoord = floor(texUv.mul(dims))
68
+
69
+ // Sort axis: horizontal (x) or vertical (y)
70
+ const isHorizontal = this.directionUniform.lessThan(float(0.5))
71
+ const sortIdx = select(isHorizontal, pixelCoord.x, pixelCoord.y)
72
+ const maxIdx = select(isHorizontal, this.widthUniform, this.heightUniform)
73
+
74
+ // Odd-even transposition: alternate pair groupings each pass
75
+ const pairMod = mod(sortIdx.add(this.passOffsetUniform), float(2))
76
+ const isLeft = pairMod.lessThan(float(1))
77
+
78
+ // Neighbor pixel coordinate
79
+ const neighborDir = select(isLeft, float(1), float(-1))
80
+ const neighborCoord = vec2(
81
+ select(isHorizontal, pixelCoord.x.add(neighborDir), pixelCoord.x),
82
+ select(isHorizontal, pixelCoord.y, pixelCoord.y.add(neighborDir)),
83
+ )
84
+
85
+ // Bounds check
86
+ const neighborIdx = select(isHorizontal, neighborCoord.x, neighborCoord.y)
87
+ const inBounds = neighborIdx
88
+ .greaterThanEqual(float(0))
89
+ .and(neighborIdx.lessThan(maxIdx))
90
+
91
+ // Sample both pixels (snapped to texel centers)
92
+ const mySnapUv = pixelCoord.add(0.5).div(dims)
93
+ const neighborSnapUv = neighborCoord.add(0.5).div(dims)
94
+
95
+ this.sortTexNodeA = tslTexture(placeholder, mySnapUv)
96
+ this.sortTexNodeB = tslTexture(placeholder, neighborSnapUv)
97
+
98
+ const myColor = this.sortTexNodeA
99
+ const neighborColor = this.sortTexNodeB
100
+
101
+ // Sort value: luminance
102
+ const lumaW = vec3(0.2126, 0.7152, 0.0722)
103
+ const myLuma = dot(vec3(myColor.r, myColor.g, myColor.b), lumaW)
104
+ const neighborLuma = dot(
105
+ vec3(neighborColor.r, neighborColor.g, neighborColor.b),
106
+ lumaW,
107
+ )
108
+
109
+ // Sort value: warmth (R-B, approximates hue/temperature)
110
+ const myWarmth = myColor.r.sub(myColor.b)
111
+ const neighborWarmth = neighborColor.r.sub(neighborColor.b)
112
+
113
+ // Sort value: saturation (HSV)
114
+ const myMax = max(max(myColor.r, myColor.g), myColor.b)
115
+ const myMin = min(min(myColor.r, myColor.g), myColor.b)
116
+ const mySat = select(
117
+ myMax.greaterThan(float(0.001)),
118
+ myMax.sub(myMin).div(myMax),
119
+ float(0),
120
+ )
121
+ const nMax = max(max(neighborColor.r, neighborColor.g), neighborColor.b)
122
+ const nMin = min(min(neighborColor.r, neighborColor.g), neighborColor.b)
123
+ const nSat = select(
124
+ nMax.greaterThan(float(0.001)),
125
+ nMax.sub(nMin).div(nMax),
126
+ float(0),
127
+ )
128
+
129
+ // Select sort value based on mode (0=luma, 1=hue/warmth, 2=saturation)
130
+ const isSatMode = this.modeUniform.greaterThan(float(1.5))
131
+ const isHueMode = this.modeUniform.greaterThan(float(0.5))
132
+ const myValue = select(isSatMode, mySat, select(isHueMode, myWarmth, myLuma))
133
+ const neighborValue = select(
134
+ isSatMode,
135
+ nSat,
136
+ select(isHueMode, neighborWarmth, neighborLuma),
137
+ )
138
+
139
+ // Validation: both pixels must be within threshold band (luma-based)
140
+ const myInBand = myLuma
141
+ .greaterThan(this.thresholdUniform)
142
+ .and(myLuma.lessThan(this.upperThresholdUniform))
143
+ const neighborInBand = neighborLuma
144
+ .greaterThan(this.thresholdUniform)
145
+ .and(neighborLuma.lessThan(this.upperThresholdUniform))
146
+ const valid = myInBand.or(neighborInBand)
147
+
148
+ // Sort: swap if left > right (ascending) or left < right (descending/reverse)
149
+ const leftValue = select(isLeft, myValue, neighborValue)
150
+ const rightValue = select(isLeft, neighborValue, myValue)
151
+ const isReverse = this.reverseUniform.greaterThan(float(0.5))
152
+ const shouldSwap = select(
153
+ isReverse,
154
+ rightValue.greaterThan(leftValue),
155
+ leftValue.greaterThan(rightValue),
156
+ )
157
+
158
+ const doSwap = inBounds.and(valid).and(shouldSwap)
159
+ const result = vec4(
160
+ select(doSwap, neighborColor.r, myColor.r),
161
+ select(doSwap, neighborColor.g, myColor.g),
162
+ select(doSwap, neighborColor.b, myColor.b),
163
+ float(1),
164
+ )
165
+
166
+ this.sortMaterial = new THREE.MeshBasicNodeMaterial()
167
+ this.sortMaterial.colorNode = result as Node
168
+
169
+ const sortMesh = new THREE.Mesh(
170
+ new THREE.PlaneGeometry(2, 2),
171
+ this.sortMaterial,
172
+ )
173
+ sortMesh.frustumCulled = false
174
+ this.sortScene.add(sortMesh)
175
+
176
+ // Internal ping-pong render targets
177
+ const rtOptions = {
178
+ depthBuffer: false,
179
+ format: THREE.RGBAFormat,
180
+ generateMipmaps: false,
181
+ magFilter: THREE.NearestFilter,
182
+ minFilter: THREE.NearestFilter,
183
+ stencilBuffer: false,
184
+ type: THREE.HalfFloatType,
185
+ }
186
+ this.sortRtA = new THREE.WebGLRenderTarget(1, 1, rtOptions)
187
+ this.sortRtB = new THREE.WebGLRenderTarget(1, 1, rtOptions)
188
+
189
+ // Blit node for PassNode pipeline
190
+ const blitUv = vec2(uv().x, float(1).sub(uv().y))
191
+ this.blitInputNode = tslTexture(new THREE.Texture(), blitUv)
192
+
193
+ this.rebuildEffectNode()
194
+ }
195
+
196
+ override render(
197
+ renderer: THREE.WebGPURenderer,
198
+ inputTexture: THREE.Texture,
199
+ outputTarget: THREE.WebGLRenderTarget,
200
+ time: number,
201
+ delta: number,
202
+ ): void {
203
+ let readTexture: THREE.Texture = inputTexture
204
+ let writeTarget = this.sortRtA
205
+
206
+ for (let i = 0; i < this.passCount; i++) {
207
+ this.passOffsetUniform.value = i % 2
208
+ this.sortTexNodeA.value = readTexture
209
+ this.sortTexNodeB.value = readTexture
210
+ renderer.setRenderTarget(writeTarget)
211
+ renderer.render(this.sortScene, this.sortCamera)
212
+
213
+ if (writeTarget === this.sortRtA) {
214
+ readTexture = this.sortRtA.texture
215
+ writeTarget = this.sortRtB
216
+ } else {
217
+ readTexture = this.sortRtB.texture
218
+ writeTarget = this.sortRtA
219
+ }
220
+ }
221
+
222
+ this.blitInputNode.value = readTexture
223
+ super.render(renderer, inputTexture, outputTarget, time, delta)
224
+ }
225
+
226
+ override updateParams(params: LayerParameterValues): void {
227
+ this.thresholdUniform.value =
228
+ typeof params.threshold === "number" ? params.threshold : 0.25
229
+
230
+ this.upperThresholdUniform.value =
231
+ typeof params.upperThreshold === "number" ? params.upperThreshold : 1
232
+
233
+ this.directionUniform.value = params.direction === "vertical" ? 1 : 0
234
+
235
+ this.reverseUniform.value = params.reverse === true ? 1 : 0
236
+
237
+ if (params.mode === "hue") {
238
+ this.modeUniform.value = 1
239
+ } else if (params.mode === "saturation") {
240
+ this.modeUniform.value = 2
241
+ } else {
242
+ this.modeUniform.value = 0
243
+ }
244
+
245
+ const range = typeof params.range === "number" ? params.range : 0.3
246
+ this.passCount = Math.max(1, Math.round(range * 300))
247
+ }
248
+
249
+ override resize(width: number, height: number): void {
250
+ this.width = Math.max(1, width)
251
+ this.height = Math.max(1, height)
252
+ this.widthUniform.value = this.width
253
+ this.heightUniform.value = this.height
254
+ this.sortRtA.setSize(this.width, this.height)
255
+ this.sortRtB.setSize(this.width, this.height)
256
+ }
257
+
258
+ override dispose(): void {
259
+ this.sortRtA.dispose()
260
+ this.sortRtB.dispose()
261
+ this.sortMaterial.dispose()
262
+ super.dispose()
263
+ }
264
+
265
+ protected override buildEffectNode(): Node {
266
+ if (!this.blitInputNode) {
267
+ return this.inputNode
268
+ }
269
+
270
+ return vec4(
271
+ this.blitInputNode.r,
272
+ this.blitInputNode.g,
273
+ this.blitInputNode.b,
274
+ float(1),
275
+ )
276
+ }
277
+ }
@@ -0,0 +1,57 @@
1
+ import type { ProjectClock } from "@/renderer/contracts"
2
+ import type { TimelineStateSnapshot } from "@/types/editor"
3
+
4
+ export type TimelineClockState = Pick<
5
+ TimelineStateSnapshot,
6
+ "currentTime" | "duration" | "isPlaying" | "loop"
7
+ >
8
+
9
+ const MIN_DURATION = 0.25
10
+
11
+ export function createProjectClock(
12
+ timeline: TimelineClockState,
13
+ delta: number,
14
+ explicitTime?: number,
15
+ ): ProjectClock {
16
+ return {
17
+ delta,
18
+ duration: timeline.duration,
19
+ isPlaying: timeline.isPlaying,
20
+ loop: timeline.loop,
21
+ time: typeof explicitTime === "number" ? explicitTime : timeline.currentTime,
22
+ }
23
+ }
24
+
25
+ export function advanceProjectTimeline(
26
+ timeline: TimelineClockState,
27
+ delta: number,
28
+ ): Pick<TimelineClockState, "currentTime" | "isPlaying"> {
29
+ if (!Number.isFinite(delta) || delta <= 0 || !timeline.isPlaying) {
30
+ return {
31
+ currentTime: timeline.currentTime,
32
+ isPlaying: timeline.isPlaying,
33
+ }
34
+ }
35
+
36
+ const duration = Math.max(timeline.duration, MIN_DURATION)
37
+ const nextTime = timeline.currentTime + delta
38
+
39
+ if (timeline.loop) {
40
+ return {
41
+ currentTime: nextTime % duration,
42
+ isPlaying: timeline.isPlaying,
43
+ }
44
+ }
45
+
46
+ if (nextTime >= duration) {
47
+ return {
48
+ currentTime: duration,
49
+ isPlaying: false,
50
+ }
51
+ }
52
+
53
+ return {
54
+ currentTime: nextTime,
55
+ isPlaying: true,
56
+ }
57
+ }