WIP: Add support for basic drawing on images

This commit is contained in:
Jonatan Heyman 2026-01-24 17:06:42 +01:00
parent b3229b7af4
commit baf60cb55d
8 changed files with 1430 additions and 3 deletions

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 640 640" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M100.4,417.2C104.5,402.6 112.2,389.3 123,378.5L338.1,163.4L476,301.3L260.9,516.4C250.2,527.1 236.8,534.9 222.2,539L94.4,574.6C86.1,576.9 77.1,574.6 71,568.4C64.9,562.2 62.6,553.3 64.9,545L100.4,417.2ZM156,413.5C151.6,418.2 148.4,423.9 146.7,430.1L122.6,517L209.5,492.9C215.9,491.1 221.7,487.8 226.5,483.2L155.9,413.5L156,413.5ZM510,267.4C493.4,250.8 458.7,216.1 406,163.4L372,129.5C398.5,103 413.4,88.1 416.9,84.6C430.4,71 448.8,63.4 468,63.4C487.2,63.4 505.6,71 519.1,84.6L554.8,120.3C568.4,133.9 576,152.3 576,171.4C576,190.5 568.4,209 554.8,222.5C551.3,226 536.4,240.9 509.9,267.4L510,267.4Z" style="fill:white;fill-rule:nonzero;"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

742
package-lock.json generated
View File

@ -56,6 +56,7 @@
"electron-builder": "^26.4.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.6.2",
"fabric": "^7.1.0",
"fs-jetpack": "^5.1.0",
"heynote-lang-mathjs": "^1.0.1",
"lezer-elixir": "^1.1.2",
@ -74,6 +75,29 @@
"vuedraggable": "^4.1.0"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
@ -419,6 +443,126 @@
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"optional": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@ -3813,6 +3957,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/canvas": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz",
"integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.3"
},
"engines": {
"node": "^18.12.0 || >= 20.9.0"
}
},
"node_modules/canvas/node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
@ -4185,12 +4353,42 @@
"node": ">= 8"
}
},
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@ -4238,6 +4436,14 @@
}
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@ -4277,6 +4483,17 @@
"node": ">=6"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@ -5069,6 +5286,17 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"dev": true,
"license": "(MIT OR WTFPL)",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@ -5118,6 +5346,20 @@
"license": "MIT",
"optional": true
},
"node_modules/fabric": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fabric/-/fabric-7.1.0.tgz",
"integrity": "sha512-061QsoSw6xn7UoRXYq816qMyvObP4gRNVph0jAFWtG5E2kBlfdjrYBiLPRuaAHSmVQUz9RjbPpePB/hljiYJIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.0.0"
},
"optionalDependencies": {
"canvas": "^3.2.0",
"jsdom": "^26.1.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -5312,6 +5554,14 @@
"node": ">= 0.6"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@ -5476,6 +5726,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -5687,6 +5945,20 @@
"node": ">=10"
}
},
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -5823,6 +6095,14 @@
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
@ -5939,6 +6219,14 @@
"node": ">=8"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@ -6049,6 +6337,47 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsdom": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.5.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.1.1",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.1.1",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@ -6581,6 +6910,14 @@
"node": ">=10"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@ -6622,6 +6959,14 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@ -6780,6 +7125,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nwsapi": {
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@ -6965,6 +7318,34 @@
"url": "https://github.com/sponsors/dword-design"
}
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/patch-package": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
@ -7304,6 +7685,48 @@
"node": "^12.20.0 || >=14"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prebuild-install/node_modules/node-abi": {
"version": "3.87.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
@ -7411,6 +7834,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"optional": true,
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/read-binary-file-arch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
@ -7655,6 +8095,14 @@
"rollup": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
}
},
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@ -8134,6 +8582,20 @@
"dev": true,
"license": "ISC"
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@ -8240,6 +8702,55 @@
"dev": true,
"license": "ISC"
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"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"
}
],
"license": "MIT",
"optional": true
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"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"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@ -8536,6 +9047,17 @@
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
@ -8582,6 +9104,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/sync-child-process": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
@ -8624,6 +9154,46 @@
"node": ">=10"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-fs/node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar/node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@ -8853,6 +9423,28 @@
"node": ">=14.0.0"
}
},
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tldts-core": "^6.1.86"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
@ -8886,6 +9478,34 @@
"node": ">=8.0"
}
},
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"tldts": "^6.1.32"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@ -8903,6 +9523,20 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
@ -10357,6 +10991,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@ -10367,6 +11015,58 @@
"defaults": "^1.0.3"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -10444,6 +11144,40 @@
"dev": true,
"license": "ISC"
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=18"
}
},
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@ -10454,6 +11188,14 @@
"node": ">=8.0"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -70,6 +70,7 @@
"electron-builder": "^26.4.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.6.2",
"fabric": "^7.1.0",
"fs-jetpack": "^5.1.0",
"heynote-lang-mathjs": "^1.0.1",
"lezer-elixir": "^1.1.2",

