diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..07d66b8 --- /dev/null +++ b/demo.html @@ -0,0 +1,217 @@ + + + + Rough Notation Demo + + + +
+ + +
+ +
+ This demo shows both batched resize handling and SVG translation + optimization.
+ - Resize Elements: Size changes → Full re-render (paths + recreated)
+
+ +
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+
Click me to toggle the annotation.
+ + + + + diff --git a/package-lock.json b/package-lock.json index 327d3b2..05583ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,458 +1,563 @@ { "name": "rough-notation", "version": "0.5.1", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@babel/code-frame": { + "packages": { + "": { + "name": "rough-notation", + "version": "0.5.1", + "license": "MIT", + "devDependencies": { + "rollup": "^2.32.1", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-terser": "^6.1.0", + "roughjs": "^4.3.1", + "tslint": "^6.1.3", + "typescript": "^4.0.5" + } + }, + "node_modules/@babel/code-frame": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", "dev": true, - "requires": { + "dependencies": { "@babel/highlight": "^7.8.3" } }, - "@babel/helper-validator-identifier": { + "node_modules/@babel/helper-validator-identifier": { "version": "7.9.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", "dev": true }, - "@babel/highlight": { + "node_modules/@babel/highlight": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, - "@types/node": { + "node_modules/@types/node": { "version": "14.0.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.5.tgz", "integrity": "sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA==", "dev": true }, - "@types/resolve": { + "node_modules/@types/resolve": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", "dev": true, - "requires": { + "dependencies": { "@types/node": "*" } }, - "ansi-styles": { + "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "requires": { + "dependencies": { "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, - "argparse": { + "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "requires": { + "dependencies": { "sprintf-js": "~1.0.2" } }, - "balanced-match": { + "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "brace-expansion": { + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "buffer-from": { + "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "builtin-modules": { + "node_modules/builtin-modules": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "chalk": { + "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "requires": { + "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "color-convert": { + "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "requires": { + "dependencies": { "color-name": "1.1.3" } }, - "color-name": { + "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, - "commander": { + "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "diff": { + "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.3.1" + } }, - "escape-string-regexp": { + "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, - "esprima": { + "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } }, - "estree-walker": { + "node_modules/estree-walker": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", "dev": true }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { + "node_modules/fsevents": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", "dev": true, - "optional": true + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "glob": { + "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "requires": { + "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "has-flag": { + "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "dev": true, + "engines": { + "node": ">=4" + } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "requires": { + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "is-module": { + "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", "dev": true }, - "jest-worker": { + "node_modules/jest-worker": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz", "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==", "dev": true, - "requires": { + "dependencies": { "merge-stream": "^2.0.0", "supports-color": "^7.0.0" }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-worker/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/jest-worker/node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, "dependencies": { - "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 - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "js-tokens": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, - "js-yaml": { + "node_modules/js-yaml": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, - "requires": { + "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "merge-stream": { + "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "minimatch": { + "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "requires": { + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "minimist": { + "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "mkdirp": { + "node_modules/mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, - "requires": { + "dependencies": { "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "requires": { + "dependencies": { "wrappy": "1" } }, - "path-data-parser": { + "node_modules/path-data-parser": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", "dev": true }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "path-parse": { + "node_modules/path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, - "points-on-curve": { + "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", "dev": true }, - "points-on-path": { + "node_modules/points-on-path": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", "dev": true, - "requires": { + "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, - "resolve": { + "node_modules/resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "dev": true, - "requires": { + "dependencies": { "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "rollup": { + "node_modules/rollup": { "version": "2.32.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.1.tgz", "integrity": "sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==", "dev": true, - "requires": { + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { "fsevents": "~2.1.2" } }, - "rollup-plugin-node-resolve": { + "node_modules/rollup-plugin-node-resolve": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.", "dev": true, - "requires": { + "dependencies": { "@types/resolve": "0.0.8", "builtin-modules": "^3.1.0", "is-module": "^1.0.0", "resolve": "^1.11.1", "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.11.0" } }, - "rollup-plugin-terser": { + "node_modules/rollup-plugin-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-6.1.0.tgz", "integrity": "sha512-4fB3M9nuoWxrwm39habpd4hvrbrde2W2GG4zEGPQg1YITNkM3Tqur5jSuXlWNzbv/2aMLJ+dZJaySc3GCD8oDw==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", "dev": true, - "requires": { + "dependencies": { "@babel/code-frame": "^7.8.3", "jest-worker": "^26.0.0", "serialize-javascript": "^3.0.0", "terser": "^4.7.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" } }, - "rollup-pluginutils": { + "node_modules/rollup-pluginutils": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", "dev": true, - "requires": { + "dependencies": { "estree-walker": "^0.6.1" } }, - "roughjs": { + "node_modules/roughjs": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.3.1.tgz", "integrity": "sha512-m42+OBaBR7x5UhIKyjBCnWqqkaEkBKLkXvHv4pOWJXPofvMnQY4ZcFEQlqf3coKKyZN2lfWMyx7QXSg2GD7SGA==", "dev": true, - "requires": { + "dependencies": { "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, - "semver": { + "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "dev": true, + "bin": { + "semver": "bin/semver" + } }, - "serialize-javascript": { + "node_modules/serialize-javascript": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", "dev": true }, - "source-map": { + "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "source-map-support": { + "node_modules/source-map-support": { "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, - "requires": { + "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "sprintf-js": { + "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "supports-color": { + "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "requires": { + "dependencies": { "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "terser": { + "node_modules/terser": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", "dev": true, - "requires": { + "dependencies": { "commander": "^2.20.0", "source-map": "~0.6.1", "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" } }, - "tslib": { + "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "tslint": { + "node_modules/tslint": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", "dev": true, - "requires": { + "dependencies": { "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", "chalk": "^2.3.0", @@ -467,31 +572,51 @@ "tslib": "^1.13.0", "tsutils": "^2.29.0" }, - "dependencies": { - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - } + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" + } + }, + "node_modules/tslint/node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "tsutils": { + "node_modules/tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, - "requires": { + "dependencies": { "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" } }, - "typescript": { + "node_modules/typescript": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", - "dev": true + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", diff --git a/src/keyframes.ts b/src/keyframes.ts index 805b187..2a82fad 100644 --- a/src/keyframes.ts +++ b/src/keyframes.ts @@ -1,7 +1,11 @@ export function ensureKeyframes() { if (!(window as any).__rno_kf_s) { - const style = (window as any).__rno_kf_s = document.createElement('style'); - style.textContent = `@keyframes rough-notation-dash { to { stroke-dashoffset: 0; } }`; + const style = ((window as any).__rno_kf_s = + document.createElement("style")); + style.textContent = ` + @keyframes rough-notation-dash { to { stroke-dashoffset: 0; } } + @keyframes rough-notation-dash-reverse { to { stroke-dashoffset: var(--path-length); } } + `; document.head.appendChild(style); } -} \ No newline at end of file +} diff --git a/src/model.ts b/src/model.ts index 0783e08..ba731aa 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,4 +1,4 @@ -export const SVG_NS = 'http://www.w3.org/2000/svg'; +export const SVG_NS = "http://www.w3.org/2000/svg"; export const DEFAULT_ANIMATION_DURATION = 800; @@ -9,10 +9,17 @@ export interface Rect { h: number; } -export type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' | 'crossed-off' | 'bracket'; +export type RoughAnnotationType = + | "underline" + | "box" + | "circle" + | "highlight" + | "strike-through" + | "crossed-off" + | "bracket"; export type FullPadding = [number, number, number, number]; export type RoughPadding = number | [number, number] | FullPadding; -export type BracketType = 'left' | 'right' | 'top' | 'bottom'; +export type BracketType = "left" | "right" | "top" | "bottom"; export interface RoughAnnotationConfig extends RoughAnnotationConfigBase { type: RoughAnnotationType; @@ -23,6 +30,7 @@ export interface RoughAnnotationConfig extends RoughAnnotationConfigBase { export interface RoughAnnotationConfigBase { animate?: boolean; // defaults to true animationDuration?: number; // defaulst to 1000ms + animateOnHide?: boolean; // defaults to false color?: string; // defaults to currentColor strokeWidth?: number; // default based on type padding?: RoughPadding; // defaults to 5px @@ -40,4 +48,4 @@ export interface RoughAnnotation extends RoughAnnotationConfigBase { export interface RoughAnnotationGroup { show(): void; hide(): void; -} \ No newline at end of file +} diff --git a/src/rough-notation.ts b/src/rough-notation.ts index 3ca0ba5..5826b7d 100644 --- a/src/rough-notation.ts +++ b/src/rough-notation.ts @@ -1,14 +1,59 @@ -import { Rect, RoughAnnotationConfig, RoughAnnotation, SVG_NS, RoughAnnotationGroup, DEFAULT_ANIMATION_DURATION } from './model.js'; -import { renderAnnotation } from './render.js'; -import { ensureKeyframes } from './keyframes.js'; -import { randomSeed } from 'roughjs/bin/math'; +import { + Rect, + RoughAnnotationConfig, + RoughAnnotation, + SVG_NS, + RoughAnnotationGroup, + DEFAULT_ANIMATION_DURATION, +} from "./model.js"; +import { renderAnnotation } from "./render.js"; +import { ensureKeyframes } from "./keyframes.js"; +import { randomSeed } from "roughjs/bin/math"; -type AnnotationState = 'unattached' | 'not-showing' | 'showing'; +type AnnotationState = "unattached" | "not-showing" | "showing"; + +// Global batching system for resize handling +const dirtyAnnotations = new Set(); +let pendingUpdate = false; + +function scheduleUpdate() { + if (!pendingUpdate) { + pendingUpdate = true; + requestAnimationFrame(() => { + // First pass: measure all dirty annotations in batch + const annotationsToUpdate: { + annotation: RoughAnnotationImpl; + newRects: Rect[]; + }[] = []; + + for (const annotation of dirtyAnnotations) { + if (annotation._state === "showing") { + const newRects = annotation.measureRects(); + if (annotation.haveRectsChanged(newRects)) { + annotationsToUpdate.push({ annotation, newRects }); + } + } + } + + // Second pass: update DOM for annotations that actually changed + for (const { annotation, newRects } of annotationsToUpdate) { + annotation.updateWithNewRects(newRects); + } + + dirtyAnnotations.clear(); + pendingUpdate = false; + }); + } +} + +function markAnnotationDirty(annotation: RoughAnnotationImpl) { + dirtyAnnotations.add(annotation); + scheduleUpdate(); +} class RoughAnnotationImpl implements RoughAnnotation { - private _state: AnnotationState = 'unattached'; + _state: AnnotationState = "unattached"; // Made public for batching system private _config: RoughAnnotationConfig; - private _resizing = false; private _ro?: any; // ResizeObserver is not supported in typescript std lib yet private _seed = randomSeed(); @@ -24,16 +69,37 @@ class RoughAnnotationImpl implements RoughAnnotation { this.attach(); } - get animate() { return this._config.animate; } - set animate(value) { this._config.animate = value; } + get animate() { + return this._config.animate; + } + set animate(value) { + this._config.animate = value; + } - get animationDuration() { return this._config.animationDuration; } - set animationDuration(value) { this._config.animationDuration = value; } + get animationDuration() { + return this._config.animationDuration; + } + set animationDuration(value) { + this._config.animationDuration = value; + } - get iterations() { return this._config.iterations; } - set iterations(value) { this._config.iterations = value; } + get animateOnHide() { + return this._config.animateOnHide; + } + set animateOnHide(value) { + this._config.animateOnHide = value; + } + + get iterations() { + return this._config.iterations; + } + set iterations(value) { + this._config.iterations = value; + } - get color() { return this._config.color; } + get color() { + return this._config.color; + } set color(value) { if (this._config.color !== value) { this._config.color = value; @@ -41,7 +107,9 @@ class RoughAnnotationImpl implements RoughAnnotation { } } - get strokeWidth() { return this._config.strokeWidth; } + get strokeWidth() { + return this._config.strokeWidth; + } set strokeWidth(value) { if (this._config.strokeWidth !== value) { this._config.strokeWidth = value; @@ -49,7 +117,9 @@ class RoughAnnotationImpl implements RoughAnnotation { } } - get padding() { return this._config.padding; } + get padding() { + return this._config.padding; + } set padding(value) { if (this._config.padding !== value) { this._config.padding = value; @@ -58,42 +128,32 @@ class RoughAnnotationImpl implements RoughAnnotation { } private _resizeListener = () => { - if (!this._resizing) { - this._resizing = true; - setTimeout(() => { - this._resizing = false; - if (this._state === 'showing') { - if (this.haveRectsChanged()) { - this.show(); - } - } - }, 400); - } - } + markAnnotationDirty(this); + }; private attach() { - if (this._state === 'unattached' && this._e.parentElement) { + if (this._state === "unattached" && this._e.parentElement) { ensureKeyframes(); - const svg = this._svg = document.createElementNS(SVG_NS, 'svg'); - svg.setAttribute('class', 'rough-annotation'); + const svg = (this._svg = document.createElementNS(SVG_NS, "svg")); + svg.setAttribute("class", "rough-annotation"); const style = svg.style; - style.position = 'absolute'; - style.top = '0'; - style.left = '0'; - style.overflow = 'visible'; - style.pointerEvents = 'none'; - style.width = '100px'; - style.height = '100px'; - const prepend = this._config.type === 'highlight'; - this._e.insertAdjacentElement(prepend ? 'beforebegin' : 'afterend', svg); - this._state = 'not-showing'; + style.position = "absolute"; + style.top = "0"; + style.left = "0"; + style.overflow = "visible"; + style.pointerEvents = "none"; + style.width = "100px"; + style.height = "100px"; + const prepend = this._config.type === "highlight"; + this._e.insertAdjacentElement(prepend ? "beforebegin" : "afterend", svg); + this._state = "not-showing"; // ensure e is positioned if (prepend) { const computedPos = window.getComputedStyle(this._e).position; - const unpositioned = (!computedPos) || (computedPos === 'static'); + const unpositioned = !computedPos || computedPos === "static"; if (unpositioned) { - this._e.style.position = 'relative'; + this._e.style.position = "relative"; } } this.attachListeners(); @@ -101,7 +161,7 @@ class RoughAnnotationImpl implements RoughAnnotation { } private detachListeners() { - window.removeEventListener('resize', this._resizeListener); + window.removeEventListener("resize", this._resizeListener); if (this._ro) { this._ro.unobserve(this._e); } @@ -109,8 +169,8 @@ class RoughAnnotationImpl implements RoughAnnotation { private attachListeners() { this.detachListeners(); - window.addEventListener('resize', this._resizeListener, { passive: true }); - if ((!this._ro) && ('ResizeObserver' in window)) { + window.addEventListener("resize", this._resizeListener, { passive: true }); + if (!this._ro && "ResizeObserver" in window) { this._ro = new (window as any).ResizeObserver((entries: any) => { for (const entry of entries) { if (entry.contentRect) { @@ -118,18 +178,31 @@ class RoughAnnotationImpl implements RoughAnnotation { } } }); - } - if (this._ro) { this._ro.observe(this._e); } } - private haveRectsChanged(): boolean { + measureRects(): Rect[] { + const ret: Rect[] = []; + if (this._svg) { + if (this._config.multiline) { + const elementRects = this._e.getClientRects(); + for (let i = 0; i < elementRects.length; i++) { + ret.push(this.svgRect(this._svg, elementRects[i])); + } + } else { + ret.push(this.svgRect(this._svg, this._e.getBoundingClientRect())); + } + } + return ret; + } + + haveRectsChanged(newRects?: Rect[]): boolean { + const rectsToCompare = newRects || this.measureRects(); if (this._lastSizes.length) { - const newRects = this.rects(); - if (newRects.length === this._lastSizes.length) { - for (let i = 0; i < newRects.length; i++) { - if (!this.isSameRect(newRects[i], this._lastSizes[i])) { + if (rectsToCompare.length === this._lastSizes.length) { + for (let i = 0; i < rectsToCompare.length; i++) { + if (!this.isSameRect(rectsToCompare[i], this._lastSizes[i])) { return true; } } @@ -140,6 +213,123 @@ class RoughAnnotationImpl implements RoughAnnotation { return false; } + private hasOnlyPositionChanged(newRects: Rect[]): boolean { + if (this._lastSizes.length !== newRects.length) { + return false; + } + + const si = (a: number, b: number) => Math.round(a) === Math.round(b); + let hasPositionChange = false; + + for (let i = 0; i < newRects.length; i++) { + const oldRect = this._lastSizes[i]; + const newRect = newRects[i]; + + // Check if size changed - if so, we can't use translation + if (!si(oldRect.w, newRect.w) || !si(oldRect.h, newRect.h)) { + return false; + } + + // Check if position changed + if (!si(oldRect.x, newRect.x) || !si(oldRect.y, newRect.y)) { + hasPositionChange = true; + } + } + + // Only return true if position changed but size didn't + return hasPositionChange; + } + + private translateSVGContent(newRects: Rect[]): void { + if (!this._svg || this._lastSizes.length === 0) return; + + if (newRects.length === 1 && this._lastSizes.length === 1) { + // Single rect case (most common) + const oldRect = this._lastSizes[0]; + const newRect = newRects[0]; + + const deltaX = newRect.x - oldRect.x; + const deltaY = newRect.y - oldRect.y; + + this.applyTranslationToAllPaths(deltaX, deltaY); + } else if (newRects.length === this._lastSizes.length) { + // Multi-rect case: calculate average translation + // This works well for multiline text where all lines move together + let totalDeltaX = 0; + let totalDeltaY = 0; + + for (let i = 0; i < newRects.length; i++) { + totalDeltaX += newRects[i].x - this._lastSizes[i].x; + totalDeltaY += newRects[i].y - this._lastSizes[i].y; + } + + const avgDeltaX = totalDeltaX / newRects.length; + const avgDeltaY = totalDeltaY / newRects.length; + + // Only apply if all deltas are reasonably consistent (same direction/magnitude) + const isConsistent = newRects.every((rect, i) => { + const deltaX = rect.x - this._lastSizes[i].x; + const deltaY = rect.y - this._lastSizes[i].y; + return ( + Math.abs(deltaX - avgDeltaX) < 2 && Math.abs(deltaY - avgDeltaY) < 2 + ); + }); + + if (isConsistent) { + this.applyTranslationToAllPaths(avgDeltaX, avgDeltaY); + } else { + // Fall back to full re-render for complex multi-rect changes + this.renderWithRects(this._svg, newRects, true); + return; + } + } + + this._lastSizes = newRects; + } + + private applyTranslationToAllPaths(deltaX: number, deltaY: number): void { + if (!this._svg) return; + + const paths = this._svg.querySelectorAll("path"); + paths.forEach((path) => { + const currentTransform = path.getAttribute("transform") || ""; + let newTransform = ""; + + if (currentTransform.includes("translate")) { + // Extract existing translate values and add deltas + const translateMatch = currentTransform.match(/translate\(([^)]+)\)/); + if (translateMatch) { + const coords = translateMatch[1].split(/[,\s]+/).map(Number); + const currentX = coords[0] || 0; + const currentY = coords[1] || 0; + + newTransform = currentTransform.replace( + /translate\([^)]+\)/, + `translate(${currentX + deltaX}, ${currentY + deltaY})` + ); + } + } else { + // Add new translate + newTransform = + `translate(${deltaX}, ${deltaY}) ${currentTransform}`.trim(); + } + + path.setAttribute("transform", newTransform); + }); + } + + updateWithNewRects(newRects: Rect[]): void { + if (this._state === "showing" && this._svg) { + // Check if we can optimize with just a translation + if (this.hasOnlyPositionChanged(newRects)) { + this.translateSVGContent(newRects); + } else { + // Full re-render needed + this.renderWithRects(this._svg, newRects, true); + } + } + } + private isSameRect(rect1: Rect, rect2: Rect): boolean { const si = (a: number, b: number) => Math.round(a) === Math.round(b); return ( @@ -151,12 +341,12 @@ class RoughAnnotationImpl implements RoughAnnotation { } isShowing(): boolean { - return (this._state !== 'not-showing'); + return this._state !== "not-showing"; } private pendingRefresh?: Promise; private refresh() { - if (this.isShowing() && (!this.pendingRefresh)) { + if (this.isShowing() && !this.pendingRefresh) { this.pendingRefresh = Promise.resolve().then(() => { if (this.isShowing()) { this.show(); @@ -168,15 +358,15 @@ class RoughAnnotationImpl implements RoughAnnotation { show(): void { switch (this._state) { - case 'unattached': + case "unattached": break; - case 'showing': - this.hide(); + case "showing": + this.hide(/* force */ true); if (this._svg) { this.render(this._svg, true); } break; - case 'not-showing': + case "not-showing": this.attach(); if (this._svg) { this.render(this._svg, false); @@ -185,13 +375,71 @@ class RoughAnnotationImpl implements RoughAnnotation { } } - hide(): void { - if (this._svg) { + /** + * @param force - If true, the annotation will be hidden immediately without animation. + */ + hide(force?: boolean): void { + if (!this.isShowing()) { + return; + } + const animate = this.animateOnHide ?? false; + if (this._svg && !force && animate) { + const paths = Array.from(this._svg.querySelectorAll("path")); + if (paths.length > 0) { + const animationDuration = + this._config.animationDuration || DEFAULT_ANIMATION_DURATION; + const animationGroupDelay = this._animationDelay; + const animations: (string | null)[] = []; + const lengths: number[] = []; + let totalLength = 0; + + console.log("paths", paths); + for (const path of paths) { + const style = path.style; + const animation = style.animation; + animations.push(animation); + style.animation = "none"; + const length = path.getTotalLength(); + lengths.push(length); + totalLength += length; + } + + requestAnimationFrame(() => { + let durationOffset = 0; + for (let i = paths.length - 1; i >= 0; i--) { + const path = paths[i]; + const animation = animations[i]; + if (animation) { + const length = lengths[i]; + const duration = totalLength + ? animationDuration * (length / totalLength) + : 0; + const delay = animationGroupDelay + durationOffset; + const style = path.style; + style.strokeDashoffset = "0"; + style.strokeDasharray = `${length}`; + style.setProperty("--path-length", `${length}`); + style.animation = `rough-notation-dash-reverse ${duration}ms ease-out ${delay}ms forwards`; + durationOffset += duration; + } + } + }); + + const totalAnimationTime = animationDuration + animationGroupDelay; + setTimeout(() => { + paths.forEach((p) => { + if (p.parentElement) { + p.parentElement.removeChild(p); + } + }); + }, totalAnimationTime); + } + } else if (this._svg) { while (this._svg.lastChild) { this._svg.removeChild(this._svg.lastChild); } } - this._state = 'not-showing'; + this._state = "not-showing"; } remove(): void { @@ -199,44 +447,50 @@ class RoughAnnotationImpl implements RoughAnnotation { this._svg.parentElement.removeChild(this._svg); } this._svg = undefined; - this._state = 'unattached'; + this._state = "unattached"; this.detachListeners(); } private render(svg: SVGSVGElement, ensureNoAnimation: boolean) { + const rects = this.measureRects(); + this.renderWithRects(svg, rects, ensureNoAnimation); + } + + private renderWithRects( + svg: SVGSVGElement, + rects: Rect[], + ensureNoAnimation: boolean + ) { + // Clear existing paths first + while (svg.lastChild) { + svg.removeChild(svg.lastChild); + } + let config = this._config; if (ensureNoAnimation) { config = JSON.parse(JSON.stringify(this._config)); config.animate = false; } - const rects = this.rects(); let totalWidth = 0; - rects.forEach((rect) => totalWidth += rect.w); - const totalDuration = (config.animationDuration || DEFAULT_ANIMATION_DURATION); + rects.forEach((rect) => (totalWidth += rect.w)); + const totalDuration = + config.animationDuration || DEFAULT_ANIMATION_DURATION; let delay = 0; for (let i = 0; i < rects.length; i++) { const rect = rects[i]; const ad = totalDuration * (rect.w / totalWidth); - renderAnnotation(svg, rects[i], config, delay + this._animationDelay, ad, this._seed); + renderAnnotation( + svg, + rects[i], + config, + delay + this._animationDelay, + ad, + this._seed + ); delay += ad; } this._lastSizes = rects; - this._state = 'showing'; - } - - private rects(): Rect[] { - const ret: Rect[] = []; - if (this._svg) { - if (this._config.multiline) { - const elementRects = this._e.getClientRects(); - for (let i = 0; i < elementRects.length; i++) { - ret.push(this.svgRect(this._svg, elementRects[i])); - } - } else { - ret.push(this.svgRect(this._svg, this._e.getBoundingClientRect())); - } - } - return ret; + this._state = "showing"; } private svgRect(svg: SVGSVGElement, bounds: DOMRect | DOMRectReadOnly): Rect { @@ -246,21 +500,29 @@ class RoughAnnotationImpl implements RoughAnnotation { x: (rect2.x || rect2.left) - (rect1.x || rect1.left), y: (rect2.y || rect2.top) - (rect1.y || rect1.top), w: rect2.width, - h: rect2.height + h: rect2.height, }; } } -export function annotate(element: HTMLElement, config: RoughAnnotationConfig): RoughAnnotation { +export function annotate( + element: HTMLElement, + config: RoughAnnotationConfig +): RoughAnnotation { return new RoughAnnotationImpl(element, config); } -export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotationGroup { +export function annotationGroup( + annotations: RoughAnnotation[] +): RoughAnnotationGroup { let delay = 0; for (const a of annotations) { const ai = a as RoughAnnotationImpl; ai._animationDelay = delay; - const duration = ai.animationDuration === 0 ? 0 : (ai.animationDuration || DEFAULT_ANIMATION_DURATION); + const duration = + ai.animationDuration === 0 + ? 0 + : ai.animationDuration || DEFAULT_ANIMATION_DURATION; delay += duration; } const list = [...annotations]; @@ -274,6 +536,6 @@ export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotation for (const a of list) { a.hide(); } - } + }, }; -} \ No newline at end of file +}