diff --git a/.github/workflows/Automated Test (full).yml b/.github/workflows/Automated Test (full).yml index cc81b62..3fc879a 100644 --- a/.github/workflows/Automated Test (full).yml +++ b/.github/workflows/Automated Test (full).yml @@ -53,7 +53,7 @@ jobs: - name: Setup node uses: actions/setup-node@v2 with: - node-version: '14' + node-version: '15' - name: Cache modules psychojs_testing uses: actions/cache@v2 env: diff --git a/.github/workflows/Automated Test (short).yml b/.github/workflows/Automated Test (short).yml index 7d6001b..8d25449 100644 --- a/.github/workflows/Automated Test (short).yml +++ b/.github/workflows/Automated Test (short).yml @@ -45,7 +45,7 @@ jobs: - name: Setup node uses: actions/setup-node@v1 with: - node-version: '12' + node-version: '15' # START: install psychojs_testing - name: Checkout psychojs_testing diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..6ca79e2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,25 @@ +name: Build Branch +on: workflow_dispatch + +jobs: + build_all: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + with: + path: app + - uses: actions/setup-node@master + with: + node-version: 19 + - name: Install Node dependencies + run: | + cd app + npm install + + - name: Build + run: | + cd app + echo "testing GITHUB_REF with details availability: ${GITHUB_REF#refs/heads/}" + npm run build:js && npm run build:css + echo "executing ls out on the directory:" + ls out diff --git a/.gitignore b/.gitignore index c19bd94..fa13e0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.vscode/ dist out node_modules +src/test_experiment.js +src/test_resources diff --git a/package-lock.json b/package-lock.json index cb6071b..0188cd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,36 @@ { "name": "psychojs", - "version": "2022.2.0", + "version": "2024.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "psychojs", - "version": "2022.2.0", + "version": "2024.1.0", "license": "MIT", "dependencies": { "@pixi/filter-adjustment": "^4.1.3", "a11y-dialog": "^7.5.0", "docdash": "^1.2.0", "esbuild-plugin-glsl": "^1.0.5", + "gifuct-js": "^2.1.2", "howler": "^2.2.1", "log4javascript": "github:Ritzlgrmft/log4javascript", "pako": "^1.0.10", + "pixi-filters": "^5.0.0", "pixi.js-legacy": "^6.0.4", "seedrandom": "^3.0.5", "tone": "^14.7.77", - "xlsx": "^0.17.0" + "xlsx": "^0.18.5" }, "devDependencies": { "csslint": "^1.0.5", "dprint": "^0.15.3", "esbuild": "^0.12.1", "eslint": "^7.26.0", - "jsdoc": "^3.6.7" + "jsdoc": "^3.6.7", + "vite": "^5.1.6", + "vite-plugin-glsl": "^1.2.1" }, "engines": { "node": ">=14.15.0", @@ -79,6 +83,374 @@ "regenerator-runtime": "^0.13.4" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint/eslintrc": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.1.tgz", @@ -288,6 +660,15 @@ "@pixi/text": "6.0.4" } }, + "node_modules/@pixi/color": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.2.4.tgz", + "integrity": "sha512-B/+9JRcXe2uE8wQfsueFRPZVayF2VEMRB7XGeRAsWCryOX19nmWhv0Nt3nOU2rvzI0niz9XgugJXsB6vVmDFSg==", + "peer": true, + "dependencies": { + "colord": "^2.9.3" + } + }, "node_modules/@pixi/compressed-textures": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.0.4.tgz", @@ -331,6 +712,12 @@ "@pixi/utils": "6.0.4" } }, + "node_modules/@pixi/extensions": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.2.4.tgz", + "integrity": "sha512-Mnqv9scbL1ARD3QFKfOWs2aSVJJfP1dL8g5UiqGImYO3rZbz/9QCzXOeMVIZ5n3iaRyKMNhFFr84/zUja2H7Dw==", + "peer": true + }, "node_modules/@pixi/extract": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.0.4.tgz", @@ -639,11 +1026,220 @@ "url": "^0.11.0" } }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "peer": true + }, "node_modules/@types/earcut": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz", "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==" }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", + "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==", + "peer": true + }, "node_modules/a11y-dialog": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.0.tgz", @@ -674,16 +1270,9 @@ } }, "node_modules/adler-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", - "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=", - "dependencies": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - }, - "bin": { - "adler32": "bin/adler32.njs" - }, + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", "engines": { "node": ">=0.8" } @@ -799,13 +1388,12 @@ } }, "node_modules/cfb": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.0.tgz", - "integrity": "sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", "dependencies": { - "adler-32": "~1.2.0", - "crc-32": "~1.2.0", - "printj": "~1.1.2" + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" }, "engines": { "node": ">=0.8" @@ -835,25 +1423,13 @@ } }, "node_modules/codepage": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", - "integrity": "sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k=", - "dependencies": { - "commander": "~2.14.1", - "exit-on-epipe": "~1.0.1" - }, - "bin": { - "codepage": "bin/codepage.njs" - }, + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", "engines": { "node": ">=0.8" } }, - "node_modules/codepage/node_modules/commander": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", - "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -869,6 +1445,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "peer": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -876,13 +1458,9 @@ "dev": true }, "node_modules/crc-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", - "dependencies": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - }, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "bin": { "crc32": "bin/crc32.njs" }, @@ -957,9 +1535,9 @@ } }, "node_modules/earcut": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz", - "integrity": "sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1405,6 +1983,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1419,14 +2003,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, - "node_modules/exit-on-epipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", - "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1445,11 +2021,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "node_modules/fflate": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", - "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1500,12 +2071,34 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "node_modules/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -1624,6 +2217,11 @@ "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==" }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1856,6 +2454,24 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -1954,6 +2570,427 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixi-filters": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-5.0.0.tgz", + "integrity": "sha512-j90nvbiRpDozxalSUaQ2kTIyFNGAUKxJ2qhPs4ThmVLiR9lam5x+GpP+c1Yx5N+qc+u0tH5G3VRY1usB69atrw==", + "dependencies": { + "@pixi/filter-adjustment": "5.0.0", + "@pixi/filter-advanced-bloom": "5.0.0", + "@pixi/filter-ascii": "5.0.0", + "@pixi/filter-bevel": "5.0.0", + "@pixi/filter-bloom": "5.0.0", + "@pixi/filter-bulge-pinch": "5.0.0", + "@pixi/filter-color-map": "5.0.0", + "@pixi/filter-color-overlay": "5.0.0", + "@pixi/filter-color-replace": "5.0.0", + "@pixi/filter-convolution": "5.0.0", + "@pixi/filter-cross-hatch": "5.0.0", + "@pixi/filter-crt": "5.0.0", + "@pixi/filter-dot": "5.0.0", + "@pixi/filter-drop-shadow": "5.0.0", + "@pixi/filter-emboss": "5.0.0", + "@pixi/filter-glitch": "5.0.0", + "@pixi/filter-glow": "5.0.0", + "@pixi/filter-godray": "5.0.0", + "@pixi/filter-kawase-blur": "5.0.0", + "@pixi/filter-motion-blur": "5.0.0", + "@pixi/filter-multi-color-replace": "5.0.0", + "@pixi/filter-old-film": "5.0.0", + "@pixi/filter-outline": "5.0.0", + "@pixi/filter-pixelate": "5.0.0", + "@pixi/filter-radial-blur": "5.0.0", + "@pixi/filter-reflection": "5.0.0", + "@pixi/filter-rgb-split": "5.0.0", + "@pixi/filter-shockwave": "5.0.0", + "@pixi/filter-simple-lightmap": "5.0.0", + "@pixi/filter-tilt-shift": "5.0.0", + "@pixi/filter-twist": "5.0.0", + "@pixi/filter-zoom-blur": "5.0.0" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/constants": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.2.4.tgz", + "integrity": "sha512-hKuHBWR6N4Q0Sf5MGF3/9l+POg/G5rqhueHfzofiuelnKg7aBs3BVjjZ+6hZbd6M++vOUmxYelEX/NEFBxrheA==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/core": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.2.4.tgz", + "integrity": "sha512-0XtvrfxHlS2T+beBBSpo7GI8+QLyyTqMVQpNmPqB4woYxzrOEJ9JaUFBaBfCvycLeUkfVih1u6HAbtF+2d1EjQ==", + "peer": true, + "dependencies": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/extensions": "7.2.4", + "@pixi/math": "7.2.4", + "@pixi/runner": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/ticker": "7.2.4", + "@pixi/utils": "7.2.4", + "@types/offscreencanvas": "^2019.6.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-adjustment": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-adjustment/-/filter-adjustment-5.0.0.tgz", + "integrity": "sha512-Epci8zSWCNWhFtnarvQqOcnmOqLfhXIJ7NNENEi2E1rom1Ar13RLM76CBGBbuDRK7flweqcWmZb0QZLxqwxTDg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-advanced-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-advanced-bloom/-/filter-advanced-bloom-5.0.0.tgz", + "integrity": "sha512-P5Xt65GLBEqjZVUkLe4ZZk4D1/j9UEXYnYFG3JrLPYkdcniwD4Y+NIyNCJ+eP91ivgoCmK/+SyBRv0P0AEQkTw==", + "dependencies": { + "@pixi/filter-kawase-blur": "5.0.0" + }, + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-alpha": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.2.4.tgz", + "integrity": "sha512-UTUMSGyktUr+I9vmigqJo9iUhb0nwGyqTTME2xBWZvVGCnl5z+/wHxvIBBCe5pNZ66IM15pGXQ4cDcfqCuP2kA==", + "peer": true, + "peerDependencies": { + "@pixi/core": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-ascii": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-ascii/-/filter-ascii-5.0.0.tgz", + "integrity": "sha512-A49yNhiye/aFDOnI11zwEm/td2xho0td/Cvzvru8FUgi1MzJvZE03W/JoLl04ToZczw143wFPxutl6V/Ohw5bQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bevel": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bevel/-/filter-bevel-5.0.0.tgz", + "integrity": "sha512-0Odat0tW/uoS/uyp0rigm07Q3YPgwKLTgkZZZSzIUVsPnwcJjiocSzWel73JkiY3m2ZjTrj+JZjkyGjkYH+2gQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bloom/-/filter-bloom-5.0.0.tgz", + "integrity": "sha512-vOSNJNV5y+ifwQWfzEmml3owcgoJAQIQtMR17SELBUwfYP60qxy5bNWBdYBlipSJVwX2AuGi8Xk5Ia9dijcqZQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X", + "@pixi/filter-alpha": "^7.0.0-X", + "@pixi/filter-blur": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-blur": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.2.4.tgz", + "integrity": "sha512-aLyXIoxy14bTansCPtbY8x7Sdn2OrrqkF/pcKiRXHJGGhi7wPacvB/NcmYJdnI/n2ExQ6V5Njuj/nfrsejVwcA==", + "peer": true, + "peerDependencies": { + "@pixi/core": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bulge-pinch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bulge-pinch/-/filter-bulge-pinch-5.0.0.tgz", + "integrity": "sha512-j1feWsCpyTZk4aHbYNjax52lt0OtyYDbHvYaePYzGO/SBb1t/spDnHQEkAP7R3bZ7Ud/GI4RgefAFnvsYeSetQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-map": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-map/-/filter-color-map-5.0.0.tgz", + "integrity": "sha512-w77mRi89sLUMwjhl7qL/q1YrhEKyOk2MJZQdKBksvGEV/Mf5mV2h3+EOC62wB18Q4iUVQy1MS4sANyVaCctu2w==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-overlay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-overlay/-/filter-color-overlay-5.0.0.tgz", + "integrity": "sha512-AjxVN6gnZ+xCryQUmI+TVy3yVF+CcLgDPv+nSVPDlQowuqYhZjD6qSzgRCl3Kezdi3AxrL1vi1fnBudEnzdDJg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-replace/-/filter-color-replace-5.0.0.tgz", + "integrity": "sha512-u4VOtKbY6SSr2P9v5AL8/2MVsUcAH9z92c1eaqeE3PXCPNyCgZKuNHWl8+FjBIDl/1UMQVhXH2zNrC3Vuqo3JA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-convolution": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-convolution/-/filter-convolution-5.0.0.tgz", + "integrity": "sha512-SYjyKXODdHbjzBP9c5QGMOfowNwkNFi7zW1XzGwEadmv6mLHNanO3nm0PtRu/3B9B6AW1fvOaUecYmhjAZfQjg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-cross-hatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-cross-hatch/-/filter-cross-hatch-5.0.0.tgz", + "integrity": "sha512-J4bcI3MUc/Ol3nQIsXZldYEtiLAl3ktU28zlidwffkANyl/XjP76bLEgFBoc4RE2iP/FQ+9ZeEqpsN8DIg6vVg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-crt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-crt/-/filter-crt-5.0.0.tgz", + "integrity": "sha512-/kgjNW+BCCVtUa0s8Usk3WyxgBX8kelAiqkyVnM1g8xM19Dh2689gK2wjx0ibS0p74EHs42QpkJj7jTL+1MS7A==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-dot": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-dot/-/filter-dot-5.0.0.tgz", + "integrity": "sha512-kytardK58Ifl5D8Ss3kkfI29FMzV3+npJYr5GAKnA80R7XGOPOMoxrknhou8y+Dw9LUcOv8y643wryvL43P2vw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-drop-shadow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-drop-shadow/-/filter-drop-shadow-5.0.0.tgz", + "integrity": "sha512-kz2eL+ikCLL7/2RICyIkw3pZXkyMY0Ji6skhnPj7JaZSjH4V+7TiKqYXp532gTbwSRj/mzLCvFfOL3WwTDgZ1w==", + "dependencies": { + "@pixi/filter-kawase-blur": "5.0.0" + }, + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-emboss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-emboss/-/filter-emboss-5.0.0.tgz", + "integrity": "sha512-wvrk9zB62lGaPcCWbTwoaO48FrLIE4+hi02BVS+exx5RvIniNUJD/ledGxdmUjcHX/2mDIIs7PH0kAs1L/ziZw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-glitch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glitch/-/filter-glitch-5.0.0.tgz", + "integrity": "sha512-yK3plqExyQp9eo3dwV03dnSHpQgh0xeD112ieAsqefrAOLc5AXSfTelPvEQaZ07ZkcxSDE5eqKcRvcIVi2IgLQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-glow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glow/-/filter-glow-5.0.0.tgz", + "integrity": "sha512-D+YE9DGSJXtmZa6aoWJfuNu+6MnSw90GP7oRRzr7S1/4moeFZ7EWbvQehl9Y9j98idHG87Cvuh6mmsRqpgS6ow==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-godray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-godray/-/filter-godray-5.0.0.tgz", + "integrity": "sha512-L4PD3cysUMjTSDYk5q5xUtal9q6kfH8NVIdNT3aTDJpR0VW4b/ClanmOTFpJVzN6Ld/JlJbdg8ogUpXBe1gVuw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-kawase-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-kawase-blur/-/filter-kawase-blur-5.0.0.tgz", + "integrity": "sha512-dKSTaPUOvdVkfx9x+kp0TzYjGAl8CLxIRGz6Wh43NKx96nVqd/lWqvlda+zloHVgZyQoJNHZZ4Spjcw2mYoaWg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-motion-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-motion-blur/-/filter-motion-blur-5.0.0.tgz", + "integrity": "sha512-2av4dnVL1uyyCKF8RlZaMfeO8YnQwA893j24S15ubWHZaz4WlWH3lFIYmCMqlEqHPlFDBER4vLxpR1WjsUsX/Q==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-multi-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-multi-color-replace/-/filter-multi-color-replace-5.0.0.tgz", + "integrity": "sha512-hcmCKFFQ1baGDrZc/blK9zWpe3f02rqWGsPx5VRRgc1sk44UYXHCKZnDjF80/g0ls8U4Lj+/5Xb7HOQq2LyyDg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-old-film": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-old-film/-/filter-old-film-5.0.0.tgz", + "integrity": "sha512-XSHBz4JDbvYtUrf/NP5eKCw/wvaKTAKXQENDxk480tKYtDuteSCMg87ZjLrPlyKtGySW8KTmdzl58bZjSYpiyA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-outline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-outline/-/filter-outline-5.0.0.tgz", + "integrity": "sha512-efS3Or7VQFXo2ZyPFR2M/JlZrcLAxeVbOTPYvgKe574yUghSQbQ/pyqDWE16tRB/W7+osMrTV0+C4/N/9wIxhQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-pixelate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-pixelate/-/filter-pixelate-5.0.0.tgz", + "integrity": "sha512-3g1ajOLsYy+x0FCC67WhDcjixrcBlhK3Zo+JP9zlHSxh0W4yNzfhsw9EsIb9XP4WnMtMAUMg5T0MLTnjbsrK4g==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-radial-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-radial-blur/-/filter-radial-blur-5.0.0.tgz", + "integrity": "sha512-zafBJCAiqRtsTNGKiQ8iMt00KbG20qtBi71h286wWbr0na37iXsRcg4EN76eyNbpfAOX+1ylBgIuSd9hLyQBFA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-reflection": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-reflection/-/filter-reflection-5.0.0.tgz", + "integrity": "sha512-PuZe19XUq0gTdmAStu3hcyGKkNlKGrpblN4s6vJmV+vAKVcFv2OpfjtuGUXcP/oi2LmLakC/vKfEx4bDgZzz+w==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-rgb-split": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-rgb-split/-/filter-rgb-split-5.0.0.tgz", + "integrity": "sha512-zsWBrDkj9EdjJRPjGCt/0O2Vx/8Gt+8VTmjRA0ONoegcMD9slJdJMgL9EbH/1y5WHgmzGbgZIPvWULIqepVxBQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-shockwave": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-shockwave/-/filter-shockwave-5.0.0.tgz", + "integrity": "sha512-aL0ExAkJGcUo463Ktq4HXjZGlJDpoYcyZhwd87maJrFsBjQZl2gopse6bEsy7IJxbAKzlpUKFmAP9rxwZWqMVQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-simple-lightmap": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-simple-lightmap/-/filter-simple-lightmap-5.0.0.tgz", + "integrity": "sha512-0WIKQIGZ3aNafe2VZIbGQJWxSlBMbmjM9J+Tswjaeg8Z1dz6Qux5lYIC16wZOaIqVlWL5GTpfn8HU0BHCOvESA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-tilt-shift": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-tilt-shift/-/filter-tilt-shift-5.0.0.tgz", + "integrity": "sha512-nIxYoTU9kFDx3EE1fyoIEOfAia9Tvoj+sakTKCJZUvTk+5tjpZdAm+Ump42cnb6UxTR8AMTQiwH54C7I0pbA4Q==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-twist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-twist/-/filter-twist-5.0.0.tgz", + "integrity": "sha512-YVtz3ZPfvaz22gZRZo+cOC0/L6SgSZmr/HEa6Ir+BRNVqLff6CpPx6YBVJqPREh+HFZjDomSP0kf5JasQYhzSg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-zoom-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-zoom-blur/-/filter-zoom-blur-5.0.0.tgz", + "integrity": "sha512-Q1ftuY/KPgbVtJHCvl0p4hrwVWRMWZ/yX1YRjdLGSyOwMEN8u16MEEXFQUtixEHY7+MBRBWaPOaXBaQrd+Xq7A==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/math": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.2.4.tgz", + "integrity": "sha512-LJB+mozyEPllxa0EssFZrKNfVwysfaBun4b2dJKQQInp0DafgbA0j7A+WVg0oe51KhFULTJMpDqbLn/ITFc41A==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/runner": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.2.4.tgz", + "integrity": "sha512-YtyqPk1LA+0guEFKSFx6t/YSvbEQwajFwi4Ft8iDhioa6VK2MmTir1GjWwy7JQYLcDmYSAcQjnmFtVTZohyYSw==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/settings": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.2.4.tgz", + "integrity": "sha512-ZPKRar9EwibijGmH8EViu4Greq1I/O7V/xQx2rNqN23XA7g09Qo6yfaeQpufu5xl8+/lZrjuHtQSnuY7OgG1CA==", + "peer": true, + "dependencies": { + "@pixi/constants": "7.2.4", + "@types/css-font-loading-module": "^0.0.7", + "ismobilejs": "^1.1.0" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/ticker": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.2.4.tgz", + "integrity": "sha512-hQQHIHvGeFsP4GNezZqjzuhUgNQEVgCH9+qU05UX1Mc5UHC9l6OJnY4VTVhhcHxZjA6RnyaY+1zBxCnoXuazpg==", + "peer": true, + "dependencies": { + "@pixi/extensions": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/utils": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/utils": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.2.4.tgz", + "integrity": "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ==", + "peer": true, + "dependencies": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/settings": "7.2.4", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, + "node_modules/pixi-filters/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + }, "node_modules/pixi.js": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.0.4.tgz", @@ -2022,6 +3059,34 @@ "url": "https://opencollective.com/pixijs" } }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2031,17 +3096,6 @@ "node": ">= 0.8.0" } }, - "node_modules/printj": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", - "bin": { - "printj": "bin/printj.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -2132,6 +3186,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", + "fsevents": "~2.3.2" + } + }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -2187,6 +3273,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -2382,6 +3477,115 @@ "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", "dev": true }, + "node_modules/vite": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-glsl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/vite-plugin-glsl/-/vite-plugin-glsl-1.2.1.tgz", + "integrity": "sha512-yBpBHWfdiRVMxN3yIKx4qmwuqMwoMAnEMipVI0NbdIieyRFO8hpW8VTFHYi3W75h7CkvsotteP9C4pln51OE0A==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.2" + }, + "engines": { + "node": ">= 16.15.1", + "npm": ">= 8.11.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, "node_modules/wmf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", @@ -2414,17 +3618,14 @@ "dev": true }, "node_modules/xlsx": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.17.0.tgz", - "integrity": "sha512-bZ36FSACiAyjoldey1+7it50PMlDp1pcAJrZKcVZHzKd8BC/z6TQ/QAN8onuqcepifqSznR6uKnjPhaGt6ig9A==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", "dependencies": { - "adler-32": "~1.2.0", - "cfb": "^1.1.4", - "codepage": "~1.14.0", - "commander": "~2.17.1", - "crc-32": "~1.2.0", - "exit-on-epipe": "~1.0.1", - "fflate": "^0.3.8", + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" @@ -2436,11 +3637,6 @@ "node": ">=0.8" } }, - "node_modules/xlsx/node_modules/commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" - }, "node_modules/xmlcreate": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz", @@ -2495,6 +3691,167 @@ "regenerator-runtime": "^0.13.4" } }, + "@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "dev": true, + "optional": true + }, "@eslint/eslintrc": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.1.tgz", @@ -2682,6 +4039,15 @@ "@pixi/text": "6.0.4" } }, + "@pixi/color": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.2.4.tgz", + "integrity": "sha512-B/+9JRcXe2uE8wQfsueFRPZVayF2VEMRB7XGeRAsWCryOX19nmWhv0Nt3nOU2rvzI0niz9XgugJXsB6vVmDFSg==", + "peer": true, + "requires": { + "colord": "^2.9.3" + } + }, "@pixi/compressed-textures": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.0.4.tgz", @@ -2721,6 +4087,12 @@ "@pixi/utils": "6.0.4" } }, + "@pixi/extensions": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.2.4.tgz", + "integrity": "sha512-Mnqv9scbL1ARD3QFKfOWs2aSVJJfP1dL8g5UiqGImYO3rZbz/9QCzXOeMVIZ5n3iaRyKMNhFFr84/zUja2H7Dw==", + "peer": true + }, "@pixi/extract": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.0.4.tgz", @@ -3026,11 +4398,131 @@ "url": "^0.11.0" } }, + "@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "dev": true, + "optional": true + }, + "@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "peer": true + }, "@types/earcut": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz", "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==" }, + "@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "@types/offscreencanvas": { + "version": "2019.7.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", + "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==", + "peer": true + }, "a11y-dialog": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.0.tgz", @@ -3053,13 +4545,9 @@ "requires": {} }, "adler-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", - "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=", - "requires": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - } + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==" }, "ajv": { "version": "6.12.6", @@ -3150,13 +4638,12 @@ } }, "cfb": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.0.tgz", - "integrity": "sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", "requires": { - "adler-32": "~1.2.0", - "crc-32": "~1.2.0", - "printj": "~1.1.2" + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" } }, "chalk": { @@ -3177,20 +4664,9 @@ "dev": true }, "codepage": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", - "integrity": "sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k=", - "requires": { - "commander": "~2.14.1", - "exit-on-epipe": "~1.0.1" - }, - "dependencies": { - "commander": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", - "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" - } - } + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==" }, "color-convert": { "version": "1.9.3", @@ -3207,6 +4683,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "peer": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3214,13 +4696,9 @@ "dev": true }, "crc-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", - "requires": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - } + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" }, "csslint": { "version": "1.0.5", @@ -3268,9 +4746,9 @@ "dev": true }, "earcut": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz", - "integrity": "sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, "emoji-regex": { "version": "8.0.0", @@ -3587,6 +5065,12 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3598,11 +5082,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, - "exit-on-epipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", - "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3621,11 +5100,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fflate": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", - "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" - }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3667,12 +5141,27 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "requires": { + "js-binary-schema-parser": "^2.0.3" + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -3767,6 +5256,11 @@ "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==" }, + "js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3965,6 +5459,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4044,6 +5544,347 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pixi-filters": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-5.0.0.tgz", + "integrity": "sha512-j90nvbiRpDozxalSUaQ2kTIyFNGAUKxJ2qhPs4ThmVLiR9lam5x+GpP+c1Yx5N+qc+u0tH5G3VRY1usB69atrw==", + "requires": { + "@pixi/filter-adjustment": "5.0.0", + "@pixi/filter-advanced-bloom": "5.0.0", + "@pixi/filter-ascii": "5.0.0", + "@pixi/filter-bevel": "5.0.0", + "@pixi/filter-bloom": "5.0.0", + "@pixi/filter-bulge-pinch": "5.0.0", + "@pixi/filter-color-map": "5.0.0", + "@pixi/filter-color-overlay": "5.0.0", + "@pixi/filter-color-replace": "5.0.0", + "@pixi/filter-convolution": "5.0.0", + "@pixi/filter-cross-hatch": "5.0.0", + "@pixi/filter-crt": "5.0.0", + "@pixi/filter-dot": "5.0.0", + "@pixi/filter-drop-shadow": "5.0.0", + "@pixi/filter-emboss": "5.0.0", + "@pixi/filter-glitch": "5.0.0", + "@pixi/filter-glow": "5.0.0", + "@pixi/filter-godray": "5.0.0", + "@pixi/filter-kawase-blur": "5.0.0", + "@pixi/filter-motion-blur": "5.0.0", + "@pixi/filter-multi-color-replace": "5.0.0", + "@pixi/filter-old-film": "5.0.0", + "@pixi/filter-outline": "5.0.0", + "@pixi/filter-pixelate": "5.0.0", + "@pixi/filter-radial-blur": "5.0.0", + "@pixi/filter-reflection": "5.0.0", + "@pixi/filter-rgb-split": "5.0.0", + "@pixi/filter-shockwave": "5.0.0", + "@pixi/filter-simple-lightmap": "5.0.0", + "@pixi/filter-tilt-shift": "5.0.0", + "@pixi/filter-twist": "5.0.0", + "@pixi/filter-zoom-blur": "5.0.0" + }, + "dependencies": { + "@pixi/constants": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.2.4.tgz", + "integrity": "sha512-hKuHBWR6N4Q0Sf5MGF3/9l+POg/G5rqhueHfzofiuelnKg7aBs3BVjjZ+6hZbd6M++vOUmxYelEX/NEFBxrheA==", + "peer": true + }, + "@pixi/core": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.2.4.tgz", + "integrity": "sha512-0XtvrfxHlS2T+beBBSpo7GI8+QLyyTqMVQpNmPqB4woYxzrOEJ9JaUFBaBfCvycLeUkfVih1u6HAbtF+2d1EjQ==", + "peer": true, + "requires": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/extensions": "7.2.4", + "@pixi/math": "7.2.4", + "@pixi/runner": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/ticker": "7.2.4", + "@pixi/utils": "7.2.4", + "@types/offscreencanvas": "^2019.6.4" + } + }, + "@pixi/filter-adjustment": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-adjustment/-/filter-adjustment-5.0.0.tgz", + "integrity": "sha512-Epci8zSWCNWhFtnarvQqOcnmOqLfhXIJ7NNENEi2E1rom1Ar13RLM76CBGBbuDRK7flweqcWmZb0QZLxqwxTDg==", + "requires": {} + }, + "@pixi/filter-advanced-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-advanced-bloom/-/filter-advanced-bloom-5.0.0.tgz", + "integrity": "sha512-P5Xt65GLBEqjZVUkLe4ZZk4D1/j9UEXYnYFG3JrLPYkdcniwD4Y+NIyNCJ+eP91ivgoCmK/+SyBRv0P0AEQkTw==", + "requires": { + "@pixi/filter-kawase-blur": "5.0.0" + } + }, + "@pixi/filter-alpha": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.2.4.tgz", + "integrity": "sha512-UTUMSGyktUr+I9vmigqJo9iUhb0nwGyqTTME2xBWZvVGCnl5z+/wHxvIBBCe5pNZ66IM15pGXQ4cDcfqCuP2kA==", + "peer": true, + "requires": {} + }, + "@pixi/filter-ascii": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-ascii/-/filter-ascii-5.0.0.tgz", + "integrity": "sha512-A49yNhiye/aFDOnI11zwEm/td2xho0td/Cvzvru8FUgi1MzJvZE03W/JoLl04ToZczw143wFPxutl6V/Ohw5bQ==", + "requires": {} + }, + "@pixi/filter-bevel": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bevel/-/filter-bevel-5.0.0.tgz", + "integrity": "sha512-0Odat0tW/uoS/uyp0rigm07Q3YPgwKLTgkZZZSzIUVsPnwcJjiocSzWel73JkiY3m2ZjTrj+JZjkyGjkYH+2gQ==", + "requires": {} + }, + "@pixi/filter-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bloom/-/filter-bloom-5.0.0.tgz", + "integrity": "sha512-vOSNJNV5y+ifwQWfzEmml3owcgoJAQIQtMR17SELBUwfYP60qxy5bNWBdYBlipSJVwX2AuGi8Xk5Ia9dijcqZQ==", + "requires": {} + }, + "@pixi/filter-blur": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.2.4.tgz", + "integrity": "sha512-aLyXIoxy14bTansCPtbY8x7Sdn2OrrqkF/pcKiRXHJGGhi7wPacvB/NcmYJdnI/n2ExQ6V5Njuj/nfrsejVwcA==", + "peer": true, + "requires": {} + }, + "@pixi/filter-bulge-pinch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bulge-pinch/-/filter-bulge-pinch-5.0.0.tgz", + "integrity": "sha512-j1feWsCpyTZk4aHbYNjax52lt0OtyYDbHvYaePYzGO/SBb1t/spDnHQEkAP7R3bZ7Ud/GI4RgefAFnvsYeSetQ==", + "requires": {} + }, + "@pixi/filter-color-map": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-map/-/filter-color-map-5.0.0.tgz", + "integrity": "sha512-w77mRi89sLUMwjhl7qL/q1YrhEKyOk2MJZQdKBksvGEV/Mf5mV2h3+EOC62wB18Q4iUVQy1MS4sANyVaCctu2w==", + "requires": {} + }, + "@pixi/filter-color-overlay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-overlay/-/filter-color-overlay-5.0.0.tgz", + "integrity": "sha512-AjxVN6gnZ+xCryQUmI+TVy3yVF+CcLgDPv+nSVPDlQowuqYhZjD6qSzgRCl3Kezdi3AxrL1vi1fnBudEnzdDJg==", + "requires": {} + }, + "@pixi/filter-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-replace/-/filter-color-replace-5.0.0.tgz", + "integrity": "sha512-u4VOtKbY6SSr2P9v5AL8/2MVsUcAH9z92c1eaqeE3PXCPNyCgZKuNHWl8+FjBIDl/1UMQVhXH2zNrC3Vuqo3JA==", + "requires": {} + }, + "@pixi/filter-convolution": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-convolution/-/filter-convolution-5.0.0.tgz", + "integrity": "sha512-SYjyKXODdHbjzBP9c5QGMOfowNwkNFi7zW1XzGwEadmv6mLHNanO3nm0PtRu/3B9B6AW1fvOaUecYmhjAZfQjg==", + "requires": {} + }, + "@pixi/filter-cross-hatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-cross-hatch/-/filter-cross-hatch-5.0.0.tgz", + "integrity": "sha512-J4bcI3MUc/Ol3nQIsXZldYEtiLAl3ktU28zlidwffkANyl/XjP76bLEgFBoc4RE2iP/FQ+9ZeEqpsN8DIg6vVg==", + "requires": {} + }, + "@pixi/filter-crt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-crt/-/filter-crt-5.0.0.tgz", + "integrity": "sha512-/kgjNW+BCCVtUa0s8Usk3WyxgBX8kelAiqkyVnM1g8xM19Dh2689gK2wjx0ibS0p74EHs42QpkJj7jTL+1MS7A==", + "requires": {} + }, + "@pixi/filter-dot": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-dot/-/filter-dot-5.0.0.tgz", + "integrity": "sha512-kytardK58Ifl5D8Ss3kkfI29FMzV3+npJYr5GAKnA80R7XGOPOMoxrknhou8y+Dw9LUcOv8y643wryvL43P2vw==", + "requires": {} + }, + "@pixi/filter-drop-shadow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-drop-shadow/-/filter-drop-shadow-5.0.0.tgz", + "integrity": "sha512-kz2eL+ikCLL7/2RICyIkw3pZXkyMY0Ji6skhnPj7JaZSjH4V+7TiKqYXp532gTbwSRj/mzLCvFfOL3WwTDgZ1w==", + "requires": { + "@pixi/filter-kawase-blur": "5.0.0" + } + }, + "@pixi/filter-emboss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-emboss/-/filter-emboss-5.0.0.tgz", + "integrity": "sha512-wvrk9zB62lGaPcCWbTwoaO48FrLIE4+hi02BVS+exx5RvIniNUJD/ledGxdmUjcHX/2mDIIs7PH0kAs1L/ziZw==", + "requires": {} + }, + "@pixi/filter-glitch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glitch/-/filter-glitch-5.0.0.tgz", + "integrity": "sha512-yK3plqExyQp9eo3dwV03dnSHpQgh0xeD112ieAsqefrAOLc5AXSfTelPvEQaZ07ZkcxSDE5eqKcRvcIVi2IgLQ==", + "requires": {} + }, + "@pixi/filter-glow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glow/-/filter-glow-5.0.0.tgz", + "integrity": "sha512-D+YE9DGSJXtmZa6aoWJfuNu+6MnSw90GP7oRRzr7S1/4moeFZ7EWbvQehl9Y9j98idHG87Cvuh6mmsRqpgS6ow==", + "requires": {} + }, + "@pixi/filter-godray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-godray/-/filter-godray-5.0.0.tgz", + "integrity": "sha512-L4PD3cysUMjTSDYk5q5xUtal9q6kfH8NVIdNT3aTDJpR0VW4b/ClanmOTFpJVzN6Ld/JlJbdg8ogUpXBe1gVuw==", + "requires": {} + }, + "@pixi/filter-kawase-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-kawase-blur/-/filter-kawase-blur-5.0.0.tgz", + "integrity": "sha512-dKSTaPUOvdVkfx9x+kp0TzYjGAl8CLxIRGz6Wh43NKx96nVqd/lWqvlda+zloHVgZyQoJNHZZ4Spjcw2mYoaWg==", + "requires": {} + }, + "@pixi/filter-motion-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-motion-blur/-/filter-motion-blur-5.0.0.tgz", + "integrity": "sha512-2av4dnVL1uyyCKF8RlZaMfeO8YnQwA893j24S15ubWHZaz4WlWH3lFIYmCMqlEqHPlFDBER4vLxpR1WjsUsX/Q==", + "requires": {} + }, + "@pixi/filter-multi-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-multi-color-replace/-/filter-multi-color-replace-5.0.0.tgz", + "integrity": "sha512-hcmCKFFQ1baGDrZc/blK9zWpe3f02rqWGsPx5VRRgc1sk44UYXHCKZnDjF80/g0ls8U4Lj+/5Xb7HOQq2LyyDg==", + "requires": {} + }, + "@pixi/filter-old-film": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-old-film/-/filter-old-film-5.0.0.tgz", + "integrity": "sha512-XSHBz4JDbvYtUrf/NP5eKCw/wvaKTAKXQENDxk480tKYtDuteSCMg87ZjLrPlyKtGySW8KTmdzl58bZjSYpiyA==", + "requires": {} + }, + "@pixi/filter-outline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-outline/-/filter-outline-5.0.0.tgz", + "integrity": "sha512-efS3Or7VQFXo2ZyPFR2M/JlZrcLAxeVbOTPYvgKe574yUghSQbQ/pyqDWE16tRB/W7+osMrTV0+C4/N/9wIxhQ==", + "requires": {} + }, + "@pixi/filter-pixelate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-pixelate/-/filter-pixelate-5.0.0.tgz", + "integrity": "sha512-3g1ajOLsYy+x0FCC67WhDcjixrcBlhK3Zo+JP9zlHSxh0W4yNzfhsw9EsIb9XP4WnMtMAUMg5T0MLTnjbsrK4g==", + "requires": {} + }, + "@pixi/filter-radial-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-radial-blur/-/filter-radial-blur-5.0.0.tgz", + "integrity": "sha512-zafBJCAiqRtsTNGKiQ8iMt00KbG20qtBi71h286wWbr0na37iXsRcg4EN76eyNbpfAOX+1ylBgIuSd9hLyQBFA==", + "requires": {} + }, + "@pixi/filter-reflection": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-reflection/-/filter-reflection-5.0.0.tgz", + "integrity": "sha512-PuZe19XUq0gTdmAStu3hcyGKkNlKGrpblN4s6vJmV+vAKVcFv2OpfjtuGUXcP/oi2LmLakC/vKfEx4bDgZzz+w==", + "requires": {} + }, + "@pixi/filter-rgb-split": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-rgb-split/-/filter-rgb-split-5.0.0.tgz", + "integrity": "sha512-zsWBrDkj9EdjJRPjGCt/0O2Vx/8Gt+8VTmjRA0ONoegcMD9slJdJMgL9EbH/1y5WHgmzGbgZIPvWULIqepVxBQ==", + "requires": {} + }, + "@pixi/filter-shockwave": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-shockwave/-/filter-shockwave-5.0.0.tgz", + "integrity": "sha512-aL0ExAkJGcUo463Ktq4HXjZGlJDpoYcyZhwd87maJrFsBjQZl2gopse6bEsy7IJxbAKzlpUKFmAP9rxwZWqMVQ==", + "requires": {} + }, + "@pixi/filter-simple-lightmap": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-simple-lightmap/-/filter-simple-lightmap-5.0.0.tgz", + "integrity": "sha512-0WIKQIGZ3aNafe2VZIbGQJWxSlBMbmjM9J+Tswjaeg8Z1dz6Qux5lYIC16wZOaIqVlWL5GTpfn8HU0BHCOvESA==", + "requires": {} + }, + "@pixi/filter-tilt-shift": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-tilt-shift/-/filter-tilt-shift-5.0.0.tgz", + "integrity": "sha512-nIxYoTU9kFDx3EE1fyoIEOfAia9Tvoj+sakTKCJZUvTk+5tjpZdAm+Ump42cnb6UxTR8AMTQiwH54C7I0pbA4Q==", + "requires": {} + }, + "@pixi/filter-twist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-twist/-/filter-twist-5.0.0.tgz", + "integrity": "sha512-YVtz3ZPfvaz22gZRZo+cOC0/L6SgSZmr/HEa6Ir+BRNVqLff6CpPx6YBVJqPREh+HFZjDomSP0kf5JasQYhzSg==", + "requires": {} + }, + "@pixi/filter-zoom-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-zoom-blur/-/filter-zoom-blur-5.0.0.tgz", + "integrity": "sha512-Q1ftuY/KPgbVtJHCvl0p4hrwVWRMWZ/yX1YRjdLGSyOwMEN8u16MEEXFQUtixEHY7+MBRBWaPOaXBaQrd+Xq7A==", + "requires": {} + }, + "@pixi/math": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.2.4.tgz", + "integrity": "sha512-LJB+mozyEPllxa0EssFZrKNfVwysfaBun4b2dJKQQInp0DafgbA0j7A+WVg0oe51KhFULTJMpDqbLn/ITFc41A==", + "peer": true + }, + "@pixi/runner": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.2.4.tgz", + "integrity": "sha512-YtyqPk1LA+0guEFKSFx6t/YSvbEQwajFwi4Ft8iDhioa6VK2MmTir1GjWwy7JQYLcDmYSAcQjnmFtVTZohyYSw==", + "peer": true + }, + "@pixi/settings": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.2.4.tgz", + "integrity": "sha512-ZPKRar9EwibijGmH8EViu4Greq1I/O7V/xQx2rNqN23XA7g09Qo6yfaeQpufu5xl8+/lZrjuHtQSnuY7OgG1CA==", + "peer": true, + "requires": { + "@pixi/constants": "7.2.4", + "@types/css-font-loading-module": "^0.0.7", + "ismobilejs": "^1.1.0" + } + }, + "@pixi/ticker": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.2.4.tgz", + "integrity": "sha512-hQQHIHvGeFsP4GNezZqjzuhUgNQEVgCH9+qU05UX1Mc5UHC9l6OJnY4VTVhhcHxZjA6RnyaY+1zBxCnoXuazpg==", + "peer": true, + "requires": { + "@pixi/extensions": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/utils": "7.2.4" + } + }, + "@pixi/utils": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.2.4.tgz", + "integrity": "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ==", + "peer": true, + "requires": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/settings": "7.2.4", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + } + } + }, "pixi.js": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.0.4.tgz", @@ -4104,17 +5945,23 @@ "pixi.js": "6.0.4" } }, + "postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "printj": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" - }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -4181,6 +6028,29 @@ "glob": "^7.1.3" } }, + "rollup": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@types/estree": "1.0.5", + "fsevents": "~2.3.2" + } + }, "seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -4223,6 +6093,12 @@ } } }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4394,6 +6270,60 @@ "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", "dev": true }, + "vite": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", + "dev": true, + "requires": { + "esbuild": "^0.19.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "dependencies": { + "esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + } + } + }, + "vite-plugin-glsl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/vite-plugin-glsl/-/vite-plugin-glsl-1.2.1.tgz", + "integrity": "sha512-yBpBHWfdiRVMxN3yIKx4qmwuqMwoMAnEMipVI0NbdIieyRFO8hpW8VTFHYi3W75h7CkvsotteP9C4pln51OE0A==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.2" + } + }, "wmf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", @@ -4417,27 +6347,17 @@ "dev": true }, "xlsx": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.17.0.tgz", - "integrity": "sha512-bZ36FSACiAyjoldey1+7it50PMlDp1pcAJrZKcVZHzKd8BC/z6TQ/QAN8onuqcepifqSznR6uKnjPhaGt6ig9A==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", "requires": { - "adler-32": "~1.2.0", - "cfb": "^1.1.4", - "codepage": "~1.14.0", - "commander": "~2.17.1", - "crc-32": "~1.2.0", - "exit-on-epipe": "~1.0.1", - "fflate": "^0.3.8", + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" - }, - "dependencies": { - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" - } } }, "xmlcreate": { diff --git a/package.json b/package.json index 8c7e2cb..ea75245 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psychojs", - "version": "2022.3.1", + "version": "2024.2.0", "private": true, "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "license": "MIT", @@ -15,6 +15,9 @@ }, "main": "./src/index.js", "scripts": { + "dev": "vite", + "vitebuild": "vite build", + "preview": "vite preview", "build": "npm run build:js && npm run build:css && npm run build:docs", "build:css": "node ./scripts/build.css.cjs", "build:docs": "jsdoc src -c jsdoc.json & cp jsdoc.css docs/styles/", @@ -31,15 +34,19 @@ "a11y-dialog": "^7.5.0", "docdash": "^1.2.0", "esbuild-plugin-glsl": "^1.0.5", + "gifuct-js": "^2.1.2", "howler": "^2.2.1", "log4javascript": "github:Ritzlgrmft/log4javascript", "pako": "^1.0.10", + "pixi-filters": "^5.0.0", "pixi.js-legacy": "^6.0.4", "seedrandom": "^3.0.5", "tone": "^14.7.77", - "xlsx": "^0.17.0" + "xlsx": "^0.18.5" }, "devDependencies": { + "vite": "^5.1.6", + "vite-plugin-glsl": "^1.2.1", "csslint": "^1.0.5", "dprint": "^0.15.3", "esbuild": "^0.12.1", diff --git a/src/core/EventManager.js b/src/core/EventManager.js index c9f8255..49e6b8f 100644 --- a/src/core/EventManager.js +++ b/src/core/EventManager.js @@ -49,6 +49,12 @@ export class EventManager // clock reset when mouse is moved: moveClock: new Clock(), }; + + // storing touches in both map and array for fast search and fast access if touchID is known + this._touchInfo = { + touchesArray: [], + touchesMap: {} + }; } /** @@ -140,6 +146,19 @@ export class EventManager return this._mouseInfo; } + /** + * Returns all the data gathered about touches. + * + * @name module:core.EventManager#getTouchInfo + * @function + * @public + * @return {object} the touch info. + */ + getTouchInfo () + { + return this._touchInfo; + } + /** * Clear all events from the event buffer. * @@ -200,7 +219,6 @@ export class EventManager self._mouseInfo.buttons.pressed[event.button] = 1; self._mouseInfo.buttons.times[event.button] = self._psychoJS._monotonicClock.getTime() - self._mouseInfo.buttons.clocks[event.button].getLastResetTime(); - self._mouseInfo.pos = [event.offsetX, event.offsetY]; this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button down, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")"); @@ -212,10 +230,21 @@ export class EventManager self._mouseInfo.buttons.pressed[0] = 1; self._mouseInfo.buttons.times[0] = self._psychoJS._monotonicClock.getTime() - self._mouseInfo.buttons.clocks[0].getLastResetTime(); + self._mouseInfo.pos = [event.changedTouches[0].pageX, event.changedTouches[0].pageY]; - // we use the first touch, discarding all others: - const touches = event.changedTouches; - self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY]; + this._touchInfo.touchesArray = new Array(event.touches.length); + this._touchInfo.touchesMap = {}; + let i; + for (i = 0; i < event.touches.length; i++) + { + this._touchInfo.touchesArray[i] = { + id: event.touches[i].identifier, + force: event.touches[i].force, + pos: [event.touches[i].pageX, event.touches[i].pageY], + busy: false + }; + this._touchInfo.touchesMap[event.touches[i].identifier] = this._touchInfo.touchesArray[i]; + } this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button down, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")"); }, false); @@ -249,10 +278,20 @@ export class EventManager self._mouseInfo.buttons.pressed[0] = 0; self._mouseInfo.buttons.times[0] = self._psychoJS._monotonicClock.getTime() - self._mouseInfo.buttons.clocks[0].getLastResetTime(); + self._mouseInfo.pos = [event.changedTouches[0].pageX, event.changedTouches[0].pageY]; - // we use the first touch, discarding all others: - const touches = event.changedTouches; - self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY]; + this._touchInfo.touchesArray = new Array(event.touches.length); + this._touchInfo.touchesMap = {}; + let i; + for (i = 0; i < event.touches.length; i++) + { + this._touchInfo.touchesArray[i] = { + id: event.touches[i].identifier, + force: event.touches[i].force, + pos: [event.touches[i].pageX, event.touches[i].pageY] + }; + this._touchInfo.touchesMap[event.touches[i].identifier] = this._touchInfo.touchesArray[i]; + } this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button up, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")"); }, false); @@ -270,10 +309,20 @@ export class EventManager event.preventDefault(); self._mouseInfo.moveClock.reset(); + self._mouseInfo.pos = [event.changedTouches[0].pageX, event.changedTouches[0].pageY]; - // we use the first touch, discarding all others: - const touches = event.changedTouches; - self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY]; + this._touchInfo.touchesArray = new Array(event.touches.length); + this._touchInfo.touchesMap = {}; + let i; + for (i = 0; i < event.touches.length; i++) + { + this._touchInfo.touchesArray[i] = { + id: event.touches[i].identifier, + force: event.touches[i].force, + pos: [event.touches[i].pageX, event.touches[i].pageY] + }; + this._touchInfo.touchesMap[event.touches[i].identifier] = this._touchInfo.touchesArray[i]; + } }, false); // (*) wheel @@ -302,7 +351,13 @@ export class EventManager { const timestamp = MonotonicClock.getReferenceTime(); - let code = event.code; + // Note: we are using event.key since we are interested in the input character rather than + // the physical key position on the keyboard, i.e. we need to take into account the keyboard + // layout + // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding + // event.code's lack of suitability + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge): if (typeof code === "undefined") diff --git a/src/core/GUI.js b/src/core/GUI.js index 90bd3d1..e3cc571 100644 --- a/src/core/GUI.js +++ b/src/core/GUI.js @@ -50,6 +50,9 @@ export class GUI { this._psychoJS = psychoJS; + // info fields excluded from the GUI: + this._excludedInfo = {}; + // gui listens to RESOURCE events from the server manager: psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) => { @@ -87,9 +90,6 @@ export class GUI requireParticipantClick = GUI.DEFAULT_SETTINGS.DlgFromDict.requireParticipantClick }) { - // get info from URL: - const infoFromUrl = util.getUrlParameters(); - this._progressBarMax = 0; this._allResourcesDownloaded = false; this._requiredKeys = []; @@ -113,6 +113,19 @@ export class GUI self._dialogComponent.tStart = t; self._dialogComponent.status = PsychoJS.Status.STARTED; + // prepare the info fields excluded from the GUI, including those from the URL: + const excludedInfo = {}; + for (let key in self._excludedInfo) + { + excludedInfo[key.trim().toLowerCase()] = self._excludedInfo[key]; + } + const infoFromUrl = util.getUrlParameters(); + infoFromUrl.forEach((value, key) => + { + excludedInfo[key.trim().toLowerCase()] = value; + }); + + // if the experiment is licensed, and running on the license rather than on credit, // we use the license logo: if (self._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER @@ -130,7 +143,13 @@ export class GUI markup += "
"; // alert title and close button: - markup += `

${title}

`; + markup += "
"; + markup += `

${title}

`; + markup += ""; + markup += "
"; + + // everything above the buttons is in a scrollable container: + markup += "
"; // logo, if need be: if (typeof logoUrl === "string") @@ -139,14 +158,16 @@ export class GUI } // add a combobox or text areas for each entry in the dictionary: + let atLeastOneIncludedKey = false; Object.keys(dictionary).forEach((key, keyIdx) => { const value = dictionary[key]; const keyId = "form-input-" + keyIdx; // only create an input if the key is not in the URL: - let inUrl = false; const cleanedDictKey = key.trim().toLowerCase(); + const isIncluded = !(cleanedDictKey in excludedInfo); + /*let inUrl = false; infoFromUrl.forEach((urlValue, urlKey) => { const cleanedUrlKey = urlKey.trim().toLowerCase(); @@ -155,10 +176,13 @@ export class GUI inUrl = true; // break; } - }); + });*/ - if (!inUrl) + if (isIncluded) + // if (!inUrl) { + atLeastOneIncludedKey = true; + markup += ``; // if the field is required: @@ -185,7 +209,7 @@ export class GUI markup += ""; } - // otherwise we use a single string input: + // otherwise we use a single string input: //if (typeof value === 'string') else { @@ -199,17 +223,27 @@ export class GUI markup += "

Fields marked with an asterisk (*) are required.

"; } + markup += "
"; // scrollable-container + + // separator, if need be: + if (atLeastOneIncludedKey) + { + markup += "
"; + } + // progress bar: - markup += `
${self._progressMessage}
`; + markup += `
${self._progressMessage}
`; markup += "
"; // buttons: markup += "
"; + markup += "
"; markup += ""; if (self._requireParticipantClick) { markup += ""; } + markup += "
"; // button-group markup += "
"; @@ -346,14 +380,18 @@ export class GUI { const error = this._userFriendlyError(errorCode); markup += `

