@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.
- package/.biome/plugins/README.md +21 -0
- package/.biome/plugins/no-anchor-element.grit +12 -0
- package/.biome/plugins/no-relative-parent-imports.grit +10 -0
- package/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
- package/.changeset/README.md +17 -0
- package/.changeset/config.json +11 -0
- package/.editorconfig +40 -0
- package/.env.example +81 -0
- package/.gitattributes +19 -0
- package/.github/workflows/canary.yml +80 -0
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/release.yml +56 -0
- package/.tldrignore +84 -0
- package/.vscode/extensions.json +20 -0
- package/.vscode/settings.json +105 -0
- package/README.md +119 -0
- package/biome.json +249 -0
- package/bun.lock +1224 -0
- package/next.config.ts +131 -0
- package/package.json +73 -0
- package/packages/shader-lab-react/CHANGELOG.md +9 -0
- package/packages/shader-lab-react/README.md +119 -0
- package/packages/shader-lab-react/assets/patterns/bars/1.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/bars/2.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/bars/3.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/bars/4.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/bars/5.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/bars/6.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/candles/1.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/candles/2.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/candles/3.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/candles/4.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/shapes/1.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/shapes/2.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/shapes/3.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/shapes/4.svg +4 -0
- package/packages/shader-lab-react/assets/patterns/shapes/5.svg +3 -0
- package/packages/shader-lab-react/assets/patterns/shapes/6.svg +4 -0
- package/packages/shader-lab-react/assets/textures/blue-noise.png +0 -0
- package/packages/shader-lab-react/package.json +36 -0
- package/packages/shader-lab-react/scripts/fix-esm-specifiers.mjs +57 -0
- package/packages/shader-lab-react/scripts/prepare-dist.mjs +4 -0
- package/packages/shader-lab-react/src/ambient/three-tsl.d.ts +146 -0
- package/packages/shader-lab-react/src/ambient/three-webgpu.d.ts +51 -0
- package/packages/shader-lab-react/src/easings.ts +4 -0
- package/packages/shader-lab-react/src/index.ts +35 -0
- package/packages/shader-lab-react/src/lib/editor/custom-shader/shared.ts +2 -0
- package/packages/shader-lab-react/src/renderer/ascii-atlas.ts +83 -0
- package/packages/shader-lab-react/src/renderer/ascii-pass.ts +416 -0
- package/packages/shader-lab-react/src/renderer/asset-url.ts +3 -0
- package/packages/shader-lab-react/src/renderer/blend-modes.ts +229 -0
- package/packages/shader-lab-react/src/renderer/contracts.ts +54 -0
- package/packages/shader-lab-react/src/renderer/create-webgpu-renderer.ts +48 -0
- package/packages/shader-lab-react/src/renderer/crt-pass.ts +1040 -0
- package/packages/shader-lab-react/src/renderer/custom-shader-pass.ts +108 -0
- package/packages/shader-lab-react/src/renderer/custom-shader-runtime.ts +309 -0
- package/packages/shader-lab-react/src/renderer/dither-textures.ts +99 -0
- package/packages/shader-lab-react/src/renderer/dithering-pass.ts +322 -0
- package/packages/shader-lab-react/src/renderer/gradient-pass.ts +521 -0
- package/packages/shader-lab-react/src/renderer/halftone-pass.ts +932 -0
- package/packages/shader-lab-react/src/renderer/ink-pass.ts +802 -0
- package/packages/shader-lab-react/src/renderer/live-pass.ts +194 -0
- package/packages/shader-lab-react/src/renderer/media-pass.ts +187 -0
- package/packages/shader-lab-react/src/renderer/media-texture.ts +66 -0
- package/packages/shader-lab-react/src/renderer/particle-grid-pass.ts +389 -0
- package/packages/shader-lab-react/src/renderer/pass-node.ts +209 -0
- package/packages/shader-lab-react/src/renderer/pattern-atlas.ts +133 -0
- package/packages/shader-lab-react/src/renderer/pattern-pass.ts +552 -0
- package/packages/shader-lab-react/src/renderer/pipeline-manager.ts +369 -0
- package/packages/shader-lab-react/src/renderer/pixel-sorting-pass.ts +277 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/color/tonemapping.ts +87 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/cosine-palette.ts +9 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/common.ts +31 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/curl-noise-3d.ts +36 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/curl-noise-4d.ts +36 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/fbm.ts +13 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/perlin-noise-3d.ts +96 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/ridge-noise.ts +24 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/simplex-noise-3d.ts +79 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/simplex-noise-4d.ts +89 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/turbulence.ts +56 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/value-noise-3d.ts +32 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/voronoi-noise-3d.ts +60 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/bloom-edge-pattern.ts +15 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/bloom.ts +11 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/canvas-weave-pattern.ts +24 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/grain-texture-pattern.ts +9 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/repeating-pattern.ts +11 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/atan2.ts +9 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-conj.ts +9 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-cos.ts +10 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-div.ts +11 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-log.ts +7 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-mobius.ts +12 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-mul.ts +9 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-pow.ts +16 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-sin.ts +10 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-sqrt.ts +18 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-tan.ts +12 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-to-polar.ts +10 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/hyperbolic.ts +20 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/index.ts +48 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/rotate.ts +15 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/screen-aspect-uv.ts +15 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-box-2d.ts +6 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-diamond.ts +6 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-rhombus.ts +27 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-sphere.ts +6 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/smax.ts +7 -0
- package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/smin.ts +7 -0
- package/packages/shader-lab-react/src/renderer/text-pass.ts +176 -0
- package/packages/shader-lab-react/src/runtime-clock.ts +42 -0
- package/packages/shader-lab-react/src/runtime-frame.ts +29 -0
- package/packages/shader-lab-react/src/shader-lab-composition.tsx +163 -0
- package/packages/shader-lab-react/src/timeline.ts +283 -0
- package/packages/shader-lab-react/src/types/editor.ts +5 -0
- package/packages/shader-lab-react/src/types.ts +141 -0
- package/packages/shader-lab-react/tsconfig.build.json +8 -0
- package/packages/shader-lab-react/tsconfig.json +21 -0
- package/postcss.config.mjs +5 -0
- package/public/assets/fonts/msdf/geist-mono/GeistMono-Regular-msdf-atlas.png +0 -0
- package/public/assets/fonts/msdf/geist-mono/GeistMono-Regular-msdf.json +1412 -0
- package/public/assets/patterns/bars/1.svg +3 -0
- package/public/assets/patterns/bars/2.svg +3 -0
- package/public/assets/patterns/bars/3.svg +3 -0
- package/public/assets/patterns/bars/4.svg +3 -0
- package/public/assets/patterns/bars/5.svg +3 -0
- package/public/assets/patterns/bars/6.svg +3 -0
- package/public/assets/patterns/candles/1.svg +3 -0
- package/public/assets/patterns/candles/2.svg +3 -0
- package/public/assets/patterns/candles/3.svg +3 -0
- package/public/assets/patterns/candles/4.svg +3 -0
- package/public/assets/patterns/shapes/1.svg +3 -0
- package/public/assets/patterns/shapes/2.svg +3 -0
- package/public/assets/patterns/shapes/3.svg +3 -0
- package/public/assets/patterns/shapes/4.svg +4 -0
- package/public/assets/patterns/shapes/5.svg +3 -0
- package/public/assets/patterns/shapes/6.svg +4 -0
- package/public/fonts/geist/Geist-Mono.woff2 +0 -0
- package/public/textures/blue-noise.png +0 -0
- package/public/textures/crt-mask.png +0 -0
- package/src/app/design/page.tsx +398 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +280 -0
- package/src/app/layout.tsx +89 -0
- package/src/app/page.tsx +20 -0
- package/src/app/robots.ts +13 -0
- package/src/app/sitemap.ts +13 -0
- package/src/components/editor/editor-canvas-viewport.tsx +116 -0
- package/src/components/editor/editor-export-dialog.tsx +1177 -0
- package/src/components/editor/editor-timeline-overlay.tsx +983 -0
- package/src/components/editor/editor-topbar.tsx +287 -0
- package/src/components/editor/layer-sidebar.tsx +738 -0
- package/src/components/editor/properties-sidebar-content.tsx +574 -0
- package/src/components/editor/properties-sidebar-fields.tsx +389 -0
- package/src/components/editor/properties-sidebar-utils.ts +178 -0
- package/src/components/editor/properties-sidebar.tsx +421 -0
- package/src/components/ui/button/index.tsx +57 -0
- package/src/components/ui/color-picker/index.tsx +358 -0
- package/src/components/ui/glass-panel/index.tsx +45 -0
- package/src/components/ui/icon-button/index.tsx +46 -0
- package/src/components/ui/select/index.tsx +136 -0
- package/src/components/ui/slider/index.tsx +192 -0
- package/src/components/ui/toggle/index.tsx +34 -0
- package/src/components/ui/typography/index.tsx +61 -0
- package/src/components/ui/xy-pad/index.tsx +160 -0
- package/src/features/editor/components/editor-export-dialog.module.css +271 -0
- package/src/hooks/use-editor-renderer.ts +182 -0
- package/src/lib/app.ts +6 -0
- package/src/lib/cn.ts +7 -0
- package/src/lib/easings.ts +240 -0
- package/src/lib/editor/config/layer-registry.ts +2434 -0
- package/src/lib/editor/custom-shader/shared.ts +28 -0
- package/src/lib/editor/export.ts +420 -0
- package/src/lib/editor/history.ts +71 -0
- package/src/lib/editor/layers.ts +76 -0
- package/src/lib/editor/parameter-schema.ts +75 -0
- package/src/lib/editor/project-file.ts +145 -0
- package/src/lib/editor/shader-export-snippet.ts +37 -0
- package/src/lib/editor/shader-export.ts +315 -0
- package/src/lib/editor/timeline/evaluate.ts +252 -0
- package/src/lib/editor/view-transform.ts +58 -0
- package/src/lib/fonts.ts +28 -0
- package/src/renderer/ascii-atlas.ts +83 -0
- package/src/renderer/ascii-pass.ts +416 -0
- package/src/renderer/blend-modes.ts +229 -0
- package/src/renderer/contracts.ts +161 -0
- package/src/renderer/create-webgpu-renderer.ts +48 -0
- package/src/renderer/crt-pass.ts +1040 -0
- package/src/renderer/custom-shader-pass.ts +117 -0
- package/src/renderer/custom-shader-runtime.ts +309 -0
- package/src/renderer/dither-textures.ts +99 -0
- package/src/renderer/dithering-pass.ts +322 -0
- package/src/renderer/gradient-pass.ts +520 -0
- package/src/renderer/halftone-pass.ts +932 -0
- package/src/renderer/ink-pass.ts +683 -0
- package/src/renderer/live-pass.ts +194 -0
- package/src/renderer/media-pass.ts +187 -0
- package/src/renderer/media-texture.ts +66 -0
- package/src/renderer/particle-grid-pass.ts +389 -0
- package/src/renderer/pass-node-factory.ts +33 -0
- package/src/renderer/pass-node.ts +209 -0
- package/src/renderer/pattern-atlas.ts +97 -0
- package/src/renderer/pattern-pass.ts +552 -0
- package/src/renderer/pipeline-manager.ts +343 -0
- package/src/renderer/pixel-sorting-pass.ts +277 -0
- package/src/renderer/project-clock.ts +57 -0
- package/src/renderer/shaders/tsl/color/tonemapping.ts +86 -0
- package/src/renderer/shaders/tsl/cosine-palette.ts +8 -0
- package/src/renderer/shaders/tsl/noise/common.ts +30 -0
- package/src/renderer/shaders/tsl/noise/curl-noise-3d.ts +35 -0
- package/src/renderer/shaders/tsl/noise/curl-noise-4d.ts +35 -0
- package/src/renderer/shaders/tsl/noise/fbm.ts +12 -0
- package/src/renderer/shaders/tsl/noise/perlin-noise-3d.ts +97 -0
- package/src/renderer/shaders/tsl/noise/ridge-noise.ts +23 -0
- package/src/renderer/shaders/tsl/noise/simplex-noise-3d.ts +78 -0
- package/src/renderer/shaders/tsl/noise/simplex-noise-4d.ts +88 -0
- package/src/renderer/shaders/tsl/noise/turbulence.ts +55 -0
- package/src/renderer/shaders/tsl/noise/value-noise-3d.ts +31 -0
- package/src/renderer/shaders/tsl/noise/voronoi-noise-3d.ts +59 -0
- package/src/renderer/shaders/tsl/patterns/bloom-edge-pattern.ts +14 -0
- package/src/renderer/shaders/tsl/patterns/bloom.ts +10 -0
- package/src/renderer/shaders/tsl/patterns/canvas-weave-pattern.ts +23 -0
- package/src/renderer/shaders/tsl/patterns/grain-texture-pattern.ts +8 -0
- package/src/renderer/shaders/tsl/patterns/repeating-pattern.ts +10 -0
- package/src/renderer/shaders/tsl/utils/atan2.ts +8 -0
- package/src/renderer/shaders/tsl/utils/complex-conj.ts +8 -0
- package/src/renderer/shaders/tsl/utils/complex-cos.ts +9 -0
- package/src/renderer/shaders/tsl/utils/complex-div.ts +10 -0
- package/src/renderer/shaders/tsl/utils/complex-log.ts +6 -0
- package/src/renderer/shaders/tsl/utils/complex-mobius.ts +11 -0
- package/src/renderer/shaders/tsl/utils/complex-mul.ts +8 -0
- package/src/renderer/shaders/tsl/utils/complex-pow.ts +15 -0
- package/src/renderer/shaders/tsl/utils/complex-sin.ts +9 -0
- package/src/renderer/shaders/tsl/utils/complex-sqrt.ts +17 -0
- package/src/renderer/shaders/tsl/utils/complex-tan.ts +11 -0
- package/src/renderer/shaders/tsl/utils/complex-to-polar.ts +9 -0
- package/src/renderer/shaders/tsl/utils/hyperbolic.ts +19 -0
- package/src/renderer/shaders/tsl/utils/index.ts +47 -0
- package/src/renderer/shaders/tsl/utils/rotate.ts +14 -0
- package/src/renderer/shaders/tsl/utils/screen-aspect-uv.ts +14 -0
- package/src/renderer/shaders/tsl/utils/sd-box-2d.ts +5 -0
- package/src/renderer/shaders/tsl/utils/sd-diamond.ts +5 -0
- package/src/renderer/shaders/tsl/utils/sd-rhombus.ts +26 -0
- package/src/renderer/shaders/tsl/utils/sd-sphere.ts +5 -0
- package/src/renderer/shaders/tsl/utils/smax.ts +7 -0
- package/src/renderer/shaders/tsl/utils/smin.ts +6 -0
- package/src/renderer/text-pass.ts +176 -0
- package/src/store/asset-store.ts +193 -0
- package/src/store/editor-store.ts +223 -0
- package/src/store/history-store.ts +172 -0
- package/src/store/index.ts +31 -0
- package/src/store/layer-store.ts +675 -0
- package/src/store/timeline-store.ts +572 -0
- package/src/types/assets.d.ts +6 -0
- package/src/types/css.d.ts +21 -0
- package/src/types/editor.ts +357 -0
- package/src/types/react.d.ts +15 -0
- package/src/types/three-tsl.d.ts +146 -0
- package/src/types/three-webgpu.d.ts +51 -0
- 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
|
+
}
|