diff --git a/package-lock.json b/package-lock.json index 7f258af..da0a034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "@ffmpeg-installer/ffmpeg": "^1.1.0", "@ffprobe-installer/ffprobe": "^2.1.2", "@types/opentype.js": "^1.3.4", + "ag-psd": "^30.1.0", + "ag-psd-psdtool": "^1.1.10", "mathjax-full": "^3.2.1", "opentype.js": "^1.3.4", "prismjs": "^1.30.0", @@ -1410,308 +1412,325 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openharmony" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "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==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -2135,13 +2154,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2278,6 +2296,46 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/ag-psd": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/ag-psd/-/ag-psd-30.1.0.tgz", + "integrity": "sha512-1ce6o84aC+oVyl83A35HHUniGjwA3piHmGem3J2odBOFRq5p7i4htaco94vePLfOcingZu4fsyospJLwPagkhg==", + "dependencies": { + "base64-js": "1.5.1", + "pako": "2.1.0" + } + }, + "node_modules/ag-psd-psdtool": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/ag-psd-psdtool/-/ag-psd-psdtool-1.1.10.tgz", + "integrity": "sha512-UgyFPtgToJImsCHl9szHMxqYR0VfPZm3b7TIrbrIk7GrNLaAARqjQvTptGVRMsm1WgTDUTJZwd3kOk83wEWnfg==", + "hasInstallScript": true, + "dependencies": { + "ag-psd": "^30.1.0", + "ajv": "^8.18.0", + "es-toolkit": "^1.45.1" + } + }, + "node_modules/ag-psd-psdtool/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ag-psd-psdtool/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -2288,11 +2346,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2477,6 +2534,25 @@ "bare-path": "^3.0.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/baseline-browser-mapping": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.2.tgz", @@ -2488,10 +2564,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "license": "MIT", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", "engines": { "node": ">=10.0.0" } @@ -3147,6 +3222,11 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==" + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -3477,7 +3557,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -3500,6 +3579,21 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -4364,11 +4458,10 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4587,6 +4680,11 @@ "node": ">= 14" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4897,6 +4995,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -4946,11 +5052,10 @@ } }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, @@ -4962,28 +5067,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@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", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 0d33137..f740a47 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "@ffmpeg-installer/ffmpeg": "^1.1.0", "@ffprobe-installer/ffprobe": "^2.1.2", "@types/opentype.js": "^1.3.4", + "ag-psd": "^30.1.0", + "ag-psd-psdtool": "^1.1.10", "mathjax-full": "^3.2.1", "opentype.js": "^1.3.4", "prismjs": "^1.30.0", diff --git a/src/lib/character/ast.ts b/src/lib/character/ast.ts new file mode 100644 index 0000000..10f2b6e --- /dev/null +++ b/src/lib/character/ast.ts @@ -0,0 +1,92 @@ +import type { Variable } from "../animation" +import type { Trim } from "../trim" + +export const PsdCharacterElement = { + Character: "Character", + MotionSequence: "MotionSequence", + DeclareVariable: "DeclareVariable", + Block: "Block", + DeclareAnimation: "DeclareAnimation", + Voice: "Voice", + Motion: "Motion", +} as const + + +export type CharacterChild = + | MotionSequenceNode + | DeclareVariableNode + | VoiceNode + | MotionNode + +export type MotionSequenceChild = + | BlockNode + | DeclareVariableNode + | VoiceNode + | MotionNode + +export type DeclareVariableChild = + | DeclareVariableNode + | DeclareAnimationNode + +export type BlockChild = + | MotionSequenceNode + | DeclareVariableNode + | VoiceNode + | MotionNode + +export type DeclareAnimationChild = + | MotionSequenceNode + | DeclareVariableNode + | VoiceNode + | MotionNode + +export type VoiceChild = + | MotionNode + + + + +export interface CharacterNode { + type: typeof PsdCharacterElement.Character + children: CharacterChild[] +} + +export interface MotionSequenceNode { + type: typeof PsdCharacterElement.MotionSequence + children: MotionSequenceChild[] +} + +export interface DeclareVariableNode { + type: typeof PsdCharacterElement.DeclareVariable + variableName: string + initValue: any + children: DeclareVariableChild +} + +export interface BlockNode { + type: typeof PsdCharacterElement.Block + children: BlockChild[] +} + +export interface DeclareAnimationNode { + type: typeof PsdCharacterElement.DeclareAnimation + f: (ctx: any, variable: Record>) => Promise + children: DeclareAnimationChild[] +} + +export interface VoiceNode { + type: typeof PsdCharacterElement.Voice + voice: string + trim?: Trim + fadeInFrames?: number + fadeOutFrames?: number + volume: undefined | number | ((variables: Record>, frames: number[]) => number) + showWaveform?: boolean + children: VoiceChild[] +} + +export interface MotionNode { + type: typeof PsdCharacterElement.Motion + motion: (variables: Record>, frames: number[]) => Record +} + diff --git a/src/lib/character/defineDSL.ts b/src/lib/character/defineDSL.ts new file mode 100644 index 0000000..9f51c67 --- /dev/null +++ b/src/lib/character/defineDSL.ts @@ -0,0 +1,12 @@ +import type { ReactElement } from "react" + +export type DslComponent

