add a costume editor

This commit is contained in:
2026-01-20 21:33:23 -06:00
parent 23cb91f548
commit 6f59e34761
8 changed files with 2441 additions and 11 deletions

1282
costume-editor-backup.txt Normal file

File diff suppressed because it is too large Load Diff

463
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@blockly/plugin-strict-connection-checker": "^6.0.1",
"@fortawesome/fontawesome-free": "^7.0.1",
"blockly": "^12.2.0",
"fabric": "^7.1.0",
"jszip": "^3.10.1",
"pako": "^2.1.0",
"pixi.js-legacy": "^7.4.3",
@@ -1455,6 +1456,54 @@
"node": ">= 14"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"optional": true,
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/blockly": {
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/blockly/-/blockly-12.2.0.tgz",
@@ -1467,6 +1516,31 @@
"node": ">=18"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -1496,6 +1570,28 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/canvas": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz",
"integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.3"
},
"engines": {
"node": "^18.12.0 || >= 20.9.0"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC",
"optional": true
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -1551,6 +1647,42 @@
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1571,6 +1703,16 @@
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
"license": "ISC"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"optional": true,
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
@@ -1721,6 +1863,29 @@
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/fabric": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fabric/-/fabric-7.1.0.tgz",
"integrity": "sha512-061QsoSw6xn7UoRXYq816qMyvObP4gRNVph0jAFWtG5E2kBlfdjrYBiLPRuaAHSmVQUz9RjbPpePB/hljiYJIw==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
},
"optionalDependencies": {
"canvas": "^3.2.0",
"jsdom": "^26.1.0"
}
},
"node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
@@ -1736,6 +1901,13 @@
}
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT",
"optional": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1797,6 +1969,13 @@
"node": ">= 0.4"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT",
"optional": true
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -1883,6 +2062,27 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -1895,6 +2095,13 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC",
"optional": true
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -1994,6 +2201,36 @@
"node": ">= 0.4"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"optional": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT",
"optional": true
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2019,6 +2256,33 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT",
"optional": true
},
"node_modules/node-abi": {
"version": "3.87.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT",
"optional": true
},
"node_modules/nwsapi": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
@@ -2037,6 +2301,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"optional": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
@@ -2169,12 +2443,50 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"optional": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2199,6 +2511,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"optional": true,
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -2284,6 +2612,19 @@
"node": ">=v12.22.7"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -2362,6 +2703,53 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
@@ -2443,12 +2831,67 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -2508,6 +2951,19 @@
"node": ">=18"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/url": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
@@ -2663,6 +3119,13 @@
"node": ">=18"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC",
"optional": true
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",

View File

