diff --git a/package-lock.json b/package-lock.json index 5a6879d..07d10d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1386,9 +1385,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", - "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", "cpu": [ "arm" ], @@ -1399,9 +1398,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", - "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", "cpu": [ "arm64" ], @@ -1412,9 +1411,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", - "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", "cpu": [ "arm64" ], @@ -1425,9 +1424,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", - "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", "cpu": [ "x64" ], @@ -1438,9 +1437,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", - "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", "cpu": [ "arm64" ], @@ -1451,9 +1450,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", - "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", "cpu": [ "x64" ], @@ -1464,9 +1463,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", - "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", "cpu": [ "arm" ], @@ -1477,9 +1476,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", - "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", "cpu": [ "arm" ], @@ -1490,9 +1489,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", - "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", "cpu": [ "arm64" ], @@ -1503,9 +1502,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", - "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", "cpu": [ "arm64" ], @@ -1516,9 +1515,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", - "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", "cpu": [ "loong64" ], @@ -1529,9 +1528,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", - "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", "cpu": [ "ppc64" ], @@ -1542,9 +1541,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", - "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", "cpu": [ "riscv64" ], @@ -1555,9 +1554,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", - "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", "cpu": [ "riscv64" ], @@ -1568,9 +1567,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", - "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", "cpu": [ "s390x" ], @@ -1581,9 +1580,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", - "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "cpu": [ "x64" ], @@ -1594,9 +1593,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", - "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", "cpu": [ "x64" ], @@ -1607,9 +1606,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", - "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", "cpu": [ "arm64" ], @@ -1620,9 +1619,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", - "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", "cpu": [ "arm64" ], @@ -1633,9 +1632,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", - "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", "cpu": [ "ia32" ], @@ -1646,9 +1645,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", - "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", "cpu": [ "x64" ], @@ -1659,9 +1658,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "cpu": [ "x64" ], @@ -1984,7 +1983,6 @@ "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2113,7 +2111,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz", "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.24", @@ -2377,9 +2374,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", - "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", + "version": "2.8.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", + "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2438,7 +2435,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2477,9 +2473,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001755", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", - "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", "dev": true, "funding": [ { @@ -2670,9 +2666,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.255", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.255.tgz", - "integrity": "sha512-Z9oIp4HrFF/cZkDPMpz2XSuVpc1THDpT4dlmATFlJUIBVCy9Vap5/rIXsASP1CscBacBqhabwh8vLctqBwEerQ==", + "version": "1.5.259", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", + "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", "dev": true, "license": "ISC" }, @@ -3809,7 +3805,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3832,7 +3827,6 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3915,11 +3909,10 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", - "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -3931,28 +3924,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.2", - "@rollup/rollup-android-arm64": "4.53.2", - "@rollup/rollup-darwin-arm64": "4.53.2", - "@rollup/rollup-darwin-x64": "4.53.2", - "@rollup/rollup-freebsd-arm64": "4.53.2", - "@rollup/rollup-freebsd-x64": "4.53.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", - "@rollup/rollup-linux-arm-musleabihf": "4.53.2", - "@rollup/rollup-linux-arm64-gnu": "4.53.2", - "@rollup/rollup-linux-arm64-musl": "4.53.2", - "@rollup/rollup-linux-loong64-gnu": "4.53.2", - "@rollup/rollup-linux-ppc64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-musl": "4.53.2", - "@rollup/rollup-linux-s390x-gnu": "4.53.2", - "@rollup/rollup-linux-x64-gnu": "4.53.2", - "@rollup/rollup-linux-x64-musl": "4.53.2", - "@rollup/rollup-openharmony-arm64": "4.53.2", - "@rollup/rollup-win32-arm64-msvc": "4.53.2", - "@rollup/rollup-win32-ia32-msvc": "4.53.2", - "@rollup/rollup-win32-x64-gnu": "4.53.2", - "@rollup/rollup-win32-x64-msvc": "4.53.2", + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" } }, @@ -4012,7 +4005,6 @@ "integrity": "sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.5.0", "buffer-builder": "^0.2.0", @@ -4569,7 +4561,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4613,7 +4604,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4701,7 +4691,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -4881,7 +4870,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4901,7 +4889,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz", "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.24", "@vue/compiler-sfc": "3.5.24", diff --git a/src/App.vue b/src/App.vue index d57a487..8e1228d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,7 +11,6 @@ Help Feedback -

Upload sprites

@@ -26,7 +25,7 @@
-
+

Create spritesheets for your game development and animation projects with our completely free, open-source spritesheet generator.

This powerful online tool lets you upload individual sprite images and automatically arrange them into optimized sprite sheets with customizable layouts - perfect for indie developers, animators, and studios of any size.

@@ -37,7 +36,24 @@
-
+
+
+
+ Layers + +
+
+
+ + + + + +
+
+
@@ -107,14 +123,13 @@ Preview animation
- - +
- + @@ -150,25 +165,41 @@ import SpritesheetSplitter from './components/SpritesheetSplitter.vue'; import GifFpsModal from './components/GifFpsModal.vue'; import DarkModeToggle from './components/utilities/DarkModeToggle.vue'; - import { useSprites, getMaxDimensions } from './composables/useSprites'; - import { useExport } from './composables/useExport'; + import { useExportLayers } from './composables/useExportLayers'; + import { useLayers } from './composables/useLayers'; + import { getMaxDimensionsAcrossLayers } from './composables/useLayers'; import { useSettingsStore } from './stores/useSettingsStore'; import { calculateNegativeSpacing } from './composables/useNegativeSpacing'; import type { SpriteFile } from './types/sprites'; const settingsStore = useSettingsStore(); - const { sprites, columns, updateSpritePosition, updateSpriteCell, removeSprite, replaceSprite, addSprite, addSpriteWithResize, processImageFiles, alignSprites } = useSprites(); + const { + layers, + visibleLayers, + activeLayer, + activeLayerId, + columns, + updateSpritePosition, + updateSpriteCell, + removeSprite, + replaceSprite, + addSprite, + addSpriteWithResize, + processImageFiles, + alignSprites, + addLayer, + removeLayer, + moveLayer, + } = useLayers(); - const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExport(sprites, columns, toRef(settingsStore, 'negativeSpacingEnabled')); + const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(layers, columns, toRef(settingsStore, 'negativeSpacingEnabled')); const cellSize = computed(() => { - if (!sprites.value.length) return { width: 0, height: 0 }; - const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); - const negativeSpacing = calculateNegativeSpacing(sprites.value, settingsStore.negativeSpacingEnabled); - return { - width: maxWidth + negativeSpacing, - height: maxHeight + negativeSpacing, - }; + if (!layers.value.length) return { width: 0, height: 0 }; + const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value); + const allSprites = visibleLayers.value.flatMap(l => l.sprites); + const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled); + return { width: maxWidth + negativeSpacing, height: maxHeight + negativeSpacing }; }); const isPreviewModalOpen = ref(false); const isHelpModalOpen = ref(false); @@ -216,7 +247,7 @@ }; const openPreviewModal = () => { - if (sprites.value.length === 0) { + if (!visibleLayers.value.some(l => l.sprites.length > 0)) { alert('Please upload or import sprites to preview an animation.'); return; } @@ -253,7 +284,7 @@ }; const openGifFpsModal = () => { - if (sprites.value.length === 0) { + if (!visibleLayers.value.some(l => l.sprites.length > 0)) { alert('Please upload or import sprites before generating a GIF.'); return; } diff --git a/src/components/SpriteCanvas.vue b/src/components/SpriteCanvas.vue index e991832..776cd8a 100644 --- a/src/components/SpriteCanvas.vue +++ b/src/components/SpriteCanvas.vue @@ -100,7 +100,7 @@ diff --git a/src/composables/useExportLayers.ts b/src/composables/useExportLayers.ts new file mode 100644 index 0000000..beae08b --- /dev/null +++ b/src/composables/useExportLayers.ts @@ -0,0 +1,246 @@ +import type { Ref } from 'vue'; +import GIF from 'gif.js'; +import gifWorkerUrl from 'gif.js/dist/gif.worker.js?url'; +import JSZip from 'jszip'; +import type { Layer, Sprite } from '@/types/sprites'; +import { getMaxDimensionsAcrossLayers } from './useLayers'; +import { calculateNegativeSpacing } from './useNegativeSpacing'; + +export const useExportLayers = (layersRef: Ref, columns: Ref, negativeSpacingEnabled: Ref) => { + const getVisibleLayers = () => layersRef.value.filter(l => l.visible); + const getAllVisibleSprites = () => getVisibleLayers().flatMap(l => l.sprites); + + const drawCompositeCell = (ctx: CanvasRenderingContext2D, cellIndex: number, cellWidth: number, cellHeight: number, negativeSpacing: number) => { + ctx.clearRect(0, 0, cellWidth, cellHeight); + ctx.fillStyle = '#f9fafb'; + ctx.fillRect(0, 0, cellWidth, cellHeight); + const vLayers = getVisibleLayers(); + vLayers.forEach(layer => { + const sprite = layer.sprites[cellIndex]; + if (!sprite) return; + ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y)); + }); + }; + + const downloadSpritesheet = () => { + const visibleLayers = getVisibleLayers(); + if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) { + alert('Please upload or import sprites before downloading the spritesheet.'); + return; + } + + const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); + const negativeSpacing = calculateNegativeSpacing(getAllVisibleSprites(), negativeSpacingEnabled.value); + const cellWidth = maxWidth + negativeSpacing; + const cellHeight = maxHeight + negativeSpacing; + const maxLen = Math.max(...visibleLayers.map(l => l.sprites.length)); + const rows = Math.ceil(maxLen / columns.value); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return; + canvas.width = cellWidth * columns.value; + canvas.height = cellHeight * rows; + ctx.imageSmoothingEnabled = false; + + for (let index = 0; index < maxLen; index++) { + const col = index % columns.value; + const row = Math.floor(index / columns.value); + const cellX = Math.floor(col * cellWidth); + const cellY = Math.floor(row * cellHeight); + + const cellCanvas = document.createElement('canvas'); + const cellCtx = cellCanvas.getContext('2d'); + if (!cellCtx) return; + cellCanvas.width = cellWidth; + cellCanvas.height = cellHeight; + drawCompositeCell(cellCtx, index, cellWidth, cellHeight, negativeSpacing); + ctx.drawImage(cellCanvas, cellX, cellY); + } + + const link = document.createElement('a'); + link.download = 'spritesheet.png'; + link.href = canvas.toDataURL('image/png', 1.0); + link.click(); + }; + + const exportSpritesheetJSON = async () => { + const visibleLayers = getVisibleLayers(); + if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) { + alert('Nothing to export. Please add sprites first.'); + return; + } + + const layersData = await Promise.all( + layersRef.value.map(async layer => { + const sprites = await Promise.all( + layer.sprites.map(async sprite => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + canvas.width = sprite.width; + canvas.height = sprite.height; + ctx.drawImage(sprite.img, 0, 0); + const base64 = canvas.toDataURL('image/png'); + return { id: sprite.id, width: sprite.width, height: sprite.height, x: sprite.x, y: sprite.y, base64, name: sprite.file.name }; + }) + ); + return { id: layer.id, name: layer.name, visible: layer.visible, locked: layer.locked, sprites: sprites.filter(Boolean) }; + }) + ); + + const json = { version: 2, columns: columns.value, negativeSpacingEnabled: negativeSpacingEnabled.value, layers: layersData }; + const jsonString = JSON.stringify(json, null, 2); + const blob = new Blob([jsonString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'spritesheet.json'; + a.click(); + URL.revokeObjectURL(url); + }; + + const importSpritesheetJSON = async (jsonFile: File) => { + const text = await jsonFile.text(); + const data = JSON.parse(text); + + const loadSprite = (spriteData: any) => + new Promise(resolve => { + const img = new Image(); + img.onload = () => { + const byteString = atob(spriteData.base64.split(',')[1]); + const mimeType = spriteData.base64.split(',')[0].split(':')[1].split(';')[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) ia[i] = byteString.charCodeAt(i); + const blob = new Blob([ab], { type: mimeType }); + const fileName = spriteData.name || `sprite-${spriteData.id}.png`; + const file = new File([blob], fileName, { type: mimeType }); + resolve({ id: spriteData.id || crypto.randomUUID(), file, img, url: spriteData.base64, width: spriteData.width, height: spriteData.height, x: spriteData.x || 0, y: spriteData.y || 0 }); + }; + img.src = spriteData.base64; + }); + + if (typeof data.columns === 'number') columns.value = data.columns; + if (typeof data.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = data.negativeSpacingEnabled; + + if (Array.isArray(data.layers)) { + const newLayers: Layer[] = []; + for (const layerData of data.layers) { + const sprites: Sprite[] = await Promise.all(layerData.sprites.map((s: any) => loadSprite(s))); + newLayers.push({ id: layerData.id || crypto.randomUUID(), name: layerData.name || 'Layer', visible: layerData.visible !== false, locked: !!layerData.locked, sprites }); + } + layersRef.value = newLayers; + return; + } + + if (Array.isArray(data.sprites)) { + const sprites: Sprite[] = await Promise.all(data.sprites.map((s: any) => loadSprite(s))); + layersRef.value = [ + { id: crypto.randomUUID(), name: 'Base', visible: true, locked: false, sprites }, + { id: crypto.randomUUID(), name: 'Clothes', visible: true, locked: false, sprites: [] }, + ]; + return; + } + + throw new Error('Invalid JSON format'); + }; + + const downloadAsGif = (fps: number) => { + const visibleLayers = getVisibleLayers(); + if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) { + alert('Please upload or import sprites before generating a GIF.'); + return; + } + + const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); + const negativeSpacing = calculateNegativeSpacing(getAllVisibleSprites(), negativeSpacingEnabled.value); + const cellWidth = maxWidth + negativeSpacing; + const cellHeight = maxHeight + negativeSpacing; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return; + canvas.width = cellWidth; + canvas.height = cellHeight; + ctx.imageSmoothingEnabled = false; + + const gif = new GIF({ workers: 2, quality: 10, width: cellWidth, height: cellHeight, workerScript: gifWorkerUrl }); + const maxLen = Math.max(...visibleLayers.map(l => l.sprites.length)); + for (let i = 0; i < maxLen; i++) { + drawCompositeCell(ctx, i, cellWidth, cellHeight, negativeSpacing); + gif.addFrame(ctx, { copy: true, delay: 1000 / fps }); + } + + gif.on('finished', (blob: Blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'animation.gif'; + a.click(); + URL.revokeObjectURL(url); + }); + gif.render(); + }; + + const downloadAsZip = async () => { + const visibleLayers = getVisibleLayers(); + if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) { + alert('Please upload or import sprites before downloading a ZIP.'); + return; + } + const zip = new JSZip(); + + const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); + const negativeSpacing = calculateNegativeSpacing(getAllVisibleSprites(), negativeSpacingEnabled.value); + const cellWidth = maxWidth + negativeSpacing; + const cellHeight = maxHeight + negativeSpacing; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return; + canvas.width = cellWidth; + canvas.height = cellHeight; + ctx.imageSmoothingEnabled = false; + + const maxLen = Math.max(...visibleLayers.map(l => l.sprites.length)); + for (let i = 0; i < maxLen; i++) { + drawCompositeCell(ctx, i, cellWidth, cellHeight, negativeSpacing); + const dataURL = canvas.toDataURL('image/png'); + const binary = atob(dataURL.split(',')[1]); + const buf = new ArrayBuffer(binary.length); + const view = new Uint8Array(buf); + for (let j = 0; j < binary.length; j++) view[j] = binary.charCodeAt(j); + zip.file(`frames/frame_${String(i + 1).padStart(3, '0')}.png`, view); + } + + const jsonFolder = zip.folder('export')!; + const jsonBlobPromise = (async () => { + const layersPayload = await Promise.all( + layersRef.value.map(async layer => ({ + id: layer.id, + name: layer.name, + visible: layer.visible, + locked: layer.locked, + sprites: await Promise.all( + layer.sprites.map(async s => ({ id: s.id, width: s.width, height: s.height, x: s.x, y: s.y, name: s.file.name })) + ), + })) + ); + const meta = { version: 2, columns: columns.value, negativeSpacingEnabled: negativeSpacingEnabled.value, layers: layersPayload }; + const metaStr = JSON.stringify(meta, null, 2); + jsonFolder.file('spritesheet.meta.json', metaStr); + })(); + await jsonBlobPromise; + + const content = await zip.generateAsync({ type: 'blob' }); + const url = URL.createObjectURL(content); + const a = document.createElement('a'); + a.href = url; + a.download = 'sprites.zip'; + a.click(); + URL.revokeObjectURL(url); + }; + + return { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip }; +}; diff --git a/src/composables/useFileDrop.ts b/src/composables/useFileDrop.ts index 92304d0..c05a7f8 100644 --- a/src/composables/useFileDrop.ts +++ b/src/composables/useFileDrop.ts @@ -1,9 +1,9 @@ -import { ref, type Ref } from 'vue'; +import { ref, type Ref, type ComputedRef } from 'vue'; import type { Sprite } from '@/types/sprites'; import { getMaxDimensions } from './useSprites'; export interface FileDropOptions { - sprites: Ref | Sprite[]; + sprites: Ref | ComputedRef | Sprite[]; onAddSprite: (file: File) => void; onAddSpriteWithResize: (file: File) => void; } diff --git a/src/composables/useLayers.ts b/src/composables/useLayers.ts new file mode 100644 index 0000000..466a0fa --- /dev/null +++ b/src/composables/useLayers.ts @@ -0,0 +1,205 @@ +import { computed, ref, watch } from 'vue'; +import type { Layer, Sprite } from '@/types/sprites'; +import { getMaxDimensions as getMaxDimensionsSingle, useSprites as useSpritesSingle } from './useSprites'; + +export const createEmptyLayer = (name: string): Layer => ({ + id: crypto.randomUUID(), + name, + sprites: [], + visible: true, + locked: false, +}); + +export const useLayers = () => { + const layers = ref([createEmptyLayer('Base')]); + const activeLayerId = ref(layers.value[0].id); + const columns = ref(4); + + watch(columns, val => { + const num = typeof val === 'number' ? val : parseInt(String(val)); + const safe = Number.isFinite(num) && num >= 1 ? Math.min(num, 10) : 1; + if (safe !== columns.value) columns.value = safe; + }); + + const activeLayer = computed(() => layers.value.find(l => l.id === activeLayerId.value) || layers.value[0]); + + const getMaxDimensions = (sprites: Sprite[]) => getMaxDimensionsSingle(sprites); + + const updateSpritePosition = (id: string, x: number, y: number) => { + const l = activeLayer.value; + if (!l) return; + const i = l.sprites.findIndex(s => s.id === id); + if (i !== -1) { + l.sprites[i].x = Math.floor(x); + l.sprites[i].y = Math.floor(y); + } + }; + + const alignSprites = (position: 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom') => { + const l = activeLayer.value; + if (!l || !l.sprites.length) return; + const { maxWidth, maxHeight } = getMaxDimensions(l.sprites); + l.sprites = l.sprites.map(sprite => { + let x = sprite.x; + let y = sprite.y; + switch (position) { + case 'left': + x = 0; + break; + case 'center': + x = Math.floor((maxWidth - sprite.width) / 2); + break; + case 'right': + x = Math.floor(maxWidth - sprite.width); + break; + case 'top': + y = 0; + break; + case 'middle': + y = Math.floor((maxHeight - sprite.height) / 2); + break; + case 'bottom': + y = Math.floor(maxHeight - sprite.height); + break; + } + return { ...sprite, x: Math.floor(x), y: Math.floor(y) }; + }); + }; + + const updateSpriteCell = (id: string, newIndex: number) => { + const l = activeLayer.value; + if (!l) return; + const currentIndex = l.sprites.findIndex(s => s.id === id); + if (currentIndex === -1 || currentIndex === newIndex) return; + const next = [...l.sprites]; + if (newIndex < next.length) { + const moving = { ...next[currentIndex] }; + const target = { ...next[newIndex] }; + next[currentIndex] = target; + next[newIndex] = moving; + } else { + const [moved] = next.splice(currentIndex, 1); + next.splice(newIndex, 0, moved); + } + l.sprites = next; + }; + + const removeSprite = (id: string) => { + const l = activeLayer.value; + if (!l) return; + const i = l.sprites.findIndex(s => s.id === id); + if (i === -1) return; + const s = l.sprites[i]; + if (s.url && s.url.startsWith('blob:')) { + try { + URL.revokeObjectURL(s.url); + } catch {} + } + l.sprites.splice(i, 1); + }; + + const replaceSprite = (id: string, file: File) => { + const l = activeLayer.value; + if (!l) return; + const i = l.sprites.findIndex(s => s.id === id); + if (i === -1) return; + const old = l.sprites[i]; + if (old.url && old.url.startsWith('blob:')) { + try { + URL.revokeObjectURL(old.url); + } catch {} + } + + const url = URL.createObjectURL(file); + const img = new Image(); + img.onload = () => { + l.sprites[i] = { id: old.id, file, img, url, width: img.width, height: img.height, x: old.x, y: old.y }; + }; + img.onerror = () => { + URL.revokeObjectURL(url); + }; + img.src = url; + }; + + const addSprite = (file: File) => addSpriteWithResize(file); + + const addSpriteWithResize = (file: File) => { + const l = activeLayer.value; + if (!l) return; + const url = URL.createObjectURL(file); + const img = new Image(); + img.onload = () => { + const next: Sprite = { + id: crypto.randomUUID(), + file, + img, + url, + width: img.width, + height: img.height, + x: 0, + y: 0, + }; + l.sprites = [...l.sprites, next]; + }; + img.onerror = () => URL.revokeObjectURL(url); + img.src = url; + }; + + const processImageFiles = async (files: File[]) => { + for (const f of files) addSpriteWithResize(f); + }; + + const addLayer = (name?: string) => { + const l = createEmptyLayer(name || `Layer ${layers.value.length + 1}`); + layers.value.push(l); + activeLayerId.value = l.id; + }; + + const removeLayer = (id: string) => { + if (layers.value.length === 1) return; + const idx = layers.value.findIndex(l => l.id === id); + if (idx === -1) return; + layers.value.splice(idx, 1); + if (activeLayerId.value === id) activeLayerId.value = layers.value[0].id; + }; + + const moveLayer = (id: string, direction: 'up' | 'down') => { + const idx = layers.value.findIndex(l => l.id === id); + if (idx === -1) return; + if (direction === 'up' && idx > 0) { + const [l] = layers.value.splice(idx, 1); + layers.value.splice(idx - 1, 0, l); + } + if (direction === 'down' && idx < layers.value.length - 1) { + const [l] = layers.value.splice(idx, 1); + layers.value.splice(idx + 1, 0, l); + } + }; + + const visibleLayers = computed(() => layers.value.filter(l => l.visible)); + + return { + layers, + visibleLayers, + activeLayerId, + activeLayer, + columns, + getMaxDimensions, + updateSpritePosition, + updateSpriteCell, + removeSprite, + replaceSprite, + addSprite, + addSpriteWithResize, + processImageFiles, + alignSprites, + addLayer, + removeLayer, + moveLayer, + }; +}; + +export const getMaxDimensionsAcrossLayers = (layers: Layer[]) => { + const sprites = layers.flatMap(l => l.visible ? l.sprites : []); + return getMaxDimensionsSingle(sprites); +}; \ No newline at end of file diff --git a/src/types/sprites.ts b/src/types/sprites.ts index e65597a..5833a86 100644 --- a/src/types/sprites.ts +++ b/src/types/sprites.ts @@ -16,3 +16,11 @@ export interface SpriteFile { width: number; height: number; } + +export interface Layer { + id: string; + name: string; + sprites: Sprite[]; + visible: boolean; + locked: boolean; +}