View File

@ -18,6 +18,7 @@
import NewBuffer from './NewBuffer.vue'
import EditBuffer from './EditBuffer.vue'
import TabBar from './tabs/TabBar.vue'
import DrawImageModal from './draw/DrawImageModal.vue'
export default {
components: {
@ -30,6 +31,7 @@
NewBuffer,
EditBuffer,
TabBar,
DrawImageModal,
},
data() {
@ -91,6 +93,7 @@
showEditBuffer(value) { this.dialogWatcher(value) },
showMoveToBufferSelector(value) { this.dialogWatcher(value) },
showCommandPalette(value) { this.dialogWatcher(value) },
showDrawImageModal(value) { this.dialogWatcher(value) },
currentBufferPath() {
this.focusEditor()
@ -112,6 +115,9 @@
"showEditBuffer",
"showMoveToBufferSelector",
"showCommandPalette",
"showDrawImageModal",
"drawImageUrl",
"drawImageId",
"isFullscreen",
]),
...mapState(useSettingsStore, [
@ -119,7 +125,7 @@
]),
dialogVisible() {
return this.showLanguageSelector || this.showBufferSelector || this.showCreateBuffer || this.showEditBuffer || this.showMoveToBufferSelector || this.showCommandPalette || this.showSettings
return this.showLanguageSelector || this.showBufferSelector || this.showCreateBuffer || this.showEditBuffer || this.showMoveToBufferSelector || this.showCommandPalette || this.showDrawImageModal || this.showSettings
},
editorInert() {
@ -145,6 +151,7 @@
"closeBufferSelector",
"openBuffer",
"closeMoveToBufferSelector",
"closeDrawImageModal",
"deleteBuffer",
"focusEditor",
]),
@ -194,6 +201,10 @@
this.editorCacheStore.moveCurrentBlockToOtherEditor(path)
this.closeMoveToBufferSelector()
},
onSaveDrawImage(imageDataUrl) {
this.closeDrawImageModal()
},
},
}
@ -262,6 +273,13 @@
v-if="showEditBuffer"
@close="closeDialog"
/>
<DrawImageModal
v-if="showDrawImageModal"
:imageUrl="drawImageUrl"
:imageId="drawImageId"
@close="closeDrawImageModal"
@save="onSaveDrawImage"
/>
<ErrorMessages />
</div>
</div>

View File

