From c6863f2bfcdf7a00e6f3a34235e3a9fa2d64c59c Mon Sep 17 00:00:00 2001 From: th Date: Sun, 19 Apr 2026 17:53:36 +0200 Subject: [PATCH] init --- .gitignore | 8 + .../Screenshot from 2026-04-19 15-34-54.png | Bin 0 -> 18728 bytes docker-compose.yml | 10 + index.html | 12 + package-lock.json | 1636 +++++++++++++++++ package.json | 26 + server/.dockerignore | 4 + server/Dockerfile | 20 + server/package.json | 16 + server/render.html | 144 ++ server/server.js | 198 ++ src/App.vue | 262 +++ src/components/AssetSidebar.vue | 275 +++ src/components/CanvasStage.vue | 581 ++++++ src/components/DocumentSettings.vue | 540 ++++++ src/components/ExportDialog.vue | 460 +++++ src/components/PropertiesPanel.vue | 288 +++ src/components/RangeSlider.vue | 228 +++ src/components/Timeline.vue | 839 +++++++++ src/composables/usePlaybackLoop.ts | 23 + src/composables/useTheme.ts | 35 + src/lib/exportPlayer.ts | 326 ++++ src/lib/fileLoader.ts | 53 + src/lib/interpolate.ts | 75 + src/lib/render.ts | 86 + src/lib/serverExport.ts | 119 ++ src/lib/videoExport.ts | 135 ++ src/main.ts | 11 + src/stores/scene.ts | 227 +++ src/styles/global.css | 140 ++ src/types/scene.ts | 90 + src/vite-env.d.ts | 7 + tsconfig.json | 23 + vite.config.ts | 12 + 34 files changed, 6909 insertions(+) create mode 100644 .gitignore create mode 100644 animatr_reference/Screenshot from 2026-04-19 15-34-54.png create mode 100644 docker-compose.yml create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile create mode 100644 server/package.json create mode 100644 server/render.html create mode 100644 server/server.js create mode 100644 src/App.vue create mode 100644 src/components/AssetSidebar.vue create mode 100644 src/components/CanvasStage.vue create mode 100644 src/components/DocumentSettings.vue create mode 100644 src/components/ExportDialog.vue create mode 100644 src/components/PropertiesPanel.vue create mode 100644 src/components/RangeSlider.vue create mode 100644 src/components/Timeline.vue create mode 100644 src/composables/usePlaybackLoop.ts create mode 100644 src/composables/useTheme.ts create mode 100644 src/lib/exportPlayer.ts create mode 100644 src/lib/fileLoader.ts create mode 100644 src/lib/interpolate.ts create mode 100644 src/lib/render.ts create mode 100644 src/lib/serverExport.ts create mode 100644 src/lib/videoExport.ts create mode 100644 src/main.ts create mode 100644 src/stores/scene.ts create mode 100644 src/styles/global.css create mode 100644 src/types/scene.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89f1a15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +dist +dist-ssr +*.local +.vite + +.DS_Store +*.log diff --git a/animatr_reference/Screenshot from 2026-04-19 15-34-54.png b/animatr_reference/Screenshot from 2026-04-19 15-34-54.png new file mode 100644 index 0000000000000000000000000000000000000000..5c54f279a2eab5fecd79ea5df8a81e7ced180c9f GIT binary patch literal 18728 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 literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7da796f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + animatr-render: + build: ./server + ports: + - "6000:6000" + environment: + - NODE_ENV=production + - PORT=6000 + shm_size: "2gb" + restart: unless-stopped diff --git a/index.html b/index.html new file mode 100644 index 0000000..17c27fe --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Animatr + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..408a060 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1636 @@ +{ + "name": "animatr", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "animatr", + "version": "0.0.0", + "dependencies": { + "@phosphor-icons/vue": "^2.2.1", + "konva": "^9.3.16", + "pinia": "^2.2.6", + "vue": "^3.5.13", + "vue-konva": "^3.1.2" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.6.3", + "vite": "^6.0.3", + "vue-tsc": "^2.1.10" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@phosphor-icons/vue": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@phosphor-icons/vue/-/vue-2.2.1.tgz", + "integrity": "sha512-3RNg1utc2Z5RwPKWFkW3eXI/0BfQAwXgtFxPUPeSzi55jGYUq16b+UqcgbKLazWFlwg5R92OCLKjDiJjeiJcnA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "vue": ">=3.2.39" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "vue": "3.5.32" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "license": "MIT" + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/konva": { + "version": "9.3.22", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.22.tgz", + "integrity": "sha512-yQI5d1bmELlD/fowuyfOp9ff+oamg26WOCkyqUyc+nczD/lhRa3EvD2MZOoc4c1293TAubW9n34fSQLgSeEgSw==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-konva": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/vue-konva/-/vue-konva-3.4.0.tgz", + "integrity": "sha512-l7yqAPmc6gPbdXzxoukTiXzA6EdqTtPhzQp1pTdppwWLiimPCgFHcqp8nApXQN5dqF11p2OejwjKv150cf9ncA==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.0.0", + "npm": ">= 3.0.0" + }, + "peerDependencies": { + "konva": ">7", + "vue": "^3" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6fbdecd --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "animatr", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "type-check": "vue-tsc --noEmit" + }, + "dependencies": { + "@phosphor-icons/vue": "^2.2.1", + "konva": "^9.3.16", + "pinia": "^2.2.6", + "vue": "^3.5.13", + "vue-konva": "^3.1.2" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.6.3", + "vite": "^6.0.3", + "vue-tsc": "^2.1.10" + } +} diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..54e5f26 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +.env +*.mp4 diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..638c64b --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,20 @@ +FROM node:20-slim + +RUN apt-get update && apt-get install -y \ + chromium \ + ffmpeg \ + fonts-liberation \ + fonts-noto-color-emoji \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium +ENV PORT=6000 + +WORKDIR /app +COPY package.json ./ +RUN npm install --omit=dev +COPY . . + +EXPOSE 6000 +CMD ["node", "server.js"] diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..dade8f8 --- /dev/null +++ b/server/package.json @@ -0,0 +1,16 @@ +{ + "name": "animatr-server", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.2", + "puppeteer-core": "^23.11.1" + } +} diff --git a/server/render.html b/server/render.html new file mode 100644 index 0000000..4c1dd90 --- /dev/null +++ b/server/render.html @@ -0,0 +1,144 @@ + + + + + + + + + + + diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..44dda60 --- /dev/null +++ b/server/server.js @@ -0,0 +1,198 @@ +import express from 'express' +import cors from 'cors' +import path from 'node:path' +import fs from 'node:fs/promises' +import os from 'node:os' +import { randomUUID } from 'node:crypto' +import { spawn } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import puppeteer from 'puppeteer-core' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const PORT = Number(process.env.PORT || 6000) +const CHROME_PATH = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium' +const RENDER_HTML = path.join(__dirname, 'render.html') + +const app = express() +app.use(cors()) +app.use(express.json({ limit: '200mb' })) + +/** @type {Map} */ +const jobs = new Map() + +app.get('/health', (_req, res) => res.json({ ok: true })) + +app.post('/render', (req, res) => { + const { scene, options } = req.body || {} + if (!scene || !scene.canvas || !Array.isArray(scene.layers)) { + return res.status(400).json({ error: 'invalid scene' }) + } + const job = { + id: randomUUID(), + progress: 0, + done: false, + error: null, + filePath: null, + subscribers: new Set(), + abort: new AbortController(), + } + jobs.set(job.id, job) + res.json({ jobId: job.id }) + runJob(job, scene, options || {}).catch((err) => { + job.error = err?.message || String(err) + job.done = true + notify(job) + }) +}) + +app.get('/render/:id/events', (req, res) => { + const job = jobs.get(req.params.id) + if (!job) return res.status(404).end() + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }) + const send = () => { + const payload = { + progress: job.progress, + done: job.done, + error: job.error, + url: job.done && !job.error ? `/render/${job.id}/file` : null, + } + res.write(`data: ${JSON.stringify(payload)}\n\n`) + } + job.subscribers.add(send) + send() + const keepalive = setInterval(() => res.write(': keepalive\n\n'), 15000) + req.on('close', () => { + job.subscribers.delete(send) + clearInterval(keepalive) + }) +}) + +app.get('/render/:id/file', (req, res) => { + const job = jobs.get(req.params.id) + if (!job || !job.filePath || job.error) return res.status(404).end() + res.download(job.filePath, 'animation.mp4') +}) + +app.delete('/render/:id', (req, res) => { + const job = jobs.get(req.params.id) + if (!job) return res.status(404).end() + job.abort.abort() + res.json({ ok: true }) +}) + +function notify(job) { + for (const sub of Array.from(job.subscribers)) { + try { sub() } catch { /* noop */ } + } +} + +function effectiveEnd(scene) { + let max = 0 + for (const layer of scene.layers) { + for (const kf of layer.keyframes) { + if (kf.time > max) max = kf.time + } + } + if (max <= 0) return scene.duration + return Math.min(max, scene.duration) +} + +async function runJob(job, scene, options) { + const fps = Number(options.fps || scene.fps || 60) + const sceneEnd = effectiveEnd(scene) + const duration = Number(options.duration || sceneEnd) + const totalFrames = Math.max(1, Math.round(duration * fps)) + const crf = String(options.crf ?? 18) + const preset = String(options.preset ?? 'fast') + + const workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'animatr-')) + const outPath = path.join(workDir, 'out.mp4') + + const browser = await puppeteer.launch({ + executablePath: CHROME_PATH, + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--hide-scrollbars', + ], + defaultViewport: { + width: scene.canvas.width, + height: scene.canvas.height, + deviceScaleFactor: 1, + }, + }) + + const ffmpeg = spawn('ffmpeg', [ + '-y', + '-f', 'image2pipe', + '-framerate', String(fps), + '-i', '-', + '-c:v', 'libx264', + '-pix_fmt', 'yuv420p', + '-preset', preset, + '-crf', crf, + '-movflags', '+faststart', + outPath, + ]) + let ffmpegErr = '' + ffmpeg.stderr.on('data', (d) => { ffmpegErr += d.toString() }) + const ffmpegDone = new Promise((resolve, reject) => { + ffmpeg.on('exit', (code) => { + if (code === 0) resolve() + else reject(new Error(`ffmpeg exited with code ${code}: ${ffmpegErr.slice(-500)}`)) + }) + ffmpeg.on('error', reject) + }) + + try { + const page = await browser.newPage() + await page.goto('file://' + RENDER_HTML, { waitUntil: 'domcontentloaded' }) + await page.evaluate((s) => window.__animatrInit(s), scene) + + for (let i = 0; i < totalFrames; i++) { + if (job.abort.signal.aborted) { + try { ffmpeg.kill('SIGKILL') } catch { /* noop */ } + throw new Error('aborted') + } + const t = Math.min(i / fps, duration) + await page.evaluate((time) => window.__animatrSeek(time), t) + const buf = await page.screenshot({ + type: 'png', + omitBackground: false, + clip: { x: 0, y: 0, width: scene.canvas.width, height: scene.canvas.height }, + }) + if (!ffmpeg.stdin.write(buf)) { + await new Promise((resolve) => ffmpeg.stdin.once('drain', resolve)) + } + job.progress = (i + 1) / totalFrames + notify(job) + } + ffmpeg.stdin.end() + await ffmpegDone + job.filePath = outPath + job.done = true + notify(job) + + setTimeout(() => cleanupJob(job.id, workDir).catch(() => {}), 10 * 60 * 1000) + } finally { + await browser.close().catch(() => {}) + } +} + +async function cleanupJob(jobId, workDir) { + jobs.delete(jobId) + await fs.rm(workDir, { recursive: true, force: true }) +} + +app.listen(PORT, () => { + console.log(`animatr render server listening on :${PORT}`) + console.log(`chromium: ${CHROME_PATH}`) +}) diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..b8c7670 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/src/components/AssetSidebar.vue b/src/components/AssetSidebar.vue new file mode 100644 index 0000000..9c8dad1 --- /dev/null +++ b/src/components/AssetSidebar.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/src/components/CanvasStage.vue b/src/components/CanvasStage.vue new file mode 100644 index 0000000..75e6d60 --- /dev/null +++ b/src/components/CanvasStage.vue @@ -0,0 +1,581 @@ + + + + + diff --git a/src/components/DocumentSettings.vue b/src/components/DocumentSettings.vue new file mode 100644 index 0000000..ca3366a --- /dev/null +++ b/src/components/DocumentSettings.vue @@ -0,0 +1,540 @@ + + + + + diff --git a/src/components/ExportDialog.vue b/src/components/ExportDialog.vue new file mode 100644 index 0000000..9db7e12 --- /dev/null +++ b/src/components/ExportDialog.vue @@ -0,0 +1,460 @@ + + + + + diff --git a/src/components/PropertiesPanel.vue b/src/components/PropertiesPanel.vue new file mode 100644 index 0000000..b888286 --- /dev/null +++ b/src/components/PropertiesPanel.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/src/components/RangeSlider.vue b/src/components/RangeSlider.vue new file mode 100644 index 0000000..70f5717 --- /dev/null +++ b/src/components/RangeSlider.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue new file mode 100644 index 0000000..227adf0 --- /dev/null +++ b/src/components/Timeline.vue @@ -0,0 +1,839 @@ + + + + + diff --git a/src/composables/usePlaybackLoop.ts b/src/composables/usePlaybackLoop.ts new file mode 100644 index 0000000..c7cf500 --- /dev/null +++ b/src/composables/usePlaybackLoop.ts @@ -0,0 +1,23 @@ +import { onBeforeUnmount, onMounted } from 'vue' +import { useSceneStore } from '@/stores/scene' + +export function usePlaybackLoop() { + const store = useSceneStore() + let raf = 0 + let last = 0 + + const loop = (now: number) => { + const dt = last === 0 ? 0 : (now - last) / 1000 + last = now + if (store.playing) store.tick(dt) + raf = requestAnimationFrame(loop) + } + + onMounted(() => { + raf = requestAnimationFrame(loop) + }) + + onBeforeUnmount(() => { + cancelAnimationFrame(raf) + }) +} diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts new file mode 100644 index 0000000..c436d3a --- /dev/null +++ b/src/composables/useTheme.ts @@ -0,0 +1,35 @@ +import { ref, watch } from 'vue' + +export type Theme = 'dark' | 'light' + +const STORAGE_KEY = 'animatr.theme' + +function readInitial(): Theme { + if (typeof localStorage === 'undefined') return 'dark' + const v = localStorage.getItem(STORAGE_KEY) + return v === 'light' ? 'light' : 'dark' +} + +const theme = ref(readInitial()) + +function apply(t: Theme) { + if (typeof document === 'undefined') return + document.documentElement.dataset.theme = t + try { + localStorage.setItem(STORAGE_KEY, t) + } catch { + /* ignore quota */ + } +} + +apply(theme.value) +watch(theme, (t) => apply(t)) + +export function useTheme() { + return { + theme, + setTheme(t: Theme) { + theme.value = t + }, + } +} diff --git a/src/lib/exportPlayer.ts b/src/lib/exportPlayer.ts new file mode 100644 index 0000000..759e02e --- /dev/null +++ b/src/lib/exportPlayer.ts @@ -0,0 +1,326 @@ +import type { Scene } from '@/types/scene' + +const RUNTIME_SOURCE = `(function(){ + var SCENE = __SCENE__; + var OPTIONS = __OPTIONS__; + + function easing(name, t){ + if(name==='easeIn')return t*t; + if(name==='easeOut')return 1-(1-t)*(1-t); + if(name==='easeInOut')return t<0.5?2*t*t:1-Math.pow(-2*t+2,2)/2; + return t; + } + function merge(base,partial){ + var o={};for(var k in base)o[k]=base[k]; + if(partial)for(var k2 in partial)o[k2]=partial[k2]; + return o; + } + function lerp(a,b,t){return a+(b-a)*t;} + function parseHex(hex){ + if(!hex)return null; + var h=String(hex).trim().replace('#',''); + if(h.length===3)h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2]; + if(h.length<6)return null; + var n=parseInt(h.slice(0,6),16); + if(isNaN(n))return null; + return [(n>>16)&255,(n>>8)&255,n&255]; + } + function formatHex(r,g,b){ + function c(v){v=Math.round(v);if(v<0)v=0;if(v>255)v=255;var s=v.toString(16);return s.length<2?'0'+s:s;} + return '#'+c(r)+c(g)+c(b); + } + function lerpColor(a,b,t){ + var pa=parseHex(a),pb=parseHex(b); + if(!pa||!pb)return t<0.5?a:b; + return formatHex(lerp(pa[0],pb[0],t),lerp(pa[1],pb[1],t),lerp(pa[2],pb[2],t)); + } + function lerpProps(a,b,t){ + var out={ + x:lerp(a.x,b.x,t), + y:lerp(a.y,b.y,t), + scaleX:lerp(a.scaleX,b.scaleX,t), + scaleY:lerp(a.scaleY,b.scaleY,t), + rotation:lerp(a.rotation,b.rotation,t), + opacity:lerp(a.opacity,b.opacity,t) + }; + if(a.fill&&b.fill)out.fill=lerpColor(a.fill,b.fill,t); + else if(b.fill)out.fill=b.fill; + else if(a.fill)out.fill=a.fill; + return out; + } + function sample(layer,time){ + var kfs=layer.keyframes.slice().sort(function(a,b){return a.time-b.time;}); + if(!kfs.length)return merge(layer.base); + if(time<=kfs[0].time)return merge(layer.base,kfs[0].props); + var last=kfs[kfs.length-1]; + if(time>=last.time)return merge(layer.base,last.props); + for(var i=0;i=a.time&&time<=b.time){ + var raw=(time-a.time)/(b.time-a.time); + var t=easing(b.easing,raw); + return lerpProps(merge(layer.base,a.props),merge(layer.base,b.props),t); + } + } + return merge(layer.base); + } + + function loadImages(assets){ + var imgs={}; + var list=assets.filter(function(a){return a.kind==='image';}); + return Promise.all(list.map(function(a){ + return new Promise(function(resolve){ + var img=new Image(); + img.onload=function(){imgs[a.id]=img;resolve();}; + img.onerror=function(){resolve();}; + img.src=a.src; + }); + })).then(function(){return imgs;}); + } + + function effectiveEnd(scene){ + var max=0; + for(var i=0;imax) max=kfs[j].time; } + } + if(max<=0) return scene.duration; + return Math.min(max,scene.duration); + } + function mount(container,scene,opts){ + if(!container)return; + opts=opts||{}; + var loop=opts.loop!==false; + var sceneEnd=effectiveEnd(scene); + var cs=getComputedStyle(container); + if(cs.position==='static')container.style.position='relative'; + var canvas=document.createElement('canvas'); + canvas.style.cssText='display:block;width:100%;height:100%;'; + container.appendChild(canvas); + var ctx=canvas.getContext('2d'); + var dpr=window.devicePixelRatio||1; + var size={w:0,h:0}; + function resize(){ + var r=container.getBoundingClientRect(); + size.w=r.width;size.h=r.height; + canvas.width=Math.max(1,Math.round(r.width*dpr)); + canvas.height=Math.max(1,Math.round(r.height*dpr)); + } + var ro=new ResizeObserver(resize); + ro.observe(container); + resize(); + var assetMap={}; + scene.assets.forEach(function(a){assetMap[a.id]=a;}); + var imgs={}; + var start=0; + function frame(now){ + if(!start)start=now; + var t=(now-start)/1000; + if(sceneEnd<=0){ draw(0); requestAnimationFrame(frame); return; } + if(loop) t=t%sceneEnd; + else if(t>sceneEnd) t=sceneEnd; + draw(t); + requestAnimationFrame(frame); + } + function draw(time){ + var lw=scene.canvas.width,lh=scene.canvas.height; + var cw=size.w,ch=size.h; + if(!cw||!ch)return; + var s=Math.min(cw/lw,ch/lh); + var ox=(cw-lw*s)/2; + var oy=(ch-lh*s)/2; + ctx.setTransform(dpr,0,0,dpr,0,0); + ctx.clearRect(0,0,cw,ch); + ctx.save(); + ctx.translate(ox,oy); + ctx.scale(s,s); + ctx.beginPath(); + ctx.rect(0,0,lw,lh); + ctx.clip(); + ctx.fillStyle=scene.canvas.background; + ctx.fillRect(0,0,lw,lh); + for(var i=0;i + + + + +Animatr — Beispiel + + + +

Animatr Export

+

Diese HTML-Datei enthält Runtime + Szene inline — einfach im Browser öffnen.

+ +
+ +
+ +
+

Einbinden in eine eigene Seite

+
<div style="width: 800px; height: 450px;">
+  <script src="animation.js"></script>
+</div>
+
+ + +` +} + +export function downloadExampleHtml(scene: Scene, filename = 'animation-example.html') { + const source = generateExampleHtml(scene) + triggerDownload(source, filename, 'text/html') +} + +function triggerDownload(source: string, filename: string, mime: string) { + const blob = new Blob([source], { type: mime }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + setTimeout(() => URL.revokeObjectURL(url), 1000) +} diff --git a/src/lib/fileLoader.ts b/src/lib/fileLoader.ts new file mode 100644 index 0000000..a81c708 --- /dev/null +++ b/src/lib/fileLoader.ts @@ -0,0 +1,53 @@ +export function readAsDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsDataURL(file) + }) +} + +export function loadImage(src: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image() + img.crossOrigin = 'anonymous' + img.onload = () => resolve(img) + img.onerror = () => reject(new Error(`failed to load image: ${src.slice(0, 60)}…`)) + img.src = src + }) +} + +export interface Dimensions { + width: number + height: number +} + +export function parseSvgDimensions(svgText: string): Dimensions | null { + const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml') + const svg = doc.documentElement + if (!svg || svg.nodeName !== 'svg') return null + + const parseLen = (attr: string | null) => { + if (!attr) return 0 + const num = parseFloat(attr) + return Number.isFinite(num) ? num : 0 + } + + let w = parseLen(svg.getAttribute('width')) + let h = parseLen(svg.getAttribute('height')) + + if (!w || !h) { + const vb = svg.getAttribute('viewBox')?.trim().split(/[\s,]+/).map(Number) + if (vb && vb.length === 4 && vb.every((n) => Number.isFinite(n))) { + if (!w) w = vb[2] + if (!h) h = vb[3] + } + } + + if (!w || !h) return null + return { width: w, height: h } +} + +export function isSvgFile(file: File): boolean { + return file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg') +} diff --git a/src/lib/interpolate.ts b/src/lib/interpolate.ts new file mode 100644 index 0000000..e43714a --- /dev/null +++ b/src/lib/interpolate.ts @@ -0,0 +1,75 @@ +import type { AnimatableProps, Easing, Keyframe, Layer } from '@/types/scene' + +const easings: Record number> = { + linear: (t) => t, + easeIn: (t) => t * t, + easeOut: (t) => 1 - (1 - t) ** 2, + easeInOut: (t) => (t < 0.5 ? 2 * t * t : 1 - ((-2 * t + 2) ** 2) / 2), +} + +function merge(base: AnimatableProps, partial: Partial): AnimatableProps { + return { ...base, ...partial } +} + +function lerp(a: number, b: number, t: number) { + return a + (b - a) * t +} + +function parseHex(hex: string): [number, number, number] | null { + const h = hex.trim().replace('#', '') + const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h + if (full.length < 6) return null + const n = parseInt(full.slice(0, 6), 16) + if (Number.isNaN(n)) return null + return [(n >> 16) & 255, (n >> 8) & 255, n & 255] +} + +function formatHex(r: number, g: number, b: number): string { + const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v))) + const h = (v: number) => clamp(v).toString(16).padStart(2, '0') + return `#${h(r)}${h(g)}${h(b)}` +} + +export function lerpColor(a: string, b: string, t: number): string { + const pa = parseHex(a) + const pb = parseHex(b) + if (!pa || !pb) return t < 0.5 ? a : b + return formatHex(lerp(pa[0], pb[0], t), lerp(pa[1], pb[1], t), lerp(pa[2], pb[2], t)) +} + +function lerpProps(a: AnimatableProps, b: AnimatableProps, t: number): AnimatableProps { + const out: AnimatableProps = { + x: lerp(a.x, b.x, t), + y: lerp(a.y, b.y, t), + scaleX: lerp(a.scaleX, b.scaleX, t), + scaleY: lerp(a.scaleY, b.scaleY, t), + rotation: lerp(a.rotation, b.rotation, t), + opacity: lerp(a.opacity, b.opacity, t), + } + if (a.fill && b.fill) out.fill = lerpColor(a.fill, b.fill, t) + else if (b.fill) out.fill = b.fill + else if (a.fill) out.fill = a.fill + return out +} + +export function sortedKeyframes(layer: Layer): Keyframe[] { + return [...layer.keyframes].sort((a, b) => a.time - b.time) +} + +export function sampleLayer(layer: Layer, time: number): AnimatableProps { + const kfs = sortedKeyframes(layer) + if (!kfs.length) return { ...layer.base } + if (time <= kfs[0].time) return merge(layer.base, kfs[0].props) + const last = kfs[kfs.length - 1] + if (time >= last.time) return merge(layer.base, last.props) + for (let i = 0; i < kfs.length - 1; i++) { + const a = kfs[i] + const b = kfs[i + 1] + if (time >= a.time && time <= b.time) { + const raw = (time - a.time) / (b.time - a.time) + const t = easings[b.easing](raw) + return lerpProps(merge(layer.base, a.props), merge(layer.base, b.props), t) + } + } + return { ...layer.base } +} diff --git a/src/lib/render.ts b/src/lib/render.ts new file mode 100644 index 0000000..d33dfd8 --- /dev/null +++ b/src/lib/render.ts @@ -0,0 +1,86 @@ +import type { Asset, Scene } from '@/types/scene' +import { sampleLayer } from './interpolate' + +export async function loadSceneImages(scene: Scene): Promise> { + const result: Record = {} + const images = scene.assets.filter( + (a): a is Extract => a.kind === 'image', + ) + await Promise.all( + images.map( + (a) => + new Promise((resolve) => { + const img = new Image() + img.crossOrigin = 'anonymous' + img.onload = () => { + result[a.id] = img + resolve() + } + img.onerror = () => resolve() + img.src = a.src + }), + ), + ) + return result +} + +export function drawSceneFrame( + ctx: CanvasRenderingContext2D, + scene: Scene, + time: number, + images: Record, +) { + const lw = scene.canvas.width + const lh = scene.canvas.height + + ctx.save() + ctx.beginPath() + ctx.rect(0, 0, lw, lh) + ctx.clip() + ctx.fillStyle = scene.canvas.background + ctx.fillRect(0, 0, lw, lh) + + const assetMap = new Map() + for (const a of scene.assets) assetMap.set(a.id, a) + + for (const layer of scene.layers) { + if (!layer.visible) continue + const asset = assetMap.get(layer.assetId) + if (!asset) continue + const p = sampleLayer(layer, time) + + ctx.save() + ctx.globalAlpha *= p.opacity + ctx.translate(p.x, p.y) + ctx.rotate((p.rotation * Math.PI) / 180) + ctx.scale(p.scaleX, p.scaleY) + + if (asset.kind === 'rect') { + const offX = layer.offsetX ?? asset.width / 2 + const offY = layer.offsetY ?? asset.height / 2 + ctx.fillStyle = p.fill ?? asset.fill + ctx.fillRect(-offX, -offY, asset.width, asset.height) + } else if (asset.kind === 'circle') { + ctx.fillStyle = p.fill ?? asset.fill + ctx.beginPath() + ctx.arc(0, 0, asset.radius, 0, Math.PI * 2) + ctx.fill() + } else if (asset.kind === 'text') { + ctx.fillStyle = p.fill ?? asset.fill + ctx.font = `${asset.fontSize}px sans-serif` + ctx.textBaseline = 'top' + ctx.fillText(asset.text, 0, 0) + } else if (asset.kind === 'image') { + const img = images[asset.id] + if (img) { + const offX = layer.offsetX ?? asset.width / 2 + const offY = layer.offsetY ?? asset.height / 2 + ctx.drawImage(img, -offX, -offY, asset.width, asset.height) + } + } + + ctx.restore() + } + + ctx.restore() +} diff --git a/src/lib/serverExport.ts b/src/lib/serverExport.ts new file mode 100644 index 0000000..13d1f86 --- /dev/null +++ b/src/lib/serverExport.ts @@ -0,0 +1,119 @@ +import type { Scene } from '@/types/scene' + +const DEFAULT_SERVER = 'http://localhost:6000' +const SERVER_URL = (import.meta.env.VITE_RENDER_SERVER as string | undefined) ?? DEFAULT_SERVER + +export interface ServerRenderOptions { + onProgress?: (progress: number) => void + signal?: AbortSignal + fps?: number + crf?: number + preset?: string +} + +export interface ServerRenderResult { + blob: Blob + extension: string +} + +export function getServerUrl(): string { + return SERVER_URL +} + +export async function renderOnServer( + scene: Scene, + opts: ServerRenderOptions = {}, +): Promise { + const baseUrl = SERVER_URL + const startRes = await fetch(`${baseUrl}/render`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + scene, + options: { + fps: opts.fps, + crf: opts.crf, + preset: opts.preset, + }, + }), + signal: opts.signal, + }) + if (!startRes.ok) { + const body = await startRes.text().catch(() => '') + throw new Error(`Server start fehlgeschlagen (${startRes.status}): ${body.slice(0, 200)}`) + } + const { jobId } = (await startRes.json()) as { jobId: string } + if (!jobId) throw new Error('Server lieferte keine jobId') + + const downloadUrl = await new Promise((resolve, reject) => { + const es = new EventSource(`${baseUrl}/render/${jobId}/events`) + const onAbort = () => { + try { + es.close() + } catch { + /* noop */ + } + fetch(`${baseUrl}/render/${jobId}`, { method: 'DELETE' }).catch(() => {}) + reject(new DOMException('aborted', 'AbortError')) + } + if (opts.signal) { + if (opts.signal.aborted) return onAbort() + opts.signal.addEventListener('abort', onAbort, { once: true }) + } + es.onmessage = (evt) => { + let data: { + progress?: number + done?: boolean + error?: string | null + url?: string | null + } + try { + data = JSON.parse(evt.data) + } catch { + return + } + if (typeof data.progress === 'number') opts.onProgress?.(data.progress) + if (data.done) { + es.close() + opts.signal?.removeEventListener('abort', onAbort) + if (data.error) reject(new Error(data.error)) + else if (data.url) resolve(`${baseUrl}${data.url}`) + else reject(new Error('Server meldete fertig ohne Download-URL')) + } + } + es.onerror = () => { + es.close() + opts.signal?.removeEventListener('abort', onAbort) + reject(new Error('Verbindung zum Render-Server verloren')) + } + }) + + const fileRes = await fetch(downloadUrl, { signal: opts.signal }) + if (!fileRes.ok) throw new Error(`Download fehlgeschlagen (${fileRes.status})`) + const blob = await fileRes.blob() + return { blob, extension: 'mp4' } +} + +export async function downloadFromServer( + scene: Scene, + opts: ServerRenderOptions = {}, +): Promise { + const { blob, extension } = await renderOnServer(scene, opts) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `animation.${extension}` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + setTimeout(() => URL.revokeObjectURL(url), 1000) +} + +export async function isServerReachable(): Promise { + try { + const res = await fetch(`${SERVER_URL}/health`, { method: 'GET' }) + return res.ok + } catch { + return false + } +} diff --git a/src/lib/videoExport.ts b/src/lib/videoExport.ts new file mode 100644 index 0000000..72eaf59 --- /dev/null +++ b/src/lib/videoExport.ts @@ -0,0 +1,135 @@ +import type { Scene } from '@/types/scene' +import { drawSceneFrame, loadSceneImages } from './render' + +function effectiveEnd(scene: Scene): number { + let max = 0 + for (const layer of scene.layers) { + for (const kf of layer.keyframes) { + if (kf.time > max) max = kf.time + } + } + if (max <= 0) return scene.duration + return Math.min(max, scene.duration) +} + +export interface VideoExportResult { + blob: Blob + mimeType: string + extension: string +} + +export interface VideoExportOptions { + onProgress?: (progress: number) => void + signal?: AbortSignal + bitsPerSecond?: number +} + +const MIME_CANDIDATES = [ + 'video/webm;codecs=vp9', + 'video/webm;codecs=vp8', + 'video/webm', + 'video/mp4;codecs=h264', + 'video/mp4', +] + +export function pickMimeType(): string | null { + if (typeof MediaRecorder === 'undefined') return null + for (const mime of MIME_CANDIDATES) { + if (MediaRecorder.isTypeSupported?.(mime)) return mime + } + return null +} + +export async function renderSceneToVideo( + scene: Scene, + opts: VideoExportOptions = {}, +): Promise { + const mimeType = pickMimeType() + if (!mimeType) { + throw new Error('MediaRecorder wird von diesem Browser nicht unterstützt.') + } + + const canvas = document.createElement('canvas') + canvas.width = scene.canvas.width + canvas.height = scene.canvas.height + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('2D context unavailable') + + const images = await loadSceneImages(scene) + + drawSceneFrame(ctx, scene, 0, images) + + const stream = canvas.captureStream(scene.fps) + const recorder = new MediaRecorder(stream, { + mimeType, + videoBitsPerSecond: opts.bitsPerSecond ?? 8_000_000, + }) + + const chunks: Blob[] = [] + recorder.ondataavailable = (e) => { + if (e.data && e.data.size > 0) chunks.push(e.data) + } + + const stopped = new Promise((resolve) => { + recorder.onstop = () => resolve() + }) + + recorder.start() + const startedAt = performance.now() + const renderDuration = effectiveEnd(scene) + + const finished = new Promise((resolve, reject) => { + let raf = 0 + const step = () => { + if (opts.signal?.aborted) { + cancelAnimationFrame(raf) + try { + recorder.stop() + } catch { + /* ignore */ + } + stream.getTracks().forEach((t) => t.stop()) + reject(new DOMException('aborted', 'AbortError')) + return + } + const elapsed = (performance.now() - startedAt) / 1000 + const t = Math.min(elapsed, renderDuration) + drawSceneFrame(ctx, scene, t, images) + opts.onProgress?.(renderDuration > 0 ? Math.min(1, t / renderDuration) : 1) + if (elapsed >= renderDuration) { + try { + recorder.stop() + } catch { + /* ignore */ + } + stream.getTracks().forEach((t) => t.stop()) + resolve() + return + } + raf = requestAnimationFrame(step) + } + raf = requestAnimationFrame(step) + }) + + await finished + await stopped + + const blob = new Blob(chunks, { type: mimeType }) + const extension = mimeType.includes('mp4') ? 'mp4' : 'webm' + return { blob, mimeType, extension } +} + +export async function downloadSceneAsVideo( + scene: Scene, + opts: VideoExportOptions = {}, +): Promise { + const { blob, extension } = await renderSceneToVideo(scene, opts) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `animation.${extension}` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + setTimeout(() => URL.revokeObjectURL(url), 1000) +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a73c994 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,11 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import VueKonva from 'vue-konva' +import App from './App.vue' +import './composables/useTheme' +import './styles/global.css' + +const app = createApp(App) +app.use(createPinia()) +app.use(VueKonva) +app.mount('#app') diff --git a/src/stores/scene.ts b/src/stores/scene.ts new file mode 100644 index 0000000..d05b072 --- /dev/null +++ b/src/stores/scene.ts @@ -0,0 +1,227 @@ +import { defineStore } from 'pinia' +import type { AnimatableProps, Asset, Easing, Keyframe, Layer, Scene } from '@/types/scene' +import { DEFAULT_BASE_PROPS } from '@/types/scene' +import { sampleLayer } from '@/lib/interpolate' + +const uid = () => Math.random().toString(36).slice(2, 10) + +type AssetInit = Asset extends infer A ? (A extends Asset ? Omit : never) : never + +const KEYFRAME_EPSILON = 1e-3 + +interface State { + scene: Scene + selectedLayerId: string | null + playhead: number + playing: boolean +} + +export const useSceneStore = defineStore('scene', { + state: (): State => ({ + scene: { + version: 1, + canvas: { width: 1280, height: 720, background: '#ffffff' }, + duration: 10, + fps: 60, + assets: [], + layers: [], + }, + selectedLayerId: null, + playhead: 0, + playing: false, + }), + getters: { + lastKeyframeTime(state): number { + let max = 0 + for (const layer of state.scene.layers) { + for (const kf of layer.keyframes) { + if (kf.time > max) max = kf.time + } + } + return Math.min(state.scene.duration, max) + }, + effectiveEnd(state): number { + let max = 0 + for (const layer of state.scene.layers) { + for (const kf of layer.keyframes) { + if (kf.time > max) max = kf.time + } + } + if (max <= 0) return state.scene.duration + return Math.min(max, state.scene.duration) + }, + }, + actions: { + addAsset(asset: AssetInit): Asset { + const created = { ...asset, id: uid() } as Asset + this.scene.assets.push(created) + return created + }, + addLayer(assetId: string, name?: string): Layer { + const asset = this.scene.assets.find((a) => a.id === assetId) + const base: AnimatableProps = { + ...DEFAULT_BASE_PROPS, + x: this.scene.canvas.width / 2, + y: this.scene.canvas.height / 2, + } + if (asset && 'fill' in asset) base.fill = asset.fill + const layer: Layer = { + id: uid(), + name: name ?? asset?.name ?? 'Layer', + assetId, + visible: true, + base, + keyframes: [ + { + id: uid(), + time: 0, + props: { ...base }, + easing: 'linear', + }, + ], + } + if (asset && (asset.kind === 'rect' || asset.kind === 'image')) { + layer.offsetX = asset.width / 2 + layer.offsetY = asset.height / 2 + } + this.scene.layers.push(layer) + this.selectedLayerId = layer.id + return layer + }, + removeLayer(id: string) { + this.scene.layers = this.scene.layers.filter((l) => l.id !== id) + if (this.selectedLayerId === id) this.selectedLayerId = null + }, + selectLayer(id: string | null) { + this.selectedLayerId = id + }, + setPlayhead(t: number) { + this.playhead = Math.max(0, Math.min(this.scene.duration, t)) + }, + setDuration(seconds: number) { + this.scene.duration = Math.max(0.1, seconds) + if (this.playhead > this.scene.duration) this.playhead = this.scene.duration + }, + setFps(fps: number) { + this.scene.fps = Math.max(1, Math.round(fps)) + }, + setCanvasSize(width: number, height: number) { + this.scene.canvas.width = Math.max(1, Math.round(width)) + this.scene.canvas.height = Math.max(1, Math.round(height)) + }, + setCanvasBackground(color: string) { + this.scene.canvas.background = color + }, + upsertKeyframe(layerId: string, time: number, props: Partial) { + const layer = this.scene.layers.find((l) => l.id === layerId) + if (!layer) return + const existing = layer.keyframes.find((k) => Math.abs(k.time - time) < KEYFRAME_EPSILON) + if (existing) { + existing.props = { ...existing.props, ...props } + return + } + const snapshot = sampleLayer(layer, time) + const kf: Keyframe = { + id: uid(), + time, + props: { ...snapshot, ...props }, + easing: 'linear', + } + layer.keyframes.push(kf) + layer.keyframes.sort((a, b) => a.time - b.time) + }, + removeKeyframeAt(layerId: string, time: number) { + const layer = this.scene.layers.find((l) => l.id === layerId) + if (!layer) return + layer.keyframes = layer.keyframes.filter((k) => Math.abs(k.time - time) >= KEYFRAME_EPSILON) + }, + setKeyframeEasing(layerId: string, time: number, easing: Easing) { + const layer = this.scene.layers.find((l) => l.id === layerId) + if (!layer) return + const kf = layer.keyframes.find((k) => Math.abs(k.time - time) < KEYFRAME_EPSILON) + if (kf) kf.easing = easing + }, + setKeyframeTime(layerId: string, keyframeId: string, newTime: number) { + const layer = this.scene.layers.find((l) => l.id === layerId) + if (!layer) return + const kf = layer.keyframes.find((k) => k.id === keyframeId) + if (!kf) return + kf.time = Math.max(0, Math.min(this.scene.duration, newTime)) + layer.keyframes.sort((a, b) => a.time - b.time) + }, + shiftLayerPivot( + layerId: string, + dWorldX: number, + dWorldY: number, + dLocalX: number, + dLocalY: number, + ) { + const layer = this.scene.layers.find((l) => l.id === layerId) + if (!layer) return + layer.offsetX = (layer.offsetX ?? 0) + dLocalX + layer.offsetY = (layer.offsetY ?? 0) + dLocalY + layer.base.x += dWorldX + layer.base.y += dWorldY + for (const kf of layer.keyframes) { + if (kf.props.x !== undefined) kf.props.x += dWorldX + if (kf.props.y !== undefined) kf.props.y += dWorldY + } + }, + duplicateKeyframe(layerId: string, keyframeId: string): string | null { + const layer = this.scene.layers.find((l) => l.id === layerId) + if (!layer) return null + const source = layer.keyframes.find((k) => k.id === keyframeId) + if (!source) return null + const fps = this.scene.fps > 0 ? this.scene.fps : 60 + const offset = Math.max(3 / fps, 0.1) + const duration = this.scene.duration + const occupied = (t: number) => + layer.keyframes.some((k) => Math.abs(k.time - t) < KEYFRAME_EPSILON) + let target = source.time + offset + while (occupied(target) && target < duration) target += offset + if (target > duration) target = duration + if (occupied(target)) { + target = source.time - offset + while (occupied(target) && target > 0) target -= offset + if (target < 0) target = 0 + if (occupied(target)) return null + } + const newKf: Keyframe = { + id: uid(), + time: target, + props: { ...source.props }, + easing: source.easing, + } + layer.keyframes.push(newKf) + layer.keyframes.sort((a, b) => a.time - b.time) + return newKf.id + }, + play() { + if (this.playing) return + this.playing = true + }, + pause() { + this.playing = false + }, + togglePlay() { + if (this.playing) this.pause() + else { + const end = this.effectiveEnd + if (end <= 0) return + if (this.playhead >= end) this.playhead = 0 + this.play() + } + }, + tick(deltaSeconds: number) { + if (!this.playing) return + const end = this.effectiveEnd + const next = this.playhead + deltaSeconds + if (next >= end) { + this.playhead = end + this.playing = false + } else { + this.playhead = next + } + }, + }, +}) diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..dc79f20 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,140 @@ +:root { + --bg: #17171b; + --panel: #1f1f24; + --panel-alt: #272730; + --panel-hover: #2f2f3a; + --border: #2e2e37; + --border-strong: #3a3a46; + --text: #ececf1; + --text-dim: #9a9aa6; + --text-dim-strong: #c7c7d1; + --accent: #6ea8ff; + --accent-soft: rgba(110, 168, 255, 0.14); + --accent-soft-strong: rgba(110, 168, 255, 0.22); + --danger: #ff7a7a; + --danger-soft: rgba(255, 122, 122, 0.12); + --radius: 6px; + --radius-sm: 4px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 18px rgba(0, 0, 0, 0.35); + --ease: cubic-bezier(0.2, 0.8, 0.2, 1); + color-scheme: dark; +} + +:root[data-theme='light'] { + --bg: #f3f3f6; + --panel: #ffffff; + --panel-alt: #f5f5f8; + --panel-hover: #eaeaf0; + --border: #e2e2e7; + --border-strong: #c9c9d0; + --text: #1c1c20; + --text-dim: #72727a; + --text-dim-strong: #3c3c42; + --accent: #2b6af5; + --accent-soft: rgba(43, 106, 245, 0.1); + --accent-soft-strong: rgba(43, 106, 245, 0.18); + --danger: #d63c3c; + --danger-soft: rgba(214, 60, 60, 0.09); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 8px 24px rgba(20, 25, 40, 0.12); + color-scheme: light; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#app { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + background: var(--bg); + color: var(--text); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; + user-select: none; + -webkit-user-select: none; + transition: background 180ms var(--ease), color 180ms var(--ease); +} + +input, +textarea, +[contenteditable='true'] { + user-select: text; + -webkit-user-select: text; +} + +button { + font: inherit; + color: inherit; + background: var(--panel-alt); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 5px 10px; + cursor: pointer; + transition: background 140ms var(--ease), border-color 140ms var(--ease), + color 140ms var(--ease), transform 120ms var(--ease); + display: inline-flex; + align-items: center; + gap: 6px; +} + +button:hover:not(:disabled) { + background: var(--panel-hover); + border-color: var(--border-strong); +} + +button:active:not(:disabled) { + transform: translateY(1px); +} + +button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +input, +select, +textarea { + transition: border-color 140ms var(--ease), background 140ms var(--ease); +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--accent) !important; +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 10px; + border: 2px solid var(--panel); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-strong); +} + +::selection { + background: var(--accent-soft-strong); + color: var(--text); +} diff --git a/src/types/scene.ts b/src/types/scene.ts new file mode 100644 index 0000000..2b7bccc --- /dev/null +++ b/src/types/scene.ts @@ -0,0 +1,90 @@ +export type Easing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' + +export interface AnimatableProps { + x: number + y: number + scaleX: number + scaleY: number + rotation: number + opacity: number + fill?: string +} + +export interface Keyframe { + id: string + time: number + props: Partial + easing: Easing +} + +export type AssetKind = 'image' | 'rect' | 'circle' | 'text' + +export interface ImageAsset { + id: string + kind: 'image' + name: string + src: string + width: number + height: number +} + +export interface RectAsset { + id: string + kind: 'rect' + name: string + width: number + height: number + fill: string +} + +export interface CircleAsset { + id: string + kind: 'circle' + name: string + radius: number + fill: string +} + +export interface TextAsset { + id: string + kind: 'text' + name: string + text: string + fontSize: number + fill: string +} + +export type Asset = ImageAsset | RectAsset | CircleAsset | TextAsset + +export interface Layer { + id: string + name: string + assetId: string + visible: boolean + base: AnimatableProps + keyframes: Keyframe[] + offsetX?: number + offsetY?: number +} + +export interface Scene { + version: 1 + canvas: { + width: number + height: number + background: string + } + duration: number + fps: number + assets: Asset[] + layers: Layer[] +} + +export const DEFAULT_BASE_PROPS: AnimatableProps = { + x: 0, + y: 0, + scaleX: 1, + scaleY: 1, + rotation: 0, + opacity: 1, +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..323c78a --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1f94304 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..a52797c --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +})