├── .gitignore ├── assets ├── favicon.svg ├── fonts │ ├── MPLUSRounded1c-Medium.woff │ └── MPLUSRounded1c-Medium.woff2 ├── icons │ ├── arrow-back-up.svg │ ├── arrow-forward-up.svg │ ├── chevron-left.svg │ ├── chevron-right.svg │ ├── color-picker.svg │ ├── door-exit.svg │ ├── dot.svg │ ├── download.svg │ ├── eraser.svg │ ├── file-download.svg │ ├── line.svg │ ├── link.svg │ ├── none.svg │ ├── palette.svg │ ├── pencil.svg │ ├── photo-down.svg │ ├── photo.svg │ ├── point.svg │ ├── pointer.svg │ ├── sq.svg │ └── trash-x.svg └── patterns │ ├── pattern_dot.svg │ ├── pattern_line.svg │ ├── pattern_none.svg │ └── pattern_sq.svg ├── board.html ├── css └── board.css └── js ├── draw.js ├── export.js ├── functions.js ├── lib ├── panzoom.js ├── simplify-svg-path.js ├── simplify.js └── svg-export.min.js ├── pan-zoom.js ├── panels.js └── settings.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/node 145 | -------------------------------------------------------------------------------- /assets/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/fonts/MPLUSRounded1c-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samutich/Doska/0a4736e1f0895454486efcf16187d651694c80f0/assets/fonts/MPLUSRounded1c-Medium.woff -------------------------------------------------------------------------------- /assets/fonts/MPLUSRounded1c-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samutich/Doska/0a4736e1f0895454486efcf16187d651694c80f0/assets/fonts/MPLUSRounded1c-Medium.woff2 -------------------------------------------------------------------------------- /assets/icons/arrow-back-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/arrow-forward-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/color-picker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/door-exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/dot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icons/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/file-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icons/none.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/palette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/photo-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/photo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/point.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/pointer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/sq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icons/trash-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/patterns/pattern_dot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/patterns/pattern_line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/patterns/pattern_none.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/patterns/pattern_sq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /board.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 16 | Board — Samutichev 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 |
36 |
37 | 43 | 49 | 55 |
56 | 57 |
58 |
59 |
60 | 67 | 73 | 79 |
80 |
81 | 86 | 91 | 96 |
97 |
98 |
103 | 108 | 114 | 119 |
120 |
125 | 131 | 136 | 141 | 146 | 151 | 156 |
157 |
162 | 168 | 173 | 178 | 183 |
184 |
185 |
186 | 187 | 188 | -------------------------------------------------------------------------------- /css/board.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | display: none; 3 | } 4 | * { 5 | -webkit-tap-highlight-color: transparent; 6 | } 7 | @font-face { 8 | font-family: 'M PLUS Medium 1c'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('M PLUS Medium 1c'), 12 | url('../assets/fonts/MPLUSRounded1c-Medium.woff2') format('woff2'), 13 | url('../assets/fonts/MPLUSRounded1c-Medium.woff') format('woff'); 14 | } 15 | body { 16 | margin: 0; 17 | padding: 0; 18 | display: flex; 19 | justify-content: center; 20 | overflow: hidden; 21 | font-family: 'M PLUS Medium 1c', 'Arial', sans-serif; 22 | } 23 | 24 | #board { 25 | position: fixed; 26 | top: 0; 27 | bottom: 0; 28 | left: 0; 29 | right: 0; 30 | width: 100%; 31 | height: 100%; 32 | outline: none; 33 | opacity: 1; 34 | fill: none; 35 | stroke-linecap: round; 36 | transition: all 150ms; 37 | cursor: crosshair; 38 | } 39 | /* path { 40 | transform-origin: 0 0; 41 | transform: translate(0px, 0px); 42 | } */ 43 | #background { 44 | position: absolute; 45 | width: 100%; 46 | height: 100%; 47 | } 48 | 49 | .panels { 50 | display: contents; 51 | } 52 | 53 | .topPanel { 54 | display: flex; 55 | position: fixed; 56 | top: 10px; 57 | left: 10px; 58 | z-index: 10; 59 | } 60 | .topPanel > button { 61 | width: 30px; 62 | height: 30px; 63 | background: #2525257a; 64 | background-size: 18px; 65 | background-repeat: no-repeat; 66 | background-position: center; 67 | border-radius: 10px; 68 | box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656; 69 | backdrop-filter: blur(4px); 70 | transition: 0.3s; 71 | } 72 | 73 | #exit { 74 | background-image: url('../assets/icons/door-exit.svg'); 75 | } 76 | #exit:hover { 77 | background-color: #ff655b; 78 | } 79 | #link { 80 | background-image: url('../assets/icons/link.svg'); 81 | } 82 | #link:hover { 83 | background-color: #00a92f; 84 | } 85 | #download { 86 | background-image: url('../assets/icons/file-download.svg'); 87 | } 88 | #download:hover { 89 | background-color: #00a92f; 90 | } 91 | 92 | .dockPanel { 93 | display: flex; 94 | align-items: center; 95 | justify-content: center; 96 | position: fixed; 97 | bottom: -4px; 98 | z-index: 10; 99 | } 100 | .main-tools { 101 | margin: 0 20px 25px; 102 | padding: 2px; 103 | display: flex; 104 | gap: 20px; 105 | z-index: 3; 106 | background-color: #2525257a; 107 | border-radius: 12px; 108 | box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656; 109 | backdrop-filter: blur(4px); 110 | } 111 | .tools { 112 | display: flex; 113 | z-index: 3; 114 | border-radius: 50px; 115 | } 116 | .settings { 117 | padding: 2px; 118 | display: none; 119 | position: absolute; 120 | bottom: 90px; 121 | background-color: #2525257a; 122 | border-radius: 12px; 123 | box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656; 124 | backdrop-filter: blur(4px); 125 | cursor: default; 126 | } 127 | #colors { 128 | padding: 3px 10px; 129 | } 130 | 131 | button { 132 | appearance: none; 133 | outline: none; 134 | margin: 6px; 135 | width: 30px; 136 | height: 30px; 137 | background: transparent; 138 | background-size: 20px; 139 | background-repeat: no-repeat; 140 | background-position: center; 141 | border: none; 142 | border-radius: 10px; 143 | cursor: pointer; 144 | transition: 0.1s ease-in; 145 | } 146 | button:hover { 147 | background-color: #7c7c7ceb; 148 | background-size: 22px; 149 | } 150 | button:active { 151 | background-color: #363636eb; 152 | background-size: 16px; 153 | } 154 | .active { 155 | background-color: #545454fa; 156 | background-size: 21.5px; 157 | box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656; 158 | } 159 | 160 | #undo { 161 | background-image: url('../assets/icons/arrow-back-up.svg'); 162 | } 163 | #redo { 164 | background-image: url('../assets/icons/arrow-forward-up.svg'); 165 | } 166 | 167 | #pointer { 168 | background-image: url('../assets/icons/pointer.svg'); 169 | } 170 | #pen { 171 | background-image: url('../assets/icons/pencil.svg'); 172 | } 173 | #eraser { 174 | background-image: url('../assets/icons/eraser.svg'); 175 | } 176 | #clear { 177 | background-image: url('../assets/icons/trash-x.svg'); 178 | } 179 | #clear:active { 180 | background-color: #ff655b; 181 | box-shadow: none; 182 | transition: 0.1s ease-in; 183 | } 184 | #photo { 185 | appearance: none; 186 | outline: none; 187 | margin: 6px; 188 | width: 30px; 189 | height: 30px; 190 | background: transparent; 191 | background-image: url('../assets/icons/photo.svg'); 192 | background-size: 20px; 193 | background-repeat: no-repeat; 194 | background-position: center; 195 | border: none; 196 | border-radius: 10px; 197 | cursor: pointer; 198 | transition: 0.1s ease-in; 199 | } 200 | #photo:hover { 201 | background-color: #7c7c7ceb; 202 | background-size: 22px; 203 | } 204 | #photo:active { 205 | background-color: #363636eb; 206 | background-size: 16px; 207 | } 208 | 209 | #color { 210 | background-image: url('../assets/icons/palette.svg'); 211 | background-color: black; 212 | } 213 | #size { 214 | background-size: 76%; 215 | } 216 | #pattern { 217 | background-image: url('../assets/icons/sq.svg'); 218 | } 219 | 220 | #prev { 221 | background-image: url('../assets/icons/chevron-left.svg'); 222 | } 223 | #next { 224 | background-image: url('../assets/icons/chevron-right.svg'); 225 | } 226 | 227 | #colors > button { 228 | margin: 6px 3px; 229 | width: 27px; 230 | height: 27px; 231 | border-radius: 100%; 232 | } 233 | #colors > button:active { 234 | border-radius: 40%; 235 | } 236 | #black { 237 | background-color: black; 238 | } 239 | #black:active { 240 | box-shadow: 4px 4px 0px 0px #7a7a7a44; 241 | } 242 | #red { 243 | background-color: #d01919; 244 | } 245 | #red:active { 246 | box-shadow: 4px 4px 0px 0px #ff6a6444; 247 | } 248 | #yellow { 249 | background-color: #eaae00; 250 | } 251 | #yellow:active { 252 | box-shadow: 4px 4px 0px 0px #ffde4b44; 253 | } 254 | #green { 255 | background-color: #16ab39; 256 | } 257 | #green:active { 258 | box-shadow: 4px 4px 0px 0px #2dff6144; 259 | } 260 | #blue { 261 | background-color: #1678c2; 262 | } 263 | #blue:active { 264 | box-shadow: 4px 4px 0px 0px #50a2ff44; 265 | } 266 | 267 | #size, 268 | #small, 269 | #medium, 270 | #large { 271 | background-image: url('../assets/icons/point.svg'); 272 | } 273 | #small { 274 | background-size: 50%; 275 | } 276 | #medium { 277 | background-size: 76%; 278 | } 279 | #large { 280 | background-size: 110%; 281 | } 282 | 283 | #none { 284 | background-image: url('../assets/icons/none.svg'); 285 | } 286 | #sq { 287 | background-image: url('../assets/icons/sq.svg'); 288 | } 289 | #line { 290 | background-image: url('../assets/icons/line.svg'); 291 | } 292 | #dot { 293 | background-image: url('../assets/icons/dot.svg'); 294 | } 295 | 296 | input[type='color'] { 297 | margin: 6px 3px; 298 | padding: 0; 299 | width: 27px; 300 | height: 27px; 301 | color: black; 302 | background-color: transparent; 303 | border: none; 304 | border-color: black; 305 | cursor: pointer; 306 | transition: 0.3s; 307 | } 308 | input[type='color']::-webkit-color-swatch-wrapper { 309 | padding: 0; 310 | transition: 0.3s; 311 | } 312 | input[type='color']::-webkit-color-swatch { 313 | background-image: url('../assets/icons/color-picker.svg'); 314 | background-size: 17px; 315 | background-repeat: no-repeat; 316 | background-position: center; 317 | border: none; 318 | border-radius: 100%; 319 | transition: 0.3s; 320 | } 321 | 322 | [data-tooltip] { 323 | position: relative; 324 | } 325 | [data-tooltip]::after { 326 | content: attr(data-tooltip); 327 | position: absolute; 328 | width: auto; 329 | top: -26px; 330 | left: -22px; 331 | background-color: #2525257a; 332 | border-radius: 12px; 333 | box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656; 334 | backdrop-filter: blur(4px); 335 | border: 1px solid #848484; 336 | font-family: 'M PLUS Medium 1c', 'Arial', sans-serif; 337 | font-size: 14px; 338 | color: white; 339 | white-space: nowrap; 340 | padding: 0.5em; 341 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3); 342 | pointer-events: none; 343 | opacity: 0; 344 | transition: 1s; 345 | transition-delay: 0s; 346 | } 347 | .topPanel > [data-tooltip]::after { 348 | left: 0px; 349 | } 350 | .topPanel > [data-tooltip]:hover::after { 351 | top: 36px; 352 | } 353 | [data-tooltip]:hover::after { 354 | opacity: 1; 355 | top: -50px; 356 | transition-delay: 2s; 357 | } 358 | 359 | @media (max-width: 610px) { 360 | .container { 361 | display: block; 362 | } 363 | .dockPanel { 364 | left: 0; 365 | right: 0; 366 | } 367 | .main-tools { 368 | overflow-x: auto; 369 | overflow-y: hidden; 370 | } 371 | [data-tooltip]::after { 372 | display: none; 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /js/draw.js: -------------------------------------------------------------------------------- 1 | function appendPath() { 2 | const path = scene.appendChild( 3 | document.createElementNS('http://www.w3.org/2000/svg', 'path') 4 | ) 5 | if (boardMode == 'pen') { 6 | path.setAttribute('stroke', colorOption) 7 | path.setAttribute('stroke-width', widthOption) 8 | } else if (boardMode == 'eraser') { 9 | path.setAttribute('stroke', 'white') 10 | path.setAttribute('stroke-width', widthOption * 5) 11 | } 12 | return path 13 | } 14 | function pointsToPath(points) { 15 | return ( 16 | 'M' + 17 | points 18 | .map(function (p) { 19 | return ( 20 | (p.x || p[0] || 0).toFixed(0) + 21 | ',' + 22 | (p.y || p[1] || 0).toFixed(0) 23 | ) 24 | }) 25 | .join('L') 26 | ) 27 | } 28 | 29 | let points 30 | let simplify2Path 31 | let lockDrawing = false 32 | 33 | board.onpointerdown = function (event) { 34 | if (!event.isPrimary) { 35 | lockDrawing = true 36 | simplify2Path.remove() 37 | } 38 | if ( 39 | event.button == 0 && 40 | !lockDrawing && 41 | (boardMode == 'pen' || boardMode == 'eraser') 42 | ) { 43 | points = [ 44 | [ 45 | (event.offsetX - transformX) / transformScale, 46 | (event.offsetY - transformY) / transformScale 47 | ] 48 | ] 49 | simplify2Path = appendPath() 50 | this.setPointerCapture(event.pointerId) 51 | } 52 | } 53 | board.onpointermove = function (event) { 54 | if ( 55 | this.hasPointerCapture(event.pointerId) && 56 | !lockDrawing && 57 | (boardMode == 'pen' || boardMode == 'eraser') 58 | ) { 59 | points.push([ 60 | (event.offsetX - transformX) / transformScale, 61 | (event.offsetY - transformY) / transformScale 62 | ]) 63 | const simplifyJsApplied = simplify( 64 | points.map(function (p) { 65 | return { x: p[0], y: p[1] } 66 | }, 2.5), 67 | true 68 | ) 69 | simplify2Path.setAttribute('d', pointsToPath(points)) 70 | // simplify2Path.setAttribute('d', simplifySvgPath(simplifyJsApplied.map(function (p) { return [p.x, p.y] }), { tolerance: 2.5, precision: 0 })) 71 | } 72 | if (!event.isPrimary || event.buttons == 4) { 73 | scene.style.willChange = 'transform' 74 | background.style.willChange = 'background-position, background-size' 75 | board.style.shapeRendering = 'optimizeSpeed' 76 | } 77 | } 78 | board.onpointerup = function (event) { 79 | if (event.button == 0 && !lockDrawing && boardMode == 'pen') { 80 | if (this.hasPointerCapture(event.pointerId)) { 81 | points.push([ 82 | (event.offsetX - transformX) / transformScale, 83 | (event.offsetY - transformY) / transformScale 84 | ]) 85 | const simplifyJsApplied = simplify( 86 | points.map(function (p) { 87 | return { x: p[0], y: p[1] } 88 | }, 2.5), 89 | true 90 | ) 91 | simplify2Path.setAttribute( 92 | 'd', 93 | simplifySvgPath( 94 | simplifyJsApplied.map(function (p) { 95 | return [p.x, p.y] 96 | }), 97 | { tolerance: 2.5, precision: 0 } 98 | ) 99 | ) 100 | // emitObject(simplify2Path) 101 | } 102 | } else if (event.button == 0 && !lockDrawing && boardMode == 'eraser') { 103 | if (this.hasPointerCapture(event.pointerId)) { 104 | points.push([ 105 | (event.offsetX - transformX) / transformScale, 106 | (event.offsetY - transformY) / transformScale 107 | ]) 108 | const simplifyJsApplied = simplify( 109 | points.map(function (p) { 110 | return { x: p[0], y: p[1] } 111 | }, 2.5), 112 | true 113 | ) 114 | simplify2Path.setAttribute( 115 | 'd', 116 | simplifySvgPath( 117 | simplifyJsApplied.map(function (p) { 118 | return [p.x, p.y] 119 | }), 120 | { tolerance: 0, precision: 0 } 121 | ) 122 | ) 123 | // emitObject(simplify2Path) 124 | } 125 | } 126 | scene.style.willChange = 'auto' 127 | background.style.willChange = 'auto' 128 | board.style.shapeRendering = 'geometricPrecision' 129 | setTimeout(() => { 130 | lockDrawing = false 131 | }, 10) 132 | } 133 | -------------------------------------------------------------------------------- /js/export.js: -------------------------------------------------------------------------------- 1 | // Export SVG 2 | const exportBoardAsSVG = () => { 3 | svgExport.downloadSvg(document.getElementById('board'), 'board', { 4 | width: 200, 5 | height: 200 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /js/functions.js: -------------------------------------------------------------------------------- 1 | // Сlear board 2 | function clearBoard() { 3 | board.style.transform = `scale(0.8)` 4 | board.style.opacity = 0 5 | setTimeout(() => { 6 | scene.innerHTML = '' 7 | board.style.opacity = 1 8 | board.style.transform = `scale(1)` 9 | }, 150) 10 | } 11 | document.addEventListener('keydown', function (event) { 12 | if (event.code == 'Delete') { 13 | clearBoard() 14 | } 15 | }) 16 | 17 | options = { 18 | color: 0, 19 | size: 1, 20 | pattern: 0 21 | } 22 | // Change pen colors by scrolling 23 | function scrollColor(event) { 24 | colorOptions = [ 25 | '#000000', 26 | '#d01919', 27 | '#eaae00', 28 | '#16ab39', 29 | '#1678c2', 30 | customColor 31 | ] 32 | if (event.deltaY > 0) { 33 | if (options.color < colorOptions.length - 1) { 34 | options.color += 1 35 | } 36 | } else { 37 | if (options.color) { 38 | options.color -= 1 39 | } 40 | } 41 | setBrush({ color: colorOptions[options.color] }) 42 | } 43 | // Change size of drawing subject by scrolling 44 | function scrollSize(event) { 45 | sizeOptions = [3, 4, 5] 46 | if (event.deltaY > 0) { 47 | if (options.size < sizeOptions.length - 1) { 48 | options.size += 1 49 | } 50 | } else { 51 | if (options.size) { 52 | options.size -= 1 53 | } 54 | } 55 | setBrush({ size: sizeOptions[options.size] }) 56 | } 57 | // Change board patterns by scrolling 58 | function scrollPattern(event) { 59 | patternOptions = ['none', 'sq', 'line', 'dot'] 60 | if (event.deltaY > 0) { 61 | if (options.pattern < patternOptions.length - 1) { 62 | options.pattern += 1 63 | } 64 | } else { 65 | if (options.pattern) { 66 | options.pattern -= 1 67 | } 68 | } 69 | setPattern(patternOptions[options.pattern]) 70 | } 71 | 72 | // Copy page link 73 | function copyLink() { 74 | navigator.clipboard 75 | .writeText(window.location.href) 76 | .then(() => { 77 | // Link copied successfully! 78 | }) 79 | .catch((err) => { 80 | console.log('Something went wrong', err) 81 | }) 82 | } 83 | document.addEventListener('keydown', function (event) { 84 | if (event.code == 'KeyC' && (event.ctrlKey || event.metaKey)) { 85 | copyLink() 86 | } 87 | }) 88 | 89 | // Effects 90 | // "Fade in" 91 | const fadeIn = (cl, timeout) => { 92 | let element = document.querySelector(cl) 93 | element.style.opacity = 0 94 | element.style.display = 'flex' 95 | element.style.transition = `opacity ${timeout}ms` 96 | setTimeout(() => { 97 | element.style.opacity = 1 98 | }, 10) 99 | } 100 | // "Fade out" 101 | const fadeOut = (cl, timeout) => { 102 | let element = document.querySelector(cl) 103 | element.style.opacity = 1 104 | element.style.transition = `opacity ${timeout}ms` 105 | element.style.opacity = 0 106 | 107 | setTimeout(() => { 108 | element.style.display = 'none' 109 | }, timeout) 110 | } 111 | -------------------------------------------------------------------------------- /js/lib/panzoom.js: -------------------------------------------------------------------------------- 1 | let transformX 2 | let transformY 3 | let transformScale 4 | 5 | 6 | (function (f) { if (typeof exports === "object" && typeof module !== "undefined") { module.exports = f() } else if (typeof define === "function" && define.amd) { define([], f) } else { var g; if (typeof window !== "undefined") { g = window } else if (typeof global !== "undefined") { g = global } else if (typeof self !== "undefined") { g = self } else { g = this } g.panzoom = f() } })(function () { 7 | var define, module, exports; return (function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a } var p = n[i] = { exports: {} }; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r) }, p, p.exports, r, e, n, t) } return n[i].exports } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)o(t[i]); return o } return r })()({ 8 | 1: [function (require, module, exports) { 9 | 'use strict'; 10 | /** 11 | * Allows to drag and zoom svg elements 12 | */ 13 | var wheel = require('wheel'); 14 | var animate = require('amator'); 15 | var eventify = require('ngraph.events'); 16 | var kinetic = require('./lib/kinetic.js'); 17 | var createTextSelectionInterceptor = require('./lib/createTextSelectionInterceptor.js'); 18 | var domTextSelectionInterceptor = createTextSelectionInterceptor(); 19 | var fakeTextSelectorInterceptor = createTextSelectionInterceptor(true); 20 | var Transform = require('./lib/transform.js'); 21 | var makeSvgController = require('./lib/svgController.js'); 22 | var makeDomController = require('./lib/domController.js'); 23 | 24 | var defaultZoomSpeed = 1; 25 | var defaultDoubleTapZoomSpeed = 1.75; 26 | var doubleTapSpeedInMS = 300; 27 | var clickEventTimeInMS = 200; 28 | 29 | module.exports = createPanZoom; 30 | 31 | /** 32 | * Creates a new instance of panzoom, so that an object can be panned and zoomed 33 | * 34 | * @param {DOMElement} domElement where panzoom should be attached. 35 | * @param {Object} options that configure behavior. 36 | */ 37 | function createPanZoom(domElement, options) { 38 | options = options || {}; 39 | 40 | var panController = options.controller; 41 | 42 | if (!panController) { 43 | if (makeSvgController.canAttach(domElement)) { 44 | panController = makeSvgController(domElement, options); 45 | } else if (makeDomController.canAttach(domElement)) { 46 | panController = makeDomController(domElement, options); 47 | } 48 | } 49 | 50 | if (!panController) { 51 | throw new Error( 52 | 'Cannot create panzoom for the current type of dom element' 53 | ); 54 | } 55 | var owner = panController.getOwner(); 56 | // just to avoid GC pressure, every time we do intermediate transform 57 | // we return this object. For internal use only. Never give it back to the consumer of this library 58 | var storedCTMResult = { x: 0, y: 0 }; 59 | 60 | var isDirty = false; 61 | var transform = new Transform(); 62 | 63 | if (panController.initTransform) { 64 | panController.initTransform(transform); 65 | } 66 | 67 | var filterKey = typeof options.filterKey === 'function' ? options.filterKey : noop; 68 | // TODO: likely need to unite pinchSpeed with zoomSpeed 69 | var pinchSpeed = typeof options.pinchSpeed === 'number' ? options.pinchSpeed : 1; 70 | var bounds = options.bounds; 71 | var maxZoom = typeof options.maxZoom === 'number' ? options.maxZoom : Number.POSITIVE_INFINITY; 72 | var minZoom = typeof options.minZoom === 'number' ? options.minZoom : 0; 73 | 74 | var boundsPadding = typeof options.boundsPadding === 'number' ? options.boundsPadding : 0.05; 75 | var zoomDoubleClickSpeed = typeof options.zoomDoubleClickSpeed === 'number' ? options.zoomDoubleClickSpeed : defaultDoubleTapZoomSpeed; 76 | var beforeWheel = options.beforeWheel || noop; 77 | var beforeMouseDown = options.beforeMouseDown || noop; 78 | var speed = typeof options.zoomSpeed === 'number' ? options.zoomSpeed : defaultZoomSpeed; 79 | var transformOrigin = parseTransformOrigin(options.transformOrigin); 80 | var textSelection = options.enableTextSelection ? fakeTextSelectorInterceptor : domTextSelectionInterceptor; 81 | 82 | validateBounds(bounds); 83 | 84 | if (options.autocenter) { 85 | autocenter(); 86 | } 87 | 88 | var frameAnimation; 89 | var lastTouchEndTime = 0; 90 | var lastTouchStartTime = 0; 91 | var pendingClickEventTimeout = 0; 92 | var lastMouseDownedEvent = null; 93 | var lastMouseDownTime = new Date(); 94 | var lastSingleFingerOffset; 95 | var touchInProgress = false; 96 | 97 | // We only need to fire panstart when actual move happens 98 | var panstartFired = false; 99 | 100 | // cache mouse coordinates here 101 | var mouseX; 102 | var mouseY; 103 | 104 | // Where the first click has happened, so that we can differentiate 105 | // between pan and click 106 | var clickX; 107 | var clickY; 108 | 109 | var pinchZoomLength; 110 | 111 | var smoothScroll; 112 | if ('smoothScroll' in options && !options.smoothScroll) { 113 | // If user explicitly asked us not to use smooth scrolling, we obey 114 | smoothScroll = rigidScroll(); 115 | } else { 116 | // otherwise we use forward smoothScroll settings to kinetic API 117 | // which makes scroll smoothing. 118 | smoothScroll = kinetic(getPoint, scroll, options.smoothScroll); 119 | } 120 | 121 | var moveByAnimation; 122 | var zoomToAnimation; 123 | 124 | var multiTouch; 125 | var paused = false; 126 | 127 | listenForEvents(); 128 | 129 | var api = { 130 | dispose: dispose, 131 | moveBy: internalMoveBy, 132 | moveTo: moveTo, 133 | smoothMoveTo: smoothMoveTo, 134 | centerOn: centerOn, 135 | zoomTo: publicZoomTo, 136 | zoomAbs: zoomAbs, 137 | smoothZoom: smoothZoom, 138 | smoothZoomAbs: smoothZoomAbs, 139 | showRectangle: showRectangle, 140 | 141 | pause: pause, 142 | resume: resume, 143 | isPaused: isPaused, 144 | 145 | getTransform: getTransformModel, 146 | 147 | getMinZoom: getMinZoom, 148 | setMinZoom: setMinZoom, 149 | 150 | getMaxZoom: getMaxZoom, 151 | setMaxZoom: setMaxZoom, 152 | 153 | getTransformOrigin: getTransformOrigin, 154 | setTransformOrigin: setTransformOrigin, 155 | 156 | getZoomSpeed: getZoomSpeed, 157 | setZoomSpeed: setZoomSpeed 158 | }; 159 | 160 | eventify(api); 161 | 162 | var initialX = typeof options.initialX === 'number' ? options.initialX : transform.x; 163 | var initialY = typeof options.initialY === 'number' ? options.initialY : transform.y; 164 | var initialZoom = typeof options.initialZoom === 'number' ? options.initialZoom : transform.scale; 165 | 166 | if (initialX != transform.x || initialY != transform.y || initialZoom != transform.scale) { 167 | zoomAbs(initialX, initialY, initialZoom); 168 | } 169 | 170 | return api; 171 | 172 | function pause() { 173 | releaseEvents(); 174 | paused = true; 175 | } 176 | 177 | function resume() { 178 | if (paused) { 179 | listenForEvents(); 180 | paused = false; 181 | } 182 | } 183 | 184 | function isPaused() { 185 | return paused; 186 | } 187 | 188 | function showRectangle(rect) { 189 | // TODO: this duplicates autocenter. I think autocenter should go. 190 | var clientRect = owner.getBoundingClientRect(); 191 | var size = transformToScreen(clientRect.width, clientRect.height); 192 | 193 | var rectWidth = rect.right - rect.left; 194 | var rectHeight = rect.bottom - rect.top; 195 | if (!Number.isFinite(rectWidth) || !Number.isFinite(rectHeight)) { 196 | throw new Error('Invalid rectangle'); 197 | } 198 | 199 | var dw = size.x / rectWidth; 200 | var dh = size.y / rectHeight; 201 | var scale = Math.min(dw, dh); 202 | transform.x = -(rect.left + rectWidth / 2) * scale + size.x / 2; 203 | transform.y = -(rect.top + rectHeight / 2) * scale + size.y / 2; 204 | transform.scale = scale; 205 | } 206 | 207 | function transformToScreen(x, y) { 208 | if (panController.getScreenCTM) { 209 | var parentCTM = panController.getScreenCTM(); 210 | var parentScaleX = parentCTM.a; 211 | var parentScaleY = parentCTM.d; 212 | var parentOffsetX = parentCTM.e; 213 | var parentOffsetY = parentCTM.f; 214 | storedCTMResult.x = x * parentScaleX - parentOffsetX; 215 | storedCTMResult.y = y * parentScaleY - parentOffsetY; 216 | } else { 217 | storedCTMResult.x = x; 218 | storedCTMResult.y = y; 219 | } 220 | 221 | return storedCTMResult; 222 | } 223 | 224 | function autocenter() { 225 | var w; // width of the parent 226 | var h; // height of the parent 227 | var left = 0; 228 | var top = 0; 229 | var sceneBoundingBox = getBoundingBox(); 230 | if (sceneBoundingBox) { 231 | // If we have bounding box - use it. 232 | left = sceneBoundingBox.left; 233 | top = sceneBoundingBox.top; 234 | w = sceneBoundingBox.right - sceneBoundingBox.left; 235 | h = sceneBoundingBox.bottom - sceneBoundingBox.top; 236 | } else { 237 | // otherwise just use whatever space we have 238 | var ownerRect = owner.getBoundingClientRect(); 239 | w = ownerRect.width; 240 | h = ownerRect.height; 241 | } 242 | var bbox = panController.getBBox(); 243 | if (bbox.width === 0 || bbox.height === 0) { 244 | // we probably do not have any elements in the SVG 245 | // just bail out; 246 | return; 247 | } 248 | var dh = h / bbox.height; 249 | var dw = w / bbox.width; 250 | var scale = Math.min(dw, dh); 251 | transform.x = -(bbox.left + bbox.width / 2) * scale + w / 2 + left; 252 | transform.y = -(bbox.top + bbox.height / 2) * scale + h / 2 + top; 253 | transform.scale = scale; 254 | } 255 | 256 | function getTransformModel() { 257 | // TODO: should this be read only? 258 | return transform; 259 | } 260 | 261 | function getMinZoom() { 262 | return minZoom; 263 | } 264 | 265 | function setMinZoom(newMinZoom) { 266 | minZoom = newMinZoom; 267 | } 268 | 269 | function getMaxZoom() { 270 | return maxZoom; 271 | } 272 | 273 | function setMaxZoom(newMaxZoom) { 274 | maxZoom = newMaxZoom; 275 | } 276 | 277 | function getTransformOrigin() { 278 | return transformOrigin; 279 | } 280 | 281 | function setTransformOrigin(newTransformOrigin) { 282 | transformOrigin = parseTransformOrigin(newTransformOrigin); 283 | } 284 | 285 | function getZoomSpeed() { 286 | return speed; 287 | } 288 | 289 | function setZoomSpeed(newSpeed) { 290 | if (!Number.isFinite(newSpeed)) { 291 | throw new Error('Zoom speed should be a number'); 292 | } 293 | speed = newSpeed; 294 | } 295 | 296 | function getPoint() { 297 | return { 298 | x: transform.x, 299 | y: transform.y 300 | }; 301 | } 302 | 303 | function moveTo(x, y) { 304 | transform.x = x; 305 | transform.y = y; 306 | 307 | keepTransformInsideBounds(); 308 | 309 | triggerEvent('pan'); 310 | makeDirty(); 311 | } 312 | 313 | function moveBy(dx, dy) { 314 | moveTo(transform.x + dx, transform.y + dy); 315 | } 316 | 317 | function keepTransformInsideBounds() { 318 | var boundingBox = getBoundingBox(); 319 | if (!boundingBox) return; 320 | 321 | var adjusted = false; 322 | var clientRect = getClientRect(); 323 | 324 | var diff = boundingBox.left - clientRect.right; 325 | if (diff > 0) { 326 | transform.x += diff; 327 | adjusted = true; 328 | } 329 | // check the other side: 330 | diff = boundingBox.right - clientRect.left; 331 | if (diff < 0) { 332 | transform.x += diff; 333 | adjusted = true; 334 | } 335 | 336 | // y axis: 337 | diff = boundingBox.top - clientRect.bottom; 338 | if (diff > 0) { 339 | // we adjust transform, so that it matches exactly our bounding box: 340 | // transform.y = boundingBox.top - (boundingBox.height + boundingBox.y) * transform.scale => 341 | // transform.y = boundingBox.top - (clientRect.bottom - transform.y) => 342 | // transform.y = diff + transform.y => 343 | transform.y += diff; 344 | adjusted = true; 345 | } 346 | 347 | diff = boundingBox.bottom - clientRect.top; 348 | if (diff < 0) { 349 | transform.y += diff; 350 | adjusted = true; 351 | } 352 | return adjusted; 353 | } 354 | 355 | /** 356 | * Returns bounding box that should be used to restrict scene movement. 357 | */ 358 | function getBoundingBox() { 359 | if (!bounds) return; // client does not want to restrict movement 360 | 361 | if (typeof bounds === 'boolean') { 362 | // for boolean type we use parent container bounds 363 | var ownerRect = owner.getBoundingClientRect(); 364 | var sceneWidth = ownerRect.width; 365 | var sceneHeight = ownerRect.height; 366 | 367 | return { 368 | left: sceneWidth * boundsPadding, 369 | top: sceneHeight * boundsPadding, 370 | right: sceneWidth * (1 - boundsPadding), 371 | bottom: sceneHeight * (1 - boundsPadding) 372 | }; 373 | } 374 | 375 | return bounds; 376 | } 377 | 378 | function getClientRect() { 379 | var bbox = panController.getBBox(); 380 | var leftTop = client(bbox.left, bbox.top); 381 | 382 | return { 383 | left: leftTop.x, 384 | top: leftTop.y, 385 | right: bbox.width * transform.scale + leftTop.x, 386 | bottom: bbox.height * transform.scale + leftTop.y 387 | }; 388 | } 389 | 390 | function client(x, y) { 391 | return { 392 | x: x * transform.scale + transform.x, 393 | y: y * transform.scale + transform.y 394 | }; 395 | } 396 | 397 | function makeDirty() { 398 | isDirty = true; 399 | frameAnimation = window.requestAnimationFrame(frame); 400 | } 401 | 402 | function zoomByRatio(clientX, clientY, ratio) { 403 | if (isNaN(clientX) || isNaN(clientY) || isNaN(ratio)) { 404 | throw new Error('zoom requires valid numbers'); 405 | } 406 | 407 | var newScale = transform.scale * ratio; 408 | 409 | if (newScale < minZoom) { 410 | if (transform.scale === minZoom) return; 411 | 412 | ratio = minZoom / transform.scale; 413 | } 414 | if (newScale > maxZoom) { 415 | if (transform.scale === maxZoom) return; 416 | 417 | ratio = maxZoom / transform.scale; 418 | } 419 | 420 | var size = transformToScreen(clientX, clientY); 421 | 422 | transform.x = size.x - ratio * (size.x - transform.x); 423 | transform.y = size.y - ratio * (size.y - transform.y); 424 | 425 | // TODO: https://github.com/anvaka/panzoom/issues/112 426 | if (bounds && boundsPadding === 1 && minZoom === 1) { 427 | transform.scale *= ratio; 428 | keepTransformInsideBounds(); 429 | } else { 430 | var transformAdjusted = keepTransformInsideBounds(); 431 | if (!transformAdjusted) transform.scale *= ratio; 432 | } 433 | 434 | triggerEvent('zoom'); 435 | 436 | makeDirty(); 437 | } 438 | 439 | function zoomAbs(clientX, clientY, zoomLevel) { 440 | var ratio = zoomLevel / transform.scale; 441 | zoomByRatio(clientX, clientY, ratio); 442 | } 443 | 444 | function centerOn(ui) { 445 | var parent = ui.ownerSVGElement; 446 | if (!parent) 447 | throw new Error('ui element is required to be within the scene'); 448 | 449 | // TODO: should i use controller's screen CTM? 450 | var clientRect = ui.getBoundingClientRect(); 451 | var cx = clientRect.left + clientRect.width / 2; 452 | var cy = clientRect.top + clientRect.height / 2; 453 | 454 | var container = parent.getBoundingClientRect(); 455 | var dx = container.width / 2 - cx; 456 | var dy = container.height / 2 - cy; 457 | 458 | internalMoveBy(dx, dy, true); 459 | } 460 | 461 | function smoothMoveTo(x, y) { 462 | internalMoveBy(x - transform.x, y - transform.y, true); 463 | } 464 | 465 | function internalMoveBy(dx, dy, smooth) { 466 | if (!smooth) { 467 | return moveBy(dx, dy); 468 | } 469 | 470 | if (moveByAnimation) moveByAnimation.cancel(); 471 | 472 | var from = { x: 0, y: 0 }; 473 | var to = { x: dx, y: dy }; 474 | var lastX = 0; 475 | var lastY = 0; 476 | 477 | moveByAnimation = animate(from, to, { 478 | step: function (v) { 479 | moveBy(v.x - lastX, v.y - lastY); 480 | 481 | lastX = v.x; 482 | lastY = v.y; 483 | } 484 | }); 485 | } 486 | 487 | function scroll(x, y) { 488 | cancelZoomAnimation(); 489 | moveTo(x, y); 490 | } 491 | 492 | function dispose() { 493 | releaseEvents(); 494 | } 495 | 496 | function listenForEvents() { 497 | owner.addEventListener('mousedown', onMouseDown, { passive: false }); 498 | owner.addEventListener('dblclick', onDoubleClick, { passive: false }); 499 | owner.addEventListener('touchstart', onTouch, { passive: false }); 500 | owner.addEventListener('keydown', onKeyDown, { passive: false }); 501 | 502 | // Need to listen on the owner container, so that we are not limited 503 | // by the size of the scrollable domElement 504 | wheel.addWheelListener(owner, onMouseWheel, { passive: false }); 505 | 506 | makeDirty(); 507 | } 508 | 509 | function releaseEvents() { 510 | wheel.removeWheelListener(owner, onMouseWheel); 511 | owner.removeEventListener('mousedown', onMouseDown); 512 | owner.removeEventListener('keydown', onKeyDown); 513 | owner.removeEventListener('dblclick', onDoubleClick); 514 | owner.removeEventListener('touchstart', onTouch); 515 | 516 | if (frameAnimation) { 517 | window.cancelAnimationFrame(frameAnimation); 518 | frameAnimation = 0; 519 | } 520 | 521 | smoothScroll.cancel(); 522 | 523 | releaseDocumentMouse(); 524 | releaseTouches(); 525 | textSelection.release(); 526 | 527 | triggerPanEnd(); 528 | } 529 | 530 | function frame() { 531 | if (isDirty) applyTransform(); 532 | } 533 | 534 | function applyTransform() { 535 | isDirty = false; 536 | 537 | // TODO: Should I allow to cancel this? 538 | panController.applyTransform(transform); 539 | 540 | triggerEvent('transform'); 541 | frameAnimation = 0; 542 | } 543 | 544 | function onKeyDown(e) { 545 | var x = 0, 546 | y = 0, 547 | z = 0; 548 | if (e.keyCode === 38) { 549 | y = 1; // up 550 | } else if (e.keyCode === 40) { 551 | y = -1; // down 552 | } else if (e.keyCode === 37) { 553 | x = 1; // left 554 | } else if (e.keyCode === 39) { 555 | x = -1; // right 556 | } else if (e.keyCode === 189 || e.keyCode === 109) { 557 | // DASH or SUBTRACT 558 | z = 1; // `-` - zoom out 559 | } else if (e.keyCode === 187 || e.keyCode === 107) { 560 | // EQUAL SIGN or ADD 561 | z = -1; // `=` - zoom in (equal sign on US layout is under `+`) 562 | } 563 | 564 | if (filterKey(e, x, y, z)) { 565 | // They don't want us to handle the key: https://github.com/anvaka/panzoom/issues/45 566 | return; 567 | } 568 | 569 | if (x || y) { 570 | e.preventDefault(); 571 | e.stopPropagation(); 572 | 573 | var clientRect = owner.getBoundingClientRect(); 574 | // movement speed should be the same in both X and Y direction: 575 | var offset = Math.min(clientRect.width, clientRect.height); 576 | var moveSpeedRatio = 0.05; 577 | var dx = offset * moveSpeedRatio * x; 578 | var dy = offset * moveSpeedRatio * y; 579 | 580 | // TODO: currently we do not animate this. It could be better to have animation 581 | internalMoveBy(dx, dy); 582 | } 583 | 584 | if (z) { 585 | var scaleMultiplier = getScaleMultiplier(z * 100); 586 | var offset = transformOrigin ? getTransformOriginOffset() : midPoint(); 587 | publicZoomTo(offset.x, offset.y, scaleMultiplier); 588 | } 589 | } 590 | 591 | function midPoint() { 592 | var ownerRect = owner.getBoundingClientRect(); 593 | return { 594 | x: ownerRect.width / 2, 595 | y: ownerRect.height / 2 596 | }; 597 | } 598 | 599 | function onTouch(e) { 600 | // let them override the touch behavior 601 | beforeTouch(e); 602 | clearPendingClickEventTimeout(); 603 | 604 | if (e.touches.length === 2) { 605 | handleFingersTouch(e); 606 | // handleTouchMove() will care about pinch zoom. 607 | pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]); 608 | multiTouch = true; 609 | startTouchListenerIfNeeded(); 610 | } 611 | } 612 | 613 | function beforeTouch(e) { 614 | // TODO: Need to unify this filtering names. E.g. use `beforeTouch` 615 | if (options.onTouch && !options.onTouch(e)) { 616 | // if they return `false` from onTouch, we don't want to stop 617 | // events propagation. Fixes https://github.com/anvaka/panzoom/issues/12 618 | return; 619 | } 620 | 621 | e.stopPropagation(); 622 | e.preventDefault(); 623 | } 624 | 625 | function beforeDoubleClick(e) { 626 | clearPendingClickEventTimeout(); 627 | 628 | // TODO: Need to unify this filtering names. E.g. use `beforeDoubleClick`` 629 | if (options.onDoubleClick && !options.onDoubleClick(e)) { 630 | // if they return `false` from onTouch, we don't want to stop 631 | // events propagation. Fixes https://github.com/anvaka/panzoom/issues/46 632 | return; 633 | } 634 | 635 | e.preventDefault(); 636 | e.stopPropagation(); 637 | } 638 | 639 | function handleFingersTouch(e) { 640 | lastTouchStartTime = new Date(); 641 | 642 | var touch1 = e.touches[0]; 643 | var touch2 = e.touches[1]; 644 | 645 | var offset1 = getOffsetXY(touch1); 646 | var offset2 = getOffsetXY(touch2); 647 | var offset = {x: (offset1.x + offset2.x) / 2, y: (offset1.y + offset2.y) / 2} 648 | lastSingleFingerOffset = offset; 649 | 650 | var point = transformToScreen(offset.x, offset.y); 651 | 652 | mouseX = point.x; 653 | mouseY = point.y; 654 | 655 | clickX = mouseX; 656 | clickY = mouseY; 657 | 658 | smoothScroll.cancel(); 659 | startTouchListenerIfNeeded(); 660 | } 661 | 662 | function startTouchListenerIfNeeded() { 663 | if (touchInProgress) { 664 | // no need to do anything, as we already listen to events; 665 | return; 666 | } 667 | 668 | touchInProgress = true; 669 | document.addEventListener('touchmove', handleTouchMove); 670 | document.addEventListener('touchend', handleTouchEnd); 671 | document.addEventListener('touchcancel', handleTouchEnd); 672 | } 673 | 674 | function handleTouchMove(e) { 675 | if (e.touches.length === 2) { 676 | multiTouch = true; 677 | //it's two finger touch, we need to move first; and keep the mouseX/Y for move; 678 | var touch1 = e.touches[0]; 679 | var touch2 = e.touches[1]; 680 | 681 | var offset1 = getOffsetXY(touch1); 682 | var offset2 = getOffsetXY(touch2); 683 | var offset = {x: (offset1.x + offset2.x) / 2, y: (offset1.y + offset2.y) / 2} 684 | 685 | var point = transformToScreen(offset.x, offset.y); 686 | 687 | var dx = point.x - mouseX; 688 | var dy = point.y - mouseY; 689 | 690 | if (dx !== 0 && dy !== 0) { 691 | triggerPanStart(); 692 | } 693 | mouseX = offset.x; 694 | mouseY = offset.y; 695 | 696 | internalMoveBy(dx, dy); 697 | //move code up 698 | 699 | // it's a zoom, let's find direction 700 | //Then let's start to move, caclulate the zoom, 701 | 702 | var currentPinchLength = getPinchZoomLength(touch1, touch2); 703 | 704 | // since the zoom speed is always based on distance from 1, we need to apply 705 | // pinch speed only on that distance from 1: 706 | var scaleMultiplier = 707 | 1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed; 708 | 709 | if (transformOrigin) { 710 | // console.log("transform origin"); 711 | var offset = getTransformOriginOffset(); 712 | mouseX = offset.x; 713 | mouseY = offset.y; 714 | } 715 | 716 | publicZoomTo(mouseX, mouseY, scaleMultiplier); 717 | 718 | pinchZoomLength = currentPinchLength; 719 | e.stopPropagation(); 720 | // e.preventDefault(); 721 | } 722 | } 723 | 724 | function clearPendingClickEventTimeout() { 725 | if (pendingClickEventTimeout) { 726 | clearTimeout(pendingClickEventTimeout); 727 | pendingClickEventTimeout = 0; 728 | } 729 | } 730 | 731 | function handlePotentialClickEvent(e) { 732 | // we could still be in the double tap mode, let's wait until double tap expires, 733 | // and then notify: 734 | if (!options.onClick) return; 735 | clearPendingClickEventTimeout(); 736 | var dx = mouseX - clickX; 737 | var dy = mouseY - clickY; 738 | var l = Math.sqrt(dx * dx + dy * dy); 739 | if (l > 5) return; // probably they are panning, ignore it 740 | 741 | pendingClickEventTimeout = setTimeout(function () { 742 | pendingClickEventTimeout = 0; 743 | options.onClick(e); 744 | }, doubleTapSpeedInMS); 745 | } 746 | 747 | function handleTouchEnd(e) { 748 | clearPendingClickEventTimeout(); 749 | if (e.touches.length > 0) { 750 | var offset = getOffsetXY(e.touches[0]); 751 | var point = transformToScreen(offset.x, offset.y); 752 | mouseX = point.x; 753 | mouseY = point.y; 754 | } else { 755 | var now = new Date(); 756 | if (now - lastTouchEndTime < doubleTapSpeedInMS) { 757 | // They did a double tap here 758 | if (transformOrigin) { 759 | var offset = getTransformOriginOffset(); 760 | smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); 761 | } else { 762 | // We want untransformed x/y here. 763 | smoothZoom(lastSingleFingerOffset.x, lastSingleFingerOffset.y, zoomDoubleClickSpeed); 764 | } 765 | } else if (now - lastTouchStartTime < clickEventTimeInMS) { 766 | handlePotentialClickEvent(e); 767 | } 768 | 769 | lastTouchEndTime = now; 770 | 771 | triggerPanEnd(); 772 | releaseTouches(); 773 | } 774 | } 775 | 776 | function getPinchZoomLength(finger1, finger2) { 777 | var dx = finger1.clientX - finger2.clientX; 778 | var dy = finger1.clientY - finger2.clientY; 779 | return Math.sqrt(dx * dx + dy * dy); 780 | } 781 | 782 | function onDoubleClick(e) { 783 | beforeDoubleClick(e); 784 | var offset = getOffsetXY(e); 785 | if (transformOrigin) { 786 | // TODO: looks like this is duplicated in the file. 787 | // Need to refactor 788 | offset = getTransformOriginOffset(); 789 | } 790 | smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); 791 | } 792 | 793 | function onMouseDown(e) { 794 | clearPendingClickEventTimeout(); 795 | 796 | // if client does not want to handle this event - just ignore the call 797 | if (beforeMouseDown(e)) return; 798 | 799 | lastMouseDownedEvent = e; 800 | lastMouseDownTime = new Date(); 801 | 802 | if (touchInProgress) { 803 | // modern browsers will fire mousedown for touch events too 804 | // we do not want this: touch is handled separately. 805 | e.stopPropagation(); 806 | return false; 807 | } 808 | // for IE, left click == 1 809 | // for Firefox, left click == 0 810 | var isLeftButton = 811 | (e.button === 1 && window.event !== null) || e.button === 0; 812 | if (!isLeftButton) return; 813 | 814 | smoothScroll.cancel(); 815 | 816 | var offset = getOffsetXY(e); 817 | var point = transformToScreen(offset.x, offset.y); 818 | clickX = mouseX = point.x; 819 | clickY = mouseY = point.y; 820 | 821 | // We need to listen on document itself, since mouse can go outside of the 822 | // window, and we will loose it 823 | document.addEventListener('mousemove', onMouseMove); 824 | document.addEventListener('mouseup', onMouseUp); 825 | textSelection.capture(e.target || e.srcElement); 826 | 827 | return false; 828 | } 829 | 830 | function onMouseMove(e) { 831 | // no need to worry about mouse events when touch is happening 832 | if (touchInProgress) return; 833 | 834 | triggerPanStart(); 835 | 836 | var offset = getOffsetXY(e); 837 | var point = transformToScreen(offset.x, offset.y); 838 | var dx = point.x - mouseX; 839 | var dy = point.y - mouseY; 840 | 841 | mouseX = point.x; 842 | mouseY = point.y; 843 | 844 | internalMoveBy(dx, dy); 845 | } 846 | 847 | function onMouseUp() { 848 | var now = new Date(); 849 | if (now - lastMouseDownTime < clickEventTimeInMS) handlePotentialClickEvent(lastMouseDownedEvent); 850 | textSelection.release(); 851 | triggerPanEnd(); 852 | releaseDocumentMouse(); 853 | } 854 | 855 | function releaseDocumentMouse() { 856 | document.removeEventListener('mousemove', onMouseMove); 857 | document.removeEventListener('mouseup', onMouseUp); 858 | panstartFired = false; 859 | } 860 | 861 | function releaseTouches() { 862 | document.removeEventListener('touchmove', handleTouchMove); 863 | document.removeEventListener('touchend', handleTouchEnd); 864 | document.removeEventListener('touchcancel', handleTouchEnd); 865 | panstartFired = false; 866 | multiTouch = false; 867 | touchInProgress = false; 868 | } 869 | 870 | function onMouseWheel(e) { 871 | // if client does not want to handle this event - just ignore the call 872 | if (beforeWheel(e)) return; 873 | 874 | smoothScroll.cancel(); 875 | 876 | var delta = e.deltaY; 877 | if (e.deltaMode > 0) delta *= 100; 878 | 879 | var scaleMultiplier = getScaleMultiplier(delta); 880 | 881 | if (scaleMultiplier !== 1) { 882 | var offset = transformOrigin 883 | ? getTransformOriginOffset() 884 | : getOffsetXY(e); 885 | publicZoomTo(offset.x, offset.y, scaleMultiplier); 886 | e.preventDefault(); 887 | } 888 | } 889 | 890 | function getOffsetXY(e) { 891 | var offsetX, offsetY; 892 | // I tried using e.offsetX, but that gives wrong results for svg, when user clicks on a path. 893 | var ownerRect = owner.getBoundingClientRect(); 894 | offsetX = e.clientX - ownerRect.left; 895 | offsetY = e.clientY - ownerRect.top; 896 | 897 | return { x: offsetX, y: offsetY }; 898 | } 899 | 900 | function smoothZoom(clientX, clientY, scaleMultiplier) { 901 | var fromValue = transform.scale; 902 | var from = { scale: fromValue }; 903 | var to = { scale: scaleMultiplier * fromValue }; 904 | 905 | smoothScroll.cancel(); 906 | cancelZoomAnimation(); 907 | 908 | zoomToAnimation = animate(from, to, { 909 | step: function (v) { 910 | zoomAbs(clientX, clientY, v.scale); 911 | }, 912 | done: triggerZoomEnd 913 | }); 914 | } 915 | 916 | function smoothZoomAbs(clientX, clientY, toScaleValue) { 917 | var fromValue = transform.scale; 918 | var from = { scale: fromValue }; 919 | var to = { scale: toScaleValue }; 920 | 921 | smoothScroll.cancel(); 922 | cancelZoomAnimation(); 923 | 924 | zoomToAnimation = animate(from, to, { 925 | step: function (v) { 926 | zoomAbs(clientX, clientY, v.scale); 927 | } 928 | }); 929 | } 930 | 931 | function getTransformOriginOffset() { 932 | var ownerRect = owner.getBoundingClientRect(); 933 | return { 934 | x: ownerRect.width * transformOrigin.x, 935 | y: ownerRect.height * transformOrigin.y 936 | }; 937 | } 938 | 939 | function publicZoomTo(clientX, clientY, scaleMultiplier) { 940 | smoothScroll.cancel(); 941 | cancelZoomAnimation(); 942 | return zoomByRatio(clientX, clientY, scaleMultiplier); 943 | } 944 | 945 | function cancelZoomAnimation() { 946 | if (zoomToAnimation) { 947 | zoomToAnimation.cancel(); 948 | zoomToAnimation = null; 949 | } 950 | } 951 | 952 | function getScaleMultiplier(delta) { 953 | var sign = Math.sign(delta); 954 | var deltaAdjustedSpeed = Math.min(0.25, Math.abs(speed * delta / 128)); 955 | return 1 - sign * deltaAdjustedSpeed; 956 | } 957 | 958 | function triggerPanStart() { 959 | if (!panstartFired) { 960 | triggerEvent('panstart'); 961 | panstartFired = true; 962 | smoothScroll.start(); 963 | } 964 | } 965 | 966 | function triggerPanEnd() { 967 | if (panstartFired) { 968 | // we should never run smooth scrolling if it was multiTouch (pinch zoom animation): 969 | if (!multiTouch) smoothScroll.stop(); 970 | triggerEvent('panend'); 971 | } 972 | } 973 | 974 | function triggerZoomEnd() { 975 | triggerEvent('zoomend'); 976 | } 977 | 978 | function triggerEvent(name) { 979 | api.fire(name, api); 980 | } 981 | } 982 | 983 | function parseTransformOrigin(options) { 984 | if (!options) return; 985 | if (typeof options === 'object') { 986 | if (!isNumber(options.x) || !isNumber(options.y)) 987 | failTransformOrigin(options); 988 | return options; 989 | } 990 | 991 | failTransformOrigin(); 992 | } 993 | 994 | function failTransformOrigin(options) { 995 | console.error(options); 996 | throw new Error( 997 | [ 998 | 'Cannot parse transform origin.', 999 | 'Some good examples:', 1000 | ' "center center" can be achieved with {x: 0.5, y: 0.5}', 1001 | ' "top center" can be achieved with {x: 0.5, y: 0}', 1002 | ' "bottom right" can be achieved with {x: 1, y: 1}' 1003 | ].join('\n') 1004 | ); 1005 | } 1006 | 1007 | function noop() { } 1008 | 1009 | function validateBounds(bounds) { 1010 | var boundsType = typeof bounds; 1011 | if (boundsType === 'undefined' || boundsType === 'boolean') return; // this is okay 1012 | // otherwise need to be more thorough: 1013 | var validBounds = 1014 | isNumber(bounds.left) && 1015 | isNumber(bounds.top) && 1016 | isNumber(bounds.bottom) && 1017 | isNumber(bounds.right); 1018 | 1019 | if (!validBounds) 1020 | throw new Error( 1021 | 'Bounds object is not valid. It can be: ' + 1022 | 'undefined, boolean (true|false) or an object {left, top, right, bottom}' 1023 | ); 1024 | } 1025 | 1026 | function isNumber(x) { 1027 | return Number.isFinite(x); 1028 | } 1029 | 1030 | // IE 11 does not support isNaN: 1031 | function isNaN(value) { 1032 | if (Number.isNaN) { 1033 | return Number.isNaN(value); 1034 | } 1035 | 1036 | return value !== value; 1037 | } 1038 | 1039 | function rigidScroll() { 1040 | return { 1041 | start: noop, 1042 | stop: noop, 1043 | cancel: noop 1044 | }; 1045 | } 1046 | 1047 | function autoRun() { 1048 | if (typeof document === 'undefined') return; 1049 | 1050 | var scripts = document.getElementsByTagName('script'); 1051 | if (!scripts) return; 1052 | var panzoomScript; 1053 | 1054 | for (var i = 0; i < scripts.length; ++i) { 1055 | var x = scripts[i]; 1056 | if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { 1057 | panzoomScript = x; 1058 | break; 1059 | } 1060 | } 1061 | 1062 | if (!panzoomScript) return; 1063 | 1064 | var query = panzoomScript.getAttribute('query'); 1065 | if (!query) return; 1066 | 1067 | var globalName = panzoomScript.getAttribute('name') || 'pz'; 1068 | var started = Date.now(); 1069 | 1070 | tryAttach(); 1071 | 1072 | function tryAttach() { 1073 | var el = document.querySelector(query); 1074 | if (!el) { 1075 | var now = Date.now(); 1076 | var elapsed = now - started; 1077 | if (elapsed < 2000) { 1078 | // Let's wait a bit 1079 | setTimeout(tryAttach, 100); 1080 | return; 1081 | } 1082 | // If we don't attach within 2 seconds to the target element, consider it a failure 1083 | console.error('Cannot find the panzoom element', globalName); 1084 | return; 1085 | } 1086 | var options = collectOptions(panzoomScript); 1087 | console.log(options); 1088 | window[globalName] = createPanZoom(el, options); 1089 | } 1090 | 1091 | function collectOptions(script) { 1092 | var attrs = script.attributes; 1093 | var options = {}; 1094 | for (var j = 0; j < attrs.length; ++j) { 1095 | var attr = attrs[j]; 1096 | var nameValue = getPanzoomAttributeNameValue(attr); 1097 | if (nameValue) { 1098 | options[nameValue.name] = nameValue.value; 1099 | } 1100 | } 1101 | 1102 | return options; 1103 | } 1104 | 1105 | function getPanzoomAttributeNameValue(attr) { 1106 | if (!attr.name) return; 1107 | var isPanZoomAttribute = 1108 | attr.name[0] === 'p' && attr.name[1] === 'z' && attr.name[2] === '-'; 1109 | 1110 | if (!isPanZoomAttribute) return; 1111 | 1112 | var name = attr.name.substr(3); 1113 | var value = JSON.parse(attr.value); 1114 | return { name: name, value: value }; 1115 | } 1116 | } 1117 | 1118 | autoRun(); 1119 | 1120 | }, { "./lib/createTextSelectionInterceptor.js": 2, "./lib/domController.js": 3, "./lib/kinetic.js": 4, "./lib/svgController.js": 5, "./lib/transform.js": 6, "amator": 7, "ngraph.events": 9, "wheel": 10 }], 2: [function (require, module, exports) { 1121 | /** 1122 | * Disallows selecting text. 1123 | */ 1124 | module.exports = createTextSelectionInterceptor; 1125 | 1126 | function createTextSelectionInterceptor(useFake) { 1127 | if (useFake) { 1128 | return { 1129 | capture: noop, 1130 | release: noop 1131 | }; 1132 | } 1133 | 1134 | var dragObject; 1135 | var prevSelectStart; 1136 | var prevDragStart; 1137 | var wasCaptured = false; 1138 | 1139 | return { 1140 | capture: capture, 1141 | release: release 1142 | }; 1143 | 1144 | function capture(domObject) { 1145 | wasCaptured = true; 1146 | prevSelectStart = window.document.onselectstart; 1147 | prevDragStart = window.document.ondragstart; 1148 | 1149 | window.document.onselectstart = disabled; 1150 | 1151 | dragObject = domObject; 1152 | dragObject.ondragstart = disabled; 1153 | } 1154 | 1155 | function release() { 1156 | if (!wasCaptured) return; 1157 | 1158 | wasCaptured = false; 1159 | window.document.onselectstart = prevSelectStart; 1160 | if (dragObject) dragObject.ondragstart = prevDragStart; 1161 | } 1162 | } 1163 | 1164 | function disabled(e) { 1165 | e.stopPropagation(); 1166 | return false; 1167 | } 1168 | 1169 | function noop() { } 1170 | 1171 | }, {}], 3: [function (require, module, exports) { 1172 | module.exports = makeDomController; 1173 | 1174 | module.exports.canAttach = isDomElement; 1175 | 1176 | function makeDomController(domElement, options) { 1177 | var elementValid = isDomElement(domElement); 1178 | if (!elementValid) { 1179 | throw new Error('panzoom requires DOM element to be attached to the DOM tree'); 1180 | } 1181 | 1182 | var owner = domElement.parentElement; 1183 | domElement.scrollTop = 0; 1184 | 1185 | if (!options.disableKeyboardInteraction) { 1186 | owner.setAttribute('tabindex', 0); 1187 | } 1188 | 1189 | var api = { 1190 | getBBox: getBBox, 1191 | getOwner: getOwner, 1192 | applyTransform: applyTransform, 1193 | }; 1194 | 1195 | return api; 1196 | 1197 | function getOwner() { 1198 | return owner; 1199 | } 1200 | 1201 | function getBBox() { 1202 | // TODO: We should probably cache this? 1203 | return { 1204 | left: 0, 1205 | top: 0, 1206 | width: domElement.clientWidth, 1207 | height: domElement.clientHeight 1208 | }; 1209 | } 1210 | 1211 | function applyTransform(transform) { 1212 | // TODO: Should we cache this? 1213 | domElement.style.transformOrigin = '0 0 0'; 1214 | domElement.style.transform = 'matrix(' + 1215 | transform.scale.toFixed(4) + ', 0, 0, ' + 1216 | transform.scale.toFixed(4) + ', ' + 1217 | transform.x.toFixed(4) + ', ' + transform.y.toFixed(4) + ')'; 1218 | } 1219 | } 1220 | 1221 | function isDomElement(element) { 1222 | return element && element.parentElement && element.style; 1223 | } 1224 | 1225 | }, {}], 4: [function (require, module, exports) { 1226 | /** 1227 | * Allows smooth kinetic scrolling of the surface 1228 | */ 1229 | module.exports = kinetic; 1230 | 1231 | function kinetic(getPoint, scroll, settings) { 1232 | if (typeof settings !== 'object') { 1233 | // setting could come as boolean, we should ignore it, and use an object. 1234 | settings = {}; 1235 | } 1236 | 1237 | var minVelocity = typeof settings.minVelocity === 'number' ? settings.minVelocity : 5; 1238 | var amplitude = typeof settings.amplitude === 'number' ? settings.amplitude : 0.25; 1239 | var cancelAnimationFrame = typeof settings.cancelAnimationFrame === 'function' ? settings.cancelAnimationFrame : getCancelAnimationFrame(); 1240 | var requestAnimationFrame = typeof settings.requestAnimationFrame === 'function' ? settings.requestAnimationFrame : getRequestAnimationFrame(); 1241 | 1242 | var lastPoint; 1243 | var timestamp; 1244 | var timeConstant = 342; 1245 | 1246 | var ticker; 1247 | var vx, targetX, ax; 1248 | var vy, targetY, ay; 1249 | 1250 | var raf; 1251 | 1252 | return { 1253 | start: start, 1254 | stop: stop, 1255 | cancel: dispose 1256 | }; 1257 | 1258 | function dispose() { 1259 | cancelAnimationFrame(ticker); 1260 | cancelAnimationFrame(raf); 1261 | } 1262 | 1263 | function start() { 1264 | lastPoint = getPoint(); 1265 | 1266 | ax = ay = vx = vy = 0; 1267 | timestamp = new Date(); 1268 | 1269 | cancelAnimationFrame(ticker); 1270 | cancelAnimationFrame(raf); 1271 | 1272 | // we start polling the point position to accumulate velocity 1273 | // Once we stop(), we will use accumulated velocity to keep scrolling 1274 | // an object. 1275 | ticker = requestAnimationFrame(track); 1276 | } 1277 | 1278 | function track() { 1279 | var now = Date.now(); 1280 | var elapsed = now - timestamp; 1281 | timestamp = now; 1282 | 1283 | var currentPoint = getPoint(); 1284 | 1285 | var dx = currentPoint.x - lastPoint.x; 1286 | var dy = currentPoint.y - lastPoint.y; 1287 | 1288 | lastPoint = currentPoint; 1289 | 1290 | var dt = 1000 / (1 + elapsed); 1291 | 1292 | // moving average 1293 | vx = 0.8 * dx * dt + 0.2 * vx; 1294 | vy = 0.8 * dy * dt + 0.2 * vy; 1295 | 1296 | ticker = requestAnimationFrame(track); 1297 | } 1298 | 1299 | function stop() { 1300 | cancelAnimationFrame(ticker); 1301 | cancelAnimationFrame(raf); 1302 | 1303 | var currentPoint = getPoint(); 1304 | 1305 | targetX = currentPoint.x; 1306 | targetY = currentPoint.y; 1307 | timestamp = Date.now(); 1308 | 1309 | if (vx < -minVelocity || vx > minVelocity) { 1310 | ax = amplitude * vx; 1311 | targetX += ax; 1312 | } 1313 | 1314 | if (vy < -minVelocity || vy > minVelocity) { 1315 | ay = amplitude * vy; 1316 | targetY += ay; 1317 | } 1318 | 1319 | raf = requestAnimationFrame(autoScroll); 1320 | } 1321 | 1322 | function autoScroll() { 1323 | var elapsed = Date.now() - timestamp; 1324 | 1325 | var moving = false; 1326 | var dx = 0; 1327 | var dy = 0; 1328 | 1329 | if (ax) { 1330 | dx = -ax * Math.exp(-elapsed / timeConstant); 1331 | 1332 | if (dx > 0.5 || dx < -0.5) moving = true; 1333 | else dx = ax = 0; 1334 | } 1335 | 1336 | if (ay) { 1337 | dy = -ay * Math.exp(-elapsed / timeConstant); 1338 | 1339 | if (dy > 0.5 || dy < -0.5) moving = true; 1340 | else dy = ay = 0; 1341 | } 1342 | 1343 | if (moving) { 1344 | scroll(targetX + dx, targetY + dy); 1345 | raf = requestAnimationFrame(autoScroll); 1346 | } 1347 | } 1348 | } 1349 | 1350 | function getCancelAnimationFrame() { 1351 | if (typeof cancelAnimationFrame === 'function') return cancelAnimationFrame; 1352 | return clearTimeout; 1353 | } 1354 | 1355 | function getRequestAnimationFrame() { 1356 | if (typeof requestAnimationFrame === 'function') return requestAnimationFrame; 1357 | 1358 | return function (handler) { 1359 | return setTimeout(handler, 16); 1360 | }; 1361 | } 1362 | }, {}], 5: [function (require, module, exports) { 1363 | module.exports = makeSvgController; 1364 | module.exports.canAttach = isSVGElement; 1365 | 1366 | function makeSvgController(svgElement, options) { 1367 | if (!isSVGElement(svgElement)) { 1368 | throw new Error('svg element is required for svg.panzoom to work'); 1369 | } 1370 | 1371 | var owner = svgElement.ownerSVGElement; 1372 | if (!owner) { 1373 | throw new Error( 1374 | 'Do not apply panzoom to the root element. ' + 1375 | 'Use its child instead (e.g. ). ' + 1376 | 'As of March 2016 only FireFox supported transform on the root element'); 1377 | } 1378 | 1379 | if (!options.disableKeyboardInteraction) { 1380 | owner.setAttribute('tabindex', 0); 1381 | } 1382 | 1383 | var api = { 1384 | getBBox: getBBox, 1385 | getScreenCTM: getScreenCTM, 1386 | getOwner: getOwner, 1387 | applyTransform: applyTransform, 1388 | initTransform: initTransform 1389 | }; 1390 | 1391 | return api; 1392 | 1393 | function getOwner() { 1394 | return owner; 1395 | } 1396 | 1397 | function getBBox() { 1398 | var bbox = svgElement.getBBox(); 1399 | return { 1400 | left: bbox.x, 1401 | top: bbox.y, 1402 | width: bbox.width, 1403 | height: bbox.height, 1404 | }; 1405 | } 1406 | 1407 | function getScreenCTM() { 1408 | var ctm = owner.getCTM(); 1409 | if (!ctm) { 1410 | // This is likely firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=873106 1411 | // The code below is not entirely correct, but still better than nothing 1412 | return owner.getScreenCTM(); 1413 | } 1414 | return ctm; 1415 | } 1416 | 1417 | function initTransform(transform) { 1418 | var screenCTM = svgElement.getCTM(); 1419 | 1420 | // The above line returns null on Firefox 1421 | if (screenCTM === null) { 1422 | screenCTM = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix(); 1423 | } 1424 | 1425 | transform.x = screenCTM.e; 1426 | transform.y = screenCTM.f; 1427 | transform.scale = screenCTM.a; 1428 | owner.removeAttributeNS(null, 'viewBox'); 1429 | } 1430 | 1431 | function applyTransform(transform) { 1432 | transformX = transform.x.toFixed(4) 1433 | transformY = transform.y.toFixed(4) 1434 | transformScale = transform.scale.toFixed(4) 1435 | svgElement.style.transform = `matrix(${transformScale}, 0, 0, ${transformScale}, ${transformX}, ${transformY})` 1436 | } 1437 | } 1438 | 1439 | function isSVGElement(element) { 1440 | return element && element.ownerSVGElement && element.getCTM; 1441 | } 1442 | }, {}], 6: [function (require, module, exports) { 1443 | module.exports = Transform; 1444 | 1445 | function Transform() { 1446 | this.x = 0; 1447 | this.y = 0; 1448 | this.scale = 1; 1449 | } 1450 | 1451 | }, {}], 7: [function (require, module, exports) { 1452 | var BezierEasing = require('bezier-easing') 1453 | 1454 | // Predefined set of animations. Similar to CSS easing functions 1455 | var animations = { 1456 | ease: BezierEasing(0.25, 0.1, 0.25, 1), 1457 | easeIn: BezierEasing(0.42, 0, 1, 1), 1458 | easeOut: BezierEasing(0, 0, 0.58, 1), 1459 | easeInOut: BezierEasing(0.42, 0, 0.58, 1), 1460 | linear: BezierEasing(0, 0, 1, 1) 1461 | } 1462 | 1463 | 1464 | module.exports = animate; 1465 | module.exports.makeAggregateRaf = makeAggregateRaf; 1466 | module.exports.sharedScheduler = makeAggregateRaf(); 1467 | 1468 | 1469 | function animate(source, target, options) { 1470 | var start = Object.create(null) 1471 | var diff = Object.create(null) 1472 | options = options || {} 1473 | // We let clients specify their own easing function 1474 | var easing = (typeof options.easing === 'function') ? options.easing : animations[options.easing] 1475 | 1476 | // if nothing is specified, default to ease (similar to CSS animations) 1477 | if (!easing) { 1478 | if (options.easing) { 1479 | console.warn('Unknown easing function in amator: ' + options.easing); 1480 | } 1481 | easing = animations.ease 1482 | } 1483 | 1484 | var step = typeof options.step === 'function' ? options.step : noop 1485 | var done = typeof options.done === 'function' ? options.done : noop 1486 | 1487 | var scheduler = getScheduler(options.scheduler) 1488 | 1489 | var keys = Object.keys(target) 1490 | keys.forEach(function (key) { 1491 | start[key] = source[key] 1492 | diff[key] = target[key] - source[key] 1493 | }) 1494 | 1495 | var durationInMs = typeof options.duration === 'number' ? options.duration : 400 1496 | var durationInFrames = Math.max(1, durationInMs * 0.06) // 0.06 because 60 frames pers 1,000 ms 1497 | var previousAnimationId 1498 | var frame = 0 1499 | 1500 | previousAnimationId = scheduler.next(loop) 1501 | 1502 | return { 1503 | cancel: cancel 1504 | } 1505 | 1506 | function cancel() { 1507 | scheduler.cancel(previousAnimationId) 1508 | previousAnimationId = 0 1509 | } 1510 | 1511 | function loop() { 1512 | var t = easing(frame / durationInFrames) 1513 | frame += 1 1514 | setValues(t) 1515 | if (frame <= durationInFrames) { 1516 | previousAnimationId = scheduler.next(loop) 1517 | step(source) 1518 | } else { 1519 | previousAnimationId = 0 1520 | setTimeout(function () { done(source) }, 0) 1521 | } 1522 | } 1523 | 1524 | function setValues(t) { 1525 | keys.forEach(function (key) { 1526 | source[key] = diff[key] * t + start[key] 1527 | }) 1528 | } 1529 | } 1530 | 1531 | function noop() { } 1532 | 1533 | function getScheduler(scheduler) { 1534 | if (!scheduler) { 1535 | var canRaf = typeof window !== 'undefined' && window.requestAnimationFrame 1536 | return canRaf ? rafScheduler() : timeoutScheduler() 1537 | } 1538 | if (typeof scheduler.next !== 'function') throw new Error('Scheduler is supposed to have next(cb) function') 1539 | if (typeof scheduler.cancel !== 'function') throw new Error('Scheduler is supposed to have cancel(handle) function') 1540 | 1541 | return scheduler 1542 | } 1543 | 1544 | function rafScheduler() { 1545 | return { 1546 | next: window.requestAnimationFrame.bind(window), 1547 | cancel: window.cancelAnimationFrame.bind(window) 1548 | } 1549 | } 1550 | 1551 | function timeoutScheduler() { 1552 | return { 1553 | next: function (cb) { 1554 | return setTimeout(cb, 1000 / 60) 1555 | }, 1556 | cancel: function (id) { 1557 | return clearTimeout(id) 1558 | } 1559 | } 1560 | } 1561 | 1562 | function makeAggregateRaf() { 1563 | var frontBuffer = new Set(); 1564 | var backBuffer = new Set(); 1565 | var frameToken = 0; 1566 | 1567 | return { 1568 | next: next, 1569 | cancel: next, 1570 | clearAll: clearAll 1571 | } 1572 | 1573 | function clearAll() { 1574 | frontBuffer.clear(); 1575 | backBuffer.clear(); 1576 | cancelAnimationFrame(frameToken); 1577 | frameToken = 0; 1578 | } 1579 | 1580 | function next(callback) { 1581 | backBuffer.add(callback); 1582 | renderNextFrame(); 1583 | } 1584 | 1585 | function renderNextFrame() { 1586 | if (!frameToken) frameToken = requestAnimationFrame(renderFrame); 1587 | } 1588 | 1589 | function renderFrame() { 1590 | frameToken = 0; 1591 | 1592 | var t = backBuffer; 1593 | backBuffer = frontBuffer; 1594 | frontBuffer = t; 1595 | 1596 | frontBuffer.forEach(function (callback) { 1597 | callback(); 1598 | }); 1599 | frontBuffer.clear(); 1600 | } 1601 | 1602 | function cancel(callback) { 1603 | backBuffer.delete(callback); 1604 | } 1605 | } 1606 | 1607 | }, { "bezier-easing": 8 }], 8: [function (require, module, exports) { 1608 | /** 1609 | * https://github.com/gre/bezier-easing 1610 | * BezierEasing - use bezier curve for transition easing function 1611 | * by Gaëtan Renaudeau 2014 - 2015 – MIT License 1612 | */ 1613 | 1614 | // These values are established by empiricism with tests (tradeoff: performance VS precision) 1615 | var NEWTON_ITERATIONS = 4; 1616 | var NEWTON_MIN_SLOPE = 0.001; 1617 | var SUBDIVISION_PRECISION = 0.0000001; 1618 | var SUBDIVISION_MAX_ITERATIONS = 10; 1619 | 1620 | var kSplineTableSize = 11; 1621 | var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); 1622 | 1623 | var float32ArraySupported = typeof Float32Array === 'function'; 1624 | 1625 | function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } 1626 | function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } 1627 | function C(aA1) { return 3.0 * aA1; } 1628 | 1629 | // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. 1630 | function calcBezier(aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; } 1631 | 1632 | // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. 1633 | function getSlope(aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } 1634 | 1635 | function binarySubdivide(aX, aA, aB, mX1, mX2) { 1636 | var currentX, currentT, i = 0; 1637 | do { 1638 | currentT = aA + (aB - aA) / 2.0; 1639 | currentX = calcBezier(currentT, mX1, mX2) - aX; 1640 | if (currentX > 0.0) { 1641 | aB = currentT; 1642 | } else { 1643 | aA = currentT; 1644 | } 1645 | } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); 1646 | return currentT; 1647 | } 1648 | 1649 | function newtonRaphsonIterate(aX, aGuessT, mX1, mX2) { 1650 | for (var i = 0; i < NEWTON_ITERATIONS; ++i) { 1651 | var currentSlope = getSlope(aGuessT, mX1, mX2); 1652 | if (currentSlope === 0.0) { 1653 | return aGuessT; 1654 | } 1655 | var currentX = calcBezier(aGuessT, mX1, mX2) - aX; 1656 | aGuessT -= currentX / currentSlope; 1657 | } 1658 | return aGuessT; 1659 | } 1660 | 1661 | function LinearEasing(x) { 1662 | return x; 1663 | } 1664 | 1665 | module.exports = function bezier(mX1, mY1, mX2, mY2) { 1666 | if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { 1667 | throw new Error('bezier x values must be in [0, 1] range'); 1668 | } 1669 | 1670 | if (mX1 === mY1 && mX2 === mY2) { 1671 | return LinearEasing; 1672 | } 1673 | 1674 | // Precompute samples table 1675 | var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); 1676 | for (var i = 0; i < kSplineTableSize; ++i) { 1677 | sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); 1678 | } 1679 | 1680 | function getTForX(aX) { 1681 | var intervalStart = 0.0; 1682 | var currentSample = 1; 1683 | var lastSample = kSplineTableSize - 1; 1684 | 1685 | for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { 1686 | intervalStart += kSampleStepSize; 1687 | } 1688 | --currentSample; 1689 | 1690 | // Interpolate to provide an initial guess for t 1691 | var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); 1692 | var guessForT = intervalStart + dist * kSampleStepSize; 1693 | 1694 | var initialSlope = getSlope(guessForT, mX1, mX2); 1695 | if (initialSlope >= NEWTON_MIN_SLOPE) { 1696 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2); 1697 | } else if (initialSlope === 0.0) { 1698 | return guessForT; 1699 | } else { 1700 | return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); 1701 | } 1702 | } 1703 | 1704 | return function BezierEasing(x) { 1705 | // Because JavaScript number are imprecise, we should guarantee the extremes are right. 1706 | if (x === 0) { 1707 | return 0; 1708 | } 1709 | if (x === 1) { 1710 | return 1; 1711 | } 1712 | return calcBezier(getTForX(x), mY1, mY2); 1713 | }; 1714 | }; 1715 | 1716 | }, {}], 9: [function (require, module, exports) { 1717 | module.exports = function eventify(subject) { 1718 | validateSubject(subject); 1719 | 1720 | var eventsStorage = createEventsStorage(subject); 1721 | subject.on = eventsStorage.on; 1722 | subject.off = eventsStorage.off; 1723 | subject.fire = eventsStorage.fire; 1724 | return subject; 1725 | }; 1726 | 1727 | function createEventsStorage(subject) { 1728 | // Store all event listeners to this hash. Key is event name, value is array 1729 | // of callback records. 1730 | // 1731 | // A callback record consists of callback function and its optional context: 1732 | // { 'eventName' => [{callback: function, ctx: object}] } 1733 | var registeredEvents = Object.create(null); 1734 | 1735 | return { 1736 | on: function (eventName, callback, ctx) { 1737 | if (typeof callback !== 'function') { 1738 | throw new Error('callback is expected to be a function'); 1739 | } 1740 | var handlers = registeredEvents[eventName]; 1741 | if (!handlers) { 1742 | handlers = registeredEvents[eventName] = []; 1743 | } 1744 | handlers.push({ callback: callback, ctx: ctx }); 1745 | 1746 | return subject; 1747 | }, 1748 | 1749 | off: function (eventName, callback) { 1750 | var wantToRemoveAll = (typeof eventName === 'undefined'); 1751 | if (wantToRemoveAll) { 1752 | // Killing old events storage should be enough in this case: 1753 | registeredEvents = Object.create(null); 1754 | return subject; 1755 | } 1756 | 1757 | if (registeredEvents[eventName]) { 1758 | var deleteAllCallbacksForEvent = (typeof callback !== 'function'); 1759 | if (deleteAllCallbacksForEvent) { 1760 | delete registeredEvents[eventName]; 1761 | } else { 1762 | var callbacks = registeredEvents[eventName]; 1763 | for (var i = 0; i < callbacks.length; ++i) { 1764 | if (callbacks[i].callback === callback) { 1765 | callbacks.splice(i, 1); 1766 | } 1767 | } 1768 | } 1769 | } 1770 | 1771 | return subject; 1772 | }, 1773 | 1774 | fire: function (eventName) { 1775 | var callbacks = registeredEvents[eventName]; 1776 | if (!callbacks) { 1777 | return subject; 1778 | } 1779 | 1780 | var fireArguments; 1781 | if (arguments.length > 1) { 1782 | fireArguments = Array.prototype.splice.call(arguments, 1); 1783 | } 1784 | for (var i = 0; i < callbacks.length; ++i) { 1785 | var callbackInfo = callbacks[i]; 1786 | callbackInfo.callback.apply(callbackInfo.ctx, fireArguments); 1787 | } 1788 | 1789 | return subject; 1790 | } 1791 | }; 1792 | } 1793 | 1794 | function validateSubject(subject) { 1795 | if (!subject) { 1796 | throw new Error('Eventify cannot use falsy object as events subject'); 1797 | } 1798 | var reservedWords = ['on', 'fire', 'off']; 1799 | for (var i = 0; i < reservedWords.length; ++i) { 1800 | if (subject.hasOwnProperty(reservedWords[i])) { 1801 | throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'"); 1802 | } 1803 | } 1804 | } 1805 | 1806 | }, {}], 10: [function (require, module, exports) { 1807 | /** 1808 | * This module used to unify mouse wheel behavior between different browsers in 2014 1809 | * Now it's just a wrapper around addEventListener('wheel'); 1810 | * 1811 | * Usage: 1812 | * var addWheelListener = require('wheel').addWheelListener; 1813 | * var removeWheelListener = require('wheel').removeWheelListener; 1814 | * addWheelListener(domElement, function (e) { 1815 | * // mouse wheel event 1816 | * }); 1817 | * removeWheelListener(domElement, function); 1818 | */ 1819 | 1820 | module.exports = addWheelListener; 1821 | 1822 | // But also expose "advanced" api with unsubscribe: 1823 | module.exports.addWheelListener = addWheelListener; 1824 | module.exports.removeWheelListener = removeWheelListener; 1825 | 1826 | 1827 | function addWheelListener(element, listener, useCapture) { 1828 | element.addEventListener('wheel', listener, useCapture); 1829 | } 1830 | 1831 | function removeWheelListener(element, listener, useCapture) { 1832 | element.removeEventListener('wheel', listener, useCapture); 1833 | } 1834 | }, {}] 1835 | }, {}, [1])(1) 1836 | }); 1837 | -------------------------------------------------------------------------------- /js/lib/simplify-svg-path.js: -------------------------------------------------------------------------------- 1 | var simplifySvgPath=(function(){class t{constructor(t){this.t=t,this.i=10**this.t}h(t){return this.t<16?Math.round(t*this.i)/this.i:t}o(t,s){return this.h(t)+","+this.h(s)}}class s{constructor(t,s){this.x=t,this.y=s}u(){return new s(-this.x,-this.y)}l(t=1){return this._(t/(this.M()||1/0))}g(t){return new s(this.x+t.x,this.y+t.y)}v(t){return new s(this.x-t.x,this.y-t.y)}_(t){return new s(this.x*t,this.y*t)}m(t){return this.x*t.x+this.y*t.y}M(){return Math.sqrt(this.x*this.x+this.y*this.y)}p(t){const s=this.x-t.x,e=this.y-t.y;return Math.sqrt(s*s+e*e)}}class e{constructor(t,s){this.C=t,this.L=s}}class r{constructor(t,s){this.k=t,this.B=s}D(t){const s=this.k;this.B&&(s.unshift(s[s.length-1]),s.push(s[1]));const r=s.length;if(0===r)return[];const i=[new e(s[0])];return this.P(i,t,0,r-1,s[1].v(s[0]),s[r-2].v(s[r-1])),this.B&&(i.shift(),i.pop()),i}P(t,s,e,r,i,n){const h=this.k;if(r-e==1){const s=h[e],o=h[r],c=s.p(o)/3;return void this.R(t,[s,s.g(i.l(c)),o.g(n.l(c)),o])}const o=this.j(e,r);let c,u=Math.max(s,s*s),l=!0;for(let h=0;h<=4;h++){const h=this.q(e,r,o,i,n),a=this.A(e,r,h,o);if(a.error=u)break;l=this.F(e,r,o,h),u=a.error}const a=h[c-1].v(h[c+1]);this.P(t,s,e,c,i,a),this.P(t,s,c,r,a.u(),n)}R(t,s){t[t.length-1].G=s[1].v(s[0]),t.push(new e(s[3],s[2].v(s[3])))}q(t,s,e,r,i){const n=1e-12,h=Math.abs,o=this.k,c=o[t],u=o[s],l=[[0,0],[0,0]],a=[0,0];for(let n=0,h=s-t+1;nn){const t=l[0][0]*a[1]-l[1][0]*a[0];_=(a[0]*l[1][1]-a[1]*l[0][1])/f,d=t/f}else{const t=l[0][0]+l[0][1],s=l[1][0]+l[1][1];_=d=h(t)>n?a[0]/t:h(s)>n?a[1]/s:0}const w=u.p(c),M=n*w;let g,v;if(_w*w&&(_=d=w/3,g=v=null)}return[c,c.g(g||r.l(_)),u.g(v||i.l(d)),u]}F(t,s,e,r){for(let i=t;i<=s;i++)e[i-t]=this.H(r,this.k[i],e[i-t]);for(let t=1,s=e.length;t=-112e-18&&l<=112e-18?e:e-c.m(h)/u;var l}I(t,s,e){const r=s.slice();for(let s=1;s<=t;s++)for(let i=0;i<=t-s;i++)r[i]=r[i]._(1-e).g(r[i+1]._(e));return r[0]}j(t,s){const e=[0];for(let r=t+1;r<=s;r++)e[r-t]=e[r-t-1]+this.k[r].p(this.k[r-1]);for(let r=1,i=s-t;r<=i;r++)e[r]/=e[i];return e}A(t,s,e,r){let i=Math.floor((s-t+1)/2),n=0;for(let h=t+1;h=n&&(n=o,i=h)}return{error:n,index:i}}}return(e,i={})=>((s,e,r)=>{const i=s.length,n=new t(r);let h,o,c,u,l=!0;const a=[],f=(t,s)=>{const e=t.C.x,r=t.C.y;if(l)a.push("M"+n.o(e,r)),l=!1;else{const i=e+(t.L?.x??0),l=r+(t.L?.y??0);if(i===e&&l===r&&c===h&&u===o){if(!s){const t=e-h,s=r-o;a.push(0===t?"v"+n.h(s):0===s?"h"+n.h(t):"l"+n.o(t,s))}}else a.push("c"+n.o(c-h,u-o)+" "+n.o(i-h,l-o)+" "+n.o(e-h,r-o))}h=e,o=r,c=e+(t.G?.x??0),u=r+(t.G?.y??0)};if(!i)return"";for(let t=0;t0&&(f(s[0],!0),a.push("z")),a.join("")})(new r(e.map((t=>new s(t[0],t[1]))),i.closed).D(i.tolerance??2.5),i.closed,i.precision??5);})() -------------------------------------------------------------------------------- /js/lib/simplify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using Terser v3.14.1. 3 | * Original file: /npm/simplify-js@1.2.4/simplify.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | !function(){"use strict";function n(n,e,f){var t=e.x,u=e.y,i=f.x-t,r=f.y-u;if(0!==i||0!==r){var o=((n.x-t)*i+(n.y-u)*r)/(i*i+r*r);o>1?(t=f.x,u=f.y):o>0&&(t+=i*o,u+=r*o)}return(i=n.x-t)*i+(r=n.y-u)*r}function e(e,f){var t=e.length-1,u=[e[0]];return function e(f,t,u,i,r){for(var o,d=i,s=t+1;sd&&(o=s,d=y)}d>i&&(o-t>1&&e(f,t,o,i,r),r.push(f[o]),u-o>1&&e(f,o,u,i,r))}(e,0,t,f,u),u.push(e[t]),u}function f(n,f,t){if(n.length<=2)return n;var u=void 0!==f?f*f:1;return n=e(n=t?n:function(n,e){for(var f,t,u,i,r,o=n[0],d=[o],s=1,y=n.length;se&&(d.push(f),o=f);return o!==f&&d.push(f),d}(n,u),u)}"function"==typeof define&&define.amd?define(function(){return f}):"undefined"!=typeof module?(module.exports=f,module.exports.default=f):"undefined"!=typeof self?self.simplify=f:window.simplify=f}(); 8 | //# sourceMappingURL=/sm/eca0f9334aa4defd09cf83dfc6d2cfe23f472e87984c4b6af46ebd122c29a43e.map -------------------------------------------------------------------------------- /js/lib/svg-export.min.js: -------------------------------------------------------------------------------- 1 | (function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):(t="undefined"!=typeof globalThis?globalThis:t||self,e(t.svgExport=t.svgExport||{}))})(this,function(t){"use strict";function e(t){void 0!==typeof console&&"function"==typeof console.warn&&console.warn(t)}function i(t){var i=document.createElement("div");if(i.className="tempdiv-svg-exportJS","string"==typeof t&&(i.insertAdjacentHTML("beforeend",t.trim()),t=i.firstChild),!t.nodeType||1!==t.nodeType)return e("Error svg-export: The input svg was not recognized"),null;var n=t.cloneNode(!0);return n.style.display=null,i.appendChild(n),i.style.visibility="hidden",i.style.display="table",i.style.position="absolute",document.body.appendChild(i),n}function n(t){t&&t.pdfOptions&&(Object.keys(y.pdfOptions).forEach(function(e){if(t.pdfOptions.hasOwnProperty(e)&&typeof t.pdfOptions[e]==typeof y.pdfOptions[e]){if(""===t.pdfOptions[e])return;y.pdfOptions[e]=t.pdfOptions[e]}}),y.pdfOptions.pageLayout.margin||(y.pdfOptions.pageLayout.margin=50),y.pdfOptions.pageLayout.margins||(y.pdfOptions.pageLayout.margins={})),y.pdfOptions.pageLayout.margins.top=y.pdfOptions.pageLayout.margins.top||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.bottom=y.pdfOptions.pageLayout.margins.bottom||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.left=y.pdfOptions.pageLayout.margins.left||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.right=y.pdfOptions.pageLayout.margins.top||y.pdfOptions.pageLayout.margin,delete y.pdfOptions.pageLayout.margin,t&&y.pdfOptions.pageLayout.size||(y.pdfOptions.pageLayout.size=[Math.max(300,y.width)+y.pdfOptions.pageLayout.margins.left+y.pdfOptions.pageLayout.margins.right,Math.max(300,y.height)+y.pdfOptions.pageLayout.margins.top+y.pdfOptions.pageLayout.margins.bottom+(y.pdfOptions.addTitleToPage?2*y.pdfOptions.pdfTitleFontSize+10:0)+(""!==y.pdfOptions.chartCaption?4*y.pdfOptions.pdfCaptionFontSize+10:0)])}function o(t,e){y={originalWidth:100,originalHeight:100,originalMinXViewBox:0,originalMinYViewBox:0,width:100,height:100,scale:1,useCSS:!0,transparentBackgroundReplace:"white",allowCrossOriginImages:!1,elementsToExclude:[],pdfOptions:{customFonts:[],pageLayout:{margin:50,margins:{}},addTitleToPage:!0,chartCaption:"",pdfTextFontFamily:"Helvetica",pdfTitleFontSize:20,pdfCaptionFontSize:14}},y.originalHeight=-1!==t.style.getPropertyValue("height").indexOf("%")||t.getAttribute("height")&&-1!==t.getAttribute("height").indexOf("%")?t.getBBox().height*y.scale:t.getBoundingClientRect().height*y.scale,y.originalWidth=-1!==t.style.getPropertyValue("width").indexOf("%")||t.getAttribute("width")&&-1!==t.getAttribute("width").indexOf("%")?t.getBBox().width*y.scale:t.getBoundingClientRect().width*y.scale,y.originalMinXViewBox=t.getAttribute("viewBox")?t.getAttribute("viewBox").split(/\s/)[0]:0,y.originalMinYViewBox=t.getAttribute("viewBox")?t.getAttribute("viewBox").split(/\s/)[1]:0,e&&e.scale&&"number"==typeof e.scale&&(y.scale=e.scale),e&&e.height?"number"==typeof e.height&&(y.height=e.height*y.scale):y.height=y.originalHeight*y.scale,e&&e.width?"number"==typeof e.width&&(y.width=e.width*y.scale):y.width=y.originalWidth*y.scale,e&&!1===e.useCSS&&(y.useCSS=!1),e&&e.transparentBackgroundReplace&&(y.transparentBackgroundReplace=e.transparentBackgroundReplace),e&&e.allowCrossOriginImages&&(y.allowCrossOriginImages=e.allowCrossOriginImages),e&&e.excludeByCSSSelector&&"string"==typeof e.excludeByCSSSelector&&(y.elementsToExclude=t.querySelectorAll(e.excludeByCSSSelector)),n(e)}function a(t,i){if("function"==typeof getComputedStyle){for(var n=0;n0)for(const t of o)-1===["width","height","inline-size","block-size"].indexOf(t)&&i.style.setProperty(t,o.getPropertyValue(t));t.childNodes.forEach(function(t,e){1===t.nodeType&&a(t,i.childNodes[parseInt(e,10)])})}else e("Warning svg-export: this browser is not able to get computed styles")}function r(t,e,i){void 0===i&&(i=!0),y.useCSS&&"object"==typeof e&&(a(e,t),t.style.display=null),y.elementsToExclude.forEach(function(t){t.remove()}),t.style.width=null,t.style.height=null,t.setAttribute("width",y.width),t.setAttribute("height",y.height),t.setAttribute("preserveAspectRatio","none"),t.setAttribute("viewBox",y.originalMinXViewBox+" "+y.originalMinYViewBox+" "+y.originalWidth+" "+y.originalHeight);for(var n=document.getElementsByClassName("tempdiv-svg-exportJS");n.length>0;)n[0].parentNode.removeChild(n[0]);if(i){var o=new XMLSerializer,r=o.serializeToString(t).replace(/currentColor/g,"black");return r.match(/^]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)||(r=r.replace(/^]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)||(r=r.replace(/^]/g,"_"),navigator.msSaveBlob){for(var n=decodeURIComponent(t.split(",")[1]),o=[],a=t.split(",")[0].split(":")[1].split(";")[0],r=0;r\r\n'+i;var n="data:image/svg+xml;charset=utf-8,"+encodeURIComponent(i);g(n,e+".svg")})}}function d(t,n,a,s){if("object"==typeof canvg){s=s.toLowerCase().replace("jpg","jpeg"),"png"!==s&&"jpeg"!==s&&(s="png");var p=i(t);if(p){null==n&&(n="chart");var l=document.createElement("canvas");a&&(a.width||a.height)||(a||(a={}),a.scale=10),o(p,a);var d=r(p,t);"jpeg"===s&&(d=d.replace(">",'>'));var f=l.getContext("2d"),u=canvg.Canvg.fromString(f,d,{anonymousCrossOrigin:y.allowCrossOriginImages});u.start(),u.ready().then(function(){var t=l.toDataURL("image/"+s);g(t,n+"."+s,l)})}}else e("Error svg-export: PNG/JPEG export requires Canvg.js")}function f(t,e,i){d(t,e,i,"png")}function u(t,e,i){d(t,e,i,"jpeg")}function h(t,e,i){y.pdfOptions.addTitleToPage&&t.font(y.pdfOptions.pdfTextFontFamily).fontSize(y.pdfOptions.pdfTitleFontSize).text(e,{width:y.pdfOptions.pageLayout.size[0]-y.pdfOptions.pageLayout.margins.left-y.pdfOptions.pageLayout.margins.right}),SVGtoPDF(t,i,y.pdfOptions.pageLayout.margins.left,t.y+10,{width:y.width,height:y.height,preserveAspectRatio:"none",useCSS:y.useCSS}),""!==y.pdfOptions.chartCaption&&t.font(y.pdfOptions.pdfTextFontFamily).fontSize(y.pdfOptions.pdfCaptionFontSize).text(y.pdfOptions.chartCaption,y.pdfOptions.pageLayout.margins.left,y.pdfOptions.pageLayout.size[1]-y.pdfOptions.pageLayout.margins.bottom-4*y.pdfOptions.pdfCaptionFontSize,{width:y.pdfOptions.pageLayout.size[0]-y.pdfOptions.pageLayout.margins.left-y.pdfOptions.pageLayout.margins.right})}function c(t,n,a){if("function"==typeof PDFDocument&&"function"==typeof SVGtoPDF&&"function"==typeof blobStream){var l=i(t);if(l){null==n&&(n="chart"),o(l,a);var d=r(l,t,!1),f=new PDFDocument(y.pdfOptions.pageLayout),u=f.pipe(blobStream()),c=l.getElementsByTagName("image"),m=[];if(c)for(var w of c)(w.getAttribute("href")&&-1===w.getAttribute("href").indexOf("data:")||w.getAttribute("xlink:href")&&-1===w.getAttribute("xlink:href").indexOf("data:"))&&m.push(s(w));Promise.all(m).then(function(){if(y.pdfOptions.customFonts.length>0){var t=p(y.pdfOptions.customFonts.map(function(t){return t.url}));Promise.all(t).then(function(t){t.forEach(function(t,e){var i=y.pdfOptions.customFonts[parseInt(e,10)],n=d.querySelectorAll('[style*="'+i.fontName+'"]');n.forEach(function(t){t.style.fontFamily=i.fontName}),-1===i.url.indexOf(".ttc")&&-1===i.url.indexOf(".dfont")||!i.styleName?f.registerFont(i.fontName,t):f.registerFont(i.fontName,t,i.styleName)}),h(f,n,d),f.end()})}else h(f,n,d),f.end()}),u.on("finish",function(){var t=u.toBlobURL("application/pdf");g(t,n+".pdf")})}}else e("Error svg-export: PDF export requires PDFKit.js, blob-stream and SVG-to-PDFKit")}var m="1.2.0",y={};t.version=m,t.downloadSvg=l,t.downloadPng=f,t.downloadJpeg=u,t.downloadPdf=c,Object.defineProperty(t,"__esModule",{value:!0})}); -------------------------------------------------------------------------------- /js/pan-zoom.js: -------------------------------------------------------------------------------- 1 | let instance = panzoom(scene, { 2 | maxZoom: 1.6, 3 | minZoom: 0.1, 4 | zoomSpeed: 0.08, 5 | beforeMouseDown: function (e) { 6 | let shouldIgnore = !e.button 7 | return shouldIgnore 8 | } 9 | }) 10 | 11 | instance.on('transform', function () { 12 | background.style.backgroundPositionX = `${transformX}px` 13 | background.style.backgroundPositionY = `${transformY}px` 14 | background.style.backgroundSize = `${55 * transformScale}px` 15 | }) 16 | -------------------------------------------------------------------------------- /js/panels.js: -------------------------------------------------------------------------------- 1 | let activePanel = false 2 | 3 | // Brush color panel 4 | color.onclick = function () { 5 | if (activePanel === false) { 6 | fadeIn('#colors', 200) 7 | setTimeout(() => { 8 | activePanel = true 9 | }, 200) 10 | } else { 11 | fadeOut('#colors', 200) 12 | setTimeout(() => { 13 | activePanel = false 14 | }, 200) 15 | } 16 | } 17 | colorInput.onpointerenter = function () { 18 | activePanel = false 19 | } 20 | colorInput.onpointerleave = function () { 21 | activePanel = true 22 | } 23 | 24 | // Brush size panel 25 | size.onclick = function () { 26 | if (activePanel === false) { 27 | fadeIn('#sizes', 200) 28 | setTimeout(() => { 29 | activePanel = true 30 | }, 200) 31 | } else { 32 | fadeOut('#sizes', 200) 33 | setTimeout(() => { 34 | activePanel = false 35 | }, 200) 36 | } 37 | } 38 | 39 | // Canvas pattern panel 40 | pattern.onclick = function () { 41 | if (activePanel === false) { 42 | fadeIn('#patterns', 200) 43 | setTimeout(() => { 44 | activePanel = true 45 | }, 200) 46 | } else { 47 | fadeOut('#patterns', 200) 48 | setTimeout(() => { 49 | activePanel = false 50 | }, 200) 51 | } 52 | } 53 | 54 | document.getElementsByTagName('body')[0].onclick = function () { 55 | if (activePanel === true) { 56 | fadeOut('#colors', 200) 57 | fadeOut('#sizes', 200) 58 | fadeOut('#patterns', 200) 59 | setTimeout(() => { 60 | activePanel = false 61 | }, 200) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | // Default brush settings 2 | let boardMode = 'pen' 3 | let widthOption = 4 4 | let colorOption = 'black' 5 | 6 | // Change tool 7 | function changeAction(target) { 8 | ;['eraser', 'pen'].forEach((action) => { 9 | const t = document.getElementById(action) 10 | t.classList.remove('active') 11 | }) 12 | if (typeof target === 'string') target = document.getElementById(target) 13 | target.classList.add('active') 14 | switch (target.id) { 15 | case 'pointer': 16 | boardMode = 'pointer' 17 | applyDraggable() 18 | break 19 | case 'eraser': 20 | boardMode = 'eraser' 21 | color.style.pointerEvents = 'none' 22 | color.style.opacity = 0.2 23 | break 24 | case 'pen': 25 | boardMode = 'pen' 26 | color.style.pointerEvents = 'auto' 27 | color.style.opacity = 1 28 | break 29 | default: 30 | break 31 | } 32 | } 33 | 34 | // Brush settings 35 | function setBrush(option) { 36 | // Size settings 37 | if (option.size !== undefined) { 38 | widthOption = option.size 39 | document 40 | .querySelector('#sizes button.active') 41 | .classList.remove('active') 42 | } 43 | switch (option.size) { 44 | case 3: 45 | size.style.backgroundSize = '50%' 46 | small.classList.add('active') 47 | break 48 | case 4: 49 | size.style.backgroundSize = '76%' 50 | medium.classList.add('active') 51 | break 52 | case 5: 53 | size.style.backgroundSize = '110%' 54 | large.classList.add('active') 55 | break 56 | default: 57 | break 58 | } 59 | // Color settings 60 | if (option.color !== undefined) { 61 | let val = option.color 62 | colorOption = val 63 | color.style.backgroundColor = val 64 | } 65 | } 66 | customColor = 'black' 67 | colorInput.onchange = function () { 68 | setBrush({ color: colorInput.value }) 69 | customColor = colorInput.value 70 | } 71 | 72 | // Set canvas pattern 73 | let currentPattern = 'none' 74 | function setPattern(name) { 75 | background.style.backgroundImage = `url('assets/patterns/pattern_${name}.svg')` 76 | pattern.style.backgroundImage = `url('assets/icons/${name}.svg')` 77 | document.querySelector('#patterns button.active').classList.remove('active') 78 | document.getElementById(name).classList.add('active') 79 | currentPattern = name 80 | } 81 | setPattern('none') 82 | --------------------------------------------------------------------------------