${error.title}

`; + markup += "
"; markup += `

${error.text}

`; + markup += "
"; } else { markup += `

Error

`; + markup += "
"; markup += `

Unfortunately we encountered the following error:

`; markup += stackCode; markup += "

Try to run the experiment again. If the error persists, contact the experiment designer.

"; + markup += "
"; } } @@ -361,27 +399,36 @@ export class GUI else if (typeof warning !== "undefined") { markup += `

Warning

`; + markup += "
"; markup += `

${warning}

`; + markup += "
"; } // we are displaying a message: else if (typeof message !== "undefined") { - markup += `

Message

`; + markup += "

Message

"; + markup += "
"; markup += `

${message}

`; + markup += "
"; } if (showOK || showCancel) { markup += "
"; } - if (showCancel) + if (showCancel || showOK) { - markup += ""; - } - if (showOK) - { - markup += ""; + markup += "
"; + if (showCancel) + { + markup += ""; + } + if (showOK) + { + markup += ""; + } + markup += "
"; // button-group } markup += ""; diff --git a/src/core/Keyboard.js b/src/core/Keyboard.js index dd2427d..56df760 100644 --- a/src/core/Keyboard.js +++ b/src/core/Keyboard.js @@ -354,7 +354,13 @@ export class Keyboard extends PsychObject */ self._previousKeydownKey = event.key; - let code = event.code; + // Note: we are using event.key since we are interested in the input character rather than + // the physical key position on the keyboard, i.e. we need to take into account the keyboard + // layout + // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding + // event.code's lack of suitability + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge): if (typeof code === "undefined") @@ -394,7 +400,9 @@ export class Keyboard extends PsychObject self._previousKeydownKey = undefined; - let code = event.code; + // Note: see above for explanation regarding the use of event.key in lieu of event.code + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft Edge: if (typeof code === "undefined") diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index ef5ef7f..c8ca9d1 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -530,6 +530,7 @@ export class PsychoJS const response = { origin: "PsychoJS.quit", context: "when terminating the experiment" }; this._experiment.experimentEnded = true; + this._experiment.isCompleted = isCompleted; this.status = PsychoJS.Status.STOPPED; const isServerEnv = (this.getEnvironment() === ExperimentHandler.Environment.SERVER); @@ -601,7 +602,7 @@ export class PsychoJS if (showOK) { - let text = "Thank you for your patience.

"; + let text = "Thank you for your patience."; text += (typeof message !== "undefined") ? message : "Goodbye!"; this._gui.dialog({ message: text, @@ -789,7 +790,7 @@ export class PsychoJS const self = this; window.onerror = function(message, source, lineno, colno, error) - {console.log('@@@', message) + { // check for ResizeObserver loop limit exceeded error: // ref: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded if (message === "ResizeObserver loop limit exceeded" || diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index e92a53c..b785743 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -315,6 +315,74 @@ export class ServerManager extends PsychObject return pathStatusData.data; } + /** + * Get full data of a resource. + * + * @name module:core.ServerManager#getFullResourceData + * @function + * @public + * @param {string} name - name of the requested resource + * @param {boolean} [errorIfNotDownloaded = false] whether or not to throw an exception if the + * resource status is not DOWNLOADED + * @return {Object} full available data for resource, or undefined if the resource has been registered + * but not downloaded yet. + * @throws {Object.} exception if no resource with that name has previously been registered + */ + getFullResourceData (name, errorIfNotDownloaded = false) + { + const response = { + origin: "ServerManager.getResource", + context: "when getting the value of resource: " + name, + }; + + const pathStatusData = this._resources.get(name); + + if (typeof pathStatusData === "undefined") + { + + // throw { ...response, error: 'unknown resource' }; + throw Object.assign(response, { error: "unknown resource" }); + } + + if (errorIfNotDownloaded && pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED) + { + throw Object.assign(response, { + error: name + " is not available for use (yet), its current status is: " + + util.toString(pathStatusData.status), + }); + } + + return pathStatusData; + } + + /** + * Release a resource. + * + * @param {string} name - the name of the resource to release + * @return {boolean} true if a resource with the given name was previously registered with the manager, + * false otherwise. + */ + releaseResource(name) + { + const response = { + origin: "ServerManager.releaseResource", + context: "when releasing resource: " + name, + }; + + const pathStatusData = this._resources.get(name); + + if (typeof pathStatusData === "undefined") + { + return false; + } + + // TODO check the current status: prevent the release of a resources currently downloading + + this._psychoJS.logger.debug(`releasing resource: ${name}`); + this._resources.delete(name); + return true; + } + /** * Get the status of a single resource or the reduced status of an array of resources. * @@ -507,18 +575,18 @@ export class ServerManager extends PsychObject // pre-process the resources: for (let r = 0; r < resources.length; ++r) { - const resource = resources[r]; - // convert those resources that are only a string to an object with name and path: - if (typeof resource === "string") + if (typeof resources[r] === "string") { resources[r] = { - name: resource, - path: resource, + name: resources[r], + path: resources[r], download: true }; } + const resource = resources[r]; + // deal with survey models: if ("surveyId" in resource) { @@ -635,6 +703,19 @@ export class ServerManager extends PsychObject } } + cacheResourceData (name, dataToCache) + { + const pathStatusData = this._resources.get(name); + + if (typeof pathStatusData === "undefined") + { + // throw { ...response, error: 'unknown resource' }; + throw Object.assign(response, { error: "unknown resource" }); + } + + pathStatusData.cachedData = dataToCache; + } + /** * Block the experiment until the specified resources have been downloaded. * @@ -1256,7 +1337,7 @@ export class ServerManager extends PsychObject } // preload.js with forced binary: - if (["csv", "odp", "xls", "xlsx", "json"].indexOf(extension) > -1) + if (["csv", "odp", "xls", "xlsx", "json", "gif"].indexOf(extension) > -1) { preloadManifest.push(/*new createjs.LoadItem().set(*/ { id: name, @@ -1284,7 +1365,7 @@ export class ServerManager extends PsychObject } // font files: - else if (["ttf", "otf", "woff", "woff2"].indexOf(extension) > -1) + else if (["ttf", "otf", "woff", "woff2", "eot"].indexOf(extension) > -1) { fontResources.push(name); } @@ -1332,7 +1413,7 @@ export class ServerManager extends PsychObject preloadManifest.push(/*new createjs.LoadItem().set(*/ { id: name, src: pathStatusData.path, - crossOrigin: "Anonymous", + crossOrigin: "Anonymous" } /*)*/); } } diff --git a/src/core/Window.js b/src/core/Window.js index cb6acbe..f699154 100644 --- a/src/core/Window.js +++ b/src/core/Window.js @@ -13,6 +13,7 @@ import { MonotonicClock } from "../util/Clock.js"; import { Color } from "../util/Color.js"; import { PsychObject } from "../util/PsychObject.js"; import { Logger } from "./Logger.js"; +import { hasTouchScreen } from "../util/Util.js"; /** *

Window displays the various stimuli of the experiment.

@@ -58,6 +59,8 @@ export class Window extends PsychObject * @param {string} [options.name] the name of the window * @param {boolean} [options.fullscr= false] whether or not to go fullscreen * @param {Color} [options.color= Color('black')] the background color of the window + * @param {string | HTMLImageElement} [options.backgroundImage = ""] - background image of the window. + * @param {string} [options.backgroundFit = "cover"] - how to fit background image in the window. * @param {number} [options.gamma= 1] sets the divisor for gamma correction. In other words gamma correction is calculated as pow(rgb, 1/gamma) * @param {number} [options.contrast= 1] sets the contrast value * @param {string} [options.units= 'pix'] the units of the window @@ -70,6 +73,8 @@ export class Window extends PsychObject name, fullscr = false, color = new Color("black"), + backgroundImage = "", + backgroundFit = "cover", gamma = 1, contrast = 1, units = "pix", @@ -92,11 +97,7 @@ export class Window extends PsychObject this._drawList = []; this._addAttribute("fullscr", fullscr); - this._addAttribute("color", color, new Color("black"), () => { - if (this._backgroundSprite) { - this._backgroundSprite.tint = this._color.int; - } - }); + this._addAttribute("color", color, new Color("black")); this._addAttribute("gamma", gamma, 1, () => { this._adjustmentFilter.gamma = this._gamma; }); @@ -107,6 +108,8 @@ export class Window extends PsychObject this._addAttribute("waitBlanking", waitBlanking); this._addAttribute("autoLog", autoLog); this._addAttribute("size", []); + this._addAttribute("backgroundImage", backgroundImage, ""); + this._addAttribute("backgroundFit", backgroundFit, "cover"); // setup PIXI: this._setupPixi(); @@ -138,6 +141,12 @@ export class Window extends PsychObject } } + static BACKGROUND_FIT_ENUM = { + cover: 0, + contain: 1, + auto: 2 + }; + /** * Close the window. * @@ -151,7 +160,7 @@ export class Window extends PsychObject } this._rootContainer.destroy(); - + if (document.body.contains(this._renderer.view)) { document.body.removeChild(this._renderer.view); @@ -165,7 +174,6 @@ export class Window extends PsychObject } this._renderer.destroy(); - window.removeEventListener("resize", this._resizeCallback); window.removeEventListener("orientationchange", this._resizeCallback); @@ -181,7 +189,7 @@ export class Window extends PsychObject { // gets updated frame by frame const lastDelta = this.psychoJS.scheduler._lastDelta; - const fps = lastDelta === 0 ? 60.0 : 1000 / lastDelta; + const fps = (lastDelta === 0) ? 60.0 : (1000.0 / lastDelta); return fps; } @@ -315,7 +323,7 @@ export class Window extends PsychObject */ removePixiObject(pixiObject) { - this._stimsContainer.removeChild(pixiObject); + this._stimsContainer.removeChild(pixiObject); } /** @@ -360,6 +368,134 @@ export class Window extends PsychObject this._refresh(); } + /** + * Set background image of the window. + * + * @name module:core.Window#setBackgroundImage + * @function + * @public + * @param {string} backgroundImage - name of the image resource (should be one specified in resource manager) + * @param {boolean} log - whether or not to log + */ + setBackgroundImage (backgroundImage = "", log = false) + { + this._setAttribute("backgroundImage", backgroundImage, log); + + if (this._backgroundSprite === undefined) + { + return; + } + + let imgResource = backgroundImage; + + if (this._backgroundSprite instanceof PIXI.Sprite && this._backgroundSprite.texture !== PIXI.Texture.WHITE) + { + this._backgroundSprite.texture.destroy(true); + } + + if (typeof backgroundImage === "string" && backgroundImage.length > 0) + { + imgResource = this.psychoJS.serverManager.getResource(backgroundImage); + } + + if (imgResource instanceof HTMLImageElement) + { + const texOpts = + { + scaleMode: PIXI.SCALE_MODES.LINEAR + }; + this._backgroundSprite.texture = new PIXI.Texture(new PIXI.BaseTexture(imgResource, texOpts)); + this._backgroundSprite.tint = 0xffffff; + this.backgroundFit = this._backgroundFit; + } + else + { + this._backgroundSprite.texture = PIXI.Texture.WHITE; + this._backgroundSprite.width = this._size[0]; + this._backgroundSprite.height = this._size[1]; + this._backgroundSprite.anchor.set(.5); + this.color = this._color; + } + } + + /** + * Set fit mode for background image of the window. + * + * @name module:core.Window#setBackgroundFit + * @function + * @public + * @param {string} [backgroundFit = "cover"] - fit mode for background image ["cover", "contain", "scaledown", "none"]. + * @param {boolean} log - whether or not to log + */ + setBackgroundFit (backgroundFit = "cover", log = false) + { + if (this._backgroundImage === "") + { + return; + } + + const backgroundFitCode = Window.BACKGROUND_FIT_ENUM[backgroundFit.replace("-", "").toLowerCase()]; + + if (backgroundFitCode === undefined) + { + return; + } + + this._setAttribute("backgroundFit", backgroundFit, log); + const backgroundAspectRatio = this._backgroundSprite.texture.width / this._backgroundSprite.texture.height; + const windowAspectRatio = this._size[0] / this._size[1]; + + if (backgroundFitCode === Window.BACKGROUND_FIT_ENUM.cover) + { + if (windowAspectRatio >= backgroundAspectRatio) + { + this._backgroundSprite.width = this._size[0]; + this._backgroundSprite.height = this._size[0] / backgroundAspectRatio; + } + else + { + this._backgroundSprite.height = this._size[1]; + this._backgroundSprite.width = this._size[1] * backgroundAspectRatio; + } + } + else if (backgroundFitCode === Window.BACKGROUND_FIT_ENUM.contain) + { + if (windowAspectRatio >= backgroundAspectRatio) + { + this._backgroundSprite.height = this._size[1]; + this._backgroundSprite.width = this._size[1] * backgroundAspectRatio; + } + else + { + this._backgroundSprite.width = this._size[0]; + this._backgroundSprite.height = this._size[0] / backgroundAspectRatio; + } + } + else if (backgroundFitCode === Window.BACKGROUND_FIT_ENUM.auto) + { + this._backgroundSprite.width = this._backgroundSprite.texture.width; + this._backgroundSprite.height = this._backgroundSprite.texture.height; + } + } + + /** + * Set foreground color value for the window. + * + * @name module:visual.Window#setColor + * @public + * @param {Color} colorVal - color value, can be String like "red" or "#ff0000" or Number like 0xff0000. + * @param {boolean} [log= false] - whether of not to log + */ + setColor(colorVal = "white", log = false) + { + const colorObj = (colorVal instanceof Color) ? colorVal : new Color(colorVal, Color.COLOR_SPACE.RGB); + this._setAttribute("color", colorObj, log); + if (this._backgroundSprite && !this._backgroundImage) + { + this._backgroundSprite.tint = this._color.int; + } + } + /** * Update this window, if need be. * @@ -372,7 +508,7 @@ export class Window extends PsychObject if (this._renderer) { this._renderer.backgroundColor = this._color.int; - this._backgroundSprite.tint = this._color.int; + this.color = this._color; } // we also change the background color of the body since @@ -466,6 +602,7 @@ export class Window extends PsychObject // background sprite so that if we need to move all stims at once, the background sprite // won't get affected. this._backgroundSprite = new PIXI.Sprite(PIXI.Texture.WHITE); + this._backgroundSprite.scale.y = -1; this._backgroundSprite.tint = this.color.int; this._backgroundSprite.width = this._size[0]; this._backgroundSprite.height = this._size[1]; @@ -476,11 +613,11 @@ export class Window extends PsychObject // create a top-level PIXI container: this._rootContainer = new PIXI.Container(); this._rootContainer.addChild(this._backgroundSprite, this._stimsContainer); - + // sorts children according to their zIndex value. Higher zIndex means it will be moved towards the end of the array, // and thus rendered on top of previous one. this._rootContainer.sortableChildren = true; - + this._rootContainer.interactive = true; this._rootContainer.filters = [this._adjustmentFilter]; @@ -489,17 +626,7 @@ export class Window extends PsychObject // touch/mouse events are treated by PsychoJS' event manager: this.psychoJS.eventManager.addMouseListeners(this._renderer); - - // update the renderer size and the Window's stimuli whenever the browser's size or orientation change: - this._resizeCallback = (e) => - { - Window._resizePixiRenderer(this, e); - this._backgroundSprite.width = this._size[0]; - this._backgroundSprite.height = this._size[1]; - this._fullRefresh(); - }; - window.addEventListener("resize", this._resizeCallback); - window.addEventListener("orientationchange", this._resizeCallback); + this._addEventListeners(); } /** @@ -532,6 +659,87 @@ export class Window extends PsychObject pjsWindow._rootContainer.scale.y = -1; } + _handlePointerDown (e) + { + let i; + let pickedPixi; + let tmpPoint = new PIXI.Point(); + const cursorPos = new PIXI.Point(e.pageX, e.pageY); + for (i = this._stimsContainer.children.length - 1; i >= 0; i--) + { + if (typeof this._stimsContainer.children[i].containsPoint === "function" && + this._stimsContainer.children[i].containsPoint(cursorPos)) + { + pickedPixi = this._stimsContainer.children[i]; + break; + } + else if (this._stimsContainer.children[i].containsPoint === undefined && + this._stimsContainer.children[i] instanceof PIXI.DisplayObject) + { + this._stimsContainer.children[i].worldTransform.applyInverse(cursorPos, tmpPoint); + if (this._stimsContainer.children[i].getLocalBounds().contains(tmpPoint.x, tmpPoint.y)) + { + pickedPixi = this._stimsContainer.children[i]; + break; + } + } + } + this.emit("pointerdown", { + pixi: pickedPixi, + originalEvent: e + }); + } + + _handlePointerUp (e) + { + this.emit("pointerup", { + originalEvent: e + }); + } + + _handlePointerMove (e) + { + this.emit("pointermove", { + originalEvent: e + }); + } + + _addEventListeners () + { + this._renderer.view.addEventListener("pointerdown", this._handlePointerDown.bind(this)); + this._renderer.view.addEventListener("pointerup", this._handlePointerUp.bind(this)); + this._renderer.view.addEventListener("pointermove", this._handlePointerMove.bind(this)); + + // update the renderer size and the Window's stimuli whenever the browser's size or orientation change: + this._resizeCallback = (e) => + { + // if the user device is a mobile phone or tablet (we use the presence of a touch screen as a + // proxy), we need to detect whether the change in size is due to the appearance of a virtual keyboard + // in which case we do not want to resize the canvas. This is rather tricky and so we resort to + // the below trick. It would be better to use the VirtualKeyboard API, but it is not widely + // available just yet, as of 2023-06. + const keyboardHeight = 300; + if (hasTouchScreen() && (window.screen.height - window.visualViewport.height) > keyboardHeight) + { + return; + } + + Window._resizePixiRenderer(this, e); + if (this._backgroundImage === undefined) + { + this._backgroundSprite.width = this._size[0]; + this._backgroundSprite.height = this._size[1]; + } + else + { + this.backgroundFit = this._backgroundFit; + } + this._fullRefresh(); + }; + window.addEventListener("resize", this._resizeCallback); + window.addEventListener("orientationchange", this._resizeCallback); + } + /** * Send all logged messages to the {@link Logger}. * diff --git a/src/data/ExperimentHandler.js b/src/data/ExperimentHandler.js index 7a30578..97692b5 100644 --- a/src/data/ExperimentHandler.js +++ b/src/data/ExperimentHandler.js @@ -276,6 +276,7 @@ export class ExperimentHandler extends PsychObject } let data = this._trialsData; + // if the experiment data have to be cleared, we first make a copy of them: if (clear) { @@ -351,6 +352,19 @@ export class ExperimentHandler extends PsychObject } } + /** + * Get the results of the experiment as a .csv string, ready to be uploaded or stored. + * + * @return {string} a .csv representation of the experiment results. + */ + getResultAsCsv() + { + // note: we use the XLSX library as it automatically deals with header, takes care of quotes, + // newlines, etc. + const worksheet = XLSX.utils.json_to_sheet(this._trialsData); + return "\ufeff" + XLSX.utils.sheet_to_csv(worksheet); + } + /** * Get the attribute names and values for the current trial of a given loop. *

Only info relating to the trial execution are returned.

diff --git a/src/index.css b/src/index.css index c903ea8..9ed941f 100644 --- a/src/index.css +++ b/src/index.css @@ -13,7 +13,7 @@ body { /* Initialisation message (which will disappear behind the canvas) */ #root::after { - content: "initialising the experiment..."; + content: "initialising..."; position: fixed; top: 50%; left: 50%; @@ -26,13 +26,12 @@ body { /* Project and resource dialogs */ - .dialog-container label, .dialog-container input, .dialog-container select { - box-sizing: border-box; - display: block; - padding-bottom: 0.5em; + box-sizing: border-box; + display: block; + padding-bottom: 0.5em; } .dialog-container input.text, @@ -40,6 +39,13 @@ body { margin-bottom: 1em; padding: 0.5em; width: 100%; + + height: 34px; + border: 1px solid #767676; + border-radius: 2px; + background: #ffffff; + color: #333; + font-size: 14px; } .dialog-container fieldset { @@ -71,12 +77,19 @@ body { } .dialog-content { + display: flex; + flex-direction: column; + row-gap: 0; + margin: auto; z-index: 2; position: relative; width: 500px; max-width: 88vw; + /*max-height: 90vh;*/ + max-height: 93%; + padding: 0.5em; border-radius: 2px; @@ -88,11 +101,24 @@ body { box-shadow: 1px 1px 3px #555555; } +.dialog-content .scrollable-container { + height: 100%; + padding: 0 0.5em; + + overflow-x: hidden; + overflow-y: auto; +} + +.dialog-content hr { + width: 100%; +} + .dialog-title { padding: 0.5em; margin-bottom: 1em; - background-color: #009900; + background-color: #00dd00; + /*background-color: #009900;*/ border-radius: 2px; } @@ -111,6 +137,11 @@ body { } .dialog-close { + display: flex; + justify-content: center; + align-items: center; + line-height: 1.1em; + position: absolute; top: 0.7em; right: 0.7em; @@ -153,7 +184,7 @@ body { .dialog-button { padding: 0.5em 1em 0.5em 1em; - margin: 0.5em 0.5em 0.5em 0; + /*margin: 0.5em 0.5em 0.5em 0;*/ border: 1px solid #555555; border-radius: 2px; @@ -176,6 +207,14 @@ body { border: 1px solid #000000; } +.dialog-button-group { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + column-gap: 0.5em; +} + .disabled { border: 1px solid #AAAAAA; color: #AAAAAA; @@ -186,10 +225,15 @@ body { } .logo { - display: block; + display: flex; + flex: 0 1 auto; + height: 100%; + width: auto; + + /*display: block; margin: 0 auto 1em; max-height: 20vh; - max-width: 100%; + max-width: 100%;*/ } a, @@ -204,3 +248,30 @@ a:hover { color: #000; } +.yt-iframe { + display: block; + position: absolute; + border: none; +} + +.yt-player-wrapper { + display: flex; + justify-content: center; + align-items: center; +} + +.yt-player-wrapper.hidden { + display: none; +} + +.yt-player-wrapper.inprogress:after { + content: "loading youtube..."; + display: flex; + position: absolute; + color: white; + background: black; + padding: 10px; + justify-content: center; + align-items: center; +} + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..b5170ad --- /dev/null +++ b/src/index.html @@ -0,0 +1,14 @@ + + + + + + Test Experiment + + + + + +
+ + diff --git a/src/test_resources/cool.gif b/src/test_resources/cool.gif new file mode 100644 index 0000000..47ae578 Binary files /dev/null and b/src/test_resources/cool.gif differ diff --git a/src/test_resources/delorean.gif b/src/test_resources/delorean.gif new file mode 100644 index 0000000..cd38909 Binary files /dev/null and b/src/test_resources/delorean.gif differ diff --git a/src/util/Clock.js b/src/util/Clock.js index 3e92b5d..f0e7874 100644 --- a/src/util/Clock.js +++ b/src/util/Clock.js @@ -90,6 +90,7 @@ export class MonotonicClock { // yyyy-mm-dd, hh:mm:ss.sss return MonotonicClock.getDate() + .replaceAll("/","-") // yyyy-mm-dd_hh:mm:ss.sss .replace(", ", "_") // yyyy-mm-dd_hh[h]mm:ss.sss diff --git a/src/util/GifParser.js b/src/util/GifParser.js new file mode 100644 index 0000000..535201b --- /dev/null +++ b/src/util/GifParser.js @@ -0,0 +1,278 @@ +/** + * Tool for parsing gif files and decoding it's data to frames. + * + * @author "Matt Way" (https://github.com/matt-way), Nikita Agafonov (https://github.com/lightest) + * @copyright (c) 2015 Matt Way, (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * + * @note Based on https://github.com/matt-way/gifuct-js + * + */ + +import GIF from 'js-binary-schema-parser/lib/schemas/gif' +import { parse } from 'js-binary-schema-parser' +import { buildStream } from 'js-binary-schema-parser/lib/parsers/uint8' + +/** + * Deinterlace function from https://github.com/shachaf/jsgif + */ + +export const deinterlace = (pixels, width) => { + const newPixels = new Array(pixels.length) + const rows = pixels.length / width + const cpRow = function(toRow, fromRow) { + const fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width) + newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels)) + } + + // See appendix E. + const offsets = [0, 4, 2, 1] + const steps = [8, 8, 4, 2] + + var fromRow = 0 + for (var pass = 0; pass < 4; pass++) { + for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) { + cpRow(toRow, fromRow) + fromRow++ + } + } + + return newPixels +} + + +/** + * javascript port of java LZW decompression + * Original java author url: https://gist.github.com/devunwired/4479231 + */ + +export const lzw = (minCodeSize, data, pixelCount, memoryBuffer, bufferOffset) => { + const MAX_STACK_SIZE = 4096 + const nullCode = -1 + const npix = pixelCount + var available, + clear, + code_mask, + code_size, + end_of_information, + in_code, + old_code, + bits, + code, + i, + datum, + data_size, + first, + top, + bi, + pi + + // const dstPixels = new Array(pixelCount) + // const prefix = new Array(MAX_STACK_SIZE) + // const suffix = new Array(MAX_STACK_SIZE) + // const pixelStack = new Array(MAX_STACK_SIZE + 1) + + const dstPixels = new Uint8Array(memoryBuffer, bufferOffset, pixelCount) + const prefix = new Uint16Array(MAX_STACK_SIZE) + const suffix = new Uint16Array(MAX_STACK_SIZE) + const pixelStack = new Uint8Array(MAX_STACK_SIZE + 1) + + // Initialize GIF data stream decoder. + data_size = minCodeSize + clear = 1 << data_size + end_of_information = clear + 1 + available = clear + 2 + old_code = nullCode + code_size = data_size + 1 + code_mask = (1 << code_size) - 1 + for (code = 0; code < clear; code++) { + // prefix[code] = 0 + suffix[code] = code + } + + // Decode GIF pixel stream. + var datum, bits, count, first, top, pi, bi + datum = bits = count = first = top = pi = bi = 0 + for (i = 0; i < npix; ) { + if (top === 0) { + if (bits < code_size) { + // get the next byte + datum += data[bi] << bits + + bits += 8 + bi++ + continue + } + // Get the next code. + code = datum & code_mask + datum >>= code_size + bits -= code_size + // Interpret the code + if (code > available || code == end_of_information) { + break + } + if (code == clear) { + // Reset decoder. + code_size = data_size + 1 + code_mask = (1 << code_size) - 1 + available = clear + 2 + old_code = nullCode + continue + } + if (old_code == nullCode) { + pixelStack[top++] = suffix[code] + old_code = code + first = code + continue + } + in_code = code + if (code == available) { + pixelStack[top++] = first + code = old_code + } + while (code > clear) { + pixelStack[top++] = suffix[code] + code = prefix[code] + } + + first = suffix[code] & 0xff + pixelStack[top++] = first + + // add a new string to the table, but only if space is available + // if not, just continue with current table until a clear code is found + // (deferred clear code implementation as per GIF spec) + if (available < MAX_STACK_SIZE) { + prefix[available] = old_code + suffix[available] = first + available++ + if ((available & code_mask) === 0 && available < MAX_STACK_SIZE) { + code_size++ + code_mask += available + } + } + old_code = in_code + } + // Pop a pixel off the pixel stack. + top-- + dstPixels[pi++] = pixelStack[top] + i++ + } + + // for (i = pi; i < npix; i++) { + // dstPixels[i] = 0 // clear missing pixels + // } + + return dstPixels +} + +export const parseGIF = arrayBuffer => { + const byteData = new Uint8Array(arrayBuffer) + return parse(buildStream(byteData), GIF) +} + +const generatePatch = image => { + const totalPixels = image.pixels.length + const patchData = new Uint8ClampedArray(totalPixels * 4) + for (var i = 0; i < totalPixels; i++) { + const pos = i * 4 + const colorIndex = image.pixels[i] + const color = image.colorTable[colorIndex] || [0, 0, 0] + patchData[pos] = color[0] + patchData[pos + 1] = color[1] + patchData[pos + 2] = color[2] + patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0 + } + + return patchData +} + +export const decompressFrame = (frame, gct, buildImagePatch, memoryBuffer, memoryOffset) => { + if (!frame.image) { + console.warn('gif frame does not have associated image.') + return + } + + const { image } = frame + + // get the number of pixels + const totalPixels = image.descriptor.width * image.descriptor.height + // do lzw decompression + var pixels = lzw(image.data.minCodeSize, image.data.blocks, totalPixels, memoryBuffer, memoryOffset) + + // deal with interlacing if necessary + if (image.descriptor.lct.interlaced) { + pixels = deinterlace(pixels, image.descriptor.width) + } + + const resultImage = { + pixels: pixels, + dims: { + top: frame.image.descriptor.top, + left: frame.image.descriptor.left, + width: frame.image.descriptor.width, + height: frame.image.descriptor.height + } + } + + // color table + if (image.descriptor.lct && image.descriptor.lct.exists) { + resultImage.colorTable = image.lct + } else { + resultImage.colorTable = gct + } + + // add per frame relevant gce information + if (frame.gce) { + resultImage.delay = (frame.gce.delay || 10) * 10 // convert to ms + resultImage.disposalType = frame.gce.extras.disposal + // transparency + if (frame.gce.extras.transparentColorGiven) { + resultImage.transparentIndex = frame.gce.transparentColorIndex + } + } + + // create canvas usable imagedata if desired + if (buildImagePatch) { + resultImage.patch = generatePatch(resultImage) + } + + return resultImage +} + +export const decompressFrames = (parsedGif, buildImagePatches) => { + // return parsedGif.frames + // .filter(f => f.image) + // .map(f => decompressFrame(f, parsedGif.gct, buildImagePatches)) + let totalPixels = 0; + let framesWithData = 0; + let out ; + let i, j = 0; + + for (i = 0; i < parsedGif.frames.length; i++) { + if (parsedGif.frames[i].image) + { + totalPixels += parsedGif.frames[i].image.descriptor.width * parsedGif.frames[i].image.descriptor.height; + framesWithData++; + } + } + + // const dstPixels = new Uint16Array(totalPixels); + // let frameStart = 0; + // let frameEnd = 0; + + const buf = new ArrayBuffer(totalPixels); + let bufOffset = 0; + out = new Array(framesWithData); + + for (i = 0; i < parsedGif.frames.length; i++) { + if (parsedGif.frames[i].image) + { + out[j] = decompressFrame(parsedGif.frames[i], parsedGif.gct, buildImagePatches, buf, bufOffset); + bufOffset += parsedGif.frames[i].image.descriptor.width * parsedGif.frames[i].image.descriptor.height; + // out[j] = decompressFrame(parsedGif.frames[i], parsedGif.gct, buildImagePatches, prefix, suffix, pixelStack, dstPixels, frameStart, frameEnd); + j++; + } + } + + return out; +} diff --git a/src/util/Scheduler.js b/src/util/Scheduler.js index bad709c..4bbaf6e 100644 --- a/src/util/Scheduler.js +++ b/src/util/Scheduler.js @@ -117,9 +117,12 @@ export class Scheduler * Start this scheduler. * *

Note: tasks are run after each animation frame.

+ * + * @return {Promise} a promise resolved when the scheduler stops, e.g. when the experiments finishes */ start() { + let shedulerResolve; const self = this; const update = async (timestamp) => { @@ -127,6 +130,7 @@ export class Scheduler if (self._stopAtNextUpdate) { self._status = Scheduler.Status.STOPPED; + shedulerResolve(); return; } @@ -137,6 +141,7 @@ export class Scheduler if (state === Scheduler.Event.QUIT) { self._status = Scheduler.Status.STOPPED; + shedulerResolve(); return; } @@ -155,6 +160,12 @@ export class Scheduler // start the animation: requestAnimationFrame(update); + + // return a promise resolved when the scheduler is stopped: + return new Promise((resolve, _) => + { + shedulerResolve = resolve; + }); } /** diff --git a/src/util/Util.js b/src/util/Util.js index 02a6133..1e4d2a5 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -322,27 +322,64 @@ export function IsPointInsidePolygon(point, vertices) } /** - * Shuffle an array in place using the Fisher-Yastes's modern algorithm + * Shuffle an array, or a portion of that array, in place using the Fisher-Yastes's modern algorithm *

See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm

* * @param {Object[]} array - the input 1-D array - * @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random + * @param {Function} [randomNumberGenerator= undefined] - A function used to generated random numbers in the interval [0, 1). Defaults to Math.random + * @param [startIndex= undefined] - start index in the array + * @param [endIndex= undefined] - end index in the array * @return {Object[]} the shuffled array */ -export function shuffle(array, randomNumberGenerator = undefined) +export function shuffle(array, randomNumberGenerator = undefined, startIndex = undefined, endIndex = undefined) { - if (randomNumberGenerator === undefined) + // if array is not an array, we return it untouched rather than throwing an exception: + if (!array || !Array.isArray(array)) + { + return array; + } + + if (typeof startIndex === "undefined") + { + startIndex = 0; + } + if (typeof endIndex === "undefined") + { + endIndex = array.length - 1; + } + + if (typeof randomNumberGenerator === "undefined") { randomNumberGenerator = Math.random; } - for (let i = array.length - 1; i > 0; i--) + + for (let i = endIndex; i > startIndex; i--) { const j = Math.floor(randomNumberGenerator() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } + return array; } +/** + * linspace + * + * @name module:util.linspace + * @function + * @public + * @param {Object[]} startValue, stopValue, cardinality + * @return {Object[]} an array from startValue to stopValue with cardinality steps + */ +export function linspace(startValue, stopValue, cardinality) { + var arr = []; + var step = (stopValue - startValue) / (cardinality - 1); + for (var i = 0; i < cardinality; i++) { + arr.push(startValue + (step * i)); + } + return arr; +} + /** * Pick a random value from an array, uses `util.shuffle` to shuffle the array and returns the last value. * @@ -610,6 +647,11 @@ export function toString(object) return object.toString(); } + if (typeof object === "function") + { + return ``; + } + try { const symbolReplacer = (key, value) => @@ -1436,6 +1478,47 @@ export function loadCss(cssId, cssPath) } } +/** + * Whether the user device has a touchscreen, e.g. it is a mobile phone or tablet. + * + * @return {boolean} true if the user device has a touchscreen. + * @note the code below is directly adapted from MDN + */ +export function hasTouchScreen() +{ + let hasTouchScreen = false; + + if ("maxTouchPoints" in navigator) + { + hasTouchScreen = navigator.maxTouchPoints > 0; + } + else if ("msMaxTouchPoints" in navigator) + { + hasTouchScreen = navigator.msMaxTouchPoints > 0; + } + else + { + const mQ = matchMedia?.("(pointer:coarse)"); + if (mQ?.media === "(pointer:coarse)") + { + hasTouchScreen = !!mQ.matches; + } + else if ("orientation" in window) + { + hasTouchScreen = true; + } + else + { + const UA = navigator.userAgent; + hasTouchScreen = + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || + /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA); + } + } + + return hasTouchScreen; +} + /** * Enum that stores possible text directions. * Note that Arabic is the same as RTL but added here to support PsychoPy's diff --git a/src/visual/AnimatedGIF.js b/src/visual/AnimatedGIF.js new file mode 100644 index 0000000..c324cde --- /dev/null +++ b/src/visual/AnimatedGIF.js @@ -0,0 +1,441 @@ +/** + * Animated gif sprite. + * + * @author Nikita Agafonov (https://github.com/lightest), Matt Karl (https://github.com/bigtimebuddy) + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * + * @note Based on https://github.com/pixijs/gif and heavily modified. + * + */ + +import * as PIXI from "pixi.js-legacy"; + +/** + * Runtime object to play animated GIFs. This object is similar to an AnimatedSprite. + * It support playback (seek, play, stop) as well as animation speed and looping. + */ +class AnimatedGIF extends PIXI.Sprite +{ + /** + * Default options for all AnimatedGIF objects. + * @property {PIXI.SCALE_MODES} [scaleMode=PIXI.SCALE_MODES.LINEAR] - Scale mode to use for the texture. + * @property {boolean} [loop=true] - To enable looping. + * @property {number} [animationSpeed=1] - Speed of the animation. + * @property {boolean} [autoUpdate=true] - Set to `false` to manage updates yourself. + * @property {boolean} [autoPlay=true] - To start playing right away. + * @property {Function} [onComplete=null] - The completed callback, optional. + * @property {Function} [onLoop=null] - The loop callback, optional. + * @property {Function} [onFrameChange=null] - The frame callback, optional. + * @property {number} [fps=PIXI.Ticker.shared.FPS] - Default FPS. + */ + static defaultOptions = { + scaleMode: PIXI.SCALE_MODES.LINEAR, + fps: PIXI.Ticker.shared.FPS, + loop: true, + animationSpeed: 1, + autoPlay: true, + autoUpdate: true, + onComplete: null, + onFrameChange: null, + onLoop: null + }; + + /** + * @param frames - Data of the GIF image. + * @param options - Options for the AnimatedGIF + */ + constructor(decompressedFrames, options) + { + // Get the options, apply defaults + const { scaleMode, width, height, ...rest } = Object.assign({}, + AnimatedGIF.defaultOptions, + options + ); + + super(new PIXI.Texture(PIXI.BaseTexture.fromBuffer(new Uint8Array(width * height * 4), width, height, options))); + this._name = options.name; + this._useFullFrames = false; + this._decompressedFrameData = decompressedFrames; + this._origDims = { width, height }; + let i, j, time = 0; + this._frameTimings = new Array(decompressedFrames.length); + for (i = 0; i < decompressedFrames.length; i++) + { + this._frameTimings[i] = + { + start: time, + end: time + decompressedFrames[i].delay + }; + time += decompressedFrames[i].delay; + } + this.duration = this._frameTimings[decompressedFrames.length - 1].end; + this._fullPixelData = []; + if (options.fullFrames !== undefined && options.fullFrames.length > 0) + { + this._fullPixelData = options.fullFrames; + this._useFullFrames = true; + } + this._playing = false; + this._currentTime = 0; + this._isConnectedToTicker = false; + Object.assign(this, rest); + + // Draw the first frame + this.currentFrame = 0; + this._prevRenderedFrameIdx = -1; + if (this.autoPlay) + { + this.play(); + } + } + + static updatePixelsForOneFrame (decompressedFrameData, pixelBuffer, gifWidth) + { + let i = 0; + let patchRow = 0, patchCol = 0; + let offset = 0; + let colorData; + + if (decompressedFrameData.pixels.length === pixelBuffer.length / 4) + { + // Not all GIF files are perfectly optimized + // and instead of having tiny patch of pixels that actually changed from previous frame + // they would have a full next frame. + // Knowing that, we can go faster by skipping math needed to determine where to put new pixels + // and just place them 1 to 1 over existing frame (probably internal browser optimizations also kick in). + // For large amounts of gifs running simultaniously this results in 58+FPS vs 15-25+FPS for "else" case. + for (i = 0; i < decompressedFrameData.pixels.length; i++) { + if (decompressedFrameData.pixels[i] !== decompressedFrameData.transparentIndex) { + colorData = decompressedFrameData.colorTable[decompressedFrameData.pixels[i]]; + offset = i * 4; + pixelBuffer[offset] = colorData[0]; + pixelBuffer[offset + 1] = colorData[1]; + pixelBuffer[offset + 2] = colorData[2]; + pixelBuffer[offset + 3] = 255; + } + } + } + else + { + for (i = 0; i < decompressedFrameData.pixels.length; i++) { + if (decompressedFrameData.pixels[i] !== decompressedFrameData.transparentIndex) { + colorData = decompressedFrameData.colorTable[decompressedFrameData.pixels[i]]; + patchRow = (i / decompressedFrameData.dims.width) | 0; + patchCol = i % decompressedFrameData.dims.width; + offset = (gifWidth * (decompressedFrameData.dims.top + patchRow) + decompressedFrameData.dims.left + patchCol) * 4; + pixelBuffer[offset] = colorData[0]; + pixelBuffer[offset + 1] = colorData[1]; + pixelBuffer[offset + 2] = colorData[2]; + pixelBuffer[offset + 3] = 255; + } + } + } + + } + + static computeFullFrames (decompressedFrames, gifWidth, gifHeight) + { + let t = performance.now(); + let i, j; + let patchRow = 0, patchCol = 0; + let offset = 0; + let colorData; + let pixelData = new Uint8Array(gifWidth * gifHeight * 4); + let fullPixelData = new Uint8Array(gifWidth * gifHeight * 4 * decompressedFrames.length); + for (i = 0; i < decompressedFrames.length; i++) + { + AnimatedGIF.updatePixelsForOneFrame(decompressedFrames[i], pixelData, gifWidth); + fullPixelData.set(pixelData, pixelData.length * i); + } + console.log("full frames construction time", performance.now() - t); + return fullPixelData; + } + + _constructNthFullFrame (desiredFrameIdx, prevRenderedFrameIdx, decompressedFrames, pixelBuffer) + { + let t = performance.now(); + // saving to variable instead of referencing object in the loop wins up to 5ms! + // (at the moment of development observed on Win10, Chrome 103.0.5060.114 (Official Build) (64-bit)) + const gifWidth = this._origDims.width; + let i; + for (i = prevRenderedFrameIdx + 1; i <= desiredFrameIdx; i++) + { + // this._updatePixelsForOneFrame(decompressedFrames[i], pixelBuffer); + AnimatedGIF.updatePixelsForOneFrame(decompressedFrames[i], pixelBuffer, gifWidth) + } + // console.log("constructed frames from", prevRenderedFrameIdx, "to", desiredFrameIdx, "(", desiredFrameIdx - prevRenderedFrameIdx, ")", performance.now() - t); + } + + /** Stops the animation. */ + stop() + { + if (!this._playing) + { + return; + } + + this._playing = false; + if (this._autoUpdate && this._isConnectedToTicker) + { + PIXI.Ticker.shared.remove(this.update, this); + this._isConnectedToTicker = false; + } + } + + /** Plays the animation. */ + play() + { + if (this._playing) + { + return; + } + + this._playing = true; + if (this._autoUpdate && !this._isConnectedToTicker) + { + PIXI.Ticker.shared.add(this.update, this, PIXI.UPDATE_PRIORITY.HIGH); + this._isConnectedToTicker = true; + } + + // If were on the last frame and stopped, play should resume from beginning + if (!this.loop && this.currentFrame === this._decompressedFrameData.length - 1) + { + this._currentTime = 0; + } + } + + /** + * Get the current progress of the animation from 0 to 1. + * @readonly + */ + get progress() + { + return this._currentTime / this.duration; + } + + /** `true` if the current animation is playing */ + get playing() + { + return this._playing; + } + + /** + * Updates the object transform for rendering. You only need to call this + * if the `autoUpdate` property is set to `false`. + * + * @param deltaTime - Time since last tick. + */ + update(deltaTime) + { + if (!this._playing) + { + return; + } + + const elapsed = this.animationSpeed * deltaTime / PIXI.settings.TARGET_FPMS; + const currentTime = this._currentTime + elapsed; + const localTime = currentTime % this.duration; + + const localFrame = this._frameTimings.findIndex((ft) => + ft.start <= localTime && ft.end > localTime); + + if (this._prevRenderedFrameIdx > localFrame) + { + this._prevRenderedFrameIdx = -1; + } + + if (currentTime >= this.duration) + { + if (this.loop) + { + this._currentTime = localTime; + this.updateFrameIndex(localFrame); + if (typeof this.onLoop === "function") + { + this.onLoop(); + } + } + else + { + this._currentTime = this.duration; + this.updateFrameIndex(this._decompressedFrameData.length - 1); + if (typeof this.onComplete === "function") + { + this.onComplete(); + } + this.stop(); + } + } + else + { + this._currentTime = localTime; + this.updateFrameIndex(localFrame); + } + } + + /** + * Redraw the current frame, is necessary for the animation to work when + */ + updateFrame() + { + // if (!this.dirty) + // { + // return; + // } + + if (this._prevRenderedFrameIdx === this._currentFrame) + { + return; + } + + // Update the current frame + if (this._useFullFrames) + { + this.texture.baseTexture.resource.data = new Uint8Array + ( + this._fullPixelData.buffer, this._currentFrame * this._origDims.width * this._origDims.height * 4, + this._origDims.width * this._origDims.height * 4 + ); + } + else + { + // this._updatePixelsForOneFrame(this._decompressedFrameData[this._currentFrame], this.texture.baseTexture.resource.data); + this._constructNthFullFrame(this._currentFrame, this._prevRenderedFrameIdx, this._decompressedFrameData, this.texture.baseTexture.resource.data); + } + + this.texture.update(); + // Mark as clean + // this.dirty = false; + this._prevRenderedFrameIdx = this._currentFrame; + } + + /** + * Renders the object using the WebGL renderer + * + * @param {PIXI.Renderer} renderer - The renderer + * @private + */ + _render(renderer) + { + let t = performance.now(); + this.updateFrame(); + // console.log("t2", this._name, performance.now() - t); + super._render(renderer); + } + + /** + * Renders the object using the WebGL renderer + * + * @param {PIXI.CanvasRenderer} renderer - The renderer + * @private + */ + _renderCanvas(renderer) + { + this.updateFrame(); + super._renderCanvas(renderer); + } + + /** + * Whether to use PIXI.Ticker.shared to auto update animation time. + * @default true + */ + get autoUpdate() + { + return this._autoUpdate; + } + + set autoUpdate(value) + { + if (value !== this._autoUpdate) + { + this._autoUpdate = value; + + if (!this._autoUpdate && this._isConnectedToTicker) + { + PIXI.Ticker.shared.remove(this.update, this); + this._isConnectedToTicker = false; + } + else if (this._autoUpdate && !this._isConnectedToTicker && this._playing) + { + PIXI.Ticker.shared.add(this.update, this); + this._isConnectedToTicker = true; + } + } + } + + /** Set the current frame number */ + get currentFrame() + { + return this._currentFrame; + } + + set currentFrame(value) + { + this.updateFrameIndex(value); + this._currentTime = this._frameTimings[value].start; + } + + /** Internally handle updating the frame index */ + updateFrameIndex(value) + { + if (value < 0 || value >= this._decompressedFrameData.length) + { + throw new Error(`Frame index out of range, expecting 0 to ${this.totalFrames}, got ${value}`); + } + if (this._currentFrame !== value) + { + this._currentFrame = value; + // this.dirty = true; + if (typeof this.onFrameChange === "function") + { + this.onFrameChange(value); + } + } + } + + /** + * Get the total number of frame in the GIF. + */ + get totalFrames() + { + return this._decompressedFrameData.length; + } + + /** Destroy and don't use after this. */ + destroy() + { + this.stop(); + super.destroy(true); + this._decompressedFrameData = null; + this._fullPixelData = null; + this.onComplete = null; + this.onFrameChange = null; + this.onLoop = null; + } + + /** + * Cloning the animation is a useful way to create a duplicate animation. + * This maintains all the properties of the original animation but allows + * you to control playback independent of the original animation. + * If you want to create a simple copy, and not control independently, + * then you can simply create a new Sprite, e.g. `const sprite = new Sprite(animation.texture)`. + */ + clone() + { + return new AnimatedGIF([...this._decompressedFrameData], { + autoUpdate: this._autoUpdate, + loop: this.loop, + autoPlay: this.autoPlay, + scaleMode: this.texture.baseTexture.scaleMode, + animationSpeed: this.animationSpeed, + width: this._origDims.width, + height: this._origDims.height, + onComplete: this.onComplete, + onFrameChange: this.onFrameChange, + onLoop: this.onLoop, + }); + } +} + +export { AnimatedGIF }; diff --git a/src/visual/ButtonStim.js b/src/visual/ButtonStim.js index c007b51..cadc819 100644 --- a/src/visual/ButtonStim.js +++ b/src/visual/ButtonStim.js @@ -9,6 +9,7 @@ import { Mouse } from "../core/Mouse.js"; import { TextBox } from "./TextBox.js"; +import * as util from "../util/Util"; /** *

ButtonStim visual stimulus.

@@ -32,11 +33,13 @@ export class ButtonStim extends TextBox * @param {Color} [options.borderColor= Color("white")] the border color * @param {Color} [options.borderWidth= 0] the border width * @param {number} [options.opacity= 1.0] - the opacity + * @param {number} [options.depth= 0] - the depth (i.e. the z order) * @param {number} [options.letterHeight= undefined] - the height of the text * @param {boolean} [options.bold= true] - whether or not the text is bold * @param {boolean} [options.italic= false] - whether or not the text is italic * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ constructor( { @@ -54,11 +57,15 @@ export class ButtonStim extends TextBox borderColor, borderWidth = 0, opacity, + depth, letterHeight, bold = true, italic, autoDraw, autoLog, + draggable, + boxFn, + multiline } = {}, ) { @@ -77,12 +84,16 @@ export class ButtonStim extends TextBox borderColor, borderWidth, opacity, + depth, letterHeight, + multiline, bold, italic, alignment: "center", autoDraw, autoLog, + draggable, + boxFn }); this.psychoJS.logger.debug("create a new Button with name: ", name); @@ -112,7 +123,7 @@ export class ButtonStim extends TextBox if (this._autoLog) { - this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`); } } diff --git a/src/visual/FaceDetector.js b/src/visual/FaceDetector.js index a4ce95c..92f518f 100644 --- a/src/visual/FaceDetector.js +++ b/src/visual/FaceDetector.js @@ -42,10 +42,11 @@ export class FaceDetector extends VisualStim * @param {number} [options.opacity= 1.0] - the opacity * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ - constructor({name, win, input, modelDir, faceApiUrl, units, ori, opacity, pos, size, autoDraw, autoLog} = {}) + constructor({name, win, input, modelDir, faceApiUrl, units, ori, opacity, pos, size, autoDraw, autoLog, draggable} = {}) { - super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog}); + super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog, draggable}); // TODO deal with onChange (see MovieStim and Camera) this._addAttribute("input", input, undefined); diff --git a/src/visual/Form.js b/src/visual/Form.js index abe6281..f262a2e 100644 --- a/src/visual/Form.js +++ b/src/visual/Form.js @@ -54,6 +54,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin) * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every * frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ constructor( { @@ -82,10 +83,11 @@ export class Form extends util.mix(VisualStim).with(ColorMixin) clipMask, autoDraw, autoLog, + draggable } = {}, ) { - super({ name, win, units, opacity, depth, pos, size, clipMask, autoDraw, autoLog }); + super({ name, win, units, opacity, depth, pos, size, clipMask, autoDraw, autoLog, draggable }); this._addAttribute( "itemPadding", diff --git a/src/visual/GifStim.js b/src/visual/GifStim.js new file mode 100644 index 0000000..59dc135 --- /dev/null +++ b/src/visual/GifStim.js @@ -0,0 +1,515 @@ +/** + * Gif Stimulus. + * + * @author Nikita Agafonov + * @version 2022.2.0 + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + */ + +import * as PIXI from "pixi.js-legacy"; +import { Color } from "../util/Color.js"; +import { ColorMixin } from "../util/ColorMixin.js"; +import { to_pixiPoint } from "../util/Pixi.js"; +import * as util from "../util/Util.js"; +import { VisualStim } from "./VisualStim.js"; +import {Camera} from "../hardware"; +// import { parseGIF, decompressFrames } from "gifuct-js"; +import { AnimatedGIF } from "./AnimatedGIF.js"; +import { parseGIF, decompressFrames } from "../util/GifParser.js"; + +/** + * Gif Stimulus. + * + * @name module:visual.GifStim + * @class + * @extends VisualStim + * @mixes ColorMixin + * @param {Object} options + * @param {String} options.name - the name used when logging messages from this stimulus + * @param {Window} options.win - the associated Window + * @param {boolean} options.precomputeFrames - compute full frames of the GIF and store them. Setting this to true will take the load off the CPU + * @param {string | HTMLImageElement} options.image - the name of the image resource or the HTMLImageElement corresponding to the image + * @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask + * but GIF will take longer to load and occupy more memory space. In case when there's not enough CPU peformance (e.g. due to large amount of GIFs + * playing simultaneously or heavy load elsewhere in experiment) and you don't care much about app memory usage, use this flag to easily gain more performance. + * @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices) + * @param {Array.} [options.pos= [0, 0]] - the position of the center of the stimulus + * @param {string} [options.units= 'norm'] - the units of the stimulus vertices, size and position + * @param {number} [options.ori= 0.0] - the orientation (in degrees) + * @param {number} [options.size] - the size of the rendered image (the size of the image will be used if size is not specified) + * @param {Color} [options.color= 'white'] the background color + * @param {number} [options.opacity= 1.0] - the opacity + * @param {number} [options.contrast= 1.0] - the contrast + * @param {number} [options.depth= 0] - the depth (i.e. the z order) + * @param {number} [options.texRes= 128] - the resolution of the text + * @param {boolean} [options.loop= true] - whether or not to loop the animation + * @param {boolean} [options.autoPlay= true] - whether or not to autoPlay the animation + * @param {boolean} [options.animationSpeed= 1] - animation speed, works as multiplyer e.g. 1 - normal speed, 0.5 - half speed, 2 - twice as fast etc. + * @param {boolean} [options.interpolate= false] - whether or not the image is interpolated + * @param {boolean} [options.flipHoriz= false] - whether or not to flip horizontally + * @param {boolean} [options.flipVert= false] - whether or not to flip vertically + * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip + * @param {boolean} [options.autoLog= false] - whether or not to log + */ +export class GifStim extends util.mix(VisualStim).with(ColorMixin) +{ + constructor({ + name, + win, + image, + mask, + precomputeFrames, + pos, + units, + ori, + size, + color, + opacity, + contrast, + texRes, + depth, + interpolate, + loop, + autoPlay, + animationSpeed, + flipHoriz, + flipVert, + autoDraw, + autoLog + } = {}) + { + super({ name, win, units, ori, opacity, depth, pos, size, autoDraw, autoLog }); + + this._resource = undefined; + + this._addAttribute("precomputeFrames", precomputeFrames, false); + this._addAttribute("image", image); + this._addAttribute("mask", mask); + this._addAttribute("color", color, "white", this._onChange(true, false)); + this._addAttribute("contrast", contrast, 1.0, this._onChange(true, false)); + this._addAttribute("texRes", texRes, 128, this._onChange(true, false)); + this._addAttribute("interpolate", interpolate, false); + this._addAttribute("flipHoriz", flipHoriz, false, this._onChange(false, false)); + this._addAttribute("flipVert", flipVert, false, this._onChange(false, false)); + this._addAttribute("loop", loop, true); + this._addAttribute("autoPlay", autoPlay, true); + this._addAttribute("animationSpeed", animationSpeed, 1); + + // estimate the bounding box: + this._estimateBoundingBox(); + + if (this._autoLog) + { + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + } + } + + /** + * Getter for the playing property. + * + * @name module:visual.GifStim#isPlaying + * @public + */ + get isPlaying () + { + if (this._pixi) + { + return this._pixi.playing; + } + return false; + } + + /** + * Getter for the duration property. Shows animation duration time in milliseconds. + * + * @name module:visual.GifStim#duration + * @public + */ + get duration () + { + if (this._pixi) + { + return this._pixi.duration; + } + } + + /** + * Starts GIF playback. + * + * @name module:visual.GifStim#play + * @public + */ + play () + { + if (this._pixi) + { + this._pixi.play(); + } + } + + /** + * Pauses GIF playback. + * + * @name module:visual.GifStim#pause + * @public + */ + pause () + { + if (this._pixi) + { + this._pixi.stop(); + } + } + + /** + * Set wether or not to loop the animation. + * + * @name module:visual.GifStim#setLoop + * @public + * @param {boolean} [loop=true] - flag value + * @param {boolean} [log=false] - whether or not to log. + */ + setLoop (loop, log = false) + { + this._setAttribute("loop", loop, log); + if (this._pixi) + { + this._pixi.loop = loop; + } + } + + /** + * Set wether or not to autoplay the animation. + * + * @name module:visual.GifStim#setAutoPlay + * @public + * @param {boolean} [autoPlay=true] - flag value + * @param {boolean} [log=false] - whether or not to log. + */ + setAutoPlay (autoPlay, log = false) + { + this._setAttribute("autoPlay", autoPlay, log); + if (this._pixi) + { + this._pixi.autoPlay = autoPlay; + } + } + + /** + * Set animation speed of the animation. + * + * @name module:visual.GifStim#setAnimationSpeed + * @public + * @param {boolean} [animationSpeed=1] - multiplyer of the animation speed e.g. 1 - normal, 0.5 - half speed, 2 - twice as fast. + * @param {boolean} [log=false] - whether or not to log. + */ + setAnimationSpeed (animationSpeed = 1, log = false) + { + this._setAttribute("animationSpeed", animationSpeed, log); + if (this._pixi) + { + this._pixi.animationSpeed = animationSpeed; + } + } + + /** + * Setter for the image attribute. + * + * @name module:visual.GifStim#setImage + * @public + * @param {HTMLImageElement | string} image - the name of the image resource or HTMLImageElement corresponding to the image + * @param {boolean} [log= false] - whether or not to log + */ + setImage(image, log = false) + { + const response = { + origin: "GifStim.setImage", + context: "when setting the image of GifStim: " + this._name, + }; + + try + { + // image is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem + if (typeof image === "undefined") + { + this.psychoJS.logger.warn("setting the image of GifStim: " + this._name + " with argument: undefined."); + this.psychoJS.logger.debug("set the image of GifStim: " + this._name + " as: undefined"); + } + else if (typeof image === "string") + { + // image is a string: it should be the name of a resource, which we load + const fullRD = this.psychoJS.serverManager.getFullResourceData(image); + console.log("gif resource", fullRD); + if (fullRD.cachedData === undefined) + { + // How GIF works: http://www.matthewflickinger.com/lab/whatsinagif/animation_and_transparency.asp + let t0 = performance.now(); + let parsedGif = parseGIF(fullRD.data); + let pt = performance.now() - t0; + let t2 = performance.now(); + let decompressedFrames = decompressFrames(parsedGif, false); + let dect = performance.now() - t2; + let fullFrames; + if (this._precomputeFrames) + { + fullFrames = AnimatedGIF.computeFullFrames(decompressedFrames, parsedGif.lsd.width, parsedGif.lsd.height); + } + this._resource = { parsedGif, decompressedFrames, fullFrames }; + this.psychoJS.serverManager.cacheResourceData(image, this._resource); + console.log(`animated gif "${this._name}",`, "parse=", pt, "decompress=", dect); + } + else + { + this._resource = fullRD.cachedData; + } + + // this.psychoJS.logger.debug(`set resource of GifStim: ${this._name} as ArrayBuffer(${this._resource.length})`); + const hasChanged = this._setAttribute("image", image, log); + if (hasChanged) + { + this._onChange(true, true)(); + } + } + } + catch (error) + { + throw Object.assign(response, { error }); + } + } + + /** + * Setter for the mask attribute. + * + * @name module:visual.GifStim#setMask + * @public + * @param {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask + * @param {boolean} [log= false] - whether of not to log + */ + setMask(mask, log = false) + { + const response = { + origin: "GifStim.setMask", + context: "when setting the mask of GifStim: " + this._name, + }; + + try + { + // mask is undefined: that's fine but we raise a warning in case this is a sympton of an actual problem + if (typeof mask === "undefined") + { + this.psychoJS.logger.warn("setting the mask of GifStim: " + this._name + " with argument: undefined."); + this.psychoJS.logger.debug("set the mask of GifStim: " + this._name + " as: undefined"); + } + else + { + // mask is a string: it should be the name of a resource, which we load + if (typeof mask === "string") + { + mask = this.psychoJS.serverManager.getResource(mask); + } + + // mask should now be an actual HTMLImageElement: we raise an error if it is not + if (!(mask instanceof HTMLImageElement)) + { + throw "the argument: " + mask.toString() + ' is not an image" }'; + } + + this.psychoJS.logger.debug("set the mask of GifStim: " + this._name + " as: src= " + mask.src + ", size= " + mask.width + "x" + mask.height); + } + + this._setAttribute("mask", mask, log); + this._onChange(true, false)(); + } + catch (error) + { + throw Object.assign(response, { error }); + } + } + + /** + * Whether to interpolate (linearly) the texture in the stimulus. + * + * @name module:visual.GifStim#setInterpolate + * @public + * @param {boolean} interpolate - interpolate or not. + * @param {boolean} [log=false] - whether or not to log + */ + setInterpolate (interpolate = false, log = false) + { + this._setAttribute("interpolate", interpolate, log); + if (this._pixi instanceof PIXI.Sprite) { + this._pixi.texture.baseTexture.scaleMode = interpolate ? PIXI.SCALE_MODES.LINEAR : PIXI.SCALE_MODES.NEAREST; + this._pixi.texture.baseTexture.update(); + } + } + + /** + * Setter for the size attribute. + * + * @param {undefined | null | number | number[]} size - the stimulus size + * @param {boolean} [log= false] - whether of not to log + */ + setSize(size, log = false) + { + // size is either undefined, null, or a tuple of numbers: + if (typeof size !== "undefined" && size !== null) + { + size = util.toNumerical(size); + if (!Array.isArray(size)) + { + size = [size, size]; + } + } + + this._setAttribute("size", size, log); + + if (this._pixi) + { + const size_px = util.to_px(size, this.units, this.win); + const scaleX = size_px[0] / this._pixi.texture.width; + const scaleY = size_px[1] / this._pixi.texture.height; + this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; + this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; + } + } + + /** + * Estimate the bounding box. + * + * @name module:visual.GifStim#_estimateBoundingBox + * @function + * @override + * @protected + */ + _estimateBoundingBox() + { + const size = this._getDisplaySize(); + if (typeof size !== "undefined") + { + this._boundingBox = new PIXI.Rectangle( + this._pos[0] - size[0] / 2, + this._pos[1] - size[1] / 2, + size[0], + size[1], + ); + } + + // TODO take the orientation into account + } + + /** + * Update the stimulus, if necessary. + * + * @name module:visual.GifStim#_updateIfNeeded + * @private + */ + _updateIfNeeded() + { + if (!this._needUpdate) + { + return; + } + this._needUpdate = false; + + // update the PIXI representation, if need be: + if (this._needPixiUpdate) + { + this._needPixiUpdate = false; + + if (typeof this._pixi !== "undefined") + { + this._pixi.destroy(true); + } + this._pixi = undefined; + + // no image to draw: return immediately + if (typeof this._resource === "undefined") + { + return; + } + + const gifOpts = + { + name: this._name, + width: this._resource.parsedGif.lsd.width, + height: this._resource.parsedGif.lsd.height, + fullFrames: this._resource.fullFrames, + scaleMode: this._interpolate ? PIXI.SCALE_MODES.LINEAR : PIXI.SCALE_MODES.NEAREST, + loop: this._loop, + autoPlay: this._autoPlay, + animationSpeed: this._animationSpeed + }; + + let t = performance.now(); + this._pixi = new AnimatedGIF(this._resource.decompressedFrames, gifOpts); + console.log(`animatedGif "${this._name}" instancing:`, performance.now() - t); + + // add a mask if need be: + if (typeof this._mask !== "undefined") + { + // Building new PIXI.BaseTexture each time we create a mask, to avoid PIXI's caching and use a unique resource. + this._pixi.mask = PIXI.Sprite.from(new PIXI.Texture(new PIXI.BaseTexture(this._mask))); + + // a 0.5, 0.5 anchor is required for the mask to be aligned with the image + this._pixi.mask.anchor.x = 0.5; + this._pixi.mask.anchor.y = 0.5; + this._pixi.addChild(this._pixi.mask); + } + + // since _texture.width may not be immediately available but the rest of the code needs its value + // we arrange for repeated calls to _updateIfNeeded until we have a width: + if (this._pixi.texture.width === 0) + { + this._needUpdate = true; + this._needPixiUpdate = true; + return; + } + } + + this._pixi.zIndex = -this._depth; + this._pixi.alpha = this.opacity; + + // set the scale: + const displaySize = this._getDisplaySize(); + const size_px = util.to_px(displaySize, this.units, this.win); + const scaleX = size_px[0] / this._pixi.texture.width; + const scaleY = size_px[1] / this._pixi.texture.height; + this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; + this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; + + // set the position, rotation, and anchor (image centered on pos): + this._pixi.position = to_pixiPoint(this.pos, this.units, this.win); + this._pixi.rotation = -this.ori * Math.PI / 180; + this._pixi.anchor.x = 0.5; + this._pixi.anchor.y = 0.5; + + // re-estimate the bounding box, as the texture's width may now be available: + this._estimateBoundingBox(); + } + + /** + * Get the size of the display image, which is either that of the GifStim or that of the image + * it contains. + * + * @name module:visual.GifStim#_getDisplaySize + * @private + * @return {number[]} the size of the displayed image + */ + _getDisplaySize() + { + let displaySize = this.size; + + if (this._pixi && typeof displaySize === "undefined") + { + // use the size of the texture, if we have access to it: + if (typeof this._pixi.texture !== "undefined" && this._pixi.texture.width > 0) + { + const textureSize = [this._pixi.texture.width, this._pixi.texture.height]; + displaySize = util.to_unit(textureSize, "pix", this.win, this.units); + } + } + + return displaySize; + } +} diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js index ae040c1..401ade7 100644 --- a/src/visual/GratingStim.js +++ b/src/visual/GratingStim.js @@ -426,6 +426,7 @@ export class GratingStim extends VisualStim * @param {String} [options.blendmode= "avg"] - blend mode of the stimulus, determines how the stimulus is blended with the background. Supported values: "avg", "add", "mul", "screen". * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ constructor({ name, @@ -448,10 +449,11 @@ export class GratingStim extends VisualStim blendmode, autoDraw, autoLog, - maskParams + maskParams, + draggable } = {}) { - super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog }); + super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog, draggable }); this._adjustmentFilter = new AdjustmentFilter({ contrast diff --git a/src/visual/ImageStim.js b/src/visual/ImageStim.js index f043579..96a0979 100644 --- a/src/visual/ImageStim.js +++ b/src/visual/ImageStim.js @@ -46,10 +46,39 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) * @param {boolean} [options.flipVert= false] - whether or not to flip vertically * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device + * @param {ImageStim.AspectRatioStrategy} [options.aspectRatio= ImageStim.AspectRatioStrategy.VARIABLE] - the aspect ratio handling strategy + * @param {number} [options.blurVal= 0] - the blur value. Goes 0 to as hish as you like. 0 is no blur. */ - constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {}) + constructor({ + name, + win, + image, + mask, + pos, + anchor, + units, + ori, + size, + color, + opacity, + contrast, + texRes, + depth, + interpolate, + flipHoriz, + flipVert, + autoDraw, + autoLog, + aspectRatio, + draggable, + blurVal + } = {}) { - super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog }); + super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog, draggable }); + + // Holds an instance of PIXI blur filter. Used if blur value is passed. + this._blurFilter = undefined; this._addAttribute( "image", @@ -94,6 +123,17 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) false, this._onChange(false, false), ); + this._addAttribute( + "aspectRatio", + aspectRatio, + ImageStim.AspectRatioStrategy.VARIABLE, + this._onChange(true, true), + ); + this._addAttribute( + "blurVal", + blurVal, + 0 + ); // estimate the bounding box: this._estimateBoundingBox(); @@ -108,7 +148,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) * Setter for the image attribute. * * @param {HTMLImageElement | string} image - the name of the image resource or HTMLImageElement corresponding to the image - * @param {boolean} [log= false] - whether of not to log + * @param {boolean} [log= false] - whether or not to log */ setImage(image, log = false) { @@ -176,7 +216,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) * Setter for the mask attribute. * * @param {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask - * @param {boolean} [log= false] - whether of not to log + * @param {boolean} [log= false] - whether or not to log */ setMask(mask, log = false) { @@ -234,6 +274,129 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) } } + /** + * Sets the amount of blur for image stimuli. + * + * @param {number} blurVal - the amount of blur. 0 is no blur, max is as high as you like. + * @param {boolean} [log=false] - whether or not to log. + */ + setBlurVal (blurVal = 0, log = false) + { + this._setAttribute("blurVal", blurVal, log); + if (this._pixi instanceof PIXI.Sprite) + { + if (this._blurFilter === undefined) + { + this._blurFilter = new PIXI.filters.BlurFilter(); + this._blurFilter.blur = blurVal; + } + else + { + this._blurFilter.blur = blurVal; + } + + // this._pixi might get destroyed and recreated again with no filters. + if (this._pixi.filters instanceof Array && this._pixi.filters.indexOf(this._blurFilter) === -1) + { + this._pixi.filters.push(this._blurFilter); + } + else + { + this._pixi.filters = [this._blurFilter]; + } + } + } + + /** + * Setter for the size attribute. + * + * @param {undefined | null | number | number[]} size - the stimulus size + * @param {boolean} [log= false] - whether or not to log + */ + setSize(size, log = false) + { + if (!Array.isArray(size)) + { + size = [size, size]; + } + + if (Array.isArray(size) && size.length <= 1) + { + size = [size[0], size[0]]; + } + + for (let i = 0; i < size.length; i++) + { + try + { + size[i] = util.toNumerical(size[i]); + } + catch (err) + { + // Failed to convert to numeric. Set to NaN. + size[ i ] = NaN; + } + } + + if (this._texture !== undefined) + { + size = this._ensureNaNSizeConversion(size, this._texture); + this._applySizeToPixi(size); + } + + this._setAttribute("size", size, log); + } + + /** + * Applies given size values to underlying pixi component of the stim. + * + * @param {Array} size + */ + _applySizeToPixi(size) + { + const size_px = util.to_px(size, this._units, this._win); + let scaleX = size_px[0] / this._texture.width; + let scaleY = size_px[1] / this._texture.height; + if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH) + { + scaleY = scaleX; + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT) + { + scaleX = scaleY; + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + scaleX = 1.0; + scaleY = 1.0; + } + this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; + this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; + } + + /** + * Ensures to convert NaN in the size values to proper, numerical values using given texture dimensions. + * + * @param {Array} size + */ + _ensureNaNSizeConversion(size, pixiTex) + { + if (Number.isNaN(size[0]) && Number.isNaN(size[1])) + { + size = util.to_unit([pixiTex.width, pixiTex.height], "pix", this._win, this._units); + } + else if (Number.isNaN(size[0])) + { + size[0] = size[1] * (pixiTex.width / pixiTex.height); + } + else if (Number.isNaN(size[1])) + { + size[1] = size[0] / (pixiTex.width / pixiTex.height); + } + + return size; + } + /** * Estimate the bounding box. * @@ -276,6 +439,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) if (typeof this._pixi !== "undefined") { + this._pixi.filters = null; this._pixi.destroy(true); } this._pixi = undefined; @@ -292,7 +456,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) // Not using PIXI.Texture.from() on purpose, as it caches both PIXI.Texture and PIXI.BaseTexture. // As a result of that we can have multiple ImageStim instances using same PIXI.BaseTexture, // thus changing texture related properties like interpolation, or calling _pixi.destroy(true) - // will affect all ImageStims who happen to share that BaseTexture. + // will affect all ImageStims which happen to share that BaseTexture. const texOpts = { scaleMode: this._interpolate ? PIXI.SCALE_MODES.LINEAR : PIXI.SCALE_MODES.NEAREST @@ -309,7 +473,17 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image, texOpts)); } - this._pixi = PIXI.Sprite.from(this._texture); + if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + const [width_px, _] = util.to_px([this.size[0], 0], this.units, this.win); + this._pixi = PIXI.TilingSprite.from(this._texture, 1, 1); + this._pixi.width = width_px; + this._pixi.height = this._texture.height; + } + else + { + this._pixi = PIXI.Sprite.from(this._texture); + } // add a mask if need be: if (typeof this._mask !== "undefined") @@ -346,19 +520,24 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) this._pixi.zIndex = -this._depth; this._pixi.alpha = this.opacity; - // set the scale: - const displaySize = this._getDisplaySize(); - const size_px = util.to_px(displaySize, this.units, this.win); - const scaleX = size_px[0] / this._texture.width; - const scaleY = size_px[1] / this._texture.height; + // initial setSize might be called with incomplete values like [512, null]. + // Before texture is loaded they are converted to [512, NaN]. + // At this point the texture is loaded and we can convert NaN to proper values. + this.size = this._getDisplaySize(); + + // note: this calls VisualStim.setAnchor, which properly sets the PixiJS anchor + // from the PsychoPy text format this.anchor = this._anchor; - this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; - this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; // set the position, rotation, and anchor (image centered on pos): this._pixi.position = to_pixiPoint(this.pos, this.units, this.win); this._pixi.rotation = -this.ori * Math.PI / 180; + if (this._blurVal > 0) + { + this.setBlurVal(this._blurVal); + } + // re-estimate the bounding box, as the texture's width may now be available: this._estimateBoundingBox(); } @@ -383,7 +562,47 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) displaySize = util.to_unit(textureSize, "pix", this.win, this.units); } } + else + { + if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[0], displaySize[0] * this._texture.height / this._texture.width]; + } + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[1] * this._texture.width / this._texture.height, displaySize[1]]; + } + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[0], this._texture.height]; + } + } + } return displaySize; } } + +/** + * ImageStim Aspect Ratio Strategy. + * + * @enum {Symbol} + * @readonly + */ +ImageStim.AspectRatioStrategy = { + FIT_TO_WIDTH: Symbol.for("FIT_TO_WIDTH"), + HORIZONTAL_TILING: Symbol.for("HORIZONTAL_TILING"), + FIT_TO_HEIGHT: Symbol.for("FIT_TO_HEIGHT"), + VARIABLE: Symbol.for("VARIABLE"), +}; diff --git a/src/visual/MovieStim.js b/src/visual/MovieStim.js index 2ebd832..b9c2ff4 100644 --- a/src/visual/MovieStim.js +++ b/src/visual/MovieStim.js @@ -15,7 +15,7 @@ import { to_pixiPoint } from "../util/Pixi.js"; import * as util from "../util/Util.js"; import { VisualStim } from "./VisualStim.js"; import {Camera} from "../hardware/Camera.js"; - +import YoutubeIframeAPIHandler from "./YoutubeIframeAPI.js"; /** * Movie Stimulus. @@ -32,6 +32,9 @@ export class MovieStim extends VisualStim * @param {module:core.Window} options.win - the associated Window * @param {string | HTMLVideoElement | module:visual.Camera} movie - the name of a * movie resource or of a HTMLVideoElement or of a Camera component + * @param {string} [options.youtubeUrl] - link to a youtube video. If this parameter is present, movie stim will embed a youtube video to an experiment. + * @param {boolean} [options.showYoutubeControls] - whether or not to show youtube player controls. + * @oaram {boolean} [options.disableYoutubePlayerKeyboardControls=false] - Setting the parameter's value to true causes the youtube player to not respond to keyboard controls. * @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices) * @param {Array.} [options.pos= [0, 0]] - the position of the center of the stimulus * @param {string} [options.anchor = "center"] - sets the origin point of the stim @@ -50,20 +53,65 @@ export class MovieStim extends VisualStim * @param {boolean} [options.autoPlay= true] - whether or not to autoplay the video * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ - constructor({ name, win, movie, pos, anchor, units, ori, size, color, opacity, contrast, interpolate, flipHoriz, flipVert, loop, volume, noAudio, autoPlay, autoDraw, autoLog } = {}) + constructor({ + name, + win, + movie, + youtubeUrl, + showYoutubeControls, + disableYoutubePlayerKeyboardControls, + pos, + anchor, + units, + ori, + size, + color, + opacity, + contrast, + interpolate, + flipHoriz, + flipVert, + loop, + volume, + noAudio, + autoPlay, + autoDraw, + autoLog, + draggable + } = {}) { - super({ name, win, units, ori, opacity, pos, anchor, size, autoDraw, autoLog }); + super({ name, win, units, ori, opacity, pos, anchor, size, autoDraw, autoLog, draggable }); this.psychoJS.logger.debug("create a new MovieStim with name: ", name); this._pixiTextureResource = undefined; + // Used in case when youtubeUrl parameter is set to a proper youtube url. + this._youtubePlayer = undefined; + this._ytPlayerIsReady = false; + // movie and movie control: this._addAttribute( "movie", movie, ); + this._addAttribute( + "youtubeUrl", + youtubeUrl, + "" + ); + this._addAttribute( + "showYoutubeControls", + showYoutubeControls, + true + ); + this._addAttribute( + "disableYoutubePlayerKeyboardControls", + disableYoutubePlayerKeyboardControls, + false + ); this._addAttribute( "volume", volume, @@ -140,7 +188,7 @@ export class MovieStim extends VisualStim * * @param {string | HTMLVideoElement | module:visual.Camera} movie - the name of a * movie resource or of a HTMLVideoElement or of a Camera component - * @param {boolean} [log= false] - whether of not to log + * @param {boolean} [log= false] - whether or not to log */ setMovie(movie, log = false) { @@ -162,7 +210,6 @@ export class MovieStim extends VisualStim `setting the movie of MovieStim: ${this._name} with argument: undefined.`); this.psychoJS.logger.debug(`set the movie of MovieStim: ${this._name} as: undefined`); } - else { let videoResource; @@ -182,13 +229,13 @@ export class MovieStim extends VisualStim { // old behaviour: feeding a Camera to MovieStim plays the live stream: videoResource = movie.getVideo(); - // TODO remove previous movie one if there is one + // TODO remove previous movie if there is one /* // new behaviour: feeding a Camera to MovieStim replays the video previously recorded by the Camera: const video = movie.getRecording(); movie = video; - */ + */ } if (videoResource instanceof HTMLVideoElement) @@ -196,6 +243,16 @@ export class MovieStim extends VisualStim htmlVideo = videoResource; htmlVideo.playsInline = true; this._pixiTextureResource = PIXI.Texture.from(htmlVideo, { resourceOptions: { autoPlay: false } }); + // Not using PIXI.Texture.from() on purpose, as it caches both PIXI.Texture and PIXI.BaseTexture. + // As a result of that we can have multiple MovieStim instances using same PIXI.BaseTexture, + // thus changing texture related properties like interpolation, or calling _pixi.destroy(true) + // will affect all MovieStims which happen to share that BaseTexture. + this._pixiTextureResource = new PIXI.Texture(new PIXI.BaseTexture( + this._movie, + { + resourceOptions: { autoPlay: this.autoPlay } + } + )); } else if (videoResource instanceof PIXI.Texture) { @@ -220,6 +277,20 @@ export class MovieStim extends VisualStim this.status = PsychoJS.Status.FINISHED; }; } + + // Resize the stim when video is loaded. Otherwise this._pixiTextureResource.width is 1. + const loadedDataCb = () => + { + this.size = this._size; + movie.removeEventListener("loadeddata", loadedDataCb); + }; + + if (movie.readyState < movie.HAVE_FUTURE_DATA) + { + movie.addEventListener("loadeddata", loadedDataCb) + } + + this.hideYoutubePlayer(); } this._setAttribute("movie", htmlVideo, log); @@ -232,10 +303,268 @@ export class MovieStim extends VisualStim } } + /** + * Setter for the size attribute. + * + * @param {undefined | null | number | number[]} size - the stimulus size + * @param {boolean} [log= false] - whether or not to log + */ + setSize(size, log = false) + { + if (!Array.isArray(size)) + { + size = [size, size]; + } + + if (Array.isArray(size) && size.length <= 1) + { + size = [size[0], size[0]]; + } + + for (let i = 0; i < size.length; i++) + { + try + { + size[i] = util.toNumerical(size[i]); + } + catch (err) + { + // Failed to convert to numeric. Set to NaN. + size[ i ] = NaN; + } + } + + // If the html5Video is available and loaded enough, use information from it to convert NaN to proper values. + if (this._movie !== undefined && this._movie.readyState >= this._movie.HAVE_FUTURE_DATA) + { + size = this._ensureNaNSizeConversion(size, this._movie); + } + + if (this._pixiTextureResource !== undefined) + { + this._applySizeToPixi(size); + } + + if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + // Handling youtube iframe resize here, since _updateIfNeeded aint going to be triggered due to absence of _pixi component. + this._applySizeToYoutubeIframe(size); + + // Youtube player handles NaN size automatically. Leveraging that to cover unset size. + // IMPORTANT! this._youtubePlayer.getSize() is not used intentionally, because it returns initial values event after different size was set. + const ytPlayerBCR = this._youtubePlayer.getIframe().getBoundingClientRect(); + size = util.to_unit([ ytPlayerBCR.width, ytPlayerBCR.height ], "pix", this._win, this._units); + } + + this._setAttribute("size", size, log); + } + + /** + * Setter for the position attribute. + * + * @param {Array.} pos - position of the center of the stimulus, in stimulus units + * @param {boolean} [log= false] - whether or not to log + */ + setPos(pos, log = false) + { + super.setPos(pos, log); + // if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + if (this._youtubePlayer !== undefined) + { + const pos_px = util.to_px(pos, this._units, this._win, false); + pos_px[1] *= this._win._rootContainer.scale.y; + this._youtubePlayer.getIframe().style.transform = `translate3d(${pos_px[0]}px, ${pos_px[1]}px, 0)`; + } + } + + /** + * Setter for the volume attribute. + * + * @param {number} volume - desired volume of the movie in [0, 1]. + * @param {boolean} [log= false] - whether of not to log + */ + setVolume(vol, log = false) + { + this._setAttribute("volume", vol, log); + if (this._movie !== undefined) + { + this._movie.volume = vol; + } + else if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + // Original movie takes volume in [0, 1], whereas youtube's player [0, 100]. + this._youtubePlayer.setVolume(vol * 100); + } + } + + /** + * Draw this stimulus on the next frame draw. + */ + draw() + { + super.draw(); + if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + this.showYoutubePlayer(); + } + } + + /** + * Hide this stimulus on the next frame draw. + */ + hide() + { + super.hide(); + if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + this.hideYoutubePlayer(); + } + } + + /** + * Handling youtube player being ready to work. + * + * @param {string} link to a youtube video. If this parameter is present, movie stim will embed a youtube video to an experiment. + * @param {boolean} [log= false] - whether or not to log. + */ + _onYoutubePlayerReady (e) + { + this._ytPlayerIsReady = true; + + if (Number.isNaN(this._size[ 0 ]) || Number.isNaN(this._size[ 1 ])) + { + // Youtube player handles NaN size automatically. Leveraging that to cover unset size. + // IMPORTANT! this._youtubePlayer.getSize() is not used intentionally, because it returns initial values event after different size was set. + const ytPlayerBCR = this._youtubePlayer.getIframe().getBoundingClientRect(); + this._setAttribute("size", util.to_unit([ ytPlayerBCR.width, ytPlayerBCR.height ], "pix", this._win, this._units), true); + } + + this.setVolume(this._volume, true); + } + + /** + * Handling youtube player state change. + * + * @param {string} link to a youtube video. If this parameter is present, movie stim will embed a youtube video to an experiment. + * @param {boolean} [log= false] - whether or not to log. + */ + _onYoutubePlayerStateChange (e) + { + if (e.data === YT.PlayerState.PLAYING) + { + // Just in case for potential future requirements. + } + else if (e.data === YT.PlayerState.PAUSED) + { + // Just in case for potential future requirements. + } + else if (e.data === YT.PlayerState.ENDED) + { + // Just in case for potential future requirements. + } + else if (e.data === YT.PlayerState.ENDED) + { + // Just in case for potential future requirements. + } + } + + /** + * Handling youtube player errors. + * + * @param {string} link to a youtube video. If this parameter is present, movie stim will embed a youtube video to an experiment. + * @param {boolean} [log= false] - whether or not to log. + */ + _onYoutubePlayerError (err) + { + // Just in case for potential future requirements. + console.error("youtube player error:", arguments); + } + + hideYoutubePlayer () + { + if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + this._youtubePlayer.stopVideo(); + this._youtubePlayer.getIframe().parentElement.classList.add("hidden"); + } + } + + showYoutubePlayer () + { + if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + this._youtubePlayer.getIframe().parentElement.classList.remove("hidden"); + } + } + + /** + * Setter for the youtubeUrl attribute. + * + * @param {string} link to a youtube video. If this parameter is present, movie stim will embed a youtube video to an experiment. + * @param {boolean} [log= false] - whether or not to log. + */ + async setYoutubeUrl (urlString = "", log = false) + { + if (urlString.length === 0) + { + this.hideYoutubePlayer(); + return; + } + + // Handling the case when there's already regular movie is set. + if (this._movie !== undefined) + { + this.stop(); + this.setMovie(undefined); + + // Removing stimuli from the drawing list. + this.hide(); + } + + const urlObj = new URL(urlString); + if (this._youtubePlayer === undefined) + { + const vidSizePx = util.to_unit(this._size, this.units, this.win, "pix"); + + await YoutubeIframeAPIHandler.init(); + + this._youtubePlayer = YoutubeIframeAPIHandler.createPlayer({ + videoId: urlObj.searchParams.get("v"), + width: vidSizePx[0], + height: vidSizePx[ 1 ], + playerVars: { + "rel": 0, + "playsinline": 1, + "modestbranding": 1, + "disablekb": Number(this._disableYoutubePlayerKeyboardControls) || 0, + "autoplay": Number(this._autoPlay) || 0, + "controls": Number(this._showYoutubeControls) || 0, + "loop": Number(this._loop) || 0, + }, + events: { + "onReady": this._onYoutubePlayerReady.bind(this), + "onStateChange": this._onYoutubePlayerStateChange.bind(this), + "onError": this._onYoutubePlayerError.bind(this), + // "onPlaybackQualityChange": + // "onPlaybackRateChange": + // "onApiChange": + } + }); + + // At this point youtube player is added to the page. Invoking position setter to ensure html element is placed as expected. + this.pos = this._pos; + } + else + { + this._youtubePlayer.loadVideoById(urlObj.searchParams.get("v")); + this.showYoutubePlayer(); + } + } + /** * Reset the stimulus. * - * @param {boolean} [log= false] - whether of not to log + * @param {boolean} [log= false] - whether or not to log */ reset(log = false) { @@ -247,49 +576,70 @@ export class MovieStim extends VisualStim /** * Start playing the movie. * - * @param {boolean} [log= false] - whether of not to log + * @param {boolean} [log= false] - whether or not to log */ play(log = false) { this.status = PsychoJS.Status.STARTED; - // As found on https://goo.gl/LdLk22 - const playPromise = this._movie.play(); - - if (playPromise !== undefined) + if (this._movie !== undefined) { - playPromise.catch((error) => + // As found on https://goo.gl/LdLk22 + const playPromise = this._movie.play(); + + if (playPromise !== undefined) { - throw { - origin: "MovieStim.play", - context: `when attempting to play MovieStim: ${this._name}`, - error, - }; - }); + playPromise.catch((error) => + { + throw { + origin: "MovieStim.play", + context: `when attempting to play MovieStim: ${this._name}`, + error, + }; + }); + } + } + else if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + this._youtubePlayer.playVideo(); } } /** * Pause the movie. * - * @param {boolean} [log= false] - whether of not to log + * @param {boolean} [log= false] - whether or not to log */ pause(log = false) { this.status = PsychoJS.Status.STOPPED; - this._movie.pause(); + if (this._movie !== undefined) + { + this._movie.pause(); + } + else if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + this._youtubePlayer.pauseVideo(); + } } /** * Stop the movie and reset to 0s. * - * @param {boolean} [log= false] - whether of not to log + * @param {boolean} [log= false] - whether or not to log */ stop(log = false) { this.status = PsychoJS.Status.STOPPED; - this._movie.pause(); - this.seek(0, log); + if (this._movie !== undefined) + { + this._movie.pause(); + this.seek(0, log); + } + else if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + this._youtubePlayer.stopVideo(); + } } /** @@ -298,45 +648,116 @@ export class MovieStim extends VisualStim *

