` for the content to annotate.*
49 |
50 | ## Annotation Group
51 |
52 | rough-notation provides a way to order the animation of annotations by creating an annotation-group. Pass the list of annotations to create a group. When show is called on the group, the annotations are animated in order.
53 |
54 | ```javascript
55 | import { annotate, annotationGroup } from 'rough-notation';
56 |
57 | const a1 = annotate(document.querySelector('#e1'), { type: 'underline' });
58 | const a2 = annotate(document.querySelector('#e3'), { type: 'box' });
59 | const a3 = annotate(document.querySelector('#e3'), { type: 'circle' });
60 |
61 | const ag = annotationGroup([a3, a1, a2]);
62 | ag.show();
63 | ```
64 |
65 | ## Live examples
66 | I have created some basic examples on Glitch for you to remix and play with the code:
67 |
68 | [Basic demo](https://glitch.com/~basic-rough-notation)
69 |
70 | [Annotation group demo](https://glitch.com/~annotation-group)
71 |
72 | ## Configuring the Annotation
73 |
74 | When you create an annotation object, you pass in a config. The config only has one mandatory field, which is the `type` of the annotation. But you can configure the annotation in many ways.
75 |
76 | #### type
77 | This is a mandatory field. It sets the annotation style. Following are the list of supported annotation types:
78 |
79 | * __underline__: This style creates a sketchy underline below an element.
80 | * __box__: This style draws a box around the element.
81 | * __circle__: This style draws a circle around the element.
82 | * __highlight__: This style creates a highlight effect as if marked by a highlighter.
83 | * __strike-through__: This style draws horizontal lines through the element.
84 | * __crossed-off__: This style draws an 'X' across the element.
85 | * __bracket__: This style draws a bracket around an element, usually a paragraph of text. By default on the right side, but can be configured to any or all of *left, right, top, bottom*.
86 |
87 | #### animate
88 | Boolean property to turn on/off animation when annotating. Default value is `true`.
89 |
90 | #### animationDuration
91 | Duration of the animation in milliseconds. Default is `800ms`.
92 |
93 | #### color
94 | String value representing the color of the annotation sketch. Default value is `currentColor`.
95 |
96 | #### strokeWidth
97 | Width of the annotation strokes. Default value is `1`.
98 |
99 | #### padding
100 | Padding between the element and roughly where the annotation is drawn. Default value is `5` (in pixels).
101 | If you wish to specify different `top`, `left`, `right`, `bottom` paddings, you can set the value to an array akin to CSS style padding `[top, right, bottom, left]` or just `[top & bottom, left & right]`.
102 |
103 | #### multiline
104 | This property only applies to inline text. To annotate multiline text (each line separately), set this property to `true`.
105 |
106 | #### iterations
107 | By default annotations are drawn in two iterations, e.g. when underlining, drawing from left to right and then back from right to left. Setting this property can let you configure the number of iterations.
108 |
109 | #### brackets
110 | Value could be a string or an array of strings, each string being one of these values: **left, right, top, bottom**. When drawing a bracket, this configures which side(s) of the element to bracket. Default value is `right`.
111 |
112 | #### rtl
113 | By default annotations are drawn from left to right. To start with right to left, set this property to `true`.
114 |
115 | ## Annotation Object
116 |
117 | When you call the `annotate` function, you get back an annotation object, which has the following methods:
118 |
119 | #### isShowing(): boolean
120 | Returns if the annotation is showing
121 |
122 | #### show()
123 | Draws the annotation. If the annotation is set to animate (default), it will animate the drawing. If called again, it will re-render the annotation, updating any size or location changes.
124 |
125 | *Note: to reanimate the annotation, call `hide()` and then `show()` again.
126 |
127 | #### hide()
128 | Hides the annotation if showing. This is not animated.
129 |
130 | #### remove()
131 | Unlinks the annotation from the element.
132 |
133 | #### Updating styles
134 | All the properties in the configuration are also exposed in this object. e.g. if you'd like to change the color, you can do that after the annotation has been drawn.
135 |
136 | ```javascript
137 | const e = document.querySelector('#myElement');
138 | const annotation = annotate(e, { type: 'underline', color: 'red' });
139 | annotation.show();
140 | annotation.color = 'green';
141 | ```
142 |
143 | *Note: the type of the annotation cannot be changed. Create a new annotation for that.*
144 |
145 | ## Annotation Group Object
146 |
147 | When you call the `annotationGroup` function, you get back an annotation group object, which has the following methods:
148 |
149 | #### show()
150 | Draws all the annotations in order. If the annotation is set to animate (default), it will animate the drawing.
151 |
152 | #### hide()
153 | Hides all the annotations if showing. This is not animated.
154 |
155 | ## Wrappers
156 |
157 | Others have created handy Rough Notation wrappers for multiple libraries and frameworks:
158 |
159 | - [React Rough Notation](https://github.com/linkstrifer/react-rough-notation)
160 | - [Svelte Rough Notation](https://github.com/dimfeld/svelte-rough-notation)
161 | - [Vue Rough Notation](https://github.com/Leecason/vue-rough-notation)
162 | - [Web Component Rough Notation](https://github.com/Matsuuu/vanilla-rough-notation)
163 | - [Angular Rough Notation](https://github.com/mikyaj/ngx-rough-notation)
164 |
165 | ## Contributors
166 |
167 | ### Financial Contributors
168 |
169 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/rough/contribute)]
170 |
171 | #### Individuals
172 |
173 |

174 |
175 | #### Organizations
176 |
177 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/rough/contribute)]
178 |
179 |

180 |

181 |

182 |

183 |

184 |

185 |

186 |

187 |

188 |

189 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rough-notation",
3 | "version": "0.5.1",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@babel/code-frame": {
8 | "version": "7.8.3",
9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
10 | "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
11 | "dev": true,
12 | "requires": {
13 | "@babel/highlight": "^7.8.3"
14 | }
15 | },
16 | "@babel/helper-validator-identifier": {
17 | "version": "7.9.5",
18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz",
19 | "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==",
20 | "dev": true
21 | },
22 | "@babel/highlight": {
23 | "version": "7.9.0",
24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz",
25 | "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==",
26 | "dev": true,
27 | "requires": {
28 | "@babel/helper-validator-identifier": "^7.9.0",
29 | "chalk": "^2.0.0",
30 | "js-tokens": "^4.0.0"
31 | }
32 | },
33 | "@types/node": {
34 | "version": "14.0.5",
35 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.5.tgz",
36 | "integrity": "sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA==",
37 | "dev": true
38 | },
39 | "@types/resolve": {
40 | "version": "0.0.8",
41 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
42 | "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
43 | "dev": true,
44 | "requires": {
45 | "@types/node": "*"
46 | }
47 | },
48 | "ansi-styles": {
49 | "version": "3.2.1",
50 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
51 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
52 | "dev": true,
53 | "requires": {
54 | "color-convert": "^1.9.0"
55 | }
56 | },
57 | "argparse": {
58 | "version": "1.0.10",
59 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
60 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
61 | "dev": true,
62 | "requires": {
63 | "sprintf-js": "~1.0.2"
64 | }
65 | },
66 | "balanced-match": {
67 | "version": "1.0.0",
68 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
69 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
70 | "dev": true
71 | },
72 | "brace-expansion": {
73 | "version": "1.1.11",
74 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
75 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
76 | "dev": true,
77 | "requires": {
78 | "balanced-match": "^1.0.0",
79 | "concat-map": "0.0.1"
80 | }
81 | },
82 | "buffer-from": {
83 | "version": "1.1.1",
84 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
85 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
86 | "dev": true
87 | },
88 | "builtin-modules": {
89 | "version": "3.1.0",
90 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
91 | "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
92 | "dev": true
93 | },
94 | "chalk": {
95 | "version": "2.4.2",
96 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
97 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
98 | "dev": true,
99 | "requires": {
100 | "ansi-styles": "^3.2.1",
101 | "escape-string-regexp": "^1.0.5",
102 | "supports-color": "^5.3.0"
103 | }
104 | },
105 | "color-convert": {
106 | "version": "1.9.3",
107 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
108 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
109 | "dev": true,
110 | "requires": {
111 | "color-name": "1.1.3"
112 | }
113 | },
114 | "color-name": {
115 | "version": "1.1.3",
116 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
117 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
118 | "dev": true
119 | },
120 | "commander": {
121 | "version": "2.20.3",
122 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
123 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
124 | "dev": true
125 | },
126 | "concat-map": {
127 | "version": "0.0.1",
128 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
129 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
130 | "dev": true
131 | },
132 | "diff": {
133 | "version": "4.0.2",
134 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
135 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
136 | "dev": true
137 | },
138 | "escape-string-regexp": {
139 | "version": "1.0.5",
140 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
141 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
142 | "dev": true
143 | },
144 | "esprima": {
145 | "version": "4.0.1",
146 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
147 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
148 | "dev": true
149 | },
150 | "estree-walker": {
151 | "version": "0.6.1",
152 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
153 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
154 | "dev": true
155 | },
156 | "fs.realpath": {
157 | "version": "1.0.0",
158 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
159 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
160 | "dev": true
161 | },
162 | "fsevents": {
163 | "version": "2.1.3",
164 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
165 | "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
166 | "dev": true,
167 | "optional": true
168 | },
169 | "glob": {
170 | "version": "7.1.6",
171 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
172 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
173 | "dev": true,
174 | "requires": {
175 | "fs.realpath": "^1.0.0",
176 | "inflight": "^1.0.4",
177 | "inherits": "2",
178 | "minimatch": "^3.0.4",
179 | "once": "^1.3.0",
180 | "path-is-absolute": "^1.0.0"
181 | }
182 | },
183 | "has-flag": {
184 | "version": "3.0.0",
185 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
186 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
187 | "dev": true
188 | },
189 | "inflight": {
190 | "version": "1.0.6",
191 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
192 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
193 | "dev": true,
194 | "requires": {
195 | "once": "^1.3.0",
196 | "wrappy": "1"
197 | }
198 | },
199 | "inherits": {
200 | "version": "2.0.4",
201 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
202 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
203 | "dev": true
204 | },
205 | "is-module": {
206 | "version": "1.0.0",
207 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
208 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
209 | "dev": true
210 | },
211 | "jest-worker": {
212 | "version": "26.0.0",
213 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz",
214 | "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==",
215 | "dev": true,
216 | "requires": {
217 | "merge-stream": "^2.0.0",
218 | "supports-color": "^7.0.0"
219 | },
220 | "dependencies": {
221 | "has-flag": {
222 | "version": "4.0.0",
223 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
224 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
225 | "dev": true
226 | },
227 | "supports-color": {
228 | "version": "7.1.0",
229 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
230 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
231 | "dev": true,
232 | "requires": {
233 | "has-flag": "^4.0.0"
234 | }
235 | }
236 | }
237 | },
238 | "js-tokens": {
239 | "version": "4.0.0",
240 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
241 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
242 | "dev": true
243 | },
244 | "js-yaml": {
245 | "version": "3.14.0",
246 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
247 | "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
248 | "dev": true,
249 | "requires": {
250 | "argparse": "^1.0.7",
251 | "esprima": "^4.0.0"
252 | }
253 | },
254 | "merge-stream": {
255 | "version": "2.0.0",
256 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
257 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
258 | "dev": true
259 | },
260 | "minimatch": {
261 | "version": "3.0.4",
262 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
263 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
264 | "dev": true,
265 | "requires": {
266 | "brace-expansion": "^1.1.7"
267 | }
268 | },
269 | "minimist": {
270 | "version": "1.2.5",
271 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
272 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
273 | "dev": true
274 | },
275 | "mkdirp": {
276 | "version": "0.5.5",
277 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
278 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
279 | "dev": true,
280 | "requires": {
281 | "minimist": "^1.2.5"
282 | }
283 | },
284 | "once": {
285 | "version": "1.4.0",
286 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
287 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
288 | "dev": true,
289 | "requires": {
290 | "wrappy": "1"
291 | }
292 | },
293 | "path-data-parser": {
294 | "version": "0.1.0",
295 | "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
296 | "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
297 | "dev": true
298 | },
299 | "path-is-absolute": {
300 | "version": "1.0.1",
301 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
302 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
303 | "dev": true
304 | },
305 | "path-parse": {
306 | "version": "1.0.6",
307 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
308 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
309 | "dev": true
310 | },
311 | "points-on-curve": {
312 | "version": "0.2.0",
313 | "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
314 | "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
315 | "dev": true
316 | },
317 | "points-on-path": {
318 | "version": "0.2.1",
319 | "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
320 | "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
321 | "dev": true,
322 | "requires": {
323 | "path-data-parser": "0.1.0",
324 | "points-on-curve": "0.2.0"
325 | }
326 | },
327 | "resolve": {
328 | "version": "1.17.0",
329 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
330 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
331 | "dev": true,
332 | "requires": {
333 | "path-parse": "^1.0.6"
334 | }
335 | },
336 | "rollup": {
337 | "version": "2.32.1",
338 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.1.tgz",
339 | "integrity": "sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==",
340 | "dev": true,
341 | "requires": {
342 | "fsevents": "~2.1.2"
343 | }
344 | },
345 | "rollup-plugin-node-resolve": {
346 | "version": "5.2.0",
347 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
348 | "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
349 | "dev": true,
350 | "requires": {
351 | "@types/resolve": "0.0.8",
352 | "builtin-modules": "^3.1.0",
353 | "is-module": "^1.0.0",
354 | "resolve": "^1.11.1",
355 | "rollup-pluginutils": "^2.8.1"
356 | }
357 | },
358 | "rollup-plugin-terser": {
359 | "version": "6.1.0",
360 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-6.1.0.tgz",
361 | "integrity": "sha512-4fB3M9nuoWxrwm39habpd4hvrbrde2W2GG4zEGPQg1YITNkM3Tqur5jSuXlWNzbv/2aMLJ+dZJaySc3GCD8oDw==",
362 | "dev": true,
363 | "requires": {
364 | "@babel/code-frame": "^7.8.3",
365 | "jest-worker": "^26.0.0",
366 | "serialize-javascript": "^3.0.0",
367 | "terser": "^4.7.0"
368 | }
369 | },
370 | "rollup-pluginutils": {
371 | "version": "2.8.2",
372 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
373 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
374 | "dev": true,
375 | "requires": {
376 | "estree-walker": "^0.6.1"
377 | }
378 | },
379 | "roughjs": {
380 | "version": "4.3.1",
381 | "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.3.1.tgz",
382 | "integrity": "sha512-m42+OBaBR7x5UhIKyjBCnWqqkaEkBKLkXvHv4pOWJXPofvMnQY4ZcFEQlqf3coKKyZN2lfWMyx7QXSg2GD7SGA==",
383 | "dev": true,
384 | "requires": {
385 | "path-data-parser": "^0.1.0",
386 | "points-on-curve": "^0.2.0",
387 | "points-on-path": "^0.2.1"
388 | }
389 | },
390 | "semver": {
391 | "version": "5.7.1",
392 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
393 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
394 | "dev": true
395 | },
396 | "serialize-javascript": {
397 | "version": "3.0.0",
398 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz",
399 | "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==",
400 | "dev": true
401 | },
402 | "source-map": {
403 | "version": "0.6.1",
404 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
405 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
406 | "dev": true
407 | },
408 | "source-map-support": {
409 | "version": "0.5.19",
410 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
411 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
412 | "dev": true,
413 | "requires": {
414 | "buffer-from": "^1.0.0",
415 | "source-map": "^0.6.0"
416 | }
417 | },
418 | "sprintf-js": {
419 | "version": "1.0.3",
420 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
421 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
422 | "dev": true
423 | },
424 | "supports-color": {
425 | "version": "5.5.0",
426 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
427 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
428 | "dev": true,
429 | "requires": {
430 | "has-flag": "^3.0.0"
431 | }
432 | },
433 | "terser": {
434 | "version": "4.7.0",
435 | "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz",
436 | "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==",
437 | "dev": true,
438 | "requires": {
439 | "commander": "^2.20.0",
440 | "source-map": "~0.6.1",
441 | "source-map-support": "~0.5.12"
442 | }
443 | },
444 | "tslib": {
445 | "version": "1.14.1",
446 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
447 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
448 | "dev": true
449 | },
450 | "tslint": {
451 | "version": "6.1.3",
452 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
453 | "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
454 | "dev": true,
455 | "requires": {
456 | "@babel/code-frame": "^7.0.0",
457 | "builtin-modules": "^1.1.1",
458 | "chalk": "^2.3.0",
459 | "commander": "^2.12.1",
460 | "diff": "^4.0.1",
461 | "glob": "^7.1.1",
462 | "js-yaml": "^3.13.1",
463 | "minimatch": "^3.0.4",
464 | "mkdirp": "^0.5.3",
465 | "resolve": "^1.3.2",
466 | "semver": "^5.3.0",
467 | "tslib": "^1.13.0",
468 | "tsutils": "^2.29.0"
469 | },
470 | "dependencies": {
471 | "builtin-modules": {
472 | "version": "1.1.1",
473 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
474 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
475 | "dev": true
476 | }
477 | }
478 | },
479 | "tsutils": {
480 | "version": "2.29.0",
481 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
482 | "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
483 | "dev": true,
484 | "requires": {
485 | "tslib": "^1.8.1"
486 | }
487 | },
488 | "typescript": {
489 | "version": "4.0.5",
490 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz",
491 | "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==",
492 | "dev": true
493 | },
494 | "wrappy": {
495 | "version": "1.0.2",
496 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
497 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
498 | "dev": true
499 | }
500 | }
501 | }
502 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rough-notation",
3 | "version": "0.5.1",
4 | "description": "Create and animate hand-drawn annotations on a web page",
5 | "main": "lib/rough-notation.cjs.js",
6 | "module": "lib/rough-notation.esm.js",
7 | "types": "lib/rough-notation.d.ts",
8 | "scripts": {
9 | "build": "rm -rf lib && tsc && rollup -c",
10 | "lint": "tslint -p tsconfig.json",
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/pshihn/rough-notation.git"
16 | },
17 | "keywords": [
18 | "annotate",
19 | "rough",
20 | "sketchy"
21 | ],
22 | "author": "Preet Shihn",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/pshihn/rough-notation/issues"
26 | },
27 | "homepage": "https://github.com/pshihn/rough-notation#readme",
28 | "devDependencies": {
29 | "rollup": "^2.32.1",
30 | "rollup-plugin-node-resolve": "^5.2.0",
31 | "rollup-plugin-terser": "^6.1.0",
32 | "roughjs": "^4.3.1",
33 | "tslint": "^6.1.3",
34 | "typescript": "^4.0.5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import { terser } from "rollup-plugin-terser";
3 |
4 | const input = 'lib/rough-notation.js';
5 |
6 | export default [
7 | {
8 | input,
9 | output: {
10 | file: 'lib/rough-notation.iife.js',
11 | format: 'iife',
12 | name: 'RoughNotation'
13 | },
14 | plugins: [resolve(), terser()]
15 | },
16 | {
17 | input,
18 | output: {
19 | file: 'lib/rough-notation.esm.js',
20 | format: 'esm'
21 | },
22 | plugins: [resolve(), terser()]
23 | },
24 | {
25 | input,
26 | output: {
27 | file: 'lib/rough-notation.cjs.js',
28 | format: 'cjs'
29 | },
30 | plugins: [resolve(), terser()]
31 | },
32 | ];
--------------------------------------------------------------------------------
/src/keyframes.ts:
--------------------------------------------------------------------------------
1 | export function ensureKeyframes() {
2 | if (!(window as any).__rno_kf_s) {
3 | const style = (window as any).__rno_kf_s = document.createElement('style');
4 | style.textContent = `@keyframes rough-notation-dash { to { stroke-dashoffset: 0; } }`;
5 | document.head.appendChild(style);
6 | }
7 | }
--------------------------------------------------------------------------------
/src/model.ts:
--------------------------------------------------------------------------------
1 | export const SVG_NS = 'http://www.w3.org/2000/svg';
2 |
3 | export const DEFAULT_ANIMATION_DURATION = 800;
4 |
5 | export interface Rect {
6 | x: number;
7 | y: number;
8 | w: number;
9 | h: number;
10 | }
11 |
12 | export type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' | 'crossed-off' | 'bracket';
13 | export type FullPadding = [number, number, number, number];
14 | export type RoughPadding = number | [number, number] | FullPadding;
15 | export type BracketType = 'left' | 'right' | 'top' | 'bottom';
16 |
17 | export interface RoughAnnotationConfig extends RoughAnnotationConfigBase {
18 | type: RoughAnnotationType;
19 | multiline?: boolean;
20 | rtl?: boolean;
21 | }
22 |
23 | export interface RoughAnnotationConfigBase {
24 | animate?: boolean; // defaults to true
25 | animationDuration?: number; // defaulst to 1000ms
26 | color?: string; // defaults to currentColor
27 | strokeWidth?: number; // default based on type
28 | padding?: RoughPadding; // defaults to 5px
29 | iterations?: number; // defaults to 2
30 | brackets?: BracketType | BracketType[]; // defaults to 'right'
31 | }
32 |
33 | export interface RoughAnnotation extends RoughAnnotationConfigBase {
34 | isShowing(): boolean;
35 | show(): void;
36 | hide(): void;
37 | remove(): void;
38 | }
39 |
40 | export interface RoughAnnotationGroup {
41 | show(): void;
42 | hide(): void;
43 | }
--------------------------------------------------------------------------------
/src/render.ts:
--------------------------------------------------------------------------------
1 | import { Rect, RoughAnnotationConfig, SVG_NS, FullPadding, BracketType } from './model.js';
2 | import { ResolvedOptions, OpSet } from 'roughjs/bin/core';
3 | import { line, rectangle, ellipse, linearPath } from 'roughjs/bin/renderer';
4 | import { Point } from 'roughjs/bin/geometry';
5 |
6 | type RoughOptionsType = 'highlight' | 'single' | 'double';
7 |
8 | function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions {
9 | return {
10 | maxRandomnessOffset: 2,
11 | roughness: type === 'highlight' ? 3 : 1.5,
12 | bowing: 1,
13 | stroke: '#000',
14 | strokeWidth: 1.5,
15 | curveTightness: 0,
16 | curveFitting: 0.95,
17 | curveStepCount: 9,
18 | fillStyle: 'hachure',
19 | fillWeight: -1,
20 | hachureAngle: -41,
21 | hachureGap: -1,
22 | dashOffset: -1,
23 | dashGap: -1,
24 | zigzagOffset: -1,
25 | combineNestedSvgPaths: false,
26 | disableMultiStroke: type !== 'double',
27 | disableMultiStrokeFill: false,
28 | seed
29 | };
30 | }
31 |
32 | function parsePadding(config: RoughAnnotationConfig): FullPadding {
33 | const p = config.padding;
34 | if (p || (p === 0)) {
35 | if (typeof p === 'number') {
36 | return [p, p, p, p];
37 | } else if (Array.isArray(p)) {
38 | const pa = p as number[];
39 | if (pa.length) {
40 | switch (pa.length) {
41 | case 4:
42 | return [...pa] as FullPadding;
43 | case 1:
44 | return [pa[0], pa[0], pa[0], pa[0]];
45 | case 2:
46 | return [...pa, ...pa] as FullPadding;
47 | case 3:
48 | return [...pa, pa[1]] as FullPadding;
49 | default:
50 | return [pa[0], pa[1], pa[2], pa[3]];
51 | }
52 | }
53 | }
54 | }
55 | return [5, 5, 5, 5];
56 | }
57 |
58 | export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAnnotationConfig, animationGroupDelay: number, animationDuration: number, seed: number) {
59 | const opList: OpSet[] = [];
60 | let strokeWidth = config.strokeWidth || 2;
61 | const padding = parsePadding(config);
62 | const animate = (config.animate === undefined) ? true : (!!config.animate);
63 | const iterations = config.iterations || 2;
64 | const rtl = config.rtl ? 1 : 0;
65 | const o = getOptions('single', seed);
66 |
67 | switch (config.type) {
68 | case 'underline': {
69 | const y = rect.y + rect.h + padding[2];
70 | for (let i = rtl; i < iterations + rtl; i++) {
71 | if (i % 2) {
72 | opList.push(line(rect.x + rect.w, y, rect.x, y, o));
73 | } else {
74 | opList.push(line(rect.x, y, rect.x + rect.w, y, o));
75 | }
76 | }
77 | break;
78 | }
79 | case 'strike-through': {
80 | const y = rect.y + (rect.h / 2);
81 | for (let i = rtl; i < iterations + rtl; i++) {
82 | if (i % 2) {
83 | opList.push(line(rect.x + rect.w, y, rect.x, y, o));
84 | } else {
85 | opList.push(line(rect.x, y, rect.x + rect.w, y, o));
86 | }
87 | }
88 | break;
89 | }
90 | case 'box': {
91 | const x = rect.x - padding[3];
92 | const y = rect.y - padding[0];
93 | const width = rect.w + (padding[1] + padding[3]);
94 | const height = rect.h + (padding[0] + padding[2]);
95 | for (let i = 0; i < iterations; i++) {
96 | opList.push(rectangle(x, y, width, height, o));
97 | }
98 | break;
99 | }
100 | case 'bracket': {
101 | const brackets: BracketType[] = Array.isArray(config.brackets) ? config.brackets : (config.brackets ? [config.brackets] : ['right']);
102 | const lx = rect.x - padding[3] * 2;
103 | const rx = rect.x + rect.w + padding[1] * 2;
104 | const ty = rect.y - padding[0] * 2;
105 | const by = rect.y + rect.h + padding[2] * 2;
106 | for (const br of brackets) {
107 | let points: Point[];
108 | switch (br) {
109 | case 'bottom':
110 | points = [
111 | [lx, rect.y + rect.h],
112 | [lx, by],
113 | [rx, by],
114 | [rx, rect.y + rect.h]
115 | ];
116 | break;
117 | case 'top':
118 | points = [
119 | [lx, rect.y],
120 | [lx, ty],
121 | [rx, ty],
122 | [rx, rect.y]
123 | ];
124 | break;
125 | case 'left':
126 | points = [
127 | [rect.x, ty],
128 | [lx, ty],
129 | [lx, by],
130 | [rect.x, by]
131 | ];
132 | break;
133 | case 'right':
134 | points = [
135 | [rect.x + rect.w, ty],
136 | [rx, ty],
137 | [rx, by],
138 | [rect.x + rect.w, by]
139 | ];
140 | break;
141 | }
142 | if (points) {
143 | opList.push(linearPath(points, false, o));
144 | }
145 | }
146 | break;
147 | }
148 | case 'crossed-off': {
149 | const x = rect.x;
150 | const y = rect.y;
151 | const x2 = x + rect.w;
152 | const y2 = y + rect.h;
153 | for (let i = rtl; i < iterations + rtl; i++) {
154 | if (i % 2) {
155 | opList.push(line(x2, y2, x, y, o));
156 | } else {
157 | opList.push(line(x, y, x2, y2, o));
158 | }
159 | }
160 | for (let i = rtl; i < iterations + rtl; i++) {
161 | if (i % 2) {
162 | opList.push(line(x, y2, x2, y, o));
163 | } else {
164 | opList.push(line(x2, y, x, y2, o));
165 | }
166 | }
167 | break;
168 | }
169 | case 'circle': {
170 | const doubleO = getOptions('double', seed);
171 | const width = rect.w + (padding[1] + padding[3]);
172 | const height = rect.h + (padding[0] + padding[2]);
173 | const x = rect.x - padding[3] + (width / 2);
174 | const y = rect.y - padding[0] + (height / 2);
175 | const fullItr = Math.floor(iterations / 2);
176 | const singleItr = iterations - (fullItr * 2);
177 | for (let i = 0; i < fullItr; i++) {
178 | opList.push(ellipse(x, y, width, height, doubleO));
179 | }
180 | for (let i = 0; i < singleItr; i++) {
181 | opList.push(ellipse(x, y, width, height, o));
182 | }
183 | break;
184 | }
185 | case 'highlight': {
186 | const o = getOptions('highlight', seed);
187 | strokeWidth = rect.h * 0.95;
188 | const y = rect.y + (rect.h / 2);
189 | for (let i = rtl; i < iterations + rtl; i++) {
190 | if (i % 2) {
191 | opList.push(line(rect.x + rect.w, y, rect.x, y, o));
192 | } else {
193 | opList.push(line(rect.x, y, rect.x + rect.w, y, o));
194 | }
195 | }
196 | break;
197 | }
198 | }
199 |
200 | if (opList.length) {
201 | const pathStrings = opsToPath(opList);
202 | const lengths: number[] = [];
203 | const pathElements: SVGPathElement[] = [];
204 | let totalLength = 0;
205 | const setAttr = (p: SVGPathElement, an: string, av: string) => p.setAttribute(an, av);
206 |
207 | for (const d of pathStrings) {
208 | const path = document.createElementNS(SVG_NS, 'path');
209 | setAttr(path, 'd', d);
210 | setAttr(path, 'fill', 'none');
211 | setAttr(path, 'stroke', config.color || 'currentColor');
212 | setAttr(path, 'stroke-width', `${strokeWidth}`);
213 | if (animate) {
214 | const length = path.getTotalLength();
215 | lengths.push(length);
216 | totalLength += length;
217 | }
218 | svg.appendChild(path);
219 | pathElements.push(path);
220 | }
221 |
222 | if (animate) {
223 | let durationOffset = 0;
224 | for (let i = 0; i < pathElements.length; i++) {
225 | const path = pathElements[i];
226 | const length = lengths[i];
227 | const duration = totalLength ? (animationDuration * (length / totalLength)) : 0;
228 | const delay = animationGroupDelay + durationOffset;
229 | const style = path.style;
230 | style.strokeDashoffset = `${length}`;
231 | style.strokeDasharray = `${length}`;
232 | style.animation = `rough-notation-dash ${duration}ms ease-out ${delay}ms forwards`;
233 | durationOffset += duration;
234 | }
235 | }
236 | }
237 | }
238 |
239 | function opsToPath(opList: OpSet[]): string[] {
240 | const paths: string[] = [];
241 | for (const drawing of opList) {
242 | let path = '';
243 | for (const item of drawing.ops) {
244 | const data = item.data;
245 | switch (item.op) {
246 | case 'move':
247 | if (path.trim()) {
248 | paths.push(path.trim());
249 | }
250 | path = `M${data[0]} ${data[1]} `;
251 | break;
252 | case 'bcurveTo':
253 | path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `;
254 | break;
255 | case 'lineTo':
256 | path += `L${data[0]} ${data[1]} `;
257 | break;
258 | }
259 | }
260 | if (path.trim()) {
261 | paths.push(path.trim());
262 | }
263 | }
264 | return paths;
265 | }
--------------------------------------------------------------------------------
/src/rough-notation.ts:
--------------------------------------------------------------------------------
1 | import { Rect, RoughAnnotationConfig, RoughAnnotation, SVG_NS, RoughAnnotationGroup, DEFAULT_ANIMATION_DURATION } from './model.js';
2 | import { renderAnnotation } from './render.js';
3 | import { ensureKeyframes } from './keyframes.js';
4 | import { randomSeed } from 'roughjs/bin/math';
5 |
6 | type AnnotationState = 'unattached' | 'not-showing' | 'showing';
7 |
8 | class RoughAnnotationImpl implements RoughAnnotation {
9 | private _state: AnnotationState = 'unattached';
10 | private _config: RoughAnnotationConfig;
11 | private _resizing = false;
12 | private _ro?: any; // ResizeObserver is not supported in typescript std lib yet
13 | private _seed = randomSeed();
14 |
15 | private _e: HTMLElement;
16 | private _svg?: SVGSVGElement;
17 | private _lastSizes: Rect[] = [];
18 |
19 | _animationDelay = 0;
20 |
21 | constructor(e: HTMLElement, config: RoughAnnotationConfig) {
22 | this._e = e;
23 | this._config = JSON.parse(JSON.stringify(config));
24 | this.attach();
25 | }
26 |
27 | get animate() { return this._config.animate; }
28 | set animate(value) { this._config.animate = value; }
29 |
30 | get animationDuration() { return this._config.animationDuration; }
31 | set animationDuration(value) { this._config.animationDuration = value; }
32 |
33 | get iterations() { return this._config.iterations; }
34 | set iterations(value) { this._config.iterations = value; }
35 |
36 | get color() { return this._config.color; }
37 | set color(value) {
38 | if (this._config.color !== value) {
39 | this._config.color = value;
40 | this.refresh();
41 | }
42 | }
43 |
44 | get strokeWidth() { return this._config.strokeWidth; }
45 | set strokeWidth(value) {
46 | if (this._config.strokeWidth !== value) {
47 | this._config.strokeWidth = value;
48 | this.refresh();
49 | }
50 | }
51 |
52 | get padding() { return this._config.padding; }
53 | set padding(value) {
54 | if (this._config.padding !== value) {
55 | this._config.padding = value;
56 | this.refresh();
57 | }
58 | }
59 |
60 | private _resizeListener = () => {
61 | if (!this._resizing) {
62 | this._resizing = true;
63 | setTimeout(() => {
64 | this._resizing = false;
65 | if (this._state === 'showing') {
66 | if (this.haveRectsChanged()) {
67 | this.show();
68 | }
69 | }
70 | }, 400);
71 | }
72 | }
73 |
74 | private attach() {
75 | if (this._state === 'unattached' && this._e.parentElement) {
76 | ensureKeyframes();
77 | const svg = this._svg = document.createElementNS(SVG_NS, 'svg');
78 | svg.setAttribute('class', 'rough-annotation');
79 | const style = svg.style;
80 | style.position = 'absolute';
81 | style.top = '0';
82 | style.left = '0';
83 | style.overflow = 'visible';
84 | style.pointerEvents = 'none';
85 | style.width = '100px';
86 | style.height = '100px';
87 | const prepend = this._config.type === 'highlight';
88 | this._e.insertAdjacentElement(prepend ? 'beforebegin' : 'afterend', svg);
89 | this._state = 'not-showing';
90 |
91 | // ensure e is positioned
92 | if (prepend) {
93 | const computedPos = window.getComputedStyle(this._e).position;
94 | const unpositioned = (!computedPos) || (computedPos === 'static');
95 | if (unpositioned) {
96 | this._e.style.position = 'relative';
97 | }
98 | }
99 | this.attachListeners();
100 | }
101 | }
102 |
103 | private detachListeners() {
104 | window.removeEventListener('resize', this._resizeListener);
105 | if (this._ro) {
106 | this._ro.unobserve(this._e);
107 | }
108 | }
109 |
110 | private attachListeners() {
111 | this.detachListeners();
112 | window.addEventListener('resize', this._resizeListener, { passive: true });
113 | if ((!this._ro) && ('ResizeObserver' in window)) {
114 | this._ro = new (window as any).ResizeObserver((entries: any) => {
115 | for (const entry of entries) {
116 | if (entry.contentRect) {
117 | this._resizeListener();
118 | }
119 | }
120 | });
121 | }
122 | if (this._ro) {
123 | this._ro.observe(this._e);
124 | }
125 | }
126 |
127 | private haveRectsChanged(): boolean {
128 | if (this._lastSizes.length) {
129 | const newRects = this.rects();
130 | if (newRects.length === this._lastSizes.length) {
131 | for (let i = 0; i < newRects.length; i++) {
132 | if (!this.isSameRect(newRects[i], this._lastSizes[i])) {
133 | return true;
134 | }
135 | }
136 | } else {
137 | return true;
138 | }
139 | }
140 | return false;
141 | }
142 |
143 | private isSameRect(rect1: Rect, rect2: Rect): boolean {
144 | const si = (a: number, b: number) => Math.round(a) === Math.round(b);
145 | return (
146 | si(rect1.x, rect2.x) &&
147 | si(rect1.y, rect2.y) &&
148 | si(rect1.w, rect2.w) &&
149 | si(rect1.h, rect2.h)
150 | );
151 | }
152 |
153 | isShowing(): boolean {
154 | return (this._state !== 'not-showing');
155 | }
156 |
157 | private pendingRefresh?: Promise
;
158 | private refresh() {
159 | if (this.isShowing() && (!this.pendingRefresh)) {
160 | this.pendingRefresh = Promise.resolve().then(() => {
161 | if (this.isShowing()) {
162 | this.show();
163 | }
164 | delete this.pendingRefresh;
165 | });
166 | }
167 | }
168 |
169 | show(): void {
170 | switch (this._state) {
171 | case 'unattached':
172 | break;
173 | case 'showing':
174 | this.hide();
175 | if (this._svg) {
176 | this.render(this._svg, true);
177 | }
178 | break;
179 | case 'not-showing':
180 | this.attach();
181 | if (this._svg) {
182 | this.render(this._svg, false);
183 | }
184 | break;
185 | }
186 | }
187 |
188 | hide(): void {
189 | if (this._svg) {
190 | while (this._svg.lastChild) {
191 | this._svg.removeChild(this._svg.lastChild);
192 | }
193 | }
194 | this._state = 'not-showing';
195 | }
196 |
197 | remove(): void {
198 | if (this._svg && this._svg.parentElement) {
199 | this._svg.parentElement.removeChild(this._svg);
200 | }
201 | this._svg = undefined;
202 | this._state = 'unattached';
203 | this.detachListeners();
204 | }
205 |
206 | private render(svg: SVGSVGElement, ensureNoAnimation: boolean) {
207 | let config = this._config;
208 | if (ensureNoAnimation) {
209 | config = JSON.parse(JSON.stringify(this._config));
210 | config.animate = false;
211 | }
212 | const rects = this.rects();
213 | let totalWidth = 0;
214 | rects.forEach((rect) => totalWidth += rect.w);
215 | const totalDuration = (config.animationDuration || DEFAULT_ANIMATION_DURATION);
216 | let delay = 0;
217 | for (let i = 0; i < rects.length; i++) {
218 | const rect = rects[i];
219 | const ad = totalDuration * (rect.w / totalWidth);
220 | renderAnnotation(svg, rects[i], config, delay + this._animationDelay, ad, this._seed);
221 | delay += ad;
222 | }
223 | this._lastSizes = rects;
224 | this._state = 'showing';
225 | }
226 |
227 | private rects(): Rect[] {
228 | const ret: Rect[] = [];
229 | if (this._svg) {
230 | if (this._config.multiline) {
231 | const elementRects = this._e.getClientRects();
232 | for (let i = 0; i < elementRects.length; i++) {
233 | ret.push(this.svgRect(this._svg, elementRects[i]));
234 | }
235 | } else {
236 | ret.push(this.svgRect(this._svg, this._e.getBoundingClientRect()));
237 | }
238 | }
239 | return ret;
240 | }
241 |
242 | private svgRect(svg: SVGSVGElement, bounds: DOMRect | DOMRectReadOnly): Rect {
243 | const rect1 = svg.getBoundingClientRect();
244 | const rect2 = bounds;
245 | return {
246 | x: (rect2.x || rect2.left) - (rect1.x || rect1.left),
247 | y: (rect2.y || rect2.top) - (rect1.y || rect1.top),
248 | w: rect2.width,
249 | h: rect2.height
250 | };
251 | }
252 | }
253 |
254 | export function annotate(element: HTMLElement, config: RoughAnnotationConfig): RoughAnnotation {
255 | return new RoughAnnotationImpl(element, config);
256 | }
257 |
258 | export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotationGroup {
259 | let delay = 0;
260 | for (const a of annotations) {
261 | const ai = a as RoughAnnotationImpl;
262 | ai._animationDelay = delay;
263 | const duration = ai.animationDuration === 0 ? 0 : (ai.animationDuration || DEFAULT_ANIMATION_DURATION);
264 | delay += duration;
265 | }
266 | const list = [...annotations];
267 | return {
268 | show() {
269 | for (const a of list) {
270 | a.show();
271 | }
272 | },
273 | hide() {
274 | for (const a of list) {
275 | a.hide();
276 | }
277 | }
278 | };
279 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "es2015",
5 | "moduleResolution": "node",
6 | "lib": [
7 | "es2017",
8 | "dom"
9 | ],
10 | "declaration": true,
11 | "outDir": "./lib",
12 | "baseUrl": ".",
13 | "strict": true,
14 | "strictNullChecks": true,
15 | "noImplicitAny": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "noImplicitReturns": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": [
22 | "src/**/*.ts"
23 | ]
24 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "arrow-parens": true,
4 | "class-name": true,
5 | "indent": [
6 | true,
7 | "spaces",
8 | 2
9 | ],
10 | "prefer-const": true,
11 | "no-duplicate-variable": true,
12 | "no-eval": true,
13 | "no-internal-module": true,
14 | "no-trailing-whitespace": false,
15 | "no-var-keyword": true,
16 | "one-line": [
17 | true,
18 | "check-open-brace",
19 | "check-whitespace"
20 | ],
21 | "quotemark": [
22 | true,
23 | "single",
24 | "avoid-escape"
25 | ],
26 | "semicolon": [
27 | true,
28 | "always"
29 | ],
30 | "trailing-comma": [
31 | true,
32 | "multiline"
33 | ],
34 | "triple-equals": [
35 | true,
36 | "allow-null-check"
37 | ],
38 | "typedef-whitespace": [
39 | true,
40 | {
41 | "call-signature": "nospace",
42 | "index-signature": "nospace",
43 | "parameter": "nospace",
44 | "property-declaration": "nospace",
45 | "variable-declaration": "nospace"
46 | }
47 | ],
48 | "variable-name": [
49 | true,
50 | "ban-keywords"
51 | ],
52 | "whitespace": [
53 | true,
54 | "check-branch",
55 | "check-decl",
56 | "check-operator",
57 | "check-separator",
58 | "check-type"
59 | ]
60 | }
61 | }
--------------------------------------------------------------------------------