@@ -17,6 +17,7 @@
"@blockly/plugin-strict-connection-checker": "^6.0.1",
"@fortawesome/fontawesome-free": "^7.0.1",
"blockly": "^12.2.0",
"fabric": "^7.1.0",
"jszip": "^3.10.1",
"pako": "^2.1.0",
"pixi.js-legacy": "^7.4.3",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -4,10 +4,10 @@ html {
--font: "Inter", sans-serif;
--dark: #2b323b;
--dark-light: #424c5a;
--primary: #833bf6;
--primary-dark: #8930dc;
--primary: #ff5959;
--primary-dark: #ff7575;
--danger: #f63b3b;
--danger-dark: #dd3434;
--danger-dark: #ff3434;
--color1: #f3f4f6;
--color2: #e4e5e7;
--color3: #cbcdcf;
@@ -18,8 +18,8 @@ html {
html.dark {
--dark: #e2e8f0;
--dark-light: #c8cdd4;
--primary: #833bf6;
--primary-dark: #8930dc;
--primary: #ff5959;
--primary-dark: #ff7575;
--danger: #f63b3b;
--danger-dark: #dd3434;
--color1: #262d36;

View File

@@ -0,0 +1,590 @@
import { Canvas, PencilBrush, Circle, Rect, Line, IText, FabricImage, Path } from 'fabric';
let canvas = null;
let editorContainer = null;
let currentTool = 'draw';
let currentColor = '#000000';
let currentSize = 5;
let fillEnabled = true;
export function openCostumeEditor(existingCostume = null, onSave) {
closeCostumeEditor();
editorContainer = document.createElement('div');
editorContainer.className = 'costume-editor-overlay';
editorContainer.innerHTML = `
<div class="costume-editor-modal">
<div class="costume-editor-header">
<h2>Costume Editor</h2>
<button class="close-editor-btn">×</button>
</div>
<div class="costume-editor-content">
<div class="costume-editor-toolbar">
<div class="tool-group">
<button class="tool-btn active" data-tool="draw" title="Draw">✏️</button>
<button class="tool-btn" data-tool="line" title="Line">📏</button>
<button class="tool-btn" data-tool="rect" title="Rectangle">⬜</button>
<button class="tool-btn" data-tool="circle" title="Circle">⭕</button>
<button class="tool-btn" data-tool="text" title="Text">T</button>
<button class="tool-btn" data-tool="bucket" title="Paint Bucket">🪣</button>
<button class="tool-btn" data-tool="erase" title="Eraser">🧹</button>
<button class="tool-btn" data-tool="select" title="Select">👆</button>
</div>
<div class="tool-group">
<label>Fill: <input type="color" id="color-picker" value="#000000"></label>
<label><input type="checkbox" id="fill-enabled" checked></label>
</div>
<div class="tool-group">
<label>Outline: <input type="color" id="outline-color-picker" value="#000000"></label>
<label>Width: <input type="number" id="outline-width" min="0" max="50" value="2" style="width: 60px;"></label>
</div>
<div class="tool-group">
<label>Size: <input type="range" id="brush-size" min="1" max="50" value="5"><span id="size-display">5px</span></label>
</div>
<div class="tool-group">
<button class="action-btn" id="clear-canvas">🗑️ Clear</button>
<button class="action-btn" id="delete-selected">❌ Delete</button>
<button class="action-btn" id="undo-btn" disabled>↶ Undo</button>
<button class="action-btn" id="redo-btn" disabled>↷ Redo</button>
</div>
</div>
<div class="costume-editor-canvas-container">
<canvas id="fabric-canvas"></canvas>
</div>
<div class="costume-editor-footer">
<button class="cancel-btn">Cancel</button>
<button class="save-btn primary">Save Costume</button>
</div>
</div>
</div>
`;
document.body.appendChild(editorContainer);
// Initialize Fabric.js canvas
const canvasEl = document.getElementById('fabric-canvas');
canvas = new Canvas(canvasEl, {
width: 720,
height: 480,
backgroundColor: null,
isDrawingMode: false
});
// Load existing costume if provided
setupControls(onSave, existingCostume);
}
function setupControls(onSave, existingCostume) {
const colorPicker = document.getElementById('color-picker');
const brushSize = document.getElementById('brush-size');
const sizeDisplay = document.getElementById('size-display');
const fillEnabledCheckbox = document.getElementById('fill-enabled');
const toolButtons = document.querySelectorAll('.tool-btn');
let isDrawing = false;
let startPoint = null;
let currentShape = null;
let eraserPaths = [];
let outlineColor = '#000000';
let outlineWidth = 2;
// History setup
const history = [];
let historyStep = -1;
function saveHistory() {
if (historyStep < history.length - 1) {
history.splice(historyStep + 1);
}
const json = canvas.toJSON();
history.push(JSON.stringify(json));
historyStep++;
if (history.length > 50) {
history.shift();
historyStep--;
}
updateUndoRedoButtons();
}
function updateUndoRedoButtons() {
document.getElementById('undo-btn').disabled = historyStep <= 0;
document.getElementById('redo-btn').disabled = historyStep >= history.length - 1;
}
// Tool selection
toolButtons.forEach(btn => {
btn.addEventListener('click', () => {
toolButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentTool = btn.dataset.tool;
// Clean up existing listeners
canvas.off('mouse:down');
canvas.off('mouse:move');
canvas.off('mouse:up');
canvas.isDrawingMode = false;
canvas.selection = false;
if (currentTool === 'draw') {
canvas.isDrawingMode = true;
canvas.freeDrawingBrush = new PencilBrush(canvas);
canvas.freeDrawingBrush.color = currentColor;
canvas.freeDrawingBrush.width = currentSize;
} else if (currentTool === 'erase') {
canvas.isDrawingMode = true;
const eraserBrush = new PencilBrush(canvas);
eraserBrush.width = currentSize * 2;
eraserBrush.color = '#FFFFFF';
eraserBrush.inverted = true;
canvas.freeDrawingBrush = eraserBrush;
// Use destination-out for actual erasing
canvas.on('before:path:created', (e) => {
e.path.globalCompositeOperation = 'destination-out';
});
} else if (currentTool === 'select') {
canvas.selection = true;
} else if (currentTool === 'text') {
canvas.on('mouse:down', (options) => {
if (options.target) return; // Don't create new text if clicking existing object
const pointer = canvas.getScenePoint(options.e);
const text = new IText('Text', {
left: pointer.x,
top: pointer.y,
fill: currentColor,
fontSize: Math.max(20, currentSize * 4),
fontFamily: 'Arial'
});
canvas.add(text);
canvas.setActiveObject(text);
text.enterEditing();
text.selectAll();
setTimeout(() => saveHistory(), 100);
});
} else if (currentTool === 'line' || currentTool === 'rect' || currentTool === 'circle') {
canvas.on('mouse:down', (options) => {
if (options.target) return; // Don't draw if clicking on existing object
isDrawing = true;
const pointer = canvas.getScenePoint(options.e);
startPoint = { x: pointer.x, y: pointer.y };
if (currentTool === 'line') {
currentShape = new Line([startPoint.x, startPoint.y, startPoint.x, startPoint.y], {
stroke: outlineColor,
strokeWidth: outlineWidth,
selectable: false
});
} else if (currentTool === 'rect') {
currentShape = new Rect({
left: startPoint.x,
top: startPoint.y,
width: 1,
height: 1,
fill: fillEnabled ? currentColor : 'transparent',
stroke: outlineColor,
strokeWidth: outlineWidth,
selectable: false
});
} else if (currentTool === 'circle') {
currentShape = new Circle({
left: startPoint.x,
top: startPoint.y,
radius: 1,
fill: fillEnabled ? currentColor : 'transparent',
stroke: outlineColor,
strokeWidth: outlineWidth,
selectable: false,
originX: 'center',
originY: 'center'
});
}
if (currentShape) {
canvas.add(currentShape);
}
});
canvas.on('mouse:move', (options) => {
if (!isDrawing || !currentShape) return;
const pointer = canvas.getScenePoint(options.e);
if (currentTool === 'line') {
currentShape.set({ x2: pointer.x, y2: pointer.y });
} else if (currentTool === 'rect') {
const width = pointer.x - startPoint.x;
const height = pointer.y - startPoint.y;
currentShape.set({
width: Math.abs(width),
height: Math.abs(height),
left: width > 0 ? startPoint.x : pointer.x,
top: height > 0 ? startPoint.y : pointer.y
});
} else if (currentTool === 'circle') {
const radius = Math.sqrt(
Math.pow(pointer.x - startPoint.x, 2) +
Math.pow(pointer.y - startPoint.y, 2)
);
currentShape.set({ radius: Math.max(1, radius) });
}
canvas.renderAll();
});
canvas.on('mouse:up', () => {
if (isDrawing && currentShape) {
currentShape.setCoords();
currentShape.set({ selectable: true });
saveHistory();
}
isDrawing = false;
currentShape = null;
});
} else if (currentTool === 'bucket') {
canvas.on('mouse:down', (options) => {
if (!options.target) return;
const target = options.target;
if (target.type === 'rect' || target.type === 'circle' || target.type === 'triangle' || target.type === 'polygon') {
target.set({
fill: currentColor,
stroke: outlineColor,
strokeWidth: outlineWidth
});
canvas.renderAll();
saveHistory();
} else if (target.type === 'line' || target.type === 'path') {
target.set({
stroke: currentColor,
strokeWidth: outlineWidth
});
canvas.renderAll();
saveHistory();
} else if (target.type === 'i-text' || target.type === 'text') {
target.set('fill', currentColor);
canvas.renderAll();
saveHistory();
}
});
}
});
});
colorPicker.addEventListener('input', (e) => {
currentColor = e.target.value;
if (canvas.freeDrawingBrush && currentTool === 'draw') {
canvas.freeDrawingBrush.color = currentColor;
}
});
const outlineColorPicker = document.getElementById('outline-color-picker');
const outlineWidthInput = document.getElementById('outline-width');
outlineColorPicker.addEventListener('input', (e) => {
outlineColor = e.target.value;
});
outlineWidthInput.addEventListener('input', (e) => {
outlineWidth = parseInt(e.target.value);
});
brushSize.addEventListener('input', (e) => {
currentSize = parseInt(e.target.value);
sizeDisplay.textContent = `${currentSize}px`;
if (canvas.freeDrawingBrush) {
canvas.freeDrawingBrush.width = currentSize;
}
});
fillEnabledCheckbox.addEventListener('change', (e) => {
fillEnabled = e.target.checked;
});
document.getElementById('clear-canvas').addEventListener('click', () => {
if (confirm('Clear entire canvas?')) {
canvas.clear();
canvas.backgroundColor = null;
canvas.renderAll();
saveHistory();
}
});
document.getElementById('delete-selected').addEventListener('click', () => {
const activeObjects = canvas.getActiveObjects();
if (activeObjects.length > 0) {
activeObjects.forEach(obj => canvas.remove(obj));
canvas.discardActiveObject();
canvas.renderAll();
saveHistory();
}
});
canvas.on('path:created', saveHistory);
canvas.on('object:added', (e) => {
if (e.target && e.target.type !== 'path') {
saveHistory();
}
});
canvas.on('object:modified', saveHistory);
// Snap to center functionality
const SNAP_DISTANCE = 15;
const centerX = 240;
const centerY = 180;
canvas.on('object:moving', (e) => {
const obj = e.target;
// Snap to horizontal center
if (Math.abs(obj.left - centerX) < SNAP_DISTANCE) {
obj.set({ left: centerX });
}
// Snap to vertical center
if (Math.abs(obj.top - centerY) < SNAP_DISTANCE) {
obj.set({ top: centerY });
}
canvas.renderAll();
});
document.getElementById('undo-btn').addEventListener('click', () => {
if (historyStep > 0) {
historyStep--;
canvas.loadFromJSON(history[historyStep]).then(() => {
canvas.renderAll();
updateUndoRedoButtons();
});
}
});
document.getElementById('redo-btn').addEventListener('click', () => {
if (historyStep < history.length - 1) {
historyStep++;
canvas.loadFromJSON(history[historyStep]).then(() => {
canvas.renderAll();
updateUndoRedoButtons();
});
}
});
// Trigger draw mode by default
// Trigger draw mode by default
document.querySelector('[data-tool="draw"]').click();
// Load existing costume if provided
if (existingCostume && existingCostume.texture) {
const url = existingCostume.texture.baseTexture?.resource?.url || existingCostume.texture.baseTexture?.cacheId;
if (url) {
FabricImage.fromURL(url).then((img) => {
img.set({
left: 240,
top: 180,
originX: 'center',
originY: 'center'
});
const scale = Math.min(460 / img.width, 340 / img.height, 1);
img.scale(scale);
canvas.add(img);
canvas.renderAll();
saveHistory();
}).catch(err => {
console.error('Failed to load costume:', err);
});
}
} else {
// Save initial empty state
saveHistory();
}
document.querySelector('.save-btn').addEventListener('click', () => {
const dataURL = canvas.toDataURL({
format: 'png',
quality: 1,
multiplier: 1
});
if (onSave) onSave(dataURL);
closeCostumeEditor();
});
document.querySelector('.cancel-btn').addEventListener('click', closeCostumeEditor);
document.querySelector('.close-editor-btn').addEventListener('click', closeCostumeEditor);
}
export function closeCostumeEditor() {
if (editorContainer) {
editorContainer.remove();
editorContainer = null;
}
if (canvas) {
canvas.dispose();
canvas = null;
}
}
// CSS (same as before)
const style = document.createElement('style');
style.textContent = `
.costume-editor-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.costume-editor-modal {
background: #2b2b2b;
border-radius: 8px;
width: 90%;
max-width: 800px;
max-height: 90vh;
display: flex;
flex-direction: column;
color: white;
}
.costume-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #444;
}
.costume-editor-header h2 {
margin: 0;
font-size: 20px;
}
.close-editor-btn {
background: none;
border: none;
color: white;
font-size: 30px;
cursor: pointer;
}
.close-editor-btn:hover {
color: #ff4444;
}
.costume-editor-content {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.costume-editor-toolbar {
display: flex;
gap: 15px;
padding: 15px 20px;
border-bottom: 1px solid #444;
flex-wrap: wrap;
background: #333;
align-items: center;
}
.tool-group {
display: flex;
gap: 8px;
align-items: center;
}
.tool-btn, .action-btn {
background: #444;
border: 2px solid transparent;
color: white;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 16px;
}
.tool-btn:hover, .action-btn:hover {
background: #555;
}
.tool-btn.active {
background: #0066ff;
border-color: #0044cc;
}
.costume-editor-toolbar label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
#color-picker {
width: 40px;
height: 30px;
border: none;
cursor: pointer;
}
#brush-size {
width: 100px;
}
#size-display {
font-size: 12px;
color: #aaa;
min-width: 35px;
}
.costume-editor-canvas-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #1a1a1a;
overflow: auto;
padding: 20px;
}
.canvas-container {
border: 2px solid #444;
border-radius: 4px;
background-image:
linear-gradient(45deg, #666 25%, transparent 25%),
linear-gradient(-45deg, #666 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #666 75%),
linear-gradient(-45deg, transparent 75%, #666 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.costume-editor-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 15px 20px;
border-top: 1px solid #444;
}
.costume-editor-footer button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.cancel-btn {
background: #555;
color: white;
}
.cancel-btn:hover {
background: #666;
}
.save-btn {
background: #0066ff;
color: white;
}
.save-btn:hover {
background: #0055dd;
}
.primary {
font-weight: bold;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
document.head.appendChild(style);

View File

@@ -6,6 +6,7 @@ import * as PIXI from "pixi.js-legacy";
import pako from "pako";
import JSZip from "jszip";
import { io } from "socket.io-client";
import { openCostumeEditor, closeCostumeEditor } from "./costumeEditor.js";
import CustomRenderer from "../functions/render.js";
import { setupThemeButton } from "../functions/theme.js";
@@ -64,6 +65,19 @@ const tabButtons = document.querySelectorAll(".tab-button");
const tabContents = document.querySelectorAll(".tab-content");
const fullscreenButton = document.getElementById("fullscreen-button");
// Add this after the costumes-list setup or in your HTML
const createCostumeButton = document.createElement('button');
createCostumeButton.id = 'create-costume-button';
createCostumeButton.className = 'primary';
createCostumeButton.innerHTML = '<i class="fa-solid fa-paintbrush"></i> Create New Costume (WORK IN PROGRESS)';
createCostumeButton.style.margin = '10px';
// Insert it before the costumes list
const costumesTab = document.getElementById('costumes-tab');
if (costumesTab) {
costumesTab.insertBefore(createCostumeButton, costumesList);
}
export const BASE_WIDTH = 480;
export const BASE_HEIGHT = 360;
const MAX_HTTP_BUFFER = 20 * 1024 * 1024;
@@ -109,7 +123,7 @@ createPenGraphics();
window.projectVariables = {};
export const projectVariables = window.projectVariables;
window.sprites = [];
export const sprites = window.sprites;
export let sprites = window.sprites;
export let activeSprite = null;
window.projectSounds = [];
window.projectCostumes = ["default"];
@@ -394,7 +408,9 @@ function deleteSprite(id, emit = false) {
}
});
sprites = sprites.filter(s => s.id !== sprite.id);
window.sprites = sprites.filter(s => s.id !== sprite.id);
sprites.length = 0;
window.sprites.forEach(s => sprites.push(s));
workspace.clear();
@@ -541,7 +557,6 @@ function renderCostumesList() {
const oldName = costume.name;
costume.name = newName;
// ADD THIS CODE:
const oldIndex = window.projectCostumes.indexOf(oldName);
if (oldIndex !== -1 && !window.projectCostumes.includes(newName)) {
window.projectCostumes[oldIndex] = newName;
@@ -573,11 +588,46 @@ function renderCostumesList() {
});
}
// ADD THIS: Edit button
const editBtn = document.createElement("button");
editBtn.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
editBtn.className = "button";
editBtn.draggable = false;
editBtn.title = "Edit costume";
editBtn.onclick = () => {
openCostumeEditor(costume, async (dataURL) => {
if (!dataURL) return;
// Update the existing costume
const newTexture = PIXI.Texture.from(dataURL);
costume.texture = newTexture;
// Update sprite if this is the current costume
if (activeSprite.pixiSprite.texture === costume.texture) {
activeSprite.pixiSprite.texture = newTexture;
}
renderCostumesList();
showNotification({ message: '✓ Costume updated' });
if (currentSocket && currentRoom) {
currentSocket.emit("projectUpdate", {
roomId: currentRoom,
type: "updateCostume",
data: {
spriteId: activeSprite.id,
name: costume.name,
texture: dataURL,
},
});
}
});
};
const deleteBtn = createDeleteButton(() => {
const deleted = activeSprite.costumes[index];
activeSprite.costumes.splice(index, 1);
// ADD THIS CODE to remove from global array if not used elsewhere:
if (deleted) {
const existsElsewhere = sprites.some(s =>
s.id !== activeSprite.id && s.costumes.some(c => c.name === deleted.name)
@@ -593,8 +643,6 @@ function renderCostumesList() {
activeSprite.pixiSprite.texture = PIXI.Texture.EMPTY;
}
renderCostumesList();
// ADD THIS LINE to refresh toolbox:
workspace.updateToolbox(document.getElementById('toolbox'));
if (currentSocket && currentRoom && deleted) {
@@ -611,6 +659,7 @@ function renderCostumesList() {
costumeContainer.appendChild(img);
costumeContainer.appendChild(renameableLabel);
costumeContainer.appendChild(editBtn);
costumeContainer.appendChild(deleteBtn);
costumeContainer.appendChild(sizeLabel);
@@ -1776,6 +1825,51 @@ loadButton.addEventListener("click", () => {
});
loadInput.addEventListener("change", loadProject);
// Create new costume with editor
document.getElementById('create-costume-button')?.addEventListener('click', () => {
if (!activeSprite) {
showNotification({ message: 'Please select a sprite first' });
return;
}
openCostumeEditor(null, async (dataURL) => {
if (!dataURL || !activeSprite) return;
const texture = PIXI.Texture.from(dataURL);
let uniqueName = 'costume';
let counter = 1;
const nameExists = name => activeSprite.costumes.some(c => c.name === name);
while (nameExists(uniqueName)) {
counter++;
uniqueName = `costume_${counter}`;
}
activeSprite.costumes.push({ name: uniqueName, texture });
if (!window.projectCostumes.includes(uniqueName)) {
window.projectCostumes.push(uniqueName);
}
workspace.updateToolbox(document.getElementById('toolbox'));
if (currentSocket && currentRoom) {
currentSocket.emit("projectUpdate", {
roomId: currentRoom,
type: "addCostume",
data: {
spriteId: activeSprite.id,
name: uniqueName,
texture: dataURL,
},
});
}
renderCostumesList();
showNotification({ message: '✓ Costume created successfully' });
});
});
document.getElementById("costume-upload").addEventListener("change", e => {
const file = e.target.files[0];
if (!file || !activeSprite) return;