= { + (props: P): ReactElement | null + __dslType: string +} + +export function defineDSL

(type: string): DslComponent

{ + const C = ((_: P) => null) as DslComponent

+ C.__dslType = type + return C +} diff --git a/src/lib/character/index.ts b/src/lib/character/index.ts new file mode 100644 index 0000000..568f16d --- /dev/null +++ b/src/lib/character/index.ts @@ -0,0 +1,3 @@ +export * from "./psd-character" +export * from "./psd-character-component" +export * from "./util-motions" diff --git a/src/lib/character/parser.tsx b/src/lib/character/parser.tsx new file mode 100644 index 0000000..b755e6a --- /dev/null +++ b/src/lib/character/parser.tsx @@ -0,0 +1,324 @@ +import React, { isValidElement, type ReactElement, type ReactNode } from "react" +import type { BlockChild, BlockNode, CharacterChild, CharacterNode, DeclareAnimationChild, DeclareAnimationNode, DeclareVariableChild, DeclareVariableNode, MotionNode, MotionSequenceChild, MotionSequenceNode, VoiceChild, VoiceNode } from "./ast" +import { PsdCharacterElement as PsdElm } from "./ast" + +type AnyElement = ReactElement + + +export function parsePsdCharacter( + children: ReactNode, +): CharacterNode { + const body = parsePsdCharacterChildren(children) + return { + type: PsdElm.Character, + children: body, + } +} + +function parsePsdCharacterChildren( + children: ReactNode, +): CharacterChild[] { + const result: CharacterChild[] = [] + + React.Children.forEach(children, (child) => { + if (!isValidElement(child)) return + + const type = getDslType(child) + + switch (type) { + case PsdElm.MotionSequence: + result.push(parseMotionSequence(child)) + break + + case PsdElm.DeclareVariable: + result.push(parseDeclareVariable(child)) + break + + case PsdElm.Voice: + result.push(parseVoice(child)) + break + case PsdElm.Motion: + result.push(parseMotion(child)) + break + case "function": + const expanded = child.type(child.props) + const expandedAst = parsePsdCharacterChildren(expanded) + result.push(...expandedAst) + break + + default: + throw new Error(`Invalid DSL type in root: ${type}`) + } + }) + + return result +} + +function parseMotionSequence( + self: AnyElement, +): MotionSequenceNode { + const { children } = self.props + const body = parseMotionSequenceChildren(children) + return { + type: PsdElm.MotionSequence, + children: body, + } +} + +function parseMotionSequenceChildren( + children: ReactNode, +): MotionSequenceChild[] { + const result: MotionSequenceChild[] = [] + + React.Children.forEach(children, (child) => { + if (!isValidElement(child)) return + + const type = getDslType(child) + + switch (type) { + case PsdElm.Block: + result.push(parseBlock(child)) + break + + case PsdElm.DeclareVariable: + result.push(parseDeclareVariable(child)) + break + + case PsdElm.Voice: + result.push(parseVoice(child)) + break + case PsdElm.Motion: + result.push(parseMotion(child)) + break + case "function": + const expanded = child.type(child.props) + const expandedAst = parseMotionSequenceChildren(expanded) + result.push(...expandedAst) + break + + default: + throw new Error(`Invalid DSL type in ${PsdElm.MotionSequence}: ${type}`) + } + }) + + return result +} + +function parseDeclareVariable( + self: AnyElement, +): DeclareVariableNode { + const { variableName, initValue, children } = self.props + const body = parseDeclareVariableChild(children) + + return { + type: PsdElm.DeclareVariable, + variableName, + initValue, + children: body, + } +} + + +function parseDeclareVariableChild( + children: ReactNode, +): DeclareVariableChild { + + const single = React.Children.toArray(children) + if (single.length == 1) { + const child = single[0] + + if (!isValidElement(child)) { + throw new Error(`Invalid Element in ${PsdElm.DeclareVariable}`) + } + + const type = getDslType(child) + + + switch (type) { + case PsdElm.DeclareVariable: + return parseDeclareVariable(child) + + case PsdElm.DeclareAnimation: + return parseDeclareAnimation(child) + case "function": + const expanded = child.type(child.props) + const expandedAst = parseDeclareVariable(expanded) + return expandedAst + + default: + throw new Error(`Invalid DSL type in ${PsdElm.DeclareVariable}: ${type}`) + } + } else { + throw new Error(`${PsdElm.DeclareVariable} take just one element`) + } +} + +function parseBlock( + self: AnyElement, +): BlockNode { + const { children } = self.props + const body = parseBlockChildren(children) + return { + type: PsdElm.Block, + children: body, + } +} + +function parseBlockChildren( + children: ReactNode, +): BlockChild[] { + const result: BlockChild[] = [] + + React.Children.forEach(children, (child) => { + if (!isValidElement(child)) return + + const type = getDslType(child) + + switch (type) { + case PsdElm.MotionSequence: + result.push(parseMotionSequence(child)) + break + + case PsdElm.DeclareVariable: + result.push(parseDeclareVariable(child)) + break + + case PsdElm.Voice: + result.push(parseVoice(child)) + break + case PsdElm.Motion: + result.push(parseMotion(child)) + break + case "function": + const expanded = child.type(child.props) + const expandedAst = parseBlockChildren(expanded) + result.push(...expandedAst) + break + + default: + throw new Error(`Invalid DSL type in ${PsdElm.Block}: ${type}`) + } + }) + + return result +} + +function parseDeclareAnimation( + self: AnyElement, +): DeclareAnimationNode { + const { f, children } = self.props + const body = parseDeclareAnimationChildren(children) + return { + type: PsdElm.DeclareAnimation, + f, + children: body, + } +} + +function parseDeclareAnimationChildren( + children: ReactNode, +): DeclareAnimationChild[] { + const result: DeclareAnimationChild[] = [] + + React.Children.forEach(children, (child) => { + if (!isValidElement(child)) return + + const type = getDslType(child) + + switch (type) { + case PsdElm.MotionSequence: + result.push(parseMotionSequence(child)) + break + + case PsdElm.DeclareVariable: + result.push(parseDeclareVariable(child)) + break + + case PsdElm.Voice: + result.push(parseVoice(child)) + break + case PsdElm.Motion: + result.push(parseMotion(child)) + break + case "function": + const expanded = child.type(child.props) + const expandedAst = parseDeclareAnimationChildren(expanded) + result.push(...expandedAst) + break + + default: + throw new Error(`Invalid DSL type in ${PsdElm.DeclareAnimation}: ${type}`) + } + }) + + return result +} + +function parseVoice( + self: AnyElement, +): VoiceNode { + const { voice, trim, fadeInFrames, fadeOutFrames, volume, showWaveform, children } = self.props + const body = parseVoiceChildren(children) + return { + type: PsdElm.Voice, + voice, + trim, + fadeInFrames, + fadeOutFrames, + volume: volume ?? undefined, + showWaveform, + children: body, + } +} + +function parseVoiceChildren( + children: ReactNode, +): VoiceChild[] { + const result: VoiceChild[] = [] + + React.Children.forEach(children, (child) => { + if (!isValidElement(child)) return + + const type = getDslType(child) + + switch (type) { + case PsdElm.Motion: + result.push(parseMotion(child)) + break + case "function": + const expanded = child.type(child.props) + const expandedAst = parseVoiceChildren(expanded) + result.push(...expandedAst) + break + + default: + throw new Error(`Invalid DSL type in ${PsdElm.Voice}: ${type}`) + } + }) + + return result +} + +function parseMotion( + self: AnyElement, +): MotionNode { + const { motion } = self.props + return { + type: PsdElm.Motion, + motion + } +} + + +function getDslType(el: AnyElement): string | undefined { + const type = el.type as any + + if (type?.__dslType) { + return type.__dslType + } + if (typeof type === "function") { + return "function" + } + + return undefined +} diff --git a/src/lib/character/psd-character-component.ts b/src/lib/character/psd-character-component.ts new file mode 100644 index 0000000..3d4dc82 --- /dev/null +++ b/src/lib/character/psd-character-component.ts @@ -0,0 +1,45 @@ +import type { Variable } from "../animation" +import type { Trim } from "../trim" +import { defineDSL } from "./defineDSL" +import { PsdCharacterElement } from "./ast" + +// 要素を直列化する +export const MotionSequence = defineDSL<{ + children: React.ReactNode +}>(PsdCharacterElement.MotionSequence) + +// Variableを宣言する +export const DeclareVariable = defineDSL<{ + variableName: string + initValue: any + children: React.ReactNode +}>(PsdCharacterElement.DeclareVariable) + +// MotionSequence直下で使用し、Block内を並列化する +export const Block = defineDSL<{ + children: React.ReactNode +}>(PsdCharacterElement.Block) + +// 宣言されたVariableをアニメーションとして登録する +export const DeclareAnimation = defineDSL<{ + f: (ctx: any, variable: Record>) => Promise + children: React.ReactNode +}>(PsdCharacterElement.DeclareAnimation) + +// 音声を配置する(ファイルのみ) +export const Voice = defineDSL<{ + voice: string + trim?: Trim + fadeInFrames?: number + fadeOutFrames?: number + volume?: number + showWaveform?: boolean + children?: React.ReactNode +}>(PsdCharacterElement.Voice) + +// psdファイルのオプションを制御し、動きをつける +// frames[0]: useCurrentFrame +// frames[frames.length - 1]: useGlobalCurrentFrame +export const Motion = defineDSL<{ + motion: (variables: Record>, frames: number[]) => Record +}>(PsdCharacterElement.Motion) diff --git a/src/lib/character/psd-character.tsx b/src/lib/character/psd-character.tsx new file mode 100644 index 0000000..1ece8ab --- /dev/null +++ b/src/lib/character/psd-character.tsx @@ -0,0 +1,574 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" + +import { PsdCharacterElement as PsdElm, type BlockNode, type CharacterNode, type DeclareAnimationNode, type DeclareVariableNode, type MotionNode, type MotionSequenceNode, type VoiceNode } from "./ast" +import { readPsd, type Psd } from "ag-psd" +import { parsePsdCharacter } from "./parser" +import { renderPsd } from "ag-psd-psdtool" +import { useAnimation, useVariable, type Variable } from "../animation" +import { useCurrentFrame, useGlobalCurrentFrame } from "../frame" +import { Sound } from "../sound/sound" +import { Clip, ClipSequence } from "../clip" + +type PsdCharacterProps = { + psd: string + className?: string + children: React.ReactNode +} + +type PsdPath = { + path: string +} + +type PsdOptions = Record +type OptionRegister = () => { + update: (opt: Record) => void + getter: () => Record + unregister: () => void +} + + +export const PsdCharacter = ({ + psd, + className, + children +}: PsdCharacterProps) => { + const [myPsd, setPsd] = useState(undefined) + const [ast, setAst] = useState(undefined) + + // オプションをレイヤーごとに管理する + const registry = useRef(new Map()) + const order = useRef([]) + + const options = useRef({}) + + const canvas = useRef(null) + + useEffect(() => { + fetchPsd(normalizePsdPath(psd)).then(p => setPsd(p)) + setAst(parsePsdCharacter(children)) + }, [psd]) + + // 毎フレーム実行 + const frame = useCurrentFrame() + useEffect(() => { + if (typeof myPsd !== "undefined" && canvas.current) { + renderPsd(myPsd, options.current, { canvas: canvas.current }) + } + }, [frame]) + + // registryをmergeしてoptionsを変更 + const recompute = useCallback(() => { + const merged = Object.assign({}, ...registry.current.values()) + options.current = merged + }, []) + + // registerを分配し、registryに記録 + const register = useCallback(() => { + const id = crypto.randomUUID() + + registry.current.set(id, {}) + order.current.push(id) + + const update = (opt: PsdOptions) => { + registry.current.set(id, opt) + recompute() + } + + const unregister = () => { + registry.current.delete(id) + order.current = order.current.filter(x => x !== id) + recompute() + } + + const getter = () => { + const index = order.current.indexOf(id) + + const prevIds = order.current.slice(0, index) + + const prevOptions = prevIds.map(i => registry.current.get(i) ?? {}) + + return Object.assign({}, ...prevOptions) + } + + return { + update, + getter, + unregister, + } + }, []) + + + return ( + <> + + {ast?.children.map((child, i) => { + switch (child.type) { + case PsdElm.MotionSequence: + return + case PsdElm.DeclareVariable: + return + case PsdElm.Voice: + return + case PsdElm.Motion: + return + default: + return null + } + })} + + ) +} + +type MotionSequenceRuntimeProps = { + ast: MotionSequenceNode + variables: Record> + register: OptionRegister +} + +const MotionSequenceRuntime = ({ + ast, + variables, + register +}: MotionSequenceRuntimeProps) => { + const reg = useRef>(undefined) + if (!reg.current) { + reg.current = register() + } + const {update, getter, unregister} = reg.current + + useEffect(() => { + return () => unregister() + }, []) + + // 直列のため同じregisterを使う + const curRegister: OptionRegister = useCallback(() => { + return {update, getter, unregister: () => {}} + }, []) + + return ( + + {ast.children.map(child => { + switch (child.type) { + case PsdElm.DeclareVariable: + return + case PsdElm.Block: + return + case PsdElm.Voice: + return + case PsdElm.Motion: + return + default: + return null + } + }).map((child, i) => {child} )} + + ) +} + +type DeclareVariableRuntimeProps = { + ast: DeclareVariableNode + variables: Record> + initializingVariables: Record> + register: OptionRegister +} + +const DeclareVariableRuntime = ({ + ast, + variables, + initializingVariables, + register +}: DeclareVariableRuntimeProps) => { + const variable = useVariable(ast.initValue) + const newInitVariables = {[ast.variableName]: variable, ...initializingVariables} + + switch (ast.children.type) { + case PsdElm.DeclareVariable: + return + case PsdElm.DeclareAnimation: + return + default: + return null + } +} + +type BlockRuntimeProps = { + ast: BlockNode + variables: Record> + register: OptionRegister +} + +const BlockRuntime = ({ + ast, + variables, + register +}: BlockRuntimeProps) => { + const reg = useRef>(undefined) + if (!reg.current) { + reg.current = register() + } + const {update, getter: superGetter, unregister} = reg.current + + useEffect(() => { + return () => unregister() + }, []) + + const curRegistry = useRef(new Map()) + const order = useRef([]) + + const options = useRef({}) + + const recompute = useCallback(() => { + const merged = Object.assign({}, ...curRegistry.current.values()) + options.current = merged + }, []) + + const curRegister = useCallback(() => { + const id = crypto.randomUUID() + + curRegistry.current.set(id, {}) + order.current.push(id) + + const update = (opt: PsdOptions) => { + curRegistry.current.set(id, opt) + recompute() + } + + const unregister = () => { + curRegistry.current.delete(id) + order.current = order.current.filter(x => x !== id) + recompute() + } + + const getter = () => { + const index = order.current.indexOf(id) + + const prevIds = order.current.slice(0, index) + + const prevOptions = prevIds.map(i => curRegistry.current.get(i) ?? {}) + + return Object.assign(superGetter(), ...prevOptions) + } + + return { + update, + getter, + unregister, + } + }, []) + + const frame = useCurrentFrame() + useEffect(() => { + update(options.current) + }, [frame]) + + + return ( + <> + {ast.children.map((child, i) => { + switch (child.type) { + case PsdElm.MotionSequence: + return + case PsdElm.DeclareVariable: + return + case PsdElm.Voice: + return + case PsdElm.Motion: + return + default: + return null + } + })} + + ) +} + +type DeclareAnimationRuntimeProps = { + ast: DeclareAnimationNode + variables: Record> + initializingVariables: Record> + register: OptionRegister +} + +const DeclareAnimationRuntime = ({ + ast, + variables, + initializingVariables, + register +}: DeclareAnimationRuntimeProps) => { + + useAnimation(async (ctx) => { + ast.f(ctx, initializingVariables) + }, []) + + const curVariables = {...variables, ...initializingVariables} + + const reg = useRef>(undefined) + if (!reg.current) { + reg.current = register() + } + const {update, getter: superGetter, unregister} = reg.current + + useEffect(() => { + return () => unregister() + }, []) + + const curRegistry = useRef(new Map()) + const order = useRef([]) + + const options = useRef({}) + + const recompute = useCallback(() => { + const merged = Object.assign({}, ...curRegistry.current.values()) + options.current = merged + }, []) + + const curRegister = useCallback(() => { + const id = crypto.randomUUID() + + curRegistry.current.set(id, {}) + order.current.push(id) + + const update = (opt: PsdOptions) => { + curRegistry.current.set(id, opt) + recompute() + } + + const unregister = () => { + curRegistry.current.delete(id) + order.current = order.current.filter(x => x !== id) + recompute() + } + + const getter = () => { + const index = order.current.indexOf(id) + + const prevIds = order.current.slice(0, index) + + const prevOptions = prevIds.map(i => curRegistry.current.get(i) ?? {}) + + return Object.assign(superGetter(), ...prevOptions) + } + + return { + update, + getter, + unregister, + } + }, []) + + const frame = useCurrentFrame() + useEffect(() => { + update(options.current) + }, [frame]) + + return ( + <> + {ast.children.map((child, i) => { + switch (child.type) { + case PsdElm.MotionSequence: + return + case PsdElm.DeclareVariable: + return + case PsdElm.Voice: + return + case PsdElm.Motion: + return + default: + return null + } + })} + + ) +} + +type VoiceRuntimeProps = { + ast: VoiceNode, + variables: Record> + register: OptionRegister +} + +const VoiceRuntime = ({ + ast, + variables, + register +}: VoiceRuntimeProps) => { + const local_frame = useCurrentFrame() + const global_frame = useGlobalCurrentFrame() + const frames = [local_frame, global_frame] + + const volume = + typeof ast.volume === "function" + ? ast.volume(variables, frames) + : ast.volume + + return ( + + ) +} + +type MotionRuntimeProps = { + ast: MotionNode, + variables: Record> + register: OptionRegister +} + +const MotionRuntime = ({ + ast, + variables, + register +}: MotionRuntimeProps) => { + const reg = useRef>(undefined) + if (!reg.current) { + reg.current = register() + } + const { update, getter, unregister } = reg.current + + useEffect(() => { + return () => unregister() + }, []) + + const localTime = useCurrentFrame() + const globalTime = useGlobalCurrentFrame() + + useEffect(() => { + update(ast.motion(variables, [localTime, globalTime])) + }, [localTime]) + + return null +} + + +const psdCache = new Map() +const psdPending = new Map>() + +const fetchPsd = async (psd: PsdPath): Promise => { + const cached = psdCache.get(psd.path) + if (cached != null) return cached + + const pending = psdPending.get(psd.path) + if (pending) return pending + + const next = (async () => { + const res = await fetch(buildPsdUrl(psd)) + if (!res.ok) { + throw new Error("failed to fetch psd file") + } + + const file = readPsd(await res.arrayBuffer()) + psdCache.set(psd.path, file) + return file + })().finally(() => { + psdPending.delete(psd.path) + }) + + psdPending.set(psd.path, next) + return next +} + +const normalizePsdPath = (psd: PsdPath | string): PsdPath => { + if (typeof psd === "string") return { path: psd } + return psd +} + +const buildPsdUrl = (pad: PsdPath) => { + const url = new URL("http://localhost:3000/file") + url.searchParams.set("path", pad.path) + return url.toString() +} diff --git a/src/lib/character/util-motions.tsx b/src/lib/character/util-motions.tsx new file mode 100644 index 0000000..6e3ded8 --- /dev/null +++ b/src/lib/character/util-motions.tsx @@ -0,0 +1,190 @@ +import type { warn } from "console" +import { framesToSeconds } from "../audio" +import { useCurrentFrame } from "../frame" +import { Motion } from "./psd-character-component" + + +export type BasicPsdOptions = { + eye: EyeOptions + mouth: MouthOptions +} + +export type EyeOptions = { kind: "enum"; options: EyeEnum } | { kind: "bool"; options: EyeBool } +export type MouthOptions = { kind: "enum"; options: MouthEnum } | { kind: "bool"; options: MouthBool } + +type EyeShape = "Open" | "HalfOpen" | "HalfClosed" | "Closed" + +export type EyeEnum = { + // enum本体 + Eye: string + Default: string +} & Record + +export type EyeBool = { + Default: string +} & Record + +type MouthShape = "a" | "i" | "u" | "e" | "o" | "x" + +export type MouthEnum = { + Mouth: string + Default: string +} & Record + +export type MouthBool = { + Default: string +} & Record + +type HasKey = { + [P in K]: V +} & Record + + +// lip sync -------------------------------- + +export type LipSyncData = HasKey<"mouthCues", {start: number, end: number, value: string}[]> + +export type LipSyncProps = { + data: LipSyncData +} + +// psdに対応する辞書を用いてあいうえお口パクするコンポーネントを返す +export const createLipSync = (mouthOptions: MouthOptions) => { + return ({ data }: LipSyncProps) => { + return { + const t = framesToSeconds(frames[0]) + let shape: MouthShape | undefined = undefined + for (let section of data.mouthCues) { + if (section.start <= t && t < section.end) { + shape = lipSyncValueToMouthShape(section.value) + break + } + } + + if (!shape) { + return {} + } + + + if (mouthOptions.kind === "enum") { + return { + [mouthOptions.options.Mouth]: mouthOptions.options[shape] + } + } else if (mouthOptions.options[shape] == mouthOptions.options.Default) { + return { + [mouthOptions.options.Default]: true + } + } else { + const opt = { + [mouthOptions.options.Default]: false, + [mouthOptions.options[shape]]: true + } + + return opt + } + + }} /> + } +} + +const lipSyncValueToMouthShape = (value: string): MouthShape => { + switch (value) { + case "A": + return "a" + case "B": + return "i" + case "C": + return "e" + case "D": + return "a" + case "E": + return "o" + case "F": + return "u" + case "G": + return "i" + case "H": + return "u" + case "X": + return "x" + default: + return "x" + } +} + +// blink -------------------------------- + +export type BlinkData = HasKey<"blinkCues", {start: number, end: number, value: string}[]> + +export type BlinkProps = { + data: BlinkData +} + +// psdに対応する辞書を用いて目パチするコンポーネントを返す +export const createBlink = (eyeOptions: EyeOptions) => { + return ({ data }: BlinkProps) => { + return { + + const t = framesToSeconds(frames[1]) + const sections = data.blinkCues + + let lo = 0 + let hi = sections.length - 1 + let idx = -1 + + while (lo <= hi) { + const mid = (lo + hi) >> 1 + + if (sections[mid].start <= t) { + idx = mid + lo = mid + 1 + } else { + hi = mid - 1 + } + } + + let shape: EyeShape | undefined = undefined + if (idx !== -1 && t < sections[idx].end) { + shape = BlinkValueToEyeShape(sections[idx].value) + } + + if (!shape) { + return {} + } + + + if (eyeOptions.kind === "enum") { + return { + [eyeOptions.options.Eye]: eyeOptions.options[shape] + } + } else if (eyeOptions.options[shape] == eyeOptions.options.Default) { + return { + [eyeOptions.options.Default]: true + } + } else { + const opt = { + [eyeOptions.options.Default]: false, + [eyeOptions.options[shape]]: true + } + + return opt + } + + }} /> + } +} + +const BlinkValueToEyeShape = (value: string): EyeShape => { + switch (value) { + case "A": + return "Open" + case "B": + return "HalfOpen" + case "C": + return "HalfClosed" + case "D": + return "Closed" + default: + return "Open" + } +}