@ -0,0 +1,623 @@
<script>
import { Canvas, PencilBrush, FabricImage } from "fabric"
export default {
props: {
imageUrl: {
type: String,
required: true,
},
imageId: {
type: [String, Number],
required: true,
},
},
emits: ["save", "close"],
data() {
return {
canvas: null,
isLoading: true,
loadError: null,
zoom: 1,
baseScale: 1,
imageWidth: 1,
imageHeight: 1,
scaledWidth: 1,
scaledHeight: 1,
stagePaddingX: 0,
stagePaddingY: 0,
stageOverflow: "hidden",
history: [],
historyIndex: -1,
isRestoring: false,
isDrawing: false,
brushColor: "#f42525",
brushWidth: 3,
}
},
async mounted() {
window.addEventListener("keydown", this.onKeyDown)
window.addEventListener("resize", this.onResize)
this.$nextTick(() => {
const stage = this.$refs.stage
stage?.addEventListener("wheel", this.onWheel, { passive: false })
})
await this.initCanvas()
},
beforeUnmount() {
window.removeEventListener("keydown", this.onKeyDown)
window.removeEventListener("resize", this.onResize)
const stage = this.$refs.stage
stage?.removeEventListener("wheel", this.onWheel)
this.disposeCanvas()
},
watch: {
imageUrl() {
this.initCanvas()
},
},
methods: {
async initCanvas() {
this.disposeCanvas()
this.isLoading = true
this.loadError = null
const canvasEl = this.$refs.canvas
if (!canvasEl) {
return
}
this.canvas = new Canvas(canvasEl, {
selection: false,
})
this.canvas.isDrawingMode = true
this.canvas.defaultCursor = "crosshair"
this.canvas.on("path:created", this.onPathCreated)
this.canvas.on("mouse:down", this.onMouseDown)
this.canvas.on("mouse:up", this.onMouseUp)
const brush = new PencilBrush(this.canvas)
brush.color = this.brushColor
brush.width = this.brushWidth
this.canvas.freeDrawingBrush = brush
await this.loadImage()
},
async loadImage() {
if (!this.canvas) {
return
}
try {
const image = await FabricImage.fromURL(this.imageUrl)
image.set({
left: 0,
top: 0,
originX: "left",
originY: "top",
selectable: false,
evented: false,
})
const width = image.width || 1
const height = image.height || 1
this.canvas.clear()
this.imageWidth = width
this.imageHeight = height
this.zoom = 1
this.updateCanvasScale()
this.canvas.add(image)
this.canvas.sendObjectToBack(image)
this.canvas.requestRenderAll()
this.resetHistory()
this.captureHistory(true)
// calculate brush width
this.brushWidth = Math.min(10, Math.max(3, width / 300))
this.canvas.freeDrawingBrush.width = this.brushWidth
//console.log("brush width:", this.brushWidth)
this.isLoading = false
} catch (error) {
console.error("Failed to load draw image", error)
this.isLoading = false
this.loadError = error
}
},
onKeyDown(event) {
if (event.key === "Escape") {
this.$emit("close")
return
}
if (this.isDrawing) {
return
}
const target = event.target
const tagName = target?.tagName?.toLowerCase()
if (tagName === "input" || tagName === "textarea" || target?.isContentEditable) {
return
}
const isMod = event.metaKey || event.ctrlKey
if (!isMod) {
return
}
if (event.key === "z" && !event.shiftKey) {
event.preventDefault()
this.undo()
return
}
if (event.key === "z" && event.shiftKey) {
event.preventDefault()
this.redo()
return
}
if (event.key === "y") {
event.preventDefault()
this.redo()
}
},
onResize() {
this.updateCanvasScale()
},
onMouseDown() {
if (this.canvas?.isDrawingMode) {
this.isDrawing = true
}
},
onMouseUp() {
this.isDrawing = false
},
onPathCreated() {
if (this.isRestoring) {
return
}
this.isDrawing = false
this.captureHistory()
},
onWheel(event) {
if (!event.altKey || this.isLoading) {
return
}
event.preventDefault()
const direction = event.deltaY < 0 ? 1 : -1
const nextZoom = this.zoom * (direction > 0 ? 1.03 : 0.97)
const stage = this.$refs.stage
if (stage) {
const rect = stage.getBoundingClientRect()
const anchor = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
}
this.setZoom(nextZoom, anchor)
return
}
this.setZoom(nextZoom)
},
saveImage() {
if (!this.canvas || this.isLoading) {
return
}
const dataUrl = this.canvas.toDataURL({
format: "png",
})
this.$emit("save", dataUrl)
this.$emit("close")
},
resetHistory() {
this.history = []
this.historyIndex = -1
},
captureHistory(force = false) {
if (!this.canvas) {
return
}
if (!force && this.historyIndex >= 0 && this.isRestoring) {
return
}
const snapshot = this.canvas.toJSON()
if (this.historyIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.historyIndex + 1)
}
this.history.push(snapshot)
this.historyIndex = this.history.length - 1
},
async restoreHistory(index) {
if (!this.canvas || index < 0 || index >= this.history.length) {
return
}
this.isRestoring = true
const snapshot = this.history[index]
await new Promise((resolve) => {
this.canvas.loadFromJSON(snapshot, () => {
resolve()
this.canvas.requestRenderAll()
})
})
this.canvas.selection = false
this.canvas.isDrawingMode = true
this.canvas.defaultCursor = "crosshair"
if (this.canvas.freeDrawingBrush) {
this.canvas.freeDrawingBrush.color = this.brushColor
this.canvas.freeDrawingBrush.width = this.brushWidth
}
this.canvas.requestRenderAll()
this.isRestoring = false
this.historyIndex = index
},
undo() {
if (this.historyIndex <= 0) {
return
}
this.restoreHistory(this.historyIndex - 1)
},
redo() {
if (this.historyIndex >= this.history.length - 1) {
return
}
this.restoreHistory(this.historyIndex + 1)
},
updateCanvasScale(anchor) {
if (!this.canvas) {
return
}
const stage = this.$refs.stage
const stageWidth = stage?.clientWidth || this.imageWidth
const stageHeight = stage?.clientHeight || this.imageHeight
const prevScrollLeft = stage?.scrollLeft || 0
const prevScrollTop = stage?.scrollTop || 0
const prevScaledWidth = this.scaledWidth || stageWidth
const prevScaledHeight = this.scaledHeight || stageHeight
const prevPaddingX = this.stagePaddingX
const prevPaddingY = this.stagePaddingY
const prevAnchor = anchor || {
x: stageWidth / 2,
y: stageHeight / 2,
}
const prevViewportAnchor = {
x: prevScrollLeft + prevAnchor.x - prevPaddingX,
y: prevScrollTop + prevAnchor.y - prevPaddingY,
}
const dpr = window.devicePixelRatio || 1
const logicalWidth = this.imageWidth / dpr
const logicalHeight = this.imageHeight / dpr
this.baseScale = Math.min(stageWidth / logicalWidth, stageHeight / logicalHeight, 1)
const scale = this.baseScale * this.zoom
const scaledWidth = Math.round(logicalWidth * scale)
const scaledHeight = Math.round(logicalHeight * scale)
this.canvas.setDimensions({ width: this.imageWidth, height: this.imageHeight })
this.canvas.setDimensions(
{ width: `${scaledWidth}px`, height: `${scaledHeight}px` },
{ cssOnly: true },
)
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0])
const nextOverflow = scaledWidth > stageWidth || scaledHeight > stageHeight
this.stageOverflow = nextOverflow ? "auto" : "hidden"
this.scaledWidth = scaledWidth
this.scaledHeight = scaledHeight
this.stagePaddingX = scaledWidth < stageWidth ? Math.floor((stageWidth - scaledWidth) / 2) : 0
this.stagePaddingY = scaledHeight < stageHeight ? Math.floor((stageHeight - scaledHeight) / 2) : 0
if (stage) {
const anchorRatioX = prevViewportAnchor.x / Math.max(prevScaledWidth, 1)
const anchorRatioY = prevViewportAnchor.y / Math.max(prevScaledHeight, 1)
const nextAnchor = {
x: anchorRatioX * scaledWidth,
y: anchorRatioY * scaledHeight,
}
this.$nextTick(() => {
const maxScrollLeft = Math.max(0, scaledWidth + this.stagePaddingX * 2 - stageWidth)
const maxScrollTop = Math.max(0, scaledHeight + this.stagePaddingY * 2 - stageHeight)
stage.scrollLeft = Math.min(
maxScrollLeft,
Math.max(0, nextAnchor.x + this.stagePaddingX - prevAnchor.x),
)
stage.scrollTop = Math.min(
maxScrollTop,
Math.max(0, nextAnchor.y + this.stagePaddingY - prevAnchor.y),
)
})
}
},
zoomIn() {
if (this.isLoading) {
return
}
this.setZoom(this.zoom + 0.2)
},
zoomOut() {
if (this.isLoading) {
return
}
this.setZoom(this.zoom - 0.2)
},
setZoom(nextZoom, anchor) {
this.zoom = Math.min(Math.max(nextZoom, 0.2), 4)
this.updateCanvasScale(anchor)
},
resetZoom() {
this.zoom = 1
this.updateCanvasScale()
},
onBrushColorChange(event) {
this.brushColor = event.target.value
if (this.canvas?.freeDrawingBrush) {
this.canvas.freeDrawingBrush.color = this.brushColor
}
},
disposeCanvas() {
if (this.canvas) {
this.canvas.off("path:created", this.onPathCreated)
this.canvas.off("mouse:down", this.onMouseDown)
this.canvas.off("mouse:up", this.onMouseUp)
this.canvas.dispose()
this.canvas = null
}
},
},
}
</script>
<template>
<div class="draw-modal">
<div class="dialog">
<div class="header">
<div class="header-tools-left">
<label class="color-picker">
<input
type="color"
:value="brushColor"
:disabled="isLoading"
@input="onBrushColorChange"
/>
</label>
</div>
<div class="header-tools">
<div class="history-controls">
<button class="history" @click="undo" :disabled="historyIndex <= 0 || isLoading">Undo</button>
<button class="history" @click="redo" :disabled="historyIndex >= history.length - 1 || isLoading">Redo</button>
</div>
<div class="zoom-controls">
<button class="zoom" @click="zoomOut" :disabled="isLoading">-</button>
<div class="zoom-value">{{ Math.round(baseScale * zoom * 100) }}%</div>
<button class="zoom" @click="zoomIn" :disabled="isLoading">+</button>
<button class="zoom reset" @click="resetZoom" :disabled="isLoading">Fit</button>
</div>
</div>
</div>
<div class="dialog-content">
<div
class="canvas-stage"
ref="stage"
:class="{ scrollable: stageOverflow === 'auto' }"
:style="{ padding: `${stagePaddingY}px ${stagePaddingX}px` }"
>
<canvas ref="canvas"></canvas>
</div>
<div v-if="loadError" class="error">Failed to load image.</div>
</div>
<div class="bottom-bar">
<button @click="$emit('close')" class="close">Cancel</button>
<button @click="saveImage" class="save" :disabled="isLoading">Save</button>
</div>
</div>
<div class="shader"></div>
</div>
</template>
<style lang="sass" scoped>
.draw-modal
z-index: 500
position: fixed
top: 0
left: 0
bottom: 0
right: 0
.shader
z-index: 1
position: absolute
top: 0
left: 0
bottom: 0
right: 0
background: rgba(0, 0, 0, 0.5)
.dialog
--dialog-height: 680px
--bottom-bar-height: 56px
box-sizing: border-box
z-index: 2
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
width: 920px
height: var(--dialog-height)
max-width: 100%
max-height: 100%
display: flex
flex-direction: column
border-radius: 6px
background: #fff
color: #333
box-shadow: 0 0 25px rgba(0, 0, 0, 0.2)
overflow: hidden
+dark-mode
background: #2f2f2f
color: #eee
box-shadow: 0 0 25px rgba(0, 0, 0, 0.35)
.header
display: flex
align-items: center
justify-content: space-between
padding: 10px 18px
border-bottom: 1px solid #eee
+dark-mode
border-bottom: 1px solid #1e1e1e
.header-tools-left
display: flex
align-items: center
gap: 10px
.color-picker input[type="color"]
width: 24px
height: 24px
padding: 0 2px
background-color: #3e3e3e
border: none
border-radius: 3px
.header-tools
display: flex
align-items: center
gap: 16px
.history-controls
display: flex
align-items: center
gap: 6px
.history
height: 28px
padding: 0 10px
border-radius: 4px
border: 1px solid #c7c7c7
background: #fff
cursor: pointer
font-size: 12px
+dark-mode
border-color: #444
background: #1f1f1f
color: #eee
&:disabled
opacity: 0.6
cursor: not-allowed
.zoom-controls
display: flex
align-items: center
gap: 6px
font-size: 12px
.zoom
width: 28px
height: 28px
border-radius: 4px
border: 1px solid #c7c7c7
background: #fff
cursor: pointer
+dark-mode
border-color: #444
background: #1f1f1f
color: #eee
&:disabled
opacity: 0.6
cursor: not-allowed
.reset
width: auto
padding: 0 10px
.zoom-value
min-width: 48px
text-align: center
.dialog-content
flex-grow: 1
//padding: 18px 22px
overflow: hidden
display: flex
flex-direction: column
gap: 12px
.canvas-stage
flex-grow: 1
//border: 1px solid #e5e5e5
//border-radius: 6px
background: #f7f7f7
overflow: hidden
display: flex
align-items: center
justify-content: center
&.scrollable
overflow: auto
align-items: flex-start
justify-content: flex-start
+dark-mode
//border-color: #1f1f1f
background: #222
canvas
display: block
.error
font-size: 12px
color: #c04343
.bottom-bar
box-sizing: border-box
height: var(--bottom-bar-height)
background: #f0f0f0
text-align: right
padding: 12px 22px
display: flex
justify-content: flex-end
gap: 10px
border-top: 1px solid #eee
+dark-mode
background: #2f2f2f
border-top: 1px solid #1e1e1e
button
height: 32px
padding: 0 14px
border-radius: 4px
border: 1px solid transparent
cursor: pointer
.close
background: transparent
border-color: #c7c7c7
color: inherit
+dark-mode
border-color: #444
.save
background: #1d7cf2
color: #fff
border-color: #1d7cf2
&:disabled
opacity: 0.6
cursor: not-allowed
</style>

