├── .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 |
6 |
--------------------------------------------------------------------------------
/assets/icons/arrow-forward-up.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/icons/chevron-left.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/color-picker.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/icons/door-exit.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/assets/icons/dot.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/assets/icons/download.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/assets/icons/eraser.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/icons/file-download.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/assets/icons/line.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/assets/icons/link.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/assets/icons/none.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/palette.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/assets/icons/pencil.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/icons/photo-down.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/assets/icons/photo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/assets/icons/point.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/pointer.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/sq.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/assets/icons/trash-x.svg:
--------------------------------------------------------------------------------
1 |
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 |
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