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
+}