diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7a48ed3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,152 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+.pnpm-debug.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# .lock files
+package-lock.json
+yarn.lock
+
+# yarn
+.yarn/
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Yarn rc.yml file
+.yarnrc.yml
+
+# Yarn Integrity file
+.yarn-integrity
+
+# Optional npm cache directory
+.npm
+
+# TypeScript v1 declaration files
+typings/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# dotenv environment variables files
+.env
+.env.development.local
+.env.test
+.env.test.local
+.env.production
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+dist/
+.output/
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Visual Studio Code
+.vscode
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# macOS-specific files
+.DS_Store
diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..299e1da
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+samutichev.ru
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d7bbedc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Степан Самутичев
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0bb2381
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# Samutichev.github.io
+Samutichev's Website
diff --git a/assets/favicon.svg b/assets/favicon.svg
new file mode 100644
index 0000000..2684928
--- /dev/null
+++ b/assets/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/fonts/MPLUSRounded1c-Medium.woff b/assets/fonts/MPLUSRounded1c-Medium.woff
new file mode 100644
index 0000000..e494646
Binary files /dev/null and b/assets/fonts/MPLUSRounded1c-Medium.woff differ
diff --git a/assets/fonts/MPLUSRounded1c-Medium.woff2 b/assets/fonts/MPLUSRounded1c-Medium.woff2
new file mode 100644
index 0000000..1fc5ee4
Binary files /dev/null and b/assets/fonts/MPLUSRounded1c-Medium.woff2 differ
diff --git a/assets/icons/arrow-back-up.svg b/assets/icons/arrow-back-up.svg
new file mode 100644
index 0000000..c8a28d7
--- /dev/null
+++ b/assets/icons/arrow-back-up.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/arrow-forward-up.svg b/assets/icons/arrow-forward-up.svg
new file mode 100644
index 0000000..4ccd4a4
--- /dev/null
+++ b/assets/icons/arrow-forward-up.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/chevron-left.svg b/assets/icons/chevron-left.svg
new file mode 100644
index 0000000..5dbdc8a
--- /dev/null
+++ b/assets/icons/chevron-left.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/chevron-right.svg b/assets/icons/chevron-right.svg
new file mode 100644
index 0000000..82fe616
--- /dev/null
+++ b/assets/icons/chevron-right.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/color-picker.svg b/assets/icons/color-picker.svg
new file mode 100644
index 0000000..3b7b90b
--- /dev/null
+++ b/assets/icons/color-picker.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/door-exit.svg b/assets/icons/door-exit.svg
new file mode 100644
index 0000000..fff791d
--- /dev/null
+++ b/assets/icons/door-exit.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/dot.svg b/assets/icons/dot.svg
new file mode 100644
index 0000000..0a7af88
--- /dev/null
+++ b/assets/icons/dot.svg
@@ -0,0 +1,12 @@
+
diff --git a/assets/icons/download.svg b/assets/icons/download.svg
new file mode 100644
index 0000000..cd214aa
--- /dev/null
+++ b/assets/icons/download.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/eraser.svg b/assets/icons/eraser.svg
new file mode 100644
index 0000000..8ccd96e
--- /dev/null
+++ b/assets/icons/eraser.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/file-download.svg b/assets/icons/file-download.svg
new file mode 100644
index 0000000..698c332
--- /dev/null
+++ b/assets/icons/file-download.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/line.svg b/assets/icons/line.svg
new file mode 100644
index 0000000..7938414
--- /dev/null
+++ b/assets/icons/line.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/link.svg b/assets/icons/link.svg
new file mode 100644
index 0000000..c877892
--- /dev/null
+++ b/assets/icons/link.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/none.svg b/assets/icons/none.svg
new file mode 100644
index 0000000..e2bc095
--- /dev/null
+++ b/assets/icons/none.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/palette.svg b/assets/icons/palette.svg
new file mode 100644
index 0000000..d5cc931
--- /dev/null
+++ b/assets/icons/palette.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/pencil.svg b/assets/icons/pencil.svg
new file mode 100644
index 0000000..f9dd7d3
--- /dev/null
+++ b/assets/icons/pencil.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/photo-down.svg b/assets/icons/photo-down.svg
new file mode 100644
index 0000000..8f8fb7d
--- /dev/null
+++ b/assets/icons/photo-down.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/icons/photo.svg b/assets/icons/photo.svg
new file mode 100644
index 0000000..b0d340c
--- /dev/null
+++ b/assets/icons/photo.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/point.svg b/assets/icons/point.svg
new file mode 100644
index 0000000..6d5b55d
--- /dev/null
+++ b/assets/icons/point.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/pointer.svg b/assets/icons/pointer.svg
new file mode 100644
index 0000000..bb0f500
--- /dev/null
+++ b/assets/icons/pointer.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/sq.svg b/assets/icons/sq.svg
new file mode 100644
index 0000000..4aabe91
--- /dev/null
+++ b/assets/icons/sq.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/trash-x.svg b/assets/icons/trash-x.svg
new file mode 100644
index 0000000..ba51af1
--- /dev/null
+++ b/assets/icons/trash-x.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/patterns/pattern_dot.svg b/assets/patterns/pattern_dot.svg
new file mode 100644
index 0000000..61311a8
--- /dev/null
+++ b/assets/patterns/pattern_dot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/patterns/pattern_line.svg b/assets/patterns/pattern_line.svg
new file mode 100644
index 0000000..bd49ce1
--- /dev/null
+++ b/assets/patterns/pattern_line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/patterns/pattern_none.svg b/assets/patterns/pattern_none.svg
new file mode 100644
index 0000000..dcd8823
--- /dev/null
+++ b/assets/patterns/pattern_none.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/patterns/pattern_sq.svg b/assets/patterns/pattern_sq.svg
new file mode 100644
index 0000000..a57b3ba
--- /dev/null
+++ b/assets/patterns/pattern_sq.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/board.html b/board.html
new file mode 100644
index 0000000..1060672
--- /dev/null
+++ b/board.html
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+ Board — Samutichev
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/css/board.css b/css/board.css
new file mode 100644
index 0000000..c5d4b75
--- /dev/null
+++ b/css/board.css
@@ -0,0 +1,374 @@
+::-webkit-scrollbar {
+ display: none;
+}
+* {
+ -webkit-tap-highlight-color: transparent;
+}
+@font-face {
+ font-family: 'M PLUS Medium 1c';
+ font-style: normal;
+ font-weight: 400;
+ src: local('M PLUS Medium 1c'),
+ url('/assets/fonts/MPLUSRounded1c-Medium.woff2') format('woff2'),
+ url('/assets/fonts/MPLUSRounded1c-Medium.woff') format('woff');
+}
+body {
+ margin: 0;
+ padding: 0;
+ display: flex;
+ justify-content: center;
+ overflow: hidden;
+ font-family: 'M PLUS Medium 1c', 'Arial', sans-serif;
+}
+
+#board {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ outline: none;
+ opacity: 1;
+ fill: none;
+ stroke-linecap: round;
+ transition: all 150ms;
+ cursor: crosshair;
+}
+/* path {
+ transform-origin: 0 0;
+ transform: translate(0px, 0px);
+} */
+#background {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+}
+
+.panels {
+ display: contents;
+}
+
+.topPanel {
+ display: flex;
+ position: fixed;
+ top: 10px;
+ left: 10px;
+ z-index: 10;
+}
+.topPanel > button {
+ width: 30px;
+ height: 30px;
+ background: #2525257a;
+ background-size: 18px;
+ background-repeat: no-repeat;
+ background-position: center;
+ border-radius: 10px;
+ box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656;
+ backdrop-filter: blur(4px);
+ transition: 0.3s;
+}
+
+#exit {
+ background-image: url(/assets/icons/door-exit.svg);
+}
+#exit:hover {
+ background-color: #ff655b;
+}
+#link {
+ background-image: url(/assets/icons/link.svg);
+}
+#link:hover {
+ background-color: #00a92f;
+}
+#download {
+ background-image: url(/assets/icons/file-download.svg);
+}
+#download:hover {
+ background-color: #00a92f;
+}
+
+.dockPanel {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: fixed;
+ bottom: -4px;
+ z-index: 10;
+}
+.main-tools {
+ margin: 0 20px 25px;
+ padding: 2px;
+ display: flex;
+ gap: 20px;
+ z-index: 3;
+ background-color: #2525257a;
+ border-radius: 12px;
+ box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656;
+ backdrop-filter: blur(4px);
+}
+.tools {
+ display: flex;
+ z-index: 3;
+ border-radius: 50px;
+}
+.settings {
+ padding: 2px;
+ display: none;
+ position: absolute;
+ bottom: 90px;
+ background-color: #2525257a;
+ border-radius: 12px;
+ box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656;
+ backdrop-filter: blur(4px);
+ cursor: default;
+}
+#colors {
+ padding: 3px 10px;
+}
+
+button {
+ appearance: none;
+ outline: none;
+ margin: 6px;
+ width: 30px;
+ height: 30px;
+ background: transparent;
+ background-size: 20px;
+ background-repeat: no-repeat;
+ background-position: center;
+ border: none;
+ border-radius: 10px;
+ cursor: pointer;
+ transition: 0.1s ease-in;
+}
+button:hover {
+ background-color: #7c7c7ceb;
+ background-size: 22px;
+}
+button:active {
+ background-color: #363636eb;
+ background-size: 16px;
+}
+.active {
+ background-color: #545454fa;
+ background-size: 21.5px;
+ box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656;
+}
+
+#undo {
+ background-image: url(/assets/icons/arrow-back-up.svg);
+}
+#redo {
+ background-image: url(/assets/icons/arrow-forward-up.svg);
+}
+
+#pointer {
+ background-image: url(/assets/icons/pointer.svg);
+}
+#pen {
+ background-image: url(/assets/icons/pencil.svg);
+}
+#eraser {
+ background-image: url(/assets/icons/eraser.svg);
+}
+#clear {
+ background-image: url(/assets/icons/trash-x.svg);
+}
+#clear:active {
+ background-color: #ff655b;
+ box-shadow: none;
+ transition: 0.1s ease-in;
+}
+#photo {
+ appearance: none;
+ outline: none;
+ margin: 6px;
+ width: 30px;
+ height: 30px;
+ background: transparent;
+ background-image: url(/assets/icons/photo.svg);
+ background-size: 20px;
+ background-repeat: no-repeat;
+ background-position: center;
+ border: none;
+ border-radius: 10px;
+ cursor: pointer;
+ transition: 0.1s ease-in;
+}
+#photo:hover {
+ background-color: #7c7c7ceb;
+ background-size: 22px;
+}
+#photo:active {
+ background-color: #363636eb;
+ background-size: 16px;
+}
+
+#color {
+ background-image: url(/assets/icons/palette.svg);
+ background-color: black;
+}
+#size {
+ background-size: 76%;
+}
+#pattern {
+ background-image: url(/assets/icons/sq.svg);
+}
+
+#prev {
+ background-image: url(/assets/icons/chevron-left.svg);
+}
+#next {
+ background-image: url(/assets/icons/chevron-right.svg);
+}
+
+#colors > button {
+ margin: 6px 3px;
+ width: 27px;
+ height: 27px;
+ border-radius: 100%;
+}
+#colors > button:active {
+ border-radius: 40%;
+}
+#black {
+ background-color: black;
+}
+#black:active {
+ box-shadow: 4px 4px 0px 0px #7a7a7a44;
+}
+#red {
+ background-color: #d01919;
+}
+#red:active {
+ box-shadow: 4px 4px 0px 0px #ff6a6444;
+}
+#yellow {
+ background-color: #eaae00;
+}
+#yellow:active {
+ box-shadow: 4px 4px 0px 0px #ffde4b44;
+}
+#green {
+ background-color: #16ab39;
+}
+#green:active {
+ box-shadow: 4px 4px 0px 0px #2dff6144;
+}
+#blue {
+ background-color: #1678c2;
+}
+#blue:active {
+ box-shadow: 4px 4px 0px 0px #50a2ff44;
+}
+
+#size,
+#small,
+#medium,
+#large {
+ background-image: url(/assets/icons/point.svg);
+}
+#small {
+ background-size: 50%;
+}
+#medium {
+ background-size: 76%;
+}
+#large {
+ background-size: 110%;
+}
+
+#none {
+ background-image: url(/assets/icons/none.svg);
+}
+#sq {
+ background-image: url(/assets/icons/sq.svg);
+}
+#line {
+ background-image: url(/assets/icons/line.svg);
+}
+#dot {
+ background-image: url(/assets/icons/dot.svg);
+}
+
+input[type='color'] {
+ margin: 6px 3px;
+ padding: 0;
+ width: 27px;
+ height: 27px;
+ color: black;
+ background-color: transparent;
+ border: none;
+ border-color: black;
+ cursor: pointer;
+ transition: 0.3s;
+}
+input[type='color']::-webkit-color-swatch-wrapper {
+ padding: 0;
+ transition: 0.3s;
+}
+input[type='color']::-webkit-color-swatch {
+ background-image: url(/assets/icons/color-picker.svg);
+ background-size: 17px;
+ background-repeat: no-repeat;
+ background-position: center;
+ border: none;
+ border-radius: 100%;
+ transition: 0.3s;
+}
+
+[data-tooltip] {
+ position: relative;
+}
+[data-tooltip]::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ width: auto;
+ top: -26px;
+ left: -22px;
+ background-color: #2525257a;
+ border-radius: 12px;
+ box-shadow: 1px 2px 3px #5454547a, 0px 0px 1px #565656;
+ backdrop-filter: blur(4px);
+ border: 1px solid #848484;
+ font-family: 'M PLUS Medium 1c', 'Arial', sans-serif;
+ font-size: 14px;
+ color: white;
+ white-space: nowrap;
+ padding: 0.5em;
+ box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
+ pointer-events: none;
+ opacity: 0;
+ transition: 1s;
+ transition-delay: 0s;
+}
+.topPanel > [data-tooltip]::after {
+ left: 0px;
+}
+.topPanel > [data-tooltip]:hover::after {
+ top: 36px;
+}
+[data-tooltip]:hover::after {
+ opacity: 1;
+ top: -50px;
+ transition-delay: 2s;
+}
+
+@media (max-width: 610px) {
+ .container {
+ display: block;
+ }
+ .dockPanel {
+ left: 0;
+ right: 0;
+ }
+ .main-tools {
+ overflow-x: auto;
+ overflow-y: hidden;
+ }
+ [data-tooltip]::after {
+ display: none;
+ }
+}
diff --git a/css/empty.css b/css/empty.css
new file mode 100644
index 0000000..68bedf6
--- /dev/null
+++ b/css/empty.css
@@ -0,0 +1,37 @@
+::-webkit-scrollbar {
+ display: none;
+}
+* {
+ -webkit-tap-highlight-color: transparent;
+}
+html,
+body {
+ display: flex;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+}
+body {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-end;
+ gap: 10px;
+ background-color: black;
+}
+
+a {
+ color: white;
+ font: 1.5em 'Fira Sans', sans-serif;
+ text-decoration: none;
+ border-bottom: 0.1em solid #00000000;
+ transition: transform 0.3s, border-bottom 0.3s;
+}
+a:hover {
+ border-bottom: 0.1em solid white;
+ transform: scale(1.1);
+}
+a:active {
+ border-bottom: 0.1em solid white;
+ transform: scale(0.9);
+}
diff --git a/css/index.css b/css/index.css
new file mode 100644
index 0000000..f72326a
--- /dev/null
+++ b/css/index.css
@@ -0,0 +1,51 @@
+::-webkit-scrollbar {
+ display: none;
+}
+* {
+ -webkit-tap-highlight-color: transparent;
+}
+html,
+body {
+ display: flex;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+}
+body {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ background-color: black;
+}
+
+#background {
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ left: 0px;
+ z-index: -1;
+ background: linear-gradient(130deg, #03001e, #cc5333);
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+a:first-of-type:hover ~ #background {
+ opacity: 1;
+}
+
+a {
+ color: white;
+ font: 1.5em 'Fira Sans', sans-serif;
+ text-decoration: none;
+ border-bottom: 0.1em solid #00000000;
+ transition: transform 0.3s, border-bottom 0.3s;
+}
+a:hover {
+ border-bottom: 0.1em solid white;
+ transform: scale(1.1);
+}
+a:active {
+ border-bottom: 0.1em solid white;
+ transform: scale(0.9);
+}
diff --git a/empty.html b/empty.html
new file mode 100644
index 0000000..a49783f
--- /dev/null
+++ b/empty.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+ Empty page — Samutichev
+
+
+
+ ← Go back
+
+
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..abac44f
Binary files /dev/null and b/favicon.ico differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..2548c45
--- /dev/null
+++ b/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+ Samutichev
+
+
+
+ https://github.com/Samutichev
+ Empty page →
+ Go drawing →
+
+
+
diff --git a/js/draw.js b/js/draw.js
new file mode 100644
index 0000000..0807011
--- /dev/null
+++ b/js/draw.js
@@ -0,0 +1,132 @@
+function appendPath() {
+ const path = scene.appendChild(
+ document.createElementNS('http://www.w3.org/2000/svg', 'path')
+ )
+ if (boardMode == 'pen') {
+ path.setAttribute('stroke', colorOption)
+ path.setAttribute('stroke-width', widthOption)
+ } else if (boardMode == 'eraser') {
+ path.setAttribute('stroke', 'white')
+ path.setAttribute('stroke-width', widthOption * 5)
+ }
+ return path
+}
+function pointsToPath(points) {
+ return (
+ 'M' +
+ points
+ .map(function (p) {
+ return (
+ (p.x || p[0] || 0).toFixed(0) +
+ ',' +
+ (p.y || p[1] || 0).toFixed(0)
+ )
+ })
+ .join('L')
+ )
+}
+
+let points
+let simplify2Path
+let lockDrawing = false
+
+board.onpointerdown = function (event) {
+ if (!event.isPrimary) {
+ lockDrawing = true
+ simplify2Path.remove()
+ }
+ if (
+ event.button == 0 &&
+ !lockDrawing &&
+ (boardMode == 'pen' || boardMode == 'eraser')
+ ) {
+ points = [
+ [
+ (event.offsetX - transformX) / transformScale,
+ (event.offsetY - transformY) / transformScale
+ ]
+ ]
+ simplify2Path = appendPath()
+ this.setPointerCapture(event.pointerId)
+ }
+}
+board.onpointermove = function (event) {
+ if (
+ this.hasPointerCapture(event.pointerId) &&
+ !lockDrawing &&
+ (boardMode == 'pen' || boardMode == 'eraser')
+ ) {
+ points.push([
+ (event.offsetX - transformX) / transformScale,
+ (event.offsetY - transformY) / transformScale
+ ])
+ const simplifyJsApplied = simplify(
+ points.map(function (p) {
+ return { x: p[0], y: p[1] }
+ }, 2.5),
+ true
+ )
+ simplify2Path.setAttribute('d', pointsToPath(points))
+ // simplify2Path.setAttribute('d', simplifySvgPath(simplifyJsApplied.map(function (p) { return [p.x, p.y] }), { tolerance: 2.5, precision: 0 }))
+ }
+ if (!event.isPrimary || event.buttons == 4) {
+ scene.style.willChange = 'transform'
+ background.style.willChange = 'background-position, background-size'
+ board.style.shapeRendering = 'optimizeSpeed'
+ }
+}
+board.onpointerup = function (event) {
+ if (event.button == 0 && !lockDrawing && boardMode == 'pen') {
+ if (this.hasPointerCapture(event.pointerId)) {
+ points.push([
+ (event.offsetX - transformX) / transformScale,
+ (event.offsetY - transformY) / transformScale
+ ])
+ const simplifyJsApplied = simplify(
+ points.map(function (p) {
+ return { x: p[0], y: p[1] }
+ }, 2.5),
+ true
+ )
+ simplify2Path.setAttribute(
+ 'd',
+ simplifySvgPath(
+ simplifyJsApplied.map(function (p) {
+ return [p.x, p.y]
+ }),
+ { tolerance: 2.5, precision: 0 }
+ )
+ )
+ // emitObject(simplify2Path)
+ }
+ } else if (event.button == 0 && !lockDrawing && boardMode == 'eraser') {
+ if (this.hasPointerCapture(event.pointerId)) {
+ points.push([
+ (event.offsetX - transformX) / transformScale,
+ (event.offsetY - transformY) / transformScale
+ ])
+ const simplifyJsApplied = simplify(
+ points.map(function (p) {
+ return { x: p[0], y: p[1] }
+ }, 2.5),
+ true
+ )
+ simplify2Path.setAttribute(
+ 'd',
+ simplifySvgPath(
+ simplifyJsApplied.map(function (p) {
+ return [p.x, p.y]
+ }),
+ { tolerance: 0, precision: 0 }
+ )
+ )
+ // emitObject(simplify2Path)
+ }
+ }
+ scene.style.willChange = 'auto'
+ background.style.willChange = 'auto'
+ board.style.shapeRendering = 'geometricPrecision'
+ setTimeout(() => {
+ lockDrawing = false
+ }, 10)
+}
diff --git a/js/export.js b/js/export.js
new file mode 100644
index 0000000..468418d
--- /dev/null
+++ b/js/export.js
@@ -0,0 +1,7 @@
+// Export SVG
+const exportBoardAsSVG = () => {
+ svgExport.downloadSvg(document.getElementById('board'), 'board', {
+ width: 200,
+ height: 200
+ })
+}
diff --git a/js/functions.js b/js/functions.js
new file mode 100644
index 0000000..b49f313
--- /dev/null
+++ b/js/functions.js
@@ -0,0 +1,121 @@
+// Сlear board
+function clearBoard() {
+ board.style.transform = `scale(0.8)`
+ board.style.opacity = 0
+ setTimeout(() => {
+ scene.innerHTML = ''
+ board.style.opacity = 1
+ board.style.transform = `scale(1)`
+ }, 150)
+}
+document.addEventListener('keydown', function (event) {
+ if (event.code == 'Delete') {
+ clearBoard()
+ }
+})
+
+options = {
+ color: 0,
+ size: 1,
+ pattern: 0
+}
+// Change pen colors by scrolling
+function scrollColor(event) {
+ colorOptions = [
+ '#000000',
+ '#d01919',
+ '#eaae00',
+ '#16ab39',
+ '#1678c2',
+ customColor
+ ]
+ if (event.deltaY > 0) {
+ options.color += 1
+ if (options.color == colorOptions.length) {
+ options.color = 0
+ }
+ } else {
+ if (!options.color) {
+ options.color = colorOptions.length
+ }
+ options.color -= 1
+ }
+ setBrush({ color: colorOptions[options.color] })
+}
+// Change size of drawing subject by scrolling
+function scrollSize(event) {
+ sizeOptions = [3, 4, 5]
+ if (event.deltaY > 0) {
+ options.size += 1
+ if (options.size == sizeOptions.length) {
+ options.size = 0
+ }
+ } else {
+ if (!options.size) {
+ options.size = sizeOptions.length
+ }
+ options.size -= 1
+ }
+ setBrush({ size: sizeOptions[options.size] })
+}
+// Change board patterns by scrolling
+function scrollPattern(event) {
+ patternOptions = ['none', 'sq', 'line', 'dot']
+ if (event.deltaY > 0) {
+ options.pattern += 1
+ if (options.pattern == patternOptions.length) {
+ options.pattern = 0
+ }
+ } else {
+ if (!options.pattern) {
+ options.pattern = patternOptions.length
+ }
+ options.pattern -= 1
+ }
+ setPattern(patternOptions[options.pattern])
+}
+
+// Copy page link
+function copyLink() {
+ navigator.clipboard
+ .writeText(window.location.href)
+ .then(() => {
+ // Link copied successfully!
+ })
+ .catch((err) => {
+ console.log('Something went wrong', err)
+ })
+}
+document.addEventListener('keydown', function (event) {
+ if (event.code == 'KeyC' && (event.ctrlKey || event.metaKey)) {
+ copyLink()
+ }
+})
+
+// Effects
+// "Fade in"
+const fadeIn = (cl, timeout) => {
+ let element = document.querySelector(cl)
+ element.style.opacity = 0
+ element.style.display = 'flex'
+ element.style.transition = `opacity ${timeout}ms`
+ setTimeout(() => {
+ element.style.opacity = 1
+ }, 10)
+}
+// "Fade out"
+const fadeOut = (cl, timeout) => {
+ let element = document.querySelector(cl)
+ element.style.opacity = 1
+ element.style.transition = `opacity ${timeout}ms`
+ element.style.opacity = 0
+
+ setTimeout(() => {
+ element.style.display = 'none'
+ }, timeout)
+}
+
+// Command
+function sendCommand(command) {
+ console.log('New command:', command)
+}
diff --git a/js/lib/panzoom.js b/js/lib/panzoom.js
new file mode 100644
index 0000000..bad06c8
--- /dev/null
+++ b/js/lib/panzoom.js
@@ -0,0 +1,1836 @@
+let transformX
+let transformY
+let transformScale
+
+
+(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 () {
+ 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 })()({
+ 1: [function (require, module, exports) {
+ 'use strict';
+ /**
+ * Allows to drag and zoom svg elements
+ */
+ var wheel = require('wheel');
+ var animate = require('amator');
+ var eventify = require('ngraph.events');
+ var kinetic = require('./lib/kinetic.js');
+ var createTextSelectionInterceptor = require('./lib/createTextSelectionInterceptor.js');
+ var domTextSelectionInterceptor = createTextSelectionInterceptor();
+ var fakeTextSelectorInterceptor = createTextSelectionInterceptor(true);
+ var Transform = require('./lib/transform.js');
+ var makeSvgController = require('./lib/svgController.js');
+ var makeDomController = require('./lib/domController.js');
+
+ var defaultZoomSpeed = 1;
+ var defaultDoubleTapZoomSpeed = 1.75;
+ var doubleTapSpeedInMS = 300;
+ var clickEventTimeInMS = 200;
+
+ module.exports = createPanZoom;
+
+ /**
+ * Creates a new instance of panzoom, so that an object can be panned and zoomed
+ *
+ * @param {DOMElement} domElement where panzoom should be attached.
+ * @param {Object} options that configure behavior.
+ */
+ function createPanZoom(domElement, options) {
+ options = options || {};
+
+ var panController = options.controller;
+
+ if (!panController) {
+ if (makeSvgController.canAttach(domElement)) {
+ panController = makeSvgController(domElement, options);
+ } else if (makeDomController.canAttach(domElement)) {
+ panController = makeDomController(domElement, options);
+ }
+ }
+
+ if (!panController) {
+ throw new Error(
+ 'Cannot create panzoom for the current type of dom element'
+ );
+ }
+ var owner = panController.getOwner();
+ // just to avoid GC pressure, every time we do intermediate transform
+ // we return this object. For internal use only. Never give it back to the consumer of this library
+ var storedCTMResult = { x: 0, y: 0 };
+
+ var isDirty = false;
+ var transform = new Transform();
+
+ if (panController.initTransform) {
+ panController.initTransform(transform);
+ }
+
+ var filterKey = typeof options.filterKey === 'function' ? options.filterKey : noop;
+ // TODO: likely need to unite pinchSpeed with zoomSpeed
+ var pinchSpeed = typeof options.pinchSpeed === 'number' ? options.pinchSpeed : 1;
+ var bounds = options.bounds;
+ var maxZoom = typeof options.maxZoom === 'number' ? options.maxZoom : Number.POSITIVE_INFINITY;
+ var minZoom = typeof options.minZoom === 'number' ? options.minZoom : 0;
+
+ var boundsPadding = typeof options.boundsPadding === 'number' ? options.boundsPadding : 0.05;
+ var zoomDoubleClickSpeed = typeof options.zoomDoubleClickSpeed === 'number' ? options.zoomDoubleClickSpeed : defaultDoubleTapZoomSpeed;
+ var beforeWheel = options.beforeWheel || noop;
+ var beforeMouseDown = options.beforeMouseDown || noop;
+ var speed = typeof options.zoomSpeed === 'number' ? options.zoomSpeed : defaultZoomSpeed;
+ var transformOrigin = parseTransformOrigin(options.transformOrigin);
+ var textSelection = options.enableTextSelection ? fakeTextSelectorInterceptor : domTextSelectionInterceptor;
+
+ validateBounds(bounds);
+
+ if (options.autocenter) {
+ autocenter();
+ }
+
+ var frameAnimation;
+ var lastTouchEndTime = 0;
+ var lastTouchStartTime = 0;
+ var pendingClickEventTimeout = 0;
+ var lastMouseDownedEvent = null;
+ var lastMouseDownTime = new Date();
+ var lastSingleFingerOffset;
+ var touchInProgress = false;
+
+ // We only need to fire panstart when actual move happens
+ var panstartFired = false;
+
+ // cache mouse coordinates here
+ var mouseX;
+ var mouseY;
+
+ // Where the first click has happened, so that we can differentiate
+ // between pan and click
+ var clickX;
+ var clickY;
+
+ var pinchZoomLength;
+
+ var smoothScroll;
+ if ('smoothScroll' in options && !options.smoothScroll) {
+ // If user explicitly asked us not to use smooth scrolling, we obey
+ smoothScroll = rigidScroll();
+ } else {
+ // otherwise we use forward smoothScroll settings to kinetic API
+ // which makes scroll smoothing.
+ smoothScroll = kinetic(getPoint, scroll, options.smoothScroll);
+ }
+
+ var moveByAnimation;
+ var zoomToAnimation;
+
+ var multiTouch;
+ var paused = false;
+
+ listenForEvents();
+
+ var api = {
+ dispose: dispose,
+ moveBy: internalMoveBy,
+ moveTo: moveTo,
+ smoothMoveTo: smoothMoveTo,
+ centerOn: centerOn,
+ zoomTo: publicZoomTo,
+ zoomAbs: zoomAbs,
+ smoothZoom: smoothZoom,
+ smoothZoomAbs: smoothZoomAbs,
+ showRectangle: showRectangle,
+
+ pause: pause,
+ resume: resume,
+ isPaused: isPaused,
+
+ getTransform: getTransformModel,
+
+ getMinZoom: getMinZoom,
+ setMinZoom: setMinZoom,
+
+ getMaxZoom: getMaxZoom,
+ setMaxZoom: setMaxZoom,
+
+ getTransformOrigin: getTransformOrigin,
+ setTransformOrigin: setTransformOrigin,
+
+ getZoomSpeed: getZoomSpeed,
+ setZoomSpeed: setZoomSpeed
+ };
+
+ eventify(api);
+
+ var initialX = typeof options.initialX === 'number' ? options.initialX : transform.x;
+ var initialY = typeof options.initialY === 'number' ? options.initialY : transform.y;
+ var initialZoom = typeof options.initialZoom === 'number' ? options.initialZoom : transform.scale;
+
+ if (initialX != transform.x || initialY != transform.y || initialZoom != transform.scale) {
+ zoomAbs(initialX, initialY, initialZoom);
+ }
+
+ return api;
+
+ function pause() {
+ releaseEvents();
+ paused = true;
+ }
+
+ function resume() {
+ if (paused) {
+ listenForEvents();
+ paused = false;
+ }
+ }
+
+ function isPaused() {
+ return paused;
+ }
+
+ function showRectangle(rect) {
+ // TODO: this duplicates autocenter. I think autocenter should go.
+ var clientRect = owner.getBoundingClientRect();
+ var size = transformToScreen(clientRect.width, clientRect.height);
+
+ var rectWidth = rect.right - rect.left;
+ var rectHeight = rect.bottom - rect.top;
+ if (!Number.isFinite(rectWidth) || !Number.isFinite(rectHeight)) {
+ throw new Error('Invalid rectangle');
+ }
+
+ var dw = size.x / rectWidth;
+ var dh = size.y / rectHeight;
+ var scale = Math.min(dw, dh);
+ transform.x = -(rect.left + rectWidth / 2) * scale + size.x / 2;
+ transform.y = -(rect.top + rectHeight / 2) * scale + size.y / 2;
+ transform.scale = scale;
+ }
+
+ function transformToScreen(x, y) {
+ if (panController.getScreenCTM) {
+ var parentCTM = panController.getScreenCTM();
+ var parentScaleX = parentCTM.a;
+ var parentScaleY = parentCTM.d;
+ var parentOffsetX = parentCTM.e;
+ var parentOffsetY = parentCTM.f;
+ storedCTMResult.x = x * parentScaleX - parentOffsetX;
+ storedCTMResult.y = y * parentScaleY - parentOffsetY;
+ } else {
+ storedCTMResult.x = x;
+ storedCTMResult.y = y;
+ }
+
+ return storedCTMResult;
+ }
+
+ function autocenter() {
+ var w; // width of the parent
+ var h; // height of the parent
+ var left = 0;
+ var top = 0;
+ var sceneBoundingBox = getBoundingBox();
+ if (sceneBoundingBox) {
+ // If we have bounding box - use it.
+ left = sceneBoundingBox.left;
+ top = sceneBoundingBox.top;
+ w = sceneBoundingBox.right - sceneBoundingBox.left;
+ h = sceneBoundingBox.bottom - sceneBoundingBox.top;
+ } else {
+ // otherwise just use whatever space we have
+ var ownerRect = owner.getBoundingClientRect();
+ w = ownerRect.width;
+ h = ownerRect.height;
+ }
+ var bbox = panController.getBBox();
+ if (bbox.width === 0 || bbox.height === 0) {
+ // we probably do not have any elements in the SVG
+ // just bail out;
+ return;
+ }
+ var dh = h / bbox.height;
+ var dw = w / bbox.width;
+ var scale = Math.min(dw, dh);
+ transform.x = -(bbox.left + bbox.width / 2) * scale + w / 2 + left;
+ transform.y = -(bbox.top + bbox.height / 2) * scale + h / 2 + top;
+ transform.scale = scale;
+ }
+
+ function getTransformModel() {
+ // TODO: should this be read only?
+ return transform;
+ }
+
+ function getMinZoom() {
+ return minZoom;
+ }
+
+ function setMinZoom(newMinZoom) {
+ minZoom = newMinZoom;
+ }
+
+ function getMaxZoom() {
+ return maxZoom;
+ }
+
+ function setMaxZoom(newMaxZoom) {
+ maxZoom = newMaxZoom;
+ }
+
+ function getTransformOrigin() {
+ return transformOrigin;
+ }
+
+ function setTransformOrigin(newTransformOrigin) {
+ transformOrigin = parseTransformOrigin(newTransformOrigin);
+ }
+
+ function getZoomSpeed() {
+ return speed;
+ }
+
+ function setZoomSpeed(newSpeed) {
+ if (!Number.isFinite(newSpeed)) {
+ throw new Error('Zoom speed should be a number');
+ }
+ speed = newSpeed;
+ }
+
+ function getPoint() {
+ return {
+ x: transform.x,
+ y: transform.y
+ };
+ }
+
+ function moveTo(x, y) {
+ transform.x = x;
+ transform.y = y;
+
+ keepTransformInsideBounds();
+
+ triggerEvent('pan');
+ makeDirty();
+ }
+
+ function moveBy(dx, dy) {
+ moveTo(transform.x + dx, transform.y + dy);
+ }
+
+ function keepTransformInsideBounds() {
+ var boundingBox = getBoundingBox();
+ if (!boundingBox) return;
+
+ var adjusted = false;
+ var clientRect = getClientRect();
+
+ var diff = boundingBox.left - clientRect.right;
+ if (diff > 0) {
+ transform.x += diff;
+ adjusted = true;
+ }
+ // check the other side:
+ diff = boundingBox.right - clientRect.left;
+ if (diff < 0) {
+ transform.x += diff;
+ adjusted = true;
+ }
+
+ // y axis:
+ diff = boundingBox.top - clientRect.bottom;
+ if (diff > 0) {
+ // we adjust transform, so that it matches exactly our bounding box:
+ // transform.y = boundingBox.top - (boundingBox.height + boundingBox.y) * transform.scale =>
+ // transform.y = boundingBox.top - (clientRect.bottom - transform.y) =>
+ // transform.y = diff + transform.y =>
+ transform.y += diff;
+ adjusted = true;
+ }
+
+ diff = boundingBox.bottom - clientRect.top;
+ if (diff < 0) {
+ transform.y += diff;
+ adjusted = true;
+ }
+ return adjusted;
+ }
+
+ /**
+ * Returns bounding box that should be used to restrict scene movement.
+ */
+ function getBoundingBox() {
+ if (!bounds) return; // client does not want to restrict movement
+
+ if (typeof bounds === 'boolean') {
+ // for boolean type we use parent container bounds
+ var ownerRect = owner.getBoundingClientRect();
+ var sceneWidth = ownerRect.width;
+ var sceneHeight = ownerRect.height;
+
+ return {
+ left: sceneWidth * boundsPadding,
+ top: sceneHeight * boundsPadding,
+ right: sceneWidth * (1 - boundsPadding),
+ bottom: sceneHeight * (1 - boundsPadding)
+ };
+ }
+
+ return bounds;
+ }
+
+ function getClientRect() {
+ var bbox = panController.getBBox();
+ var leftTop = client(bbox.left, bbox.top);
+
+ return {
+ left: leftTop.x,
+ top: leftTop.y,
+ right: bbox.width * transform.scale + leftTop.x,
+ bottom: bbox.height * transform.scale + leftTop.y
+ };
+ }
+
+ function client(x, y) {
+ return {
+ x: x * transform.scale + transform.x,
+ y: y * transform.scale + transform.y
+ };
+ }
+
+ function makeDirty() {
+ isDirty = true;
+ frameAnimation = window.requestAnimationFrame(frame);
+ }
+
+ function zoomByRatio(clientX, clientY, ratio) {
+ if (isNaN(clientX) || isNaN(clientY) || isNaN(ratio)) {
+ throw new Error('zoom requires valid numbers');
+ }
+
+ var newScale = transform.scale * ratio;
+
+ if (newScale < minZoom) {
+ if (transform.scale === minZoom) return;
+
+ ratio = minZoom / transform.scale;
+ }
+ if (newScale > maxZoom) {
+ if (transform.scale === maxZoom) return;
+
+ ratio = maxZoom / transform.scale;
+ }
+
+ var size = transformToScreen(clientX, clientY);
+
+ transform.x = size.x - ratio * (size.x - transform.x);
+ transform.y = size.y - ratio * (size.y - transform.y);
+
+ // TODO: https://github.com/anvaka/panzoom/issues/112
+ if (bounds && boundsPadding === 1 && minZoom === 1) {
+ transform.scale *= ratio;
+ keepTransformInsideBounds();
+ } else {
+ var transformAdjusted = keepTransformInsideBounds();
+ if (!transformAdjusted) transform.scale *= ratio;
+ }
+
+ triggerEvent('zoom');
+
+ makeDirty();
+ }
+
+ function zoomAbs(clientX, clientY, zoomLevel) {
+ var ratio = zoomLevel / transform.scale;
+ zoomByRatio(clientX, clientY, ratio);
+ }
+
+ function centerOn(ui) {
+ var parent = ui.ownerSVGElement;
+ if (!parent)
+ throw new Error('ui element is required to be within the scene');
+
+ // TODO: should i use controller's screen CTM?
+ var clientRect = ui.getBoundingClientRect();
+ var cx = clientRect.left + clientRect.width / 2;
+ var cy = clientRect.top + clientRect.height / 2;
+
+ var container = parent.getBoundingClientRect();
+ var dx = container.width / 2 - cx;
+ var dy = container.height / 2 - cy;
+
+ internalMoveBy(dx, dy, true);
+ }
+
+ function smoothMoveTo(x, y) {
+ internalMoveBy(x - transform.x, y - transform.y, true);
+ }
+
+ function internalMoveBy(dx, dy, smooth) {
+ if (!smooth) {
+ return moveBy(dx, dy);
+ }
+
+ if (moveByAnimation) moveByAnimation.cancel();
+
+ var from = { x: 0, y: 0 };
+ var to = { x: dx, y: dy };
+ var lastX = 0;
+ var lastY = 0;
+
+ moveByAnimation = animate(from, to, {
+ step: function (v) {
+ moveBy(v.x - lastX, v.y - lastY);
+
+ lastX = v.x;
+ lastY = v.y;
+ }
+ });
+ }
+
+ function scroll(x, y) {
+ cancelZoomAnimation();
+ moveTo(x, y);
+ }
+
+ function dispose() {
+ releaseEvents();
+ }
+
+ function listenForEvents() {
+ owner.addEventListener('mousedown', onMouseDown, { passive: false });
+ owner.addEventListener('dblclick', onDoubleClick, { passive: false });
+ owner.addEventListener('touchstart', onTouch, { passive: false });
+ owner.addEventListener('keydown', onKeyDown, { passive: false });
+
+ // Need to listen on the owner container, so that we are not limited
+ // by the size of the scrollable domElement
+ wheel.addWheelListener(owner, onMouseWheel, { passive: false });
+
+ makeDirty();
+ }
+
+ function releaseEvents() {
+ wheel.removeWheelListener(owner, onMouseWheel);
+ owner.removeEventListener('mousedown', onMouseDown);
+ owner.removeEventListener('keydown', onKeyDown);
+ owner.removeEventListener('dblclick', onDoubleClick);
+ owner.removeEventListener('touchstart', onTouch);
+
+ if (frameAnimation) {
+ window.cancelAnimationFrame(frameAnimation);
+ frameAnimation = 0;
+ }
+
+ smoothScroll.cancel();
+
+ releaseDocumentMouse();
+ releaseTouches();
+ textSelection.release();
+
+ triggerPanEnd();
+ }
+
+ function frame() {
+ if (isDirty) applyTransform();
+ }
+
+ function applyTransform() {
+ isDirty = false;
+
+ // TODO: Should I allow to cancel this?
+ panController.applyTransform(transform);
+
+ triggerEvent('transform');
+ frameAnimation = 0;
+ }
+
+ function onKeyDown(e) {
+ var x = 0,
+ y = 0,
+ z = 0;
+ if (e.keyCode === 38) {
+ y = 1; // up
+ } else if (e.keyCode === 40) {
+ y = -1; // down
+ } else if (e.keyCode === 37) {
+ x = 1; // left
+ } else if (e.keyCode === 39) {
+ x = -1; // right
+ } else if (e.keyCode === 189 || e.keyCode === 109) {
+ // DASH or SUBTRACT
+ z = 1; // `-` - zoom out
+ } else if (e.keyCode === 187 || e.keyCode === 107) {
+ // EQUAL SIGN or ADD
+ z = -1; // `=` - zoom in (equal sign on US layout is under `+`)
+ }
+
+ if (filterKey(e, x, y, z)) {
+ // They don't want us to handle the key: https://github.com/anvaka/panzoom/issues/45
+ return;
+ }
+
+ if (x || y) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ var clientRect = owner.getBoundingClientRect();
+ // movement speed should be the same in both X and Y direction:
+ var offset = Math.min(clientRect.width, clientRect.height);
+ var moveSpeedRatio = 0.05;
+ var dx = offset * moveSpeedRatio * x;
+ var dy = offset * moveSpeedRatio * y;
+
+ // TODO: currently we do not animate this. It could be better to have animation
+ internalMoveBy(dx, dy);
+ }
+
+ if (z) {
+ var scaleMultiplier = getScaleMultiplier(z * 100);
+ var offset = transformOrigin ? getTransformOriginOffset() : midPoint();
+ publicZoomTo(offset.x, offset.y, scaleMultiplier);
+ }
+ }
+
+ function midPoint() {
+ var ownerRect = owner.getBoundingClientRect();
+ return {
+ x: ownerRect.width / 2,
+ y: ownerRect.height / 2
+ };
+ }
+
+ function onTouch(e) {
+ // let them override the touch behavior
+ beforeTouch(e);
+ clearPendingClickEventTimeout();
+
+ if (e.touches.length === 2) {
+ handleFingersTouch(e);
+ // handleTouchMove() will care about pinch zoom.
+ pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]);
+ multiTouch = true;
+ startTouchListenerIfNeeded();
+ }
+ }
+
+ function beforeTouch(e) {
+ // TODO: Need to unify this filtering names. E.g. use `beforeTouch`
+ if (options.onTouch && !options.onTouch(e)) {
+ // if they return `false` from onTouch, we don't want to stop
+ // events propagation. Fixes https://github.com/anvaka/panzoom/issues/12
+ return;
+ }
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ function beforeDoubleClick(e) {
+ clearPendingClickEventTimeout();
+
+ // TODO: Need to unify this filtering names. E.g. use `beforeDoubleClick``
+ if (options.onDoubleClick && !options.onDoubleClick(e)) {
+ // if they return `false` from onTouch, we don't want to stop
+ // events propagation. Fixes https://github.com/anvaka/panzoom/issues/46
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ function handleFingersTouch(e) {
+ lastTouchStartTime = new Date();
+
+ var touch1 = e.touches[0];
+ var touch2 = e.touches[1];
+
+ var offset1 = getOffsetXY(touch1);
+ var offset2 = getOffsetXY(touch2);
+ var offset = {x: (offset1.x + offset2.x) / 2, y: (offset1.y + offset2.y) / 2}
+ lastSingleFingerOffset = offset;
+
+ var point = transformToScreen(offset.x, offset.y);
+
+ mouseX = point.x;
+ mouseY = point.y;
+
+ clickX = mouseX;
+ clickY = mouseY;
+
+ smoothScroll.cancel();
+ startTouchListenerIfNeeded();
+ }
+
+ function startTouchListenerIfNeeded() {
+ if (touchInProgress) {
+ // no need to do anything, as we already listen to events;
+ return;
+ }
+
+ touchInProgress = true;
+ document.addEventListener('touchmove', handleTouchMove);
+ document.addEventListener('touchend', handleTouchEnd);
+ document.addEventListener('touchcancel', handleTouchEnd);
+ }
+
+ function handleTouchMove(e) {
+ if (e.touches.length === 2) {
+ multiTouch = true;
+ //it's two finger touch, we need to move first; and keep the mouseX/Y for move;
+ var touch1 = e.touches[0];
+ var touch2 = e.touches[1];
+
+ var offset1 = getOffsetXY(touch1);
+ var offset2 = getOffsetXY(touch2);
+ var offset = {x: (offset1.x + offset2.x) / 2, y: (offset1.y + offset2.y) / 2}
+
+ var point = transformToScreen(offset.x, offset.y);
+
+ var dx = point.x - mouseX;
+ var dy = point.y - mouseY;
+
+ if (dx !== 0 && dy !== 0) {
+ triggerPanStart();
+ }
+ mouseX = offset.x;
+ mouseY = offset.y;
+
+ internalMoveBy(dx, dy);
+ //move code up
+
+ // it's a zoom, let's find direction
+ //Then let's start to move, caclulate the zoom,
+
+ var currentPinchLength = getPinchZoomLength(touch1, touch2);
+
+ // since the zoom speed is always based on distance from 1, we need to apply
+ // pinch speed only on that distance from 1:
+ var scaleMultiplier =
+ 1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed;
+
+ if (transformOrigin) {
+ // console.log("transform origin");
+ var offset = getTransformOriginOffset();
+ mouseX = offset.x;
+ mouseY = offset.y;
+ }
+
+ publicZoomTo(mouseX, mouseY, scaleMultiplier);
+
+ pinchZoomLength = currentPinchLength;
+ e.stopPropagation();
+ // e.preventDefault();
+ }
+ }
+
+ function clearPendingClickEventTimeout() {
+ if (pendingClickEventTimeout) {
+ clearTimeout(pendingClickEventTimeout);
+ pendingClickEventTimeout = 0;
+ }
+ }
+
+ function handlePotentialClickEvent(e) {
+ // we could still be in the double tap mode, let's wait until double tap expires,
+ // and then notify:
+ if (!options.onClick) return;
+ clearPendingClickEventTimeout();
+ var dx = mouseX - clickX;
+ var dy = mouseY - clickY;
+ var l = Math.sqrt(dx * dx + dy * dy);
+ if (l > 5) return; // probably they are panning, ignore it
+
+ pendingClickEventTimeout = setTimeout(function () {
+ pendingClickEventTimeout = 0;
+ options.onClick(e);
+ }, doubleTapSpeedInMS);
+ }
+
+ function handleTouchEnd(e) {
+ clearPendingClickEventTimeout();
+ if (e.touches.length > 0) {
+ var offset = getOffsetXY(e.touches[0]);
+ var point = transformToScreen(offset.x, offset.y);
+ mouseX = point.x;
+ mouseY = point.y;
+ } else {
+ var now = new Date();
+ if (now - lastTouchEndTime < doubleTapSpeedInMS) {
+ // They did a double tap here
+ if (transformOrigin) {
+ var offset = getTransformOriginOffset();
+ smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed);
+ } else {
+ // We want untransformed x/y here.
+ smoothZoom(lastSingleFingerOffset.x, lastSingleFingerOffset.y, zoomDoubleClickSpeed);
+ }
+ } else if (now - lastTouchStartTime < clickEventTimeInMS) {
+ handlePotentialClickEvent(e);
+ }
+
+ lastTouchEndTime = now;
+
+ triggerPanEnd();
+ releaseTouches();
+ }
+ }
+
+ function getPinchZoomLength(finger1, finger2) {
+ var dx = finger1.clientX - finger2.clientX;
+ var dy = finger1.clientY - finger2.clientY;
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ function onDoubleClick(e) {
+ beforeDoubleClick(e);
+ var offset = getOffsetXY(e);
+ if (transformOrigin) {
+ // TODO: looks like this is duplicated in the file.
+ // Need to refactor
+ offset = getTransformOriginOffset();
+ }
+ smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed);
+ }
+
+ function onMouseDown(e) {
+ clearPendingClickEventTimeout();
+
+ // if client does not want to handle this event - just ignore the call
+ if (beforeMouseDown(e)) return;
+
+ lastMouseDownedEvent = e;
+ lastMouseDownTime = new Date();
+
+ if (touchInProgress) {
+ // modern browsers will fire mousedown for touch events too
+ // we do not want this: touch is handled separately.
+ e.stopPropagation();
+ return false;
+ }
+ // for IE, left click == 1
+ // for Firefox, left click == 0
+ var isLeftButton =
+ (e.button === 1 && window.event !== null) || e.button === 0;
+ if (!isLeftButton) return;
+
+ smoothScroll.cancel();
+
+ var offset = getOffsetXY(e);
+ var point = transformToScreen(offset.x, offset.y);
+ clickX = mouseX = point.x;
+ clickY = mouseY = point.y;
+
+ // We need to listen on document itself, since mouse can go outside of the
+ // window, and we will loose it
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ textSelection.capture(e.target || e.srcElement);
+
+ return false;
+ }
+
+ function onMouseMove(e) {
+ // no need to worry about mouse events when touch is happening
+ if (touchInProgress) return;
+
+ triggerPanStart();
+
+ var offset = getOffsetXY(e);
+ var point = transformToScreen(offset.x, offset.y);
+ var dx = point.x - mouseX;
+ var dy = point.y - mouseY;
+
+ mouseX = point.x;
+ mouseY = point.y;
+
+ internalMoveBy(dx, dy);
+ }
+
+ function onMouseUp() {
+ var now = new Date();
+ if (now - lastMouseDownTime < clickEventTimeInMS) handlePotentialClickEvent(lastMouseDownedEvent);
+ textSelection.release();
+ triggerPanEnd();
+ releaseDocumentMouse();
+ }
+
+ function releaseDocumentMouse() {
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ panstartFired = false;
+ }
+
+ function releaseTouches() {
+ document.removeEventListener('touchmove', handleTouchMove);
+ document.removeEventListener('touchend', handleTouchEnd);
+ document.removeEventListener('touchcancel', handleTouchEnd);
+ panstartFired = false;
+ multiTouch = false;
+ touchInProgress = false;
+ }
+
+ function onMouseWheel(e) {
+ // if client does not want to handle this event - just ignore the call
+ if (beforeWheel(e)) return;
+
+ smoothScroll.cancel();
+
+ var delta = e.deltaY;
+ if (e.deltaMode > 0) delta *= 100;
+
+ var scaleMultiplier = getScaleMultiplier(delta);
+
+ if (scaleMultiplier !== 1) {
+ var offset = transformOrigin
+ ? getTransformOriginOffset()
+ : getOffsetXY(e);
+ publicZoomTo(offset.x, offset.y, scaleMultiplier);
+ e.preventDefault();
+ }
+ }
+
+ function getOffsetXY(e) {
+ var offsetX, offsetY;
+ // I tried using e.offsetX, but that gives wrong results for svg, when user clicks on a path.
+ var ownerRect = owner.getBoundingClientRect();
+ offsetX = e.clientX - ownerRect.left;
+ offsetY = e.clientY - ownerRect.top;
+
+ return { x: offsetX, y: offsetY };
+ }
+
+ function smoothZoom(clientX, clientY, scaleMultiplier) {
+ var fromValue = transform.scale;
+ var from = { scale: fromValue };
+ var to = { scale: scaleMultiplier * fromValue };
+
+ smoothScroll.cancel();
+ cancelZoomAnimation();
+
+ zoomToAnimation = animate(from, to, {
+ step: function (v) {
+ zoomAbs(clientX, clientY, v.scale);
+ },
+ done: triggerZoomEnd
+ });
+ }
+
+ function smoothZoomAbs(clientX, clientY, toScaleValue) {
+ var fromValue = transform.scale;
+ var from = { scale: fromValue };
+ var to = { scale: toScaleValue };
+
+ smoothScroll.cancel();
+ cancelZoomAnimation();
+
+ zoomToAnimation = animate(from, to, {
+ step: function (v) {
+ zoomAbs(clientX, clientY, v.scale);
+ }
+ });
+ }
+
+ function getTransformOriginOffset() {
+ var ownerRect = owner.getBoundingClientRect();
+ return {
+ x: ownerRect.width * transformOrigin.x,
+ y: ownerRect.height * transformOrigin.y
+ };
+ }
+
+ function publicZoomTo(clientX, clientY, scaleMultiplier) {
+ smoothScroll.cancel();
+ cancelZoomAnimation();
+ return zoomByRatio(clientX, clientY, scaleMultiplier);
+ }
+
+ function cancelZoomAnimation() {
+ if (zoomToAnimation) {
+ zoomToAnimation.cancel();
+ zoomToAnimation = null;
+ }
+ }
+
+ function getScaleMultiplier(delta) {
+ var sign = Math.sign(delta);
+ var deltaAdjustedSpeed = Math.min(0.25, Math.abs(speed * delta / 128));
+ return 1 - sign * deltaAdjustedSpeed;
+ }
+
+ function triggerPanStart() {
+ if (!panstartFired) {
+ triggerEvent('panstart');
+ panstartFired = true;
+ smoothScroll.start();
+ }
+ }
+
+ function triggerPanEnd() {
+ if (panstartFired) {
+ // we should never run smooth scrolling if it was multiTouch (pinch zoom animation):
+ if (!multiTouch) smoothScroll.stop();
+ triggerEvent('panend');
+ }
+ }
+
+ function triggerZoomEnd() {
+ triggerEvent('zoomend');
+ }
+
+ function triggerEvent(name) {
+ api.fire(name, api);
+ }
+ }
+
+ function parseTransformOrigin(options) {
+ if (!options) return;
+ if (typeof options === 'object') {
+ if (!isNumber(options.x) || !isNumber(options.y))
+ failTransformOrigin(options);
+ return options;
+ }
+
+ failTransformOrigin();
+ }
+
+ function failTransformOrigin(options) {
+ console.error(options);
+ throw new Error(
+ [
+ 'Cannot parse transform origin.',
+ 'Some good examples:',
+ ' "center center" can be achieved with {x: 0.5, y: 0.5}',
+ ' "top center" can be achieved with {x: 0.5, y: 0}',
+ ' "bottom right" can be achieved with {x: 1, y: 1}'
+ ].join('\n')
+ );
+ }
+
+ function noop() { }
+
+ function validateBounds(bounds) {
+ var boundsType = typeof bounds;
+ if (boundsType === 'undefined' || boundsType === 'boolean') return; // this is okay
+ // otherwise need to be more thorough:
+ var validBounds =
+ isNumber(bounds.left) &&
+ isNumber(bounds.top) &&
+ isNumber(bounds.bottom) &&
+ isNumber(bounds.right);
+
+ if (!validBounds)
+ throw new Error(
+ 'Bounds object is not valid. It can be: ' +
+ 'undefined, boolean (true|false) or an object {left, top, right, bottom}'
+ );
+ }
+
+ function isNumber(x) {
+ return Number.isFinite(x);
+ }
+
+ // IE 11 does not support isNaN:
+ function isNaN(value) {
+ if (Number.isNaN) {
+ return Number.isNaN(value);
+ }
+
+ return value !== value;
+ }
+
+ function rigidScroll() {
+ return {
+ start: noop,
+ stop: noop,
+ cancel: noop
+ };
+ }
+
+ function autoRun() {
+ if (typeof document === 'undefined') return;
+
+ var scripts = document.getElementsByTagName('script');
+ if (!scripts) return;
+ var panzoomScript;
+
+ for (var i = 0; i < scripts.length; ++i) {
+ var x = scripts[i];
+ if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) {
+ panzoomScript = x;
+ break;
+ }
+ }
+
+ if (!panzoomScript) return;
+
+ var query = panzoomScript.getAttribute('query');
+ if (!query) return;
+
+ var globalName = panzoomScript.getAttribute('name') || 'pz';
+ var started = Date.now();
+
+ tryAttach();
+
+ function tryAttach() {
+ var el = document.querySelector(query);
+ if (!el) {
+ var now = Date.now();
+ var elapsed = now - started;
+ if (elapsed < 2000) {
+ // Let's wait a bit
+ setTimeout(tryAttach, 100);
+ return;
+ }
+ // If we don't attach within 2 seconds to the target element, consider it a failure
+ console.error('Cannot find the panzoom element', globalName);
+ return;
+ }
+ var options = collectOptions(panzoomScript);
+ console.log(options);
+ window[globalName] = createPanZoom(el, options);
+ }
+
+ function collectOptions(script) {
+ var attrs = script.attributes;
+ var options = {};
+ for (var j = 0; j < attrs.length; ++j) {
+ var attr = attrs[j];
+ var nameValue = getPanzoomAttributeNameValue(attr);
+ if (nameValue) {
+ options[nameValue.name] = nameValue.value;
+ }
+ }
+
+ return options;
+ }
+
+ function getPanzoomAttributeNameValue(attr) {
+ if (!attr.name) return;
+ var isPanZoomAttribute =
+ attr.name[0] === 'p' && attr.name[1] === 'z' && attr.name[2] === '-';
+
+ if (!isPanZoomAttribute) return;
+
+ var name = attr.name.substr(3);
+ var value = JSON.parse(attr.value);
+ return { name: name, value: value };
+ }
+ }
+
+ autoRun();
+
+ }, { "./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) {
+ /**
+ * Disallows selecting text.
+ */
+ module.exports = createTextSelectionInterceptor;
+
+ function createTextSelectionInterceptor(useFake) {
+ if (useFake) {
+ return {
+ capture: noop,
+ release: noop
+ };
+ }
+
+ var dragObject;
+ var prevSelectStart;
+ var prevDragStart;
+ var wasCaptured = false;
+
+ return {
+ capture: capture,
+ release: release
+ };
+
+ function capture(domObject) {
+ wasCaptured = true;
+ prevSelectStart = window.document.onselectstart;
+ prevDragStart = window.document.ondragstart;
+
+ window.document.onselectstart = disabled;
+
+ dragObject = domObject;
+ dragObject.ondragstart = disabled;
+ }
+
+ function release() {
+ if (!wasCaptured) return;
+
+ wasCaptured = false;
+ window.document.onselectstart = prevSelectStart;
+ if (dragObject) dragObject.ondragstart = prevDragStart;
+ }
+ }
+
+ function disabled(e) {
+ e.stopPropagation();
+ return false;
+ }
+
+ function noop() { }
+
+ }, {}], 3: [function (require, module, exports) {
+ module.exports = makeDomController;
+
+ module.exports.canAttach = isDomElement;
+
+ function makeDomController(domElement, options) {
+ var elementValid = isDomElement(domElement);
+ if (!elementValid) {
+ throw new Error('panzoom requires DOM element to be attached to the DOM tree');
+ }
+
+ var owner = domElement.parentElement;
+ domElement.scrollTop = 0;
+
+ if (!options.disableKeyboardInteraction) {
+ owner.setAttribute('tabindex', 0);
+ }
+
+ var api = {
+ getBBox: getBBox,
+ getOwner: getOwner,
+ applyTransform: applyTransform,
+ };
+
+ return api;
+
+ function getOwner() {
+ return owner;
+ }
+
+ function getBBox() {
+ // TODO: We should probably cache this?
+ return {
+ left: 0,
+ top: 0,
+ width: domElement.clientWidth,
+ height: domElement.clientHeight
+ };
+ }
+
+ function applyTransform(transform) {
+ // TODO: Should we cache this?
+ domElement.style.transformOrigin = '0 0 0';
+ domElement.style.transform = 'matrix(' +
+ transform.scale.toFixed(4) + ', 0, 0, ' +
+ transform.scale.toFixed(4) + ', ' +
+ transform.x.toFixed(4) + ', ' + transform.y.toFixed(4) + ')';
+ }
+ }
+
+ function isDomElement(element) {
+ return element && element.parentElement && element.style;
+ }
+
+ }, {}], 4: [function (require, module, exports) {
+ /**
+ * Allows smooth kinetic scrolling of the surface
+ */
+ module.exports = kinetic;
+
+ function kinetic(getPoint, scroll, settings) {
+ if (typeof settings !== 'object') {
+ // setting could come as boolean, we should ignore it, and use an object.
+ settings = {};
+ }
+
+ var minVelocity = typeof settings.minVelocity === 'number' ? settings.minVelocity : 5;
+ var amplitude = typeof settings.amplitude === 'number' ? settings.amplitude : 0.25;
+ var cancelAnimationFrame = typeof settings.cancelAnimationFrame === 'function' ? settings.cancelAnimationFrame : getCancelAnimationFrame();
+ var requestAnimationFrame = typeof settings.requestAnimationFrame === 'function' ? settings.requestAnimationFrame : getRequestAnimationFrame();
+
+ var lastPoint;
+ var timestamp;
+ var timeConstant = 342;
+
+ var ticker;
+ var vx, targetX, ax;
+ var vy, targetY, ay;
+
+ var raf;
+
+ return {
+ start: start,
+ stop: stop,
+ cancel: dispose
+ };
+
+ function dispose() {
+ cancelAnimationFrame(ticker);
+ cancelAnimationFrame(raf);
+ }
+
+ function start() {
+ lastPoint = getPoint();
+
+ ax = ay = vx = vy = 0;
+ timestamp = new Date();
+
+ cancelAnimationFrame(ticker);
+ cancelAnimationFrame(raf);
+
+ // we start polling the point position to accumulate velocity
+ // Once we stop(), we will use accumulated velocity to keep scrolling
+ // an object.
+ ticker = requestAnimationFrame(track);
+ }
+
+ function track() {
+ var now = Date.now();
+ var elapsed = now - timestamp;
+ timestamp = now;
+
+ var currentPoint = getPoint();
+
+ var dx = currentPoint.x - lastPoint.x;
+ var dy = currentPoint.y - lastPoint.y;
+
+ lastPoint = currentPoint;
+
+ var dt = 1000 / (1 + elapsed);
+
+ // moving average
+ vx = 0.8 * dx * dt + 0.2 * vx;
+ vy = 0.8 * dy * dt + 0.2 * vy;
+
+ ticker = requestAnimationFrame(track);
+ }
+
+ function stop() {
+ cancelAnimationFrame(ticker);
+ cancelAnimationFrame(raf);
+
+ var currentPoint = getPoint();
+
+ targetX = currentPoint.x;
+ targetY = currentPoint.y;
+ timestamp = Date.now();
+
+ if (vx < -minVelocity || vx > minVelocity) {
+ ax = amplitude * vx;
+ targetX += ax;
+ }
+
+ if (vy < -minVelocity || vy > minVelocity) {
+ ay = amplitude * vy;
+ targetY += ay;
+ }
+
+ raf = requestAnimationFrame(autoScroll);
+ }
+
+ function autoScroll() {
+ var elapsed = Date.now() - timestamp;
+
+ var moving = false;
+ var dx = 0;
+ var dy = 0;
+
+ if (ax) {
+ dx = -ax * Math.exp(-elapsed / timeConstant);
+
+ if (dx > 0.5 || dx < -0.5) moving = true;
+ else dx = ax = 0;
+ }
+
+ if (ay) {
+ dy = -ay * Math.exp(-elapsed / timeConstant);
+
+ if (dy > 0.5 || dy < -0.5) moving = true;
+ else dy = ay = 0;
+ }
+
+ if (moving) {
+ scroll(targetX + dx, targetY + dy);
+ raf = requestAnimationFrame(autoScroll);
+ }
+ }
+ }
+
+ function getCancelAnimationFrame() {
+ if (typeof cancelAnimationFrame === 'function') return cancelAnimationFrame;
+ return clearTimeout;
+ }
+
+ function getRequestAnimationFrame() {
+ if (typeof requestAnimationFrame === 'function') return requestAnimationFrame;
+
+ return function (handler) {
+ return setTimeout(handler, 16);
+ };
+ }
+ }, {}], 5: [function (require, module, exports) {
+ module.exports = makeSvgController;
+ module.exports.canAttach = isSVGElement;
+
+ function makeSvgController(svgElement, options) {
+ if (!isSVGElement(svgElement)) {
+ throw new Error('svg element is required for svg.panzoom to work');
+ }
+
+ var owner = svgElement.ownerSVGElement;
+ if (!owner) {
+ throw new Error(
+ 'Do not apply panzoom to the root