diff --git a/bun.lock b/bun.lock index 58aa1264..a5119c4c 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "ajv": "^8.18.0", "husky": "^9.1.7", "prettier": "^3.7.4", + "route-graphics": "1.7.0", "vitest": "^4.0.16", }, }, @@ -98,6 +99,28 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@pixi/color": ["@pixi/color@7.4.3", "", { "dependencies": { "@pixi/colord": "^2.9.6" } }, "sha512-a6R+bXKeXMDcRmjYQoBIK+v2EYqxSX49wcjAY579EYM/WrFKS98nSees6lqVUcLKrcQh2DT9srJHX7XMny3voQ=="], + + "@pixi/colord": ["@pixi/colord@2.9.6", "", {}, "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA=="], + + "@pixi/constants": ["@pixi/constants@7.4.3", "", {}, "sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ=="], + + "@pixi/core": ["@pixi/core@7.4.3", "", { "dependencies": { "@pixi/color": "7.4.3", "@pixi/constants": "7.4.3", "@pixi/extensions": "7.4.3", "@pixi/math": "7.4.3", "@pixi/runner": "7.4.3", "@pixi/settings": "7.4.3", "@pixi/ticker": "7.4.3", "@pixi/utils": "7.4.3" } }, "sha512-5YDs11faWgVVTL8VZtLU05/Fl47vaP5Tnsbf+y/WRR0VSW3KhRRGTBU1J3Gdc2xEWbJhUK07KGP7eSZpvtPVgA=="], + + "@pixi/extensions": ["@pixi/extensions@7.4.3", "", {}, "sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw=="], + + "@pixi/math": ["@pixi/math@7.4.3", "", {}, "sha512-/uJOVhR2DOZ+zgdI6Bs/CwcXT4bNRKsS+TqX3ekRIxPCwaLra+Qdm7aDxT5cTToDzdxbKL5+rwiLu3Y1egILDw=="], + + "@pixi/runner": ["@pixi/runner@7.4.3", "", {}, "sha512-TJyfp7y23u5vvRAyYhVSa7ytq0PdKSvPLXu4G3meoFh1oxTLHH6g/RIzLuxUAThPG2z7ftthuW3qWq6dRV+dhw=="], + + "@pixi/settings": ["@pixi/settings@7.4.3", "", { "dependencies": { "@pixi/constants": "7.4.3", "@types/css-font-loading-module": "^0.0.12", "ismobilejs": "^1.1.0" } }, "sha512-SmGK8smc0PxRB9nr0UJioEtE9hl4gvj9OedCvZx3bxBwA3omA5BmP3CyhQfN8XJ29+o2OUL01r3zAPVol4l4lA=="], + + "@pixi/ticker": ["@pixi/ticker@7.4.3", "", { "dependencies": { "@pixi/extensions": "7.4.3", "@pixi/settings": "7.4.3", "@pixi/utils": "7.4.3" } }, "sha512-tHsAD0iOUb6QSGGw+c8cyRBvxsq/NlfzIFBZLEHhWZ+Bx4a0MmXup6I/yJDGmyPCYE+ctCcAfY13wKAzdiVFgQ=="], + + "@pixi/unsafe-eval": ["@pixi/unsafe-eval@7.4.3", "", { "peerDependencies": { "@pixi/core": "7.4.3" } }, "sha512-iR2iLcMculCSmn3x446ICCL5iokmFuLZDgNo7k1Q5rzNiULELIixfpSJwvlJqrJPY1kda0oqGHgVGRQ2vGfaLg=="], + + "@pixi/utils": ["@pixi/utils@7.4.3", "", { "dependencies": { "@pixi/color": "7.4.3", "@pixi/constants": "7.4.3", "@pixi/settings": "7.4.3", "@types/earcut": "^2.1.0", "earcut": "^2.2.4", "eventemitter3": "^4.0.0", "url": "^0.11.0" } }, "sha512-NO3Y9HAn2UKS1YdxffqsPp+kDpVm8XWvkZcS/E+rBzY9VTLnNOI7cawSRm+dacdET3a8Jad3aDKEDZ0HmAqAFA=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -150,8 +173,12 @@ "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], + "@types/css-font-loading-module": ["@types/css-font-loading-module@0.0.12", "", {}, "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/earcut": ["@types/earcut@3.0.0", "", {}, "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ=="], + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="], @@ -172,6 +199,10 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -186,6 +217,10 @@ "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -196,16 +231,28 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -222,10 +269,26 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gifuct-js": ["gifuct-js@2.1.2", "", { "dependencies": { "js-binary-schema-parser": "^2.0.3" } }, "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg=="], + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hotkeys-js": ["hotkeys-js@4.0.3", "", {}, "sha512-adK3E0KGXiIm8rcRhn2JHEvCVzgg54UtHtufxHVWM0I95hakyxn80aX6ez0nxVEdbzzgrb9g2lMJIThFSyTSFQ=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], @@ -236,6 +299,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "ismobilejs": ["ismobilejs@1.1.1", "", {}, "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], @@ -248,6 +313,8 @@ "jempl": ["jempl@1.0.0", "", {}, "sha512-tYWOHqlIwev20Z47k8m1FeLM2EVxG91JY3ogMR166hdzUN1KeOGHxDhRj8Wfh0PmYIVWf57LB+fTUT/vOahoUg=="], + "js-binary-schema-parser": ["js-binary-schema-parser@2.0.3", "", {}, "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="], + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], @@ -264,6 +331,8 @@ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -274,10 +343,14 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parse-svg-path": ["parse-svg-path@0.1.2", "", {}, "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], @@ -288,22 +361,38 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pixi.js": ["pixi.js@8.17.1", "", { "dependencies": { "@pixi/colord": "^2.9.6", "@types/earcut": "^3.0.0", "@webgpu/types": "^0.1.69", "@xmldom/xmldom": "^0.8.11", "earcut": "^3.0.2", "eventemitter3": "^5.0.1", "gifuct-js": "^2.1.2", "ismobilejs": "^1.1.1", "parse-svg-path": "^0.1.2", "tiny-lru": "^11.4.7" } }, "sha512-OB4TpZHrP5RYy+7FqmFrAc0IHRhfOoNIfF4sVeinvK3aG1r2pYrSMneJAKi9+WvGKC70Dj7GEpZ2OZGB6o/xdg=="], + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], + "punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], + "puty": ["puty@0.1.1", "", { "dependencies": { "js-yaml": "~4.1.0" }, "peerDependencies": { "vitest": ">=1.0.0" } }, "sha512-B5GQiWDIsnbolNVXGRFX/2tmH9uavszQ/uWlsLEgo0mYh4/+YgB20u5QIxpk7TYa3geOkLohvRFtPm3qku372A=="], + "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + "route-graphics": ["route-graphics@1.7.0", "", { "dependencies": { "@pixi/unsafe-eval": "^7.4.3", "hotkeys-js": "^4.0.0-beta.7", "pixi.js": "^8.7.1" } }, "sha512-70yMFdQyuuSHNwdtFcA0YYaCF0qZl3JJXTXsLFrl9N4HwqRTYO9Iepyp9Aqfi587dZRjqjTz7pk935TiFdYWtQ=="], + "semver": ["semver@7.6.3", "", { "bin": "bin/semver.js" }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -328,6 +417,8 @@ "test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="], + "tiny-lru": ["tiny-lru@11.4.7", "", {}, "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], @@ -340,6 +431,8 @@ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "url": ["url@0.11.4", "", { "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" } }, "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="], @@ -354,6 +447,12 @@ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "@pixi/utils/@types/earcut": ["@types/earcut@2.1.4", "", {}, "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ=="], + + "@pixi/utils/earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="], + + "@pixi/utils/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "@vitest/expect/@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], "@vitest/expect/tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], diff --git a/docs/AnimationModel.md b/docs/AnimationModel.md index ea641bf4..df64b960 100644 --- a/docs/AnimationModel.md +++ b/docs/AnimationModel.md @@ -2,10 +2,8 @@ ## Status -This note records the intended animation model and authoring direction. - -Some current engine schema and fixtures still expose `in/out/update` fields. -Those are compatibility shapes. The target design is the one described below. +This note records the current animation model and the background-specific +behavior implemented in the engine. ## Decision @@ -22,7 +20,7 @@ There are only two structural animation kinds: `in`, `out`, and `replace` are not separate structural types. They are semantic cases of `transition`. -At the engine authoring layer, the direction is to converge on a single animation reference: +At the engine authoring layer, animations use a single reference: ```yaml background: @@ -36,11 +34,13 @@ The referenced animation resource declares the structural type through its own ` - `type: update` - `type: transition` -That means the engine does not need separate author-facing fields like: +Legacy resource types such as `live` and `replace` are not supported. + +The legacy wrapper fields are no longer supported: - `animations.in.resourceId` - `animations.out.resourceId` -- `animations.replace.resourceId` +- `animations.update.resourceId` ## Motivation @@ -68,6 +68,55 @@ In practice: If a `transition` resolves to the same compatible visual subject on both sides, the runtime may optimize execution into a single-subject tween. That is an execution optimization, not a separate authoring concept. +## Current Background Behavior + +Background animation dispatch is based on resolved presentation state, not only +the raw action shape. + +### Animations-only background actions + +If a line provides: + +```yaml +background: + animations: + resourceId: bg-slide-out +``` + +and a background already exists in presentation state, the engine resolves the +next background as: + +- same `resourceId` as the currently resolved background +- updated `animations` + +That means this shape means "animate the current background from state", not +"remove the background". + +### Same-subject transitions + +Because the comparison is done against resolved previous and next presentation +state: + +- repeating the same `background.resourceId` with a `transition` animates +- omitting `background.resourceId` and providing only `background.animations` + still animates if the background persists from state + +For persisted backgrounds, the runtime treats the resolved subject as both the +`prev` and `next` side of the `transition`. + +### Background update fallback + +Background currently has one narrow compatibility fallback: + +- if the referenced animation resource is `type: update` +- and the resolved lifecycle is not true `update` +- and there is an incoming background target + +the engine animates the incoming background target instead of throwing. + +This fallback exists for background enter/replace handling only. It should be +treated as compatibility behavior, not the general rule for all element types. + ## State Rule Animation-only values are not written back into stored presentation state. @@ -85,6 +134,11 @@ On completion: - `transition` commits the authored next state if `next` exists - `transition` commits removal if `next` does not exist +For backgrounds specifically, an animations-only action may still resolve to a +persistent next background state if the previous presentation state already has +one. In that case the persisted `resourceId` comes from state resolution, not +from animation transforms being written back. + ## Consequences - high-level APIs like background and character do not need separate `in/out` animation fields diff --git a/docs/RouteEngine.md b/docs/RouteEngine.md index 849113b5..6b328df9 100644 --- a/docs/RouteEngine.md +++ b/docs/RouteEngine.md @@ -63,7 +63,9 @@ engine.init({ For browser-backed save/load hydration, the runtime also exports `createIndexedDbPersistence({ namespace })`. Use the same `namespace` both when loading persisted data before init and when calling `engine.init(...)` so -different visual novels on the same domain do not share persistence. +different visual novels on the same domain do not share persistence. The +returned adapter also exposes `clear()` to delete persisted data for that +namespace. Localization is not implemented in the current runtime. The planned patch-based l10n model is documented in [L10n.md](./L10n.md). @@ -389,18 +391,18 @@ Playback timing semantics: ### State Management Actions -| Action | Payload | Description | -| ------------------- | -------------------- | ------------------------------ | -| `setNextLineConfig` | `{ manual?, auto? }` | Configure line advancement | -| `updateProjectData` | `{ projectData }` | Replace project data | +| Action | Payload | Description | +| ------------------- | -------------------- | ------------------------------- | +| `setNextLineConfig` | `{ manual?, auto? }` | Configure line advancement | +| `updateProjectData` | `{ projectData }` | Replace project data | | `resetStorySession` | - | Reset story-local session state | ### Registry Actions -| Action | Payload | Description | -| ------------------- | ----------------------- | ----------------------------- | -| `addViewedLine` | `{ sectionId, lineId }` | Mark line as viewed | -| `addViewedResource` | `{ resourceId }` | Mark resource as viewed | +| Action | Payload | Description | +| ------------------- | ----------------------- | ----------------------- | +| `addViewedLine` | `{ sectionId, lineId }` | Mark line as viewed | +| `addViewedResource` | `{ resourceId }` | Mark resource as viewed | Seen-line semantics: @@ -411,10 +413,10 @@ Seen-line semantics: ### Save System Actions -| Action | Payload | Description | -| -------------- | --------------------------------- | -------------------------- | -| `saveSlot` | `{ slotId, thumbnailImage? }` | Save game to a slot | -| `loadSlot` | `{ slotId }` | Load game from a slot | +| Action | Payload | Description | +| ---------- | ----------------------------- | --------------------- | +| `saveSlot` | `{ slotId, thumbnailImage? }` | Save game to a slot | +| `loadSlot` | `{ slotId }` | Load game from a slot | Save/load design, requirements, and storage boundaries are documented in [SaveLoad.md](./SaveLoad.md). Story-session reset semantics are documented in [StorySessionReset.md](./StorySessionReset.md). @@ -432,12 +434,12 @@ Notes: These actions exist inside the store/runtime but are not part of the stable authored/public API surface: -| Action | Payload | Description | -| --------------------- | ---------------------- | ---------------------- | +| Action | Payload | Description | +| --------------------- | ---------------------- | ----------------------------------- | | `markLineCompleted` | - | Internal render-complete transition | | `nextLineFromSystem` | - | Internal timer-driven advance | -| `appendPendingEffect` | `{ name, ...options }` | Queue a side effect | -| `clearPendingEffects` | - | Clear the effect queue | +| `appendPendingEffect` | `{ name, ...options }` | Queue a side effect | +| `clearPendingEffects` | - | Clear the effect queue | Use these only if you are extending engine internals or writing engine-level tests. diff --git a/docs/SaveLoad.md b/docs/SaveLoad.md index 1c70c457..60a87cbd 100644 --- a/docs/SaveLoad.md +++ b/docs/SaveLoad.md @@ -390,6 +390,7 @@ The host app is responsible for: - hydrating `initialState.global.saveSlots` from durable storage before engine init - hydrating persistent global variables before engine init - choosing and reusing a per-VN `namespace` during persistence hydration and `engine.init(...)` +- calling `createIndexedDbPersistence({ namespace }).clear()` when the host wants to wipe one VN's persisted data - providing thumbnail image payloads when a save action wants one - mapping dynamic UI/event data into the action `slotId` field when save/load is triggered from generated UI - executing storage effects emitted by the engine diff --git a/docs/vt-guidelines.md b/docs/vt-guidelines.md index 8e6567d5..21d2586d 100644 --- a/docs/vt-guidelines.md +++ b/docs/vt-guidelines.md @@ -1,6 +1,6 @@ # VT Guidelines -Last updated: 2026-03-24 +Last updated: 2026-04-09 ## Purpose @@ -13,20 +13,15 @@ Define one stable standard for VT authoring in this repo: ## Runtime Notes - The VT Docker flow runs Chromium in a software WebGL fallback path on this repo's current container/runtime setup. -- Full-size `1920x1080` Pixi capture on that path is expensive, so VT syncs a local ignored `vt/static/RouteGraphics.js` bundle before VT runs. -- The sync script downloads `route-graphics@1.1.4/dist/RouteGraphics.js` from jsDelivr by default. -- Override the source with `VT_ROUTE_GRAPHICS_URL`, or just the package version with `VT_ROUTE_GRAPHICS_VERSION`. -- That VT-only bundle is patched during sync to initialize Pixi with `resolution: 0.5` and `preserveDrawingBuffer: true`. -- Full-frame VT references are exported from that native half-resolution render surface. -- Keep this path VT-only. Do not copy these settings into the product runtime without a separate rendering review. -- Do not switch VT back to the CDN `route-graphics` import unless you re-benchmark the Docker capture path. +- VT now copies the published `route-graphics` package bundle from `node_modules` into the ignored local `vt/static/RouteGraphics.js` path during `bun run esbuild.js`. +- VT captures keep the existing `960x540` reference size by downscaling screenshot output inside the VT harness, not by patching `RouteGraphics`. +- Keep this path VT-only. Do not copy VT-specific screenshot scaling into the product runtime without a separate rendering review. ## VT Workflow - Capture screenshots with `bun run vt:docker`. - Generate the comparison report with `bun run vt:report`. - Accept an intentional visual change with `rtgl vt accept`. -- If the sync step needs a different upstream build, point `VT_ROUTE_GRAPHICS_URL` at the desired CDN file. - Local VT defaults to `2` Docker workers and a `60000ms` timeout. - Local VT reports default to a `0.8%` diff threshold to tolerate small Docker text raster drift. - VT is currently local-only. GitHub Actions VT is disabled again until the runner instability is fixed separately. diff --git a/esbuild.js b/esbuild.js index f67d4899..46892aa4 100644 --- a/esbuild.js +++ b/esbuild.js @@ -1,4 +1,25 @@ import esbuild from "esbuild"; +import fs from "node:fs"; +import path from "node:path"; + +const copyVtRouteGraphicsBundle = () => { + const source = path.resolve( + "node_modules", + "route-graphics", + "dist", + "RouteGraphics.js", + ); + const target = path.resolve("vt", "static", "RouteGraphics.js"); + + if (!fs.existsSync(source)) { + throw new Error( + "Missing route-graphics dist bundle. Run `bun install` before building VT assets.", + ); + } + + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(source, target); +}; esbuild .build({ @@ -14,6 +35,8 @@ esbuild console.log("Build failed"); }); +copyVtRouteGraphicsBundle(); + esbuild .build({ bundle: true, diff --git a/package.json b/package.json index 9ea15d26..55675a03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "route-engine-js", - "version": "0.7.4", + "version": "0.7.5", "description": "A lightweight Visual Novel engine built in JavaScript for creating interactive narrative games with branching storylines", "repository": { "type": "git", @@ -40,12 +40,11 @@ "lint": "bun run prettier src -c", "lint:fix": "bun run prettier src -w", "coverage": "bun run vitest run --coverage", - "vt:sync-route-graphics": "bun scripts/sync-vt-route-graphics.js", - "vt:generate": "bun scripts/sync-vt-route-graphics.js && bun run esbuild.js && rtgl vt generate", - "vt:docker": "bun scripts/sync-vt-route-graphics.js && bun run esbuild.js && docker run --rm ${VT_DOCKER_ARGS:-} --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -v \"$PWD:/app\" -w /app docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10 rtgl vt screenshot --wait-event vt:ready --concurrency ${VT_DOCKER_CONCURRENCY:-2} --timeout ${VT_DOCKER_TIMEOUT:-60000}", + "vt:generate": "bun run esbuild.js && rtgl vt generate", + "vt:docker": "bun run esbuild.js && docker run --rm ${VT_DOCKER_ARGS:-} --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -v \"$PWD:/app\" -w /app docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10 rtgl vt screenshot --wait-event vt:ready --concurrency ${VT_DOCKER_CONCURRENCY:-2} --timeout ${VT_DOCKER_TIMEOUT:-60000}", "vt:report": "bun run vt:docker && rtgl vt report --diff-threshold ${VT_REPORT_DIFF_THRESHOLD:-0.8}", "vt:accept": "rtgl vt accept", - "serve": "bun run esbuild.js && bunx serve -p 3004 .rettangoli/vt/_site" + "serve": "bun run vt:generate && bunx serve -p 3004 .rettangoli/vt/_site" }, "dependencies": { "immer": "^10.1.1", @@ -59,6 +58,7 @@ "ajv": "^8.18.0", "husky": "^9.1.7", "prettier": "^3.7.4", + "route-graphics": "1.7.0", "vitest": "^4.0.16" } } diff --git a/scripts/sync-vt-route-graphics.js b/scripts/sync-vt-route-graphics.js deleted file mode 100644 index cca449f6..00000000 --- a/scripts/sync-vt-route-graphics.js +++ /dev/null @@ -1,44 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const repoRoot = path.resolve(__dirname, ".."); -const targetPath = path.resolve(repoRoot, "vt/static/RouteGraphics.js"); -const defaultVersion = process.env.VT_ROUTE_GRAPHICS_VERSION || "1.1.4"; -const sourceUrl = - process.env.VT_ROUTE_GRAPHICS_URL || - `https://cdn.jsdelivr.net/npm/route-graphics@${defaultVersion}/dist/RouteGraphics.js`; -const initPattern = /await r\.init\(\{([^}]*)preference:"webgl"\}\)/; - -const response = await fetch(sourceUrl); - -if (!response.ok) { - console.error( - `Failed to download RouteGraphics from ${sourceUrl}: ` + - `${response.status} ${response.statusText}`, - ); - process.exit(1); -} - -const source = await response.text(); - -if (!initPattern.test(source)) { - console.error( - `Could not find the expected Pixi init call in ${sourceUrl}. ` + - "Upstream RouteGraphics.js likely changed and the VT sync patch needs an update.", - ); - process.exit(1); -} - -const patched = source.replace( - initPattern, - (_, initPrefix) => - `await r.init({${initPrefix}preference:"webgl",resolution:(globalThis.RTGL_VT_DEBUG||navigator.webdriver)? .5 : 1,preserveDrawingBuffer:!!(globalThis.RTGL_VT_DEBUG||navigator.webdriver),clearBeforeRender:!0})`, -); - -fs.mkdirSync(path.dirname(targetPath), { recursive: true }); -fs.writeFileSync(targetPath, patched); - -console.log(`Synced VT RouteGraphics from ${sourceUrl} to ${targetPath}`); diff --git a/spec/indexedDbPersistence.test.js b/spec/indexedDbPersistence.test.js index 427b6000..e3d37ce4 100644 --- a/spec/indexedDbPersistence.test.js +++ b/spec/indexedDbPersistence.test.js @@ -63,6 +63,18 @@ class FakeObjectStore { return request; } + + delete(key) { + const request = new FakeRequest(); + + this.transaction.track(() => { + this.definition.records.delete(key); + request.result = undefined; + request.onsuccess?.({ target: request }); + }); + + return request; + } } class FakeTransaction { @@ -262,6 +274,44 @@ describe("indexedDbPersistence", () => { ); }); + it("clears persisted data for a single namespace", async () => { + const indexedDB = createFakeIndexedDB(); + const alphaPersistence = createIndexedDbPersistence({ + indexedDB, + namespace: "vn-alpha", + }); + const betaPersistence = createIndexedDbPersistence({ + indexedDB, + namespace: "vn-beta", + }); + + await alphaPersistence.saveSlots({ + 1: { + slotId: 1, + savedAt: 1700000000000, + }, + }); + await betaPersistence.saveGlobalAccountVariables({ + routeUnlocked: true, + }); + + await alphaPersistence.clear(); + + expect(await alphaPersistence.load()).toEqual({ + saveSlots: {}, + globalDeviceVariables: {}, + globalAccountVariables: {}, + }); + + expect(await betaPersistence.load()).toEqual({ + saveSlots: {}, + globalDeviceVariables: {}, + globalAccountVariables: { + routeUnlocked: true, + }, + }); + }); + it("normalizes namespace values", () => { expect(normalizeNamespace(" sample-vn ")).toBe("sample-vn"); expect(normalizeNamespace("")).toBeNull(); diff --git a/spec/projectDataSchema.test.js b/spec/projectDataSchema.test.js index d91f2b21..1509e2e2 100644 --- a/spec/projectDataSchema.test.js +++ b/spec/projectDataSchema.test.js @@ -209,6 +209,99 @@ describe("projectData schema", () => { ); }); + it("rejects legacy animations.in/out/update selection objects", () => { + const projectData = createMinimalProjectData({ + story: { + initialSceneId: "scene1", + scenes: { + scene1: { + name: "Scene 1", + initialSectionId: "section1", + sections: { + section1: { + name: "Section 1", + lines: [ + { + id: "line1", + actions: { + background: { + resourceId: "bg1", + animations: { + in: { + resourceId: "fadeIn", + }, + }, + }, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + expect(validateProjectData(projectData)).toBe(false); + expect(validateProjectData.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + instancePath: + "/story/scenes/scene1/sections/section1/lines/0/actions/background/animations", + }), + ]), + ); + }); + + it("rejects legacy animation resource types", () => { + const projectData = createMinimalProjectData({ + resources: { + animations: { + fadeIn: { + type: "live", + tween: { + alpha: { + keyframes: [ + { + duration: 500, + value: 1, + }, + ], + }, + }, + }, + crossFade: { + type: "replace", + next: { + tween: { + alpha: { + keyframes: [ + { + duration: 500, + value: 1, + }, + ], + }, + }, + }, + }, + }, + }, + }); + + expect(validateProjectData(projectData)).toBe(false); + expect(validateProjectData.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + instancePath: "/resources/animations/fadeIn/type", + }), + expect.objectContaining({ + instancePath: "/resources/animations/crossFade/type", + }), + ]), + ); + }); + it("accepts templated save/load slot ids in system actions", () => { expect( validateSystemActions({ diff --git a/spec/system/constructPresentationState.spec.yaml b/spec/system/constructPresentationState.spec.yaml index e41faa18..c80c578a 100644 --- a/spec/system/constructPresentationState.spec.yaml +++ b/spec/system/constructPresentationState.spec.yaml @@ -115,9 +115,7 @@ in: - - background: resourceId: "bg-school" animations: - in: - fade: - duration: 500 + resourceId: "fade" - dialogue: mode: "adv" content: @@ -138,9 +136,7 @@ in: - id: "sakura" resourceId: "char-sakura" animations: - in: - slide: - from: "left" + resourceId: "slide" - dialogue: mode: "adv" content: @@ -163,9 +159,7 @@ in: - id: "sparkle" resourceId: "effect-sparkle" animations: - in: - fade: - duration: 300 + resourceId: "fade" - dialogue: mode: "adv" content: @@ -197,14 +191,25 @@ out: content: - text: "Hello!" --- +case: preserve existing background resource when next line only adds animations +in: + - - background: + resourceId: "bg-school" + - background: + animations: + resourceId: "fade-in" +out: + background: + resourceId: "bg-school" + animations: + resourceId: "fade-in" +--- case: clear animations across multiple lines in: - - background: resourceId: "bg-school" animations: - in: - fade: - duration: 500 + resourceId: "fade" - dialogue: mode: "adv" content: @@ -228,8 +233,7 @@ in: - id: "char-1" resourceId: "res-1" animations: - in: - fade: {} + resourceId: "fade" - id: "char-2" resourceId: "res-2" - dialogue: diff --git a/spec/system/renderState/addBackgroundOrCg.spec.yaml b/spec/system/renderState/addBackgroundOrCg.spec.yaml index ad376383..f7e4f32f 100644 --- a/spec/system/renderState/addBackgroundOrCg.spec.yaml +++ b/spec/system/renderState/addBackgroundOrCg.spec.yaml @@ -45,7 +45,7 @@ out: height: 1080 animations: [] --- -case: add background with in animation +case: add background with single transition animation reference in: - elements: - id: "story" @@ -58,8 +58,7 @@ in: background: resourceId: "bg1" animations: - in: - resourceId: "fadeIn" + resourceId: "fadeIn" resources: images: bg1: @@ -108,7 +107,7 @@ out: - duration: 500 value: 1 --- -case: reject legacy live background animation type on enter +case: reject unsupported legacy live background animation type on enter in: - elements: - id: "story" @@ -121,8 +120,7 @@ in: background: resourceId: "bg1" animations: - in: - resourceId: "fadeIn" + resourceId: "fadeIn" resources: images: bg1: @@ -138,9 +136,9 @@ in: keyframes: - duration: 500 value: 1 -throws: 'Animation "fadeIn" has type "update", but update animations can only be used when the same target persists across the state change. Use type "transition" for enter, exit, and replace.' +throws: '[background.animations] Animation "fadeIn" has unsupported type "live". Use "update" or "transition".' --- -case: skip background in animation when skipTransitionsAndAnimations is true +case: skip background transition animation when skipTransitionsAndAnimations is true in: - elements: - id: "story" @@ -153,8 +151,7 @@ in: background: resourceId: "bg1" animations: - in: - resourceId: "fadeIn" + resourceId: "fadeIn" resources: images: bg1: @@ -194,7 +191,7 @@ out: height: 1080 animations: [] --- -case: add background with update animation +case: add background with single update animation reference in: - elements: - id: "story" @@ -214,8 +211,7 @@ in: background: resourceId: "bg1" animations: - update: - resourceId: "crossFade" + resourceId: "crossFade" previousPresentationState: background: resourceId: "bg1" @@ -414,8 +410,7 @@ in: background: resourceId: "bg2" animations: - in: - resourceId: "slideIn" + resourceId: "slideIn" resources: images: bg2: @@ -745,27 +740,14 @@ out: - duration: 500 value: 1 --- -case: remove background with transition animation reference that defines both prev and next +case: animate persisted background with transition animation reference when resourceId is omitted in: - elements: - id: "story" type: "container" x: 0 y: 0 - children: - - id: "bg-cg-bg1" - type: "sprite" - alpha: 1 - anchorX: 0 - anchorY: 0 - rotation: 0 - scaleX: 1 - scaleY: 1 - x: 0 - y: 0 - src: "image1.png" - width: 1920 - height: 1080 + children: [] animations: [] - presentationState: background: @@ -818,7 +800,7 @@ out: width: 1920 height: 1080 animations: - - id: "bg-cg-animation-out" + - id: "bg-cg-animation-transition" type: "transition" targetId: "bg-cg-bg1" prev: @@ -899,7 +881,7 @@ out: - duration: 250 value: 1 --- -case: throw when single update animation reference is used for background enter +case: animate incoming background when single update animation reference is used on enter in: - elements: - id: "story" @@ -928,9 +910,38 @@ in: keyframes: - duration: 500 value: 1 -throws: '[background.animations] Animation "fadeIn" has type "update", but update animations can only be used when the same target persists across the state change. Use type "transition" for enter, exit, and replace.' +out: + elements: + - id: "story" + type: "container" + x: 0 + y: 0 + children: + - id: "bg-cg-bg1" + type: "sprite" + alpha: 1 + anchorX: 0 + anchorY: 0 + rotation: 0 + scaleX: 1 + scaleY: 1 + x: 0 + y: 0 + src: "image1.png" + width: 1920 + height: 1080 + animations: + - id: "bg-cg-animation-update" + type: "update" + targetId: "bg-cg-bg1" + tween: + alpha: + initialValue: 0 + keyframes: + - duration: 500 + value: 1 --- -case: throw when legacy background in animation references update type +case: reject legacy lifecycle animation object for background animations in: - elements: - id: "story" @@ -960,4 +971,4 @@ in: keyframes: - duration: 500 value: 1 -throws: '[background.animations.in] Animation "fadeIn" has type "update", but update animations can only be used when the same target persists across the state change. Use type "transition" for enter, exit, and replace.' +throws: '[background.animations] Legacy animations.in/out/update is no longer supported. Use a single animations.resourceId reference.' diff --git a/spec/system/renderState/addCharacters.spec.yaml b/spec/system/renderState/addCharacters.spec.yaml index 55ea16fc..2b91de22 100644 --- a/spec/system/renderState/addCharacters.spec.yaml +++ b/spec/system/renderState/addCharacters.spec.yaml @@ -76,7 +76,7 @@ out: y: 0 animations: [] --- -case: add character with in animation +case: add character with single transition animation reference in: - elements: - id: "story" @@ -94,8 +94,7 @@ in: - id: "body" resourceId: "body" animations: - in: - resourceId: "fadeIn" + resourceId: "fadeIn" resources: transforms: transform1: @@ -157,7 +156,7 @@ out: - duration: 500 value: 1 --- -case: add character with out animation only (no container created) +case: add character with transition animation on removal (no container created) in: - elements: - id: "story" @@ -171,8 +170,7 @@ in: items: - id: "char1" animations: - out: - resourceId: "fadeOut" + resourceId: "fadeOut" previousPresentationState: character: items: @@ -210,7 +208,7 @@ out: - duration: 300 value: 0 --- -case: add character with in animation (new character) +case: add character with single transition animation reference (new character) in: - elements: - id: "story" @@ -228,8 +226,7 @@ in: - id: "body" resourceId: "body" animations: - in: - resourceId: "fadeIn" + resourceId: "fadeIn" resources: transforms: transform1: @@ -291,7 +288,7 @@ out: - duration: 500 value: 1 --- -case: add character update animation with final tween properties +case: add character update animation with single reference and final tween properties in: - elements: - id: "story" @@ -309,8 +306,7 @@ in: - id: "body" resourceId: "body" animations: - update: - resourceId: "scaleUp" + resourceId: "scaleUp" previousPresentationState: character: items: diff --git a/spec/system/renderState/addLayeredViews.spec.yaml b/spec/system/renderState/addLayeredViews.spec.yaml index 35990267..140845b2 100644 --- a/spec/system/renderState/addLayeredViews.spec.yaml +++ b/spec/system/renderState/addLayeredViews.spec.yaml @@ -310,7 +310,7 @@ out: - duration: 300 value: 0 --- -case: normalize legacy live layout transitions to update +case: reject unsupported legacy live layout transitions in: - elements: - id: "story" @@ -350,33 +350,7 @@ out: x: 0 y: 0 children: [] - - id: "layeredView-0" - type: "container" - x: 0 - y: 0 - children: - - id: "layeredView-0-blocker" - type: "rect" - fill: "transparent" - width: 1920 - height: 1080 - x: 0 - y: 0 - click: - payload: - actions: {} - - type: "text" - content: "Animated" - animations: - - id: "animated-layout-fade-in" - type: "update" - targetId: "layeredView-0" - tween: - alpha: - initialValue: 0 - keyframes: - - duration: 500 - value: 1 +throws: '[layeredViews[0].transitions[0]] Animation "animated-layout-fade-in" has unsupported type "live". Use "update" or "transition".' --- case: add layered view child animation with explicit target id in: diff --git a/spec/system/renderState/addVisuals.spec.yaml b/spec/system/renderState/addVisuals.spec.yaml index 813a3028..95f93238 100644 --- a/spec/system/renderState/addVisuals.spec.yaml +++ b/spec/system/renderState/addVisuals.spec.yaml @@ -98,7 +98,7 @@ in: transforms: {} throws: 'Transform "missing" not found for visual item "visual1"' --- -case: add visual with in animation +case: add visual with single transition animation reference in: - elements: - id: "story" @@ -115,8 +115,7 @@ in: resourceId: "image1" transformId: "transform1" animations: - in: - resourceId: "fadeIn" + resourceId: "fadeIn" resources: images: image1: @@ -170,7 +169,7 @@ out: value: 1 audio: [] --- -case: add visual with out animation +case: add visual with transition animation on removal in: - elements: - id: "story" @@ -185,7 +184,7 @@ in: items: - id: "visual1" animations: - out: "fadeOut" + resourceId: "fadeOut" previousPresentationState: visual: items: @@ -222,7 +221,7 @@ out: value: 0 audio: [] --- -case: add visual with in animation (no previous state) +case: add visual with single transition animation reference (no previous state) in: - elements: - id: "story" @@ -239,8 +238,7 @@ in: resourceId: "image1" transformId: "transform1" animations: - in: - resourceId: "slideIn" + resourceId: "slideIn" resources: images: image1: @@ -740,7 +738,7 @@ out: animations: [] audio: [] --- -case: add visual layout with in animation +case: add visual layout with single transition animation reference in: - elements: - id: "story" @@ -757,8 +755,7 @@ in: resourceId: "overlayLayout" transformId: "transform1" animations: - in: - resourceId: "fadeIn" + resourceId: "fadeIn" resources: layouts: overlayLayout: diff --git a/src/indexedDbPersistence.js b/src/indexedDbPersistence.js index 1970409c..cdc2644a 100644 --- a/src/indexedDbPersistence.js +++ b/src/indexedDbPersistence.js @@ -196,6 +196,70 @@ const writeNamespaceRecord = async ({ }); }; +const clearNamespaceRecord = async ({ + databasePromise, + objectStoreName, + namespace, +}) => { + const database = await databasePromise; + + return new Promise((resolve, reject) => { + const transaction = database.transaction(objectStoreName, "readwrite"); + const store = transaction.objectStore(objectStoreName); + const deleteRequest = store.delete(namespace); + let settled = false; + + const rejectOnce = (error) => { + if (settled) { + return; + } + + settled = true; + reject(error); + }; + + const resolveOnce = () => { + if (settled) { + return; + } + + settled = true; + resolve(); + }; + + deleteRequest.onerror = () => { + rejectOnce( + deleteRequest.error ?? + new Error( + `Failed to clear persisted namespace "${namespace}" from IndexedDB.`, + ), + ); + }; + + transaction.oncomplete = () => { + resolveOnce(); + }; + + transaction.onerror = () => { + rejectOnce( + transaction.error ?? + new Error( + `IndexedDB transaction failed while clearing namespace "${namespace}".`, + ), + ); + }; + + transaction.onabort = () => { + rejectOnce( + transaction.error ?? + new Error( + `IndexedDB transaction aborted while clearing namespace "${namespace}".`, + ), + ); + }; + }); +}; + export const createIndexedDbPersistence = (options = {}) => { const { indexedDB: indexedDBOverride, @@ -226,6 +290,12 @@ export const createIndexedDbPersistence = (options = {}) => { ...normalizePersistedState(record), }; }, + clear: async () => + clearNamespaceRecord({ + databasePromise, + objectStoreName, + namespace: resolvedNamespace, + }), saveSlots: async (saveSlots) => writeNamespaceRecord({ databasePromise, diff --git a/src/schemas/presentationActions.yaml b/src/schemas/presentationActions.yaml index 2e688e09..8510541b 100644 --- a/src/schemas/presentationActions.yaml +++ b/src/schemas/presentationActions.yaml @@ -13,24 +13,9 @@ definitions: required: [resourceId] additionalProperties: false - legacyAnimationSelection: - type: object - description: Legacy lifecycle-specific animation selection. Prefer a single resourceId reference instead. - properties: - in: - $ref: "#/definitions/animationRef" - out: - $ref: "#/definitions/animationRef" - update: - $ref: "#/definitions/animationRef" - minProperties: 1 - additionalProperties: false - animationSelection: - description: Animation selection. Prefer a single resourceId that points to an animation whose type decides update vs transition. Legacy in/out/update remains supported for compatibility. - oneOf: - - $ref: "#/definitions/animationRef" - - $ref: "#/definitions/legacyAnimationSelection" + description: Animation selection. Provide a single resourceId whose animation type decides update vs transition. + $ref: "#/definitions/animationRef" properties: cleanAll: diff --git a/src/schemas/projectData/animationResource.yaml b/src/schemas/projectData/animationResource.yaml index ea4843a8..87891c90 100644 --- a/src/schemas/projectData/animationResource.yaml +++ b/src/schemas/projectData/animationResource.yaml @@ -192,7 +192,7 @@ properties: type: type: string enum: [update, transition] - description: Animation structure. Legacy `live` and `replace` payloads are normalized during render-state construction, but authored project data should use `update` and `transition`. + description: Animation structure. Only `update` and `transition` are supported. complete: type: object properties: diff --git a/src/stores/constructPresentationState.js b/src/stores/constructPresentationState.js index e9a0cf94..38ca039a 100644 --- a/src/stores/constructPresentationState.js +++ b/src/stores/constructPresentationState.js @@ -64,7 +64,12 @@ export const background = (state, presentation) => { ); if (animationsOnly) { - state.background = animState; + state.background = state.background?.resourceId + ? { + ...structuredClone(state.background), + ...animState, + } + : animState; return; } diff --git a/src/stores/constructRenderState.js b/src/stores/constructRenderState.js index cd66212d..edd43ba2 100644 --- a/src/stores/constructRenderState.js +++ b/src/stores/constructRenderState.js @@ -100,31 +100,46 @@ const expandLoopTemplates = (node, templateData, options) => { return expanded; }; -const LEGACY_ANIMATION_TYPE_MAP = { - live: "update", - replace: "transition", -}; - -const cloneAndNormalizeAnimation = (animation) => { - const normalized = structuredClone(animation); +const assertSupportedAnimationType = ({ + animationType, + animationId, + animationPath, +}) => { + if (animationType === undefined) { + return; + } - if (typeof normalized.type === "string") { - normalized.type = - LEGACY_ANIMATION_TYPE_MAP[normalized.type] || normalized.type; + if (animationType === "update" || animationType === "transition") { + return; } - return normalized; + throw new Error( + `[${animationPath}] Animation "${animationId}" has unsupported type "${animationType}". Use "update" or "transition".`, + ); }; -const createAnimationInstance = ({ id, targetId, animation }) => { - const normalized = cloneAndNormalizeAnimation(animation); +const createAnimationInstance = ({ + id, + targetId, + animation, + animationPath = "animation", +}) => { + const normalized = structuredClone(animation); + assertSupportedAnimationType({ + animationType: normalized.type, + animationId: id, + animationPath, + }); delete normalized.name; normalized.id = id; normalized.targetId = targetId; return normalized; }; -const getAnimationType = (animation) => { +const getAnimationType = ( + animation, + { animationId = "animation", animationPath = "animation" } = {}, +) => { if (!animation || typeof animation !== "object" || Array.isArray(animation)) { return undefined; } @@ -133,7 +148,13 @@ const getAnimationType = (animation) => { return undefined; } - return LEGACY_ANIMATION_TYPE_MAP[animation.type] || animation.type; + assertSupportedAnimationType({ + animationType: animation.type, + animationId, + animationPath, + }); + + return animation.type; }; const resolveAnimationResourceId = (animationDef) => { @@ -166,12 +187,26 @@ const hasLegacyAnimationLifecycleConfig = (animationsDef) => { ); }; +const assertNoLegacyAnimationLifecycleConfig = ( + animationsDef, + animationPath, +) => { + if (!hasLegacyAnimationLifecycleConfig(animationsDef)) { + return; + } + + throw new Error( + `[${animationPath}] Legacy animations.in/out/update is no longer supported. Use a single animations.resourceId reference.`, + ); +}; + const pushAnimationInstance = ({ animations, resources, animationId, instanceId, targetId, + animationPath = "animation", }) => { if (!animationId || !targetId) { return false; @@ -187,6 +222,7 @@ const pushAnimationInstance = ({ id: instanceId, targetId, animation, + animationPath, }), ); @@ -234,12 +270,20 @@ const assertUpdateAnimationLifecycle = ({ ); }; -const cloneAnimation = (animation, { defaultTargetId, defaultId } = {}) => { +const cloneAnimation = ( + animation, + { defaultTargetId, defaultId, animationPath = "animation" } = {}, +) => { if (!animation || typeof animation !== "object" || Array.isArray(animation)) { return animation; } - const normalized = cloneAndNormalizeAnimation(animation); + const normalized = structuredClone(animation); + assertSupportedAnimationType({ + animationType: normalized.type, + animationId: normalized.id ?? defaultId, + animationPath, + }); normalized.id ??= defaultId; normalized.targetId ??= defaultTargetId; return normalized; @@ -250,12 +294,14 @@ const pushNormalizedLayoutTransitions = ({ transitions, defaultTargetId, idPrefix, + animationPathPrefix = idPrefix, }) => { transitions.forEach((transition, index) => { animations.push( cloneAnimation(transition, { defaultTargetId, defaultId: `${idPrefix}-transition-${index}`, + animationPath: `${animationPathPrefix}.transitions[${index}]`, }), ); }); @@ -1096,6 +1142,7 @@ const pushAnimations = ({ currentTargetId, animationPath, idPrefix, + allowIncomingUpdateFallback = false, }) => { if (!animationsDef) return; @@ -1117,76 +1164,29 @@ const pushAnimations = ({ sharedTarget, }); - if (hasLegacyAnimationLifecycleConfig(animationsDef)) { - if (animationsDef.in && !hasPrevious) { - const animationId = resolveAnimationResourceId(animationsDef.in); - assertUpdateAnimationLifecycle({ - animationType: getAnimationType(resources?.animations?.[animationId]), - animationId, - animationPath: `${animationPath}.in`, - lifecycle: "enter", - }); - - pushAnimationInstance({ - animations, - resources, - animationId, - instanceId: `${idPrefix}-animation-in`, - targetId: currentTargetId, - }); - } - - if ( - animationsDef.out && - hasPrevious && - previousResourceId !== currentResourceId - ) { - const animationId = resolveAnimationResourceId(animationsDef.out); - assertUpdateAnimationLifecycle({ - animationType: getAnimationType(resources?.animations?.[animationId]), - animationId, - animationPath: `${animationPath}.out`, - lifecycle: "exit", - }); - - pushAnimationInstance({ - animations, - resources, - animationId, - instanceId: `${idPrefix}-animation-out`, - targetId: previousTargetId, - }); - } - - if ( - animationsDef.update && - hasPrevious && - hasCurrent && - previousResourceId === currentResourceId && - sharedTarget - ) { - pushAnimationInstance({ - animations, - resources, - animationId: resolveAnimationResourceId(animationsDef.update), - instanceId: `${idPrefix}-animation-update`, - targetId: currentTargetId, - }); - } - - return; - } + assertNoLegacyAnimationLifecycleConfig(animationsDef, animationPath); const animationId = resolveAnimationResourceId(animationsDef); const animation = resources?.animations?.[animationId]; - const animationType = getAnimationType(animation); - - assertUpdateAnimationLifecycle({ - animationType, + const animationType = getAnimationType(animation, { animationId, animationPath, - lifecycle, }); + const canFallbackIncomingUpdate = + allowIncomingUpdateFallback && + animationType === "update" && + lifecycle !== "update" && + hasCurrent && + currentTargetId; + + if (!canFallbackIncomingUpdate) { + assertUpdateAnimationLifecycle({ + animationType, + animationId, + animationPath, + lifecycle, + }); + } if (animationType === "update") { if ( @@ -1201,6 +1201,16 @@ const pushAnimations = ({ animationId, instanceId: `${idPrefix}-animation-update`, targetId: currentTargetId, + animationPath, + }); + } else if (canFallbackIncomingUpdate) { + pushAnimationInstance({ + animations, + resources, + animationId, + instanceId: `${idPrefix}-animation-update`, + targetId: currentTargetId, + animationPath, }); } @@ -1215,6 +1225,7 @@ const pushAnimations = ({ animationId, instanceId: `${idPrefix}-animation-transition`, targetId: currentTargetId, + animationPath, }); return; @@ -1227,6 +1238,7 @@ const pushAnimations = ({ animationId, instanceId: `${idPrefix}-animation-out`, targetId: previousTargetId, + animationPath, }); } @@ -1237,6 +1249,7 @@ const pushAnimations = ({ animationId, instanceId: `${idPrefix}-animation-in`, targetId: currentTargetId, + animationPath, }); } } @@ -1283,16 +1296,23 @@ export const addBackgroundOrCg = ( return state; } - if (presentationState.background.resourceId) { + const previousBackgroundResourceId = + previousPresentationState?.background?.resourceId; + const currentBackgroundResourceId = + presentationState.background.resourceId ?? + (presentationState.background.animations + ? previousBackgroundResourceId + : undefined); + + if (currentBackgroundResourceId) { const { images = {}, videos = {} } = resources; const background = - images[presentationState.background.resourceId] || - videos[presentationState.background.resourceId]; + images[currentBackgroundResourceId] || + videos[currentBackgroundResourceId]; if (background) { - const isVideo = - videos[presentationState.background.resourceId] !== undefined; + const isVideo = videos[currentBackgroundResourceId] !== undefined; const element = { - id: `bg-cg-${presentationState.background.resourceId}`, + id: `bg-cg-${currentBackgroundResourceId}`, type: isVideo ? "video" : "sprite", x: 0, y: 0, @@ -1316,12 +1336,12 @@ export const addBackgroundOrCg = ( } } - if (presentationState.background.resourceId) { + if (currentBackgroundResourceId) { const { layouts = {} } = resources; - const layout = layouts[presentationState.background.resourceId]; + const layout = layouts[currentBackgroundResourceId]; if (layout) { const bgContainer = { - id: `bg-cg-${presentationState.background.resourceId}`, + id: `bg-cg-${currentBackgroundResourceId}`, type: "container", children: layout.elements, }; @@ -1356,24 +1376,21 @@ export const addBackgroundOrCg = ( !isLineCompleted && !skipTransitionsAndAnimations ) { - const previousResourceId = - previousPresentationState?.background?.resourceId; - const currentResourceId = presentationState.background.resourceId; - pushAnimations({ animations, animationsDef: presentationState.background.animations, resources, - previousResourceId, - currentResourceId, - previousTargetId: previousResourceId - ? `bg-cg-${previousResourceId}` + previousResourceId: previousBackgroundResourceId, + currentResourceId: currentBackgroundResourceId, + previousTargetId: previousBackgroundResourceId + ? `bg-cg-${previousBackgroundResourceId}` : undefined, - currentTargetId: currentResourceId - ? `bg-cg-${currentResourceId}` + currentTargetId: currentBackgroundResourceId + ? `bg-cg-${currentBackgroundResourceId}` : undefined, animationPath: "background.animations", idPrefix: "bg-cg", + allowIncomingUpdateFallback: true, }); } } @@ -2131,6 +2148,7 @@ export const addLayout = ( transitions: layout.transitions, defaultTargetId: `layout-${presentationState.layout.resourceId}`, idPrefix: `layout-${presentationState.layout.resourceId}`, + animationPathPrefix: "layout", }); } @@ -2226,6 +2244,7 @@ export const addLayeredViews = ( transitions: layout.transitions, defaultTargetId: `layeredView-${index}`, idPrefix: `layeredView-${index}`, + animationPathPrefix: `layeredViews[${index}]`, }); } } @@ -2332,6 +2351,7 @@ export const addConfirmDialog = ( transitions: layout.transitions, defaultTargetId: "confirmDialog", idPrefix: "confirmDialog", + animationPathPrefix: "confirmDialog", }); } diff --git a/src/stores/system.store.js b/src/stores/system.store.js index ed5e23cb..0834f5a9 100644 --- a/src/stores/system.store.js +++ b/src/stores/system.store.js @@ -1063,7 +1063,7 @@ export const createInitialState = (payload) => { const { globalVariablesDefaultValues } = getDefaultVariablesFromProjectData(projectData); - // Merge with loaded globalVariablesDefaultValues from localStorage (if provided) + // Merge with loaded global variables from persisted browser storage (if provided) const globalVariables = { ...globalVariablesDefaultValues, ...loadedGlobalVariables, diff --git a/src/util.js b/src/util.js index ad8c4387..31025380 100644 --- a/src/util.js +++ b/src/util.js @@ -636,6 +636,14 @@ export const diffPresentationState = (prev = {}, curr = {}) => { export const normalizePersistentPresentationState = (state = {}) => { const normalizedState = structuredClone(state); + if (normalizedState.background) { + delete normalizedState.background.animations; + + if (!normalizedState.background.resourceId) { + delete normalizedState.background; + } + } + const stripAnimationsFromObject = (key) => { if (!normalizedState[key]) { return; @@ -648,7 +656,6 @@ export const normalizePersistentPresentationState = (state = {}) => { } }; - stripAnimationsFromObject("background"); stripAnimationsFromObject("layout"); stripAnimationsFromObject("choice"); diff --git a/vt/reference/background/transition-both-sides-exit-02.webp b/vt/reference/background/transition-both-sides-exit-02.webp index c6a7109f..4a7c8e76 100644 --- a/vt/reference/background/transition-both-sides-exit-02.webp +++ b/vt/reference/background/transition-both-sides-exit-02.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c94876c0be480eec2fbbfa1fddb27d47e05e1944ddbd2dedbe6b55dc1e23f4f3 -size 3276 +oid sha256:2e52672768f01d8884066367aad5a042a35f0a02eb2ec09698027e48d4f68bf7 +size 3822 diff --git a/vt/reference/background/transition-both-sides-exit-03.webp b/vt/reference/background/transition-both-sides-exit-03.webp index 6c462259..c43f5ede 100644 --- a/vt/reference/background/transition-both-sides-exit-03.webp +++ b/vt/reference/background/transition-both-sides-exit-03.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea705101cac922dd2fe8b7d30ea22d4da12ff083833a10381515a829dca10552 -size 990 +oid sha256:e33f11e6b08e2f9ca12a2152e84480cf7a18f7c6abab0de6d23bd94d0f64c501 +size 4092 diff --git a/vt/reference/background/transition-mask-out-01.webp b/vt/reference/background/transition-mask-out-01.webp index c43f5ede..896ece9e 100644 --- a/vt/reference/background/transition-mask-out-01.webp +++ b/vt/reference/background/transition-mask-out-01.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e33f11e6b08e2f9ca12a2152e84480cf7a18f7c6abab0de6d23bd94d0f64c501 -size 4092 +oid sha256:c0143ca68d822efff1d341350e4ef179626007ad547686390a0320c131f47b8d +size 3746 diff --git a/vt/reference/background/transition-mask-out-02.webp b/vt/reference/background/transition-mask-out-02.webp index 0135152b..4e38dfd3 100644 --- a/vt/reference/background/transition-mask-out-02.webp +++ b/vt/reference/background/transition-mask-out-02.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:746d5637a23670e98afb991a1aac1b03a709ed3034d224bc556edf48fa0a5616 -size 1038 +oid sha256:a65e42374d885372fca65327be3aaf818f4082419f52c3b6ee8f4c66343cb7f0 +size 4088 diff --git a/vt/reference/background/transition-mask-out-03.webp b/vt/reference/background/transition-mask-out-03.webp index 6c462259..896ece9e 100644 --- a/vt/reference/background/transition-mask-out-03.webp +++ b/vt/reference/background/transition-mask-out-03.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea705101cac922dd2fe8b7d30ea22d4da12ff083833a10381515a829dca10552 -size 990 +oid sha256:c0143ca68d822efff1d341350e4ef179626007ad547686390a0320c131f47b8d +size 3746 diff --git a/vt/reference/background/transition-persisted-state-01.webp b/vt/reference/background/transition-persisted-state-01.webp new file mode 100644 index 00000000..c43f5ede --- /dev/null +++ b/vt/reference/background/transition-persisted-state-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e33f11e6b08e2f9ca12a2152e84480cf7a18f7c6abab0de6d23bd94d0f64c501 +size 4092 diff --git a/vt/reference/background/transition-persisted-state-02.webp b/vt/reference/background/transition-persisted-state-02.webp new file mode 100644 index 00000000..4912cfe9 --- /dev/null +++ b/vt/reference/background/transition-persisted-state-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:419b948ab9b3f445c34ea3d4dd59f11c6f4fc626ad7dc3b33d87555f8cb2e532 +size 1482 diff --git a/vt/reference/background/transition-persisted-state-03.webp b/vt/reference/background/transition-persisted-state-03.webp new file mode 100644 index 00000000..fedb4d19 --- /dev/null +++ b/vt/reference/background/transition-persisted-state-03.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bc9b0510ecde42859c7773c93c7ed5b64c57b36fb3c9e11de530dddb6a6e972 +size 3452 diff --git a/vt/reference/background/transition-persisted-state-04.webp b/vt/reference/background/transition-persisted-state-04.webp new file mode 100644 index 00000000..c43f5ede --- /dev/null +++ b/vt/reference/background/transition-persisted-state-04.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e33f11e6b08e2f9ca12a2152e84480cf7a18f7c6abab0de6d23bd94d0f64c501 +size 4092 diff --git a/vt/reference/background/transition-tween-out-01.webp b/vt/reference/background/transition-tween-out-01.webp index e4361cb8..44e637b0 100644 --- a/vt/reference/background/transition-tween-out-01.webp +++ b/vt/reference/background/transition-tween-out-01.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68c39d92a0b3b32b95ff34fcaa6790534acfa5811df30e432e51e3f2a3c80b1c -size 10862 +oid sha256:1f0c764500abab4a069417e9cd05ba64b91a5a9e53497053555dbee774647348 +size 9800 diff --git a/vt/reference/background/transition-tween-out-02.webp b/vt/reference/background/transition-tween-out-02.webp index c5e4fae7..5207f05d 100644 --- a/vt/reference/background/transition-tween-out-02.webp +++ b/vt/reference/background/transition-tween-out-02.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95469662ffc11a48fdfb3114fb2b560f9deab2f6ec8b6a9709b0a44506b75a00 -size 6920 +oid sha256:492076e4ced59a7faf8324016f6d2fdbb0228e8ab083ca9daddc01886df56d18 +size 10860 diff --git a/vt/reference/background/transition-tween-out-03.webp b/vt/reference/background/transition-tween-out-03.webp index 6c462259..44e637b0 100644 --- a/vt/reference/background/transition-tween-out-03.webp +++ b/vt/reference/background/transition-tween-out-03.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea705101cac922dd2fe8b7d30ea22d4da12ff083833a10381515a829dca10552 -size 990 +oid sha256:1f0c764500abab4a069417e9cd05ba64b91a5a9e53497053555dbee774647348 +size 9800 diff --git a/vt/reference/background/update-enter-fallback-01.webp b/vt/reference/background/update-enter-fallback-01.webp new file mode 100644 index 00000000..82f6fb54 --- /dev/null +++ b/vt/reference/background/update-enter-fallback-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:168819cb2b6cdc41f3787da45379334733daaf8c5d4f1669a59e94a5c2df3ae0 +size 990 diff --git a/vt/reference/background/update-enter-fallback-02.webp b/vt/reference/background/update-enter-fallback-02.webp new file mode 100644 index 00000000..5207f05d --- /dev/null +++ b/vt/reference/background/update-enter-fallback-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:492076e4ced59a7faf8324016f6d2fdbb0228e8ab083ca9daddc01886df56d18 +size 10860 diff --git a/vt/reference/background/update-enter-fallback-03.webp b/vt/reference/background/update-enter-fallback-03.webp new file mode 100644 index 00000000..44e637b0 --- /dev/null +++ b/vt/reference/background/update-enter-fallback-03.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f0c764500abab4a069417e9cd05ba64b91a5a9e53497053555dbee774647348 +size 9800 diff --git a/vt/reference/choice/blocked-non-choice-actions--capture-01.webp b/vt/reference/choice/blocked-non-choice-actions--capture-01.webp new file mode 100644 index 00000000..392d7876 --- /dev/null +++ b/vt/reference/choice/blocked-non-choice-actions--capture-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a784b9501c516d53e607bd6a8f58fa2830a4e36f1c95106a23e7e9de17c3e8d +size 9196 diff --git a/vt/reference/choice/blocked-non-choice-actions--capture-02.webp b/vt/reference/choice/blocked-non-choice-actions--capture-02.webp new file mode 100644 index 00000000..392d7876 --- /dev/null +++ b/vt/reference/choice/blocked-non-choice-actions--capture-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a784b9501c516d53e607bd6a8f58fa2830a4e36f1c95106a23e7e9de17c3e8d +size 9196 diff --git a/vt/reference/choice/interaction-guards--capture-01.webp b/vt/reference/choice/interaction-guards--capture-01.webp new file mode 100644 index 00000000..5fc45ffc --- /dev/null +++ b/vt/reference/choice/interaction-guards--capture-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8098299910ce6f454f66061543b220e903a4887d44012881e2bea7ccc412fe9e +size 9652 diff --git a/vt/reference/choice/interaction-guards--capture-02.webp b/vt/reference/choice/interaction-guards--capture-02.webp new file mode 100644 index 00000000..65e776db --- /dev/null +++ b/vt/reference/choice/interaction-guards--capture-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd03a2b6a6aea07dc31619fdcf83aee1a5b7eeaddc1a7bf0aa53c3d5d72cf07b +size 12546 diff --git a/vt/reference/choice/interaction-guards--capture-03.webp b/vt/reference/choice/interaction-guards--capture-03.webp new file mode 100644 index 00000000..65e776db --- /dev/null +++ b/vt/reference/choice/interaction-guards--capture-03.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd03a2b6a6aea07dc31619fdcf83aee1a5b7eeaddc1a7bf0aa53c3d5d72cf07b +size 12546 diff --git a/vt/reference/choice/interaction-guards--capture-04.webp b/vt/reference/choice/interaction-guards--capture-04.webp new file mode 100644 index 00000000..65e776db --- /dev/null +++ b/vt/reference/choice/interaction-guards--capture-04.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd03a2b6a6aea07dc31619fdcf83aee1a5b7eeaddc1a7bf0aa53c3d5d72cf07b +size 12546 diff --git a/vt/reference/choice/interaction-guards--capture-05.webp b/vt/reference/choice/interaction-guards--capture-05.webp new file mode 100644 index 00000000..65e776db --- /dev/null +++ b/vt/reference/choice/interaction-guards--capture-05.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd03a2b6a6aea07dc31619fdcf83aee1a5b7eeaddc1a7bf0aa53c3d5d72cf07b +size 12546 diff --git a/vt/reference/choice/interaction-guards--capture-06.webp b/vt/reference/choice/interaction-guards--capture-06.webp new file mode 100644 index 00000000..65e776db --- /dev/null +++ b/vt/reference/choice/interaction-guards--capture-06.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd03a2b6a6aea07dc31619fdcf83aee1a5b7eeaddc1a7bf0aa53c3d5d72cf07b +size 12546 diff --git a/vt/reference/choice/interaction-guards--capture-07.webp b/vt/reference/choice/interaction-guards--capture-07.webp new file mode 100644 index 00000000..65e776db --- /dev/null +++ b/vt/reference/choice/interaction-guards--capture-07.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd03a2b6a6aea07dc31619fdcf83aee1a5b7eeaddc1a7bf0aa53c3d5d72cf07b +size 12546 diff --git a/vt/reference/choice/skip-stops-on-choice--capture-01.webp b/vt/reference/choice/skip-stops-on-choice--capture-01.webp new file mode 100644 index 00000000..9ba591b1 --- /dev/null +++ b/vt/reference/choice/skip-stops-on-choice--capture-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4aba58e77a9f7dfe00d6de5515c5b36e4299b3bebd74ef2750bf7ed368d109e +size 5442 diff --git a/vt/reference/choice/skip-stops-on-choice--capture-02.webp b/vt/reference/choice/skip-stops-on-choice--capture-02.webp new file mode 100644 index 00000000..9ba591b1 --- /dev/null +++ b/vt/reference/choice/skip-stops-on-choice--capture-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4aba58e77a9f7dfe00d6de5515c5b36e4299b3bebd74ef2750bf7ed368d109e +size 5442 diff --git a/vt/reference/choice/skip-stops-on-choice--capture-03.webp b/vt/reference/choice/skip-stops-on-choice--capture-03.webp new file mode 100644 index 00000000..9ba591b1 --- /dev/null +++ b/vt/reference/choice/skip-stops-on-choice--capture-03.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4aba58e77a9f7dfe00d6de5515c5b36e4299b3bebd74ef2750bf7ed368d109e +size 5442 diff --git a/vt/reference/choice/skip-stops-on-choice--capture-04.webp b/vt/reference/choice/skip-stops-on-choice--capture-04.webp new file mode 100644 index 00000000..9ba591b1 --- /dev/null +++ b/vt/reference/choice/skip-stops-on-choice--capture-04.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4aba58e77a9f7dfe00d6de5515c5b36e4299b3bebd74ef2750bf7ed368d109e +size 5442 diff --git a/vt/reference/nextLineConfig/choice-stops-auto--capture-01.webp b/vt/reference/nextLineConfig/choice-stops-auto--capture-01.webp new file mode 100644 index 00000000..aa766ca9 --- /dev/null +++ b/vt/reference/nextLineConfig/choice-stops-auto--capture-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a90c59f39a07fa65ea38b8d7eeeb55b2a88457adfb3c3992f22df887408b1d3c +size 5162 diff --git a/vt/reference/nextLineConfig/choice-stops-auto--capture-02.webp b/vt/reference/nextLineConfig/choice-stops-auto--capture-02.webp new file mode 100644 index 00000000..1d30bdba --- /dev/null +++ b/vt/reference/nextLineConfig/choice-stops-auto--capture-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0209018af61988939722931ca77ec7dcaf8ff76b0391e821d3dd86c0f4c18ded +size 5242 diff --git a/vt/reference/nextLineConfig/choice-stops-auto--capture-03.webp b/vt/reference/nextLineConfig/choice-stops-auto--capture-03.webp new file mode 100644 index 00000000..3192f552 --- /dev/null +++ b/vt/reference/nextLineConfig/choice-stops-auto--capture-03.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c36b40e9d7f1cf17c9ad1a062fcd42f11128c0bcf55236ad4e6927005b0bc0ef +size 3338 diff --git a/vt/reference/nextLineConfig/choice-stops-auto-01.webp b/vt/reference/nextLineConfig/choice-stops-auto-01.webp deleted file mode 100644 index e684d546..00000000 --- a/vt/reference/nextLineConfig/choice-stops-auto-01.webp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:466ac75eb9891a020bea160cca113a3cfee145454cc2e7107b9443963587b154 -size 3096 diff --git a/vt/reference/variables/boolean-test-01.webp b/vt/reference/variables/boolean-test-01.webp index 8b2d1338..2320c316 100644 --- a/vt/reference/variables/boolean-test-01.webp +++ b/vt/reference/variables/boolean-test-01.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:529d723ba7c3fd89f0e1f3b0c5e8fc2e1051213708b7158330057216590d521a -size 13578 +oid sha256:9d7d58b01a4fa8d4012e79c1c169a8dc8ec4794b3c76dca46fa798e12aa8410c +size 14182 diff --git a/vt/reference/variables/object-test-01.webp b/vt/reference/variables/object-test-01.webp index baa71ae0..d0c94736 100644 --- a/vt/reference/variables/object-test-01.webp +++ b/vt/reference/variables/object-test-01.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb1e77ff3382b7862de4771cc82e4178410f9b4abdaad8dd6484948f84002b85 -size 19922 +oid sha256:67ea01561fce12fce8a73073f1303738648c8a5909c1b92ec29402ec5d2bd33d +size 19676 diff --git a/vt/reference/variables/scope-comparison-01.webp b/vt/reference/variables/scope-comparison-01.webp index ff9429d8..47a7d357 100644 --- a/vt/reference/variables/scope-comparison-01.webp +++ b/vt/reference/variables/scope-comparison-01.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfde1d9356aa145418249236197245b58e3126ed8462d244444996d66a10a924 -size 23578 +oid sha256:cfa9cb4dd18725263cde07450cd059dd8b7c76dd59256c190a4f3f1331b2a951 +size 24008 diff --git a/vt/specs/background/transition-both-sides-exit.yaml b/vt/specs/background/transition-both-sides-exit.yaml index cee65a0d..fdf982d6 100644 --- a/vt/specs/background/transition-both-sides-exit.yaml +++ b/vt/specs/background/transition-both-sides-exit.yaml @@ -1,14 +1,15 @@ --- title: Background Transition Exit With Both Sides description: | - Background image exiting to transparency with a transition resource that + Background image animating in place with a transition resource that defines both prev and next alpha tweens. - Only the prev side should matter for this exit lifecycle. + When the same background still resolves from state, the transition should + run against that single persisted background target. specs: - - background exit should tolerate transition resources that also define next + - background should tolerate transition resources that define both prev and next without repeating resourceId - the background should be partially visible halfway through the transition - - the background should be fully cleared after the transition completes + - the background should remain resolved from state after the transition completes steps: - action: customEvent name: vt:nextLine diff --git a/vt/specs/background/transition-mask-out.yaml b/vt/specs/background/transition-mask-out.yaml index cd3c3988..560540f8 100644 --- a/vt/specs/background/transition-mask-out.yaml +++ b/vt/specs/background/transition-mask-out.yaml @@ -1,10 +1,10 @@ --- title: Background Transition Mask Out -description: Background image exiting to transparency with a mask-driven transition. +description: Background image animating with a mask-driven transition while the same background persists in state. specs: - - background out should accept transition-type animation resources + - background should accept transition-type animation resources without repeating resourceId - the mask should partially remove the background halfway through the transition - - the background should be fully cleared after the transition completes + - the background should remain resolved from state after the transition completes steps: - action: customEvent name: vt:nextLine diff --git a/vt/specs/background/transition-persisted-state.yaml b/vt/specs/background/transition-persisted-state.yaml new file mode 100644 index 00000000..303a52af --- /dev/null +++ b/vt/specs/background/transition-persisted-state.yaml @@ -0,0 +1,86 @@ +--- +title: Background Transition Persisted State +description: | + Background animation-only actions should resolve the current background from + presentation state when the line omits `resourceId`. +specs: + - background should animate without repeating resourceId when the same background persists in state + - the background should still be visible on the following line because state persistence is preserved + - the transition line should not clear the background from presentation state +steps: + - action: customEvent + name: vt:nextLine + - action: customEvent + name: snapShotKeyFrame + detail: + deltaMS: 500 + - action: screenshot + - action: customEvent + name: snapShotKeyFrame + detail: + deltaMS: 500 + - action: screenshot + - action: customEvent + name: vt:nextLine + - action: screenshot +--- +screen: + width: 1920 + height: 1080 + backgroundColor: "#000000" +resources: + controls: + base1: + elements: + - id: click-area + type: rect + width: 1920 + height: 1080 + alpha: 0.001 + click: + payload: + actions: + nextLine: {} + colorId: overlayColor + images: + bg-door: + fileId: lakjf3lka + width: 1920 + height: 1080 + animations: + bg-persisted-shift: + name: Background Persisted Shift + type: transition + next: + tween: + translateX: + initialValue: 1 + keyframes: + - duration: 1000 + value: 0 + easing: linear + colors: + overlayColor: + hex: "#000000" +story: + initialSceneId: backgroundScene + scenes: + backgroundScene: + name: Background Scene + initialSectionId: main + sections: + main: + lines: + - id: line1 + actions: + control: + resourceId: base1 + background: + resourceId: bg-door + - id: line2 + actions: + background: + animations: + resourceId: bg-persisted-shift + - id: line3 + actions: {} diff --git a/vt/specs/background/transition-tween-out.yaml b/vt/specs/background/transition-tween-out.yaml index 77a9a62a..8ba4f143 100644 --- a/vt/specs/background/transition-tween-out.yaml +++ b/vt/specs/background/transition-tween-out.yaml @@ -1,10 +1,10 @@ --- title: Background Transition Tween Out -description: Background image exiting to transparency with a transition tween. +description: Background image animating with a transition tween while the same background persists in state. specs: - - background out should accept transition-type animation resources + - background should accept transition-type animation resources without repeating resourceId - the background should be partially moved offscreen halfway through the transition - - the background should be fully cleared after the transition completes + - the background should remain resolved from state after the transition completes steps: - action: customEvent name: vt:nextLine diff --git a/vt/specs/background/update-enter-fallback.yaml b/vt/specs/background/update-enter-fallback.yaml new file mode 100644 index 00000000..6c48cac8 --- /dev/null +++ b/vt/specs/background/update-enter-fallback.yaml @@ -0,0 +1,85 @@ +--- +title: Background Update Enter Fallback +description: | + Background enter should animate the incoming image even when the referenced + animation resource is typed as `update`. +specs: + - background enter should animate the incoming image for update-type animation resources + - the background should be partially visible halfway through the update tween + - the background should be fully visible after the update tween completes +steps: + - action: customEvent + name: vt:nextLine + - action: customEvent + name: snapShotKeyFrame + detail: + deltaMS: 500 + - action: screenshot + - action: customEvent + name: snapShotKeyFrame + detail: + deltaMS: 500 + - action: screenshot +--- +screen: + width: 1920 + height: 1080 + backgroundColor: "#000000" +resources: + controls: + base1: + elements: + - id: click-area + type: rect + width: 1920 + height: 1080 + alpha: 0.001 + click: + payload: + actions: + nextLine: {} + colorId: overlayColor + images: + bg-forest: + fileId: dmni32 + width: 1920 + height: 1080 + animations: + bg-update-fade-in: + name: Background Update Fade In + type: update + tween: + alpha: + initialValue: 0 + keyframes: + - duration: 1000 + value: 1 + easing: linear + x: + initialValue: 1920 + keyframes: + - duration: 1000 + value: 0 + easing: linear + colors: + overlayColor: + hex: "#000000" +story: + initialSceneId: backgroundScene + scenes: + backgroundScene: + name: Background Scene + initialSectionId: main + sections: + main: + lines: + - id: line1 + actions: + control: + resourceId: base1 + - id: line2 + actions: + background: + resourceId: bg-forest + animations: + resourceId: bg-update-fade-in diff --git a/vt/static/main.js b/vt/static/main.js index 0ac0378f..aaeeab74 100644 --- a/vt/static/main.js +++ b/vt/static/main.js @@ -23,6 +23,35 @@ import createRouteGraphics, { const projectData = parse(window.yamlContent); const namespace = `vt:${window.location.pathname}`; +const isVtCaptureMode = () => + window?.RTGL_VT_DEBUG === true || navigator.webdriver === true; + +const downscaleBase64Image = async (base64, scale = 0.5) => { + if (!isVtCaptureMode() || scale === 1) { + return base64; + } + + const blob = await (await fetch(base64)).blob(); + const bitmap = await createImageBitmap(blob); + const canvas = document.createElement("canvas"); + + canvas.width = Math.max(1, Math.round(bitmap.width * scale)); + canvas.height = Math.max(1, Math.round(bitmap.height * scale)); + + const context = canvas.getContext("2d"); + + if (!context) { + bitmap.close?.(); + throw new Error("Failed to create VT screenshot canvas."); + } + + context.imageSmoothingEnabled = true; + context.imageSmoothingQuality = "high"; + context.drawImage(bitmap, 0, 0, canvas.width, canvas.height); + bitmap.close?.(); + + return canvas.toDataURL("image/png"); +}; const init = async () => { const screenWidth = projectData?.screen?.width ?? 1920; @@ -149,11 +178,15 @@ const init = async () => { const routeGraphics = createRouteGraphics(); window.takeVtScreenshotBase64 = async (label) => { - if (label) { - return await routeGraphics.extractBase64(label); + let base64; + + try { + base64 = await routeGraphics.extractBase64(label); + } catch { + base64 = routeGraphics.canvas.toDataURL("image/png"); } - return routeGraphics.canvas.toDataURL("image/png"); + return await downscaleBase64Image(base64); }; const plugins = {