-
diff --git a/package-lock.json b/package-lock.json
index 74887f4..627b9d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,17 @@
{
- "name": "mix2",
+ "name": "mixx",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "mix2",
+ "name": "mixx",
"version": "1.0.0",
- "license": "ISC",
"devDependencies": {
+ "@eslint/js": "^9.9.0",
"electron": "^31.3.1",
- "nodeman": "^1.1.2",
+ "eslint": "^9.9.0",
+ "globals": "^15.9.0",
"nodemon": "^3.1.4"
}
},
@@ -35,6 +36,170 @@
"global-agent": "^3.0.0"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
+ "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz",
+ "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.4",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+ "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz",
+ "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+ "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
+ "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -114,6 +279,67 @@
"@types/node": "*"
}
},
+ "node_modules/acorn": {
+ "version": "8.12.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
+ "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -127,6 +353,12 @@
"node": ">= 8"
}
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -210,6 +442,52 @@
"node": ">=8"
}
},
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -246,21 +524,44 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/colors": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
- "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
"engines": {
- "node": ">=0.1.90"
+ "node": ">=7.0.0"
}
},
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
@@ -305,6 +606,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
@@ -428,7 +735,6 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
- "optional": true,
"engines": {
"node": ">=10"
},
@@ -436,6 +742,164 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/eslint": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz",
+ "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.11.0",
+ "@eslint/config-array": "^0.17.1",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "9.9.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.3.0",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.0.2",
+ "eslint-visitor-keys": "^4.0.0",
+ "espree": "^10.1.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
+ "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
+ "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.12.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -456,6 +920,33 @@
"@types/yauzl": "^2.9.1"
}
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
@@ -465,6 +956,18 @@
"pend": "~1.2.0"
}
},
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -477,6 +980,41 @@
"node": ">=8"
}
},
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "dev": true
+ },
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -593,6 +1131,18 @@
"node": ">=10"
}
},
+ "node_modules/globals": {
+ "version": "15.9.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
+ "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/globalthis": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
@@ -734,12 +1284,46 @@
"node": ">=10.19.0"
}
},
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true
},
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -782,12 +1366,51 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
@@ -813,6 +1436,40 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -862,21 +1519,11 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
- "node_modules/nodeman": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/nodeman/-/nodeman-1.1.2.tgz",
- "integrity": "sha512-RpRpDnMI4TN/EPX6+my7f02585I14AQO8YrHTkO1aTiC65+sMa5G3NwQplN6kwue1njxWiaSz2UwCnDbwkLgeQ==",
- "dev": true,
- "dependencies": {
- "colors": "*"
- },
- "bin": {
- "_nodeman": "bin/_nodeman",
- "nodeman": "bin/nodeman"
- },
- "engines": {
- "node": "*"
- }
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
},
"node_modules/nodemon": {
"version": "3.1.4",
@@ -958,6 +1605,23 @@
"wrappy": "1"
}
},
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
@@ -967,6 +1631,66 @@
"node": ">=8"
}
},
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -985,6 +1709,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/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,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -1010,6 +1743,35 @@
"once": "^1.3.1"
}
},
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
@@ -1040,6 +1802,15 @@
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true
},
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
@@ -1052,6 +1823,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
@@ -1070,6 +1851,29 @@
"node": ">=8.0"
}
},
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -1102,6 +1906,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -1133,6 +1958,30 @@
"dev": true,
"optional": true
},
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
@@ -1157,6 +2006,12 @@
"node": ">=4"
}
},
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1178,6 +2033,18 @@
"nodetouch": "bin/nodetouch.js"
}
},
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
@@ -1212,6 +2079,39 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -1227,6 +2127,18 @@
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
}
}
diff --git a/package.json b/package.json
index 79e57d4..bf5e490 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,10 @@
"start": "nodemon --exec electron ."
},
"devDependencies": {
+ "@eslint/js": "^9.9.0",
"electron": "^31.3.1",
+ "eslint": "^9.9.0",
+ "globals": "^15.9.0",
"nodemon": "^3.1.4"
}
}
diff --git a/render.js b/render.js
index aadbb71..87ba792 100644
--- a/render.js
+++ b/render.js
@@ -1,758 +1,1222 @@
-// init {{{
-
-const menuBar = document.getElementById('menu-bar');
-const toolBar = document.getElementById('tool-bar');
-const layerBar = document.getElementById('layer-bar');
-const canvasArea = document.getElementById('canvas-area');
-const infoBar = document.getElementById('info-bar');
-const canvasContainer = document.getElementById('canvas-container');
-const brushPreview = document.getElementById('brush-preview');
-
-const canvas = document.getElementById('canvas');
-// canvas.style.imageRendering = 'pixelated';
-const ctx = canvas.getContext('2d');
-ctx.imageSmoothingEnabled = false;
-ctx.webkitImageSmoothingEnabled = false;
-ctx.mozImageSmoothingEnabled = false;
-
-canvas.width = 800;
-canvas.height = 600;
-let canvasWidth = canvas.width;
-let canvasHeight = canvas.height;
-
-let undoStack = [];
-let redoStack = [];
-let maxHistory = 30;
+// CONSTANTS {{{
-const dZoom = 0.001;
-
-let zoom = 1;
-let brushSize = 5;
-let dBrushSize = 0.5;
-let maxBrushSize = 500;
-let backgroundColor = 'rgb(255, 255, 255)';
-let color = 'rgb(0, 0, 0)';
-let tool
-
-let tempCanvas;
-let startX, startY;
-let endX, endY;
-let dX, dY;
-let canvasStartX
-let canvasStartY;
-let canvasEndX;
-let canvasEndY;
-let canvasDX
-let canvasDY;
+const commandBarElement = document.getElementById('menu-bar');
+const toolBarElement = document.getElementById('tool-bar');
+const layerControllersElement = document.getElementById('layer-controllers');
+const studioElement = document.getElementById('studio');
+const infoBarElement = document.getElementById('info-bar');
+const easelElement = document.getElementById('easel');
+const brushPreviewElement = document.getElementById('brush-preview');
-let isMouseDown = false;
+const dZoom = 0.001;
+const dBrushSize = 0.5;
+const initialWidth = 800;
+const initialHeight = 600;
+const maxBrushSize = 500;
+const tolerance = 1;
+const shapes = ['circle', 'square'];
-const colorPreview = document.createElement('div');
-colorPreview.id = 'color-preview';
-colorPreview.className = 'puck';
-colorPreview.style.backgroundColor = color;
+// }}}
-menuBar.appendChild(colorPreview);
+// VARS {{{
+let brushColor = 'rgb(0, 0, 0)';
+let brushShape = 'circle'
+let brushSize = 15;
+let zoom = 1;
+let currentTool;
+let prevTool = 'brush';
+
+let startX = 0;
+let startY = 0;
+let endX = 0;
+let endY = 0;
+let dX = 0;
+let dY = 0;
+let canvasStartX = 0;
+let canvasStartY = 0;
+let canvasEndX = 0;
+let canvasEndY = 0;
+let canvasDX = 0;
+let canvasDY = 0;
+
+let isKeyDown = false;
+let isMouseDown = false;
// }}}
-// helpers {{{
-
-function saveState() {
- if (undoStack.length >= maxHistory) {
- undoStack.shift(); // Remove the oldest state if the stack exceeds the limit
- }
+// HELPERS {{{
- // Save the current canvas content and dimensions
- undoStack.push({
- imageData: canvas.toDataURL(),
- width: canvas.width,
- height: canvas.height
- });
-
- redoStack = []; // Clear the redo stack whenever a new action is performed
-}
+function disableImageSmoothing(ctx) {
+ ctx.imageSmoothingEnabled = false;
+ if (ctx.imageSmoothingEnabled !== false) {
+ ctx.mozImageSmoothingEnabled = false;
+ ctx.webkitImageSmoothingEnabled = false;
+ ctx.msImageSmoothingEnabled = false;
+ }
+};
-function undo() {
- if (undoStack.length > 0) {
- const currentState = {
- imageData: canvas.toDataURL(),
- width: canvas.width,
- height: canvas.height
- };
-
- redoStack.push(currentState); // Save current state to the redo stack
- const lastState = undoStack.pop(); // Get the last state from the undo stack
-
- // Restore the canvas dimensions
- canvas.width = lastState.width;
- canvas.height = lastState.height;
- canvas.style.width = canvas.width * zoom + 'px';
- canvas.style.height = canvas.height * zoom + 'px';
- canvasWidth = canvas.width;
- canvasHeight = canvas.height;
-
- // Restore the canvas content
- const img = new Image();
- img.src = lastState.imageData;
- img.onload = function() {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(img, 0, 0);
- };
+function hexToRgbArray(hex) {
+ if (hex.startsWith('#')) {
+ hex = hex.slice(1);
}
-}
-function redo() {
- if (redoStack.length > 0) {
- const currentState = {
- imageData: canvas.toDataURL(),
- width: canvas.width,
- height: canvas.height
- };
-
- undoStack.push(currentState); // Save current state to the undo stack
- const nextState = redoStack.pop(); // Get the last state from the redo stack
-
- // Restore the canvas dimensions
- canvas.width = nextState.width;
- canvas.height = nextState.height;
- canvas.style.width = canvas.width * zoom + 'px';
- canvas.style.height = canvas.height * zoom + 'px';
- canvasWidth = canvas.width;
- canvasHeight = canvas.height;
-
- // Restore the canvas content
- const img = new Image();
- img.src = nextState.imageData;
- img.onload = function() {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(img, 0, 0);
- };
+ if (hex.length === 3) {
+ hex = hex.split('').map(char => char + char).join('');
}
+
+ const bigint = parseInt(hex, 16);
+ return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
}
-function getPositionOnCanvas(e) {
- const rect = canvas.getBoundingClientRect();
- return {
- x: Math.round((e.clientX - rect.left) / zoom),
- y: Math.round((e.clientY - rect.top) / zoom),
- };
+function colorsMatch(color1, color2, tolerance = 0) {
+ return Math.abs(color1[0] - color2[0]) <= tolerance &&
+ Math.abs(color1[1] - color2[1]) <= tolerance &&
+ Math.abs(color1[2] - color2[2]) <= tolerance;
}
-function drawCircle(x, y) {
- ctx.beginPath();
- ctx.arc(x, y, brushSize / 2, 0, 2 * Math.PI, false);
- ctx.fillStyle = color;
- ctx.fill();
+function makeIconElement(htmlString) {
+ const parentElement = document.createElement('div');
+ parentElement.innerHTML = htmlString;
+ const iconElement = parentElement.firstChild;
+ return iconElement;
}
-function drawLineWithCircles(x1, y1, x2, y2) {
- const dx = x2 - x1;
- const dy = y2 - y1;
- const distance = Math.sqrt(dx * dx + dy * dy);
- const steps = Math.ceil(distance / (brushSize / 5));
+function makeButtonElement({icon, name, func, key}) {
+ if (!icon) throw new Error('No icon provided');
+ if (!name) throw new Error('No name provided');
+ if (!func) throw new Error('No click function provided');
+ if (!key) throw new Error('No key provided');
+
+ const button = {};
+ button.name = name;
+ button.key = key;
+ button.icon = icon;
+ button.element = document.createElement('div');
+ button.element.className = 'button';
+
+ button.element.addEventListener('click', func);
+
+ button.refresh = function() {
+ button.element.innerHTML = '';
+ const iconElement = makeIconElement(button.icon);
+ button.element.appendChild(iconElement);
+ button.element.title = button.name;
+ if (button.key) {
+ const keyHint = document.createElement('span');
+ keyHint.className = 'key-hint';
+ keyHint.innerHTML = key;
+ button.element.appendChild(keyHint);
+ }
+ }
- for (let i = 0; i <= steps; i++) {
- const x = x1 + (dx * i) / steps;
- const y = y1 + (dy * i) / steps;
- drawCircle(x, y);
- }
-}
+ button.refresh();
-function saveCanvasContents() {
- tempCanvas = document.createElement('canvas');
- tempCanvas.width = canvas.width;
- tempCanvas.height = canvas.height;
- const tempCtx = tempCanvas.getContext('2d');
- tempCtx.drawImage(canvas, 0, 0);
+ return button;
}
-function updateColorPreview() {
- colorPreview.style.backgroundColor = color;
-}
+// }}}
-function hexToRgbArray(hex) {
- if (hex.startsWith('#')) {
- hex = hex.slice(1);
- }
+// LAYERS {{{
- if (hex.length === 3) {
- hex = hex.split('').map(char => char + char).join('');
- }
+// FACTORY {{{
- const bigint = parseInt(hex, 16);
- return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
-}
+function makeCanvas({height=600, width=800}) { // {{{
+ const canvas = document.createElement('canvas');
+ canvas.style.imageRendering = 'pixelated';
+ canvas.ctx = canvas.getContext('2d');
-function floodFill(x, y, fillColor) {
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const data = imageData.data;
+ canvas.tempCanvas = document.createElement('canvas');
+ canvas.tempCtx = canvas.tempCanvas.getContext('2d');
- const targetColor = getColorAtPixel(data, x, y);
- const fillColorArray = hexToRgbArray(fillColor);
+ canvas.saveCanvas = function() {
+ canvas.ctx.save();
+ canvas.tempCanvas.width = canvas.width;
+ canvas.tempCanvas.height = canvas.height;
+ canvas.tempCtx.clearRect(0, 0, canvas.width, canvas.height);
+ disableImageSmoothing(canvas.tempCtx);
+ canvas.tempCtx.drawImage(canvas, 0, 0);
+ }
- if (colorsMatch(targetColor, fillColorArray)) {
- return; // The clicked point is already the fill color
- }
+ canvas.clearCanvas = function() {
+ canvas.ctx.clearRect(0, 0, canvas.width, canvas.height);
+ }
- const stack = [{x, y}];
+ canvas.restoreCanvas = function(x=0, y=0) {
+ canvas.ctx.drawImage(canvas.tempCanvas, x, y);
+ }
- while (stack.length > 0) {
- const {x, y} = stack.pop();
- const currentColor = getColorAtPixel(data, x, y);
+ canvas.setHeight = function(height) {
+ canvas.height = height;
+ disableImageSmoothing(canvas.ctx);
+ };
- if (colorsMatch(currentColor, targetColor)) {
- setColorAtPixel(data, x, y, fillColorArray);
+ canvas.setWidth = function(width) {
+ canvas.width = width;
+ disableImageSmoothing(canvas.ctx);
+ };
- if (x > 0) stack.push({x: x - 1, y});
- if (x < canvas.width - 1) stack.push({x: x + 1, y});
- if (y > 0) stack.push({x, y: y - 1});
- if (y < canvas.height - 1) stack.push({x, y: y + 1});
- }
- }
+ canvas.resize = function(width, height) {
+ canvas.width = width;
+ canvas.height = height;
+ disableImageSmoothing(canvas.ctx);
+ }
- ctx.putImageData(imageData, 0, 0);
-}
+ canvas.getPositionOnCanvas = function(e) {
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: Math.round((e.clientX - rect.left) / zoom),
+ y: Math.round((e.clientY - rect.top) / zoom),
+ };
+ }
-function getColorAtPixel(data, x, y) {
- const index = (y * canvas.width + x) * 4;
- return [data[index], data[index + 1], data[index + 2], data[index + 3]];
-}
+ canvas.drawPixel = function(x, y, color) {
+ canvas.ctx.fillStyle = color;
+ canvas.ctx.fillRect(x, y, 1, 1);
+ }
-function setColorAtPixel(data, x, y, color) {
- const index = (y * canvas.width + x) * 4;
- data[index] = color[0];
- data[index + 1] = color[1];
- data[index + 2] = color[2];
- data[index + 3] = 255; // Set alpha to fully opaque
-}
+ canvas.drawLineWithPixels = function(x1, y1, x2, y2, color) {
+ const dx = Math.abs(x2 - x1);
+ const dy = Math.abs(y2 - y1);
+ const sx = x1 < x2 ? 1 : -1;
+ const sy = y1 < y2 ? 1 : -1;
+ let err = dx - dy;
+ while (true) {
+ canvas.drawPixel(x1, y1, color); // Draw each pixel along the line
+ if (x1 === x2 && y1 === y2) break;
+ const e2 = err * 2;
+ if (e2 > -dy) { err -= dy; x1 += sx; }
+ if (e2 < dx) { err += dx; y1 += sy; }
+ }
+ }
-function colorsMatch(a, b) {
- return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
-}
+ canvas.drawShape = function(x, y, shape, size, color) {
+ x = Math.round(x);
+ y = Math.round(y);
-// }}}
+ if (size === 1) {
+ canvas.drawPixel(x, y, color);
+ return;
+ }
+ canvas.ctx.fillStyle = color;
+
+ if (shape === 'square') {
+ canvas.ctx.fillRect(x - Math.floor(size / 2), y - Math.floor(size / 2), size, size);
+ } else if (shape === 'circle') {
+ let radius = Math.floor(size / 2);
+ let radiusSquared = radius * radius;
+
+ for (let y1 = -radius; y1 <= radius; y1++) {
+ for (let x1 = -radius; x1 <= radius; x1++) {
+ // Adjust the condition to avoid the outcrop
+ if ((x1 * x1 + y1 * y1) <= radiusSquared - radius) {
+ canvas.ctx.fillRect(x + x1, y + y1, 1, 1);
+ }
+ }
+ }
+ } else if (shape === 'empty-circle') {
+ let radius = Math.floor(size / 2);
+ let x1 = radius;
+ let y1 = 0;
+ let radiusError = 1 - x1;
+
+ while (x1 >= y1) {
+ // Draw the 8 octants of the circle
+ canvas.ctx.fillRect(x + x1, y + y1, 1, 1);
+ canvas.ctx.fillRect(x + y1, y + x1, 1, 1);
+ canvas.ctx.fillRect(x - y1, y + x1, 1, 1);
+ canvas.ctx.fillRect(x - x1, y + y1, 1, 1);
+ canvas.ctx.fillRect(x - x1, y - y1, 1, 1);
+ canvas.ctx.fillRect(x - y1, y - x1, 1, 1);
+ canvas.ctx.fillRect(x + y1, y - x1, 1, 1);
+ canvas.ctx.fillRect(x + x1, y - y1, 1, 1);
+
+ y1++;
+ if (radiusError < 0) {
+ radiusError += 2 * y1 + 1;
+ } else {
+ x1--;
+ radiusError += 2 * (y1 - x1 + 1);
+ }
+ }
+ }
+ }
-// mousedown {{{
+ canvas.drawLineWithShape = function(x1, y1, x2, y2, shape, size, color) {
+ const dx = x2 - x1;
+ const dy = y2 - y1;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+ const steps = Math.ceil(distance / (size / 2));
-canvasArea.addEventListener('mousedown', (e) => {
- if (e.target.closest('.puck')) return;
+ for (let i = 0; i <= steps; i++) {
+ const x = Math.round(x1 + (dx * i) / steps);
+ const y = Math.round(y1 + (dy * i) / steps);
+ canvas.drawShape(x, y, shape, size, color);
+ }
+ }
- startX = e.clientX;
- startY = e.clientY;
- canvasStartX = getPositionOnCanvas(e).x;
- canvasStartY = getPositionOnCanvas(e).y;
- saveCanvasContents();
- isMouseDown = true;
+ canvas.fill = function(color) {
+ canvas.ctx.fillStyle = color;
+ canvas.ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
- if (
- tool === 'brush' ||
- tool === 'content-move' ||
- tool === 'resize' ||
- tool === 'zoom' ||
- tool === 'bucket-fill'
- ) {
- saveState();
- }
-
- if (tool === 'brush') {
- drawCircle(canvasStartX, canvasStartY);
- } else if (tool === 'bucket-fill') {
- floodFill(canvasStartX, canvasStartY, color);
- return;
- } else if (tool === 'move') {
- startX = e.clientX - canvasContainer.offsetLeft;
- startY = e.clientY - canvasContainer.offsetTop;
- } else if (tool === 'color-picker') {
- const imageData = ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data;
- const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`;
- color = pickedColor;
- console.log('Picked Color:', pickedColor);
- updateColorPreview();
- return;
+ canvas.getColorAtPixel = function(data, x, y) {
+ const index = (y * canvas.width + x) * 4;
+ return [data[index], data[index + 1], data[index + 2], data[index + 3]];
}
-});
-// }}}
+ canvas.setColorAtPixel = function(data, x, y, color) {
+ const index = (y * canvas.width + x) * 4;
+ data[index] = color[0];
+ data[index + 1] = color[1];
+ data[index + 2] = color[2];
+ data[index + 3] = 255;
+ }
-// mousemove {{{
+ canvas.floodFill = function(x, y, color) {
+ console.log('flood fill');
+ const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
-canvasArea.addEventListener('mousemove', (e) => {
+ const targetColor = canvas.getColorAtPixel(data, x, y);
+ const fillColorArray = hexToRgbArray(color);
- endX = e.clientX;
- endY = e.clientY;
- dX = endX - startX;
- dY = endY - startY;
+ if (colorsMatch(targetColor, fillColorArray, tolerance)) {
+ return;
+ }
- canvasEndX = getPositionOnCanvas(e).x;
- canvasEndY = getPositionOnCanvas(e).y;
- canvasDX = canvasEndX - canvasStartX;
- canvasDY = canvasEndY - canvasStartY;
+ const stack = [{x, y}];
- if (tool == 'brush-size') {
- brushPreview.style.display = 'block';
- brushPreview.style.width = brushSize + 'px';
- brushPreview.style.height = brushSize + 'px';
- brushPreview.style.left = e.clientX - brushSize / 2 + 'px';
- brushPreview.style.top = e.clientY - brushSize / 2 + 'px';
- }
+ while (stack.length > 0) {
+ const {x, y} = stack.pop();
+ const currentColor = canvas.getColorAtPixel(data, x, y);
- if (isMouseDown) {
+ if (colorsMatch(currentColor, targetColor, tolerance)) {
+ canvas.setColorAtPixel(data, x, y, fillColorArray);
- if (tool === 'brush-size') {
- brushSize += dX * dBrushSize;
- if (brushSize < 1) brushSize = 1;
- if (brushSize > maxBrushSize) brushSize = maxBrushSize;
- startX = endX;
- } else if (tool === 'brush') {
- drawLineWithCircles(canvasStartX, canvasStartY, canvasEndX, canvasEndY);
-
- canvasStartX = canvasEndX;
- canvasStartY = canvasEndY;
-
- } else if (tool === 'content-move') {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.fillStyle = backgroundColor;
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(tempCanvas, dX, dY);
- } else if (tool === 'move') {
- canvasContainer.style.left = dX + 'px';
- canvasContainer.style.top = dY + 'px';
- } else if (tool === 'zoom') {
- zoom += dX * dZoom;
- if (zoom < 0.1) zoom = 0.1;
- canvas.style.height = canvasHeight * zoom + 'px';
- canvas.style.width = canvasWidth * zoom + 'px';
- startX = endX;
- } else if (tool === 'resize') {
- let newWidth = canvasWidth + dX / zoom;
- let newHeight = canvasHeight + dY / zoom;
- if (newWidth > 0 && newHeight > 0) {
- canvas.width = newWidth;
- canvas.height = newHeight;
- canvas.style.width = newWidth * zoom + 'px';
- canvas.style.height = newHeight * zoom + 'px';
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.fillStyle = backgroundColor;
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(tempCanvas, 0, 0);
+ if (x > 0) stack.push({x: x - 1, y});
+ if (x < canvas.width - 1) stack.push({x: x + 1, y});
+ if (y > 0) stack.push({x, y: y - 1});
+ if (y < canvas.height - 1) stack.push({x, y: y + 1});
}
- } else if (tool === 'color-mix') {
- const imageData = ctx.getImageData(canvasEndX, canvasEndY, 1, 1).data;
- const canvasColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`;
+ }
- const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2));
- const t = Math.min(1, distance / 300);
+ canvas.ctx.putImageData(imageData, 0, 0);
+ }
- const mixedColor = mixbox.lerp(color, canvasColor, t);
+ canvas.toDataUrl = function() {
+ const dataURL = canvas.toDataURL();
+ const dimensions = `${canvas.width}x${canvas.height}`;
+ return {dataURL, dimensions};
+ }
- color = mixedColor;
+ canvas.fromDataUrl = function(dataURL, dimensions) {
+ const img = new Image();
+ img.src = dataURL;
+ img.onload = function() {
+ canvas.width = dimensions.split('x')[0];
+ canvas.height = dimensions.split('x')[1];
+ canvas.style.width = canvas.width * zoom + 'px';
+ canvas.style.height = canvas.height * zoom + 'px';
+ canvas.ctx.drawImage(img, 0, 0);
+ }
+ }
- startX = e.clientX;
- startY = e.clientY;
+ canvas.deleteCanvas = function() {
+ canvas.remove();
+ }
- }
+ canvas.setWidth(width);
+ canvas.setHeight(height);
- }
-
- updateInfos();
- updateColorPreview();
-});
+ return canvas;
-// }}}
+} // }}}
-// mouseup {{{
+function makeLayer({height=600, width=800}) { // {{{
+ const layer = {}
+ layer.canvas = makeCanvas({height, width});
+ layer.active = false;
+ layer.opacity = 1;
-canvasArea.addEventListener('mouseup', (e) => {
- isMouseDown = false;
- if (tool === 'brush') {
- ctx.closePath();
- } else if (tool === 'resize') {
- canvasWidth = canvas.width;
- canvasHeight = canvas.height;
+ layer.controllerElement = document.createElement('div');
+ layer.controllerElement.className = 'layer-controller';
+ layer.controllerElement.innerHTML = '
';
+
+ const moveUpHandle = document.createElement('div');
+ moveUpHandle.classList.add('handle');
+ moveUpHandle.classList.add('top-right');
+ moveUpHandle.innerHTML = '
';
+ moveUpHandle.addEventListener('click', () => {
+ const index = layers.indexOf(layer);
+ if (index > 0) {
+ layers.move(layer, index - 1);
+ }
+ });
+ layer.controllerElement.appendChild(moveUpHandle);
+
+ const moveDownHandle = document.createElement('div');
+ moveDownHandle.classList.add('handle');
+ moveDownHandle.classList.add('bottom-right');
+ moveDownHandle.innerHTML = '
';
+ moveDownHandle.addEventListener('click', () => {
+ const index = layers.indexOf(layer);
+ if (index < layers.length - 1) {
+ layers.move(layer, index + 1);
+ }
+ });
+ layer.controllerElement.appendChild(moveDownHandle);
+
+ layer.controllerElement.addEventListener('click', () => {
+ layers.setActive(layer);
+ });
+
+ layer.activate = function() {
+ layer.active = true;
+ layer.controllerElement.classList.add('active');
}
- updateColorPreview();
-});
+ layer.deactivate = function() {
+ layer.active = false;
+ layer.controllerElement.classList.remove('active');
+ }
-// }}}
+ return layer;
+} // }}}
-// mouseleave {{{
+function makeLayers({height=600, width=800}) { // {{{
+ const layers = [];
+ layers.height = height;
+ layers.width = width;
-canvasArea.addEventListener('mouseleave', (e) => {
- isMouseDown = false;
- brushPreview.style.display = 'none';
-});
+ layers.addButton = document.createElement('div');
+ layers.addButton.className = 'layer-add-button';
+ layers.addButton.innerHTML = '
';
+ layers.addButton.addEventListener('click', () => {
+ layers.add();
+ });
-// }}}
+ layers.setHeight = function(height) {
+ layers.height = height;
+ easelElement.style.height = height + 2 + 'px';
+ }
-// tools {{{
+ layers.setHeight(height);
-var toolButtons = [];
+ layers.setWidth = function(width) {
+ layers.width = width;
+ easelElement.style.width = width + 2 + 'px';
+ }
-function changeTool(toolName) {
- toolButtons.forEach(button => button.button.classList.remove('active'));
- toolButtons.find(button => button.name === toolName).button.classList.add('active');
- tool = toolName;
- brushPreview.style.display = 'none';
- updateInfos();
-}
+ layers.setWidth(width);
-function createToolButton(displayName, icon, toolName, jumpKey=undefined, temporaryKey=undefined) {
- const button = document.createElement('div');
- button.classList.add('button');
- button.classList.add('tool');
- button.innerHTML = icon;
- button.title = displayName;
- button.addEventListener('click', () => {
- changeTool(toolName);
- });
- if (jumpKey) {
- const jumpKeyHint = document.createElement('span');
- jumpKeyHint.className = 'jump-key-hint';
- jumpKeyHint.innerHTML = jumpKey;
- button.appendChild(jumpKeyHint);
+ layers.resetPosition = function() {
+ const studioRect = studioElement.getBoundingClientRect();
+ easelElement.style.left = `${studioRect.left}px`;
+ easelElement.style.top = `${studioRect.top}px`;
}
- if (temporaryKey) {
- const temporaryKeyHint = document.createElement('span');
- temporaryKeyHint.className = 'temporary-key-hint';
- temporaryKeyHint.innerHTML = temporaryKey;
- button.appendChild(temporaryKeyHint);
+
+ layers.refreshControllers = function() {
+ layerControllersElement.innerHTML = '';
+ layers.forEach(layer => {
+ layerControllersElement.appendChild(layer.controllerElement);
+ });
+ layerControllersElement.appendChild(layers.addButton);
}
- toolBar.appendChild(button);
- return button;
-}
+ layers.refreshLayers = function() {
+ easelElement.innerHTML = '';
+ layers.forEach(layer => {
+ easelElement.appendChild(layer.canvas);
+ });
+ }
+
+ layers.refresh = function() {
+ layers.refreshControllers();
+ layers.refreshLayers();
+ }
-toolButtons.push({'name': 'brush', 'button': createToolButton('Brush', '
', 'brush', 'e', undefined)});
-toolButtons.push({'name': 'content-move', 'button': createToolButton('Move Content', '
', 'content-move', 'h', undefined)});
-toolButtons.push({'name': 'move', 'button': createToolButton('Move Canvas', '
', 'move', 'm', undefined)});
-toolButtons.push({'name': 'zoom', 'button': createToolButton('Zoom', '
', 'zoom', 'z', undefined)});
-toolButtons.push({'name': 'resize', 'button': createToolButton('Resize', '
', 'resize', 'r', undefined)});
-toolButtons.push({'name': 'color-picker', 'button': createToolButton('Color Picker', '
', 'color-picker', 'a', undefined)});
-toolButtons.push({'name': 'color-mix', 'button': createToolButton('Color Mix', '
', 'color-mix', 's', undefined)});
-toolButtons.push({'name': 'brush-size', 'button': createToolButton('Brush Size', '
', 'brush-size', 'd', undefined)});
-toolButtons.push({'name': 'bucket-fill', 'button': createToolButton('Bucket Fill', '
', 'bucket-fill', 'f', undefined)});
+ layers.add = function() {
+ const layer = makeLayer({
+ height: layers.height,
+ width: layers.width,
+ });
+ layers.push(layer);
+ layer.activate();
+ layers.refresh();
+ }
+
+ layers.delete = function(layer) {
+ layer.canvas.deleteCanvas();
+ layers.splice(layers.indexOf(layer), 1);
+ layers.refresh();
+ }
+
+ layers.deleteAll = function() {
+ layers.forEach(layer => layer.deleteCanvas());
+ // TODO
+ }
+
+ layers.move = function(layer, index) {
+ layers.splice(layers.indexOf(layer), 1);
+ layers.splice(index, 0, layer);
+ }
+
+ layers.setActive = function(layer) {
+ layers.forEach(layer => layer.deactivate());
+ layer.activate();
+ }
+
+ layers.getActive = function() {
+ return layers.find(layer => layer.active);
+ }
+
+ return layers;
+} // }}}
// }}}
-// menu functons {{{
-
-function flipCanvasHorizontally(e) {
- saveState();
- ctx.save();
- saveCanvasContents();
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.scale(-1, 1);
- ctx.translate(-canvas.width, 0);
- ctx.drawImage(tempCanvas, 0, 0);
- ctx.restore();
+const layers = makeLayers({height: initialHeight, width: initialWidth});
+layers.add();
+layers.add();
+layers[0].canvas.fill('rgb(255, 255, 255)');
+layers.setActive(layers[1]);
+
+// }}}
+
+// COLOR PREVIEW {{{
+
+function makeColorPreview() {
+ const colorPreview = {}
+ colorPreview.element = document.createElement('div');
+ colorPreview.element.id = 'color-preview';
+ colorPreview.element.className = 'puck';
+ colorPreview.element.style.backgroundColor = brushColor;
+ commandBarElement.appendChild(colorPreview.element);
+ colorPreview.update = function() {
+ colorPreview.element.style.backgroundColor = brushColor;
+ }
}
-function flipCanvasVertically(e) {
- saveState();
- ctx.save();
- saveCanvasContents();
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.scale(1, -1);
- ctx.translate(0, -canvas.height);
- ctx.drawImage(tempCanvas, 0, 0);
- ctx.restore();
+const colorPreview = makeColorPreview();
+
+// }}}
+
+// COMMANDS {{{
+
+// FACTORY {{{
+
+function makeCommand({name, key, icon, func}) {
+ if (!name) throw new Error('No name provided');
+ if (!icon) throw new Error('No icon provided');
+ if (!func) throw new Error('No click function provided');
+ if (!key) throw new Error('No key provided');
+
+ const command = {};
+ command.name = name;
+ command.key = key;
+ command.func = function() {
+ func();
+ infos.update();
+ }
+ command.button = makeButtonElement({
+ icon: icon,
+ name: name,
+ key: key,
+ func: command.func,
+ });
+ commandBarElement.appendChild(command.button.element);
+
+ return command
}
-function saveCanvas(e) {
- const link = document.createElement('a');
- link.download = 'canvas.png';
- link.href = canvas.toDataURL();
- link.click();
+function makeCommands() {
+ const commands = [];
+
+ commands.add = function({name, key, icon, func}) {
+ const command = makeCommand({name, key, icon, func});
+ commands.push(command);
+ }
+
+ commands.get = function(name) {
+ return commands.find(command => command.name === name);
+ }
+
+ commands.click = function(name) {
+ const command = commands.get(name);
+ command.func();
+ }
+
+ return commands;
}
-function openCanvas(e) {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = 'image/*';
- input.onchange = (e) => {
- const file = e.target.files[0];
- const reader = new FileReader();
- reader.onload = (e) => {
- const img = new Image();
- img.onload = () => {
- canvas.width = img.width;
- canvas.height = img.height;
- ctx.drawImage(img, 0, 0);
+
+// }}}
+
+const commands = makeCommands();
+
+commands.add({ // flip-horizontally {{{
+ name: 'flip-horizontally',
+ key: 'f',
+ icon: '
',
+ func: function flipCanvasHorizontally() {
+ const canvas = layers.getActive().canvas;
+ const ctx = canvas.ctx;
+ ctx.save();
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.scale(-1, 1);
+ ctx.translate(-canvas.width, 0);
+ canvas.restoreCanvas();
+ ctx.restore();
+ }
+}); // }}}
+
+commands.add({ // flip-vertically {{{
+ name: 'flip-vertically',
+ key: 'v',
+ icon: '
',
+ func: function flipCanvasVertically() {
+ const canvas = layers.getActive().canvas;
+ const ctx = canvas.ctx;
+ ctx.save();
+ canvas.saveCanvas();
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.scale(1, -1);
+ ctx.translate(0, -canvas.height);
+ canvas.restoreCanvas();
+ ctx.restore();
+ }
+}); // }}}
+
+commands.add({ // export {{{
+ name: 'export',
+ key: 'e',
+ icon: '
',
+ func: function exportCanvas() {
+ const canvas = layers.getActive().canvas;
+ const link = document.createElement('a');
+ link.download = 'canvas.png';
+ link.href = canvas.toDataURL();
+ link.click();
+ }
+}); // }}}
+
+commands.add({ // import {{{
+ name: 'import',
+ key: 'i',
+ icon: '
',
+ func: function importCanvas() {
+ const canvas = layers.getActive().canvas;
+ const ctx = canvas.ctx;
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'image/*';
+ input.onchange = (e) => {
+ const file = e.target.files[0];
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const img = new Image();
+ img.onload = () => {
+ canvas.width = img.width;
+ canvas.height = img.height;
+ ctx.drawImage(img, 0, 0);
+ }
+ img.src = e.target.result;
}
- img.src = e.target.result;
+ reader.readAsDataURL(file);
}
- reader.readAsDataURL(file);
+ input.click();
}
- input.click();
-}
+}); // }}}
+
+commands.add({ // clear {{{
+ name: 'clear',
+ key: 'c',
+ icon: '
',
+ func: function clearCanvas() {
+ const canvas = layers.getActive().canvas;
+ const ctx = canvas.ctx;
+ canvas.saveCanvas();
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = 'white';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
+}); // }}}
+
+commands.add({ // change-shape {{{
+ name: 'change-shape',
+ key: 's',
+ icon: `
`,
+ func: function changeShape() {
+ const currentIndex = shapes.indexOf(brushShape);
+ brushShape = shapes[(currentIndex + 1) % shapes.length];
+ }
+}); // }}}
-function clearCanvas(e) {
- saveState();
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-}
+// }}}
+
+// TOOLS {{{
+
+// FACTORY {{{
-function resetZoom(e) {
- zoom = 1;
- canvas.style.width = canvas.width * zoom + 'px';
- canvas.style.height = canvas.height * zoom + 'px';
- canvasWidth = canvas.width;
- canvasHeight = canvas.height;
+function makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}) {
+ if (!name) throw new Error('No name provided');
+ if (!key) throw new Error('No key provided');
+ if (!icon) throw new Error('No icon provided');
- canvasAreaRect = canvasArea.getBoundingClientRect();
+ const tool = {};
+ tool.name = name;
+ tool.key = key;
+ tool.icon = icon;
+ tool.mouseDown = mouseDown;
+ tool.mouseMove = mouseMove;
+ tool.mouseUp = mouseUp;
+ tool.active = false;
+
+ tool.activate = function() {
+ currentTool = tool.name;
+ tool.active = true;
+ tool.button.element.classList.add('active');
+ }
- canvasContainer.style.left = `${canvasAreaRect.left}px`;
- canvasContainer.style.top = `${canvasAreaRect.top}px`;
+ tool.deactivate = function() {
+ tool.active = false;
+ tool.button.element.classList.remove('active');
+ }
+
+ tool.button = makeButtonElement({
+ icon: tool.icon,
+ name: tool.name,
+ key: tool.key,
+ func: function() {
+ tools.activate(tool.name);
+ }
+ });
+
+ toolBarElement.appendChild(tool.button.element);
+
+ return tool;
}
-// }}}
+function makeTools() {
+ const tools = [];
-// menu {{{
+ tools.add = function({name, key, icon, mouseDown, mouseMove, mouseUp}) {
+ const tool = makeTool({name, key, icon, mouseDown, mouseMove, mouseUp});
+ tools.push(tool);
+ }
-var menuButtons = [];
+ tools.get = function(name) {
+ return tools.find(tool => tool.name === name);
+ }
-function createMenuButton(icon, name, clickFunction) {
- const button = document.createElement('div');
- button.className = 'button';
- button.innerHTML = icon;
- button.title = name;
- if (clickFunction) {
- button.addEventListener('click', () => {
- clickFunction()
- updateInfos();
- });
+ tools.activate = function(name) {
+ const tool = tools.get(name);
+ tools.forEach(tool => tool.deactivate());
+ tool.activate();
}
- menuBar.appendChild(button);
- return button;
-}
-menuButtons.push(createMenuButton('
', 'Open', openCanvas));
-menuButtons.push(createMenuButton('
', 'Save', saveCanvas));
-menuButtons.push(createMenuButton('
', 'Flip Horizontally', flipCanvasHorizontally));
-menuButtons.push(createMenuButton('
', 'Flip Vertically', flipCanvasVertically));
-menuButtons.push(createMenuButton('
', 'Undo', undo));
-menuButtons.push(createMenuButton('
', 'Redo', redo));
-menuButtons.push(createMenuButton('
', 'Clear', clearCanvas));
-menuButtons.push(createMenuButton('
', 'Reset', resetZoom));
-menuButtons.push(createMenuButton('
', 'Add Color', createPuck));
+ return tools;
+}
// }}}
-// pucks {{{
-
-function createPuck(c, editable=true) {
- if (c === undefined) {
- c = color;
+const tools = makeTools();
+
+tools.add({ // brush {{{
+ name: 'brush',
+ key: 'b',
+ icon: '
',
+ mouseDown: function(e) {
+ const canvas = layers.getActive().canvas;
+ if (brushSize === 1) {
+ canvas.drawPixel(canvasStartX, canvasStartY, brushColor);
+ } else {
+ canvas.drawShape(canvasStartX, canvasStartY, brushShape, brushSize, brushColor);
+ }
+ },
+ mouseMove: function(e) {
+ const canvas = layers.getActive().canvas;
+ if (brushSize === 1) {
+ canvas.drawLineWithPixels(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushColor);
+ } else {
+ canvas.drawLineWithShape(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushShape, brushSize, brushColor);
+ }
+ canvasStartX = canvasEndX;
+ canvasStartY = canvasEndY;
+ },
+}); // }}}
+
+tools.add({ // content-move {{{
+ name: 'content-move',
+ key: 'h',
+ icon: '
',
+ mouseDown: function(e) {
+ const canvas = layers.getActive().canvas;
+ canvas.saveCanvas();
+ },
+ mouseMove: function(e) {
+ const canvas = layers.getActive().canvas;
+ canvas.clearCanvas();
+ canvas.restoreCanvas(dX, dY);
+ },
+}); // }}}
+
+tools.add({ // move {{{
+ name: 'move',
+ key: 'm',
+ icon: '
',
+ mouseDown: function(e) {
+ startX = e.clientX - easelElement.offsetLeft;
+ startY = e.clientY - easelElement.offsetTop;
+ },
+ mouseMove: function(e) {
+ easelElement.style.left = dX + 'px';
+ easelElement.style.top = dY + 'px';
+ },
+}); // }}}
+
+tools.add({ // zoom {{{
+ name: 'zoom',
+ key: 'z',
+ icon: '
',
+ mouseMove: function(e) {
+ // TODO all canvases
+ // const canvas = layers.getActive().canvas;
+ zoom += dX * dZoom;
+ if (zoom < 0.1) zoom = 0.1;
+ // canvas.style.height = canvasHeight * zoom + 'px';
+ // canvas.style.width = canvasWidth * zoom + 'px';
+ startX = endX;
+ }
+}); // }}}
+
+tools.add({ // bucket-fill {{{
+ name: 'bucket-fill',
+ key: 'k',
+ icon: '
',
+ mouseDown: function(e) {
+ const canvas = layers.getActive().canvas;
+ canvas.floodFill(canvasStartX, canvasStartY, brushColor);
+ }
+}); // }}}
+
+tools.add({ // color-picker {{{
+ name: 'color-picker',
+ key: 'a',
+ icon: '
',
+ mouseDown: function(e) {
+ const canvas = layers.getActive().canvas;
+ const imageData = canvas.ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data;
+ const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`;
+ brushColor = pickedColor;
+ colorPreview.update();
+ }
+}); // }}}
+
+tools.add({ // brush-size {{{
+ name: 'brush-size',
+ key: 'd',
+ icon: '
',
+ mouseMove: function(e) {
+ brushSize += dX * dBrushSize;
+ if (brushSize < 1) brushSize = 1;
+ if (brushSize > maxBrushSize) brushSize = maxBrushSize;
+ startX = endX;
}
+}); // }}}
+
+tools.add({ // resize {{{
+ name: 'resize',
+ key: 'r',
+ icon: '
',
+ mouseMove: function(e) {
+ // const canvas = layers.getActive().canvas;
+ // let newWidth = canvasWidth + dX / zoom;
+ // let newHeight = canvasHeight + dY / zoom;
+ // if (newWidth > 0 && newHeight > 0) {
+ // canvas.setWidth(newWidth);
+ // canvas.setHeight(newHeight);
+ // canvas.style.width = newWidth * zoom + 'px';
+ // canvas.style.height = newHeight * zoom + 'px';
+ // canvas.ctx.clearRect(0, 0, canvas.width, canvas.height);
+ // canvas.ctx.fillStyle = backgroundColor;
+ // canvas.ctx.fillRect(0, 0, canvas.width, canvas.height);
+ // canvas.ctx.drawImage(tempCanvas, 0, 0);
+ // }
+ }
+}); // }}}
- const puck = document.createElement('div');
- puck.className = 'puck';
- puck.style.backgroundColor = c;
+tools.add({ // color-mix {{{
+ name: 'color-mix',
+ key: 'x',
+ icon: '
',
+ mouseMove: function(e) {
+ // const canvas = layers.getActive().canvas;
+ const imageData = ctx.getImageData(canvasEndX, canvasEndY, 1, 1).data;
+ const canvasColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`;
- const selectHandle = document.createElement('div');
- selectHandle.className = 'select-handle';
- selectHandle.innerHTML = '
';
- puck.appendChild(selectHandle);
+ const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2));
+ const t = Math.min(1, distance / 300);
- selectHandle.addEventListener('click', () => {
- color = puck.style.backgroundColor;
- updateColorPreview();
- updateInfos();
- });
+ const mixedColor = mixbox.lerp(brushColor, canvasColor, t);
- if (editable) {
- const updateHandle = document.createElement('div');
- updateHandle.className = 'update-handle';
- updateHandle.innerHTML = '
';
- puck.appendChild(updateHandle);
+ brushColor = mixedColor;
- updateHandle.addEventListener('click', () => {
- puck.style.backgroundColor = color;
- });
+ startX = e.clientX;
+ startY = e.clientY;
+ }
+}); // }}}
+
+// }}}
+
+// PUCKS {{{
+
+// FACTORY {{{
+function makePuck({puckColor, key, editable=true}) {
+ if (!puckColor) throw new Error('No puck color provided');
+
+
+ const puck = {}
+ puck.element = document.createElement('div');
+ puck.element.style.backgroundColor = puckColor;
+ puck.element.className = 'puck';
+
+ if (editable) {
const deleteHandle = document.createElement('div');
deleteHandle.className = 'delete-handle';
deleteHandle.innerHTML = '
';
- puck.appendChild(deleteHandle);
-
+ puck.element.appendChild(deleteHandle);
deleteHandle.addEventListener('click', () => {
- console.log("test");
- puck.remove();
+ puck.element.remove();
});
}
- puck.addEventListener('mousedown', (e) => {
- let isMixing = true;
- const startTime = Date.now(); // Record the time when the mouse is pressed
-
- // Interval to update the color based on time
- const interval = setInterval(() => {
- if (isMixing) {
- const elapsedTime = Date.now() - startTime;
- const t = Math.min(1, elapsedTime / 10000);
-
- const mixedColor = mixbox.lerp(color, puck.style.backgroundColor, t);
-
- color = mixedColor;
-
- updateColorPreview();
- updateInfos();
- }
- }, 50); // Update every 50ms
+ if (key) {
+ puck.key = key;
+ const keyHint = document.createElement('div');
+ keyHint.className = 'key-hint';
+ keyHint.innerHTML = key;
+ puck.element.appendChild(keyHint);
+ }
- document.addEventListener('mouseup', onMouseUp);
+ function mixx(startTime) {
+ var interval = setInterval(() => {
+ const elapsedTime = Date.now() - startTime;
+ const t = Math.min(1, elapsedTime / 10000);
+ const mixedColor = mixbox.lerp(brushColor, puck.style.backgroundColor, t);
+ brushColor = mixedColor;
+ colorPreview.update();
+ infos.update();
+ }, 50);
+ return interval;
+ }
+ puck.element.addEventListener('mousedown', () => {
+ const startTime = Date.now();
+ var interval = mixx(startTime);
function onMouseUp() {
- isMixing = false;
- clearInterval(interval); // Stop the interval when the mouse is released
+ clearInterval(interval);
document.removeEventListener('mouseup', onMouseUp);
}
+ document.addEventListener('mouseup', onMouseUp);
});
- // puck.addEventListener('mousedown', (e) => {
- // let isMixing = true;
- // let startX = e.clientX;
- // let startY = e.clientY;
+ puck.keydown = function(e) {
+ if (e.key == key) {
+ const startTime = Date.now();
+ var interval = mixx(startTime);
+ function onKeyUp() {
+ clearInterval(interval);
+ document.removeEventListener('keyup', onKeyUp);
+ }
+ document.addEventListener('keyup', onKeyUp);
+ }
+ }
+
+ commandBarElement.appendChild(puck.element);
+
+ return puck;
+}
- // document.addEventListener('mousemove', onMouseMove);
- // document.addEventListener('mouseup', onMouseUp);
+function makePucks() {
+ const pucks = [];
- // function onMouseMove(e) {
- // if (isMixing) {
- // const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2));
+ pucks.add = function({puckColor, key, editable}) {
+ const puck = makePuck({puckColor, key, editable});
+ pucks.push(puck);
+ }
- // const t = Math.min(1, distance / 300);
+ return pucks;
+}
- // const mixedColor = mixbox.lerp(color, puck.style.backgroundColor, t);
+// }}}
- // color = mixedColor;
+const pucks = makePucks();
- // startX = e.clientX;
- // startY = e.clientY;
- // updateColorPreview();
- // updateInfos();
- // }
- // }
- // function onMouseUp() {
- // isMixing = false;
- // document.removeEventListener('mousemove', onMouseMove);
- // document.removeEventListener('mouseup', onMouseUp);
- // }
- // });
+pucks.add({
+ puckColor: 'rgb(0, 0, 0)',
+ key: '1',
+ editable: false,
+});
+pucks.add({
+ puckColor: 'rgb(255, 255, 255)',
+ key: '2',
+ editable: false,
+});
- menuBar.appendChild(puck);
-}
+pucks.add({
+ puckColor: 'rgb(255, 0, 0)',
+ key: '3',
+ editable: false,
+});
-createPuck(c='rgb(0, 0, 0)', editable=false);
-createPuck(c='rgb(255, 255, 255)', editale=false);
-createPuck(c='rgb(0, 255, 0)', editale=false);
-createPuck(c='rgb(0, 0, 255)', editale=false);
-createPuck(c='rgb(255, 255, 0)', editale=false);
-createPuck(c='rgb(255, 0, 0)', editale=false);
-createPuck(c='rgb(255, 0, 255)', editale=false);
-createPuck(c='rgb(0, 255, 255)', editale=false);
+pucks.add({
+ puckColor: 'rgb(0, 255, 0)',
+ key: '4',
+ editable: false,
+});
+
+pucks.add({
+ puckColor: 'rgb(0, 0, 255)',
+ key: '5',
+ editable: false,
+});
// }}}
-// info {{{
+// INFO {{{
+
+// FACTORY {{{
+
+function makeInfo({name, updateFunction}) {
+ if (!name) throw new Error('No name provided');
+ if (!updateFunction) throw new Error('No update function provided');
-var infos = [];
+ const info = {};
+ info.name = name;
+ info.updateFunction = updateFunction;
+
+ info.element = document.createElement('span');
+ info.element.className = 'info';
-function createInfo(name, updateFunction) {
- const info = document.createElement('span');
- info.className = 'info';
const key = document.createElement('span');
key.className = 'key';
- key.innerHTML = name + ':';
+ key.innerHTML = info.name + ':';
+
const value = document.createElement('span');
value.className = 'value';
value.innerHTML = '0';
- info.appendChild(key);
- info.appendChild(value);
- infoBar.appendChild(info);
- function update() {
+
+ info.element.appendChild(key);
+ info.element.appendChild(value);
+
+ infoBarElement.appendChild(info.element);
+
+ info.update = function() {
let v = updateFunction();
if (v === undefined) v = '?';
value.innerHTML = v;
}
- return update;
+
+ return info;
}
-infos.push(createInfo('zoom', function() {
- var percent = zoom * 100;
- return percent.toFixed(0) + '%';
-}));
-infos.push(createInfo('brush', function() { return brushSize; }));
-infos.push(createInfo('x', function() { return canvasEndX; }));
-infos.push(createInfo('y', function() { return canvasEndY; }));
-infos.push(createInfo('color', function() { return color; }));
-infos.push(createInfo('width', function() { return canvas.width; }));
-infos.push(createInfo('height', function() { return canvas.height; }));
-
-function updateInfos() {
- infos.forEach(info => info());
+function makeInfos() {
+ const infos = []
+ infos.add = function({name, updateFunction}) {
+ const info = makeInfo({name, updateFunction});
+ infos.push(info);
+ }
+ infos.update = function() {
+ infos.forEach(function(info){
+ info.update();
+ });
+ }
+ return infos;
}
// }}}
-// keybindings {{{
+const infos = makeInfos();
-let keyDown = false;
-let oldTool = tool;
+infos.add({
+ name: 'zoom',
+ updateFunction: function() {
+ var percent = zoom * 100;
+ return percent.toFixed(0) + '%';
+ }
+});
-const toolBindings = [
- {'key': 'e', 'tool': 'brush', 'persistent': true},
- {'key': 'h', 'tool': 'content-move', 'persistent': true},
- {'key': 'm', 'tool': 'move', 'persistent': true},
- {'key': 'z', 'tool': 'zoom', 'persistent': true},
- {'key': 'r', 'tool': 'resize', 'persistent': true},
- {'key': 'a', 'tool': 'color-picker', 'persistent': false},
- {'key': 's', 'tool': 'color-mix', 'persistent': false},
- {'key': 'd', 'tool': 'brush-size', 'persistent': false},
-]
+infos.add({
+ name: 'brush',
+ updateFunction: function() {
+ return brushSize;
+ }
+});
-const functionBindings = [
- {'key': 'u', 'function': undo},
- {'key': 'y', 'function': redo},
- {'key': 'backspace', 'function': clearCanvas},
-]
-document.addEventListener('keydown', (e) => {
- if (keyDown) return;
+infos.add({
+ name: 'x',
+ updateFunction: function() {
+ return canvasEndX;
+ }
+});
- if (toolBindings.map(b => b.key).includes(e.key)) {
- oldTool = tool;
- keyDown = true;
- changeTool(toolBindings.find(b => b.key === e.key).tool);
- return;
+
+infos.add({
+ name: 'y',
+ updateFunction: function() {
+ return canvasEndY;
}
+});
- if (functionBindings.map(b => b.key).includes(e.key)) {
- functionBindings.find(b => b.key === e.key).function();
+infos.add({
+ name: 'color',
+ updateFunction: function() {
+ return brushColor;
}
});
-document.addEventListener('keyup', (e) => {
- keyDown = false;
- if (toolBindings.filter(b => !b.persistent).map(b => b.key).includes(e.key)) {
- changeTool(oldTool);
+infos.add({
+ name: 'width',
+ updateFunction: function() {
+ return "width";
+ }
+});
+
+infos.add({
+ name: 'height',
+ updateFunction: function() {
+ return "height";
+ }
+});
+
+infos.add({
+ name: 'shape',
+ updateFunction: function() {
+ return brushShape;
}
});
// }}}
-// start {{{
+// MOUSE EVENT LISTENERS {{{
+
+studioElement.addEventListener('mousedown', (e) => {
+ const canvas = layers.getActive().canvas;
+ isMouseDown = true;
+ startX = e.clientX;
+ startY = e.clientY;
+ canvasStartX = canvas.getPositionOnCanvas(e).x;
+ canvasStartY = canvas.getPositionOnCanvas(e).y;
+
+ for (var i = 0; i < tools.length; i++) {
+ var tool = tools[i];
+ if (tool.name === currentTool) {
+ if (tool.mouseDown) {
+ tool.mouseDown(e);
+ break;
+ }
+ }
+ }
+
+ infos.update();
+
+});
+
+studioElement.addEventListener('mousemove', (e) => {
+ const canvas = layers.getActive().canvas;
+ endX = e.clientX;
+ endY = e.clientY;
+ dX = endX - startX;
+ dY = endY - startY;
+ canvasEndX = canvas.getPositionOnCanvas(e).x;
+ canvasEndY = canvas.getPositionOnCanvas(e).y;
+ canvasDX = canvasEndX - canvasStartX;
+ canvasDY = canvasEndY - canvasStartY;
+
+ if (currentTool == 'brush-size') {
+ brushPreviewElement.style.display = 'block';
+ brushPreviewElement.style.width = brushSize + 'px';
+ brushPreviewElement.style.height = brushSize + 'px';
+ brushPreviewElement.style.left = e.clientX - brushSize / 2 + 'px';
+ brushPreviewElement.style.top = e.clientY - brushSize / 2 + 'px';
+ }
-ctx.fillStyle = backgroundColor;
-ctx.fillRect(0, 0, canvas.width, canvas.height);
-updateInfos();
-toolButtons[0]['button'].click();
-resetZoom();
+ if (isMouseDown) {
+ for (var i = 0; i < tools.length; i++) {
+ var tool = tools[i];
+ if (tool.name === currentTool) {
+ if (tool.mouseMove) {
+ tool.mouseMove(e);
+ break;
+ }
+ }
+ }
+ }
+
+ infos.update();
+
+});
+
+studioElement.addEventListener('mouseup', () => {
+ isMouseDown = false;
+ infos.update();
+});
+
+studioElement.addEventListener('mouseleave', () => {
+ isMouseDown = false;
+ brushPreviewElement.style.display = 'none';
+ infos.update();
+});
+
+// }}}
+
+// KEYBINDINGS {{{
+
+document.addEventListener('keydown', (e) => {
+ if (isKeyDown) return;
+
+ tools.forEach(tool => {
+ if (tool.key.toLowerCase() === e.key.toLowerCase()) {
+ prevTool = currentTool;
+ currentTool = tool.name;
+ }
+ });
+
+ commands.forEach(command => {
+ if (command.key.toLowerCase() === e.key.toLowerCase()) {
+ command.func();
+ }
+ });
+
+ pucks.filter(puck => puck.key !== undefined).forEach(puck => {
+ if (puck.key.toLowerCase() === e.key.toLowerCase()) {
+ puck.keydown(e);
+ }
+ });
+
+ isKeyDown = true;
+
+});
+
+document.addEventListener('keyup', (e) => {
+ tools.forEach(tool => {
+ if (tool.key.toLowerCase() === e.key) {
+ currentTool = prevTool;
+ }
+ });
+
+ isKeyDown = false;
+
+});
// }}}
+layers.resetPosition();
+tools.activate('brush');
diff --git a/style.css b/style.css
index 0662bbb..4e326e6 100644
--- a/style.css
+++ b/style.css
@@ -58,6 +58,7 @@ body {
display: flex;
padding: 10px;
padding-top: 0;
+ padding-bottom: 0;
background-color: #ddd;
gap: 10px;
height: 100%;
@@ -65,22 +66,24 @@ body {
justify-content: flex-start;
}
-#canvas-area {
+#studio {
flex-grow: 1;
height: 100%;
border: 1px solid;
cursor: crosshair;
}
-#canvas-container {
+#easel {
position: absolute;
border: 1px solid;
z-index: -1;
}
-#canvas {
- display: block;
+canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
}
.button {
@@ -126,11 +129,11 @@ body {
cursor: pointer;
}
-.update-handle {
+.delete-handle {
position: absolute;
top: 0;
right: 0;
- font-size: .5em;
+ font-size: .6em;
background-color: black;
color: white;
padding: 1px 0 3px 4px;
@@ -144,33 +147,16 @@ body {
cursor: pointer;
}
-.delete-handle {
- position: absolute;
- top: 0;
- left: 0;
- font-size: .5em;
- background-color: black;
- color: white;
- padding: 1px 4px 3px 0;
- display: flex;
- align-items: center;
- justify-content: center;
- text-align: center;
- border-radius: 0 0 10px 0;
- border-right: 1px solid white;
- border-bottom: 1px solid white;
- cursor: pointer;
-}
-.jump-key-hint {
+.key-hint {
display: block;
- height: 12px;
- width: 8px;
+ height: 13px;
+ width: 9px;
line-height: 8px;
/* width: 12px; */
position: absolute;
- top: 0;
- left: 0;
+ top: 2px;
+ left: 1px;
font-size: 1em;
/* padding: 2px 2px 2px 2px; */
/* display: flex; */
@@ -180,8 +166,99 @@ body {
border-radius: 0 0 5px 0;
/* border-right: 1px solid; */
/* border-bottom: 1px solid; */
+ text-shadow:
+ 1px 1px 0 #000,
+ -1px 1px 0 #000,
+ -1px -1px 0 #000,
+ 1px -1px 0 #000;
+ color: white;
+ pointer-events: none;
}
+
.button.active, .button:active {
background-color: darkgray;
}
+
+#layer-controllers {
+ background-color: darkgrey;
+ border: 1px solid;
+ height: 100%;
+}
+
+.layer-controller {
+ background-color: lightgray;
+ height: 30px;
+ width: 60px;
+ border: 1px solid;
+ border-radius: 2px;
+ padding: 5px;
+ position: relative;
+ margin: 1px;
+}
+
+.layer-controller > i {
+ color: darkgray;
+ cursor: pointer;
+}
+
+.layer-controller.active > i {
+ color: black;
+}
+
+.handle {
+ display: block;
+ font-size: 9px;
+ height: 11px;
+ width: 24px;
+ position: absolute;
+ justify-content: center;
+ text-align: center;
+ cursor: pointer;
+ border-radius: 2px;
+ border: 1px solid;
+ margin: 2px;
+}
+
+.layer-add-button {
+ background-color: lightgray;
+ height: 12px;
+ width: 60px;
+ border: 1px solid;
+ border-radius: 2px;
+ position: relative;
+ margin: 1px;
+ text-align: center;
+ font-size: 10px;
+ cursor: pointer;
+}
+
+.layer-add-button:hover {
+ background-color: darkgray;
+}
+
+
+
+.handle:hover {
+ background-color: darkgray;
+}
+
+.top-left {
+ top: 0;
+ left: 0;
+}
+
+.bottom-right {
+ bottom: 0;
+ right: 0;
+}
+
+.top-right {
+ top: 0;
+ right: 0;
+}
+
+.bottom-left {
+ bottom: 0;
+ left: 0;
+}
diff --git a/temp.js b/temp.js
new file mode 100644
index 0000000..fdb075f
--- /dev/null
+++ b/temp.js
@@ -0,0 +1,724 @@
+const colorPreview = document.createElement('div');
+colorPreview.id = 'color-preview';
+colorPreview.className = 'puck';
+colorPreview.style.backgroundColor = color;
+
+menuBar.appendChild(colorPreview);
+
+
+// }}}
+
+// helpers {{{
+
+
+function saveCanvas() {
+ const dataURL = canvas.toDataURL();
+ const dimensions = `${canvas.width}x${canvas.height}`;
+ localStorage.setItem('mixxCanvas', dataURL);
+ localStorage.setItem('mixxDimensions', dimensions);
+ console.log('Canvas saved');
+}
+
+function loadCanvas() {
+ const dataURL = localStorage.getItem('mixxCanvas');
+ const dimensions = localStorage.getItem('mixxDimensions');
+ if (dataURL && dimensions) {
+ const img = new Image();
+ img.src = dataURL;
+ img.onload = function() {
+ canvas.width = dimensions.split('x')[0];
+ canvas.height = dimensions.split('x')[1];
+ canvas.style.width = canvas.width * zoom + 'px';
+ canvas.style.height = canvas.height * zoom + 'px';
+ canvasWidth = canvas.width;
+ canvasHeight = canvas.height;
+ ctx.drawImage(img, 0, 0);
+ }
+ } else {
+ console.log('No saved canvas found');
+ }
+}
+
+function clearCanvasFromLocalStorage() {
+ localStorage.removeItem('savedCanvas');
+ localStorage.removeItem('canvasDimensions');
+}
+
+function saveState() {
+ if (undoStack.length >= maxHistory) {
+ undoStack.shift(); // Remove the oldest state if the stack exceeds the limit
+ }
+
+ undoStack.push({
+ imageData: canvas.toDataURL(),
+ width: canvas.width,
+ height: canvas.height
+ });
+
+ redoStack = []; // Clear the redo stack whenever a new action is performed
+}
+
+function undo() {
+ if (undoStack.length > 0) {
+ const currentState = {
+ imageData: canvas.toDataURL(),
+ width: canvas.width,
+ height: canvas.height
+ };
+
+ redoStack.push(currentState); // Save current state to the redo stack
+ const lastState = undoStack.pop(); // Get the last state from the undo stack
+
+ canvas.width = lastState.width;
+ canvas.height = lastState.height;
+ canvas.style.width = canvas.width * zoom + 'px';
+ canvas.style.height = canvas.height * zoom + 'px';
+ canvasWidth = canvas.width;
+ canvasHeight = canvas.height;
+
+ const img = new Image();
+ img.src = lastState.imageData;
+ img.onload = function() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, 0, 0);
+ };
+ }
+}
+
+function redo() {
+ if (redoStack.length > 0) {
+ const currentState = {
+ imageData: canvas.toDataURL(),
+ width: canvas.width,
+ height: canvas.height
+ };
+
+ undoStack.push(currentState); // Save current state to the undo stack
+ const nextState = redoStack.pop(); // Get the last state from the redo stack
+
+ canvas.width = nextState.width;
+ canvas.height = nextState.height;
+ canvas.style.width = canvas.width * zoom + 'px';
+ canvas.style.height = canvas.height * zoom + 'px';
+ canvasWidth = canvas.width;
+ canvasHeight = canvas.height;
+
+ const img = new Image();
+ img.src = nextState.imageData;
+ img.onload = function() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, 0, 0);
+ };
+ }
+}
+
+function getPositionOnCanvas(e) {
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: Math.round((e.clientX - rect.left) / zoom),
+ y: Math.round((e.clientY - rect.top) / zoom),
+ };
+}
+
+function drawCircle(x, y) {
+ ctx.beginPath();
+ ctx.arc(x, y, brushSize / 2, 0, 2 * Math.PI, false);
+ ctx.fillStyle = color;
+ ctx.fill();
+}
+
+function drawLineWithCircles(x1, y1, x2, y2) {
+ const dx = x2 - x1;
+ const dy = y2 - y1;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+ const steps = Math.ceil(distance / (brushSize / 5));
+
+ for (let i = 0; i <= steps; i++) {
+ const x = x1 + (dx * i) / steps;
+ const y = y1 + (dy * i) / steps;
+ drawCircle(x, y);
+ }
+}
+
+function saveCanvasContents() {
+ tempCanvas = document.createElement('canvas');
+ tempCanvas.width = canvas.width;
+ tempCanvas.height = canvas.height;
+ const tempCtx = tempCanvas.getContext('2d');
+ tempCtx.drawImage(canvas, 0, 0);
+}
+
+function updateColorPreview() {
+ colorPreview.style.backgroundColor = color;
+}
+
+function hexToRgbArray(hex) {
+ if (hex.startsWith('#')) {
+ hex = hex.slice(1);
+ }
+
+ if (hex.length === 3) {
+ hex = hex.split('').map(char => char + char).join('');
+ }
+
+ const bigint = parseInt(hex, 16);
+ return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
+}
+
+function floodFill(x, y, fillColor) {
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
+
+ const targetColor = getColorAtPixel(data, x, y);
+ const fillColorArray = hexToRgbArray(fillColor);
+
+ if (colorsMatch(targetColor, fillColorArray)) {
+ return; // The clicked point is already the fill color
+ }
+
+ const stack = [{x, y}];
+
+ while (stack.length > 0) {
+ const {x, y} = stack.pop();
+ const currentColor = getColorAtPixel(data, x, y);
+
+ if (colorsMatch(currentColor, targetColor)) {
+ setColorAtPixel(data, x, y, fillColorArray);
+
+ if (x > 0) stack.push({x: x - 1, y});
+ if (x < canvas.width - 1) stack.push({x: x + 1, y});
+ if (y > 0) stack.push({x, y: y - 1});
+ if (y < canvas.height - 1) stack.push({x, y: y + 1});
+ }
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+}
+
+function getColorAtPixel(data, x, y) {
+ const index = (y * canvas.width + x) * 4;
+ return [data[index], data[index + 1], data[index + 2], data[index + 3]];
+}
+
+function setColorAtPixel(data, x, y, color) {
+ const index = (y * canvas.width + x) * 4;
+ data[index] = color[0];
+ data[index + 1] = color[1];
+ data[index + 2] = color[2];
+ data[index + 3] = 255; // Set alpha to fully opaque
+}
+
+function colorsMatch(a, b) {
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
+}
+
+// }}}
+
+// mousedown {{{
+
+canvasArea.addEventListener('mousedown', (e) => {
+ if (e.target.closest('.puck')) return;
+
+ startX = e.clientX;
+ startY = e.clientY;
+ canvasStartX = getPositionOnCanvas(e).x;
+ canvasStartY = getPositionOnCanvas(e).y;
+ saveCanvasContents();
+ isMouseDown = true;
+
+ if (
+ tool === 'brush' ||
+ tool === 'content-move' ||
+ tool === 'resize' ||
+ tool === 'zoom' ||
+ tool === 'bucket-fill'
+ ) {
+ saveState();
+ }
+
+ if (tool === 'brush') {
+ console.log('brush');
+ drawCircle(canvasStartX, canvasStartY);
+ } else if (tool === 'bucket-fill') {
+ floodFill(canvasStartX, canvasStartY, color);
+ return;
+ } else if (tool === 'move') {
+ startX = e.clientX - canvasContainer.offsetLeft;
+ startY = e.clientY - canvasContainer.offsetTop;
+ } else if (tool === 'color-picker') {
+ const imageData = ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data;
+ const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`;
+ color = pickedColor;
+ console.log('Picked Color:', pickedColor);
+ updateColorPreview();
+ return;
+ }
+});
+
+// }}}
+
+// mousemove {{{
+
+canvasArea.addEventListener('mousemove', (e) => {
+
+ endX = e.clientX;
+ endY = e.clientY;
+ dX = endX - startX;
+ dY = endY - startY;
+
+ canvasEndX = getPositionOnCanvas(e).x;
+ canvasEndY = getPositionOnCanvas(e).y;
+ canvasDX = canvasEndX - canvasStartX;
+ canvasDY = canvasEndY - canvasStartY;
+
+ if (tool == 'brush-size') {
+ brushPreview.style.display = 'block';
+ brushPreview.style.width = brushSize + 'px';
+ brushPreview.style.height = brushSize + 'px';
+ brushPreview.style.left = e.clientX - brushSize / 2 + 'px';
+ brushPreview.style.top = e.clientY - brushSize / 2 + 'px';
+ }
+
+ if (isMouseDown) {
+
+ if (tool === 'brush-size') {
+ brushSize += dX * dBrushSize;
+ if (brushSize < 1) brushSize = 1;
+ if (brushSize > maxBrushSize) brushSize = maxBrushSize;
+ startX = endX;
+ } else if (tool === 'brush') {
+ drawLineWithCircles(canvasStartX, canvasStartY, canvasEndX, canvasEndY);
+
+ canvasStartX = canvasEndX;
+ canvasStartY = canvasEndY;
+
+ } else if (tool === 'content-move') {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(tempCanvas, dX, dY);
+ } else if (tool === 'move') {
+ canvasContainer.style.left = dX + 'px';
+ canvasContainer.style.top = dY + 'px';
+ } else if (tool === 'zoom') {
+ zoom += dX * dZoom;
+ if (zoom < 0.1) zoom = 0.1;
+ canvas.style.height = canvasHeight * zoom + 'px';
+ canvas.style.width = canvasWidth * zoom + 'px';
+ startX = endX;
+ } else if (tool === 'resize') {
+ let newWidth = canvasWidth + dX / zoom;
+ let newHeight = canvasHeight + dY / zoom;
+ if (newWidth > 0 && newHeight > 0) {
+ canvas.width = newWidth;
+ canvas.height = newHeight;
+ canvas.style.width = newWidth * zoom + 'px';
+ canvas.style.height = newHeight * zoom + 'px';
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(tempCanvas, 0, 0);
+ }
+ } else if (tool === 'color-mix') {
+
+ const imageData = ctx.getImageData(canvasEndX, canvasEndY, 1, 1).data;
+ const canvasColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`;
+
+ const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2));
+ const t = Math.min(1, distance / 300);
+
+ const mixedColor = mixbox.lerp(color, canvasColor, t);
+
+ color = mixedColor;
+
+ startX = e.clientX;
+ startY = e.clientY;
+
+ }
+
+ }
+
+ updateInfos();
+ updateColorPreview();
+});
+
+// }}}
+
+// mouseup {{{
+
+canvasArea.addEventListener('mouseup', (e) => {
+ isMouseDown = false;
+ if (tool === 'brush') {
+ ctx.closePath();
+ } else if (tool === 'resize') {
+ canvasWidth = canvas.width;
+ canvasHeight = canvas.height;
+ }
+
+ updateColorPreview();
+});
+
+// }}}
+
+// mouseleave {{{
+
+canvasArea.addEventListener('mouseleave', (e) => {
+ isMouseDown = false;
+ brushPreview.style.display = 'none';
+});
+
+// }}}
+
+// keybindings {{{
+
+const toolKeyBindings = {}
+
+const functionKeyBindings = {
+}
+
+document.addEventListener('keydown', (e) => {
+ if (keyDown) return;
+
+ const newTool = toolKeyBindings[e.key.toLowerCase()]
+ if (newTool) {
+ prevTool = tool;
+ keyDown = true;
+ changeTool(newTool);
+ return;
+ }
+
+ const func = functionKeyBindings[e.key];
+ if (func) {
+ func();
+ return;
+ }
+
+});
+
+document.addEventListener('keyup', (e) => {
+ const currentTool = toolKeyBindings[e.key.toLowerCase()]
+ if (currentTool) {
+ keyDown = false;
+ if (e.key == e.key.toLowerCase()) {
+ changeTool(prevTool);
+ return;
+ }
+ }
+});
+
+// }}}
+
+// tools {{{
+
+var toolButtons = [];
+
+function changeTool(toolName) {
+ toolButtons.forEach(button => button.button.classList.remove('active'));
+ toolButtons.find(button => button.name === toolName).button.classList.add('active');
+ tool = toolName;
+ brushPreview.style.display = 'none';
+ updateInfos();
+}
+
+function createToolButton(toolName, displayName, icon, key=undefined) {
+ const button = document.createElement('div');
+ button.classList.add('button');
+ button.classList.add('tool');
+ button.innerHTML = icon;
+ button.title = displayName;
+ button.addEventListener('click', () => {
+ changeTool(toolName);
+ });
+
+ if (key) {
+ const keyHint = document.createElement('span');
+ keyHint.className = 'key-hint';
+ keyHint.innerHTML = key;
+ button.appendChild(keyHint);
+ toolKeyBindings[key] = toolName;
+ }
+
+ toolBar.appendChild(button);
+ toolButtons.push({'name': toolName, 'button': button});
+ return button;
+}
+
+createToolButton('brush', 'Brush', '
', 'b');
+createToolButton('content-move', 'Move Content', '
', 'h');
+createToolButton('move', 'Move Canvas', '
', 'm');
+createToolButton('zoom', 'Zoom', '
', 'z');
+createToolButton('resize', 'Resize', '
', 'r');
+createToolButton('color-picker', 'Color Picker', '
', 'a');
+createToolButton('color-mix', 'Color Mix', '
', 'x');
+createToolButton('brush-size', 'Brush Size', '
', 'd');
+createToolButton('bucket-fill', 'Bucket Fill', '
', 'k');
+
+// }}}
+
+// menu functons {{{
+
+function flipCanvasHorizontally(e) {
+ saveState();
+ ctx.save();
+ saveCanvasContents();
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.scale(-1, 1);
+ ctx.translate(-canvas.width, 0);
+ ctx.drawImage(tempCanvas, 0, 0);
+ ctx.restore();
+}
+
+function flipCanvasVertically(e) {
+ saveState();
+ ctx.save();
+ saveCanvasContents();
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.scale(1, -1);
+ ctx.translate(0, -canvas.height);
+ ctx.drawImage(tempCanvas, 0, 0);
+ ctx.restore();
+}
+
+function exportCanvas(e) {
+ const link = document.createElement('a');
+ link.download = 'canvas.png';
+ link.href = canvas.toDataURL();
+ link.click();
+}
+
+function importCanvas(e) {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'image/*';
+ input.onchange = (e) => {
+ const file = e.target.files[0];
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const img = new Image();
+ img.onload = () => {
+ canvas.width = img.width;
+ canvas.height = img.height;
+ ctx.drawImage(img, 0, 0);
+ }
+ img.src = e.target.result;
+ }
+ reader.readAsDataURL(file);
+ }
+ input.click();
+}
+
+function clearCanvas(e) {
+ saveState();
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+}
+
+function resetZoom(e) {
+ zoom = 1;
+ canvas.style.width = canvas.width * zoom + 'px';
+ canvas.style.height = canvas.height * zoom + 'px';
+ canvasWidth = canvas.width;
+ canvasHeight = canvas.height;
+
+ canvasAreaRect = canvasArea.getBoundingClientRect();
+
+ canvasContainer.style.left = `${canvasAreaRect.left}px`;
+ canvasContainer.style.top = `${canvasAreaRect.top}px`;
+}
+
+// }}}
+
+// menu {{{
+
+var menuButtons = [];
+
+function createMenuButton(icon, name, clickFunction, key=undefined) {
+ const button = document.createElement('div');
+ button.className = 'button';
+ button.innerHTML = icon;
+ button.title = name;
+ if (clickFunction) {
+ button.addEventListener('click', () => {
+ clickFunction()
+ updateInfos();
+ });
+ }
+ menuBar.appendChild(button);
+ if (key) {
+ const keyHint = document.createElement('span');
+ keyHint.className = 'key-hint';
+ keyHint.innerHTML = key;
+ button.appendChild(keyHint);
+ functionKeyBindings[key] = clickFunction;
+ }
+ return button;
+}
+
+menuButtons.push(createMenuButton('
', 'Save', saveCanvas, 's'));
+menuButtons.push(createMenuButton('
', 'Load', loadCanvas));
+menuButtons.push(createMenuButton('
', 'Clear', clearCanvas));
+menuButtons.push(createMenuButton('
', 'Export', exportCanvas));
+menuButtons.push(createMenuButton('
', 'Import', importCanvas));
+
+
+menuButtons.push(createMenuButton('
', 'Flip Horizontally', flipCanvasHorizontally, 'f'));
+menuButtons.push(createMenuButton('
', 'Flip Vertically', flipCanvasVertically, 'v'));
+menuButtons.push(createMenuButton('
', 'Undo', undo, 'u'));
+menuButtons.push(createMenuButton('
', 'Redo', redo, 'y'));
+menuButtons.push(createMenuButton('
', 'Clear', clearCanvas, 'c'));
+menuButtons.push(createMenuButton('
', 'Reset', resetZoom, 't'));
+menuButtons.push(createMenuButton('
', 'Add Color', createPuck));
+
+// }}}
+
+// pucks {{{
+
+function createPuck(c, editable=true, key=undefined) {
+ if (c === undefined) {
+ c = color;
+ }
+
+ const puck = document.createElement('div');
+ puck.className = 'puck';
+ puck.style.backgroundColor = c;
+
+ // const selectHandle = document.createElement('div');
+ // selectHandle.className = 'select-handle';
+ // selectHandle.innerHTML = '
';
+ // puck.appendChild(selectHandle);
+
+ // selectHandle.addEventListener('click', () => {
+ // color = puck.style.backgroundColor;
+ // updateColorPreview();
+ // updateInfos();
+ // });
+
+ if (editable) {
+ // const updateHandle = document.createElement('div');
+ // updateHandle.className = 'update-handle';
+ // updateHandle.innerHTML = '
';
+ // puck.appendChild(updateHandle);
+
+ // updateHandle.addEventListener('click', () => {
+ // puck.style.backgroundColor = color;
+ // });
+
+ const deleteHandle = document.createElement('div');
+ deleteHandle.className = 'delete-handle';
+ deleteHandle.innerHTML = '
';
+ puck.appendChild(deleteHandle);
+
+ deleteHandle.addEventListener('click', () => {
+ console.log("test");
+ puck.remove();
+ });
+ }
+
+ if (key) {
+ const keyHint = document.createElement('div');
+ keyHint.className = 'key-hint';
+ keyHint.innerHTML = key;
+ puck.appendChild(keyHint);
+ }
+
+ function mixx(startTime) {
+ var interval = setInterval(() => {
+ const elapsedTime = Date.now() - startTime;
+ const t = Math.min(1, elapsedTime / 10000);
+ const mixedColor = mixbox.lerp(color, puck.style.backgroundColor, t);
+ color = mixedColor;
+ updateColorPreview();
+ updateInfos();
+ }, 50);
+ return interval;
+ }
+
+ puck.addEventListener('mousedown', (e) => {
+ const startTime = Date.now();
+ var interval = mixx(startTime);
+ function onMouseUp() {
+ clearInterval(interval);
+ document.removeEventListener('mouseup', onMouseUp);
+ }
+ document.addEventListener('mouseup', onMouseUp);
+ });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.key == key) {
+ console.log(e.key);
+ const startTime = Date.now();
+ var interval = mixx(startTime);
+ function onKeyUp() {
+ clearInterval(interval);
+ document.removeEventListener('keyup', onKeyUp);
+ }
+ document.addEventListener('keyup', onKeyUp);
+ }
+ });
+
+ menuBar.appendChild(puck);
+}
+
+createPuck(c='rgb(0, 0, 0)', editable=false, key='1');
+createPuck(c='rgb(255, 0, 0)', editale=false, key='2');
+createPuck(c='rgb(0, 0, 255)', editale=false, key='3');
+createPuck(c='rgb(255, 255, 0)', editale=false, key='4');
+createPuck(c='rgb(99, 60, 22)', editale=false, key='5');
+createPuck(c='rgb(0, 255, 0)', editale=false, key='6');
+createPuck(c='rgb(255, 0, 255)', editale=false, key='7');
+createPuck(c='rgb(0, 255, 255)', editale=false, key='8');
+createPuck(c='rgb(255, 255, 255)', editale=false, key='9');
+
+
+// }}}
+
+// info {{{
+
+var infos = [];
+
+function createInfo(name, updateFunction) {
+ const info = document.createElement('span');
+ info.className = 'info';
+ const key = document.createElement('span');
+ key.className = 'key';
+ key.innerHTML = name + ':';
+ const value = document.createElement('span');
+ value.className = 'value';
+ value.innerHTML = '0';
+ info.appendChild(key);
+ info.appendChild(value);
+ infoBar.appendChild(info);
+ function update() {
+ let v = updateFunction();
+ if (v === undefined) v = '?';
+ value.innerHTML = v;
+ }
+ return update;
+}
+
+infos.push(createInfo('zoom', function() {
+ var percent = zoom * 100;
+ return percent.toFixed(0) + '%';
+}));
+infos.push(createInfo('brush', function() { return brushSize; }));
+infos.push(createInfo('x', function() { return canvasEndX; }));
+infos.push(createInfo('y', function() { return canvasEndY; }));
+infos.push(createInfo('color', function() { return color; }));
+infos.push(createInfo('width', function() { return canvas.width; }));
+infos.push(createInfo('height', function() { return canvas.height; }));
+
+function updateInfos() {
+ infos.forEach(info => info());
+}
+
+// }}}
+
+// start {{{
+
+ctx.fillStyle = backgroundColor;
+ctx.fillRect(0, 0, canvas.width, canvas.height);
+updateInfos();
+toolButtons[0]['button'].click();
+
+// }}}