View File

@ -62,6 +62,7 @@
overflow: hidden
container-type: inline-size
button
height: 24px
font-size: 12px
background-color: #646e71
color: #fff
@ -80,11 +81,18 @@
min-width: 0
white-space: nowrap
overflow: hidden
margin-right: 4px
&:hover
background-color: #51595c
opacity: 1.0
@container (max-width: 50px)
@container (max-width: 100px)
padding: 3px 12px
span
display: none
@container (max-width: 48px)
display: none
&.draw
background-image: url("@/assets/icons/pencil-white.svg")

View File

@ -3,6 +3,7 @@ import { WidgetType } from "@codemirror/view"
import { copyImage } from "../clipboard/copy-paste.js"
import { setImageDisplayDimensions } from "./image-parsing.js"
import { useHeynoteStore } from "../../stores/heynote-store.js"
const FOLDED_HEIGHT = 16
@ -63,6 +64,7 @@ export class ImageWidget extends WidgetType {
toDOM(view) {
//console.log("toDOM", this.selected, this.height)
const heynoteStore = useHeynoteStore()
let wrap = document.createElement("div")
wrap.dataset.id = this.id
wrap.dataset.idealWidth = this.idealWidth
@ -91,7 +93,7 @@ export class ImageWidget extends WidgetType {
buttonsContainer.className = "buttons-container"
inner.appendChild(buttonsContainer)
const copyButton = document.createElement("button")
copyButton.innerHTML = "Copy"
copyButton.innerHTML = "<span>Copy</span>"
buttonsContainer.appendChild(copyButton)
copyButton.addEventListener("mousedown", (event) => {
event.preventDefault()
@ -104,6 +106,17 @@ export class ImageWidget extends WidgetType {
copyButton.innerText = "Copy"
}, 2000)
})
const drawButton = document.createElement("button")
drawButton.className = "draw"
drawButton.innerHTML = "<span>Draw</span>"
buttonsContainer.appendChild(drawButton)
drawButton.addEventListener("mousedown", (event) => {
event.preventDefault()
})
drawButton.addEventListener("click", (event) => {
event.preventDefault()
heynoteStore.openDrawImageModal(this.path, this.id)
})
let img = document.createElement("img")

View File

@ -35,6 +35,9 @@ export const useHeynoteStore = defineStore("heynote", {
showEditBuffer: false,
showMoveToBufferSelector: false,
showCommandPalette: false,
showDrawImageModal: false,
drawImageUrl: null,
drawImageId: null,
isFullscreen: false,
isFocused: true,
@ -198,6 +201,17 @@ export const useHeynoteStore = defineStore("heynote", {
}
this.showCreateBuffer = true
},
openDrawImageModal(imageUrl, imageId) {
this.closeDialog()
this.drawImageUrl = imageUrl
this.drawImageId = imageId
this.showDrawImageModal = true
},
closeDrawImageModal() {
this.showDrawImageModal = false
this.drawImageUrl = null
this.drawImageId = null
},
closeDialog() {
this.showCreateBuffer = false
this.showBufferSelector = false
@ -205,6 +219,9 @@ export const useHeynoteStore = defineStore("heynote", {
this.showEditBuffer = false
this.showMoveToBufferSelector = false
this.showCommandPalette = false
this.showDrawImageModal = false
this.drawImageUrl = null
this.drawImageId = null
},
closeBufferSelector() {