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..3432446 --- /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..fe1d18a --- /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..38826b7 --- /dev/null +++ b/js/functions.js @@ -0,0 +1,110 @@ +// С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) { + if (options.color < colorOptions.length - 1) { + options.color += 1 + } + } else { + if (options.color) { + 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) { + if (options.size < sizeOptions.length - 1) { + options.size += 1 + } + } else { + if (options.size) { + 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) { + if (options.pattern < patternOptions.length - 1) { + options.pattern += 1 + } + } else { + if (options.pattern) { + 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) +} 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 element. ' + + 'Use its child instead (e.g. ). ' + + 'As of March 2016 only FireFox supported transform on the root element'); + } + + if (!options.disableKeyboardInteraction) { + owner.setAttribute('tabindex', 0); + } + + var api = { + getBBox: getBBox, + getScreenCTM: getScreenCTM, + getOwner: getOwner, + applyTransform: applyTransform, + initTransform: initTransform + }; + + return api; + + function getOwner() { + return owner; + } + + function getBBox() { + var bbox = svgElement.getBBox(); + return { + left: bbox.x, + top: bbox.y, + width: bbox.width, + height: bbox.height, + }; + } + + function getScreenCTM() { + var ctm = owner.getCTM(); + if (!ctm) { + // This is likely firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=873106 + // The code below is not entirely correct, but still better than nothing + return owner.getScreenCTM(); + } + return ctm; + } + + function initTransform(transform) { + var screenCTM = svgElement.getCTM(); + + // The above line returns null on Firefox + if (screenCTM === null) { + screenCTM = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix(); + } + + transform.x = screenCTM.e; + transform.y = screenCTM.f; + transform.scale = screenCTM.a; + owner.removeAttributeNS(null, 'viewBox'); + } + + function applyTransform(transform) { + transformX = transform.x.toFixed(4) + transformY = transform.y.toFixed(4) + transformScale = transform.scale.toFixed(4) + svgElement.style.transform = `matrix(${transformScale}, 0, 0, ${transformScale}, ${transformX}, ${transformY})` + } + } + + function isSVGElement(element) { + return element && element.ownerSVGElement && element.getCTM; + } + }, {}], 6: [function (require, module, exports) { + module.exports = Transform; + + function Transform() { + this.x = 0; + this.y = 0; + this.scale = 1; + } + + }, {}], 7: [function (require, module, exports) { + var BezierEasing = require('bezier-easing') + + // Predefined set of animations. Similar to CSS easing functions + var animations = { + ease: BezierEasing(0.25, 0.1, 0.25, 1), + easeIn: BezierEasing(0.42, 0, 1, 1), + easeOut: BezierEasing(0, 0, 0.58, 1), + easeInOut: BezierEasing(0.42, 0, 0.58, 1), + linear: BezierEasing(0, 0, 1, 1) + } + + + module.exports = animate; + module.exports.makeAggregateRaf = makeAggregateRaf; + module.exports.sharedScheduler = makeAggregateRaf(); + + + function animate(source, target, options) { + var start = Object.create(null) + var diff = Object.create(null) + options = options || {} + // We let clients specify their own easing function + var easing = (typeof options.easing === 'function') ? options.easing : animations[options.easing] + + // if nothing is specified, default to ease (similar to CSS animations) + if (!easing) { + if (options.easing) { + console.warn('Unknown easing function in amator: ' + options.easing); + } + easing = animations.ease + } + + var step = typeof options.step === 'function' ? options.step : noop + var done = typeof options.done === 'function' ? options.done : noop + + var scheduler = getScheduler(options.scheduler) + + var keys = Object.keys(target) + keys.forEach(function (key) { + start[key] = source[key] + diff[key] = target[key] - source[key] + }) + + var durationInMs = typeof options.duration === 'number' ? options.duration : 400 + var durationInFrames = Math.max(1, durationInMs * 0.06) // 0.06 because 60 frames pers 1,000 ms + var previousAnimationId + var frame = 0 + + previousAnimationId = scheduler.next(loop) + + return { + cancel: cancel + } + + function cancel() { + scheduler.cancel(previousAnimationId) + previousAnimationId = 0 + } + + function loop() { + var t = easing(frame / durationInFrames) + frame += 1 + setValues(t) + if (frame <= durationInFrames) { + previousAnimationId = scheduler.next(loop) + step(source) + } else { + previousAnimationId = 0 + setTimeout(function () { done(source) }, 0) + } + } + + function setValues(t) { + keys.forEach(function (key) { + source[key] = diff[key] * t + start[key] + }) + } + } + + function noop() { } + + function getScheduler(scheduler) { + if (!scheduler) { + var canRaf = typeof window !== 'undefined' && window.requestAnimationFrame + return canRaf ? rafScheduler() : timeoutScheduler() + } + if (typeof scheduler.next !== 'function') throw new Error('Scheduler is supposed to have next(cb) function') + if (typeof scheduler.cancel !== 'function') throw new Error('Scheduler is supposed to have cancel(handle) function') + + return scheduler + } + + function rafScheduler() { + return { + next: window.requestAnimationFrame.bind(window), + cancel: window.cancelAnimationFrame.bind(window) + } + } + + function timeoutScheduler() { + return { + next: function (cb) { + return setTimeout(cb, 1000 / 60) + }, + cancel: function (id) { + return clearTimeout(id) + } + } + } + + function makeAggregateRaf() { + var frontBuffer = new Set(); + var backBuffer = new Set(); + var frameToken = 0; + + return { + next: next, + cancel: next, + clearAll: clearAll + } + + function clearAll() { + frontBuffer.clear(); + backBuffer.clear(); + cancelAnimationFrame(frameToken); + frameToken = 0; + } + + function next(callback) { + backBuffer.add(callback); + renderNextFrame(); + } + + function renderNextFrame() { + if (!frameToken) frameToken = requestAnimationFrame(renderFrame); + } + + function renderFrame() { + frameToken = 0; + + var t = backBuffer; + backBuffer = frontBuffer; + frontBuffer = t; + + frontBuffer.forEach(function (callback) { + callback(); + }); + frontBuffer.clear(); + } + + function cancel(callback) { + backBuffer.delete(callback); + } + } + + }, { "bezier-easing": 8 }], 8: [function (require, module, exports) { + /** + * https://github.com/gre/bezier-easing + * BezierEasing - use bezier curve for transition easing function + * by Gaëtan Renaudeau 2014 - 2015 – MIT License + */ + + // These values are established by empiricism with tests (tradeoff: performance VS precision) + var NEWTON_ITERATIONS = 4; + var NEWTON_MIN_SLOPE = 0.001; + var SUBDIVISION_PRECISION = 0.0000001; + var SUBDIVISION_MAX_ITERATIONS = 10; + + var kSplineTableSize = 11; + var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); + + var float32ArraySupported = typeof Float32Array === 'function'; + + function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } + function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } + function C(aA1) { return 3.0 * aA1; } + + // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. + function calcBezier(aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; } + + // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. + function getSlope(aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } + + function binarySubdivide(aX, aA, aB, mX1, mX2) { + var currentX, currentT, i = 0; + do { + currentT = aA + (aB - aA) / 2.0; + currentX = calcBezier(currentT, mX1, mX2) - aX; + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); + return currentT; + } + + function newtonRaphsonIterate(aX, aGuessT, mX1, mX2) { + for (var i = 0; i < NEWTON_ITERATIONS; ++i) { + var currentSlope = getSlope(aGuessT, mX1, mX2); + if (currentSlope === 0.0) { + return aGuessT; + } + var currentX = calcBezier(aGuessT, mX1, mX2) - aX; + aGuessT -= currentX / currentSlope; + } + return aGuessT; + } + + function LinearEasing(x) { + return x; + } + + module.exports = function bezier(mX1, mY1, mX2, mY2) { + if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { + throw new Error('bezier x values must be in [0, 1] range'); + } + + if (mX1 === mY1 && mX2 === mY2) { + return LinearEasing; + } + + // Precompute samples table + var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); + for (var i = 0; i < kSplineTableSize; ++i) { + sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); + } + + function getTForX(aX) { + var intervalStart = 0.0; + var currentSample = 1; + var lastSample = kSplineTableSize - 1; + + for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; + + // Interpolate to provide an initial guess for t + var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); + var guessForT = intervalStart + dist * kSampleStepSize; + + var initialSlope = getSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return newtonRaphsonIterate(aX, guessForT, mX1, mX2); + } else if (initialSlope === 0.0) { + return guessForT; + } else { + return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); + } + } + + return function BezierEasing(x) { + // Because JavaScript number are imprecise, we should guarantee the extremes are right. + if (x === 0) { + return 0; + } + if (x === 1) { + return 1; + } + return calcBezier(getTForX(x), mY1, mY2); + }; + }; + + }, {}], 9: [function (require, module, exports) { + module.exports = function eventify(subject) { + validateSubject(subject); + + var eventsStorage = createEventsStorage(subject); + subject.on = eventsStorage.on; + subject.off = eventsStorage.off; + subject.fire = eventsStorage.fire; + return subject; + }; + + function createEventsStorage(subject) { + // Store all event listeners to this hash. Key is event name, value is array + // of callback records. + // + // A callback record consists of callback function and its optional context: + // { 'eventName' => [{callback: function, ctx: object}] } + var registeredEvents = Object.create(null); + + return { + on: function (eventName, callback, ctx) { + if (typeof callback !== 'function') { + throw new Error('callback is expected to be a function'); + } + var handlers = registeredEvents[eventName]; + if (!handlers) { + handlers = registeredEvents[eventName] = []; + } + handlers.push({ callback: callback, ctx: ctx }); + + return subject; + }, + + off: function (eventName, callback) { + var wantToRemoveAll = (typeof eventName === 'undefined'); + if (wantToRemoveAll) { + // Killing old events storage should be enough in this case: + registeredEvents = Object.create(null); + return subject; + } + + if (registeredEvents[eventName]) { + var deleteAllCallbacksForEvent = (typeof callback !== 'function'); + if (deleteAllCallbacksForEvent) { + delete registeredEvents[eventName]; + } else { + var callbacks = registeredEvents[eventName]; + for (var i = 0; i < callbacks.length; ++i) { + if (callbacks[i].callback === callback) { + callbacks.splice(i, 1); + } + } + } + } + + return subject; + }, + + fire: function (eventName) { + var callbacks = registeredEvents[eventName]; + if (!callbacks) { + return subject; + } + + var fireArguments; + if (arguments.length > 1) { + fireArguments = Array.prototype.splice.call(arguments, 1); + } + for (var i = 0; i < callbacks.length; ++i) { + var callbackInfo = callbacks[i]; + callbackInfo.callback.apply(callbackInfo.ctx, fireArguments); + } + + return subject; + } + }; + } + + function validateSubject(subject) { + if (!subject) { + throw new Error('Eventify cannot use falsy object as events subject'); + } + var reservedWords = ['on', 'fire', 'off']; + for (var i = 0; i < reservedWords.length; ++i) { + if (subject.hasOwnProperty(reservedWords[i])) { + throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'"); + } + } + } + + }, {}], 10: [function (require, module, exports) { + /** + * This module used to unify mouse wheel behavior between different browsers in 2014 + * Now it's just a wrapper around addEventListener('wheel'); + * + * Usage: + * var addWheelListener = require('wheel').addWheelListener; + * var removeWheelListener = require('wheel').removeWheelListener; + * addWheelListener(domElement, function (e) { + * // mouse wheel event + * }); + * removeWheelListener(domElement, function); + */ + + module.exports = addWheelListener; + + // But also expose "advanced" api with unsubscribe: + module.exports.addWheelListener = addWheelListener; + module.exports.removeWheelListener = removeWheelListener; + + + function addWheelListener(element, listener, useCapture) { + element.addEventListener('wheel', listener, useCapture); + } + + function removeWheelListener(element, listener, useCapture) { + element.removeEventListener('wheel', listener, useCapture); + } + }, {}] + }, {}, [1])(1) +}); diff --git a/js/lib/simplify-svg-path.js b/js/lib/simplify-svg-path.js new file mode 100644 index 0000000..afd0b3d --- /dev/null +++ b/js/lib/simplify-svg-path.js @@ -0,0 +1 @@ +var simplifySvgPath=(function(){class t{constructor(t){this.t=t,this.i=10**this.t}h(t){return this.t<16?Math.round(t*this.i)/this.i:t}o(t,s){return this.h(t)+","+this.h(s)}}class s{constructor(t,s){this.x=t,this.y=s}u(){return new s(-this.x,-this.y)}l(t=1){return this._(t/(this.M()||1/0))}g(t){return new s(this.x+t.x,this.y+t.y)}v(t){return new s(this.x-t.x,this.y-t.y)}_(t){return new s(this.x*t,this.y*t)}m(t){return this.x*t.x+this.y*t.y}M(){return Math.sqrt(this.x*this.x+this.y*this.y)}p(t){const s=this.x-t.x,e=this.y-t.y;return Math.sqrt(s*s+e*e)}}class e{constructor(t,s){this.C=t,this.L=s}}class r{constructor(t,s){this.k=t,this.B=s}D(t){const s=this.k;this.B&&(s.unshift(s[s.length-1]),s.push(s[1]));const r=s.length;if(0===r)return[];const i=[new e(s[0])];return this.P(i,t,0,r-1,s[1].v(s[0]),s[r-2].v(s[r-1])),this.B&&(i.shift(),i.pop()),i}P(t,s,e,r,i,n){const h=this.k;if(r-e==1){const s=h[e],o=h[r],c=s.p(o)/3;return void this.R(t,[s,s.g(i.l(c)),o.g(n.l(c)),o])}const o=this.j(e,r);let c,u=Math.max(s,s*s),l=!0;for(let h=0;h<=4;h++){const h=this.q(e,r,o,i,n),a=this.A(e,r,h,o);if(a.error=u)break;l=this.F(e,r,o,h),u=a.error}const a=h[c-1].v(h[c+1]);this.P(t,s,e,c,i,a),this.P(t,s,c,r,a.u(),n)}R(t,s){t[t.length-1].G=s[1].v(s[0]),t.push(new e(s[3],s[2].v(s[3])))}q(t,s,e,r,i){const n=1e-12,h=Math.abs,o=this.k,c=o[t],u=o[s],l=[[0,0],[0,0]],a=[0,0];for(let n=0,h=s-t+1;nn){const t=l[0][0]*a[1]-l[1][0]*a[0];_=(a[0]*l[1][1]-a[1]*l[0][1])/f,d=t/f}else{const t=l[0][0]+l[0][1],s=l[1][0]+l[1][1];_=d=h(t)>n?a[0]/t:h(s)>n?a[1]/s:0}const w=u.p(c),M=n*w;let g,v;if(_w*w&&(_=d=w/3,g=v=null)}return[c,c.g(g||r.l(_)),u.g(v||i.l(d)),u]}F(t,s,e,r){for(let i=t;i<=s;i++)e[i-t]=this.H(r,this.k[i],e[i-t]);for(let t=1,s=e.length;t=-112e-18&&l<=112e-18?e:e-c.m(h)/u;var l}I(t,s,e){const r=s.slice();for(let s=1;s<=t;s++)for(let i=0;i<=t-s;i++)r[i]=r[i]._(1-e).g(r[i+1]._(e));return r[0]}j(t,s){const e=[0];for(let r=t+1;r<=s;r++)e[r-t]=e[r-t-1]+this.k[r].p(this.k[r-1]);for(let r=1,i=s-t;r<=i;r++)e[r]/=e[i];return e}A(t,s,e,r){let i=Math.floor((s-t+1)/2),n=0;for(let h=t+1;h=n&&(n=o,i=h)}return{error:n,index:i}}}return(e,i={})=>((s,e,r)=>{const i=s.length,n=new t(r);let h,o,c,u,l=!0;const a=[],f=(t,s)=>{const e=t.C.x,r=t.C.y;if(l)a.push("M"+n.o(e,r)),l=!1;else{const i=e+(t.L?.x??0),l=r+(t.L?.y??0);if(i===e&&l===r&&c===h&&u===o){if(!s){const t=e-h,s=r-o;a.push(0===t?"v"+n.h(s):0===s?"h"+n.h(t):"l"+n.o(t,s))}}else a.push("c"+n.o(c-h,u-o)+" "+n.o(i-h,l-o)+" "+n.o(e-h,r-o))}h=e,o=r,c=e+(t.G?.x??0),u=r+(t.G?.y??0)};if(!i)return"";for(let t=0;t0&&(f(s[0],!0),a.push("z")),a.join("")})(new r(e.map((t=>new s(t[0],t[1]))),i.closed).D(i.tolerance??2.5),i.closed,i.precision??5);})() \ No newline at end of file diff --git a/js/lib/simplify.js b/js/lib/simplify.js new file mode 100644 index 0000000..572fd94 --- /dev/null +++ b/js/lib/simplify.js @@ -0,0 +1,8 @@ +/** + * Minified by jsDelivr using Terser v3.14.1. + * Original file: /npm/simplify-js@1.2.4/simplify.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(){"use strict";function n(n,e,f){var t=e.x,u=e.y,i=f.x-t,r=f.y-u;if(0!==i||0!==r){var o=((n.x-t)*i+(n.y-u)*r)/(i*i+r*r);o>1?(t=f.x,u=f.y):o>0&&(t+=i*o,u+=r*o)}return(i=n.x-t)*i+(r=n.y-u)*r}function e(e,f){var t=e.length-1,u=[e[0]];return function e(f,t,u,i,r){for(var o,d=i,s=t+1;sd&&(o=s,d=y)}d>i&&(o-t>1&&e(f,t,o,i,r),r.push(f[o]),u-o>1&&e(f,o,u,i,r))}(e,0,t,f,u),u.push(e[t]),u}function f(n,f,t){if(n.length<=2)return n;var u=void 0!==f?f*f:1;return n=e(n=t?n:function(n,e){for(var f,t,u,i,r,o=n[0],d=[o],s=1,y=n.length;se&&(d.push(f),o=f);return o!==f&&d.push(f),d}(n,u),u)}"function"==typeof define&&define.amd?define(function(){return f}):"undefined"!=typeof module?(module.exports=f,module.exports.default=f):"undefined"!=typeof self?self.simplify=f:window.simplify=f}(); +//# sourceMappingURL=/sm/eca0f9334aa4defd09cf83dfc6d2cfe23f472e87984c4b6af46ebd122c29a43e.map \ No newline at end of file diff --git a/js/lib/svg-export.min.js b/js/lib/svg-export.min.js new file mode 100644 index 0000000..b39ccc3 --- /dev/null +++ b/js/lib/svg-export.min.js @@ -0,0 +1 @@ +(function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):(t="undefined"!=typeof globalThis?globalThis:t||self,e(t.svgExport=t.svgExport||{}))})(this,function(t){"use strict";function e(t){void 0!==typeof console&&"function"==typeof console.warn&&console.warn(t)}function i(t){var i=document.createElement("div");if(i.className="tempdiv-svg-exportJS","string"==typeof t&&(i.insertAdjacentHTML("beforeend",t.trim()),t=i.firstChild),!t.nodeType||1!==t.nodeType)return e("Error svg-export: The input svg was not recognized"),null;var n=t.cloneNode(!0);return n.style.display=null,i.appendChild(n),i.style.visibility="hidden",i.style.display="table",i.style.position="absolute",document.body.appendChild(i),n}function n(t){t&&t.pdfOptions&&(Object.keys(y.pdfOptions).forEach(function(e){if(t.pdfOptions.hasOwnProperty(e)&&typeof t.pdfOptions[e]==typeof y.pdfOptions[e]){if(""===t.pdfOptions[e])return;y.pdfOptions[e]=t.pdfOptions[e]}}),y.pdfOptions.pageLayout.margin||(y.pdfOptions.pageLayout.margin=50),y.pdfOptions.pageLayout.margins||(y.pdfOptions.pageLayout.margins={})),y.pdfOptions.pageLayout.margins.top=y.pdfOptions.pageLayout.margins.top||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.bottom=y.pdfOptions.pageLayout.margins.bottom||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.left=y.pdfOptions.pageLayout.margins.left||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.right=y.pdfOptions.pageLayout.margins.top||y.pdfOptions.pageLayout.margin,delete y.pdfOptions.pageLayout.margin,t&&y.pdfOptions.pageLayout.size||(y.pdfOptions.pageLayout.size=[Math.max(300,y.width)+y.pdfOptions.pageLayout.margins.left+y.pdfOptions.pageLayout.margins.right,Math.max(300,y.height)+y.pdfOptions.pageLayout.margins.top+y.pdfOptions.pageLayout.margins.bottom+(y.pdfOptions.addTitleToPage?2*y.pdfOptions.pdfTitleFontSize+10:0)+(""!==y.pdfOptions.chartCaption?4*y.pdfOptions.pdfCaptionFontSize+10:0)])}function o(t,e){y={originalWidth:100,originalHeight:100,originalMinXViewBox:0,originalMinYViewBox:0,width:100,height:100,scale:1,useCSS:!0,transparentBackgroundReplace:"white",allowCrossOriginImages:!1,elementsToExclude:[],pdfOptions:{customFonts:[],pageLayout:{margin:50,margins:{}},addTitleToPage:!0,chartCaption:"",pdfTextFontFamily:"Helvetica",pdfTitleFontSize:20,pdfCaptionFontSize:14}},y.originalHeight=-1!==t.style.getPropertyValue("height").indexOf("%")||t.getAttribute("height")&&-1!==t.getAttribute("height").indexOf("%")?t.getBBox().height*y.scale:t.getBoundingClientRect().height*y.scale,y.originalWidth=-1!==t.style.getPropertyValue("width").indexOf("%")||t.getAttribute("width")&&-1!==t.getAttribute("width").indexOf("%")?t.getBBox().width*y.scale:t.getBoundingClientRect().width*y.scale,y.originalMinXViewBox=t.getAttribute("viewBox")?t.getAttribute("viewBox").split(/\s/)[0]:0,y.originalMinYViewBox=t.getAttribute("viewBox")?t.getAttribute("viewBox").split(/\s/)[1]:0,e&&e.scale&&"number"==typeof e.scale&&(y.scale=e.scale),e&&e.height?"number"==typeof e.height&&(y.height=e.height*y.scale):y.height=y.originalHeight*y.scale,e&&e.width?"number"==typeof e.width&&(y.width=e.width*y.scale):y.width=y.originalWidth*y.scale,e&&!1===e.useCSS&&(y.useCSS=!1),e&&e.transparentBackgroundReplace&&(y.transparentBackgroundReplace=e.transparentBackgroundReplace),e&&e.allowCrossOriginImages&&(y.allowCrossOriginImages=e.allowCrossOriginImages),e&&e.excludeByCSSSelector&&"string"==typeof e.excludeByCSSSelector&&(y.elementsToExclude=t.querySelectorAll(e.excludeByCSSSelector)),n(e)}function a(t,i){if("function"==typeof getComputedStyle){for(var n=0;n0)for(const t of o)-1===["width","height","inline-size","block-size"].indexOf(t)&&i.style.setProperty(t,o.getPropertyValue(t));t.childNodes.forEach(function(t,e){1===t.nodeType&&a(t,i.childNodes[parseInt(e,10)])})}else e("Warning svg-export: this browser is not able to get computed styles")}function r(t,e,i){void 0===i&&(i=!0),y.useCSS&&"object"==typeof e&&(a(e,t),t.style.display=null),y.elementsToExclude.forEach(function(t){t.remove()}),t.style.width=null,t.style.height=null,t.setAttribute("width",y.width),t.setAttribute("height",y.height),t.setAttribute("preserveAspectRatio","none"),t.setAttribute("viewBox",y.originalMinXViewBox+" "+y.originalMinYViewBox+" "+y.originalWidth+" "+y.originalHeight);for(var n=document.getElementsByClassName("tempdiv-svg-exportJS");n.length>0;)n[0].parentNode.removeChild(n[0]);if(i){var o=new XMLSerializer,r=o.serializeToString(t).replace(/currentColor/g,"black");return r.match(/^]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)||(r=r.replace(/^]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)||(r=r.replace(/^]/g,"_"),navigator.msSaveBlob){for(var n=decodeURIComponent(t.split(",")[1]),o=[],a=t.split(",")[0].split(":")[1].split(";")[0],r=0;r\r\n'+i;var n="data:image/svg+xml;charset=utf-8,"+encodeURIComponent(i);g(n,e+".svg")})}}function d(t,n,a,s){if("object"==typeof canvg){s=s.toLowerCase().replace("jpg","jpeg"),"png"!==s&&"jpeg"!==s&&(s="png");var p=i(t);if(p){null==n&&(n="chart");var l=document.createElement("canvas");a&&(a.width||a.height)||(a||(a={}),a.scale=10),o(p,a);var d=r(p,t);"jpeg"===s&&(d=d.replace(">",'>'));var f=l.getContext("2d"),u=canvg.Canvg.fromString(f,d,{anonymousCrossOrigin:y.allowCrossOriginImages});u.start(),u.ready().then(function(){var t=l.toDataURL("image/"+s);g(t,n+"."+s,l)})}}else e("Error svg-export: PNG/JPEG export requires Canvg.js")}function f(t,e,i){d(t,e,i,"png")}function u(t,e,i){d(t,e,i,"jpeg")}function h(t,e,i){y.pdfOptions.addTitleToPage&&t.font(y.pdfOptions.pdfTextFontFamily).fontSize(y.pdfOptions.pdfTitleFontSize).text(e,{width:y.pdfOptions.pageLayout.size[0]-y.pdfOptions.pageLayout.margins.left-y.pdfOptions.pageLayout.margins.right}),SVGtoPDF(t,i,y.pdfOptions.pageLayout.margins.left,t.y+10,{width:y.width,height:y.height,preserveAspectRatio:"none",useCSS:y.useCSS}),""!==y.pdfOptions.chartCaption&&t.font(y.pdfOptions.pdfTextFontFamily).fontSize(y.pdfOptions.pdfCaptionFontSize).text(y.pdfOptions.chartCaption,y.pdfOptions.pageLayout.margins.left,y.pdfOptions.pageLayout.size[1]-y.pdfOptions.pageLayout.margins.bottom-4*y.pdfOptions.pdfCaptionFontSize,{width:y.pdfOptions.pageLayout.size[0]-y.pdfOptions.pageLayout.margins.left-y.pdfOptions.pageLayout.margins.right})}function c(t,n,a){if("function"==typeof PDFDocument&&"function"==typeof SVGtoPDF&&"function"==typeof blobStream){var l=i(t);if(l){null==n&&(n="chart"),o(l,a);var d=r(l,t,!1),f=new PDFDocument(y.pdfOptions.pageLayout),u=f.pipe(blobStream()),c=l.getElementsByTagName("image"),m=[];if(c)for(var w of c)(w.getAttribute("href")&&-1===w.getAttribute("href").indexOf("data:")||w.getAttribute("xlink:href")&&-1===w.getAttribute("xlink:href").indexOf("data:"))&&m.push(s(w));Promise.all(m).then(function(){if(y.pdfOptions.customFonts.length>0){var t=p(y.pdfOptions.customFonts.map(function(t){return t.url}));Promise.all(t).then(function(t){t.forEach(function(t,e){var i=y.pdfOptions.customFonts[parseInt(e,10)],n=d.querySelectorAll('[style*="'+i.fontName+'"]');n.forEach(function(t){t.style.fontFamily=i.fontName}),-1===i.url.indexOf(".ttc")&&-1===i.url.indexOf(".dfont")||!i.styleName?f.registerFont(i.fontName,t):f.registerFont(i.fontName,t,i.styleName)}),h(f,n,d),f.end()})}else h(f,n,d),f.end()}),u.on("finish",function(){var t=u.toBlobURL("application/pdf");g(t,n+".pdf")})}}else e("Error svg-export: PDF export requires PDFKit.js, blob-stream and SVG-to-PDFKit")}var m="1.2.0",y={};t.version=m,t.downloadSvg=l,t.downloadPng=f,t.downloadJpeg=u,t.downloadPdf=c,Object.defineProperty(t,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/js/pan-zoom.js b/js/pan-zoom.js new file mode 100644 index 0000000..820fb01 --- /dev/null +++ b/js/pan-zoom.js @@ -0,0 +1,15 @@ +let instance = panzoom(scene, { + maxZoom: 1.6, + minZoom: 0.1, + zoomSpeed: 0.08, + beforeMouseDown: function (e) { + let shouldIgnore = !e.button + return shouldIgnore + } +}) + +instance.on('transform', function () { + background.style.backgroundPositionX = `${transformX}px` + background.style.backgroundPositionY = `${transformY}px` + background.style.backgroundSize = `${55 * transformScale}px` +}) diff --git a/js/panels.js b/js/panels.js new file mode 100644 index 0000000..5d0ea28 --- /dev/null +++ b/js/panels.js @@ -0,0 +1,63 @@ +let activePanel = false + +// Brush color panel +color.onclick = function () { + if (activePanel === false) { + fadeIn('#colors', 200) + setTimeout(() => { + activePanel = true + }, 200) + } else { + fadeOut('#colors', 200) + setTimeout(() => { + activePanel = false + }, 200) + } +} +colorInput.onpointerenter = function () { + activePanel = false +} +colorInput.onpointerleave = function () { + activePanel = true +} + +// Brush size panel +size.onclick = function () { + if (activePanel === false) { + fadeIn('#sizes', 200) + setTimeout(() => { + activePanel = true + }, 200) + } else { + fadeOut('#sizes', 200) + setTimeout(() => { + activePanel = false + }, 200) + } +} + +// Canvas pattern panel +pattern.onclick = function () { + if (activePanel === false) { + fadeIn('#patterns', 200) + setTimeout(() => { + activePanel = true + }, 200) + } else { + fadeOut('#patterns', 200) + setTimeout(() => { + activePanel = false + }, 200) + } +} + +document.getElementsByTagName('body')[0].onclick = function () { + if (activePanel === true) { + fadeOut('#colors', 200) + fadeOut('#sizes', 200) + fadeOut('#patterns', 200) + setTimeout(() => { + activePanel = false + }, 200) + } +} diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..6a15035 --- /dev/null +++ b/js/settings.js @@ -0,0 +1,81 @@ +// Default brush settings +let boardMode = 'pen' +let widthOption = 4 +let colorOption = 'black' + +// Change tool +function changeAction(target) { + ;['eraser', 'pen'].forEach((action) => { + const t = document.getElementById(action) + t.classList.remove('active') + }) + if (typeof target === 'string') target = document.getElementById(target) + target.classList.add('active') + switch (target.id) { + case 'pointer': + boardMode = 'pointer' + applyDraggable() + break + case 'eraser': + boardMode = 'eraser' + color.style.pointerEvents = 'none' + color.style.opacity = 0.2 + break + case 'pen': + boardMode = 'pen' + color.style.pointerEvents = 'auto' + color.style.opacity = 1 + break + default: + break + } +} + +// Brush settings +function setBrush(option) { + // Size settings + if (option.size !== undefined) { + widthOption = option.size + document + .querySelector('#sizes button.active') + .classList.remove('active') + } + switch (option.size) { + case 3: + size.style.backgroundSize = '50%' + small.classList.add('active') + break + case 4: + size.style.backgroundSize = '76%' + medium.classList.add('active') + break + case 5: + size.style.backgroundSize = '110%' + large.classList.add('active') + break + default: + break + } + // Color settings + if (option.color !== undefined) { + let val = option.color + colorOption = val + color.style.backgroundColor = val + } +} +customColor = 'black' +colorInput.onchange = function () { + setBrush({ color: colorInput.value }) + customColor = colorInput.value +} + +// Set canvas pattern +let currentPattern = 'none' +function setPattern(name) { + background.style.backgroundImage = `url('assets/patterns/pattern_${name}.svg')` + pattern.style.backgroundImage = `url('assets/icons/${name}.svg')` + document.querySelector('#patterns button.active').classList.remove('active') + document.getElementById(name).classList.add('active') + currentPattern = name +} +setPattern('none')