Compare commits

..

No commits in common. "d6262903bf48a775c29c7a7988cb56ad9372bdb0" and "3b8d066cd354000e1a754c8a0011efcc6ca34ca8" have entirely different histories.

3 changed files with 110 additions and 725 deletions

190
package-lock.json generated
View File

@ -108,16 +108,6 @@
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
@ -165,16 +155,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz",
@ -197,16 +177,6 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
@ -950,6 +920,19 @@
}
}
},
"node_modules/@ianvs/prettier-plugin-sort-imports/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@ -1313,43 +1296,43 @@
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.2.tgz",
"integrity": "sha512-ZwFnxH+1z8Ehh8bNTMX3YFrYdzAv7JLY5X5X7XSFY+G9QGJVce/P9xb2mh+j5hKt8NceuHmdtllJvAHWKtsNrQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.1.tgz",
"integrity": "sha512-xvlh4pvfG/bkv0fEtJDABAm1tjtSmSyi2QmS4zyj1EKNI1UiOYiUq1IphSwDsNJ5vJ9cWEGs4rJXpUdCN2kujQ==",
"license": "MIT",
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "1.29.2",
"tailwindcss": "4.1.2"
"tailwindcss": "4.1.1"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.2.tgz",
"integrity": "sha512-Zwz//1QKo6+KqnCKMT7lA4bspGfwEgcPAHlSthmahtgrpKDfwRGk8PKQrW8Zg/ofCDIlg6EtjSTKSxxSufC+CQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.1.tgz",
"integrity": "sha512-7+YBgnPQ4+jv6B6WVOerJ6WOzDzNJXrRKDts674v6TKAqFqYRr9+EBtSziO7nNcwQ8JtoZNMeqA+WJDjtCM/7w==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.2",
"@tailwindcss/oxide-darwin-arm64": "4.1.2",
"@tailwindcss/oxide-darwin-x64": "4.1.2",
"@tailwindcss/oxide-freebsd-x64": "4.1.2",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.2",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.2",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.2",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.2",
"@tailwindcss/oxide-linux-x64-musl": "4.1.2",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.2",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.2"
"@tailwindcss/oxide-android-arm64": "4.1.1",
"@tailwindcss/oxide-darwin-arm64": "4.1.1",
"@tailwindcss/oxide-darwin-x64": "4.1.1",
"@tailwindcss/oxide-freebsd-x64": "4.1.1",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.1",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.1",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.1",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.1",
"@tailwindcss/oxide-linux-x64-musl": "4.1.1",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.1",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.1"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.2.tgz",
"integrity": "sha512-IxkXbntHX8lwGmwURUj4xTr6nezHhLYqeiJeqa179eihGv99pRlKV1W69WByPJDQgSf4qfmwx904H6MkQqTA8w==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.1.tgz",
"integrity": "sha512-gTyRzfdParpoCU1yyUC/iN6XK6T0Ra4bDlF8Aeul5NP9cLzKEZDogdNVNGv5WZmCDkVol7qlex7TMmcfytMmmw==",
"cpu": [
"arm64"
],
@ -1363,9 +1346,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.2.tgz",
"integrity": "sha512-ZRtiHSnFYHb4jHKIdzxlFm6EDfijTCOT4qwUhJ3GWxfDoW2yT3z/y8xg0nE7e72unsmSj6dtfZ9Y5r75FIrlpA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.1.tgz",
"integrity": "sha512-dI0QbdMWBvLB3MtaTKetzUKG9CUUQow8JSP4Nm+OxVokeZ+N+f1OmZW/hW1LzMxpx9RQCBgSRL+IIvKRat5Wdg==",
"cpu": [
"arm64"
],
@ -1379,9 +1362,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.2.tgz",
"integrity": "sha512-BiKUNZf1A0pBNzndBvnPnBxonCY49mgbOsPfILhcCE5RM7pQlRoOgN7QnwNhY284bDbfQSEOWnFR0zbPo6IDTw==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.1.tgz",
"integrity": "sha512-2Y+NPQOTRBCItshPgY/CWg4bKi7E9evMg4bgdb6h9iZObCZLOe3doPcuSxGS3DB0dKyMFKE8pTdWtFUbxZBMSA==",
"cpu": [
"x64"
],
@ -1395,9 +1378,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.2.tgz",
"integrity": "sha512-Z30VcpUfRGkiddj4l5NRCpzbSGjhmmklVoqkVQdkEC0MOelpY+fJrVhzSaXHmWrmSvnX8yiaEqAbdDScjVujYQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.1.tgz",
"integrity": "sha512-N97NGMsB/7CHShbc5ube4dcsW/bYENkBrg8yWi8ieN9boYVRdw3cZviVryV/Nfu9bKbBV9kUvduFF2qBI7rEqg==",
"cpu": [
"x64"
],
@ -1411,9 +1394,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.2.tgz",
"integrity": "sha512-w3wsK1ChOLeQ3gFOiwabtWU5e8fY3P1Ss8jR3IFIn/V0va3ir//hZ8AwURveS4oK1Pu6b8i+yxesT4qWnLVUow==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.1.tgz",
"integrity": "sha512-33Lk6KbHnUZbXqza6RWNFo9wqPQ4+H5BAn1CkUUfC1RZ1vYbyDN6+iJPj53wmnWJ3mhRI8jWt3Jt1fO02IVdUQ==",
"cpu": [
"arm"
],
@ -1427,9 +1410,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.2.tgz",
"integrity": "sha512-oY/u+xJHpndTj7B5XwtmXGk8mQ1KALMfhjWMMpE8pdVAznjJsF5KkCceJ4Fmn5lS1nHMCwZum5M3/KzdmwDMdw==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.1.tgz",
"integrity": "sha512-LyW35RzSUy+80WYScv03HKasAUmMFDaSbNpWfk1gG5gEE9kuRGnDzSrqMoLAmY/kzMCYP/1kqmUiAx8EFLkI2A==",
"cpu": [
"arm64"
],
@ -1443,9 +1426,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.2.tgz",
"integrity": "sha512-k7G6vcRK/D+JOWqnKzKN/yQq1q4dCkI49fMoLcfs2pVcaUAXEqCP9NmA8Jv+XahBv5DtDjSAY3HJbjosEdKczg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.1.tgz",
"integrity": "sha512-1KPnDMlHdqjPTUSFjx55pafvs8RZXRgxfeRgUrukwDKkuj7gFk28vW3Mx65YdiugAc9NWs3VgueZWaM1Po6uGw==",
"cpu": [
"arm64"
],
@ -1459,9 +1442,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.2.tgz",
"integrity": "sha512-fLL+c678TkYKgkDLLNxSjPPK/SzTec7q/E5pTwvpTqrth867dftV4ezRyhPM5PaiCqX651Y8Yk0wRQMcWUGnmQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.1.tgz",
"integrity": "sha512-4WdzA+MRlsinEEE6yxNMLJxpw0kE9XVipbAKdTL8BeUpyC2TdA3TL46lBulXzKp3BIxh3nqyR/UCqzl5o+3waQ==",
"cpu": [
"x64"
],
@ -1475,9 +1458,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.2.tgz",
"integrity": "sha512-0tU1Vjd1WucZ2ooq6y4nI9xyTSaH2g338bhrqk+2yzkMHskBm+pMsOCfY7nEIvALkA1PKPOycR4YVdlV7Czo+A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.1.tgz",
"integrity": "sha512-q7Ugbw3ARcjCW2VMUYrcMbJ6aMQuWPArBBE2EqC/swPZTdGADvMQSlvR0VKusUM4HoSsO7ZbvcZ53YwR57+AKw==",
"cpu": [
"x64"
],
@ -1491,9 +1474,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.2.tgz",
"integrity": "sha512-r8QaMo3QKiHqUcn+vXYCypCEha+R0sfYxmaZSgZshx9NfkY+CHz91aS2xwNV/E4dmUDkTPUag7sSdiCHPzFVTg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.1.tgz",
"integrity": "sha512-0KpqsovgHcIzm7eAGzzEZsEs0/nPYXnRBv+aPq/GehpNQuE/NAQu+YgZXIIof+VflDFuyXOEnaFr7T5MZ1INhA==",
"cpu": [
"arm64"
],
@ -1507,9 +1490,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.2.tgz",
"integrity": "sha512-lYCdkPxh9JRHXoBsPE8Pu/mppUsC2xihYArNAESub41PKhHTnvn6++5RpmFM+GLSt3ewyS8fwCVvht7ulWm6cw==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.1.tgz",
"integrity": "sha512-B1mjeXNS26kBOHv5sXARf6Wd0PWHV9x1TDlW0ummrBUOUAxAy5wcy4Nii1wzNvCdvC448hgiL06ylhwAbNthmg==",
"cpu": [
"x64"
],
@ -1523,14 +1506,14 @@
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.2.tgz",
"integrity": "sha512-3r/ZdMW0gxY8uOx1To0lpYa4coq4CzINcCX4laM1rS340Kcn0ac4A/MMFfHN8qba51aorZMYwMcOxYk4wJ9FYg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.1.tgz",
"integrity": "sha512-tFTkRZwXq4XKr3S2dUZBxy80wbWYHdDSsu4QOB1yE1HJFKjfxKVpXtup4dyTVdQcLInoHC9lZXFPHnjoBP774g==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.2",
"@tailwindcss/oxide": "4.1.2",
"tailwindcss": "4.1.2"
"@tailwindcss/node": "4.1.1",
"@tailwindcss/oxide": "4.1.1",
"tailwindcss": "4.1.1"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
@ -2014,9 +1997,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001709",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz",
"integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==",
"version": "1.0.30001707",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
"dev": true,
"funding": [
{
@ -2178,9 +2161,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.131",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.131.tgz",
"integrity": "sha512-fJFRYXVEJgDCiqFOgRGJm8XR97hZ13tw7FXI9k2yC5hgY+nyzC2tMO8baq1cQR7Ur58iCkASx2zrkZPZUnfzPg==",
"version": "1.5.130",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.130.tgz",
"integrity": "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==",
"dev": true,
"license": "ISC"
},
@ -3273,16 +3256,13 @@
}
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
@ -3393,9 +3373,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.2.tgz",
"integrity": "sha512-VCsK+fitIbQF7JlxXaibFhxrPq4E2hDcG8apzHUdWFMCQWD8uLdlHg4iSkZ53cgLCCcZ+FZK7vG8VjvLcnBgKw==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz",
"integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==",
"license": "MIT"
},
"node_modules/tapable": {
@ -3493,9 +3473,9 @@
}
},
"node_modules/vite": {
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",

View File

@ -2,17 +2,7 @@
<!-- Make the outer container always pointer-events-none so clicks pass through -->
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none">
<!-- Apply pointer-events-auto ONLY to the modal itself so it can be interacted with -->
<div
class="bg-gray-800 rounded-lg overflow-auto scrollbar-hide shadow-lg pointer-events-auto relative"
:class="{ invisible: !isModalOpen, visible: isModalOpen }"
:style="{
transform: `translate3d(${position.x}px, ${position.y + (isModalOpen ? 0 : -20)}px, 0)`,
width: `${modalSize.width}px`,
height: `${modalSize.height}px`,
maxWidth: '90vw',
maxHeight: '90vh',
}"
>
<div class="bg-gray-800 rounded-lg max-w-4xl max-h-[90vh] overflow-auto scrollbar-hide shadow-lg pointer-events-auto" :class="{ invisible: !isModalOpen, visible: isModalOpen }" :style="{ transform: `translate3d(${position.x}px, ${position.y + (isModalOpen ? 0 : -20)}px, 0)` }">
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600 cursor-move" @mousedown="startDrag">
<div class="flex items-center gap-2 text-lg font-semibold select-none">
<i class="fas fa-film text-blue-500"></i>
@ -23,7 +13,7 @@
</button>
</div>
<div class="p-6 flex flex-col h-[calc(100%-64px)]">
<div class="p-6">
<div class="flex flex-wrap items-center gap-4 mb-6">
<div class="flex gap-2">
<button
@ -65,91 +55,16 @@
</div>
</div>
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
<!-- Zoom controls -->
<div class="flex items-center gap-2">
<button @click="zoomOut" :disabled="previewZoom <= 1" class="flex items-center justify-center w-8 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
<i class="fas fa-search-minus"></i>
</button>
<span class="text-sm text-gray-300">{{ Math.round(previewZoom * 100) }}%</span>
<button @click="zoomIn" :disabled="previewZoom >= 5" class="flex items-center justify-center w-8 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
<i class="fas fa-search-plus"></i>
</button>
<button
@click="applyOffsetsToMainView"
:disabled="!hasSpriteOffset"
class="flex items-center gap-1 px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
title="Permanently apply offset to sprite position"
>
<i class="fas fa-save"></i>
Apply Offset
</button>
<button @click="resetZoom" :disabled="previewZoom === 1" class="flex items-center justify-center px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">Reset Zoom</button>
</div>
<!-- Controls -->
<div class="flex items-center gap-4">
<!-- Show all sprites toggle -->
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
<input type="checkbox" v-model="showAllSprites" class="form-checkbox h-4 w-4 text-blue-500 rounded border-gray-600 bg-gray-700 focus:ring-blue-500" />
Show all frames
</label>
<!-- Reset sprite position button -->
<button
@click="resetSpritePosition"
:disabled="!hasSpriteOffset"
class="flex items-center gap-1 px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
title="Reset sprite to original position"
>
<i class="fas fa-crosshairs"></i>
Reset Position
</button>
</div>
<div class="flex justify-center bg-gray-700 p-6 rounded mb-6">
<canvas ref="animCanvas" class="block" style="image-rendering: pixelated"></canvas>
</div>
<div class="flex flex-col justify-center items-center bg-gray-700 p-6 rounded mb-6 relative overflow-auto flex-grow">
<!-- Tooltip for dragging instructions -->
<div class="text-xs text-gray-400 mb-2" v-if="hasSpriteOffset || sprites.length > 0">
<span v-if="hasSpriteOffset">Sprite offset: {{ Math.round(spriteOffset.x) }}px, {{ Math.round(spriteOffset.y) }}px</span>
<span v-else>Click and drag the sprite to move it within the cell</span>
</div>
<div
class="canvas-container relative transition-transform duration-100 flex items-center justify-center"
:style="{
minWidth: `${store.cellSize.width * previewZoom}px`,
minHeight: `${store.cellSize.height * previewZoom}px`,
cursor: previewZoom > 1 ? (isViewportDragging ? 'grabbing' : 'grab') : 'default',
}"
@mousedown="startViewportDrag"
@wheel="handleCanvasWheel"
>
<div
class="sprite-wrapper"
:style="{
transform: `scale(${previewZoom}) translate(${viewportOffset.x}px, ${viewportOffset.y}px)`,
cursor: isCanvasDragging ? 'grabbing' : 'move',
}"
@mousedown.stop="startCanvasDrag"
title="Drag to move sprite within cell"
>
<canvas ref="animCanvas" class="block" style="image-rendering: pixelated"></canvas>
</div>
</div>
</div>
</div>
<!-- Resize handle - larger and more noticeable -->
<div class="absolute bottom-0 right-0 w-8 h-8 cursor-se-resize flex items-end justify-end bg-gradient-to-br from-transparent to-gray-700 hover:to-blue-500 transition-colors duration-200" @mousedown="startResize">
<i class="fas fa-grip-lines-diagonal text-gray-400 hover:text-white p-1"></i>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
const store = useSpritesheetStore();
@ -165,59 +80,12 @@
const isDragging = ref(false);
const dragOffset = ref({ x: 0, y: 0 });
// New state for added features
const previewZoom = ref(1);
const showAllSprites = ref(false);
// Use a computed property to access and modify the store's sprite offset for the current frame
const spriteOffset = computed({
get: () => {
// Get the offset for the current frame
const frameOffset = store.getSpriteOffset(currentFrame.value);
// Update the current offset for UI display
store.currentSpriteOffset.x = frameOffset.x;
store.currentSpriteOffset.y = frameOffset.y;
return store.currentSpriteOffset;
},
set: val => {
// Update both the current offset and the frame-specific offset
store.currentSpriteOffset.x = val.x;
store.currentSpriteOffset.y = val.y;
// Get the frame-specific offset and update it
const frameOffset = store.getSpriteOffset(currentFrame.value);
frameOffset.x = val.x;
frameOffset.y = val.y;
},
});
const isCanvasDragging = ref(false);
const canvasDragStart = ref({ x: 0, y: 0 });
// Canvas viewport navigation state
const viewportOffset = ref({ x: 0, y: 0 });
const isViewportDragging = ref(false);
const viewportDragStart = ref({ x: 0, y: 0 });
// Modal size state for resize functionality
const modalSize = ref({ width: 800, height: 600 });
const isResizing = ref(false);
const initialSize = ref({ width: 0, height: 0 });
const resizeStart = ref({ x: 0, y: 0 });
const currentFrameDisplay = computed(() => {
if (sprites.value.length === 0) return '0 / 0';
return `${currentFrame.value + 1} / ${sprites.value.length}`;
const totalFrames = Math.max(1, sprites.value.length);
const frame = Math.min(currentFrame.value + 1, totalFrames);
return `${frame} / ${totalFrames}`;
});
// Computed property to check if sprite has been moved from original position
const hasSpriteOffset = computed(() => {
return spriteOffset.x !== 0 || spriteOffset.y !== 0;
});
const applyOffsetsToMainView = () => {
store.applyOffsetsToMainView();
store.showNotification('Offset permanently applied to sprite position');
};
const handleKeyDown = (e: KeyboardEvent) => {
if (!isModalOpen.value) return;
@ -239,51 +107,6 @@
// Previous frame
currentFrame.value = (currentFrame.value - 1 + sprites.value.length) % sprites.value.length;
updateFrame();
} else if (e.key === '+' || e.key === '=') {
// Zoom in
zoomIn();
e.preventDefault();
} else if (e.key === '-' || e.key === '_') {
// Zoom out
zoomOut();
e.preventDefault();
} else if (e.key === '0') {
// Reset zoom
resetZoom();
e.preventDefault();
} else if (e.key === 'r') {
if (e.shiftKey) {
// Shift+R: Reset sprite position only
resetSpritePosition();
} else {
// R: Reset both sprite position and viewport
// Reset the sprite offset for the current frame
const frameOffset = store.getSpriteOffset(currentFrame.value);
frameOffset.x = 0;
frameOffset.y = 0;
store.currentSpriteOffset.x = 0;
store.currentSpriteOffset.y = 0;
viewportOffset.value = { x: 0, y: 0 };
updateFrame();
store.showNotification('View and position reset');
}
e.preventDefault();
} else if (previewZoom.value > 1) {
// Arrow key navigation for panning when zoomed in
const panAmount = 10;
if (e.key === 'ArrowUp') {
viewportOffset.value.y += panAmount / previewZoom.value;
e.preventDefault();
} else if (e.key === 'ArrowDown') {
viewportOffset.value.y -= panAmount / previewZoom.value;
e.preventDefault();
} else if (e.key === 'ArrowLeft' && animation.value.isPlaying) {
viewportOffset.value.x += panAmount / previewZoom.value;
e.preventDefault();
} else if (e.key === 'ArrowRight' && animation.value.isPlaying) {
viewportOffset.value.x -= panAmount / previewZoom.value;
e.preventDefault();
}
}
};
@ -293,20 +116,8 @@
return;
}
// Center the modal in the viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
position.value = {
x: (viewportWidth - modalSize.value.width) / 2,
y: (viewportHeight - modalSize.value.height) / 2,
};
// Reset zoom but keep sprite offset if it exists
previewZoom.value = 1;
viewportOffset.value = { x: 0, y: 0 };
// Reset modal size to default
modalSize.value = { width: 800, height: 600 };
// Reset position when opening
position.value = { x: 0, y: 0 };
// Reset to first frame
currentFrame.value = 0;
@ -327,15 +138,7 @@
// Force render the first frame
if (sprites.value.length > 0) {
// Get the frame-specific offset for the first frame
const frameOffset = store.getSpriteOffset(0);
// Update the current offset for UI display
store.currentSpriteOffset.x = frameOffset.x;
store.currentSpriteOffset.y = frameOffset.y;
// Render with the frame-specific offset
store.renderAnimationFrame(0, showAllSprites.value, frameOffset);
store.renderAnimationFrame(0);
}
};
@ -343,9 +146,6 @@
if (animCanvas.value && store.cellSize.width && store.cellSize.height) {
animCanvas.value.width = store.cellSize.width;
animCanvas.value.height = store.cellSize.height;
// Also update container size
updateCanvasContainerSize();
}
};
@ -368,23 +168,17 @@
};
const handleFrameChange = () => {
// Stop any running animation
if (animation.value.isPlaying) {
stopAnimation();
}
// Ensure frame is within bounds
currentFrame.value = Math.max(0, Math.min(currentFrame.value, sprites.value.length - 1));
updateFrame();
};
const updateFrame = () => {
// Ensure frame is within bounds
currentFrame.value = Math.max(0, Math.min(currentFrame.value, sprites.value.length - 1));
animation.value.currentFrame = currentFrame.value;
animation.value.manualUpdate = true;
// Get the frame-specific offset
const frameOffset = store.getSpriteOffset(currentFrame.value);
store.renderAnimationFrame(currentFrame.value, showAllSprites.value, frameOffset);
store.renderAnimationFrame(currentFrame.value);
};
const handleFrameRateChange = () => {
@ -395,11 +189,7 @@
}
};
// Modal drag functionality
const startDrag = (e: MouseEvent) => {
// Don't allow drag if currently resizing
if (isResizing.value) return;
isDragging.value = true;
dragOffset.value = {
x: e.clientX - position.value.x,
@ -428,45 +218,6 @@
window.removeEventListener('mouseup', stopDrag);
};
// NEW: Modal resize functionality
const startResize = (e: MouseEvent) => {
isResizing.value = true;
initialSize.value = { ...modalSize.value };
resizeStart.value = { x: e.clientX, y: e.clientY };
// Add temporary event listeners
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', stopResize);
// Prevent default to avoid text selection
e.preventDefault();
};
const handleResize = (e: MouseEvent) => {
if (!isResizing.value) return;
const deltaX = e.clientX - resizeStart.value.x;
const deltaY = e.clientY - resizeStart.value.y;
requestAnimationFrame(() => {
// Calculate new size with minimum constraints
const newWidth = Math.max(400, initialSize.value.width + deltaX);
const newHeight = Math.max(400, initialSize.value.height + deltaY);
// Update modal size
modalSize.value = {
width: newWidth,
height: newHeight,
};
});
};
const stopResize = () => {
isResizing.value = false;
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', stopResize);
};
const initializeCanvas = async () => {
if (!animCanvas.value) {
console.error('PreviewModal: Animation canvas not found');
@ -487,211 +238,6 @@
}
};
// Zoom functions
const zoomIn = () => {
if (previewZoom.value < 5) {
previewZoom.value = Math.min(5, previewZoom.value + 0.5);
// Adjust container size after zoom change
nextTick(() => {
updateCanvasContainerSize();
});
}
};
const zoomOut = () => {
if (previewZoom.value > 1) {
previewZoom.value = Math.max(1, previewZoom.value - 0.5);
// Adjust container size after zoom change
nextTick(() => {
updateCanvasContainerSize();
});
}
};
const resetZoom = () => {
previewZoom.value = 1;
viewportOffset.value = { x: 0, y: 0 };
// Adjust container size after zoom change
nextTick(() => {
updateCanvasContainerSize();
});
};
// Reset sprite position to original
const resetSpritePosition = () => {
// Reset the sprite offset for the current frame to zero
const frameOffset = store.getSpriteOffset(currentFrame.value);
frameOffset.x = 0;
frameOffset.y = 0;
// Also update the current offset
store.currentSpriteOffset.x = 0;
store.currentSpriteOffset.y = 0;
// Update the frame to reflect the change
updateFrame();
// Show a notification
store.showNotification('Sprite position reset to original');
};
// Update canvas container size based on zoom level
const updateCanvasContainerSize = () => {
// This ensures the container grows with zoom while keeping the sprite visible
if (!store.cellSize.width || !store.cellSize.height) return;
// We'll update the container if needed through the reactive bindings
};
// Canvas drag functions for moving the sprite within its cell
const startCanvasDrag = (e: MouseEvent) => {
if (sprites.value.length === 0) return;
// Don't start sprite dragging if we're already dragging the viewport
if (isViewportDragging.value) return;
isCanvasDragging.value = true;
canvasDragStart.value = {
x: e.clientX,
y: e.clientY,
};
// Add temporary event listeners
window.addEventListener('mousemove', handleCanvasDrag);
window.addEventListener('mouseup', stopCanvasDrag);
// Prevent default to avoid text selection
e.preventDefault();
};
const handleCanvasDrag = (e: MouseEvent) => {
if (!isCanvasDragging.value) return;
const deltaX = e.clientX - canvasDragStart.value.x;
const deltaY = e.clientY - canvasDragStart.value.y;
requestAnimationFrame(() => {
// Get the frame-specific offset
const frameOffset = store.getSpriteOffset(currentFrame.value);
// Calculate new position
const newX = frameOffset.x + deltaX / previewZoom.value;
const newY = frameOffset.y + deltaY / previewZoom.value;
// Get current sprite
const currentSprite = sprites.value[currentFrame.value];
if (!currentSprite) return;
// Calculate maximum allowed offset based on sprite and cell size
const maxOffsetX = (store.cellSize.width - currentSprite.width) / 2;
const maxOffsetY = (store.cellSize.height - currentSprite.height) / 2;
// Constrain movement to stay within cell boundaries, preventing negative offsets
const constrainedX = Math.max(0, Math.min(maxOffsetX, newX));
const constrainedY = Math.max(0, Math.min(maxOffsetY, newY));
// Update both the current offset and the frame-specific offset
frameOffset.x = constrainedX;
frameOffset.y = constrainedY;
store.currentSpriteOffset.x = constrainedX;
store.currentSpriteOffset.y = constrainedY;
// Reset drag start position
canvasDragStart.value = {
x: e.clientX,
y: e.clientY,
};
// Update the frame with the new offset
updateFrame();
// Re-render the main view to reflect the changes
store.renderSpritesheetPreview();
});
};
const stopCanvasDrag = () => {
isCanvasDragging.value = false;
window.removeEventListener('mousemove', handleCanvasDrag);
window.removeEventListener('mouseup', stopCanvasDrag);
};
// Canvas viewport navigation functions
const startViewportDrag = (e: MouseEvent) => {
// Only enable viewport dragging when zoomed in
if (previewZoom.value <= 1 || isCanvasDragging.value) return;
isViewportDragging.value = true;
viewportDragStart.value = {
x: e.clientX,
y: e.clientY,
};
// Add temporary event listeners
window.addEventListener('mousemove', handleViewportDrag);
window.addEventListener('mouseup', stopViewportDrag);
// Prevent default to avoid text selection
e.preventDefault();
};
const handleViewportDrag = (e: MouseEvent) => {
if (!isViewportDragging.value) return;
const deltaX = e.clientX - viewportDragStart.value.x;
const deltaY = e.clientY - viewportDragStart.value.y;
// Update viewport offset with the delta, scaled by zoom level
viewportOffset.value = {
x: viewportOffset.value.x + deltaX / previewZoom.value,
y: viewportOffset.value.y + deltaY / previewZoom.value,
};
// Reset drag start position
viewportDragStart.value = {
x: e.clientX,
y: e.clientY,
};
};
const stopViewportDrag = () => {
isViewportDragging.value = false;
window.removeEventListener('mousemove', handleViewportDrag);
window.removeEventListener('mouseup', stopViewportDrag);
};
// Handle mouse wheel zooming and panning
const handleCanvasWheel = (e: WheelEvent) => {
// Prevent the default scroll behavior
e.preventDefault();
if (e.ctrlKey) {
// Ctrl + wheel = zoom in/out
if (e.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
} else {
// Just wheel = pan when zoomed in
if (previewZoom.value > 1) {
// Adjust pan amount by zoom level
const panFactor = 0.5 / previewZoom.value;
if (e.shiftKey) {
// Shift + wheel = horizontal pan
viewportOffset.value.x -= e.deltaY * panFactor;
} else {
// Regular wheel = vertical pan
viewportOffset.value.y -= e.deltaY * panFactor;
}
}
}
};
onMounted(() => {
initializeCanvas();
window.addEventListener('keydown', handleKeyDown);
@ -701,12 +247,6 @@
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('mousemove', handleDrag);
window.removeEventListener('mouseup', stopDrag);
window.removeEventListener('mousemove', handleCanvasDrag);
window.removeEventListener('mouseup', stopCanvasDrag);
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', stopResize);
window.removeEventListener('mousemove', handleViewportDrag);
window.removeEventListener('mouseup', stopViewportDrag);
});
// Keep currentFrame in sync with animation.currentFrame
@ -723,42 +263,23 @@
newSprites => {
if (isModalOpen.value && newSprites.length > 0) {
updateCanvasSize();
updateCanvasContainerSize();
store.renderAnimationFrame(currentFrame.value, showAllSprites.value, spriteOffset.value);
store.renderAnimationFrame(currentFrame.value);
}
},
{ deep: true }
);
// Watch zoom changes to update container size
watch(
() => previewZoom.value,
() => {
updateCanvasContainerSize();
}
);
// Watch for changes in border settings to update the preview
watch(
() => previewBorder.value,
() => {
if (isModalOpen.value && sprites.value.length > 0) {
store.renderAnimationFrame(currentFrame.value, showAllSprites.value, spriteOffset.value);
store.renderAnimationFrame(currentFrame.value);
}
},
{ deep: true }
);
// Watch for changes in showAllSprites to update the preview
watch(
() => showAllSprites.value,
() => {
if (isModalOpen.value && sprites.value.length > 0) {
store.renderAnimationFrame(currentFrame.value, showAllSprites.value, spriteOffset.value);
}
}
);
// Expose openModal for external use
defineExpose({ openModal });
</script>
@ -795,21 +316,4 @@
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
/* Cursor styles */
.cursor-se-resize {
cursor: se-resize;
}
/* Canvas container styles */
.canvas-container {
margin: auto;
transition:
min-width 0.2s,
min-height 0.2s;
}
.sprite-wrapper {
transform-origin: center;
}
</style>

View File

@ -256,16 +256,9 @@ export function useSpritesheetStore() {
const x = Math.round(sprite.x);
const y = Math.round(sprite.y);
// Get the frame-specific offset for this sprite
const frameOffset = getSpriteOffset(index);
// Apply the frame-specific offset to the sprite position
const finalX = x + frameOffset.x;
const finalY = y + frameOffset.y;
// Draw the image at its final position with pixel-perfect rendering
// Draw the image at its original size with pixel-perfect rendering
ctx.value!.imageSmoothingEnabled = false; // Keep pixel art sharp
ctx.value!.drawImage(sprite.img, finalX, finalY, sprite.width, sprite.height);
ctx.value!.drawImage(sprite.img, x, y, sprite.width, sprite.height);
} else {
console.warn(`Store: Sprite image ${index} not fully loaded, setting onload handler`);
sprite.img.onload = () => {
@ -274,15 +267,8 @@ export function useSpritesheetStore() {
const x = Math.round(sprite.x);
const y = Math.round(sprite.y);
// Get the frame-specific offset for this sprite
const frameOffset = getSpriteOffset(index);
// Apply the frame-specific offset to the sprite position
const finalX = x + frameOffset.x;
const finalY = y + frameOffset.y;
ctx.value.imageSmoothingEnabled = false; // Keep pixel art sharp
ctx.value.drawImage(sprite.img, finalX, finalY, sprite.width, sprite.height);
ctx.value.drawImage(sprite.img, x, y, sprite.width, sprite.height);
}
};
}
@ -320,28 +306,6 @@ export function useSpritesheetStore() {
}
}
function applyOffsetsToMainView() {
sprites.value.forEach((sprite, index) => {
const frameOffset = getSpriteOffset(index);
if (frameOffset.x !== 0 || frameOffset.y !== 0) {
// Update the sprite's position to include the offset
sprite.x += frameOffset.x;
sprite.y += frameOffset.y;
// Reset the offset
frameOffset.x = 0;
frameOffset.y = 0;
}
});
// Reset current offset
currentSpriteOffset.x = 0;
currentSpriteOffset.y = 0;
// Re-render the main view
renderSpritesheetPreview();
}
function drawGrid() {
if (!ctx.value || !canvas.value) return;
@ -459,7 +423,6 @@ export function useSpritesheetStore() {
animation.canvas.width = cellSize.width;
animation.canvas.height = cellSize.height;
// Start the animation loop without resetting sprite offset
animationLoop();
}
@ -472,7 +435,7 @@ export function useSpritesheetStore() {
}
}
function renderAnimationFrame(frameIndex: number, showAllSprites = false, spriteOffset = { x: 0, y: 0 }) {
function renderAnimationFrame(frameIndex: number) {
if (sprites.value.length === 0 || !animation.canvas || !animation.ctx) return;
if (animation.canvas.width !== cellSize.width || animation.canvas.height !== cellSize.height) {
@ -486,51 +449,17 @@ export function useSpritesheetStore() {
animation.ctx.fillStyle = 'transparent';
animation.ctx.fillRect(0, 0, animation.canvas.width, animation.canvas.height);
// Keep pixel art sharp
animation.ctx.imageSmoothingEnabled = false;
// If showAllSprites is enabled, draw all sprites with transparency
if (showAllSprites && sprites.value.length > 1) {
// Save the current context state
animation.ctx.save();
// Set global alpha for background sprites
animation.ctx.globalAlpha = 0.3;
// Draw all sprites except the current one
sprites.value.forEach((sprite, index) => {
if (index !== frameIndex) {
const spriteCellX = Math.floor(sprite.x / cellSize.width);
const spriteCellY = Math.floor(sprite.y / cellSize.height);
// Calculate precise offset for pixel-perfect rendering
const spriteOffsetX = Math.round(sprite.x - spriteCellX * cellSize.width);
const spriteOffsetY = Math.round(sprite.y - spriteCellY * cellSize.height);
// Draw the sprite with transparency
animation.ctx.drawImage(sprite.img, spriteOffsetX, spriteOffsetY);
}
});
// Restore the context to full opacity
animation.ctx.restore();
}
// Get the current sprite
const currentSprite = sprites.value[frameIndex % sprites.value.length];
const cellX = Math.floor(currentSprite.x / cellSize.width);
const cellY = Math.floor(currentSprite.y / cellSize.height);
// Calculate the original position (without user offset)
const originalOffsetX = Math.round(currentSprite.x - cellX * cellSize.width);
const originalOffsetY = Math.round(currentSprite.y - cellY * cellSize.height);
// Calculate precise offset for pixel-perfect rendering
const offsetX = Math.round(currentSprite.x - cellX * cellSize.width);
const offsetY = Math.round(currentSprite.y - cellY * cellSize.height);
// Calculate precise offset for pixel-perfect rendering, including the user's drag offset
const offsetX = originalOffsetX + spriteOffset.x;
const offsetY = originalOffsetY + spriteOffset.y;
// Draw the current sprite at full opacity at the new position
// Keep pixel art sharp
animation.ctx.imageSmoothingEnabled = false;
animation.ctx.drawImage(currentSprite.img, offsetX, offsetY);
// Draw border around the cell if enabled (only for preview, not included in download)
@ -548,21 +477,6 @@ export function useSpritesheetStore() {
}
}
// Store the current sprite offset for animation playback
// We'll use a Map to store offsets for each frame, so they're preserved when switching frames
const spriteOffsets = reactive(new Map<number, { x: number; y: number }>());
// Current sprite offset is a reactive object that will be used for the current frame
const currentSpriteOffset = reactive({ x: 0, y: 0 });
// Helper function to get the offset for a specific frame
function getSpriteOffset(frameIndex: number) {
if (!spriteOffsets.has(frameIndex)) {
spriteOffsets.set(frameIndex, { x: 0, y: 0 });
}
return spriteOffsets.get(frameIndex)!;
}
function animationLoop(timestamp?: number) {
if (!animation.isPlaying) return;
@ -574,17 +488,8 @@ export function useSpritesheetStore() {
animation.lastFrameTime = currentTime;
if (sprites.value.length > 0) {
// Get the stored offset for the current frame
const frameOffset = getSpriteOffset(animation.currentFrame);
renderAnimationFrame(animation.currentFrame);
// Update the current offset for rendering
currentSpriteOffset.x = frameOffset.x;
currentSpriteOffset.y = frameOffset.y;
// Render the current frame with its offset
renderAnimationFrame(animation.currentFrame, false, frameOffset);
// Move to the next frame
animation.currentFrame = (animation.currentFrame + 1) % sprites.value.length;
if (animation.slider) {
@ -644,9 +549,6 @@ export function useSpritesheetStore() {
notification,
zoomLevel,
previewBorder,
currentSpriteOffset,
spriteOffsets,
getSpriteOffset,
addSprites,
updateCellSize,
updateCanvasSize,
@ -663,6 +565,5 @@ export function useSpritesheetStore() {
zoomIn,
zoomOut,
resetZoom,
applyOffsetsToMainView,
};
}