Note: seek is experimental and does not work on all browsers at the moment.

* * @param {number} timePoint - the timepoint to which to jump (in second) - * @param {boolean} [log= false] - whether of not to log + * @param {boolean} [log= false] - whether or not to log */ seek(timePoint, log = false) { - if (timePoint < 0 || timePoint > this._movie.duration) + if (this._movie !== undefined) { - throw { - origin: "MovieStim.seek", - context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`, - error: `the timepoint does not belong to [0, ${this._movie.duration}`, - }; - } - - if (this._hasFastSeek) - { - this._movie.fastSeek(timePoint); - } - else - { - try - { - this._movie.currentTime = timePoint; - } - catch (error) + if (timePoint < 0 || timePoint > this._movie.duration) { throw { origin: "MovieStim.seek", context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`, - error, + error: `the timepoint does not belong to [0, ${this._movie.duration}`, }; } + + if (this._hasFastSeek) + { + this._movie.fastSeek(timePoint); + } + else + { + try + { + this._movie.currentTime = timePoint; + } + catch (error) + { + throw { + origin: "MovieStim.seek", + context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`, + error, + }; + } + } + } + else if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + this._youtubePlayer.seekTo(timePoint); } } /** - * Estimate the bounding box. + * Get the elapsed time in seconds since the video started playing. * - * @override - * @protected + * @return {number} playback time. + */ + getPlaybackTime () + { + if (this._movie !== undefined) + { + return this._movie.currentTime; + } + else if (this._youtubePlayer !== undefined && this._ytPlayerIsReady) + { + return this._youtubePlayer.getCurrentTime(); + } + + return 0; + } + + /** + * Applies given size values to underlying pixi component of the stim. + * + * @param {Array} size + */ + _applySizeToPixi(size) + { + const size_px = util.to_px(size, this._units, this._win); + const scaleX = size_px[0] / this._movie.videoWidth; + const scaleY = size_px[1] / this._movie.videoHeight; + this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; + this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; + } + + /** + * Applies given size values to youtube iframe. + * + * @param {*} size + */ + _applySizeToYoutubeIframe(size) + { + const size_px = util.to_px(size, this._units, this._win); + this._youtubePlayer.setSize(size_px[ 0 ], size_px[ 1 ]); + } + + /** + * Ensures to convert NaN in the size values to proper, numerical values using given texture dimensions. + * + * @param {Array} size + */ + _ensureNaNSizeConversion(size, html5Video) + { + if (Number.isNaN(size[0]) && Number.isNaN(size[1])) + { + size = util.to_unit([html5Video.videoWidth, html5Video.videoHeight], "pix", this._win, this._units); + } + else if (Number.isNaN(size[0])) + { + size[0] = size[1] * (html5Video.videoWidth / html5Video.videoHeight); + } + else if (Number.isNaN(size[1])) + { + size[1] = size[0] / (html5Video.videoWidth / html5Video.videoHeight); + } + + return size; + } + + /** + * Estimate the bounding box. */ _estimateBoundingBox() { @@ -425,16 +846,10 @@ export class MovieStim extends VisualStim // opacity: this._pixi.alpha = this.opacity; - // set the scale: - const displaySize = this._getDisplaySize(); - const size_px = util.to_px(displaySize, this.units, this.win); - const scaleX = size_px[0] / this._pixiTextureResource.width; - const scaleY = size_px[1] / this._pixiTextureResource.height; - this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; - this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; - - this._pixi.width = size_px[0]; - this._pixi.height = size_px[1]; + // initial setSize might be called with incomplete values like [512, null]. + // Before texture is loaded they are converted to [512, NaN]. + // At this point the texture is loaded and we can convert NaN to proper values. + this.size = this._size; // set the position, rotation, and anchor (movie centered on pos): this._pixi.position = to_pixiPoint(this.pos, this.units, this.win); @@ -446,9 +861,11 @@ export class MovieStim extends VisualStim } /** - * Get the size of the display image, which is either that of the ImageStim or that of the image + * Get the size of the display image, which is either that of the MovieStim or that of the image * it contains. * + * @name module:visual.MovieStim#_getDisplaySize + * @private * @protected * @return {number[]} the size of the displayed image */ diff --git a/src/visual/ParticleEmitter.js b/src/visual/ParticleEmitter.js new file mode 100644 index 0000000..b0f1ed0 --- /dev/null +++ b/src/visual/ParticleEmitter.js @@ -0,0 +1,331 @@ +/** + * Particle Emitter. + * + * @author Nikita Agafonov + * @version 2023.2.0 + * @copyright (c) 2020-2023 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + */ + +import * as PIXI from "pixi.js-legacy"; + +const DEFAULT_POOL_SIZE = 1024; +const DEFAULT_PARTICLE_WIDTH = 10; +const DEFAULT_PARTICLE_HEIGHT = 10; +const DEFAULT_PARTICLE_LIFETIME = 3; // Seconds. +const DEFAULT_PARTICLE_COLOR = 0xffffff; +const DEFAULT_PARTICLES_PER_SEC = 60; +const DEFAULT_PARTICLE_V = 100; + +class Particle +{ + constructor (cfg) + { + this.x = 0; + this.y = 0; + this.ax = 0; + this.ay = 0; + this.vx = 0; + this.vy = 0; + this.lifeTime = 0; + this.widthChange = 0; + this.heightChange = 0; + this.sprite = undefined; + this.inUse = false; + + if (cfg.particleImage !== undefined) + { + this.sprite = PIXI.Sprite.from(PIXI.Texture.from(cfg.particleImage)); + } + else + { + this.sprite = new PIXI.Sprite(PIXI.Texture.WHITE); + this.sprite.tint = cfg.particleColor || DEFAULT_PARTICLE_COLOR; + } + + // TODO: Should we instead incorporate that in position calculation? + // Consider: accurate spawn position of the particle confined by spawnArea. + this.sprite.anchor.set(0.5); + + this.width = cfg.particleWidth || DEFAULT_PARTICLE_WIDTH; + this.height = cfg.particleHeight || DEFAULT_PARTICLE_HEIGHT; + } + + set width (w) + { + this._width = w; + this.sprite.width = w; + } + + get width () + { + return this._width; + } + + set height (h) + { + this._height = h; + this.sprite.height = h; + } + + get height () + { + return this._height; + } + + update (dt) + { + const dt2 = dt * dt; + + // Update velocity with current acceleration. + this.vx += this.ax * dt; + this.vy += this.ay * dt; + + // Update position with current velocity and acceleration. + this.x = this.x + this.vx * dt + this.ax * dt2 * .5; + this.y = this.y + this.vy * dt + this.ay * dt2 * .5; + + this.sprite.rotation = Math.atan2(this.vy, this.vx); + + this.sprite.x = this.x; + this.sprite.y = this.y; + + if (this.width > 0) + { + this.width = Math.max(0, this.width + this.widthChange); + } + + if (this.height > 0) + { + this.height = Math.max(0, this.height + this.heightChange); + } + this.lifeTime -= dt; + + if (this.width <= 0 && this.height <= 0) + { + this.lifeTime = 0; + } + + if (this.lifeTime <= 0) + { + this.inUse = false; + } + } +} + +export class ParticleEmitter +{ + constructor (cfg = {}) + { + this.x = 0; + this.y = 0; + this._cfg = cfg; + this._particlesPerSec = cfg.particlesPerSec || DEFAULT_PARTICLES_PER_SEC; + this._spawnCoolDown = 0; + this._parentObj = undefined; + this._particlePool = new Array(DEFAULT_POOL_SIZE); + this.setParentObject(cfg.parentObject); + this._fillParticlePool(cfg); + } + + _fillParticlePool (cfg) + { + let i; + for (i = 0; i < this._particlePool.length; i++) + { + this._particlePool[i] = new Particle(cfg); + } + } + + _setupParticle (p) + { + let spawnAreaWidth = this._cfg.spawnAreaWidth || 0; + let spawnAreaHeight = this._cfg.spawnAreaHeight || 0; + + if (this._parentObj !== undefined && this._cfg.useParentSizeAsSpawnArea) + { + spawnAreaWidth = this._parentObj.width; + spawnAreaHeight = this._parentObj.height; + } + + const spawnOffsetX = Math.random() * spawnAreaWidth - spawnAreaWidth * .5; + const spawnOffsetY = Math.random() * spawnAreaHeight - spawnAreaHeight * .5; + const x = this.x + spawnOffsetX; + const y = this.y + spawnOffsetY; + + p.x = x; + p.y = y; + + p.ax = 0; + p.ay = 0; + + if (Number.isFinite(this._cfg.initialVx)) + { + p.vx = this._cfg.initialVx; + } + else if (this._cfg.initialVx instanceof Array && this._cfg.initialVx.length >= 2) + { + p.vx = Math.random() * (this._cfg.initialVx[1] - this._cfg.initialVx[0]) + this._cfg.initialVx[0]; + } + else + { + p.vx = Math.random() * DEFAULT_PARTICLE_V - DEFAULT_PARTICLE_V * .5; + } + + if (Number.isFinite(this._cfg.initialVy)) + { + p.vy = this._cfg.initialVy; + } + else if (this._cfg.initialVy instanceof Array && this._cfg.initialVy.length >= 2) + { + p.vy = Math.random() * (this._cfg.initialVy[1] - this._cfg.initialVy[0]) + this._cfg.initialVy[0]; + } + else + { + p.vy = Math.random() * DEFAULT_PARTICLE_V - DEFAULT_PARTICLE_V * .5; + } + + p.lifeTime = this._cfg.lifeTime || DEFAULT_PARTICLE_LIFETIME; + p.width = this._cfg.particleWidth || DEFAULT_PARTICLE_WIDTH; + p.height = this._cfg.particleHeight || DEFAULT_PARTICLE_HEIGHT; + p.widthChange = this._cfg.particleWidthChange || 0; + p.heightChange = this._cfg.particleHeightChange || 0; + + // TODO: run proper checks here. + if (this._cfg.particleImage) + { + p.sprite.texture = PIXI.Texture.from(this._cfg.particleImage); + } + else + { + p.sprite.texture = PIXI.Texture.WHITE; + } + + if (this._cfg.particleColor !== undefined) + { + p.sprite.tint = this._cfg.particleColor; + } + else + { + p.sprite.tint = 0xffffff; + } + } + + _spawnParticles (n = 0) + { + let i; + for (i = 0; i < this._particlePool.length && n > 0; i++) + { + if (this._particlePool[i].inUse === false) + { + this._particlePool[i].inUse = true; + n--; + + this._setupParticle(this._particlePool[i]); + this._cfg.container.addChild(this._particlePool[i].sprite); + } + } + } + + _getResultingExternalForce () + { + let externalForce = [0, 0]; + if (this._cfg.externalForces instanceof Array) + { + let i; + for (i = 0; i < this._cfg.externalForces.length; i++) + { + externalForce[0] += this._cfg.externalForces[i][0]; + externalForce[1] += this._cfg.externalForces[i][1]; + } + } + + return externalForce; + } + + setParentObject (po) + { + this._parentObj = po; + } + + /** + * @desc: Adds external force which acts on a particle + * @param: f - Array with two elements, first is x component, second is y component. + * It's a vector of length L which sets the direction and the margnitude of the force. + * */ + addExternalForce (f) + { + this._cfg.externalForces.push(f); + } + + removeExternalForce (f) + { + const i = this._cfg.externalForces.indexOf(f); + if (i !== -1) + { + this._cfg.externalForces.splice(i, 1); + } + } + + removeExternalForceByIdx (idx) + { + if (this._cfg.externalForces[idx] !== undefined) + { + this._cfg.externalForces.splice(idx, 1); + } + } + + update (dt) + { + let externalForce; + + // Sync with parent object if it exists. + if (this._parentObj !== undefined) + { + this.x = this._parentObj.x; + this.y = this._parentObj.y; + } + + if (Number.isFinite(this._cfg.positionOffsetX)) + { + this.x += this._cfg.positionOffsetX; + } + + if (Number.isFinite(this._cfg.positionOffsetY)) + { + this.y += this._cfg.positionOffsetY; + } + + if (this._spawnCoolDown <= 0) + { + this._spawnCoolDown = 1 / this._particlesPerSec; + + // Assuming that we have at least 60FPS. + const frameTime = Math.min(dt, 1 / 60); + const particlesPerFrame = Math.ceil(frameTime / this._spawnCoolDown); + this._spawnParticles(particlesPerFrame); + } + else + { + this._spawnCoolDown -= dt; + } + + let i; + for (i = 0; i < this._particlePool.length; i++) + { + if (this._particlePool[i].inUse) + { + externalForce = this._getResultingExternalForce(); + this._particlePool[i].ax = externalForce[0]; + this._particlePool[i].ay = externalForce[1]; + this._particlePool[i].update(dt); + } + + // Check if particle should be removed. + if (this._particlePool[i].lifeTime <= 0 && this._particlePool[i].sprite.parent) + { + this._cfg.container.removeChild(this._particlePool[i].sprite); + } + } + } +} diff --git a/src/visual/Polygon.js b/src/visual/Polygon.js index 6425cf6..1f67dfa 100644 --- a/src/visual/Polygon.js +++ b/src/visual/Polygon.js @@ -39,8 +39,9 @@ export class Polygon extends ShapeStim * @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ - constructor({ name, win, lineWidth, lineColor, fillColor, opacity, edges, radius, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog } = {}) + constructor({ name, win, lineWidth, lineColor, fillColor, opacity, edges, radius, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog, draggable } = {}) { super({ name, @@ -58,9 +59,11 @@ export class Polygon extends ShapeStim interpolate, autoDraw, autoLog, + draggable }); - this._psychoJS.logger.debug("create a new Polygon with name: ", name); + this._psychoJS.logger.debug("create a new Polygon with name: ", + name); this._addAttribute( "edges", diff --git a/src/visual/Progress.js b/src/visual/Progress.js new file mode 100644 index 0000000..0d27771 --- /dev/null +++ b/src/visual/Progress.js @@ -0,0 +1,162 @@ +import * as PIXI from "pixi.js-legacy"; +import * as util from "../util/Util.js"; +import { Color } from "../util/Color.js"; +import { to_pixiPoint } from "../util/Pixi.js"; +import { VisualStim } from "./VisualStim.js"; + +export class Progress extends VisualStim +{ + constructor ( + { + name, + win, + units = "pix", + ori, + opacity, + depth, + pos, + anchor = "left", + size = [300, 30], + clipMask, + autoDraw, + autoLog, + progress = 1, + type, + fillColor, + fillTexture + }) + { + super({ + name, + win, + units, + ori, + opacity, + depth, + pos, + anchor, + size, + clipMask, + autoDraw, + autoLog + }); + + this._addAttribute("progress", progress, 0); + this._addAttribute("type", type, PROGRESS_TYPES.BAR); + this._addAttribute("fillColor", fillColor, "lightgreen"); + this._addAttribute("fillTexture", fillTexture, PIXI.Texture.WHITE); + + if (this._autoLog) + { + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + } + } + + /** + * Setter for the progress attribute. + */ + setProgress (progress = 0, log = false) + { + this._setAttribute("progress", Math.min(1.0, Math.max(0.0, progress)), log); + if (this._pixi !== undefined) + { + this._pixi.clear(); + const size_px = util.to_px(this._size, this._units, this._win); + const progressWidth = size_px[0] * this._progress; + if (this._fillTexture) + { + let t = PIXI.Texture.WHITE; + if (typeof this._fillTexture === "string") + { + t = PIXI.Texture.from(this._fillTexture); + t.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST; + } + this._pixi.beginTextureFill({ + texture: t + }); + } + else + { + this._pixi.beginFill(new Color(this._fillColor).int, this._opacity); + } + + if (this._type === PROGRESS_TYPES.BAR) + { + this._pixi.drawRect(0, 0, progressWidth, size_px[1]); + } + + this._pixi.endFill(); + + // TODO: is there a better way to ensure anchor works? + this.anchor = this._anchor; + } + } + + /** + * Estimate the bounding box. + * + * @override + * @protected + */ + _estimateBoundingBox() + { + let boundingBox = new PIXI.Rectangle(0, 0, 0, 0); + const anchorNum = this._anchorTextToNum(this._anchor); + const pos_px = util.to_px(this._pos, this._units, this._win); + const size_px = util.to_px(this._size, this._units, this._win); + boundingBox.x = pos_px[ 0 ] - anchorNum[ 0 ] * size_px[ 0 ]; + boundingBox.y = pos_px[ 1 ] - anchorNum[ 1 ] * size_px[ 1 ]; + boundingBox.width = size_px[ 0 ]; + boundingBox.height = size_px[ 1 ]; + + this._boundingBox = boundingBox; + } + + /** + * Update the stimulus, if necessary. + * + * @protected + */ + _updateIfNeeded() + { + // TODO: figure out what is the error with estimateBoundBox on resize? + if (!this._needUpdate) + { + return; + } + this._needUpdate = false; + + // update the PIXI representation, if need be: + if (this._needPixiUpdate) + { + this._needPixiUpdate = false; + + if (typeof this._pixi !== "undefined") + { + this._pixi.destroy(true); + } + this._pixi = new PIXI.Graphics(); + // TODO: Should we do this? + // this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5); + + // TODO: Should just .setProgress() be called? + this.setProgress(this._progress); + + this._pixi.scale.y = -1; + this._pixi.zIndex = -this._depth; + this.anchor = this._anchor; + } + + // set polygon position and rotation: + this._pixi.position = to_pixiPoint(this._pos, this._units, this._win); + this._pixi.rotation = -this.ori * Math.PI / 180.0; + + this._estimateBoundingBox(); + } +} + +export const PROGRESS_TYPES = +{ + BAR: 0, + CIRCLE: 1 +} diff --git a/src/visual/Rect.js b/src/visual/Rect.js index b5eb565..9b87164 100644 --- a/src/visual/Rect.js +++ b/src/visual/Rect.js @@ -38,8 +38,9 @@ export class Rect extends ShapeStim * @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ - constructor({ name, win, lineWidth, lineColor, fillColor, opacity, width, height, pos, anchor, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog } = {}) + constructor({ name, win, lineWidth, lineColor, fillColor, opacity, width, height, pos, anchor, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog, draggable } = {}) { super({ name, @@ -58,6 +59,7 @@ export class Rect extends ShapeStim interpolate, autoDraw, autoLog, + draggable }); this._psychoJS.logger.debug("create a new Rect with name: ", name); diff --git a/src/visual/ShapeStim.js b/src/visual/ShapeStim.js index 49b1049..8630a85 100644 --- a/src/visual/ShapeStim.js +++ b/src/visual/ShapeStim.js @@ -44,10 +44,11 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin * @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ - constructor({ name, win, lineWidth, lineColor, fillColor, opacity, vertices, closeShape, pos, anchor, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog } = {}) + constructor({ name, win, lineWidth, lineColor, fillColor, opacity, vertices, closeShape, pos, anchor, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog, draggable } = {}) { - super({ name, win, units, ori, opacity, pos, anchor, depth, size, autoDraw, autoLog }); + super({ name, win, units, ori, opacity, pos, anchor, depth, size, autoDraw, autoLog, draggable }); // the PIXI polygon corresponding to the vertices, in pixel units: this._pixiPolygon_px = undefined; @@ -163,8 +164,8 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin if (typeof objectPos_px === "undefined") { throw { - origin: "VisualStim.contains", - context: "when determining whether VisualStim: " + this._name + " contains object: " + util.toString(object), + origin: "ShapeStim.contains", + context: "when determining whether ShapeStim: " + this._name + " contains object: " + util.toString(object), error: "unable to determine the position of the object", }; } @@ -176,6 +177,22 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin return util.IsPointInsidePolygon(objectPos_px, polygon_px); } + /** + * Determine whether a point that is nown to have pixel dimensions is inside the bounding box of the stimulus. + * + * @name module:visual.ShapeStim#containsPointPx + * @public + * @param {number[]} point_px - the point in pixels + * @return {boolean} whether or not the object is inside the bounding box of the stimulus + */ + containsPointPx (point_px) + { + const pos_px = util.to_px(this.pos, this.units, this.win); + this._getVertices_px(); + const polygon_px = this._vertices_px.map((v) => [v[0] + pos_px[0], v[1] + pos_px[1]]); + return util.IsPointInsidePolygon(point_px, polygon_px); + } + /** * Setter for the anchor attribute. * @@ -385,4 +402,29 @@ ShapeStim.KnownShapes = { [-0.39, 0.31], [-0.09, 0.18], ], + + triangle: [ + [+0.0, 0.5], // Point + [-0.5, -0.5], // Bottom left + [+0.5, -0.5], // Bottom right + ], + + rectangle: [ + [-.5, .5], // Top left + [ .5, .5], // Top right + [ .5, -.5], // Bottom left + [-.5, -.5], // Bottom right + ], + + arrow: [ + [0.0, 0.5], + [-0.5, 0.0], + [-1/6, 0.0], + [-1/6, -0.5], + [1/6, -0.5], + [1/6, 0.0], + [0.5, 0.0], + ], }; +// Alias some names for convenience +ShapeStim.KnownShapes['star'] = ShapeStim.KnownShapes['star7'] diff --git a/src/visual/Slider.js b/src/visual/Slider.js index c76e65d..4d85032 100644 --- a/src/visual/Slider.js +++ b/src/visual/Slider.js @@ -65,6 +65,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin) * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every * frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device * * @param {core.MinimalStim[]} [options.dependentStims = [] ] - the list of dependent stimuli, * which must be updated when this Slider is updated, e.g. a Form. @@ -99,10 +100,11 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin) autoDraw, autoLog, dependentStims, + draggable } = {}, ) { - super({ name, win, units, ori, opacity, depth, pos, size, clipMask, autoDraw, autoLog }); + super({ name, win, units, ori, opacity, depth, pos, size, clipMask, autoDraw, autoLog, draggable }); this._needMarkerUpdate = false; diff --git a/src/visual/Survey.js b/src/visual/Survey.js index d4cf781..57bf41f 100644 --- a/src/visual/Survey.js +++ b/src/visual/Survey.js @@ -23,26 +23,7 @@ import MatrixBipolar from "./survey/components/MatrixBipolar.js"; import DropdownExtensions from "./survey/components/DropdownExtensions.js"; import customExpressionFunctionsArray from "./survey/extensions/customExpressionFunctions.js"; -const CAPTIONS = { - NEXT: "Next" -}; -const SURVEY_SETTINGS = { - minWidth: "100px" -}; - -const SURVEY_COMPLETION_CODES = -{ - NORMAL: 0, - SKIP_TO_END_OF_BLOCK: 1, - SKIP_TO_END_OF_SURVEY: 2 -}; - -const NODE_EXIT_CODES = -{ - NORMAL: 0, - BREAK_FLOW: 1 -}; /** * Survey Stimulus. @@ -63,6 +44,24 @@ export class Survey extends VisualStim ENDSURVEY: "END" }; + static CAPTIONS = + { + NEXT: "Next" + }; + + static SURVEY_COMPLETION_CODES = + { + NORMAL: 0, + SKIP_TO_END_OF_BLOCK: 1, + SKIP_TO_END_OF_SURVEY: 2 + }; + + static NODE_EXIT_CODES = + { + NORMAL: 0, + BREAK_FLOW: 1 + }; + /** * @memberOf module:visual * @param {Object} options @@ -83,19 +82,16 @@ export class Survey extends VisualStim { super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog }); - // the default surveyId is an uuid based on the experiment id (or name) and the survey name: - // this way, it is always the same within a given experiment - this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined"); - const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ? - util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) : - util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`); + // Storing all existing signaturePad questions to properly handle their resize. + // Unfortunately signaturepad question type can't handle resizing properly by itself. + this._signaturePads = []; // whether the user is done with the survey, independently of whether the survey is completed: this.isFinished = false; - // Accumulated completion flag that is being set after completion of one survey node. - // This flag allows to track completion progress while moving through the survey flow. - // Initially set to true and will be flipped if at least one of the survey nodes were not fully completed. + // accumulated completion flag updated after each survey node is completed + // note: this make it possible to track completion as we move through the survey flow. + // _isCompletedAll will be flipped to false whenever a survey node is not completed this._isCompletedAll = true; // timestamps associated to each question: @@ -103,10 +99,9 @@ export class Survey extends VisualStim // timestamps clock: this._questionAnswerTimestampClock = new Clock(); - this._totalSurveyResults = {}; + this._overallSurveyResults = {}; this._surveyData = undefined; this._surveyModel = undefined; - this._signaturePadRO = undefined; this._expressionsRunner = undefined; this._lastPageSwitchHandledIdx = -1; this._variables = {}; @@ -114,23 +109,36 @@ export class Survey extends VisualStim this._surveyRunningPromise = undefined; this._surveyRunningPromiseResolve = undefined; this._surveyRunningPromiseReject = undefined; - // callback triggered when the user is done with the survey: nothing to do by default this._onFinishedCallback = () => {}; - // init SurveyJS + // init SurveyJS: this._initSurveyJS(); + // default size: + if (typeof size === "undefined") + { + this.size = (this.unit === "norm") ? [2.0, 2.0] : [1.0, 1.0]; + } + this._addAttribute( "model", model ); + + // the default surveyId is an uuid based on the experiment id (or name) and the survey name: + // this way, it is always the same within a given experiment + this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined"); + const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ? + util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) : + util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`); this._addAttribute( "surveyId", surveyId, defaultSurveyId ); + // estimate the bounding box: this._estimateBoundingBox(); @@ -213,7 +221,7 @@ export class Survey extends VisualStim logs: [] }; - this.psychoJS.logger.debug(`converted the old model to the new super-flow model: ${JSON.stringify(model)}`); + this.psychoJS.logger.debug(`converted the legacy model to the new super-flow model: ${JSON.stringify(model)}`); } this._surveyData = model; @@ -227,6 +235,24 @@ export class Survey extends VisualStim } } + /** + * Setter for the surveyId attribute. + * + * @param {string} surveyId - the survey Id + * @param {boolean} [log= false] - whether to log + * @return {void} + */ + setSurveyId(surveyId, log = false) + { + this._setAttribute("surveyId", surveyId, log); + + // only update the model if a genuine surveyId was given as parameter to the Survey: + if (!this._hasSelfGeneratedSurveyId) + { + this.setModel(`${surveyId}.sid`, log); + } + } + /** * Set survey variables. * @@ -254,7 +280,8 @@ export class Survey extends VisualStim { if (excludedNames.indexOf(name) === -1) { - this._surveyData.variables[name] = variables[name]; + this._variables[name] = variables[name]; + // this._surveyData.variables[name] = variables[name]; } } } @@ -282,22 +309,6 @@ export class Survey extends VisualStim return this._surveyModel.runExpression(expression); } - /** - * Setter for the surveyId attribute. - * - * @param {string} surveyId - the survey Id - * @param {boolean} [log= false] - whether to log - * @return {void} - */ - setSurveyId(surveyId, log = false) - { - this._setAttribute("surveyId", surveyId, log); - if (!this._hasSelfGeneratedSurveyId) - { - this.setModel(`${surveyId}.sid`, log); - } - } - /** * Add a callback that will be triggered when the participant finishes the survey. * @@ -336,7 +347,7 @@ export class Survey extends VisualStim // return this._surveyModel.data; - return this._totalSurveyResults; + return this._overallSurveyResults; } /** @@ -374,7 +385,6 @@ export class Survey extends VisualStim {} ); - // if the response cannot be uploaded, e.g. the experiment is running locally, or // if it is piloting mode, then we offer the response as a file for download: if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER || @@ -420,9 +430,7 @@ export class Survey extends VisualStim */ hide() { - // if a survey div already does not exist already, create it: - const surveyId = `survey-${this._name}`; - const surveyDiv = document.getElementById(surveyId); + const surveyDiv = document.getElementById(this._surveyDivId); if (surveyDiv !== null) { document.body.removeChild(surveyDiv); @@ -468,9 +476,9 @@ export class Survey extends VisualStim this._needPixiUpdate = false; // if a survey div does not exist, create it: - if (document.getElementById("_survey") === null) + if (document.getElementById(this._surveyDivId) === null) { - document.body.insertAdjacentHTML("beforeend", "
") + document.body.insertAdjacentHTML("beforeend", `
`) } // start the survey flow: @@ -513,8 +521,7 @@ export class Survey extends VisualStim */ _registerCustomExpressionFunctions (Survey, customFuncs = []) { - let i; - for (i = 0; i < customFuncs.length; i++) + for (let i = 0; i < customFuncs.length; i++) { Survey.FunctionFactory.Instance.register(customFuncs[i].func.name, customFuncs[i].func, customFuncs[i].isAsync); } @@ -579,6 +586,7 @@ export class Survey extends VisualStim this._questionAnswerTimestamps[questionData.name].timestamp = this._questionAnswerTimestampClock.getTime(); } +/* // This probably needs to be moved to some kind of utils.js. // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle _FisherYatesShuffle (targetArray = []) @@ -613,6 +621,7 @@ export class Survey extends VisualStim return inOutArray; } +*/ _composeModelWithRandomizedQuestions (surveyModel, inBlockRandomizationSettings) { @@ -621,31 +630,32 @@ export class Survey extends VisualStim // Hence creating a fresh survey data object with shuffled question order. let questions = []; let questionsMap = {}; - let shuffledQuestions; let newSurveyModel = { pages:[{ elements: new Array(inBlockRandomizationSettings.questionsPerPage) }] }; - let i, j, k; - for (i = 0; i < surveyModel.pages.length; i++) + for (let i = 0; i < surveyModel.pages.length; i++) { - for (j = 0; j < surveyModel.pages[i].elements.length; j++) + for (let j = 0; j < surveyModel.pages[i].elements.length; j++) { questions.push(surveyModel.pages[i].elements[j]); - k = questions.length - 1; + const k = questions.length - 1; questionsMap[questions[k].name] = questions[k]; } } if (inBlockRandomizationSettings.layout.length > 0) { - j = 0; - k = 0; + let j = 0; + let k = 0; let curPage = 0; let curElement = 0; - const shuffledSet0 = this._FisherYatesShuffle(inBlockRandomizationSettings.set0); - const shuffledSet1 = this._FisherYatesShuffle(inBlockRandomizationSettings.set1); - for (i = 0; i < inBlockRandomizationSettings.layout.length; i++) + + const shuffledSet0 = util.shuffle(Array.from(inBlockRandomizationSettings.set0)); + const shuffledSet1 = util.shuffle(Array.from(inBlockRandomizationSettings.set1)); + // const shuffledSet0 = this._FisherYatesShuffle(inBlockRandomizationSettings.set0); + // const shuffledSet1 = this._FisherYatesShuffle(inBlockRandomizationSettings.set1); + for (let i = 0; i < inBlockRandomizationSettings.layout.length; i++) { // Create new page if questionsPerPage reached. if (curElement === inBlockRandomizationSettings.questionsPerPage) @@ -675,12 +685,14 @@ export class Survey extends VisualStim else if (inBlockRandomizationSettings.showOnly > 0) { // TODO: Check if there can be questionsPerPage applicable in this case. - shuffledQuestions = this._FisherYatesShuffle(questions); + const shuffledQuestions = util.shuffle(Array.from(questions)); + // shuffledQuestions = this._FisherYatesShuffle(questions); newSurveyModel.pages[0].elements = shuffledQuestions.splice(0, inBlockRandomizationSettings.showOnly); } else { // TODO: Check if there can be questionsPerPage applicable in this case. - newSurveyModel.pages[0].elements = this._FisherYatesShuffle(questions); + newSurveyModel.pages[0].elements = util.shuffle(Array.from(questions)); + // newSurveyModel.pages[0].elements = this._FisherYatesShuffle(questions); } console.log("model recomposition took", performance.now() - t); console.log("recomposed model:", newSurveyModel); @@ -714,12 +726,14 @@ export class Survey extends VisualStim if (inQuestionRandomizationSettings.randomizeAll) { - questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]); + questionData[choicesFieldName] = util.shuffle(Array.from(questionData[choicesFieldName])); + // questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]); // Handle dynamic choices. } else if (inQuestionRandomizationSettings.showOnly > 0) { - questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly); + questionData[choicesFieldName] = util.shuffle(Array.from(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly)); + // questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly); } else if (inQuestionRandomizationSettings.reverse) { @@ -739,8 +753,10 @@ export class Survey extends VisualStim // Creating new array of choices to which we're going to write from randomized/reversed sets. questionData[choicesFieldName] = new Array(inQuestionRandomizationSettings.layout.length); - const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0); - const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1); + const shuffledSet0 = util.shuffle(Array.from(inQuestionRandomizationSettings.set0)); + const shuffledSet1 = util.shuffle(Array.from(inQuestionRandomizationSettings.set1)); + // const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0); + // const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1); const reversedSet = Math.round(Math.random()) === 1 ? inQuestionRandomizationSettings.reverseOrder.reverse() : inQuestionRandomizationSettings.reverseOrder; for (i = 0; i < inQuestionRandomizationSettings.layout.length; i++) { @@ -861,12 +877,12 @@ export class Survey extends VisualStim if (skipLogic.destination === "ENDOFSURVEY") { surveyModel.setCompleted(); - this._surveyRunningPromiseResolve(SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY); + this._surveyRunningPromiseResolve(Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY); } else if (skipLogic.destination === "ENDOFBLOCK") { surveyModel.setCompleted(); - this._surveyRunningPromiseResolve(SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK); + this._surveyRunningPromiseResolve(Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK); } else { @@ -896,13 +912,12 @@ export class Survey extends VisualStim * * @param surveyModel * @param options - * @private + * @protected */ _onSurveyComplete(surveyModel, options) { - Object.assign(this._totalSurveyResults, surveyModel.data); - this._detachResizeObservers(); - let completionCode = SURVEY_COMPLETION_CODES.NORMAL; + Object.assign(this._overallSurveyResults, surveyModel.data); + let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL; const questions = surveyModel.getAllQuestions(); // It is guaranteed that the question with skip logic is always last on the page. @@ -916,12 +931,12 @@ export class Survey extends VisualStim { if (skipLogic.destination === "ENDOFSURVEY") { - completionCode = SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY; + completionCode = Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY; surveyModel.setCompleted(); } else if (skipLogic.destination === "ENDOFBLOCK") { - completionCode = SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK; + completionCode = Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK; } } } @@ -976,22 +991,18 @@ export class Survey extends VisualStim * Run the survey using flow data provided. This method runs recursively. * * @protected - * @param {string} surveyId - the id of the DOM div * @param {Object} surveyData - surveyData / model. - * @param {Object} prevBlockResults - survey results gathered from running previous block of questions. + * @param {Object} surveyFlowBlock - XXX * @return {void} */ _beginSurvey(surveyData, surveyFlowBlock) { - let j; - let surveyIdx; this._lastPageSwitchHandledIdx = -1; - surveyIdx = surveyFlowBlock.surveyIdx; - console.log("playing survey with idx", surveyIdx); + const surveyIdx = surveyFlowBlock.surveyIdx; let surveyModelInput = this._processSurveyData(surveyData, surveyIdx); this._surveyModel = new window.Survey.Model(surveyModelInput); - for (j in this._variables) + for (let j in this._variables) { // Adding variables directly to hash to get higher performance (this is instantaneous compared to .setVariable()). // At this stage we don't care to trigger all the callbacks like .setVariable() does, since this is very beginning of survey presentation. @@ -1010,7 +1021,7 @@ export class Survey extends VisualStim this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this)); } - const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || CAPTIONS.NEXT) : undefined; + const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined; jQuery(".survey").Survey({ model: this._surveyModel, showItemsInOrder: "column", @@ -1033,15 +1044,11 @@ export class Survey extends VisualStim async _runSurveyFlow(surveyBlock, surveyData, prevBlockResults = {}) { - // let surveyBlock; - let surveyIdx; - let surveyCompletionCode; - let nodeExitCode = NODE_EXIT_CODES.NORMAL; - let i, j; + let nodeExitCode = Survey.NODE_EXIT_CODES.NORMAL; if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL) { - const dataset = Object.assign({}, this._totalSurveyResults, this._variables); + const dataset = Object.assign({}, this._overallSurveyResults, this._variables); this._expressionsRunner.expressionExecutor.setExpression(surveyBlock.condition); if (this._expressionsRunner.run(dataset) && surveyBlock.nodes[0] !== undefined) { @@ -1054,13 +1061,14 @@ export class Survey extends VisualStim } else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.RANDOMIZER) { - this._InPlaceFisherYatesShuffle(surveyBlock.nodes, 0, surveyBlock.nodes.length - 1); + util.shuffle(surveyBlock.nodes, Math.random, 0, surveyBlock.nodes.length - 1); + // this._InPlaceFisherYatesShuffle(surveyBlock.nodes, 0, surveyBlock.nodes.length - 1); } else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.EMBEDDED_DATA) { let t = performance.now(); const surveyBlockData = surveyData.embeddedData[surveyBlock.dataIdx]; - for (j = 0; j < surveyBlockData.length; j++) + for (let j = 0; j < surveyBlockData.length; j++) { // TODO: handle the rest data types. if (surveyBlockData[j].type === "Custom") @@ -1089,28 +1097,28 @@ export class Survey extends VisualStim this._surveyModel.setCompleted(); } console.log("EndSurvey block encountered, exiting."); - nodeExitCode = NODE_EXIT_CODES.BREAK_FLOW; + nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW; } else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT) { - surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock); + const surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock); Object.assign({}, prevBlockResults, this._surveyModel.data); // SkipLogic had destination set to ENDOFSURVEY. - if (surveyCompletionCode === SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY) + if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY) { - nodeExitCode = NODE_EXIT_CODES.BREAK_FLOW; + nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW; } } - if (nodeExitCode === NODE_EXIT_CODES.NORMAL && + if (nodeExitCode === Survey.NODE_EXIT_CODES.NORMAL && surveyBlock.type !== Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL && surveyBlock.nodes instanceof Array) { - for (i = 0; i < surveyBlock.nodes.length; i++) + for (let i = 0; i < surveyBlock.nodes.length; i++) { nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[i], surveyData, prevBlockResults); - if (nodeExitCode === NODE_EXIT_CODES.BREAK_FLOW) + if (nodeExitCode === Survey.NODE_EXIT_CODES.BREAK_FLOW) { break; } @@ -1131,53 +1139,64 @@ export class Survey extends VisualStim this._lastPageSwitchHandledIdx = -1; } - _handleSignaturePadResize (entries) + _handleWindowResize(e) { - let signatureCanvas; - let q; - let i; - for (i = 0; i < entries.length; i++) + if (this._surveyModel) { - signatureCanvas = entries[i].target.querySelector("canvas"); - q = this._surveyModel.getQuestionByName(entries[i].target.dataset.name); - q.signatureWidth = Math.min(q.maxSignatureWidth, entries[i].contentBoxSize[0].inlineSize); + for (let i = this._signaturePads.length - 1; i >= 0; i--) + { + // As of writing this (24.03.2023). SurveyJS doesn't have a proper event + // for question being removed from nested locations, such as dynamic panel. + // However, surveyJS will set .signaturePad property to null once the question is removed. + // Utilising this knowledge to sync our lists. + if (this._signaturePads[ i ].question.signaturePad) + { + this._signaturePads[ i ].question.signatureWidth = Math.min( + this._signaturePads[i].question.maxSignatureWidth, + this._signaturePads[ i ].htmlElement.getBoundingClientRect().width + ); + } + else + { + // Signature pad was removed. Syncing list. + this._signaturePads.splice(i, 1); + } + } } } - _addEventListeners () + _addEventListeners() { - this._signaturePadRO = new ResizeObserver(this._handleSignaturePadResize.bind(this)); + window.addEventListener("resize", (e) => this._handleWindowResize(e)); } _handleAfterQuestionRender (sender, options) { if (options.question.getType() === "signaturepad") { - this._signaturePadRO.observe(options.htmlElement); + this._signaturePads.push(options); + options.question.signatureWidth = Math.min(options.question.maxSignatureWidth, options.htmlElement.getBoundingClientRect().width); } } - _detachResizeObservers () - { - this._signaturePadRO.disconnect(); - } - /** - * Init the SurveyJS.io library. + * Init the SurveyJS.io library and various extensions, setup the theme. * * @protected */ _initSurveyJS() { - // load the Survey.js libraries, if necessary: - // TODO + // note: the Survey.js libraries must be added to the list of resources in PsychoJS.start: + // psychoJS.start({ resources: [ {'surveyLibrary': true}, ... ], ...}); + + // id of the SurveyJS html div: + this._surveyDivId = `survey-${this._name}`; - // load the PsychoJS SurveyJS extensions: - this._expressionsRunner = new window.Survey.ExpressionRunner(); this._registerCustomExpressionFunctions(window.Survey, customExpressionFunctionsArray); this._registerWidgets(window.Survey); this._registerCustomSurveyProperties(window.Survey); this._addEventListeners(); + this._expressionsRunner = new window.Survey.ExpressionRunner(); // setup the survey theme: window.Survey.Serializer.getProperty("expression", "minWidth").defaultValue = "100px"; diff --git a/src/visual/TextBox.js b/src/visual/TextBox.js index 4d8e2bc..90afbde 100644 --- a/src/visual/TextBox.js +++ b/src/visual/TextBox.js @@ -52,6 +52,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log * @param {boolean} [options.fitToContent = false] - whether or not to resize itself automaitcally to fit to the text content + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ constructor( { @@ -86,11 +87,13 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) clipMask, autoDraw, autoLog, - fitToContent + fitToContent, + draggable, + boxFn } = {}, ) { - super({ name, win, pos, anchor, size, units, ori, opacity, depth, clipMask, autoDraw, autoLog }); + super({ name, win, pos, anchor, size, units, ori, opacity, depth, clipMask, autoDraw, autoLog, draggable }); this._addAttribute( "text", @@ -202,12 +205,14 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) // and setSize called from super class would not have a proper effect this.setSize(size); + this._addAttribute("boxFn", boxFn, null); + // estimate the bounding box: this._estimateBoundingBox(); if (this._autoLog) { - this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`); } } @@ -481,6 +486,26 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) alignmentStyles = ["center", "center"]; } + let box; + if (this._boxFn !== null) + { + box = this._boxFn; + } + else + { + // note: box style properties eventually become PIXI.Graphics settings, so same syntax applies + box = { + fill: new Color(this._fillColor).int, + alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1, + rounded: 5, + stroke: { + color: new Color(this._borderColor).int, + width: borderWidth_px, + alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1 + } + }; + } + return { // input style properties eventually become CSS, so same syntax applies input: { @@ -504,41 +529,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) overflow: "hidden", pointerEvents: "none" }, - // box style properties eventually become PIXI.Graphics settings, so same syntax applies - box: { - fill: new Color(this._fillColor).int, - alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px, - alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1 - }, - /*default: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }, - focused: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }, - disabled: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }*/ - }, + box }; } diff --git a/src/visual/TextStim.js b/src/visual/TextStim.js index 8255ccf..c4fd922 100644 --- a/src/visual/TextStim.js +++ b/src/visual/TextStim.js @@ -49,6 +49,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin) * @param {PIXI.Graphics} [options.clipMask= null] - the clip mask * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ constructor( { @@ -75,10 +76,11 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin) clipMask, autoDraw, autoLog, + draggable } = {}, ) { - super({ name, win, units, ori, opacity, depth, pos, anchor, clipMask, autoDraw, autoLog }); + super({ name, win, units, ori, opacity, depth, pos, anchor, clipMask, autoDraw, autoLog, draggable }); // callback to deal with text metrics invalidation: const onChange = (withPixi = false, withBoundingBox = false, withMetrics = false) => diff --git a/src/visual/VisualStim.js b/src/visual/VisualStim.js index d74d6bb..b47bc09 100644 --- a/src/visual/VisualStim.js +++ b/src/visual/VisualStim.js @@ -35,8 +35,9 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin) * @param {PIXI.Graphics} [options.clipMask= null] - the clip mask * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log + * @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device */ - constructor({ name, win, units, ori, opacity, depth, pos, anchor, size, clipMask, autoDraw, autoLog } = {}) + constructor({ name, win, units, ori, opacity, depth, pos, anchor, size, clipMask, autoDraw, autoLog, draggable } = {}) { super({ win, name, autoDraw, autoLog }); @@ -84,6 +85,12 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin) null, this._onChange(false, false), ); + this._addAttribute("draggable", draggable, false); + + // data needed to properly support drag and drop functionality + this._associatedPointerId = undefined; + this._initialPointerOffset = [0, 0]; + this._pointerEventHandlersUuids = {}; // bounding box of the stimulus, in stimulus units // note: boundingBox does not take the orientation into account @@ -96,6 +103,14 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin) this._needPixiUpdate = true; } + /** + * Whether or not stimuli is being dragged by pointer. Works in conjunction with draggable attribute. + */ + get isDragging() + { + return this._associatedPointerId !== undefined; + } + /** * Force a refresh of the stimulus. * @@ -179,15 +194,45 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin) } } + /** + * Setter for the draggable attribute. + * + * @name module:visual.VisualStim#setDraggable + * @public + * @param {boolean} [draggable=false] - whether or not to make stim draggable using mouse/touch/other pointer device + * @param {boolean} [log= false] - whether of not to log + */ + setDraggable(draggable = false, log = false) + { + const hasChanged = this._setAttribute("draggable", draggable, log); + if (hasChanged) + { + if (draggable) + { + this._pointerEventHandlersUuids[ "pointerdown" ] = this._win.on("pointerdown", this._handlePointerDown.bind(this)); + this._pointerEventHandlersUuids[ "pointerup" ] = this._win.on("pointerup", this._handlePointerUp.bind(this)); + this._pointerEventHandlersUuids[ "pointermove" ] = this._win.on("pointermove", this._handlePointerMove.bind(this)); + } + else + { + this._win.off("pointerdown", this._pointerEventHandlersUuids[ "pointerdown" ]); + this._win.off("pointerup", this._pointerEventHandlersUuids[ "pointerup" ]); + this._win.off("pointermove", this._pointerEventHandlersUuids[ "pointermove" ]); + } + } + } + /** * Setter for the depth attribute. * * @param {Array.} depth - order in which stimuli is rendered, kind of css's z-index with a negative sign. * @param {boolean} [log= false] - whether of not to log */ - setDepth (depth = 0, log = false) { + setDepth(depth = 0, log = false) + { this._setAttribute("depth", depth, log); - if (this._pixi) { + if (this._pixi) + { this._pixi.zIndex = -this._depth; } } @@ -217,6 +262,93 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin) return this._getBoundingBox_px().contains(objectPos_px[0], objectPos_px[1]); } + /** + * Determine whether a point that is nown to have pixel dimensions is inside the bounding box of the stimulus. + * + * @name module:visual.VisualStim#containsPointPx + * @public + * @param {number[]} point_px - the point in pixels + * @return {boolean} whether or not the object is inside the bounding box of the stimulus + */ + containsPointPx (point_px) + { + return this._getBoundingBox_px().contains(point_px[0], point_px[1]); + } + + /** + * Release the PIXI representation, if there is one. + * + * @name module:core.VisualStim#release + * @function + * @public + * + * @param {boolean} [log= false] - whether or not to log + */ + release(log = false) + { + this.draggable = false; + super.release(log); + } + + /** + * Handler of pointerdown event. + * + * @name module:visual.VisualStim#_handlePointerDown + * @private + * @param {Object} e - pointerdown event data. + */ + _handlePointerDown (e) + { + if (e.pixi === undefined || e.pixi !== this._pixi) + { + return; + } + let relativePos = []; + let pixPos = util.to_unit(this._pos, this._units, this._win, "pix"); + relativePos[0] = e.originalEvent.pageX - this._win.size[0] * 0.5 - this._pixi.parent.position.x; + relativePos[1] = -(e.originalEvent.pageY - this._win.size[1] * 0.5) - this._pixi.parent.position.y; + this._associatedPointerId = e.originalEvent.pointerId; + this._initialPointerOffset[0] = relativePos[0] - pixPos[0]; + this._initialPointerOffset[1] = relativePos[1] - pixPos[1]; + this.emit("pointerdown", e); + } + + /** + * Handler of pointerup event. + * + * @name module:visual.VisualStim#_handlePointerUp + * @private + * @param {Object} e - pointerup event data. + */ + _handlePointerUp (e) + { + if (e.originalEvent.pointerId === this._associatedPointerId) + { + this._associatedPointerId = undefined; + this._initialPointerOffset.fill(0); + this.emit("pointerup", e); + } + } + + /** + * Handler of pointermove event. + * + * @name module:visual.VisualStim#_handlePointerMove + * @private + * @param {Object} e - pointermove event data. + */ + _handlePointerMove (e) + { + if (e.originalEvent.pointerId === this._associatedPointerId) + { + let newPos = []; + newPos[ 0 ] = e.originalEvent.pageX - this._win.size[ 0 ] * 0.5 - this._pixi.parent.position.x - this._initialPointerOffset[ 0 ]; + newPos[ 1 ] = -(e.originalEvent.pageY - this._win.size[ 1 ] * 0.5) - this._pixi.parent.position.y - this._initialPointerOffset[ 1 ]; + this.setPos(util.to_unit(newPos, "pix", this._win, this._units)); + this.emit("pointermove", e); + } + } + /** * Setter for the anchor attribute. * @@ -261,6 +393,7 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin) { anchor[0] = 1.0; } + if (anchorText.indexOf("top") > -1) { anchor[1] = 0.0; diff --git a/src/visual/YoutubeIframeAPI.js b/src/visual/YoutubeIframeAPI.js new file mode 100644 index 0000000..b37e028 --- /dev/null +++ b/src/visual/YoutubeIframeAPI.js @@ -0,0 +1,94 @@ +/** + * Provides a class to work with Youtube Iframe API. See https://developers.google.com/youtube/iframe_api_reference + * + * @author Nikita Agafonov + * @version 2023.2.0 + * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2023 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * + */ + +import * as util from "../util/Util.js"; + +class YoutubeIframeAPI +{ + constructor () + { + this.isReady = false; + this._initResolver = undefined; + this._initPromise = undefined; + } + + _onYoutubeIframeAPIReady () + { + this.isReady = true; + this._initResolver(); + } + + async init () + { + if (this.isReady) + { + return Promise.resolve(); + } + + // If init is in progress but not done yet, return the promise. + // This is the case when multiple movie stims are created simultaneously. + if (this._initPromise) + { + return this._initPromise; + } + + // Called by Youtube script. + window.onYouTubeIframeAPIReady = this._onYoutubeIframeAPIReady.bind(this); + + let el = document.createElement("script"); + el.src = "https://www.youtube.com/iframe_api"; + let firstScriptTag = document.getElementsByTagName("script")[0]; + firstScriptTag.parentNode.insertBefore(el, firstScriptTag); + + this._initPromise = new Promise((res, rej) => { + this._initResolver = res; + }); + + return this._initPromise; + } + + createPlayer (params = {}) + { + const uuid = util.makeUuid(); + document.body.insertAdjacentHTML("beforeend", + `
+
+
`); + document.querySelector(`#yt-iframe-placeholder-${uuid}`).parentElement.classList.add("inprogress"); + + const originalOnready = params.events.onReady; + params.events.onReady = (event) => + { + document.querySelector(`#yt-iframe-placeholder-${uuid}`).parentElement.classList.remove("inprogress"); + if (typeof originalOnready === "function") + { + originalOnready(event); + } + }; + + const ytPlayer = new YT.Player(`yt-iframe-placeholder-${uuid}`, + params + ); + + return ytPlayer; + } + + destroyPlayer (ytPlayer) + { + const elementId = ytPlayer.getIframe().id; + ytPlayer.destroy(); + + // At this point youtubeAPI destroyed the player and returned the placeholder div back in place instead of it. Cleaning up. + document.getElementById(elementId).parentElement.remove(); + } +} + +const YTAPISingleTon = new YoutubeIframeAPI(); +export default YTAPISingleTon; diff --git a/src/visual/index.js b/src/visual/index.js index 8c604fa..67890f2 100644 --- a/src/visual/index.js +++ b/src/visual/index.js @@ -2,6 +2,7 @@ export * from "./ButtonStim.js"; export * from "./Form.js"; export * from "./ImageStim.js"; export * from "./GratingStim.js"; +export * from "./GifStim.js"; export * from "./MovieStim.js"; export * from "./Polygon.js"; export * from "./Rect.js"; @@ -13,3 +14,5 @@ export * from "./TextStim.js"; export * from "./VisualStim.js"; export * from "./FaceDetector.js"; export * from "./Survey.js"; +export * from "./ParticleEmitter.js"; +export * from "./Progress.js"; \ No newline at end of file diff --git a/src/visual/survey/widgets/MaxDiffMatrix.js b/src/visual/survey/widgets/MaxDiffMatrix.js index d9958c5..a50c784 100644 --- a/src/visual/survey/widgets/MaxDiffMatrix.js +++ b/src/visual/survey/widgets/MaxDiffMatrix.js @@ -95,18 +95,11 @@ class MaxDiffMatrix question.setCssRoot(rootClass); question.cssClasses.mainRoot = rootClass; } - let html; - let headerCells = ""; - let subHeaderCells = ""; - let bodyCells = ""; - let bodyHTML = ""; - let cellGenerator; - let i, j; // Relying on a fact that there's always 2 columns. // This is correct according current Qualtrics design for MaxDiff matrices. // Header generation - headerCells = + let headerCells = `${question.columns[0].text} @@ -114,9 +107,10 @@ class MaxDiffMatrix ${question.columns[1].text}`; // Body generation - for (i = 0; i < question.rows.length; i++) + let bodyHTML = ""; + for (let i = 0; i < question.rows.length; i++) { - bodyCells = + const bodyCells = `