From 4e0e0b5d323ba4c51676a7e28d72c6710afb4a30 Mon Sep 17 00:00:00 2001 From: EmaDev Date: Sun, 25 Jan 2026 21:25:31 +0100 Subject: [PATCH 1/2] bkp --- .env.example | 3 + package.json | 4 + pnpm-lock.yaml | 190 +++++- src/lib/ai/schemas.ts | 204 ++++++ src/lib/ai/system-prompt.ts | 617 ++++++++++++++++++ src/lib/components/ai/ai-chat.svelte | 408 ++++++++++++ .../components/editor/keyboard-handler.svelte | 4 +- .../editor/panels/layers-panel.svelte | 141 ++-- src/lib/engine/layer-factory.ts | 240 ++++++- src/lib/engine/presets.ts | 421 +++++++----- src/lib/functions/ai.remote.ts | 72 ++ src/lib/functions/projects.remote.ts | 3 +- src/lib/layers/CodeLayer.svelte | 238 +++++++ src/lib/layers/DividerLayer.svelte | 96 +++ src/lib/layers/IconLayer.svelte | 255 ++++++++ src/lib/layers/ProgressLayer.svelte | 120 ++++ src/lib/layers/registry.ts | 38 +- src/lib/schemas/animation.ts | 216 ++++++ src/lib/server/db/schema/projects.ts | 16 +- src/lib/types/animation.ts | 197 ++---- src/routes/+layout.svelte | 2 + 21 files changed, 3048 insertions(+), 437 deletions(-) create mode 100644 src/lib/ai/schemas.ts create mode 100644 src/lib/ai/system-prompt.ts create mode 100644 src/lib/components/ai/ai-chat.svelte create mode 100644 src/lib/functions/ai.remote.ts create mode 100644 src/lib/layers/CodeLayer.svelte create mode 100644 src/lib/layers/DividerLayer.svelte create mode 100644 src/lib/layers/IconLayer.svelte create mode 100644 src/lib/layers/ProgressLayer.svelte create mode 100644 src/lib/schemas/animation.ts diff --git a/.env.example b/.env.example index 2308ae0..f11f398 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,6 @@ PRIVATE_BETTER_AUTH_SECRET=mysecretpassword # Google OAuth GOOGLE_CLIENT_ID=your_google_client_id_here GOOGLE_CLIENT_SECRET=your_google_client_secret_here + +# AI Generation (OpenRouter) +OPENROUTER_API_KEY=your_openrouter_api_key_here diff --git a/package.json b/package.json index 077a3ca..bb571b2 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,10 @@ "vitest-browser-svelte": "^1.1.0" }, "dependencies": { + "@ai-sdk/openai": "^3.0.18", + "@openrouter/ai-sdk-provider": "^2.0.2", "@sveltejs/adapter-static": "^3.0.10", + "ai": "^6.0.49", "better-auth": "^1.3.27", "bezier-easing": "^2.1.0", "lucide-svelte": "^0.563.0", @@ -72,6 +75,7 @@ "postgres": "^3.4.7", "runed": "^0.34.0", "schema-dts": "^1.1.5", + "svelte-sonner": "^1.0.7", "zod": "^4.3.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1db05f9..a11f665 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,21 @@ importers: .: dependencies: + '@ai-sdk/openai': + specifier: ^3.0.18 + version: 3.0.18(zod@4.3.6) + '@openrouter/ai-sdk-provider': + specifier: ^2.0.2 + version: 2.0.2(ai@6.0.49(zod@4.3.6))(zod@4.3.6) '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) + version: 3.0.10(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) + ai: + specifier: ^6.0.49 + version: 6.0.49(zod@4.3.6) better-auth: specifier: ^1.3.27 - version: 1.3.27(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + version: 1.3.27(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) bezier-easing: specifier: ^2.1.0 version: 2.1.0 @@ -31,10 +40,13 @@ importers: version: 3.4.7 runed: specifier: ^0.34.0 - version: 0.34.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + version: 0.34.0(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) schema-dts: specifier: ^1.1.5 version: 1.1.5 + svelte-sonner: + specifier: ^1.0.7 + version: 1.0.7(svelte@5.48.2) zod: specifier: ^4.3.6 version: 4.3.6 @@ -56,10 +68,10 @@ importers: version: 0.563.1(svelte@5.48.2) '@sveltejs/adapter-node': specifier: ^5.5.2 - version: 5.5.2(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) + version: 5.5.2(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.50.1 - version: 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + version: 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^6.2.4 version: 6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) @@ -77,7 +89,7 @@ importers: version: 3.2.4(playwright@1.58.0)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))(vitest@3.2.4) bits-ui: specifier: ^2.15.4 - version: 2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + version: 2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -86,7 +98,7 @@ importers: version: 0.31.8 drizzle-orm: specifier: ^0.45.1 - version: 0.45.1(kysely@0.27.6)(postgres@3.4.7) + version: 0.45.1(@opentelemetry/api@1.9.0)(kysely@0.27.6)(postgres@3.4.7) eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -156,6 +168,28 @@ importers: packages: + '@ai-sdk/gateway@3.0.22': + resolution: {integrity: sha512-NgnlY73JNuooACHqUIz5uMOEWvqR1MMVbb2soGLMozLY1fgwEIF5iJFDAGa5/YArlzw2ATVU7zQu7HkR/FUjgA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@3.0.18': + resolution: {integrity: sha512-uYscTyoaWij9FoPpKRNK8YgtDEuPpQlqREYylJCA8o5YQVQXghV0Dwgk1ehPVpg6USIO4L0C8GqQJ4AMm/Xb1g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.9': + resolution: {integrity: sha512-bB4r6nfhBOpmoS9mePxjRoCy+LnzP3AfhyMGCkGL4Mn9clVNlqEeKj26zEKEtB6yoSVcT1IQ0Zh9fytwMCDnow==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.5': + resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==} + engines: {node: '>=18'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -618,6 +652,20 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} + '@openrouter/ai-sdk-provider@2.0.2': + resolution: {integrity: sha512-G+8Z7Q4R61anj/nk/hmb1JAAjYpP/LEFA4mjQnitMGDLscoeThPpOl6xFKalzfkd2WSweeKRYAID5HxzEMCbPQ==} + engines: {node: '>=18'} + peerDependencies: + ai: ^6.0.0 + zod: ^3.25.0 || ^4.0.0 + + '@openrouter/sdk@0.1.27': + resolution: {integrity: sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ==} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@peculiar/asn1-android@2.5.0': resolution: {integrity: sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==} @@ -820,6 +868,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@sveltejs/acorn-typescript@1.0.6': resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==} peerDependencies: @@ -1069,6 +1120,10 @@ packages: resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + '@vitest/browser@3.2.4': resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} peerDependencies: @@ -1123,6 +1178,12 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ai@6.0.49: + resolution: {integrity: sha512-LABniBX/0R6Tv+iUK5keUZhZLaZUe4YjP5M2rZ4wAdZ8iKV3EfTAoJxuL1aaWTSJKIilKa9QUEkCgnp89/32bw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1532,6 +1593,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -1685,6 +1750,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2088,6 +2156,11 @@ packages: peerDependencies: svelte: ^5.7.0 + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} + peerDependencies: + svelte: ^5.7.0 + runed@0.29.2: resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==} peerDependencies: @@ -2198,6 +2271,11 @@ packages: svelte: optional: true + svelte-sonner@1.0.7: + resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==} + peerDependencies: + svelte: ^5.0.0 + svelte-toolbelt@0.10.6: resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -2494,6 +2572,30 @@ packages: snapshots: + '@ai-sdk/gateway@3.0.22(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@4.3.6) + '@vercel/oidc': 3.1.0 + zod: 4.3.6 + + '@ai-sdk/openai@3.0.18(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@4.3.6) + zod: 4.3.6 + + '@ai-sdk/provider-utils@4.0.9(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 + + '@ai-sdk/provider@3.0.5': + dependencies: + json-schema: 0.4.0 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -2829,6 +2931,18 @@ snapshots: '@noble/hashes@2.0.1': {} + '@openrouter/ai-sdk-provider@2.0.2(ai@6.0.49(zod@4.3.6))(zod@4.3.6)': + dependencies: + '@openrouter/sdk': 0.1.27 + ai: 6.0.49(zod@4.3.6) + zod: 4.3.6 + + '@openrouter/sdk@0.1.27': + dependencies: + zod: 4.3.6 + + '@opentelemetry/api@1.9.0': {} + '@peculiar/asn1-android@2.5.0': dependencies: '@peculiar/asn1-schema': 2.5.0 @@ -3048,23 +3162,25 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} + '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-node@5.5.2(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))': + '@sveltejs/adapter-node@5.5.2(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))': dependencies: '@rollup/plugin-commonjs': 28.0.6(rollup@4.52.4) '@rollup/plugin-json': 6.1.0(rollup@4.52.4) '@rollup/plugin-node-resolve': 16.0.2(rollup@4.52.4) - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) rollup: 4.52.4 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) - '@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))': + '@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) @@ -3083,6 +3199,7 @@ snapshots: svelte: 5.48.2 vite: 7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1) optionalDependencies: + '@opentelemetry/api': 1.9.0 typescript: 5.9.3 '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))': @@ -3324,6 +3441,8 @@ snapshots: '@typescript-eslint/types': 8.53.1 eslint-visitor-keys: 4.2.1 + '@vercel/oidc@3.1.0': {} + '@vitest/browser@3.2.4(playwright@1.58.0)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 @@ -3391,6 +3510,14 @@ snapshots: acorn@8.15.0: {} + ai@6.0.49(zod@4.3.6): + dependencies: + '@ai-sdk/gateway': 3.0.22(zod@4.3.6) + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@4.3.6) + '@opentelemetry/api': 1.9.0 + zod: 4.3.6 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -3428,7 +3555,7 @@ snapshots: balanced-match@1.0.2: {} - better-auth@1.3.27(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + better-auth@1.3.27(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: '@better-auth/core': 1.3.27 '@better-auth/utils': 0.3.0 @@ -3444,7 +3571,7 @@ snapshots: nanostores: 1.0.1 zod: 4.3.6 optionalDependencies: - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) svelte: 5.48.2 better-call@1.0.19: @@ -3457,15 +3584,15 @@ snapshots: bezier-easing@2.1.0: {} - bits-ui@2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + bits-ui@2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.1 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + runed: 0.35.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) svelte: 5.48.2 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) tabbable: 6.2.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -3571,8 +3698,9 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.45.1(kysely@0.27.6)(postgres@3.4.7): + drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(kysely@0.27.6)(postgres@3.4.7): optionalDependencies: + '@opentelemetry/api': 1.9.0 kysely: 0.27.6 postgres: 3.4.7 @@ -3750,6 +3878,8 @@ snapshots: esutils@2.0.3: {} + eventsource-parser@3.0.6: {} + expect-type@1.2.2: {} fast-deep-equal@3.1.3: {} @@ -3863,6 +3993,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -4166,28 +4298,33 @@ snapshots: esm-env: 1.2.2 svelte: 5.48.2 + runed@0.28.0(svelte@5.48.2): + dependencies: + esm-env: 1.2.2 + svelte: 5.48.2 + runed@0.29.2(svelte@5.48.2): dependencies: esm-env: 1.2.2 svelte: 5.48.2 - runed@0.34.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + runed@0.34.0(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 svelte: 5.48.2 optionalDependencies: - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) - runed@0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + runed@0.35.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 svelte: 5.48.2 optionalDependencies: - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) sade@1.8.1: dependencies: @@ -4270,10 +4407,15 @@ snapshots: optionalDependencies: svelte: 5.48.2 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + svelte-sonner@1.0.7(svelte@5.48.2): + dependencies: + runed: 0.28.0(svelte@5.48.2) + svelte: 5.48.2 + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + runed: 0.35.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) style-to-object: 1.0.11 svelte: 5.48.2 transitivePeerDependencies: diff --git a/src/lib/ai/schemas.ts b/src/lib/ai/schemas.ts new file mode 100644 index 0000000..3c3c6f9 --- /dev/null +++ b/src/lib/ai/schemas.ts @@ -0,0 +1,204 @@ +/** + * Zod schemas for AI tool calls + * Defines the structured operations the AI can perform + */ +import { z } from 'zod'; +import { LayerTypeSchema, EasingSchema, AnchorPointSchema } from '$lib/schemas/animation'; + +// ============================================ +// Layer Operations +// ============================================ + +/** + * Add a new layer - props should ALWAYS be specified with meaningful content + */ +export const AddLayerToolSchema = z.object({ + action: z.literal('add_layer'), + type: LayerTypeSchema.describe('The type of layer to create'), + name: z.string().optional().describe('Layer name (auto-generated if not provided)'), + position: z + .object({ + x: z.number().default(0), + y: z.number().default(0) + }) + .optional() + .describe('Initial position on canvas (0,0 is center)'), + props: z + .record(z.string(), z.unknown()) + .describe( + 'Layer-specific properties - MUST include meaningful content (e.g., text content, colors, dimensions)' + ) +}); + +/** + * Edit an existing layer + */ +export const EditLayerToolSchema = z.object({ + action: z.literal('edit_layer'), + layerId: z.string().describe('ID of the layer to edit (use actual ID from project state)'), + updates: z.object({ + name: z.string().optional(), + visible: z.boolean().optional(), + locked: z.boolean().optional(), + transform: z + .object({ + x: z.number().optional(), + y: z.number().optional(), + z: z.number().optional(), + rotationX: z.number().optional(), + rotationY: z.number().optional(), + rotationZ: z.number().optional(), + scaleX: z.number().optional(), + scaleY: z.number().optional(), + scaleZ: z.number().optional(), + anchor: AnchorPointSchema.optional() + }) + .optional(), + style: z + .object({ + opacity: z.number().min(0).max(1).optional() + }) + .optional(), + props: z.record(z.string(), z.unknown()).optional() + }) +}); + +/** + * Remove a layer + */ +export const RemoveLayerToolSchema = z.object({ + action: z.literal('remove_layer'), + layerId: z.string().describe('ID of the layer to remove') +}); + +// ============================================ +// Keyframe Operations +// ============================================ + +/** + * Add a keyframe to a layer + */ +export const AddKeyframeToolSchema = z.object({ + action: z.literal('add_keyframe'), + layerId: z + .string() + .describe( + 'ID of the layer - use "layer_0", "layer_1", etc. for newly created layers, or actual ID for existing layers' + ), + keyframe: z.object({ + time: z.number().min(0).describe('Time in seconds'), + property: z + .string() + .describe( + 'Property path: position.x, position.y, position.z, scale.x, scale.y, scale.z, rotation.x, rotation.y, rotation.z, opacity, or props.' + ), + value: z.union([z.number(), z.string(), z.boolean()]).describe('Value at this keyframe'), + easing: EasingSchema.optional().describe('Easing function (defaults to ease-in-out)') + }) +}); + +/** + * Edit an existing keyframe + */ +export const EditKeyframeToolSchema = z.object({ + action: z.literal('edit_keyframe'), + layerId: z.string().describe('ID of the layer'), + keyframeId: z.string().describe('ID of the keyframe to edit'), + updates: z.object({ + time: z.number().min(0).optional(), + value: z.union([z.number(), z.string(), z.boolean()]).optional(), + easing: EasingSchema.optional() + }) +}); + +/** + * Remove a keyframe + */ +export const RemoveKeyframeToolSchema = z.object({ + action: z.literal('remove_keyframe'), + layerId: z.string().describe('ID of the layer'), + keyframeId: z.string().describe('ID of the keyframe to remove') +}); + +// ============================================ +// Animation Preset Operations +// ============================================ + +/** + * Apply an animation preset to a layer + */ +export const ApplyPresetToolSchema = z.object({ + action: z.literal('apply_preset'), + layerId: z + .string() + .describe( + 'ID of the layer - use "layer_0", "layer_1", etc. for newly created layers, or actual ID for existing layers' + ), + presetId: z + .string() + .describe( + 'ID of the preset to apply: fade-in, fade-out, slide-in-left, slide-in-right, slide-in-top, slide-in-bottom, scale-in, scale-out, bounce, rotate-in, pop, typewriter, pulse, shake, float' + ), + startTime: z.number().min(0).default(0).describe('Time to start the animation (seconds)'), + duration: z.number().min(0.1).default(1).describe('Duration of the animation (seconds)') +}); + +/** + * Add multiple keyframes at once (batch operation) + */ +export const BatchKeyframesToolSchema = z.object({ + action: z.literal('batch_keyframes'), + layerId: z.string().describe('ID of the layer'), + keyframes: z.array( + z.object({ + time: z.number().min(0), + property: z.string(), + value: z.union([z.number(), z.string(), z.boolean()]), + easing: EasingSchema.optional() + }) + ) +}); + +// ============================================ +// Combined Schema +// ============================================ + +/** + * All possible AI tool calls + */ +export const AIToolCallSchema = z.discriminatedUnion('action', [ + AddLayerToolSchema, + EditLayerToolSchema, + RemoveLayerToolSchema, + AddKeyframeToolSchema, + EditKeyframeToolSchema, + RemoveKeyframeToolSchema, + ApplyPresetToolSchema, + BatchKeyframesToolSchema +]); + +export type AIToolCall = z.infer; +export type AddLayerTool = z.infer; +export type EditLayerTool = z.infer; +export type RemoveLayerTool = z.infer; +export type AddKeyframeTool = z.infer; +export type EditKeyframeTool = z.infer; +export type RemoveKeyframeTool = z.infer; +export type ApplyPresetTool = z.infer; +export type BatchKeyframesTool = z.infer; + +/** + * AI Response schema - what the model returns + */ +export const AIResponseSchema = z.object({ + message: z + .string() + .max(100) + .describe('A brief, friendly message to show the user (for toast notification, max 100 chars)'), + operations: z + .array(AIToolCallSchema) + .min(1) + .describe('List of operations to perform on the project - must not be empty') +}); + +export type AIResponse = z.infer; diff --git a/src/lib/ai/system-prompt.ts b/src/lib/ai/system-prompt.ts new file mode 100644 index 0000000..14ffa07 --- /dev/null +++ b/src/lib/ai/system-prompt.ts @@ -0,0 +1,617 @@ +/** + * System prompt builder for AI animation generation + * Generates context-aware prompts with layer metadata and project state + */ +import { layerRegistry, type LayerType } from '$lib/layers/registry'; +import { extractPropertyMetadata, extractDefaultValues } from '$lib/layers/base'; +import type { Project, Layer } from '$lib/types/animation'; +import { animationPresets } from '$lib/engine/presets'; + +/** + * Build the system prompt for the AI model + */ +export function buildSystemPrompt(project: Project): string { + return `You are DevMotion AI, a professional motion graphics designer that creates high-quality video animations. You translate natural language descriptions into precise, visually stunning animation sequences. + +## YOUR MISSION +Create professional-quality motion graphics that would impress clients at a top creative agency. Every animation should be: +- Visually polished with thoughtful timing +- Well-composed with clear visual hierarchy +- Engaging with smooth, purposeful motion +- Complete with all necessary content (text, colors, positions) + +## CRITICAL RULES + +### 1. Layer ID References +When adding keyframes to layers you just created, use INDEX-BASED references: +- First add_layer operation creates layer_0 +- Second add_layer operation creates layer_1 +- Third add_layer operation creates layer_2 +- And so on... + +**CORRECT:** +\`\`\`json +{"action": "add_layer", "type": "text", "props": {"content": "Hello"}}, +{"action": "add_keyframe", "layerId": "layer_0", "keyframe": {...}} +\`\`\` + +**WRONG:** +\`\`\`json +{"action": "add_layer", "type": "text", "name": "Title"}, +{"action": "add_keyframe", "layerId": "Title", "keyframe": {...}} +\`\`\` + +For existing layers, use their actual ID from the project state. + +### 2. ALWAYS Specify Props +NEVER leave props empty. Every layer MUST have meaningful content: +- Text layers: ALWAYS set "content" with actual text +- Shape layers: ALWAYS set "fill" color, "width", "height" +- Button layers: ALWAYS set "text", "backgroundColor" +- All layers: Use colors that match a cohesive visual theme + +**CORRECT:** +\`\`\`json +{"action": "add_layer", "type": "text", "props": {"content": "Welcome", "fontSize": 72, "color": "#ffffff", "fontWeight": "bold"}} +\`\`\` + +**WRONG:** +\`\`\`json +{"action": "add_layer", "type": "text", "props": {}} +\`\`\` + +### 3. Complete Animations +Every animated layer needs: +- Initial state keyframes (usually at time=0) +- End state keyframes +- Proper easing for smooth motion + +## COORDINATE SYSTEM +- Canvas: ${project.width}x${project.height} pixels +- Origin (0, 0): CENTER of canvas +- X: negative=left, positive=right (range: -${project.width / 2} to ${project.width / 2}) +- Y: negative=top, positive=bottom (range: -${project.height / 2} to ${project.height / 2}) +- Off-screen positions: x < -${project.width / 2} (left), x > ${project.width / 2} (right), y < -${project.height / 2} (top), y > ${project.height / 2} (bottom) + +## PROJECT SETTINGS +- Duration: ${project.duration} seconds +- FPS: ${project.fps} +- Background: ${project.backgroundColor} + +## LAYER TYPES & REQUIRED PROPS + +${buildLayerTypesReference()} + +## CURRENT PROJECT STATE +${buildProjectStateReference(project)} + +## ANIMATABLE PROPERTIES + +### Transform (use directly): +- position.x, position.y, position.z (pixels) +- rotation.x, rotation.y, rotation.z (radians - Math.PI = 180°) +- scale.x, scale.y, scale.z (1 = 100%, 0.5 = 50%, 2 = 200%) + +### Style: +- opacity (0 = invisible, 1 = fully visible) + +### Layer Props (prefix with "props."): +- props.content (text content) +- props.fontSize (text size) +- props.color (text/fill color - animates as color interpolation) +- props.fill (shape fill color) +- props.width, props.height (dimensions) +- Any other layer-specific property + +## ANIMATION PRESETS +You can apply these presets using the apply_preset action: +${animationPresets.map((p) => `- "${p.id}": ${p.name}`).join('\n')} + +## PROFESSIONAL ANIMATION PRINCIPLES + +### Timing & Spacing +- Entrance animations: 0.3-0.6s (quick, snappy) +- Exit animations: 0.2-0.4s (slightly faster than entrances) +- Text reveals: 0.4-0.8s +- Stagger delay between elements: 0.1-0.2s +- Hold/pause before transitions: 0.5-1.0s + +### Visual Hierarchy +1. Hero elements (titles): Largest, enter first, center or top-center +2. Supporting text: Smaller, enter after hero, below hero +3. CTAs/buttons: Enter last, bottom portion of screen +4. Background elements: Scale/fade subtly, don't compete + +### Motion Design Best Practices +- Use ease-out for entrances (fast start, slow end = "arriving") +- Use ease-in for exits (slow start, fast end = "leaving") +- Use ease-in-out for position changes within the frame +- Add slight overshoot (scale to 1.05 then 1.0) for "pop" effect +- Stagger element entrances for rhythm + +### Color Guidance +- Dark backgrounds: Use bright, high-contrast text (#ffffff, #f0f0f0) +- Light backgrounds: Use dark text (#1a1a1a, #333333) +- Accent colors: Use sparingly for CTAs and highlights +- Maintain 3-4 color palette maximum + +## SCENE STRUCTURE FOR MULTI-ELEMENT VIDEOS + +For a ${project.duration}s video, structure like this: +${generateSceneStructure(project.duration)} + +## RESPONSE FORMAT + +Return a JSON object with: +\`\`\`json +{ + "message": "Brief description (max 100 chars)", + "operations": [/* array of operations */] +} +\`\`\` + +### Operation Types: + +**add_layer** - Create a new layer +\`\`\`json +{ + "action": "add_layer", + "type": "text|shape|button|terminal|phone|browser|mouse", + "name": "Optional name", + "position": {"x": 0, "y": -200}, + "props": {/* REQUIRED: layer-specific props */} +} +\`\`\` + +**add_keyframe** - Add animation keyframe +\`\`\`json +{ + "action": "add_keyframe", + "layerId": "layer_0 or existing-id", + "keyframe": { + "time": 0.5, + "property": "position.x|opacity|scale.x|props.content|etc", + "value": 100, + "easing": {"type": "ease-out"} + } +} +\`\`\` + +**apply_preset** - Apply animation preset +\`\`\`json +{ + "action": "apply_preset", + "layerId": "layer_0", + "presetId": "fade-in|scale-in|slide-in-left|etc", + "startTime": 0.5, + "duration": 0.6 +} +\`\`\` + +**edit_layer** - Modify existing layer +\`\`\`json +{ + "action": "edit_layer", + "layerId": "existing-layer-id", + "updates": { + "props": {"content": "New text"}, + "transform": {"x": 100} + } +} +\`\`\` + +**remove_layer** / **remove_keyframe** / **edit_keyframe** - Other operations + +## COMPLETE EXAMPLE + +User: "Create a modern promo video for a tech product called 'CloudSync'" + +Response: +\`\`\`json +{ + "message": "Created CloudSync promo with animated title, tagline, and CTA", + "operations": [ + { + "action": "add_layer", + "type": "text", + "name": "Title", + "position": {"x": 0, "y": -100}, + "props": { + "content": "CloudSync", + "fontSize": 96, + "fontFamily": "Poppins", + "fontWeight": "bold", + "color": "#ffffff" + } + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0, "property": "opacity", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0, "property": "scale.x", "value": 0.8} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0, "property": "scale.y", "value": 0.8} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0.5, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0.5, "property": "scale.x", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0.5, "property": "scale.y", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_layer", + "type": "text", + "name": "Tagline", + "position": {"x": 0, "y": 50}, + "props": { + "content": "Sync Everything. Everywhere.", + "fontSize": 32, + "fontFamily": "Inter", + "fontWeight": "normal", + "color": "#a0a0a0" + } + }, + { + "action": "add_keyframe", + "layerId": "layer_1", + "keyframe": {"time": 0, "property": "opacity", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_1", + "keyframe": {"time": 0, "property": "position.y", "value": 80} + }, + { + "action": "add_keyframe", + "layerId": "layer_1", + "keyframe": {"time": 0.8, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_1", + "keyframe": {"time": 0.8, "property": "position.y", "value": 50, "easing": {"type": "ease-out"}} + }, + { + "action": "add_layer", + "type": "button", + "name": "CTA", + "position": {"x": 0, "y": 200}, + "props": { + "text": "Get Started Free", + "backgroundColor": "#3b82f6", + "textColor": "#ffffff", + "fontSize": 18, + "fontWeight": "bold", + "borderRadius": 12, + "width": 200, + "height": 56 + } + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 0, "property": "opacity", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 0, "property": "scale.x", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 0, "property": "scale.y", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.2, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.2, "property": "scale.x", "value": 1.05, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.2, "property": "scale.y", "value": 1.05, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.4, "property": "scale.x", "value": 1, "easing": {"type": "ease-in-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.4, "property": "scale.y", "value": 1, "easing": {"type": "ease-in-out"}} + } + ] +} +\`\`\` + +## VIDEO TYPE TEMPLATES + +### Tech Demo / Developer Tutorial +- Use code layer for snippets +- Use terminal for commands +- Use icon layers (code, terminal, rocket) for visual interest +- Dark background (#0f0f0f, #1a1a2e) +- Accent colors: blues (#3b82f6), greens (#22c55e), purples (#8b5cf6) + +### Feature Showcase +- Icon layer for feature icon +- Text for feature name +- Smaller text for description +- Progress bar for stats +- Staggered entrance animations + +### Social Media Ad +- Vertical format (720x1280) +- Large, punchy text +- Bold colors for engagement +- Quick animations (0.3-0.5s) +- CTA prominent at end + +## EXAMPLE: Tech Demo + +User: "Show a terminal with a npm install command" + +\`\`\`json +{ + "message": "Created terminal with npm install animation", + "operations": [ + { + "action": "add_layer", + "type": "terminal", + "name": "Terminal", + "position": {"x": 0, "y": 0}, + "props": { + "title": "Terminal", + "content": "", + "width": 600, + "height": 300, + "backgroundColor": "#1e1e1e", + "textColor": "#22c55e", + "fontSize": 16 + } + }, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.3, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.5, "property": "props.content", "value": ""}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 2, "property": "props.content", "value": "$ npm install devmotion\\n\\nadded 127 packages in 2.3s"}} + ] +} +\`\`\` + +## EXAMPLE: Feature Highlight with Icons + +User: "Show 3 features with icons" + +\`\`\`json +{ + "message": "Created 3 feature highlights with staggered animations", + "operations": [ + {"action": "add_layer", "type": "icon", "name": "Icon1", "position": {"x": -200, "y": -100}, "props": {"icon": "zap", "size": 48, "color": "#fbbf24", "backgroundColor": "#fbbf2420", "backgroundRadius": 12, "backgroundPadding": 16}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0, "property": "scale.x", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0, "property": "scale.y", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.3, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.3, "property": "scale.x", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.3, "property": "scale.y", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "text", "name": "Feature1", "position": {"x": -200, "y": 0}, "props": {"content": "Lightning Fast", "fontSize": 24, "fontWeight": "bold", "color": "#ffffff"}}, + {"action": "add_keyframe", "layerId": "layer_1", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_1", "keyframe": {"time": 0.5, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "icon", "name": "Icon2", "position": {"x": 0, "y": -100}, "props": {"icon": "shield", "size": 48, "color": "#22c55e", "backgroundColor": "#22c55e20", "backgroundRadius": 12, "backgroundPadding": 16}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0, "property": "scale.x", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0, "property": "scale.y", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0.5, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0.5, "property": "scale.x", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0.5, "property": "scale.y", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "text", "name": "Feature2", "position": {"x": 0, "y": 0}, "props": {"content": "Secure", "fontSize": 24, "fontWeight": "bold", "color": "#ffffff"}}, + {"action": "add_keyframe", "layerId": "layer_3", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_3", "keyframe": {"time": 0.7, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "icon", "name": "Icon3", "position": {"x": 200, "y": -100}, "props": {"icon": "sparkles", "size": 48, "color": "#a855f7", "backgroundColor": "#a855f720", "backgroundRadius": 12, "backgroundPadding": 16}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0, "property": "scale.x", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0, "property": "scale.y", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0.7, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0.7, "property": "scale.x", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0.7, "property": "scale.y", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "text", "name": "Feature3", "position": {"x": 200, "y": 0}, "props": {"content": "AI-Powered", "fontSize": 24, "fontWeight": "bold", "color": "#ffffff"}}, + {"action": "add_keyframe", "layerId": "layer_5", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_5", "keyframe": {"time": 0.9, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}} + ] +} +\`\`\` + +Now respond to the user's request with professional-quality animations.`; +} + +/** + * Generate scene structure guidance based on duration + */ +function generateSceneStructure(duration: number): string { + if (duration <= 3) { + return `- 0-0.5s: Main element entrance +- 0.5-${duration - 0.5}s: Display/hold +- ${duration - 0.5}-${duration}s: Optional exit or hold`; + } + + if (duration <= 5) { + return `- 0-0.8s: Hero entrance (title, main visual) +- 0.6-1.2s: Supporting elements (tagline, subtitle) +- 1.0-1.5s: CTA/action elements +- 1.5-${duration - 1}s: Hold/display period +- ${duration - 1}-${duration}s: Optional exits or final emphasis`; + } + + if (duration <= 10) { + return `- 0-1s: Scene 1 - Hero entrance +- 1-2s: Scene 1 - Supporting content +- 2-3s: Transition/hold +- 3-5s: Scene 2 - Feature highlights +- 5-7s: Scene 3 - Benefits/details +- 7-${duration - 1}s: Scene 4 - CTA/conclusion +- ${duration - 1}-${duration}s: Final hold`; + } + + // Longer videos + const sceneCount = Math.floor(duration / 3); + const lines = [`- Structure into ${sceneCount} scenes of ~3s each`]; + for (let i = 0; i < sceneCount; i++) { + const start = i * 3; + const end = Math.min((i + 1) * 3, duration); + lines.push(`- ${start}-${end}s: Scene ${i + 1}`); + } + return lines.join('\n'); +} + +/** + * Build reference documentation for all layer types + */ +function buildLayerTypesReference(): string { + const layerTypes = Object.keys(layerRegistry) as LayerType[]; + + return layerTypes + .map((type) => { + const definition = layerRegistry[type]; + const metadata = extractPropertyMetadata(definition.customPropsSchema); + const defaults = extractDefaultValues(definition.customPropsSchema); + + // Identify required/important props + const requiredProps = getRequiredPropsForType(type); + + const propsDesc = metadata + .map((m) => { + const defaultVal = defaults[m.name]; + let typeDesc: string = m.type; + if (m.options) { + typeDesc = `enum: ${m.options.map((o) => `"${o.value}"`).join(' | ')}`; + } + if (m.min !== undefined || m.max !== undefined) { + typeDesc += ` (${m.min ?? ''}..${m.max ?? ''})`; + } + const defaultStr = + defaultVal !== undefined ? ` [default: ${JSON.stringify(defaultVal)}]` : ''; + const requiredMarker = requiredProps.includes(m.name) ? ' ⚠️ REQUIRED' : ''; + return ` - ${m.name}: ${typeDesc}${defaultStr}${requiredMarker}`; + }) + .join('\n'); + + return `### ${definition.displayName} (type: "${type}") +${getLayerUsageHint(type)} +Props: +${propsDesc}`; + }) + .join('\n\n'); +} + +/** + * Get required props for each layer type + */ +function getRequiredPropsForType(type: LayerType): string[] { + const required: Record = { + text: ['content', 'fontSize', 'color'], + shape: ['shapeType', 'width', 'height', 'fill'], + terminal: ['content', 'width', 'height'], + button: ['text', 'backgroundColor', 'width', 'height'], + phone: ['width', 'height'], + browser: ['width', 'height'], + mouse: ['pointerType'], + icon: ['icon', 'size', 'color'], + divider: ['length', 'thickness', 'color'], + progress: ['progress', 'width', 'progressColor'], + code: ['code', 'language', 'width'] + }; + return required[type] || []; +} + +/** + * Get usage hints for each layer type + */ +function getLayerUsageHint(type: LayerType): string { + const hints: Record = { + text: 'Perfect for titles, taglines, labels. Always set content, fontSize, and color.', + shape: + 'Use for backgrounds, decorations, visual accents. Great for creating geometric patterns.', + terminal: 'Code/command display. Animate props.content for typing effect.', + button: 'Call-to-action elements. Set text, backgroundColor for brand colors.', + phone: 'Mobile device mockup. Great for app demos.', + browser: 'Desktop browser mockup. Perfect for website showcases.', + mouse: 'Cursor animation. Use with position animations to show interactions.', + icon: 'Lucide icons for visual elements. Perfect for feature icons, social media, UI elements.', + divider: 'Lines and separators. Use between sections or as decorative elements.', + progress: 'Progress bars and loading states. Animate props.progress for loading animations.', + code: 'Code snippets with syntax highlighting. Perfect for tech/dev tutorials.' + }; + return hints[type] || ''; +} + +/** + * Build reference of current project state + */ +function buildProjectStateReference(project: Project): string { + if (project.layers.length === 0) { + return 'No layers currently in the project. You will create new layers.'; + } + + const layerList = project.layers + .map((layer, index) => { + const keyframesSummary = + layer.keyframes.length > 0 + ? `\n Keyframes: ${summarizeKeyframes(layer)}` + : '\n No keyframes (static)'; + + const propsStr = Object.entries(layer.props) + .filter(([, v]) => v !== undefined && v !== null) + .slice(0, 5) // Limit to 5 props to keep it concise + .map(([k, v]) => `${k}=${JSON.stringify(v)}`) + .join(', '); + + return `${index}. "${layer.name}" (id: "${layer.id}", type: ${layer.type}) + Position: (${layer.transform.x}, ${layer.transform.y}) + Scale: (${layer.transform.scaleX}, ${layer.transform.scaleY}) + Opacity: ${layer.style.opacity} + Props: { ${propsStr} }${keyframesSummary}`; + }) + .join('\n\n'); + + return `Existing layers (use their IDs for edits): +${layerList}`; +} + +/** + * Summarize keyframes for a layer + */ +function summarizeKeyframes(layer: Layer): string { + const grouped = new Map(); + + for (const kf of layer.keyframes) { + const times = grouped.get(kf.property) || []; + times.push(kf.time); + grouped.set(kf.property, times); + } + + return Array.from(grouped.entries()) + .map(([prop, times]) => `${prop} @ ${times.sort((a, b) => a - b).join('s, ')}s`) + .join('; '); +} diff --git a/src/lib/components/ai/ai-chat.svelte b/src/lib/components/ai/ai-chat.svelte new file mode 100644 index 0000000..698a876 --- /dev/null +++ b/src/lib/components/ai/ai-chat.svelte @@ -0,0 +1,408 @@ + + +
+ + + +
diff --git a/src/lib/components/editor/keyboard-handler.svelte b/src/lib/components/editor/keyboard-handler.svelte index 1266e92..a6abeb9 100644 --- a/src/lib/components/editor/keyboard-handler.svelte +++ b/src/lib/components/editor/keyboard-handler.svelte @@ -58,7 +58,7 @@ // T - Add text layer if (e.key === 't' && !e.metaKey && !e.ctrlKey) { e.preventDefault(); - const layer = createTextLayer(0, 0); + const layer = createTextLayer(); projectStore.addLayer(layer); projectStore.selectedLayerId = layer.id; } @@ -66,7 +66,7 @@ // R - Add rectangle if (e.key === 'r' && !e.metaKey && !e.ctrlKey) { e.preventDefault(); - const layer = createShapeLayer(0, 0); + const layer = createShapeLayer('rectangle'); projectStore.addLayer(layer); projectStore.selectedLayerId = layer.id; } diff --git a/src/lib/components/editor/panels/layers-panel.svelte b/src/lib/components/editor/panels/layers-panel.svelte index 14b22fd..aad8131 100644 --- a/src/lib/components/editor/panels/layers-panel.svelte +++ b/src/lib/components/editor/panels/layers-panel.svelte @@ -16,56 +16,29 @@ Zap, Smartphone, Globe, - Image + Star, + Minus, + Tag, + Loader, + Code } from 'lucide-svelte'; import type { Layer } from '$lib/types/animation'; - import Tooltip from '$lib/components/ui/tooltip'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js'; - import { createTextLayer, createShapeLayer, createLayer } from '$lib/engine/layer-factory'; - - let prompt = ''; - - const readonlyItems = [{ label: 'Image', icon: Image }]; - - // Note: Coordinate system has (0, 0) at canvas center - function addTextLayer() { - const layer = createTextLayer(0, 0); - projectStore.addLayer(layer); - projectStore.selectedLayerId = layer.id; - } - - function addShapeLayer() { - const layer = createShapeLayer(0, 0); - projectStore.addLayer(layer); - projectStore.selectedLayerId = layer.id; - } - - function addTerminalLayer() { - const layer = createLayer('terminal', {}, { x: 0, y: 0 }); - projectStore.addLayer(layer); - projectStore.selectedLayerId = layer.id; - } - - function addMouseLayer() { - const layer = createLayer('mouse', {}, { x: 0, y: 0 }); - projectStore.addLayer(layer); - projectStore.selectedLayerId = layer.id; - } - - function addButtonLayer() { - const layer = createLayer('button', {}, { x: 0, y: 0 }); - projectStore.addLayer(layer); - projectStore.selectedLayerId = layer.id; - } - - function addPhoneLayer() { - const layer = createLayer('phone', {}, { x: 0, y: 0 }); - projectStore.addLayer(layer); - projectStore.selectedLayerId = layer.id; + import { createLayer } from '$lib/engine/layer-factory'; + import AiChat from '$lib/components/ai/ai-chat.svelte'; + import { toast } from 'svelte-sonner'; + + function handleAiMessage(message: string, type: 'success' | 'error') { + if (type === 'success') { + toast.success(message); + } else { + toast.error(message); + } } - function addBrowserLayer() { - const layer = createLayer('browser', {}, { x: 0, y: 0 }); + // Note: Coordinate system has (0, 0) at canvas center + function addLayer(type: string) { + const layer = createLayer(type as import('$lib/types/animation').LayerType, {}, { x: 0, y: 0 }); projectStore.addLayer(layer); projectStore.selectedLayerId = layer.id; } @@ -131,43 +104,64 @@ - - + + Basic + addLayer('text')}> Text - addShapeLayer()}> + addLayer('shape')}> Shape - - - - Terminal GUI - - - - Mouse Pointer + addLayer('icon')}> + + Icon - + addLayer('button')}> Button - + + + UI Elements + addLayer('progress')}> + + Progress + + addLayer('divider')}> + + Divider + + + + Code & Terminal + addLayer('terminal')}> + + Terminal + + addLayer('code')}> + + Code Block + + + + Mockups + addLayer('phone')}> Phone - + addLayer('browser')}> Browser - {#each readonlyItems as item (item.label)} - - {@const Icon = item.icon} - - {item.label} - - {/each} + + + Interaction + addLayer('mouse')}> + + Mouse Cursor + @@ -249,17 +243,6 @@ - -
- - - - - -
+ + diff --git a/src/lib/engine/layer-factory.ts b/src/lib/engine/layer-factory.ts index 62202d5..d05565c 100644 --- a/src/lib/engine/layer-factory.ts +++ b/src/lib/engine/layer-factory.ts @@ -6,6 +6,11 @@ import type { Layer, LayerType, Keyframe, Easing } from '$lib/types/animation'; import { getLayerDefinition } from '$lib/layers/registry'; import { extractDefaultValues } from '$lib/layers/base'; +/** + * Default easing for initial keyframes + */ +const defaultEasing: Easing = { type: 'ease-in-out' }; + /** * Create a new layer of the specified type * @param type - The layer type from the registry @@ -23,8 +28,6 @@ export function createLayer( // Extract default values from the Zod schema const defaultProps = extractDefaultValues(definition.customPropsSchema); - const defaultEasing: Easing = { type: 'ease-in-out' }; - // Create initial keyframes for position properties const initialKeyframes: Keyframe[] = [ { @@ -72,23 +75,238 @@ export function createLayer( }; } +// ============================================ +// Convenience functions for common layer types +// ============================================ + +/** + * Create a text layer + */ +export function createTextLayer( + content: string = 'New Text', + options: { + x?: number; + y?: number; + fontSize?: number; + color?: string; + fontWeight?: string; + fontFamily?: string; + } = {} +): Layer { + const { + x = 0, + y = 0, + fontSize = 48, + color = '#ffffff', + fontWeight = 'normal', + fontFamily = 'Inter' + } = options; + return createLayer('text', { content, fontSize, color, fontWeight, fontFamily }, { x, y }); +} + +/** + * Create a shape layer + */ +export function createShapeLayer( + shapeType: 'rectangle' | 'circle' | 'triangle' | 'polygon' = 'rectangle', + options: { + x?: number; + y?: number; + width?: number; + height?: number; + fill?: string; + } = {} +): Layer { + const { x = 0, y = 0, width = 200, height = 200, fill = '#4a90e2' } = options; + return createLayer('shape', { shapeType, width, height, fill }, { x, y }); +} + +/** + * Create an icon layer + */ +export function createIconLayer( + icon: string = 'star', + options: { + x?: number; + y?: number; + size?: number; + color?: string; + backgroundColor?: string; + backgroundRadius?: number; + backgroundPadding?: number; + } = {} +): Layer { + const { + x = 0, + y = 0, + size = 64, + color = '#ffffff', + backgroundColor = 'transparent', + backgroundRadius = 0, + backgroundPadding = 0 + } = options; + return createLayer( + 'icon', + { icon, size, color, backgroundColor, backgroundRadius, backgroundPadding }, + { x, y } + ); +} + +/** + * Create a button layer + */ +export function createButtonLayer( + text: string = 'Click me', + options: { + x?: number; + y?: number; + backgroundColor?: string; + textColor?: string; + width?: number; + height?: number; + borderRadius?: number; + } = {} +): Layer { + const { + x = 0, + y = 0, + backgroundColor = '#3b82f6', + textColor = '#ffffff', + width = 160, + height = 48, + borderRadius = 8 + } = options; + return createLayer( + 'button', + { text, backgroundColor, textColor, width, height, borderRadius }, + { x, y } + ); +} + +/** + * Create a progress layer + */ +export function createProgressLayer( + progress: number = 75, + options: { + x?: number; + y?: number; + width?: number; + progressColor?: string; + backgroundColor?: string; + } = {} +): Layer { + const { + x = 0, + y = 0, + width = 300, + progressColor = '#3b82f6', + backgroundColor = '#333333' + } = options; + return createLayer('progress', { progress, width, progressColor, backgroundColor }, { x, y }); +} + +/** + * Create a divider layer + */ +export function createDividerLayer( + options: { + x?: number; + y?: number; + length?: number; + thickness?: number; + color?: string; + orientation?: 'horizontal' | 'vertical'; + } = {} +): Layer { + const { + x = 0, + y = 0, + length = 200, + thickness = 2, + color = '#ffffff', + orientation = 'horizontal' + } = options; + return createLayer('divider', { length, thickness, color, orientation }, { x, y }); +} + +/** + * Create a terminal layer + */ +export function createTerminalLayer( + content: string = '$ Welcome to terminal', + options: { + x?: number; + y?: number; + width?: number; + height?: number; + title?: string; + } = {} +): Layer { + const { x = 0, y = 0, width = 600, height = 400, title = 'Terminal' } = options; + return createLayer('terminal', { content, width, height, title }, { x, y }); +} + +/** + * Create a code layer + */ +export function createCodeLayer( + code: string = 'const hello = "world";', + options: { + x?: number; + y?: number; + width?: number; + language?: string; + fileName?: string; + } = {} +): Layer { + const { x = 0, y = 0, width = 500, language = 'javascript', fileName = 'index.js' } = options; + return createLayer('code', { code, width, language, fileName }, { x, y }); +} + /** - * Convenience function for creating text layers + * Create a phone mockup layer */ -export function createTextLayer(x = 0, y = 0): Layer { - return createLayer('text', {}, { x, y }); +export function createPhoneLayer( + url: string = 'https://example.com', + options: { + x?: number; + y?: number; + width?: number; + height?: number; + } = {} +): Layer { + const { x = 0, y = 0, width = 375, height = 667 } = options; + return createLayer('phone', { url, width, height }, { x, y }); } /** - * Convenience function for creating shape layers + * Create a browser mockup layer */ -export function createShapeLayer(x = 0, y = 0): Layer { - return createLayer('shape', {}, { x, y }); +export function createBrowserLayer( + url: string = 'https://example.com', + options: { + x?: number; + y?: number; + width?: number; + height?: number; + } = {} +): Layer { + const { x = 0, y = 0, width = 800, height = 500 } = options; + return createLayer('browser', { url, width, height }, { x, y }); } /** - * Convenience function for creating image layers + * Create a mouse cursor layer */ -export function createImageLayer(src: string, x = 0, y = 0): Layer { - return createLayer('image', { src }, { x, y }); +export function createMouseLayer( + options: { + x?: number; + y?: number; + pointerType?: 'arrow' | 'pointer' | 'hand' | 'crosshair' | 'text'; + color?: string; + } = {} +): Layer { + const { x = 0, y = 0, pointerType = 'arrow', color = '#ffffff' } = options; + return createLayer('mouse', { pointerType, color }, { x, y }); } diff --git a/src/lib/engine/presets.ts b/src/lib/engine/presets.ts index 1be74de..a3e96c7 100644 --- a/src/lib/engine/presets.ts +++ b/src/lib/engine/presets.ts @@ -1,205 +1,324 @@ /** - * Animation presets + * Animation presets - reusable animation patterns + * These can be applied to any layer by the AI using apply_preset action */ import type { AnimationPreset } from '$lib/types/animation'; +/** + * All available animation presets + * Keyframe times are normalized (0-1) and will be scaled by duration when applied + */ export const animationPresets: AnimationPreset[] = [ + // ============================================ + // Fade animations + // ============================================ { id: 'fade-in', name: 'Fade In', keyframes: [ - { - time: 0, - property: 'opacity', - value: 0, - easing: { type: 'ease-out' } - }, - { - time: 1, - property: 'opacity', - value: 1, - easing: { type: 'linear' } - } + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } ] }, { id: 'fade-out', name: 'Fade Out', keyframes: [ - { - time: 0, - property: 'opacity', - value: 1, - easing: { type: 'ease-in' } - }, - { - time: 1, - property: 'opacity', - value: 0, - easing: { type: 'linear' } - } + { time: 0, property: 'opacity', value: 1, easing: { type: 'ease-in' } }, + { time: 1, property: 'opacity', value: 0, easing: { type: 'linear' } } ] }, + + // ============================================ + // Slide animations + // ============================================ { id: 'slide-in-left', - name: 'Slide In Left', + name: 'Slide In from Left', keyframes: [ - { - time: 0, - property: 'position.x', - value: -500, - easing: { type: 'ease-out' } - }, - { - time: 1, - property: 'position.x', - value: 0, - easing: { type: 'linear' } - } + { time: 0, property: 'position.x', value: -500, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'position.x', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } ] }, { id: 'slide-in-right', - name: 'Slide In Right', + name: 'Slide In from Right', keyframes: [ - { - time: 0, - property: 'position.x', - value: 500, - easing: { type: 'ease-out' } - }, - { - time: 1, - property: 'position.x', - value: 0, - easing: { type: 'linear' } - } + { time: 0, property: 'position.x', value: 500, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'position.x', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } ] }, { id: 'slide-in-top', - name: 'Slide In Top', + name: 'Slide In from Top', keyframes: [ - { - time: 0, - property: 'position.y', - value: -300, - easing: { type: 'ease-out' } - }, - { - time: 1, - property: 'position.y', - value: 0, - easing: { type: 'linear' } - } + { time: 0, property: 'position.y', value: -400, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'position.y', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } ] }, { id: 'slide-in-bottom', - name: 'Slide In Bottom', + name: 'Slide In from Bottom', keyframes: [ - { - time: 0, - property: 'position.y', - value: 300, - easing: { type: 'ease-out' } - }, - { - time: 1, - property: 'position.y', - value: 0, - easing: { type: 'linear' } - } + { time: 0, property: 'position.y', value: 400, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'position.y', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } ] }, { - id: 'bounce', - name: 'Bounce', + id: 'slide-out-left', + name: 'Slide Out to Left', keyframes: [ - { - time: 0, - property: 'scale.y', - value: 1, - easing: { type: 'ease-in-out' } - }, - { - time: 0.3, - property: 'scale.y', - value: 1.2, - easing: { type: 'ease-in-out' } - }, - { - time: 0.6, - property: 'scale.y', - value: 0.9, - easing: { type: 'ease-in-out' } - }, - { - time: 1, - property: 'scale.y', - value: 1, - easing: { type: 'linear' } - } + { time: 0, property: 'position.x', value: 0, easing: { type: 'ease-in' } }, + { time: 0, property: 'opacity', value: 1, easing: { type: 'ease-in' } }, + { time: 1, property: 'position.x', value: -500, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 0, easing: { type: 'linear' } } ] }, + { + id: 'slide-out-right', + name: 'Slide Out to Right', + keyframes: [ + { time: 0, property: 'position.x', value: 0, easing: { type: 'ease-in' } }, + { time: 0, property: 'opacity', value: 1, easing: { type: 'ease-in' } }, + { time: 1, property: 'position.x', value: 500, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 0, easing: { type: 'linear' } } + ] + }, + + // ============================================ + // Scale animations + // ============================================ { id: 'scale-in', name: 'Scale In', keyframes: [ - { - time: 0, - property: 'scale.x', - value: 0, - easing: { type: 'ease-out' } - }, - { - time: 0, - property: 'scale.y', - value: 0, - easing: { type: 'ease-out' } - }, - { - time: 1, - property: 'scale.x', - value: 1, - easing: { type: 'linear' } - }, - { - time: 1, - property: 'scale.y', - value: 1, - easing: { type: 'linear' } - } + { time: 0, property: 'scale.x', value: 0, easing: { type: 'ease-out' } }, + { time: 0, property: 'scale.y', value: 0, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'scale.x', value: 1, easing: { type: 'linear' } }, + { time: 1, property: 'scale.y', value: 1, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } + ] + }, + { + id: 'scale-out', + name: 'Scale Out', + keyframes: [ + { time: 0, property: 'scale.x', value: 1, easing: { type: 'ease-in' } }, + { time: 0, property: 'scale.y', value: 1, easing: { type: 'ease-in' } }, + { time: 0, property: 'opacity', value: 1, easing: { type: 'ease-in' } }, + { time: 1, property: 'scale.x', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'scale.y', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 0, easing: { type: 'linear' } } + ] + }, + { + id: 'pop', + name: 'Pop (Scale with Overshoot)', + keyframes: [ + { time: 0, property: 'scale.x', value: 0, easing: { type: 'ease-out' } }, + { time: 0, property: 'scale.y', value: 0, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 0.6, property: 'scale.x', value: 1.15, easing: { type: 'ease-out' } }, + { time: 0.6, property: 'scale.y', value: 1.15, easing: { type: 'ease-out' } }, + { time: 0.6, property: 'opacity', value: 1, easing: { type: 'ease-out' } }, + { time: 1, property: 'scale.x', value: 1, easing: { type: 'ease-in-out' } }, + { time: 1, property: 'scale.y', value: 1, easing: { type: 'ease-in-out' } } + ] + }, + + // ============================================ + // Bounce animations + // ============================================ + { + id: 'bounce', + name: 'Bounce', + keyframes: [ + { time: 0, property: 'scale.y', value: 1, easing: { type: 'ease-in-out' } }, + { time: 0.25, property: 'scale.y', value: 1.2, easing: { type: 'ease-in-out' } }, + { time: 0.5, property: 'scale.y', value: 0.9, easing: { type: 'ease-in-out' } }, + { time: 0.75, property: 'scale.y', value: 1.05, easing: { type: 'ease-in-out' } }, + { time: 1, property: 'scale.y', value: 1, easing: { type: 'linear' } } ] }, + { + id: 'bounce-in', + name: 'Bounce In', + keyframes: [ + { time: 0, property: 'scale.x', value: 0, easing: { type: 'ease-out' } }, + { time: 0, property: 'scale.y', value: 0, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 0.4, property: 'scale.x', value: 1.2, easing: { type: 'ease-out' } }, + { time: 0.4, property: 'scale.y', value: 1.2, easing: { type: 'ease-out' } }, + { time: 0.4, property: 'opacity', value: 1, easing: { type: 'linear' } }, + { time: 0.6, property: 'scale.x', value: 0.9, easing: { type: 'ease-in-out' } }, + { time: 0.6, property: 'scale.y', value: 0.9, easing: { type: 'ease-in-out' } }, + { time: 0.8, property: 'scale.x', value: 1.05, easing: { type: 'ease-in-out' } }, + { time: 0.8, property: 'scale.y', value: 1.05, easing: { type: 'ease-in-out' } }, + { time: 1, property: 'scale.x', value: 1, easing: { type: 'ease-in-out' } }, + { time: 1, property: 'scale.y', value: 1, easing: { type: 'ease-in-out' } } + ] + }, + + // ============================================ + // Rotation animations + // ============================================ { id: 'rotate-in', name: 'Rotate In', keyframes: [ - { - time: 0, - property: 'rotation.z', - value: -Math.PI, - easing: { type: 'ease-out' } - }, - { - time: 0, - property: 'opacity', - value: 0, - easing: { type: 'ease-out' } - }, - { - time: 1, - property: 'rotation.z', - value: 0, - easing: { type: 'linear' } - }, - { - time: 1, - property: 'opacity', - value: 1, - easing: { type: 'linear' } - } + { time: 0, property: 'rotation.z', value: -Math.PI, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 0, property: 'scale.x', value: 0.5, easing: { type: 'ease-out' } }, + { time: 0, property: 'scale.y', value: 0.5, easing: { type: 'ease-out' } }, + { time: 1, property: 'rotation.z', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } }, + { time: 1, property: 'scale.x', value: 1, easing: { type: 'linear' } }, + { time: 1, property: 'scale.y', value: 1, easing: { type: 'linear' } } + ] + }, + { + id: 'spin', + name: 'Spin (Full Rotation)', + keyframes: [ + { time: 0, property: 'rotation.z', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'rotation.z', value: Math.PI * 2, easing: { type: 'linear' } } + ] + }, + + // ============================================ + // Attention/emphasis animations + // ============================================ + { + id: 'pulse', + name: 'Pulse', + keyframes: [ + { time: 0, property: 'scale.x', value: 1, easing: { type: 'ease-in-out' } }, + { time: 0, property: 'scale.y', value: 1, easing: { type: 'ease-in-out' } }, + { time: 0.5, property: 'scale.x', value: 1.1, easing: { type: 'ease-in-out' } }, + { time: 0.5, property: 'scale.y', value: 1.1, easing: { type: 'ease-in-out' } }, + { time: 1, property: 'scale.x', value: 1, easing: { type: 'ease-in-out' } }, + { time: 1, property: 'scale.y', value: 1, easing: { type: 'ease-in-out' } } + ] + }, + { + id: 'shake', + name: 'Shake', + keyframes: [ + { time: 0, property: 'position.x', value: 0, easing: { type: 'linear' } }, + { time: 0.1, property: 'position.x', value: -10, easing: { type: 'linear' } }, + { time: 0.2, property: 'position.x', value: 10, easing: { type: 'linear' } }, + { time: 0.3, property: 'position.x', value: -10, easing: { type: 'linear' } }, + { time: 0.4, property: 'position.x', value: 10, easing: { type: 'linear' } }, + { time: 0.5, property: 'position.x', value: -5, easing: { type: 'linear' } }, + { time: 0.6, property: 'position.x', value: 5, easing: { type: 'linear' } }, + { time: 0.7, property: 'position.x', value: -2, easing: { type: 'linear' } }, + { time: 0.8, property: 'position.x', value: 2, easing: { type: 'linear' } }, + { time: 1, property: 'position.x', value: 0, easing: { type: 'linear' } } + ] + }, + { + id: 'float', + name: 'Float (Subtle Up/Down)', + keyframes: [ + { time: 0, property: 'position.y', value: 0, easing: { type: 'ease-in-out' } }, + { time: 0.5, property: 'position.y', value: -15, easing: { type: 'ease-in-out' } }, + { time: 1, property: 'position.y', value: 0, easing: { type: 'ease-in-out' } } + ] + }, + { + id: 'glow', + name: 'Glow (Opacity Pulse)', + keyframes: [ + { time: 0, property: 'opacity', value: 1, easing: { type: 'ease-in-out' } }, + { time: 0.5, property: 'opacity', value: 0.6, easing: { type: 'ease-in-out' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'ease-in-out' } } + ] + }, + + // ============================================ + // Zoom animations + // ============================================ + { + id: 'zoom-in', + name: 'Zoom In (from far)', + keyframes: [ + { time: 0, property: 'scale.x', value: 0.3, easing: { type: 'ease-out' } }, + { time: 0, property: 'scale.y', value: 0.3, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'scale.x', value: 1, easing: { type: 'linear' } }, + { time: 1, property: 'scale.y', value: 1, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } + ] + }, + { + id: 'zoom-out', + name: 'Zoom Out (to far)', + keyframes: [ + { time: 0, property: 'scale.x', value: 1, easing: { type: 'ease-in' } }, + { time: 0, property: 'scale.y', value: 1, easing: { type: 'ease-in' } }, + { time: 0, property: 'opacity', value: 1, easing: { type: 'ease-in' } }, + { time: 1, property: 'scale.x', value: 0.3, easing: { type: 'linear' } }, + { time: 1, property: 'scale.y', value: 0.3, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 0, easing: { type: 'linear' } } + ] + }, + + // ============================================ + // Special effects + // ============================================ + { + id: 'flip-in-x', + name: 'Flip In (Horizontal)', + keyframes: [ + { time: 0, property: 'rotation.y', value: Math.PI / 2, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'rotation.y', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } + ] + }, + { + id: 'flip-in-y', + name: 'Flip In (Vertical)', + keyframes: [ + { time: 0, property: 'rotation.x', value: Math.PI / 2, easing: { type: 'ease-out' } }, + { time: 0, property: 'opacity', value: 0, easing: { type: 'ease-out' } }, + { time: 1, property: 'rotation.x', value: 0, easing: { type: 'linear' } }, + { time: 1, property: 'opacity', value: 1, easing: { type: 'linear' } } + ] + }, + { + id: 'swing', + name: 'Swing', + keyframes: [ + { time: 0, property: 'rotation.z', value: 0, easing: { type: 'ease-in-out' } }, + { time: 0.2, property: 'rotation.z', value: 0.26, easing: { type: 'ease-in-out' } }, // ~15deg + { time: 0.4, property: 'rotation.z', value: -0.17, easing: { type: 'ease-in-out' } }, // ~-10deg + { time: 0.6, property: 'rotation.z', value: 0.09, easing: { type: 'ease-in-out' } }, // ~5deg + { time: 0.8, property: 'rotation.z', value: -0.05, easing: { type: 'ease-in-out' } }, // ~-3deg + { time: 1, property: 'rotation.z', value: 0, easing: { type: 'ease-in-out' } } ] } ]; + +/** + * Get a preset by ID + */ +export function getPresetById(id: string): AnimationPreset | undefined { + return animationPresets.find((p) => p.id === id); +} + +/** + * Get all preset IDs + */ +export function getPresetIds(): string[] { + return animationPresets.map((p) => p.id); +} diff --git a/src/lib/functions/ai.remote.ts b/src/lib/functions/ai.remote.ts new file mode 100644 index 0000000..045fa89 --- /dev/null +++ b/src/lib/functions/ai.remote.ts @@ -0,0 +1,72 @@ +/** + * AI Generation Remote Function + * Handles AI-powered animation generation using Gemini via OpenRouter + */ +import { command, getRequestEvent } from '$app/server'; +import { z } from 'zod'; +import { withErrorHandling } from '.'; +import { createOpenRouter } from '@openrouter/ai-sdk-provider'; +import { generateText, Output } from 'ai'; +import { AIResponseSchema } from '$lib/ai/schemas'; +import { buildSystemPrompt } from '$lib/ai/system-prompt'; +import { env } from '$env/dynamic/private'; +import { ProjectSchema } from '$lib/schemas/animation'; + +/** + * Request schema for AI generation + */ + +const GenerateRequestSchema = z.object({ + prompt: z.string().min(1, 'Prompt is required').max(2_000, 'Prompt too long'), + project: ProjectSchema +}); + +/** + * Create OpenRouter-compatible client + */ +function getOpenRouterClient() { + const apiKey = env.OPENROUTER_API_KEY; + if (!apiKey) { + throw new Error('OPENROUTER_API_KEY is not configured'); + } + return createOpenRouter({ + apiKey + }); +} + +/** + * Generate animation operations from a natural language prompt + */ +export const generateAnimation = command( + GenerateRequestSchema, + withErrorHandling(async ({ prompt, project }) => { + const { locals } = getRequestEvent(); + + // Optional: require authentication + // Uncomment if you want to restrict AI generation to logged-in users + if (!locals.user) { + throw new Error('Authentication required'); + } + + const openrouter = getOpenRouterClient(); + const systemPrompt = buildSystemPrompt(project); + + console.log({ systemPrompt }); + + const { output } = await generateText({ + model: openrouter('google/gemini-3-pro-preview'), + output: Output.object({ + schema: AIResponseSchema + }), + system: systemPrompt, + prompt: prompt + }); + + console.log('AI Output:', JSON.stringify(output, null, 2)); + + return { + message: output.message, + operations: output.operations + }; + }) +); diff --git a/src/lib/functions/projects.remote.ts b/src/lib/functions/projects.remote.ts index d774f26..139040a 100644 --- a/src/lib/functions/projects.remote.ts +++ b/src/lib/functions/projects.remote.ts @@ -1,11 +1,12 @@ import { command, getRequestEvent, query } from '$app/server'; import { db } from '$lib/server/db'; -import { project, projectDataSchema } from '$lib/server/db/schema'; +import { project } from '$lib/server/db/schema'; import { eq, and, desc } from 'drizzle-orm'; import { z } from 'zod'; import { withErrorHandling } from '.'; import { nanoid } from 'nanoid'; import { invalid } from '@sveltejs/kit'; +import { projectDataSchema } from '$lib/schemas/animation'; export const saveProject = command( z.object({ diff --git a/src/lib/layers/CodeLayer.svelte b/src/lib/layers/CodeLayer.svelte new file mode 100644 index 0000000..f3c275f --- /dev/null +++ b/src/lib/layers/CodeLayer.svelte @@ -0,0 +1,238 @@ + + + + +
+ {#if showHeader} +
+
+
+
+
+
+ {fileName} +
+ {/if} + +
+ + + {#each lines as line, i (i)} + + {#if showLineNumbers} + + {/if} + + + {/each} + +
+ {i + 1} + + + {@html highlightLine(line, language)} +
+
+
diff --git a/src/lib/layers/DividerLayer.svelte b/src/lib/layers/DividerLayer.svelte new file mode 100644 index 0000000..8c24ca3 --- /dev/null +++ b/src/lib/layers/DividerLayer.svelte @@ -0,0 +1,96 @@ + + + + +{#if style === 'solid'} +
+{:else} +
`${k}:${v}`) + .join(';')} + style:border-radius={rounded ? `${thickness / 2}px` : '0'} + style:opacity + >
+{/if} diff --git a/src/lib/layers/IconLayer.svelte b/src/lib/layers/IconLayer.svelte new file mode 100644 index 0000000..8ac2b02 --- /dev/null +++ b/src/lib/layers/IconLayer.svelte @@ -0,0 +1,255 @@ + + + + +
+ +
diff --git a/src/lib/layers/ProgressLayer.svelte b/src/lib/layers/ProgressLayer.svelte new file mode 100644 index 0000000..8c827be --- /dev/null +++ b/src/lib/layers/ProgressLayer.svelte @@ -0,0 +1,120 @@ + + + + +
+ {#if showLabel && labelPosition === 'top'} + + {Math.round(clampedProgress)}% + + {/if} + +
+
+ {#if showLabel && labelPosition === 'inside' && height >= 16} + + {Math.round(clampedProgress)}% + + {/if} +
+ + {#if animated} +
+ {/if} +
+ + {#if showLabel && labelPosition === 'right'} + + {Math.round(clampedProgress)}% + + {/if} +
+ + diff --git a/src/lib/layers/registry.ts b/src/lib/layers/registry.ts index 55e6588..4b0dd82 100644 --- a/src/lib/layers/registry.ts +++ b/src/lib/layers/registry.ts @@ -9,6 +9,10 @@ import MouseLayer, { schema as MouseLayerPropsSchema } from './MouseLayer.svelte import ButtonLayer, { schema as ButtonLayerPropsSchema } from './ButtonLayer.svelte'; import PhoneLayer, { schema as PhoneLayerPropsSchema } from './PhoneLayer.svelte'; import BrowserLayer, { schema as BrowserLayerPropsSchema } from './BrowserLayer.svelte'; +import IconLayer, { schema as IconLayerPropsSchema } from './IconLayer.svelte'; +import DividerLayer, { schema as DividerLayerPropsSchema } from './DividerLayer.svelte'; +import ProgressLayer, { schema as ProgressLayerPropsSchema } from './ProgressLayer.svelte'; +import CodeLayer, { schema as CodeLayerPropsSchema } from './CodeLayer.svelte'; import type { LayerComponentDefinition } from './base'; /** @@ -50,7 +54,7 @@ export const layerRegistry: Record = { button: { type: 'button', displayName: 'Button', - icon: 'ClickSquare', + icon: 'SquareMousePointer', customPropsSchema: ButtonLayerPropsSchema, component: ButtonLayer }, @@ -69,6 +73,38 @@ export const layerRegistry: Record = { icon: 'Globe', customPropsSchema: BrowserLayerPropsSchema, component: BrowserLayer + }, + + icon: { + type: 'icon', + displayName: 'Icon', + icon: 'Star', + customPropsSchema: IconLayerPropsSchema, + component: IconLayer + }, + + divider: { + type: 'divider', + displayName: 'Divider', + icon: 'Minus', + customPropsSchema: DividerLayerPropsSchema, + component: DividerLayer + }, + + progress: { + type: 'progress', + displayName: 'Progress', + icon: 'Loader', + customPropsSchema: ProgressLayerPropsSchema, + component: ProgressLayer + }, + + code: { + type: 'code', + displayName: 'Code', + icon: 'Code', + customPropsSchema: CodeLayerPropsSchema, + component: CodeLayer } } as const; diff --git a/src/lib/schemas/animation.ts b/src/lib/schemas/animation.ts new file mode 100644 index 0000000..aef6e44 --- /dev/null +++ b/src/lib/schemas/animation.ts @@ -0,0 +1,216 @@ +/** + * Zod schemas for animation types + * Single source of truth - TypeScript types are inferred from these schemas + */ +import { z } from 'zod'; + +// ============================================ +// Easing +// ============================================ + +export const CubicBezierPointsSchema = z.object({ + x1: z.number(), + y1: z.number(), + x2: z.number(), + y2: z.number() +}); + +export const EasingTypeSchema = z.enum([ + 'linear', + 'ease-in', + 'ease-out', + 'ease-in-out', + 'cubic-bezier' +]); + +export const EasingSchema = z.object({ + type: EasingTypeSchema, + bezier: CubicBezierPointsSchema.optional() +}); + +// ============================================ +// Animatable Properties +// ============================================ + +export const BuiltInAnimatablePropertySchema = z.enum([ + 'position.x', + 'position.y', + 'position.z', + 'scale.x', + 'scale.y', + 'scale.z', + 'rotation.x', + 'rotation.y', + 'rotation.z', + 'opacity', + 'color' +]); + +// For props properties like props.fontSize, props.fill +export const PropsAnimatablePropertySchema = z.string().regex(/^props\./); + +export const AnimatablePropertySchema = z.union([ + BuiltInAnimatablePropertySchema, + PropsAnimatablePropertySchema +]); + +export const InterpolationTypeSchema = z.enum(['number', 'color', 'text', 'discrete']); + +// ============================================ +// Transform & Style +// ============================================ + +export const AnchorPointSchema = z.enum([ + 'top-left', + 'top-center', + 'top-right', + 'center-left', + 'center', + 'center-right', + 'bottom-left', + 'bottom-center', + 'bottom-right' +]); + +export const TransformSchema = z.object({ + x: z.number(), + y: z.number(), + z: z.number(), + rotationX: z.number(), + rotationY: z.number(), + rotationZ: z.number(), + scaleX: z.number(), + scaleY: z.number(), + scaleZ: z.number(), + anchor: AnchorPointSchema +}); + +export const LayerStyleSchema = z.object({ + opacity: z.number().min(0).max(1) +}); + +// ============================================ +// Keyframe +// ============================================ + +export const KeyframeSchema = z.object({ + id: z.string(), + time: z.number().min(0), + property: AnimatablePropertySchema, + value: z.union([z.number(), z.string(), z.boolean()]), + easing: EasingSchema +}); + +// ============================================ +// Layer Types +// ============================================ + +export const LayerTypeSchema = z.enum([ + 'text', + 'shape', + 'terminal', + 'mouse', + 'button', + 'phone', + 'browser', + 'icon', + 'divider', + 'progress', + 'code' +]); + +// ============================================ +// Layer +// ============================================ + +export const LayerSchema = z.object({ + id: z.string(), + name: z.string(), + type: LayerTypeSchema, + transform: TransformSchema, + style: LayerStyleSchema, + visible: z.boolean(), + locked: z.boolean(), + keyframes: z.array(KeyframeSchema), + /** + * Layer-specific properties validated by the component's Zod schema. + * Kept flexible to allow each layer type to define its own props. + */ + props: z.record(z.string(), z.unknown()) +}); + +// ============================================ +// Project +// ============================================ + +export const ProjectSchema = z.object({ + id: z.string(), + name: z.string(), + width: z.number().positive(), + height: z.number().positive(), + duration: z.number().positive(), + fps: z.number().positive(), + backgroundColor: z.string(), + layers: z.array(LayerSchema), + currentTime: z.number().min(0) +}); + +// ============================================ +// Viewport & Export Settings +// ============================================ + +export const ViewportSettingsSchema = z.object({ + zoom: z.number(), + pan: z.object({ + x: z.number(), + y: z.number() + }), + showGuides: z.boolean(), + snapToGrid: z.boolean() +}); + +export const AnimationPresetSchema = z.object({ + id: z.string(), + name: z.string(), + keyframes: z.array(KeyframeSchema.omit({ id: true })) +}); + +export const ExportSettingsSchema = z.object({ + format: z.enum(['mp4', 'webm', 'json']), + quality: z.enum(['low', 'medium', 'high']), + fps: z.number().positive(), + width: z.number().positive(), + height: z.number().positive() +}); + +// ============================================ +// TypeScript Types (inferred from schemas) +// ============================================ + +export type CubicBezierPoints = z.infer; +export type EasingType = z.infer; +export type Easing = z.infer; + +export type BuiltInAnimatableProperty = z.infer; +export type PropsAnimatableProperty = z.infer; +export type AnimatableProperty = z.infer; +export type InterpolationType = z.infer; + +export type AnchorPoint = z.infer; +export type Transform = z.infer; +export type LayerStyle = z.infer; + +export type Keyframe = z.infer; + +export type LayerType = z.infer; +export type Layer = z.infer; + +export type Project = z.infer; + +export const projectDataSchema = ProjectSchema.omit({ id: true }); + +export type ProjectData = z.infer; + +export type ViewportSettings = z.infer; +export type AnimationPreset = z.infer; +export type ExportSettings = z.infer; diff --git a/src/lib/server/db/schema/projects.ts b/src/lib/server/db/schema/projects.ts index cbe99f7..cbba37e 100644 --- a/src/lib/server/db/schema/projects.ts +++ b/src/lib/server/db/schema/projects.ts @@ -8,21 +8,7 @@ import { type AnyPgColumn } from 'drizzle-orm/pg-core'; import { user } from './auth'; -import z from 'zod'; -import type { Layer } from '$lib/types/animation'; - -export const projectDataSchema = z.object({ - name: z.string().min(1), - width: z.number().positive(), - height: z.number().positive(), - duration: z.number().positive(), - fps: z.number().positive(), - backgroundColor: z.string(), - layers: z.custom(), - currentTime: z.number() -}); - -export type ProjectData = z.infer; +import { type ProjectData } from '$lib/schemas/animation'; export const project = pgTable( 'project', diff --git a/src/lib/types/animation.ts b/src/lib/types/animation.ts index 48ac339..f06cbc1 100644 --- a/src/lib/types/animation.ts +++ b/src/lib/types/animation.ts @@ -1,155 +1,46 @@ /** * Core types for the animation editor - */ -import type { LayerType } from '$lib/layers/registry'; - -// Re-export LayerType for convenience -export type { LayerType }; - -export type EasingType = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'cubic-bezier'; - -export interface CubicBezierPoints { - x1: number; - y1: number; - x2: number; - y2: number; -} - -export interface Easing { - type: EasingType; - bezier?: CubicBezierPoints; -} - -/** - * Built-in animatable properties for transform and style - */ -export type BuiltInAnimatableProperty = - | 'position.x' - | 'position.y' - | 'position.z' - | 'scale.x' - | 'scale.y' - | 'scale.z' - | 'rotation.x' - | 'rotation.y' - | 'rotation.z' - | 'opacity' - | 'color'; - -/** - * Dynamic props property (e.g., props.fontSize, props.fill) - */ -export type PropsAnimatableProperty = `props.${string}`; - -/** - * All animatable properties - built-in or dynamic props - */ -export type AnimatableProperty = BuiltInAnimatableProperty | PropsAnimatableProperty; - -/** - * Interpolation type for a property - * - 'number': Linear interpolation between numeric values - * - 'color': Color interpolation in RGB space - * - 'text': Character-by-character text reveal - * - 'discrete': Jump to new value (no interpolation) - */ -export type InterpolationType = 'number' | 'color' | 'text' | 'discrete'; - -/** - * Anchor point for layer positioning - * Determines which point of the layer is placed at the position coordinates - */ -export type AnchorPoint = - | 'top-left' - | 'top-center' - | 'top-right' - | 'center-left' - | 'center' - | 'center-right' - | 'bottom-left' - | 'bottom-center' - | 'bottom-right'; - -export interface Keyframe { - id: string; - time: number; // in seconds - property: AnimatableProperty; - value: number | string | boolean; - easing: Easing; -} - -/** - * Transform properties using flat structure (aligned with BaseTransform in layers/base.ts) - */ -export interface Transform { - x: number; - y: number; - z: number; - rotationX: number; - rotationY: number; - rotationZ: number; - scaleX: number; - scaleY: number; - scaleZ: number; - anchor: AnchorPoint; -} - -/** - * Base style properties shared by all layers - */ -export interface LayerStyle { - opacity: number; -} - -/** - * Generic Layer - component-based architecture - * Props are defined and validated by each layer component's Zod schema - */ -export interface Layer { - id: string; - name: string; - type: LayerType; - transform: Transform; - style: LayerStyle; - visible: boolean; - locked: boolean; - keyframes: Keyframe[]; - /** - * Layer-specific properties validated by the component's Zod schema. - * Use getLayerSchema(layer.type) from registry to get the schema for validation. - */ - props: Record; -} - -export interface Project { - id: string; - name: string; - width: number; // canvas width - height: number; // canvas height - duration: number; // in seconds - fps: number; - backgroundColor: string; - layers: Layer[]; - currentTime: number; -} - -export interface ViewportSettings { - zoom: number; - pan: { x: number; y: number }; - showGuides: boolean; - snapToGrid: boolean; -} - -export interface AnimationPreset { - id: string; - name: string; - keyframes: Omit[]; -} - -export interface ExportSettings { - format: 'mp4' | 'webm' | 'json'; - quality: 'low' | 'medium' | 'high'; - fps: number; - width: number; - height: number; -} + * These types are re-exported from Zod schemas for consistency and validation + */ + +// Re-export all types from schemas (single source of truth) +export type { + CubicBezierPoints, + EasingType, + Easing, + BuiltInAnimatableProperty, + PropsAnimatableProperty, + AnimatableProperty, + InterpolationType, + AnchorPoint, + Transform, + LayerStyle, + Keyframe, + LayerType, + Layer, + Project, + ViewportSettings, + AnimationPreset, + ExportSettings +} from '$lib/schemas/animation'; + +// Re-export schemas for validation +export { + CubicBezierPointsSchema, + EasingTypeSchema, + EasingSchema, + BuiltInAnimatablePropertySchema, + PropsAnimatablePropertySchema, + AnimatablePropertySchema, + InterpolationTypeSchema, + AnchorPointSchema, + TransformSchema, + LayerStyleSchema, + KeyframeSchema, + LayerTypeSchema, + LayerSchema, + ProjectSchema, + ViewportSettingsSchema, + AnimationPresetSchema, + ExportSettingsSchema +} from '$lib/schemas/animation'; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fd96528..271cef8 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,6 +3,7 @@ import { PUBLIC_BASE_URL } from '$env/static/public'; import { asset } from '$app/paths'; import JsonLd from '$lib/components/json-ld.svelte'; + import { Toaster } from 'svelte-sonner'; // SEO Configuration for DevMotion const baseUrl = PUBLIC_BASE_URL; @@ -98,4 +99,5 @@ }} /> + {@render children?.()} From 86d24a2edcfd8924ab7e849aae8dc202006adc13 Mon Sep 17 00:00:00 2001 From: EmaDev Date: Mon, 26 Jan 2026 23:30:27 +0100 Subject: [PATCH 2/2] cleanup layers --- src/lib/ai/system-prompt.ts | 8 +- .../components/editor/keyboard-handler.svelte | 6 +- .../editor/panels/layers-panel.svelte | 80 +----- src/lib/engine/layer-factory.ts | 240 +----------------- src/lib/layers/DividerLayer.svelte | 96 ------- src/lib/layers/base.ts | 29 +-- .../{ => components}/BrowserLayer.svelte | 13 +- .../{ => components}/ButtonLayer.svelte | 13 +- .../layers/{ => components}/CodeLayer.svelte | 13 +- .../layers/{ => components}/IconLayer.svelte | 19 +- .../layers/{ => components}/ImageLayer.svelte | 14 +- .../layers/{ => components}/MouseLayer.svelte | 13 +- .../layers/{ => components}/PhoneLayer.svelte | 13 +- .../{ => components}/ProgressLayer.svelte | 13 +- .../layers/{ => components}/ShapeLayer.svelte | 13 +- .../{ => components}/TerminalLayer.svelte | 13 +- .../layers/{ => components}/TextLayer.svelte | 15 +- src/lib/layers/index.ts | 10 - src/lib/layers/registry.ts | 142 +++-------- src/lib/schemas/animation.ts | 1 - 20 files changed, 187 insertions(+), 577 deletions(-) delete mode 100644 src/lib/layers/DividerLayer.svelte rename src/lib/layers/{ => components}/BrowserLayer.svelte (96%) rename src/lib/layers/{ => components}/ButtonLayer.svelte (90%) rename src/lib/layers/{ => components}/CodeLayer.svelte (95%) rename src/lib/layers/{ => components}/IconLayer.svelte (93%) rename src/lib/layers/{ => components}/ImageLayer.svelte (73%) rename src/lib/layers/{ => components}/MouseLayer.svelte (83%) rename src/lib/layers/{ => components}/PhoneLayer.svelte (87%) rename src/lib/layers/{ => components}/ProgressLayer.svelte (93%) rename src/lib/layers/{ => components}/ShapeLayer.svelte (91%) rename src/lib/layers/{ => components}/TerminalLayer.svelte (87%) rename src/lib/layers/{ => components}/TextLayer.svelte (90%) delete mode 100644 src/lib/layers/index.ts diff --git a/src/lib/ai/system-prompt.ts b/src/lib/ai/system-prompt.ts index 14ffa07..1f70d22 100644 --- a/src/lib/ai/system-prompt.ts +++ b/src/lib/ai/system-prompt.ts @@ -494,8 +494,8 @@ function buildLayerTypesReference(): string { return layerTypes .map((type) => { const definition = layerRegistry[type]; - const metadata = extractPropertyMetadata(definition.customPropsSchema); - const defaults = extractDefaultValues(definition.customPropsSchema); + const metadata = extractPropertyMetadata(definition.schema); + const defaults = extractDefaultValues(definition.schema); // Identify required/important props const requiredProps = getRequiredPropsForType(type); @@ -517,7 +517,7 @@ function buildLayerTypesReference(): string { }) .join('\n'); - return `### ${definition.displayName} (type: "${type}") + return `### ${definition.label} (type: "${type}") ${getLayerUsageHint(type)} Props: ${propsDesc}`; @@ -538,7 +538,6 @@ function getRequiredPropsForType(type: LayerType): string[] { browser: ['width', 'height'], mouse: ['pointerType'], icon: ['icon', 'size', 'color'], - divider: ['length', 'thickness', 'color'], progress: ['progress', 'width', 'progressColor'], code: ['code', 'language', 'width'] }; @@ -559,7 +558,6 @@ function getLayerUsageHint(type: LayerType): string { browser: 'Desktop browser mockup. Perfect for website showcases.', mouse: 'Cursor animation. Use with position animations to show interactions.', icon: 'Lucide icons for visual elements. Perfect for feature icons, social media, UI elements.', - divider: 'Lines and separators. Use between sections or as decorative elements.', progress: 'Progress bars and loading states. Animate props.progress for loading animations.', code: 'Code snippets with syntax highlighting. Perfect for tech/dev tutorials.' }; diff --git a/src/lib/components/editor/keyboard-handler.svelte b/src/lib/components/editor/keyboard-handler.svelte index a6abeb9..11c9b69 100644 --- a/src/lib/components/editor/keyboard-handler.svelte +++ b/src/lib/components/editor/keyboard-handler.svelte @@ -1,7 +1,7 @@ - - - -{#if style === 'solid'} -
-{:else} -
`${k}:${v}`) - .join(';')} - style:border-radius={rounded ? `${thickness / 2}px` : '0'} - style:opacity - >
-{/if} diff --git a/src/lib/layers/base.ts b/src/lib/layers/base.ts index 1086f3f..d04a51a 100644 --- a/src/lib/layers/base.ts +++ b/src/lib/layers/base.ts @@ -3,6 +3,7 @@ */ import { z } from 'zod'; import type { Component } from 'svelte'; +import type { LayerMeta } from './registry'; /** * Anchor point options for layer positioning @@ -61,35 +62,11 @@ export type BaseLayerProps = z.infer; /** * Layer component definition with schema and component */ -export interface LayerComponentDefinition< - T extends z.ZodObject = z.ZodObject -> { - /** - * Unique identifier for this layer type - */ - type: string; - - /** - * Display name for UI - */ - displayName: string; - - /** - * Icon component or name - */ - icon?: string; - - /** - * Zod schema for this layer's custom properties - * This schema is merged with BaseLayerSchema at runtime - */ - customPropsSchema: T; - +export interface LayerComponentDefinition extends LayerMeta { /** * The Svelte component that renders this layer */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: Component; + component: Component; } /** diff --git a/src/lib/layers/BrowserLayer.svelte b/src/lib/layers/components/BrowserLayer.svelte similarity index 96% rename from src/lib/layers/BrowserLayer.svelte rename to src/lib/layers/components/BrowserLayer.svelte index 5fe05c3..a0b38a3 100644 --- a/src/lib/layers/BrowserLayer.svelte +++ b/src/lib/layers/components/BrowserLayer.svelte @@ -1,10 +1,12 @@