├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── build ├── rollup-config.js └── rollup-watch-config.js ├── demo ├── fp.jpg ├── fp.svg ├── index.css ├── index.html ├── radar.png └── thumbR.png ├── dev ├── draw.html ├── draw.js ├── fp.jpeg ├── fp.jpg ├── fp.svg ├── index.css ├── index.html ├── index.js ├── pano.jpg ├── radar.png └── thumbR.png ├── dist ├── indoor.esm.js ├── indoor.esm.js.map ├── indoor.js ├── indoor.js.map ├── indoor.min.js └── indoor.min.js.map ├── lib └── indoor.js ├── package.json ├── readme.md ├── src ├── Indoor.js ├── core │ ├── Base.js │ ├── Constants.js │ └── index.js ├── floorplan │ ├── Floor.js │ └── index.js ├── geometry │ ├── Point.js │ └── index.js ├── grid │ ├── Axis.js │ ├── Grid.js │ └── gridStyle.js ├── layer │ ├── Connector.js │ ├── Group.js │ ├── Layer.js │ ├── Tooltip.js │ ├── index.js │ ├── marker │ │ ├── Icon.js │ │ ├── Marker.js │ │ ├── MarkerGroup.js │ │ └── index.js │ └── vector │ │ ├── Circle.js │ │ ├── Line.js │ │ ├── Polyline.js │ │ ├── Rect.js │ │ └── index.js ├── lib │ ├── MagicScroll.js │ ├── color-alpha.js │ ├── ev-pos.js │ ├── impetus.js │ ├── mix.js │ ├── mouse-event-offset.js │ ├── mouse-wheel.js │ ├── mumath │ │ ├── almost.js │ │ ├── clamp.js │ │ ├── closest.js │ │ ├── index.js │ │ ├── is-multiple.js │ │ ├── is-plain-obj.js │ │ ├── left-pad.js │ │ ├── len.js │ │ ├── lerp.js │ │ ├── log10.js │ │ ├── mod.js │ │ ├── normalize.js │ │ ├── order.js │ │ ├── parse-unit.js │ │ ├── precision.js │ │ ├── pretty.js │ │ ├── range.js │ │ ├── round.js │ │ ├── scale.js │ │ ├── to-px.js │ │ └── within.js │ ├── panzoom.js │ ├── raf.js │ └── touch-pinch.js ├── map │ ├── Map.js │ ├── ModesMixin.js │ └── index.js ├── measurement │ ├── Measurement.js │ └── Measurer.js └── paint │ ├── Arrow.js │ ├── ArrowHead.js │ ├── Canvas.js │ └── index.js ├── test └── index.spec.js ├── webpack.config.js └── webpack.config.lib.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env" 5 | ] 6 | ] 7 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist/ 3 | node_modules 4 | node_modules/ 5 | src/index.html 6 | src/lib 7 | src/lib/ 8 | build 9 | build/ 10 | lib 11 | lib/ 12 | demo/ 13 | dev/index.html 14 | dev/draw.html -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['airbnb-base'], 4 | parserOptions: { 5 | parser: 'babel-eslint' 6 | }, 7 | globals: { 8 | document: true, 9 | window: true, 10 | fabric: true, 11 | panzoom: true 12 | }, 13 | plugins: ['prettier'], 14 | rules: { 15 | 'comma-dangle': [ 16 | 'error', 17 | { 18 | arrays: 'never', 19 | objects: 'never', 20 | imports: 'never', 21 | exports: 'never', 22 | functions: 'ignore' 23 | } 24 | ], 25 | 'prefer-destructuring': [ 26 | 'error', 27 | { 28 | array: true, 29 | object: false 30 | }, 31 | { 32 | enforceForRenamedProperties: false 33 | } 34 | ], 35 | 'arrow-parens': 'off', 36 | 'implicit-arrow-linebreak': 'off', 37 | 'no-underscore-dangle': 'off', 38 | 'no-param-reassign': 'off', 39 | 'function-paren-newline': 'off', 40 | 'import/no-unresolved': 'off', 41 | 'import/extensions': 'off', 42 | 'vue/no-unused-components': { 43 | ignoreWhenBindingPresent: false 44 | }, 45 | 'no-console': 'off', 46 | 'no-continue': 'off', 47 | 'max-len': [ 48 | 'error', 49 | { 50 | code: 100, 51 | ignoreUrls: true, 52 | ignoreStrings: true 53 | } 54 | ] 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | # Compiled source # 4 | ################### 5 | *.com 6 | *.class 7 | *.dll 8 | *.exe 9 | *.o 10 | *.so 11 | *.bin 12 | 13 | # Packages # 14 | ############ 15 | # it's better to unpack these files and commit the raw source 16 | # git has its own built in compression methods 17 | *.7z 18 | *.dmg 19 | *.gz 20 | *.iso 21 | *.jar 22 | *.rar 23 | *.tar 24 | *.zip 25 | 26 | # Logs and databases # 27 | ###################### 28 | *.log 29 | *.sql 30 | *.sqlite 31 | 32 | # OS generated files # 33 | ###################### 34 | .DS_Store 35 | .DS_Store? 36 | *._* 37 | .Spotlight-V100 38 | .Trashes 39 | Icon? 40 | ehthumbs.db 41 | Thumbs.db 42 | 43 | # My extension # 44 | ################ 45 | *.lock 46 | *.bak 47 | lsn 48 | *.dump 49 | *.beam 50 | *.[0-9] 51 | *._[0-9] 52 | *.ns 53 | Scripting_* 54 | docs 55 | *.pdf 56 | *.pak 57 | 58 | design 59 | instances 60 | *node_modules 61 | 62 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mudin Ibrahim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build/rollup-config.js: -------------------------------------------------------------------------------- 1 | // Config file for running Rollup in "normal" mode (non-watch) 2 | 3 | import rollupGitVersion from 'rollup-plugin-git-version'; 4 | import babel from 'rollup-plugin-babel'; 5 | import commonjs from 'rollup-plugin-commonjs'; 6 | import builtins from 'rollup-plugin-node-builtins'; 7 | import globals from 'rollup-plugin-node-globals'; 8 | import json from 'rollup-plugin-json'; 9 | import gitRev from 'git-rev-sync'; 10 | import pkg from '../package.json'; 11 | 12 | let { version } = pkg; 13 | let release; 14 | 15 | // Skip the git branch+rev in the banner when doing a release build 16 | if (process.env.NODE_ENV === 'release') { 17 | release = true; 18 | } else { 19 | release = false; 20 | const branch = gitRev.branch(); 21 | const rev = gitRev.short(); 22 | version += `+${branch}.${rev}`; 23 | } 24 | 25 | const banner = `/* @preserve 26 | * IndoorJS ${version}, a JS library for interactive indoor maps. https://mudin.github.io/indoorjs 27 | * (c) 2019 Mudin Ibrahim 28 | */ 29 | `; 30 | 31 | const outro = `var oldI = window.I; 32 | exports.noConflict = function() { 33 | window.I = oldI; 34 | return this; 35 | } 36 | // Always export us to window global (see #2364) 37 | window.I = exports;`; 38 | 39 | 40 | export default { 41 | input: 'src/Indoor.js', 42 | output: [ 43 | { 44 | file: pkg.main, 45 | format: 'umd', 46 | name: 'Indoor', 47 | banner, 48 | outro:outro, 49 | sourcemap: true, 50 | globals:{ 51 | fabric:'fabric', 52 | impetus:'impetus', 53 | eventemitter2:'EventEmitter2', 54 | EventEmitter2:'eventemitter2' 55 | } 56 | }, 57 | { 58 | file: 'dist/indoor.esm.js', 59 | format: 'es', 60 | banner, 61 | sourcemap: true 62 | } 63 | ], 64 | plugins: [ 65 | commonjs({ 66 | include: 'src/lib/panzoom.js' 67 | }), 68 | release ? json() : rollupGitVersion(), 69 | babel({ 70 | exclude: 'node_modules/**' 71 | }), 72 | globals(), 73 | builtins() 74 | ] 75 | }; 76 | -------------------------------------------------------------------------------- /build/rollup-watch-config.js: -------------------------------------------------------------------------------- 1 | // Config file for running Rollup in "watch" mode 2 | // This adds a sanity check to help ourselves to run 'rollup -w' as needed. 3 | 4 | import rollupGitVersion from 'rollup-plugin-git-version'; 5 | import gitRev from 'git-rev-sync'; 6 | 7 | const branch = gitRev.branch(); 8 | const rev = gitRev.short(); 9 | const version = `${require('../package.json').version}+${branch}.${rev}`; 10 | 11 | const banner = `/* @preserve 12 | * IndoorJS ${version}, a JS library for interactive indoor maps. https://mudin.github.io/indoorjs 13 | * (c) 2019 Mudin Ibrahim 14 | */ 15 | `; 16 | 17 | export default { 18 | input: 'src/Indoor.js', 19 | output: { 20 | file: 'dist/indoor.js', 21 | format: 'umd', 22 | name: 'L', 23 | banner, 24 | sourcemap: true 25 | }, 26 | legacy: true, // Needed to create files loadable by IE8 27 | plugins: [ 28 | rollupGitVersion() 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /demo/fp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/indoorjs/6827c93069cde2c079a889c61c9fc48b3adf417b/demo/fp.jpg -------------------------------------------------------------------------------- /demo/fp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 ' 9 " 20 ' 11 " 40.98 f²220.25 f²95.50 f² -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | .toolbar { 8 | width: 100%; 9 | height: 100px; 10 | background: lightblue; 11 | } 12 | .context { 13 | width: 100%; 14 | height: calc(100% - 100px); 15 | } 16 | 17 | .context .sidebar { 18 | width: 300px; 19 | height: 100%; 20 | float: left; 21 | background: lightcyan 22 | } 23 | .context .my-map { 24 | display: block; 25 | overflow: hidden; 26 | height: 100%; 27 | min-height: 100%; 28 | width: auto; 29 | position: relative; 30 | } 31 | 32 | 33 | :host { 34 | position: relative; 35 | } 36 | 37 | .grid { 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | bottom: 0; 42 | right: 0; 43 | pointer-events: none; 44 | font-family: sans-serif; 45 | } 46 | .grid-lines { 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | bottom: 0; 51 | right: 0; 52 | overflow: hidden; 53 | pointer-events: none; 54 | } 55 | 56 | .grid-line { 57 | pointer-events: all; 58 | position: absolute; 59 | top: 0; 60 | left: 0; 61 | width: .5rem; 62 | height: .5rem; 63 | opacity: .25; 64 | } 65 | .grid-line[hidden] { 66 | display: none; 67 | } 68 | .grid-line:hover { 69 | opacity: .5; 70 | } 71 | 72 | @supports (--css: variables) { 73 | .grid { 74 | --opacity: .15; 75 | } 76 | .grid-line { 77 | opacity: var(--opacity); 78 | } 79 | .grid-line:hover { 80 | opacity: calc(var(--opacity) * 2); 81 | } 82 | } 83 | 84 | .grid-line-x { 85 | height: 100%; 86 | width: 0; 87 | border-left: 1px solid; 88 | margin-left: -1px; 89 | } 90 | .grid-line-x:after { 91 | content: ''; 92 | position: absolute; 93 | width: .5rem; 94 | top: 0; 95 | bottom: 0; 96 | left: -.25rem; 97 | } 98 | .grid-line-x.grid-line-min { 99 | margin-left: 0px; 100 | } 101 | 102 | .grid-line-y { 103 | width: 100%; 104 | height: 0; 105 | margin-top: -1px; 106 | border-top: 1px solid; 107 | } 108 | .grid-line-y:after { 109 | content: ''; 110 | position: absolute; 111 | height: .5rem; 112 | left: 0; 113 | right: 0; 114 | top: -.25rem; 115 | } 116 | .grid-line-y.grid-line-max { 117 | margin-top: 0px; 118 | } 119 | 120 | /* radial lines */ 121 | .grid-line-r { 122 | height: 100%; 123 | width: 100%; 124 | left: 50%; 125 | top: 50%; 126 | border-radius: 50vw; 127 | box-shadow: inset 0 0 0 1px; 128 | } 129 | 130 | /* angular lines */ 131 | .grid-line-a { 132 | height: 0; 133 | top: 50%; 134 | left: 50%; 135 | transform-origin: left center; 136 | width: 50%; 137 | border-top: 1px solid; 138 | } 139 | .grid-line-a:after { 140 | content: ''; 141 | position: absolute; 142 | height: .5rem; 143 | left: 0; 144 | right: 0; 145 | top: -.25rem; 146 | } 147 | .grid-line-a:before { 148 | content: ''; 149 | position: absolute; 150 | width: .4rem; 151 | right: 0; 152 | top: -1px; 153 | height: 0; 154 | border-bottom: 2px solid; 155 | } 156 | 157 | 158 | .grid-axis { 159 | position: absolute; 160 | } 161 | .grid-axis-x { 162 | top: auto; 163 | bottom: 0; 164 | right: 0; 165 | left: 0; 166 | border-bottom: 2px solid; 167 | margin-bottom: -.5rem; 168 | } 169 | .grid-axis-y { 170 | border-left: 2px solid; 171 | right: auto; 172 | top: 0; 173 | bottom: 0; 174 | left: -1px; 175 | margin-left: -.5rem; 176 | } 177 | .grid-axis-a { 178 | height: 100%; 179 | width: 100%; 180 | left: 50%; 181 | top: 50%; 182 | border-radius: 50vw; 183 | box-shadow: 0 0 0 2px; 184 | } 185 | .grid-axis-r { 186 | border-left: 2px solid; 187 | right: auto; 188 | top: 50%; 189 | height: 100%; 190 | left: -1px; 191 | margin-left: -.5rem; 192 | } 193 | 194 | .grid-label { 195 | position: absolute; 196 | top: auto; 197 | left: auto; 198 | min-height: 1rem; 199 | margin-top: -.5rem; 200 | font-size: .8rem; 201 | pointer-events: all; 202 | white-space: nowrap; 203 | } 204 | .grid-label-x { 205 | bottom: auto; 206 | top: 100%; 207 | margin-top: 1.5rem; 208 | width: 2rem; 209 | margin-left: -1rem; 210 | text-align: center; 211 | } 212 | .grid-label-x:before { 213 | content: ''; 214 | position: absolute; 215 | height: .5rem; 216 | width: 0; 217 | border-left: 2px solid; 218 | top: -1rem; 219 | margin-left: -1px; 220 | margin-top: -2px; 221 | left: 1rem; 222 | } 223 | 224 | .grid-label-y { 225 | right: 100%; 226 | margin-right: 1.5rem; 227 | margin-top: -.5rem; 228 | } 229 | .grid-label-y:before { 230 | content: ''; 231 | position: absolute; 232 | width: .5rem; 233 | height: 0; 234 | border-top: 2px solid; 235 | right: -1rem; 236 | top: .4rem; 237 | margin-right: -1px; 238 | } 239 | 240 | .grid-label-r { 241 | right: 100%; 242 | top: calc(50% - .5rem); 243 | margin-right: 1.5rem; 244 | } 245 | .grid-label-r:before { 246 | content: ''; 247 | position: absolute; 248 | width: .5rem; 249 | height: 0; 250 | border-top: 2px solid; 251 | right: -1rem; 252 | top: .4rem; 253 | margin-right: -1px; 254 | } 255 | 256 | 257 | .grid-label-a { 258 | bottom: auto; 259 | width: 2rem; 260 | text-align: center; 261 | } -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | Indoorjs 9 |
10 |
11 | 15 |
16 |
17 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /demo/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/indoorjs/6827c93069cde2c079a889c61c9fc48b3adf417b/demo/radar.png -------------------------------------------------------------------------------- /demo/thumbR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/indoorjs/6827c93069cde2c079a889c61c9fc48b3adf417b/demo/thumbR.png -------------------------------------------------------------------------------- /dev/draw.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | Indoorjs Free Draw 9 |
10 |
11 | 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /dev/draw.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import * as Indoor from '../src/Indoor.js'; 3 | 4 | import './index.css'; 5 | 6 | const canvasEl = document.querySelector('.my-canvas'); 7 | const drawingColorEl = document.querySelector('#drawing-color'); 8 | const drawingLineWidthEl = document.querySelector('#drawing-line-width'); 9 | const clearEl = document.querySelector('#clear-canvas'); 10 | 11 | const canvas = new Indoor.Canvas(canvasEl, {}); 12 | 13 | function oninput() { 14 | canvas.setLineWidth(parseInt(this.value, 10) || 1); 15 | } 16 | 17 | drawingLineWidthEl.addEventListener('input', oninput, false); 18 | 19 | drawingColorEl.onchange = function onchange() { 20 | canvas.setColor(this.value); 21 | }; 22 | 23 | clearEl.onclick = function onclick() { 24 | canvas.clear(); 25 | }; 26 | 27 | canvas.on('mode-changed', mode => { 28 | console.log('mode-changed', mode); 29 | }); 30 | 31 | window.canv = canvas; 32 | -------------------------------------------------------------------------------- /dev/fp.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/indoorjs/6827c93069cde2c079a889c61c9fc48b3adf417b/dev/fp.jpeg -------------------------------------------------------------------------------- /dev/fp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/indoorjs/6827c93069cde2c079a889c61c9fc48b3adf417b/dev/fp.jpg -------------------------------------------------------------------------------- /dev/fp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 ' 9 " 20 ' 11 " 40.98 f²220.25 f²95.50 f² -------------------------------------------------------------------------------- /dev/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .toolbar { 9 | width: 100%; 10 | height: 100px; 11 | background: lightblue; 12 | } 13 | 14 | .context { 15 | width: 100%; 16 | height: calc(100% - 100px); 17 | } 18 | 19 | .context .sidebar { 20 | width: 300px; 21 | height: 100%; 22 | float: left; 23 | background: lightcyan 24 | } 25 | 26 | .context .my-map { 27 | display: block; 28 | overflow: hidden; 29 | height: 100%; 30 | min-height: 100%; 31 | width: auto; 32 | position: relative; 33 | } 34 | 35 | .context .my-canvas { 36 | display: block; 37 | overflow: hidden; 38 | height: 100%; 39 | min-height: 100%; 40 | width: auto; 41 | position: relative; 42 | } 43 | 44 | 45 | :host { 46 | position: relative; 47 | } 48 | 49 | .grid { 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | bottom: 0; 54 | right: 0; 55 | pointer-events: none; 56 | font-family: sans-serif; 57 | } 58 | 59 | .grid-lines { 60 | position: absolute; 61 | top: 0; 62 | left: 0; 63 | bottom: 0; 64 | right: 0; 65 | overflow: hidden; 66 | pointer-events: none; 67 | } 68 | 69 | .grid-line { 70 | pointer-events: all; 71 | position: absolute; 72 | top: 0; 73 | left: 0; 74 | width: .5rem; 75 | height: .5rem; 76 | opacity: .25; 77 | } 78 | 79 | .grid-line[hidden] { 80 | display: none; 81 | } 82 | 83 | .grid-line:hover { 84 | opacity: .5; 85 | } 86 | 87 | @supports (--css: variables) { 88 | .grid { 89 | --opacity: .15; 90 | } 91 | 92 | .grid-line { 93 | opacity: var(--opacity); 94 | } 95 | 96 | .grid-line:hover { 97 | opacity: calc(var(--opacity) * 2); 98 | } 99 | } 100 | 101 | .grid-line-x { 102 | height: 100%; 103 | width: 0; 104 | border-left: 1px solid; 105 | margin-left: -1px; 106 | } 107 | 108 | .grid-line-x:after { 109 | content: ''; 110 | position: absolute; 111 | width: .5rem; 112 | top: 0; 113 | bottom: 0; 114 | left: -.25rem; 115 | } 116 | 117 | .grid-line-x.grid-line-min { 118 | margin-left: 0px; 119 | } 120 | 121 | .grid-line-y { 122 | width: 100%; 123 | height: 0; 124 | margin-top: -1px; 125 | border-top: 1px solid; 126 | } 127 | 128 | .grid-line-y:after { 129 | content: ''; 130 | position: absolute; 131 | height: .5rem; 132 | left: 0; 133 | right: 0; 134 | top: -.25rem; 135 | } 136 | 137 | .grid-line-y.grid-line-max { 138 | margin-top: 0px; 139 | } 140 | 141 | /* radial lines */ 142 | .grid-line-r { 143 | height: 100%; 144 | width: 100%; 145 | left: 50%; 146 | top: 50%; 147 | border-radius: 50vw; 148 | box-shadow: inset 0 0 0 1px; 149 | } 150 | 151 | /* angular lines */ 152 | .grid-line-a { 153 | height: 0; 154 | top: 50%; 155 | left: 50%; 156 | transform-origin: left center; 157 | width: 50%; 158 | border-top: 1px solid; 159 | } 160 | 161 | .grid-line-a:after { 162 | content: ''; 163 | position: absolute; 164 | height: .5rem; 165 | left: 0; 166 | right: 0; 167 | top: -.25rem; 168 | } 169 | 170 | .grid-line-a:before { 171 | content: ''; 172 | position: absolute; 173 | width: .4rem; 174 | right: 0; 175 | top: -1px; 176 | height: 0; 177 | border-bottom: 2px solid; 178 | } 179 | 180 | 181 | .grid-axis { 182 | position: absolute; 183 | } 184 | 185 | .grid-axis-x { 186 | top: auto; 187 | bottom: 0; 188 | right: 0; 189 | left: 0; 190 | border-bottom: 2px solid; 191 | margin-bottom: -.5rem; 192 | } 193 | 194 | .grid-axis-y { 195 | border-left: 2px solid; 196 | right: auto; 197 | top: 0; 198 | bottom: 0; 199 | left: -1px; 200 | margin-left: -.5rem; 201 | } 202 | 203 | .grid-axis-a { 204 | height: 100%; 205 | width: 100%; 206 | left: 50%; 207 | top: 50%; 208 | border-radius: 50vw; 209 | box-shadow: 0 0 0 2px; 210 | } 211 | 212 | .grid-axis-r { 213 | border-left: 2px solid; 214 | right: auto; 215 | top: 50%; 216 | height: 100%; 217 | left: -1px; 218 | margin-left: -.5rem; 219 | } 220 | 221 | .grid-label { 222 | position: absolute; 223 | top: auto; 224 | left: auto; 225 | min-height: 1rem; 226 | margin-top: -.5rem; 227 | font-size: .8rem; 228 | pointer-events: all; 229 | white-space: nowrap; 230 | } 231 | 232 | .grid-label-x { 233 | bottom: auto; 234 | top: 100%; 235 | margin-top: 1.5rem; 236 | width: 2rem; 237 | margin-left: -1rem; 238 | text-align: center; 239 | } 240 | 241 | .grid-label-x:before { 242 | content: ''; 243 | position: absolute; 244 | height: .5rem; 245 | width: 0; 246 | border-left: 2px solid; 247 | top: -1rem; 248 | margin-left: -1px; 249 | margin-top: -2px; 250 | left: 1rem; 251 | } 252 | 253 | .grid-label-y { 254 | right: 100%; 255 | margin-right: 1.5rem; 256 | margin-top: -.5rem; 257 | } 258 | 259 | .grid-label-y:before { 260 | content: ''; 261 | position: absolute; 262 | width: .5rem; 263 | height: 0; 264 | border-top: 2px solid; 265 | right: -1rem; 266 | top: .4rem; 267 | margin-right: -1px; 268 | } 269 | 270 | .grid-label-r { 271 | right: 100%; 272 | top: calc(50% - .5rem); 273 | margin-right: 1.5rem; 274 | } 275 | 276 | .grid-label-r:before { 277 | content: ''; 278 | position: absolute; 279 | width: .5rem; 280 | height: 0; 281 | border-top: 2px solid; 282 | right: -1rem; 283 | top: .4rem; 284 | margin-right: -1px; 285 | } 286 | 287 | 288 | .grid-label-a { 289 | bottom: auto; 290 | width: 2rem; 291 | text-align: center; 292 | } -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | Indoorjs 9 |
10 |
11 | 14 |
15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import * as Indoor from '../src/Indoor.js'; 3 | 4 | import './index.css'; 5 | 6 | const mapEl = document.querySelector('.my-map'); 7 | 8 | let radar; 9 | let markers; 10 | 11 | const map = new Indoor.Map(mapEl, { 12 | floorplan: new Indoor.Floor({ 13 | url: './fp.jpg', 14 | opacity: 0.7, 15 | width: 400, 16 | zIndex: 1 17 | }), 18 | minZoom: 0.001, 19 | maxZoom: 10 20 | }); 21 | 22 | const addLinks = () => { 23 | for (let i = 1; i < markers.length; i += 1) { 24 | markers[i].setLinks([markers[i - 1]]); 25 | } 26 | }; 27 | 28 | const addMarkers = () => { 29 | markers = []; 30 | for (let i = 0; i < 10; i += 1) { 31 | const x = Math.random() * 400 - 200; 32 | const y = Math.random() * 400 - 200; 33 | const marker = new Indoor.Marker([x, y], { 34 | text: `${i + 1}`, 35 | draggable: true, 36 | zIndex: 100, 37 | id: i 38 | }); 39 | // eslint-disable-next-line no-loop-func 40 | marker.on('ready', () => { 41 | marker.addTo(map); 42 | }); 43 | markers.push(marker); 44 | window.markers = markers; 45 | } 46 | setTimeout(() => { 47 | addLinks(); 48 | // eslint-disable-next-line no-use-before-define 49 | addRadar(markers[0]); 50 | }, 1000); 51 | 52 | const rect = Indoor.markerGroup([[0, 0], [100, 200]]); 53 | rect.on('moving', e => { 54 | console.log('moving', e); 55 | }); 56 | rect.addTo(map); 57 | }; 58 | 59 | const addRadar = marker => { 60 | if (radar) { 61 | map.removeLayer(radar); 62 | } 63 | radar = new Indoor.Marker(marker.position, { 64 | size: 100, 65 | id: marker.id, 66 | icon: { 67 | url: './radar.png' 68 | }, 69 | rotation: Math.random() * 360, 70 | clickable: false, 71 | zIndex: 290 72 | }); 73 | radar.on('ready', () => { 74 | radar.addTo(map); 75 | }); 76 | window.radar = radar; 77 | }; 78 | 79 | map.on('ready', () => { 80 | console.log('map is ready'); 81 | addMarkers(); 82 | }); 83 | 84 | // map.on('marker:added', (e) => { 85 | // // console.log('marker:added', e); 86 | // // addMarkers(); 87 | // }); 88 | 89 | map.on('marker:removed', e => { 90 | console.log('marker:removed', e); 91 | // addMarkers(); 92 | }); 93 | 94 | map.on('marker:click', e => { 95 | console.log('marker:click', e); 96 | addRadar(e); 97 | }); 98 | 99 | map.on('marker:moving', e => { 100 | // console.log('marker:moving', e); 101 | if (radar && e.id === radar.id) { 102 | // console.log(e); 103 | radar.setPosition(e.position); 104 | } 105 | }); 106 | map.on('marker:moved', e => { 107 | // console.log('marker:moved', e); 108 | if (radar && e.id === radar.id) { 109 | // console.log(e); 110 | radar.setPosition(e.position); 111 | } 112 | }); 113 | 114 | map.on('markergroup:moving', e => { 115 | console.log('markergroup:moving', e); 116 | }); 117 | map.on('markergroup:rotating', (e, angle) => { 118 | console.log('markergroup:rotating', e, angle); 119 | }); 120 | 121 | map.on('marker:rotating', (e, angle) => { 122 | console.log('marker:rotating', e, angle); 123 | }); 124 | 125 | map.on('bbox:moving', () => { 126 | // console.log('bbox:moving', e); 127 | }); 128 | 129 | map.on('object:drag', e => { 130 | console.log('object:drag', e); 131 | }); 132 | 133 | map.on('object:scaling', e => { 134 | console.log('object:scaling', e); 135 | }); 136 | 137 | map.on('object:rotate', e => { 138 | console.log('object:rotate', e); 139 | }); 140 | 141 | map.on('mouse:move', () => { 142 | // console.log('mouse:move', e); 143 | }); 144 | 145 | window.map2 = map; 146 | -------------------------------------------------------------------------------- /dev/pano.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/indoorjs/6827c93069cde2c079a889c61c9fc48b3adf417b/dev/pano.jpg -------------------------------------------------------------------------------- /dev/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/indoorjs/6827c93069cde2c079a889c61c9fc48b3adf417b/dev/radar.png -------------------------------------------------------------------------------- /dev/thumbR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/indoorjs/6827c93069cde2c079a889c61c9fc48b3adf417b/dev/thumbR.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "indoorjs", 3 | "version": "1.0.19", 4 | "description": "Canvas based indoor maps", 5 | "main": "dist/indoor.js", 6 | "scripts": { 7 | "docs": "node ./build/docs.js", 8 | "pretest": "npm run lint && npm run lint-spec", 9 | "test": "npm run test-nolint", 10 | "test-nolint": "karma start ./spec/karma.conf.js", 11 | "start": "webpack-dev-server --mode development --open", 12 | "build": "npm run rollup && npm run uglify", 13 | "release": "./build/publish.sh", 14 | "lint": "eslint src", 15 | "lint-spec": "eslint spec/suites", 16 | "lintfix": "eslint src --fix; eslint spec/suites --fix;", 17 | "rollup": "rollup -c build/rollup-config.js", 18 | "lib": "webpack", 19 | "watch": "rollup -w -c build/rollup-watch-config.js", 20 | "uglify": "uglifyjs dist/indoor.js -c -m -o dist/indoor.min.js --source-map filename=dist/indoor.min.js.map --in-source-map dist/indoor.js.map --source-map-url indoor.js.map --comments", 21 | "integrity": "node ./build/integrity.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/mudin/indoorjs.git" 26 | }, 27 | "keywords": [ 28 | "indoorjs", 29 | "maps", 30 | "indoor-map", 31 | "canvas", 32 | "grid", 33 | "axis", 34 | "polar", 35 | "cartesian" 36 | ], 37 | "author": "mudin ", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/mudin/indoorjs/issues" 41 | }, 42 | "homepage": "https://github.com/mudin/indoorjs#readme", 43 | "devDependencies": { 44 | "@babel/cli": "^7.4.4", 45 | "@babel/core": "^7.4.4", 46 | "@babel/preset-env": "^7.4.4", 47 | "@babel/register": "^7.4.4", 48 | "acorn": "^6.1.1", 49 | "babel-eslint": "^8.0.3", 50 | "babel-loader": "^8.0.6", 51 | "babel-plugin-add-module-exports": "^0.2.1", 52 | "babel-plugin-istanbul": "^5.1.0", 53 | "chai": "^4.1.2", 54 | "cross-env": "^5.2.0", 55 | "css-loader": "^2.1.1", 56 | "eslint": "^5.16.0", 57 | "eslint-config-airbnb-base": "^13.1.0", 58 | "eslint-config-prettier": "^4.2.0", 59 | "eslint-loader": "^2.0.0", 60 | "eslint-plugin-import": "^2.17.2", 61 | "eslint-plugin-jsx-a11y": "^6.2.1", 62 | "eslint-plugin-prettier": "^3.1.0", 63 | "git-rev-sync": "^1.12.0", 64 | "html-webpack-plugin": "^3.2.0", 65 | "jsdom": "11.11.0", 66 | "jsdom-global": "3.0.2", 67 | "mocha": "^4.0.1", 68 | "nyc": "^13.1.0", 69 | "prettier": "^1.17.1", 70 | "rollup": "^1.12.3", 71 | "rollup-plugin-babel": "^4.3.2", 72 | "rollup-plugin-commonjs": "^10.0.0", 73 | "rollup-plugin-git-version": "^0.2.1", 74 | "rollup-plugin-json": "^4.0.0", 75 | "rollup-plugin-node-builtins": "^2.1.2", 76 | "rollup-plugin-node-globals": "^1.4.0", 77 | "style-loader": "^0.23.1", 78 | "uglifyjs-webpack-plugin": "^1.2.7", 79 | "webpack": "^4.30.0", 80 | "webpack-cli": "^3.3.1", 81 | "webpack-dev-server": "^3.3.1", 82 | "yargs": "^10.0.3" 83 | }, 84 | "dependencies": { 85 | "eventemitter2": "^5.0.1", 86 | "fabric-pure-browser": "^3.4.0" 87 | }, 88 | "nyc": { 89 | "sourceMap": false, 90 | "instrument": false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # IndoorJS 2 | [![unstable](http://badges.github.io/stability-badges/dist/unstable.svg)](http://github.com/badges/stability-badges) 3 | ![gzip-size](https://img.shields.io/badge/size-18.4kb-brightgreen.svg) 4 | 5 | HitCount 6 | 7 | 8 | Indoor maps based on fabricjs with grid system, zooming, panning and anotations. 9 | See [demo](https://mudin.github.io/indoorjs). 10 | 11 | ![Markers and Connections demo](https://mudin.github.io/indoorjs/indoorjs.gif?raw=true) 12 | 13 | ## Usage 14 | 15 | [![npm install indoorjs](https://nodei.co/npm/indoorjs.png?mini=true)](https://npmjs.org/package/indoorjs/) 16 | 17 | ```js 18 | const mapEl = document.querySelector('.my-map'); 19 | 20 | let radar; let 21 | markers; 22 | 23 | const map = new Indoor.Map(mapEl, { 24 | floorplan: new Indoor.Floor({ 25 | url: './fp.jpeg', 26 | opacity: 0.4, 27 | width: 400, 28 | zIndex: 1 29 | }), 30 | minZoom: 0.001, 31 | maxZoom: 10, 32 | center: { 33 | x: 0, 34 | y: 0, 35 | zoom: 1 36 | } 37 | }); 38 | ``` 39 | -------------------------------------------------------------------------------- /src/Indoor.js: -------------------------------------------------------------------------------- 1 | import fabric from 'fabric-pure-browser'; 2 | 3 | import { version } from '../package.json'; 4 | 5 | console.log('fabricJS ', fabric.version || window.fabric.version); 6 | console.log('IndoorJS ', version); 7 | 8 | export { version }; 9 | 10 | // constants 11 | export * from './core/index'; 12 | 13 | // geometry 14 | export * from './geometry/index'; 15 | 16 | // map 17 | export * from './map/index'; 18 | 19 | // floorplan 20 | export * from './floorplan/index'; 21 | 22 | // layer 23 | export * from './layer/index'; 24 | 25 | // Free Drawing Canvas 26 | export * from './paint/index'; 27 | -------------------------------------------------------------------------------- /src/core/Base.js: -------------------------------------------------------------------------------- 1 | import EventEmitter2 from 'eventemitter2'; 2 | 3 | class Base extends EventEmitter2 { 4 | constructor(options) { 5 | super(options); 6 | this._options = options || {}; 7 | Object.assign(this, options); 8 | } 9 | } 10 | 11 | export default Base; 12 | -------------------------------------------------------------------------------- /src/core/Constants.js: -------------------------------------------------------------------------------- 1 | import { Point } from '../geometry/Point.js'; 2 | 3 | export const Modes = { 4 | SELECT: 'SELECT', 5 | GRAB: 'GRAB', 6 | MEASURE: 'MEASURE', 7 | DRAW: 'DRAW' 8 | }; 9 | 10 | export const MAP = { 11 | center: new Point(), 12 | zoom: 1, 13 | minZoom: 0, 14 | maxZoom: 20, 15 | gridEnabled: true, 16 | zoomEnabled: true, 17 | selectEnabled: true, 18 | mode: Modes.SELECT, 19 | showGrid: true 20 | }; 21 | 22 | export const MARKER = { 23 | position: new Point(), 24 | minZoom: 1, 25 | maxZoom: 20 26 | }; 27 | 28 | export const ICON = { 29 | url: 30 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAy8SURBVHhe7Z1r1BVVGcdn5n0hQZMyL62VEGmIN4xqhaV2QQwE0cxLSHbRMsH6nvVdQEWgPpUo0kUDQQGt5eqLIaho2jKF1irAoEi8QEtdXApEV/2e2SPC4Zl59z5n5szl7P9af877cp7/s/d+nnln9uzZe0/QQPTDEfC8IAivCYLoh/x8J1zF7+v4fQvcAXfBAwnlZ/k/vhMbsQ0W8jvacLrxFfsU3x4Vw6kk6Wo+F8DVJG0bfDsMov/lSfFpfAePmbLCr5myPbqN95OIi/mcx+dzcJ+WsG5QyjZ1CObzOZnPY6WCHvnjKAI8lc9FfG7TklEFUrd/JXWUulJnj05xJryFgG7WAl5lUucXqfuspA0ebggn8M8KgviWFtw6kTbQyQxW0qaL4qZ5ZCG6jECt0QLZBNK2tbTxK0ljPd5DOAmu1oLWRNJW7iTijmzP42z4gBakXiBtfxCOkUD0GoZAOkjRf7TA9BKJwX+JxWw4VALTA5DOUPSCFoyiKAcafBn+hQo8AX8PH4LLEsrP8n98JzaxbVcPTspbT2wmUofGYjCcqzU+TxLILZTzMJxDQK8Ngj4Zxv0oPAaGcCCIjdiiEa34EF/iM9qilZknKWcelFg1CucQyGe0BndK89caPBAF0Qw+x8IiB2DEN2XEZS2XsrU6dUpi9Sz+PyEFNgDyMCXapTW0XeLvFRwv5lNuqT4QF1MOhlEHbl3juuR6MOBvN7H7uimmvpijNa5dEhBuFcPr8XuccV8pUCepW763s/i91bivF94H79ca5Er+EmQkbSn8gjiuCT4Pl1D3XEYy8SUd1do8WzgBrtUa4kr83Avlul5XyHX81xwI72jtcyF+HocnitMq4yM0tuNbPPyshp+LPTYDn4UdXxqILbeKwfDYYwUxkgpu1CpuS/Qv05u/MfHXQEjbOussot+Eo1OMv+pA/vI3aRW2JT4Wwg/H3pqNk+CdWgxsSaw34+Pk2FsFcDwVWq9V1IZo/03Pflriq4cgU8qinVpMbIh2A06kv1Uq6O2HT2oVtCF66dicFnvqTYyCbXeYib1MYC317qDtWz20MuTpZ9kGQR+8Q4uRDdEuj72UgLYHeejo/SDx4XEQ0U1arGyI+Dbjo2sIp2kVGYg0cn9vXu9tEfcLiJEevyyilfULXcGZVHK3VoksonmDSn458eGRivhx+etaDLOIZg9imWBTKAZR0HNaBbKI5k20MhjiYYdxEjMtlllE8zzaQh8lO3dWqNR+dH5GrDsuJHbOi17QzTfy/PElrcCBGS+h8mgL4dV6TLOJcLzR5wfu96O/aoVlkd7+9xO9R9uIZmqxzSKajQhzHR+YpRWURTSFnYp6EG1ceuNpbLlgtFzHtULSyKlLRqhkgMMjH0TE1GnEVXKG7nQj7wwPaQWkkYK53Qs+ZqQeOYKYut0eovmtkbaNcILmOItc969MxB65I7pCi3kWyWEnYy+upx1ZGu1RMO7SYp9GcvhUonNFdInmMI3Y70B0vNF6FIgPEevXtBykEftLE60Lwqc0Z+kMb0iEHoUj/I6eA53Y/zER2iL8ouYojdjLciuPriJ8XMtFGrF3Ghyy7vlzenkb+08bmUcX8UmJvZYTjdhb3xGMwvEBzYlG7JcYmUcJuE/LiUbJKfajjSwbt2gONOL0Lez9Hjjl4QxyYD1Ih70sQ8+EjPn/QxNrxF5WrHiUi6VabjSS239in/WMILpYE6YRgX/GXz7O1XKTRnI8JdGpWKSJNPqef5UQWs8sxnix0RyJYzg6XtFEOsPrEp1H6Qi/pefoSJLjVxHIBhitCCdqAo04kVE/vy1qdSDb51qPDmKr7lBm/cwZ218YiUeFsFjLlUZs1bkaf9aMNXIEyV64HpVCNEXLlUaMZfLoYfslycpeq/tJ7OT0r11DPMrF0eTG6jKAnYzfHLrCOLpKM9SI8Qqj8aggrDfd5AQg71g4iPmakcYoiGYmGo/KIbpRy5lGjH9iNAaPaUYag6C/KduXNRD9YzgIrLajwXit0cQrdKOXNKNWYidDibIBlEc1MZgcWQ3lY7cd+0EiGs4vVo8Vse10kqFH8bB6lC85x1Z2UA3O0ww0YpvbXHOPwmC9hgNb2c7Ofqk3trXfubL5CKdrudMotgiim7UvNQZBn3/6V3n0Wz8dJPc/FoXVjlUYy772I0XgUWmMkFxpOWwltrIzW7BK+7KVOJVe49Ei8Kg0hpIry7u6+D0J4Trty1biVF62YLPfvke5IEfRBi2HrcRUFo3YvQQBwydj9x51gNWUcck9ttEO7ctWYiivVPGoBx7RcthKcr8TW7uXOWAo1wuPemCllsNWSu6xtVsDgKGfAVwfWM0Ultxj6w+ABsLpAPCXgObB6RLgO4HNg1Mn0N8GNg+2t4FbsbXbBwBjPxBUD7gMBD0tAj8U3Cy4DgU7PQyKJxB4VBouD4Pugv5xcLPQP07LnUZyL4+D/YSQZiG8RsudRmxlQkjf+dqXGjEecIMBj9LhsMFHn0wJc5oUKq9l96g2bDv1ByeF+mnhzYFMC9+q5a6V2B2cFi5wWBgSnGMkHhVEWwtDBNZLwyhgRqLxqBzaXhrmF4c2BC6LQw97k4vL8vDXsPfLw6sHWR7+qpazVmLXujxcELlsEHFJIvKoDKLJWq40YvsCgiOe6/gtYuqNe7RcacRW2yImnKQZa+QI8ptEVQu5bBLltE0cB8y3E51H6Qi/qeVIoxwoCDhgdDicRkJ59btHJRCu0XKkEeOsy7d9R0KI4Fyj8ygR1k//hOQ4swN/FAZ+s+h6YYmWG43kdhv2Q4wsHQ4bDMT3k2cYmUcJOJ0cuGwXb7XBx2k4dXlhxG+MzKME3KvlRCM5lad/1i+SfFhzojFx/Ckj8+gixjr+of7OyKwQOr0p3N8RlAH7LeKF2F+YCG0RPq05SiP2302EHoUjvF7LQRqxfyYRuiCaqjlLI/YywOBfHFk8jpNYazlII/aXJVpX2O0e8i4R3G10HgVioRb7NMqZPNG1g/AizWkWOdquSMQeuSO6XIt5FslhJy+PjmF9RyCkkq+j8a+Pzx8yZ8P19fEuPf9UjKZg68EGIUedLCSNjNwjBxDL8Akt1mlEkusg3WytkCyimWekHjlgrhbjLKK51UjzgTwj+JtWUBbR+HcLdIxohhbbLKLZiDDrBZFtYbxW2EDk1HXYmyk8XBBdqcV0ICJ0HfSxxjytwCzSiP3oCqtQgzGe2O3TYppFdAuMvBgMolLWk0ffJZo30Y4zLjws8Bli9oYWyyyikcmeg42L4nAWBe3RKpBFaZCMKyQ+PFIRTiBWTrd7QjR7EY8xPgqH/RLkQ0kluRz4PkE6oqug82nfsPtL92/TKzIwaeRNiQ+Pg4hmarGyIeK5xkf3sVyrkA3R3gH7Yi+9DYnB7VqMbIj2wdhLSeBe0+2B0aFEvwaOij31Jj4OrVdmt5LYy3bvA87xKxoncPqy2pZMI9qdvdkviK/3Vpt0akQr2/adaHyVj5Op0GatorbEx8/hSbG3ZkOS9jMtBrYk1i/iY3jsrUI4hYpt0ipsS/TbORvckPhrIKRt0Xat7bZMkn+q8Vc9yH5D67WKuxA/f4BNWnQig2CPam11IbGV0/6I2GOFIac4q71qs0hj38HPr2Cd31Us2+n8krZYbcKVRfzII/baXCLlSdQyrSGuJHjyXFvWHlwgjmuC8+F91N1pHkUa8bUclt7bbwdtDxZpJKCPch29Dr8fNO4rBeokK6ajjk/1hxK/txv3tUV4LUHZrTWuXeJPtjhbxOelfA6LiykHx1KHqXxKXTrq3LUSf3vCIPyGKab+4DoePqs1tFMmB8My/H/PlFPoPobim+t6XNb9lG21v6Ir8fsn/I+VApsECZ7zfAJXEry/U45sgz6LRE1PNriWnrNsdW/zvgOxEVs0oo3308VXsEp8a2XmScqR5/lN3owznEggO75VdCHl7YUvwQ1UQJaxPQJXwqUJ5Wf5P74Tm9h2r+arKFIet3jhJOrQExgK59Boq33tm0xisI9YyAROiUnPQe6TV2iB6QXSdjnz1HmcIy9Ekzn9tf1UrG6krWto85Sk8R7vIbqc4HQ8ilhVStto41eTxnqkI17PJr1u6w0QqkraIBtocFcinV8PV5wFZxPEwm+/8qbU2dQ9OFsa4tEZhhBQGfW7h89CBl/yoNSNOi7mU9bj13Lsvg4YRoClA7WAz+dhLg9c2qGUbeoge+/HdSpzWLpnMYrr6zQ+fwqldy2DOFZvzXCh+DS+4zdtUFZcZi/PZ6wsZGWMvAzpApIkD6Ju5md5QaJ0xNbx+xa4A+6CBxLKz/J/W7GRiZYylIwm+pHxET+KHgkLX3XTXQTB/wEErHoK8OgOXgAAAABJRU5ErkJggg==', 31 | size: [128, 128], 32 | anchor: [64, 64] 33 | }; 34 | 35 | fabric.Object.prototype.originX = 'center'; 36 | fabric.Object.prototype.originY = 'center'; 37 | 38 | fabric.Object.prototype.lockUniScaling = true; 39 | fabric.Object.prototype.lockScalingFlip = true; 40 | fabric.Object.prototype.transparentCorners = false; 41 | fabric.Object.prototype.centeredScaling = true; 42 | // fabric.Object.prototype.cornerStyle = 'circle'; 43 | fabric.Object.prototype.cornerColor = 'blue'; 44 | fabric.Object.prototype.borderColor = 'blue'; 45 | fabric.Object.prototype.borderOpacity = 0.7; 46 | fabric.Object.prototype.cornerOpacity = 0.7; 47 | fabric.Object.prototype.cornerStrokeColor = 'blue'; 48 | 49 | fabric.Object.prototype.borderColor = '#ff0099'; 50 | fabric.Object.prototype.cornerColor = '#00eaff'; 51 | fabric.Object.prototype.cornerStrokeColor = '#00bbff'; 52 | 53 | fabric.Object.prototype.objectCaching = false; 54 | fabric.Group.prototype.objectCaching = true; 55 | 56 | fabric.Group.prototype.selectionBackgroundColor = 'rgba(45,207,171,0.25)'; 57 | 58 | fabric.Object.prototype.borderDashArray = [3, 3]; 59 | 60 | fabric.Object.prototype.padding = 5; 61 | 62 | fabric.Object.prototype.getBounds = function getBounds() { 63 | const coords = []; 64 | coords.push(new Point(this.left - this.width / 2.0, this.top - this.height / 2.0)); 65 | coords.push(new Point(this.left + this.width / 2.0, this.top + this.height / 2.0)); 66 | return coords; 67 | }; 68 | -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | export * from './Constants'; 2 | -------------------------------------------------------------------------------- /src/floorplan/Floor.js: -------------------------------------------------------------------------------- 1 | import { Point } from '../geometry/Point'; 2 | import { Group } from '../layer/Group'; 3 | import { Layer } from '../layer/Layer'; 4 | 5 | export class Floor extends Layer { 6 | constructor(options) { 7 | super(options); 8 | 9 | this.width = this.width || -1; 10 | this.height = this.height || -1; 11 | 12 | this.position = new Point(this.position); 13 | 14 | this.class = 'floorplan'; 15 | 16 | this.load(); 17 | } 18 | 19 | load() { 20 | const vm = this; 21 | const index = this.url.lastIndexOf('.'); 22 | const ext = this.url.substr(index + 1, 3); 23 | 24 | if (ext === 'svg') { 25 | fabric.loadSVGFromURL(this.url, (objects, options) => { 26 | objects = objects.filter(e => e.id !== 'grid'); 27 | const image = fabric.util.groupSVGElements(objects, options); 28 | vm.setImage(image); 29 | }); 30 | } else { 31 | fabric.Image.fromURL( 32 | this.url, 33 | image => { 34 | vm.setImage(image); 35 | }, 36 | { 37 | selectable: false, 38 | opacity: this.opacity 39 | } 40 | ); 41 | } 42 | 43 | this.handler = new fabric.Rect({ 44 | left: 0, 45 | top: 0, 46 | width: 0.1, 47 | height: 0.1, 48 | stroke: 'black', 49 | fill: '', 50 | hasControls: false, 51 | hasBorders: false 52 | }); 53 | } 54 | 55 | setImage(image) { 56 | if (this.shape && this.image) { 57 | this.shape.remove(this.image); 58 | } 59 | const ratio = image.width / image.height; 60 | if (this.width === -1 && this.height === -1) { 61 | this.width = image.width; 62 | this.height = image.height; 63 | } else if (this.width === -1) { 64 | this.width = this.height * ratio; 65 | } else if (this.height === -1) { 66 | this.height = this.width / ratio; 67 | } 68 | image.originalWidth = image.width; 69 | image.originalHeight = image.height; 70 | this.image = image.scaleToWidth(this.width, true); 71 | 72 | this.scaleX = image.scaleX + 0; 73 | this.scaleY = image.scaleY + 0; 74 | 75 | this.drawShape(); 76 | } 77 | 78 | drawShape() { 79 | if (this.shape) { 80 | this.shape.addWithUpdate(this.image); 81 | this.emit('load', this); 82 | return; 83 | } 84 | 85 | this.shape = new Group([this.image, this.handler], { 86 | selectable: false, 87 | draggable: false, 88 | left: this.position.x, 89 | top: this.position.y, 90 | parent: this, 91 | lockMovementX: true, 92 | lockMovementY: true, 93 | class: this.class, 94 | zIndex: this.zIndex 95 | }); 96 | this.emit('load', this); 97 | } 98 | 99 | setWidth(width) { 100 | this.width = width; 101 | this.onResize(); 102 | } 103 | 104 | setHeight(height) { 105 | this.height = height; 106 | this.onResize(); 107 | } 108 | 109 | setOpacity(opacity) { 110 | this.opacity = opacity; 111 | this.image.set('opacity', opacity); 112 | if (this.image.canvas) { 113 | this.image.canvas.renderAll(); 114 | } 115 | } 116 | 117 | setPosition(position) { 118 | this.position = new Point(position); 119 | if (!this.shape) return; 120 | this.shape.set({ 121 | left: this.position.x, 122 | top: this.position.y 123 | }); 124 | } 125 | 126 | setUrl(url) { 127 | this.url = url; 128 | this.load(); 129 | } 130 | 131 | onResize(width, height) { 132 | if (width !== undefined) { 133 | this.width = width; 134 | } 135 | if (height !== undefined) { 136 | this.height = height; 137 | } 138 | 139 | const ratio = this.image.width / this.image.height; 140 | if (this.width === -1 && this.height === -1) { 141 | this.width = this.image.width; 142 | this.height = this.image.height; 143 | } else if (this.width === -1) { 144 | this.width = this.height / ratio; 145 | } else if (this.height === -1) { 146 | this.height = this.width * ratio; 147 | } 148 | this.image = this.image.scaleToWidth(this.width); 149 | this.shape.addWithUpdate(); 150 | } 151 | } 152 | 153 | export const floorplan = options => new Floor(options); 154 | -------------------------------------------------------------------------------- /src/floorplan/index.js: -------------------------------------------------------------------------------- 1 | export * from './Floor.js'; 2 | -------------------------------------------------------------------------------- /src/geometry/Point.js: -------------------------------------------------------------------------------- 1 | export class Point extends fabric.Point { 2 | constructor(...params) { 3 | let x; 4 | let y; 5 | if (params.length > 1) { 6 | [x, y] = params; 7 | } else if (params.length === 0 || !params[0]) { 8 | [x, y] = [0, 0]; 9 | } else if (Object.prototype.hasOwnProperty.call(params[0], 'x')) { 10 | x = params[0].x; 11 | y = params[0].y; 12 | } else if (params[0].length) { 13 | [[x, y]] = params; 14 | } else { 15 | console.error( 16 | 'Parameter for Point is not valid. Use Point(x,y) or Point({x,y}) or Point([x,y])', 17 | params 18 | ); 19 | } 20 | 21 | super(x, y); 22 | } 23 | 24 | setX(x) { 25 | this.x = x || 0; 26 | } 27 | 28 | setY(y) { 29 | this.y = y || 0; 30 | } 31 | 32 | copy(point) { 33 | this.x = point.x; 34 | this.y = point.y; 35 | } 36 | 37 | getArray() { 38 | return [this.x, this.y]; 39 | } 40 | } 41 | 42 | export const point = (...params) => new Point(...params); 43 | -------------------------------------------------------------------------------- /src/geometry/index.js: -------------------------------------------------------------------------------- 1 | export { Point, point } from './Point'; 2 | -------------------------------------------------------------------------------- /src/grid/Axis.js: -------------------------------------------------------------------------------- 1 | class Axis { 2 | constructor(orientation, options) { 3 | Object.assign(this, options); 4 | this.orientation = orientation || 'x'; 5 | } 6 | 7 | getCoords(values) { 8 | const coords = []; 9 | if (!values) return coords; 10 | for (let i = 0; i < values.length; i += 1) { 11 | const t = this.getRatio(values[i]); 12 | coords.push(t); 13 | coords.push(0); 14 | coords.push(t); 15 | coords.push(1); 16 | } 17 | return coords; 18 | } 19 | 20 | getRange() { 21 | let len = this.width; 22 | if (this.orientation === 'y') len = this.height; 23 | return len * this.zoom; 24 | } 25 | 26 | getRatio(value) { 27 | return (value - this.offset) / this.range; 28 | } 29 | 30 | setOffset(offset) { 31 | this.offset = offset; 32 | } 33 | 34 | update(options) { 35 | options = options || {}; 36 | Object.assign(this, options); 37 | 38 | this.range = this.getRange(); 39 | } 40 | } 41 | export default Axis; 42 | -------------------------------------------------------------------------------- /src/grid/Grid.js: -------------------------------------------------------------------------------- 1 | import alpha from '../lib/color-alpha'; 2 | import Base from '../core/Base'; 3 | import { 4 | clamp, almost, len, parseUnit, toPx, isObj 5 | } from '../lib/mumath/index'; 6 | import gridStyle from './gridStyle'; 7 | import Axis from './Axis'; 8 | import { Point } from '../geometry/Point'; 9 | 10 | // constructor 11 | class Grid extends Base { 12 | constructor(canvas, opts) { 13 | super(opts); 14 | this.canvas = canvas; 15 | this.context = this.canvas.getContext('2d'); 16 | this.state = {}; 17 | this.setDefaults(); 18 | this.update(opts); 19 | } 20 | 21 | render() { 22 | this.draw(); 23 | return this; 24 | } 25 | 26 | getCenterCoords() { 27 | let state = this.state.x; 28 | let [width, height] = state.shape; 29 | let [pt, pr, pb, pl] = state.padding; 30 | let axisCoords = state.opposite.coordinate.getCoords( 31 | [state.coordinate.axisOrigin], 32 | state.opposite 33 | ); 34 | const y = pt + axisCoords[1] * (height - pt - pb); 35 | state = this.state.y; 36 | [width, height] = state.shape; 37 | [pt, pr, pb, pl] = state.padding; 38 | axisCoords = state.opposite.coordinate.getCoords([state.coordinate.axisOrigin], state.opposite); 39 | const x = pl + axisCoords[0] * (width - pr - pl); 40 | return { x, y }; 41 | } 42 | 43 | setSize(width, height) { 44 | this.setWidth(width); 45 | this.setHeight(height); 46 | } 47 | 48 | setWidth(width) { 49 | this.canvas.width = width; 50 | } 51 | 52 | setHeight(height) { 53 | this.canvas.height = height; 54 | } 55 | 56 | // re-evaluate lines, calc options for renderer 57 | update(opts) { 58 | if (!opts) opts = {}; 59 | const shape = [this.canvas.width, this.canvas.height]; 60 | 61 | // recalc state 62 | this.state.x = this.calcCoordinate(this.axisX, shape, this); 63 | this.state.y = this.calcCoordinate(this.axisY, shape, this); 64 | this.state.x.opposite = this.state.y; 65 | this.state.y.opposite = this.state.x; 66 | this.emit('update', opts); 67 | return this; 68 | } 69 | 70 | // re-evaluate lines, calc options for renderer 71 | update2(center) { 72 | const shape = [this.canvas.width, this.canvas.height]; 73 | Object.assign(this.center, center); 74 | // recalc state 75 | this.state.x = this.calcCoordinate(this.axisX, shape, this); 76 | this.state.y = this.calcCoordinate(this.axisY, shape, this); 77 | this.state.x.opposite = this.state.y; 78 | this.state.y.opposite = this.state.x; 79 | this.emit('update', center); 80 | 81 | this.axisX.offset = center.x; 82 | this.axisX.zoom = 1 / center.zoom; 83 | 84 | this.axisY.offset = center.y; 85 | this.axisY.zoom = 1 / center.zoom; 86 | } 87 | 88 | // get state object with calculated params, ready for rendering 89 | calcCoordinate(coord, shape) { 90 | const state = { 91 | coordinate: coord, 92 | shape, 93 | grid: this 94 | }; 95 | // calculate real offset/range 96 | state.range = coord.getRange(state); 97 | state.offset = clamp( 98 | coord.offset - state.range * clamp(0.5, 0, 1), 99 | Math.max(coord.min, -Number.MAX_VALUE + 1), 100 | Math.min(coord.max, Number.MAX_VALUE) - state.range 101 | ); 102 | 103 | state.zoom = coord.zoom; 104 | // calc style 105 | state.axisColor = typeof coord.axisColor === 'number' 106 | ? alpha(coord.color, coord.axisColor) 107 | : coord.axisColor || coord.color; 108 | 109 | state.axisWidth = coord.axisWidth || coord.lineWidth; 110 | state.lineWidth = coord.lineWidth; 111 | state.tickAlign = coord.tickAlign; 112 | state.labelColor = state.color; 113 | // get padding 114 | if (typeof coord.padding === 'number') { 115 | state.padding = Array(4).fill(coord.padding); 116 | } else if (coord.padding instanceof Function) { 117 | state.padding = coord.padding(state); 118 | } else { 119 | state.padding = coord.padding; 120 | } 121 | // calc font 122 | if (typeof coord.fontSize === 'number') { 123 | state.fontSize = coord.fontSize; 124 | } else { 125 | const units = parseUnit(coord.fontSize); 126 | state.fontSize = units[0] * toPx(units[1]); 127 | } 128 | state.fontFamily = coord.fontFamily || 'sans-serif'; 129 | // get lines stops, including joined list of values 130 | let lines; 131 | if (coord.lines instanceof Function) { 132 | lines = coord.lines(state); 133 | } else { 134 | lines = coord.lines || []; 135 | } 136 | state.lines = lines; 137 | // calc colors 138 | if (coord.lineColor instanceof Function) { 139 | state.lineColors = coord.lineColor(state); 140 | } else if (Array.isArray(coord.lineColor)) { 141 | state.lineColors = coord.lineColor; 142 | } else { 143 | let color = alpha(coord.color, coord.lineColor); 144 | if (typeof coord.lineColor !== 'number') { 145 | color = coord.lineColor === false || coord.lineColor == null ? null : coord.color; 146 | } 147 | state.lineColors = Array(lines.length).fill(color); 148 | } 149 | // calc ticks 150 | let ticks; 151 | if (coord.ticks instanceof Function) { 152 | ticks = coord.ticks(state); 153 | } else if (Array.isArray(coord.ticks)) { 154 | ticks = coord.ticks; 155 | } else { 156 | const tick = coord.ticks === true || coord.ticks === true 157 | ? state.axisWidth * 2 : coord.ticks || 0; 158 | ticks = Array(lines.length).fill(tick); 159 | } 160 | state.ticks = ticks; 161 | // calc labels 162 | let labels; 163 | if (coord.labels === true) labels = state.lines; 164 | else if (coord.labels instanceof Function) { 165 | labels = coord.labels(state); 166 | } else if (Array.isArray(coord.labels)) { 167 | labels = coord.labels; 168 | } else if (isObj(coord.labels)) { 169 | labels = coord.labels; 170 | } else { 171 | labels = Array(state.lines.length).fill(null); 172 | } 173 | state.labels = labels; 174 | // convert hashmap ticks/labels to lines + colors 175 | if (isObj(ticks)) { 176 | state.ticks = Array(lines.length).fill(0); 177 | } 178 | if (isObj(labels)) { 179 | state.labels = Array(lines.length).fill(null); 180 | } 181 | if (isObj(ticks)) { 182 | // eslint-disable-next-line guard-for-in 183 | Object.keys(ticks).forEach((value, tick) => { 184 | state.ticks.push(tick); 185 | state.lines.push(parseFloat(value)); 186 | state.lineColors.push(null); 187 | state.labels.push(null); 188 | }); 189 | } 190 | 191 | if (isObj(labels)) { 192 | Object.keys(labels).forEach((label, value) => { 193 | state.labels.push(label); 194 | state.lines.push(parseFloat(value)); 195 | state.lineColors.push(null); 196 | state.ticks.push(null); 197 | }); 198 | } 199 | 200 | return state; 201 | } 202 | 203 | setDefaults() { 204 | this.pixelRatio = window.devicePixelRatio; 205 | this.autostart = true; 206 | this.interactions = true; 207 | 208 | this.defaults = Object.assign( 209 | { 210 | type: 'linear', 211 | name: '', 212 | units: '', 213 | state: {}, 214 | 215 | // visible range params 216 | minZoom: -Infinity, 217 | maxZoom: Infinity, 218 | min: -Infinity, 219 | max: Infinity, 220 | offset: 0, 221 | origin: 0.5, 222 | center: { 223 | x: 0, 224 | y: 0, 225 | zoom: 1 226 | }, 227 | zoom: 1, 228 | zoomEnabled: true, 229 | panEnabled: true, 230 | 231 | // labels 232 | labels: true, 233 | fontSize: '11pt', 234 | fontFamily: 'sans-serif', 235 | padding: 0, 236 | color: 'rgb(0,0,0,1)', 237 | 238 | // lines params 239 | lines: true, 240 | tick: 8, 241 | tickAlign: 0.5, 242 | lineWidth: 1, 243 | distance: 13, 244 | style: 'lines', 245 | lineColor: 0.4, 246 | 247 | // axis params 248 | axis: true, 249 | axisOrigin: 0, 250 | axisWidth: 2, 251 | axisColor: 0.8, 252 | 253 | // stub methods 254 | // return coords for the values, redefined by axes 255 | getCoords: () => [0, 0, 0, 0], 256 | 257 | // return 0..1 ratio based on value/offset/range, redefined by axes 258 | getRatio: () => 0, 259 | 260 | // default label formatter 261 | format: v => v 262 | }, 263 | gridStyle, 264 | this._options 265 | ); 266 | 267 | this.axisX = new Axis('x', this.defaults); 268 | this.axisY = new Axis('y', this.defaults); 269 | 270 | this.axisX = Object.assign({}, this.defaults, { 271 | orientation: 'x', 272 | offset: this.center.x, 273 | getCoords: (values, state) => { 274 | const coords = []; 275 | if (!values) return coords; 276 | for (let i = 0; i < values.length; i += 1) { 277 | const t = state.coordinate.getRatio(values[i], state); 278 | coords.push(t); 279 | coords.push(0); 280 | coords.push(t); 281 | coords.push(1); 282 | } 283 | return coords; 284 | }, 285 | getRange: state => state.shape[0] * state.coordinate.zoom, 286 | // FIXME: handle infinity case here 287 | getRatio: (value, state) => (value - state.offset) / state.range 288 | }); 289 | this.axisY = Object.assign({}, this.defaults, { 290 | orientation: 'y', 291 | offset: this.center.y, 292 | getCoords: (values, state) => { 293 | const coords = []; 294 | if (!values) return coords; 295 | for (let i = 0; i < values.length; i += 1) { 296 | const t = state.coordinate.getRatio(values[i], state); 297 | coords.push(0); 298 | coords.push(t); 299 | coords.push(1); 300 | coords.push(t); 301 | } 302 | return coords; 303 | }, 304 | getRange: state => state.shape[1] * state.coordinate.zoom, 305 | getRatio: (value, state) => 1 - (value - state.offset) / state.range 306 | }); 307 | 308 | Object.assign(this, this.defaults); 309 | Object.assign(this, this._options); 310 | 311 | this.center = new Point(this.center); 312 | } 313 | 314 | // draw grid to the canvas 315 | draw() { 316 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 317 | this.drawLines(this.state.x); 318 | this.drawLines(this.state.y); 319 | return this; 320 | } 321 | 322 | // lines instance draw 323 | drawLines(state) { 324 | // draw lines and sublines 325 | if (!state || !state.coordinate) return; 326 | 327 | const ctx = this.context; 328 | const [width, height] = state.shape; 329 | const left = 0; 330 | const top = 0; 331 | const [pt, pr, pb, pl] = state.padding; 332 | 333 | let axisRatio = state.opposite.coordinate.getRatio(state.coordinate.axisOrigin, state.opposite); 334 | axisRatio = clamp(axisRatio, 0, 1); 335 | const coords = state.coordinate.getCoords(state.lines, state); 336 | // draw state.lines 337 | ctx.lineWidth = 1; // state.lineWidth/2.; 338 | for (let i = 0, j = 0; i < coords.length; i += 4, j += 1) { 339 | const color = state.lineColors[j]; 340 | if (!color) continue; 341 | ctx.strokeStyle = color; 342 | ctx.beginPath(); 343 | const x1 = left + pl + coords[i] * (width - pr - pl); 344 | const y1 = top + pt + coords[i + 1] * (height - pb - pt); 345 | const x2 = left + pl + coords[i + 2] * (width - pr - pl); 346 | const y2 = top + pt + coords[i + 3] * (height - pb - pt); 347 | ctx.moveTo(x1, y1); 348 | ctx.lineTo(x2, y2); 349 | ctx.stroke(); 350 | ctx.closePath(); 351 | } 352 | const normals = []; 353 | for (let i = 0; i < coords.length; i += 4) { 354 | const x1 = coords[i]; 355 | const y1 = coords[i + 1]; 356 | const x2 = coords[i + 2]; 357 | const y2 = coords[i + 3]; 358 | const xDif = x2 - x1; 359 | const yDif = y2 - y1; 360 | const dist = len(xDif, yDif); 361 | normals.push(xDif / dist); 362 | normals.push(yDif / dist); 363 | } 364 | // calc state.labels/tick coords 365 | const tickCoords = []; 366 | state.labelCoords = []; 367 | const ticks = state.ticks; 368 | for (let i = 0, j = 0, k = 0; i < normals.length; k += 1, i += 2, j += 4) { 369 | const x1 = coords[j]; 370 | const y1 = coords[j + 1]; 371 | const x2 = coords[j + 2]; 372 | const y2 = coords[j + 3]; 373 | const xDif = (x2 - x1) * axisRatio; 374 | const yDif = (y2 - y1) * axisRatio; 375 | const tick = [ 376 | (normals[i] * ticks[k]) / (width - pl - pr), 377 | (normals[i + 1] * ticks[k]) / (height - pt - pb) 378 | ]; 379 | tickCoords.push(normals[i] * (xDif + tick[0] * state.tickAlign) + x1); 380 | tickCoords.push(normals[i + 1] * (yDif + tick[1] * state.tickAlign) + y1); 381 | tickCoords.push(normals[i] * (xDif - tick[0] * (1 - state.tickAlign)) + x1); 382 | tickCoords.push(normals[i + 1] * (yDif - tick[1] * (1 - state.tickAlign)) + y1); 383 | state.labelCoords.push(normals[i] * xDif + x1); 384 | state.labelCoords.push(normals[i + 1] * yDif + y1); 385 | } 386 | // draw ticks 387 | if (ticks.length) { 388 | ctx.lineWidth = state.axisWidth / 2; 389 | ctx.beginPath(); 390 | for (let i = 0, j = 0; i < tickCoords.length; i += 4, j += 1) { 391 | if (almost(state.lines[j], state.opposite.coordinate.axisOrigin)) continue; 392 | const x1 = left + pl + tickCoords[i] * (width - pl - pr); 393 | const y1 = top + pt + tickCoords[i + 1] * (height - pt - pb); 394 | const x2 = left + pl + tickCoords[i + 2] * (width - pl - pr); 395 | const y2 = top + pt + tickCoords[i + 3] * (height - pt - pb); 396 | ctx.moveTo(x1, y1); 397 | ctx.lineTo(x2, y2); 398 | } 399 | ctx.strokeStyle = state.axisColor; 400 | ctx.stroke(); 401 | ctx.closePath(); 402 | } 403 | // draw axis 404 | if (state.coordinate.axis && state.axisColor) { 405 | const axisCoords = state.opposite.coordinate.getCoords( 406 | [state.coordinate.axisOrigin], 407 | state.opposite 408 | ); 409 | ctx.lineWidth = state.axisWidth / 2; 410 | const x1 = left + pl + clamp(axisCoords[0], 0, 1) * (width - pr - pl); 411 | const y1 = top + pt + clamp(axisCoords[1], 0, 1) * (height - pt - pb); 412 | const x2 = left + pl + clamp(axisCoords[2], 0, 1) * (width - pr - pl); 413 | const y2 = top + pt + clamp(axisCoords[3], 0, 1) * (height - pt - pb); 414 | ctx.beginPath(); 415 | ctx.moveTo(x1, y1); 416 | ctx.lineTo(x2, y2); 417 | ctx.strokeStyle = state.axisColor; 418 | ctx.stroke(); 419 | ctx.closePath(); 420 | } 421 | // draw state.labels 422 | this.drawLabels(state); 423 | } 424 | 425 | drawLabels(state) { 426 | if (state.labels) { 427 | const ctx = this.context; 428 | const [width, height] = state.shape; 429 | const [pt, pr, pb, pl] = state.padding; 430 | 431 | ctx.font = `300 ${state.fontSize}px ${state.fontFamily}`; 432 | ctx.fillStyle = state.labelColor; 433 | ctx.textBaseline = 'top'; 434 | const textHeight = state.fontSize; 435 | const indent = state.axisWidth + 1.5; 436 | const textOffset = state.tickAlign < 0.5 437 | ? -textHeight - state.axisWidth * 2 : state.axisWidth * 2; 438 | const isOpp = state.coordinate.orientation === 'y' && !state.opposite.disabled; 439 | for (let i = 0; i < state.labels.length; i += 1) { 440 | let label = state.labels[i]; 441 | if (label == null) continue; 442 | 443 | if (isOpp && almost(state.lines[i], state.opposite.coordinate.axisOrigin)) continue; 444 | 445 | const textWidth = ctx.measureText(label).width; 446 | 447 | let textLeft = state.labelCoords[i * 2] * (width - pl - pr) + indent + pl; 448 | 449 | if (state.coordinate.orientation === 'y') { 450 | textLeft = clamp(textLeft, indent, width - textWidth - 1 - state.axisWidth); 451 | label *= -1; 452 | } 453 | 454 | let textTop = state.labelCoords[i * 2 + 1] * (height - pt - pb) + textOffset + pt; 455 | if (state.coordinate.orientation === 'x') { 456 | textTop = clamp(textTop, 0, height - textHeight - textOffset); 457 | } 458 | ctx.fillText(label, textLeft, textTop); 459 | } 460 | } 461 | } 462 | } 463 | 464 | export default Grid; 465 | -------------------------------------------------------------------------------- /src/grid/gridStyle.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | import alpha from '../lib/color-alpha'; 3 | import { 4 | range, almost, scale, isMultiple, lg 5 | } from '../lib/mumath/index'; 6 | 7 | const gridStyle = { 8 | steps: [1, 2, 5], 9 | distance: 20, 10 | unit: 10, 11 | lines: (state) => { 12 | const coord = state.coordinate; 13 | // eslint-disable-next-line no-multi-assign 14 | const step = state.step = scale(coord.distance * coord.zoom, coord.steps); 15 | return range(Math.floor(state.offset / step) * step, 16 | Math.ceil((state.offset + state.range) / step + 1) * step, step); 17 | }, 18 | lineColor: (state) => { 19 | if (!state.lines) return; 20 | const coord = state.coordinate; 21 | 22 | const light = alpha(coord.color, 0.1); 23 | const heavy = alpha(coord.color, 0.3); 24 | 25 | const step = state.step; 26 | const power = Math.ceil(lg(step)); 27 | const tenStep = 10 ** power; 28 | const nextStep = 10 ** (power + 1); 29 | const eps = step / 10; 30 | const colors = state.lines.map(v => { 31 | if (isMultiple(v, nextStep, eps)) return heavy; 32 | if (isMultiple(v, tenStep, eps)) return light; 33 | return null; 34 | }); 35 | return colors; 36 | }, 37 | ticks: state => { 38 | if (!state.lines) return; 39 | const coord = state.coordinate; 40 | const step = scale(scale(state.step * 1.1, coord.steps) * 1.1, coord.steps); 41 | const eps = step / 10; 42 | const tickWidth = state.axisWidth * 4; 43 | return state.lines.map(v => { 44 | if (!isMultiple(v, step, eps)) return null; 45 | if (almost(v, 0, eps)) return null; 46 | return tickWidth; 47 | }); 48 | }, 49 | labels: state => { 50 | if (!state.lines) return; 51 | const coord = state.coordinate; 52 | 53 | const step = scale(scale(state.step * 1.1, coord.steps) * 1.1, coord.steps); 54 | // let precision = clamp(Math.abs(Math.floor(lg(step))), 10, 20); 55 | const eps = step / 100; 56 | return state.lines.map(v => { 57 | if (!isMultiple(v, step, eps)) return null; 58 | if (almost(v, 0, eps)) return coord.orientation === 'y' ? null : '0'; 59 | v = Number((v / 100).toFixed(2)); 60 | return coord.format(v); 61 | }); 62 | } 63 | }; 64 | 65 | export default gridStyle; 66 | -------------------------------------------------------------------------------- /src/layer/Connector.js: -------------------------------------------------------------------------------- 1 | import { Layer } from './Layer'; 2 | import { Line } from './vector/Line'; 3 | 4 | export class Connector extends Layer { 5 | constructor(start, end, options) { 6 | options = options || {}; 7 | options.zIndex = options.zIndex || 10; 8 | options.class = 'connector'; 9 | super(options); 10 | 11 | if (!start || !end) { 12 | console.error('start or end is missing'); 13 | return; 14 | } 15 | this.start = start; 16 | this.end = end; 17 | this.strokeWidth = this.strokeWidth || 1; 18 | 19 | Object.assign(this.style, { 20 | strokeWidth: this.strokeWidth, 21 | stroke: this.color || 'grey', 22 | fill: this.fill || false, 23 | selectable: false 24 | }); 25 | 26 | this.draw(); 27 | 28 | this.registerListeners(); 29 | } 30 | 31 | registerListeners() { 32 | const vm = this; 33 | this.start.on('update:links', () => { 34 | vm.shape.set({ 35 | x1: vm.start.position.x, 36 | y1: vm.start.position.y 37 | }); 38 | }); 39 | 40 | this.end.on('update:links', () => { 41 | vm.shape.set({ 42 | x2: vm.end.position.x, 43 | y2: vm.end.position.y 44 | }); 45 | }); 46 | } 47 | 48 | draw() { 49 | this.shape = new Line( 50 | [this.start.position.x, this.start.position.y, this.end.position.x, this.end.position.y], 51 | this.style 52 | ); 53 | // this.shape.setCoords(); 54 | } 55 | 56 | redraw() { 57 | this.shape.set({ 58 | x1: this.start.position.x, 59 | y1: this.start.position.y, 60 | x2: this.end.position.x, 61 | y2: this.end.position.y 62 | }); 63 | } 64 | 65 | setStart(start) { 66 | this.start = start; 67 | this.redraw(); 68 | } 69 | 70 | setEnd(end) { 71 | this.end = end; 72 | this.redraw(); 73 | } 74 | 75 | setColor(color) { 76 | this.color = color; 77 | this.style.stroke = color; 78 | this.shape.set('stroke', color); 79 | if (this.shape.canvas) { 80 | this.shape.canvas.renderAll(); 81 | } 82 | } 83 | 84 | setStrokeWidth(strokeWidth) { 85 | this.strokeWidth = strokeWidth; 86 | this.style.strokeWidth = strokeWidth; 87 | this.shape.set('strokeWidth', strokeWidth); 88 | if (this.shape.canvas) { 89 | this.shape.canvas.renderAll(); 90 | } 91 | } 92 | } 93 | 94 | export const connector = (start, end, options) => new Connector(start, end, options); 95 | 96 | export default Connector; 97 | -------------------------------------------------------------------------------- /src/layer/Group.js: -------------------------------------------------------------------------------- 1 | import { Point } from '../geometry/Point'; 2 | 3 | export class Group extends fabric.Group { 4 | constructor(objects, options) { 5 | options = options || {}; 6 | super(objects, options); 7 | } 8 | 9 | getBounds() { 10 | const coords = []; 11 | coords.push(new Point(this.left - this.width / 2.0, this.top - this.height / 2.0)); 12 | coords.push(new Point(this.left + this.width / 2.0, this.top + this.height / 2.0)); 13 | return coords; 14 | } 15 | } 16 | 17 | export const group = (objects, options) => new Group(objects, options); 18 | 19 | export default Group; 20 | -------------------------------------------------------------------------------- /src/layer/Layer.js: -------------------------------------------------------------------------------- 1 | import Base from '../core/Base'; 2 | 3 | export class Layer extends Base { 4 | constructor(options) { 5 | super(options); 6 | this.label = this.label !== undefined ? this.label : null; 7 | this.draggable = this.draggable || false; 8 | this.zIndex = this.zIndex || 1; 9 | this.opacity = this.opacity || 1; 10 | this.keepOnZoom = this.keepOnZoom || false; 11 | this.clickable = this.clickable || false; 12 | 13 | this.hoverCursor = this.hoverCursor || this.clickable ? 'pointer' : 'default'; 14 | this.moveCursor = this.moveCursor || 'move'; 15 | 16 | // this.class = this.class || this.constructor.name.toLowerCase(); 17 | 18 | this.style = { 19 | zIndex: this.zIndex, 20 | class: this.class, 21 | parent: this, 22 | keepOnZoom: this.keepOnZoom, 23 | id: this.id, 24 | hasControls: false, 25 | hasBorders: false, 26 | lockMovementX: !this.draggable, 27 | lockMovementY: !this.draggable, 28 | draggable: this.draggable, 29 | clickable: this.clickable, 30 | evented: this.clickable, 31 | selectable: this.draggable, 32 | hoverCursor: this.hoverCursor, 33 | moveCursor: this.moveCursor 34 | }; 35 | } 36 | 37 | setOptions(options) { 38 | if (!this.shape) return; 39 | Object.keys(options).forEach(key => { 40 | this.shape.set(key, options[key]); 41 | }); 42 | if (this.shape.canvas) { 43 | this.shape.canvas.renderAll(); 44 | } 45 | } 46 | 47 | addTo(map) { 48 | if (!map) { 49 | if (this._map) { 50 | this._map.removeLayer(this); 51 | } 52 | return; 53 | } 54 | this._map = map; 55 | this._map.addLayer(this); 56 | } 57 | } 58 | 59 | export const layer = options => new Layer(options); 60 | 61 | export default Layer; 62 | -------------------------------------------------------------------------------- /src/layer/Tooltip.js: -------------------------------------------------------------------------------- 1 | import { Layer } from './Layer'; 2 | import { Group } from './Group'; 3 | import { Point } from '../geometry/Point'; 4 | 5 | class Tooltip extends Layer { 6 | constructor(position, options) { 7 | options = options || {}; 8 | options.zIndex = options.zIndex || 300; 9 | options.keepOnZoom = true; 10 | options.position = new Point(position); 11 | options.class = 'tooltip'; 12 | super(options); 13 | 14 | this.content = this.content || ''; 15 | this.size = this.size || 10; 16 | this.textColor = this.textColor || 'black'; 17 | this.fill = this.fill || 'white'; 18 | this.stroke = this.stroke || 'red'; 19 | 20 | Object.assign(this.style, { 21 | left: this.position.x, 22 | top: this.position.y 23 | }); 24 | 25 | if (this.content) { 26 | this.textObj = new fabric.Text(this.content, { 27 | fontSize: this.size, 28 | fill: this.textColor 29 | }); 30 | } 31 | this.init(); 32 | } 33 | 34 | init() { 35 | const objects = []; 36 | if (this.textObj) { 37 | objects.push(this.textObj); 38 | } 39 | this.shape = new Group(objects, this.style); 40 | process.nextTick(() => { 41 | this.emit('ready'); 42 | }); 43 | } 44 | } 45 | export default Tooltip; 46 | -------------------------------------------------------------------------------- /src/layer/index.js: -------------------------------------------------------------------------------- 1 | export * from './Layer.js'; 2 | 3 | export * from './Connector.js'; 4 | 5 | export * from './Group.js'; 6 | 7 | export * from './marker/index'; 8 | 9 | export * from './vector/index'; 10 | -------------------------------------------------------------------------------- /src/layer/marker/Icon.js: -------------------------------------------------------------------------------- 1 | import { ICON } from '../../core/Constants'; 2 | 3 | export class Icon extends fabric.Image { 4 | constructor(options) { 5 | super(options); 6 | this.defaults = Object.assign({}, ICON); 7 | Object.assign({}, this.defaults); 8 | Object.assign({}, this._options); 9 | } 10 | } 11 | export const icon = (options) => new Icon(options); 12 | -------------------------------------------------------------------------------- /src/layer/marker/Marker.js: -------------------------------------------------------------------------------- 1 | import { Layer } from '../Layer'; 2 | import { Group } from '../Group'; 3 | import { Point } from '../../geometry/Point'; 4 | import { Connector } from '../Connector'; 5 | 6 | export class Marker extends Layer { 7 | constructor(position, options) { 8 | options = options || {}; 9 | options.zIndex = options.zIndex || 100; 10 | options.keepOnZoom = options.keepOnZoom === undefined ? true : options.keepOnZoom; 11 | options.position = new Point(position); 12 | options.rotation = options.rotation || 0; 13 | options.yaw = options.yaw || 0; 14 | options.clickable = options.clickable !== undefined ? options.clickable : true; 15 | options.class = 'marker'; 16 | super(options); 17 | 18 | const vm = this; 19 | 20 | this.text = this.text || ''; 21 | this.size = this.size || 10; 22 | this.textColor = this.textColor || 'black'; 23 | this.fill = this.fill || 'white'; 24 | this.stroke = this.stroke || 'red'; 25 | 26 | Object.assign(this.style, { 27 | left: this.position.x, 28 | top: this.position.y, 29 | // selectionBackgroundColor: false, 30 | angle: this.rotation, 31 | yaw: this.yaw, 32 | clickable: this.clickable 33 | }); 34 | 35 | if (this.text) { 36 | this.textObj = new fabric.Text(this.text, { 37 | fontSize: this.size, 38 | fill: this.textColor 39 | }); 40 | } 41 | 42 | if (this.icon) { 43 | fabric.Image.fromURL( 44 | this.icon.url, 45 | image => { 46 | vm.image = image.scaleToWidth(this.size); 47 | this.init(); 48 | // vm.shape.removeWithUpdate(); 49 | }, 50 | { 51 | selectable: false, 52 | evented: this.evented, 53 | clickable: this.clickable, 54 | opacity: this.opacity 55 | } 56 | ); 57 | } else { 58 | this.circle = new fabric.Circle({ 59 | radius: this.size, 60 | strokeWidth: 2, 61 | stroke: this.stroke, 62 | fill: this.fill 63 | }); 64 | this.init(); 65 | } 66 | } 67 | 68 | init() { 69 | const objects = []; 70 | if (this.image) { 71 | objects.push(this.image); 72 | } 73 | if (this.circle) { 74 | objects.push(this.circle); 75 | } 76 | if (this.textObj) { 77 | objects.push(this.textObj); 78 | } 79 | this.shape = new Group(objects, this.style); 80 | this.links = this.links || []; 81 | this.addLinks(); 82 | this.registerListeners(); 83 | 84 | process.nextTick(() => { 85 | this.emit('ready'); 86 | }); 87 | } 88 | 89 | registerListeners() { 90 | const vm = this; 91 | this.shape.on('moving', () => { 92 | vm.onShapeDrag(); 93 | }); 94 | this.shape.on('rotating', () => { 95 | vm.emit('rotating'); 96 | }); 97 | 98 | this.shape.on('mousedown', e => { 99 | vm.onShapeMouseDown(e); 100 | }); 101 | this.shape.on('mousemove', e => { 102 | vm.onShapeMouseMove(e); 103 | }); 104 | this.shape.on('mouseup', e => { 105 | vm.onShapeMouseUp(e); 106 | }); 107 | this.shape.on('mouseover', () => { 108 | vm.emit('mouseover', vm); 109 | }); 110 | this.shape.on('mouseout', () => { 111 | vm.emit('mouseout', vm); 112 | }); 113 | } 114 | 115 | setPosition(position) { 116 | this.position = new Point(position); 117 | if (!this.shape) return; 118 | 119 | this.shape.set({ 120 | left: this.position.x, 121 | top: this.position.y 122 | }); 123 | 124 | this.emit('update:links'); 125 | 126 | if (this.shape.canvas) { 127 | this.shape.canvas.renderAll(); 128 | } 129 | } 130 | 131 | setRotation(rotation) { 132 | this.rotation = rotation; 133 | 134 | if (!this.shape) return; 135 | 136 | this.shape.set({ 137 | angle: this.rotation 138 | }); 139 | 140 | if (this.shape.canvas) { 141 | this.shape.canvas.renderAll(); 142 | } 143 | } 144 | 145 | setOptions(options) { 146 | if (!this.shape) return; 147 | 148 | Object.keys(options).forEach(key => { 149 | switch (key) { 150 | case 'textColor': 151 | this.setTextColor(options[key]); 152 | break; 153 | case 'stroke': 154 | this.setStroke(options[key]); 155 | break; 156 | case 'fill': 157 | this.setColor(options[key]); 158 | break; 159 | 160 | default: 161 | break; 162 | } 163 | }); 164 | if (this.shape.canvas) { 165 | this.shape.canvas.renderAll(); 166 | } 167 | } 168 | 169 | setTextColor(color) { 170 | if (this.text && this.textObj) { 171 | this.textObj.setColor(color); 172 | this.textObj.canvas.renderAll(); 173 | } 174 | } 175 | 176 | setText(text) { 177 | if (this.text && this.textObj) { 178 | this.textObj.set({ text }); 179 | this.textObj.canvas.renderAll(); 180 | } 181 | } 182 | 183 | setStroke(color) { 184 | if (this.circle) { 185 | this.circle.set('stroke', color); 186 | } 187 | } 188 | 189 | setColor(color) { 190 | if (this.circle) { 191 | this.circle.setColor(color); 192 | } 193 | } 194 | 195 | setLinks(links) { 196 | this.links = links; 197 | this.addLinks(); 198 | } 199 | 200 | setSize(size) { 201 | if (this.image) { 202 | this.image.scaleToWidth(size); 203 | if (this.image.canvas) { 204 | this.image.canvas.renderAll(); 205 | } 206 | } else if (this.circle) { 207 | this.circle.setRadius(size); 208 | } 209 | } 210 | 211 | addLinks() { 212 | this.connectors = []; 213 | this.links.forEach(link => { 214 | const connector = new Connector(this, link); 215 | this.connectors.push(connector); 216 | }); 217 | 218 | this.addConnectors(); 219 | } 220 | 221 | addConnectors() { 222 | const vm = this; 223 | this.connectors.forEach(connector => { 224 | vm._map.addLayer(connector); 225 | }); 226 | } 227 | 228 | onAdded() { 229 | this.addConnectors(); 230 | } 231 | 232 | onShapeDrag() { 233 | const matrix = this.shape.calcTransformMatrix(); 234 | const [, , , , x, y] = matrix; 235 | this.position = new Point(x, y); 236 | this.emit('update:links'); 237 | this.emit('moving'); 238 | } 239 | 240 | onShapeMouseDown(e) { 241 | this.dragStart = e; 242 | } 243 | 244 | onShapeMouseMove(e) { 245 | if (this.dragStart) { 246 | this.emit('dragstart'); 247 | 248 | const a = new fabric.Point(e.pointer.x, e.pointer.y); 249 | const b = new fabric.Point(this.dragStart.pointer.x, this.dragStart.pointer.y); 250 | // if distance is far enough, we don't want to fire click event 251 | if (a.distanceFrom(b) > 3) { 252 | this.dragStart = null; 253 | this.dragging = true; 254 | } else { 255 | // this.dragging = false; 256 | } 257 | } 258 | 259 | if (this.dragging) { 260 | this.emit('drag'); 261 | } else { 262 | this.emit('hover'); 263 | } 264 | } 265 | 266 | onShapeMouseUp() { 267 | if (!this.dragging) { 268 | this.emit('click'); 269 | } else { 270 | this.emit('moved'); 271 | } 272 | this.dragStart = null; 273 | this.dragging = false; 274 | } 275 | } 276 | 277 | export const marker = (position, options) => new Marker(position, options); 278 | -------------------------------------------------------------------------------- /src/layer/marker/MarkerGroup.js: -------------------------------------------------------------------------------- 1 | import { Rect } from '../vector'; 2 | import { Layer } from '../Layer'; 3 | 4 | export class MarkerGroup extends Layer { 5 | constructor(bounds, options) { 6 | options = options || {}; 7 | options.bounds = bounds; 8 | options.zIndex = options.zIndex || 50; 9 | options.class = 'markergroup'; 10 | super(options); 11 | if (!this.bounds) { 12 | console.error('bounds is missing!'); 13 | return; 14 | } 15 | this.style = { 16 | strokeWidth: 1, 17 | stroke: this.stroke || 'black', 18 | fill: this.color || '#88888822', 19 | class: this.class, 20 | zIndex: this.zIndex, 21 | parent: this 22 | }; 23 | this.draw(); 24 | } 25 | 26 | setBounds(bounds) { 27 | this.bounds = bounds; 28 | this.draw(); 29 | } 30 | 31 | draw() { 32 | const width = this.bounds[1][0] - this.bounds[0][0]; 33 | const height = this.bounds[1][1] - this.bounds[0][1]; 34 | this.coords = { 35 | left: this.bounds[0][0] + width / 2, 36 | top: this.bounds[0][1] + height / 2, 37 | width, 38 | height 39 | }; 40 | 41 | if (this.shape) { 42 | this.shape.set(this.coords); 43 | } else { 44 | Object.assign(this.style, this.coords); 45 | this.shape = new Rect(this.style); 46 | } 47 | } 48 | } 49 | export const markerGroup = (bounds, options) => new MarkerGroup(bounds, options); 50 | export default MarkerGroup; 51 | -------------------------------------------------------------------------------- /src/layer/marker/index.js: -------------------------------------------------------------------------------- 1 | export * from './Marker.js'; 2 | 3 | export * from './Icon.js'; 4 | 5 | export * from './MarkerGroup.js'; 6 | -------------------------------------------------------------------------------- /src/layer/vector/Circle.js: -------------------------------------------------------------------------------- 1 | export class Circle extends fabric.Circle {} 2 | 3 | export const circle = options => new Circle(options); 4 | -------------------------------------------------------------------------------- /src/layer/vector/Line.js: -------------------------------------------------------------------------------- 1 | export class Line extends fabric.Line { 2 | constructor(points, options) { 3 | options = options || {}; 4 | options.strokeWidth = options.strokeWidth || 1; 5 | options.class = 'line'; 6 | super(points, options); 7 | this._strokeWidth = options.strokeWidth; 8 | } 9 | 10 | _renderStroke(ctx) { 11 | const stroke = this._strokeWidth / this.canvas.getZoom(); 12 | this.strokeWidth = stroke > 0.01 ? stroke : 0.01; 13 | super._renderStroke(ctx); 14 | this.setCoords(); 15 | } 16 | } 17 | 18 | export const line = (points, options) => new Line(points, options); 19 | 20 | export default Line; 21 | -------------------------------------------------------------------------------- /src/layer/vector/Polyline.js: -------------------------------------------------------------------------------- 1 | import { Layer } from '../Layer'; 2 | import { Point } from '../../geometry/Point'; 3 | import { Group } from '../Group'; 4 | 5 | export class Polyline extends Layer { 6 | constructor(_points, options) { 7 | options = options || {}; 8 | options.points = _points || []; 9 | super(options); 10 | this.lines = []; 11 | this.class = 'polyline'; 12 | this.strokeWidth = 1; 13 | 14 | this.lineOptions = { 15 | strokeWidth: this.strokeWidth, 16 | stroke: this.color || 'grey', 17 | fill: this.fill || false 18 | }; 19 | 20 | this.shape = new Group([], { 21 | selectable: false, 22 | hasControls: false, 23 | class: this.class, 24 | parent: this 25 | }); 26 | 27 | this.setPoints(this._points); 28 | } 29 | 30 | addPoint(point) { 31 | this.points.push(new Point(point)); 32 | 33 | if (this.points.length > 1) { 34 | const i = this.points.length - 1; 35 | const j = this.points.length - 2; 36 | const p1 = this.points[i]; 37 | const p2 = this.points[j]; 38 | const line = new fabric.Line(p1.getArray().concat(p2.getArray()), this.lineOptions); 39 | this.lines.push(line); 40 | this.shape.addWithUpdate(line); 41 | } 42 | } 43 | 44 | setStrokeWidth(strokeWidth) { 45 | this.lines.forEach(line => { 46 | line.setStrokeWidth(strokeWidth); 47 | }); 48 | } 49 | 50 | setPoints(points = []) { 51 | this.removeLines(); 52 | this.points = []; 53 | for (let i = 0; i < points.length; i += 1) { 54 | const point = new Point(points[i]); 55 | this.points.push(point); 56 | this.addPoint(); 57 | } 58 | } 59 | 60 | removeLines() { 61 | for (let i = 0; i < this.lines.length; i += 1) { 62 | this.shape.remove(this.lines[i]); 63 | } 64 | this.lines = []; 65 | } 66 | } 67 | 68 | export const polyline = (points, options) => new Polyline(points, options); 69 | -------------------------------------------------------------------------------- /src/layer/vector/Rect.js: -------------------------------------------------------------------------------- 1 | export class Rect extends fabric.Rect { 2 | constructor(points, options) { 3 | options = options || {}; 4 | options.strokeWidth = options.strokeWidth || 1; 5 | options.class = 'rect'; 6 | super(points, options); 7 | this._strokeWidth = options.strokeWidth; 8 | } 9 | 10 | _renderStroke(ctx) { 11 | this.strokeWidth = this._strokeWidth / this.canvas.getZoom(); 12 | super._renderStroke(ctx); 13 | } 14 | } 15 | 16 | export const rect = (points, options) => new Rect(points, options); 17 | 18 | export default Rect; 19 | -------------------------------------------------------------------------------- /src/layer/vector/index.js: -------------------------------------------------------------------------------- 1 | export * from './Polyline.js'; 2 | 3 | export * from './Circle.js'; 4 | 5 | export * from './Line.js'; 6 | 7 | export * from './Rect.js'; 8 | -------------------------------------------------------------------------------- /src/lib/MagicScroll.js: -------------------------------------------------------------------------------- 1 | class MagicScroll { 2 | constructor(target, speed = 80, smooth = 12, current = 0, passive = false) { 3 | if (target === document) { 4 | target = document.scrollingElement 5 | || document.documentElement 6 | || document.body.parentNode 7 | || document.body; 8 | } // cross browser support for document scrolling 9 | 10 | this.speed = speed; 11 | this.smooth = smooth; 12 | this.moving = false; 13 | this.scrollTop = current * 3000; 14 | this.pos = this.scrollTop; 15 | this.frame = target === document.body && document.documentElement ? document.documentElement : target; // safari is the new IE 16 | 17 | target.addEventListener('wheel', scrolled, { passive }); 18 | target.addEventListener('DOMMouseScroll', scrolled, { passive }); 19 | const scope = this; 20 | function scrolled(e) { 21 | e.preventDefault(); // disable default scrolling 22 | 23 | const delta = scope.normalizeWheelDelta(e); 24 | 25 | scope.pos += -delta * scope.speed; 26 | // scope.pos = Math.max(0, Math.min(scope.pos, 3000)); // limit scrolling 27 | 28 | if (!scope.moving) scope.update(e); 29 | } 30 | } 31 | 32 | normalizeWheelDelta(e) { 33 | if (e.detail) { 34 | if (e.wheelDelta) return (e.wheelDelta / e.detail / 40) * (e.detail > 0 ? 1 : -1); 35 | // Opera 36 | return -e.detail / 3; // Firefox 37 | } 38 | return e.wheelDelta / 120; // IE,Safari,Chrome 39 | } 40 | 41 | update(e) { 42 | this.moving = true; 43 | 44 | const delta = (this.pos - this.scrollTop) / this.smooth; 45 | 46 | this.scrollTop += delta; 47 | 48 | // this.scrollTop = Math.round(this.scrollTop); 49 | 50 | if (this.onUpdate) { 51 | this.onUpdate(delta, e); 52 | } 53 | const scope = this; 54 | if (Math.abs(delta) > 1) { 55 | requestFrame(() => { 56 | scope.update(); 57 | }); 58 | } else this.moving = false; 59 | } 60 | } 61 | 62 | export default MagicScroll; 63 | 64 | var requestFrame = (function () { 65 | // requestAnimationFrame cross browser 66 | return ( 67 | window.requestAnimationFrame 68 | || window.webkitRequestAnimationFrame 69 | || window.mozRequestAnimationFrame 70 | || window.oRequestAnimationFrame 71 | || window.msRequestAnimationFrame 72 | || function (func) { 73 | window.setTimeout(func, 1000); 74 | } 75 | ); 76 | }()); 77 | -------------------------------------------------------------------------------- /src/lib/color-alpha.js: -------------------------------------------------------------------------------- 1 | export default alpha; 2 | 3 | function alpha (color, value) { 4 | let obj = color.replace(/[^\d,]/g, '').split(','); 5 | if (value == null) value = obj[3] || 1; 6 | obj[3] = value; 7 | return 'rgba('+obj.join(',')+')'; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/ev-pos.js: -------------------------------------------------------------------------------- 1 | const isNum = function (val) { 2 | return typeof val === 'number' && !isNaN(val); 3 | }; 4 | 5 | export default (ev, toElement) => { 6 | toElement = toElement || ev.currentTarget; 7 | 8 | const toElementBoundingRect = toElement.getBoundingClientRect(); 9 | const orgEv = ev.originalEvent || ev; 10 | const hasTouches = ev.touches && ev.touches.length; 11 | let pageX = 0; 12 | let pageY = 0; 13 | 14 | if (hasTouches) { 15 | if (isNum(ev.touches[0].pageX) && isNum(ev.touches[0].pageY)) { 16 | pageX = ev.touches[0].pageX; 17 | pageY = ev.touches[0].pageY; 18 | } else if (isNum(ev.touches[0].clientX) && isNum(ev.touches[0].clientY)) { 19 | pageX = orgEv.touches[0].clientX; 20 | pageY = orgEv.touches[0].clientY; 21 | } 22 | } else if (isNum(ev.pageX) && isNum(ev.pageY)) { 23 | pageX = ev.pageX; 24 | pageY = ev.pageY; 25 | } else if (ev.currentPoint && isNum(ev.currentPoint.x) && isNum(ev.currentPoint.y)) { 26 | pageX = ev.currentPoint.x; 27 | pageY = ev.currentPoint.y; 28 | } 29 | let isRight = false; 30 | if ('which' in ev) { 31 | // Gecko (Firefox), WebKit (Safari/Chrome) & Opera 32 | isRight = ev.which == 3; 33 | } else if ('button' in ev) { 34 | // IE, Opera 35 | isRight = ev.button == 2; 36 | } 37 | 38 | return { 39 | x: pageX - toElementBoundingRect.left, 40 | y: pageY - toElementBoundingRect.top, 41 | isRight 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/lib/impetus.js: -------------------------------------------------------------------------------- 1 | const stopThresholdDefault = 0.3; 2 | const bounceDeceleration = 0.04; 3 | const bounceAcceleration = 0.11; 4 | 5 | // fixes weird safari 10 bug where preventDefault is prevented 6 | // @see https://github.com/metafizzy/flickity/issues/457#issuecomment-254501356 7 | window.addEventListener('touchmove', () => {}); 8 | 9 | export default class Impetus { 10 | constructor({ 11 | source: sourceEl = document, 12 | update: updateCallback, 13 | stop: stopCallback, 14 | multiplier = 1, 15 | friction = 0.92, 16 | initialValues, 17 | boundX, 18 | boundY, 19 | bounce = true 20 | }) { 21 | let boundXmin; 22 | let boundXmax; 23 | let boundYmin; 24 | let boundYmax; 25 | let pointerLastX; 26 | let pointerLastY; 27 | let pointerCurrentX; 28 | let pointerCurrentY; 29 | let pointerId; 30 | let decVelX; 31 | let decVelY; 32 | let targetX = 0; 33 | let targetY = 0; 34 | let stopThreshold = stopThresholdDefault * multiplier; 35 | let ticking = false; 36 | let pointerActive = false; 37 | let paused = false; 38 | let decelerating = false; 39 | let trackingPoints = []; 40 | 41 | /** 42 | * Initialize instance 43 | */ 44 | (function init() { 45 | sourceEl = typeof sourceEl === 'string' ? document.querySelector(sourceEl) : sourceEl; 46 | if (!sourceEl) { 47 | throw new Error('IMPETUS: source not found.'); 48 | } 49 | 50 | if (!updateCallback) { 51 | throw new Error('IMPETUS: update function not defined.'); 52 | } 53 | 54 | if (initialValues) { 55 | if (initialValues[0]) { 56 | targetX = initialValues[0]; 57 | } 58 | if (initialValues[1]) { 59 | targetY = initialValues[1]; 60 | } 61 | callUpdateCallback(); 62 | } 63 | 64 | // Initialize bound values 65 | if (boundX) { 66 | boundXmin = boundX[0]; 67 | boundXmax = boundX[1]; 68 | } 69 | if (boundY) { 70 | boundYmin = boundY[0]; 71 | boundYmax = boundY[1]; 72 | } 73 | 74 | sourceEl.addEventListener('touchstart', onDown); 75 | sourceEl.addEventListener('mousedown', onDown); 76 | }()); 77 | 78 | /** 79 | * In edge cases where you may need to 80 | * reinstanciate Impetus on the same sourceEl 81 | * this will remove the previous event listeners 82 | */ 83 | this.destroy = function () { 84 | sourceEl.removeEventListener('touchstart', onDown); 85 | sourceEl.removeEventListener('mousedown', onDown); 86 | 87 | cleanUpRuntimeEvents(); 88 | 89 | // however it won't "destroy" a reference 90 | // to instance if you'd like to do that 91 | // it returns null as a convinience. 92 | // ex: `instance = instance.destroy();` 93 | return null; 94 | }; 95 | 96 | /** 97 | * Disable movement processing 98 | * @public 99 | */ 100 | this.pause = function () { 101 | cleanUpRuntimeEvents(); 102 | 103 | pointerActive = false; 104 | paused = true; 105 | }; 106 | 107 | /** 108 | * Enable movement processing 109 | * @public 110 | */ 111 | this.resume = function () { 112 | paused = false; 113 | }; 114 | 115 | /** 116 | * Update the current x and y values 117 | * @public 118 | * @param {Number} x 119 | * @param {Number} y 120 | */ 121 | this.setValues = function (x, y) { 122 | if (typeof x === 'number') { 123 | targetX = x; 124 | } 125 | if (typeof y === 'number') { 126 | targetY = y; 127 | } 128 | }; 129 | 130 | /** 131 | * Update the multiplier value 132 | * @public 133 | * @param {Number} val 134 | */ 135 | this.setMultiplier = function (val) { 136 | multiplier = val; 137 | stopThreshold = stopThresholdDefault * multiplier; 138 | }; 139 | 140 | /** 141 | * Update boundX value 142 | * @public 143 | * @param {Number[]} boundX 144 | */ 145 | this.setBoundX = function (boundX) { 146 | boundXmin = boundX[0]; 147 | boundXmax = boundX[1]; 148 | }; 149 | 150 | /** 151 | * Update boundY value 152 | * @public 153 | * @param {Number[]} boundY 154 | */ 155 | this.setBoundY = function (boundY) { 156 | boundYmin = boundY[0]; 157 | boundYmax = boundY[1]; 158 | }; 159 | 160 | /** 161 | * Removes all events set by this instance during runtime 162 | */ 163 | function cleanUpRuntimeEvents() { 164 | // Remove all touch events added during 'onDown' as well. 165 | document.removeEventListener( 166 | 'touchmove', 167 | onMove, 168 | getPassiveSupported() ? { passive: false } : false 169 | ); 170 | document.removeEventListener('touchend', onUp); 171 | document.removeEventListener('touchcancel', stopTracking); 172 | document.removeEventListener( 173 | 'mousemove', 174 | onMove, 175 | getPassiveSupported() ? { passive: false } : false 176 | ); 177 | document.removeEventListener('mouseup', onUp); 178 | } 179 | 180 | /** 181 | * Add all required runtime events 182 | */ 183 | function addRuntimeEvents() { 184 | cleanUpRuntimeEvents(); 185 | 186 | // @see https://developers.google.com/web/updates/2017/01/scrolling-intervention 187 | document.addEventListener( 188 | 'touchmove', 189 | onMove, 190 | getPassiveSupported() ? { passive: false } : false 191 | ); 192 | document.addEventListener('touchend', onUp); 193 | document.addEventListener('touchcancel', stopTracking); 194 | document.addEventListener( 195 | 'mousemove', 196 | onMove, 197 | getPassiveSupported() ? { passive: false } : false 198 | ); 199 | document.addEventListener('mouseup', onUp); 200 | } 201 | 202 | /** 203 | * Executes the update function 204 | */ 205 | function callUpdateCallback() { 206 | updateCallback.call(sourceEl, targetX, targetY); 207 | } 208 | 209 | /** 210 | * Creates a custom normalized event object from touch and mouse events 211 | * @param {Event} ev 212 | * @returns {Object} with x, y, and id properties 213 | */ 214 | function normalizeEvent(ev) { 215 | if (ev.type === 'touchmove' || ev.type === 'touchstart' || ev.type === 'touchend') { 216 | const touch = ev.targetTouches[0] || ev.changedTouches[0]; 217 | return { 218 | x: touch.clientX, 219 | y: touch.clientY, 220 | id: touch.identifier 221 | }; 222 | } 223 | // mouse events 224 | return { 225 | x: ev.clientX, 226 | y: ev.clientY, 227 | id: null 228 | }; 229 | } 230 | 231 | /** 232 | * Initializes movement tracking 233 | * @param {Object} ev Normalized event 234 | */ 235 | function onDown(ev) { 236 | const event = normalizeEvent(ev); 237 | if (!pointerActive && !paused) { 238 | pointerActive = true; 239 | decelerating = false; 240 | pointerId = event.id; 241 | 242 | pointerLastX = pointerCurrentX = event.x; 243 | pointerLastY = pointerCurrentY = event.y; 244 | trackingPoints = []; 245 | addTrackingPoint(pointerLastX, pointerLastY); 246 | 247 | addRuntimeEvents(); 248 | } 249 | } 250 | 251 | /** 252 | * Handles move events 253 | * @param {Object} ev Normalized event 254 | */ 255 | function onMove(ev) { 256 | ev.preventDefault(); 257 | const event = normalizeEvent(ev); 258 | 259 | if (pointerActive && event.id === pointerId) { 260 | pointerCurrentX = event.x; 261 | pointerCurrentY = event.y; 262 | addTrackingPoint(pointerLastX, pointerLastY); 263 | requestTick(); 264 | } 265 | } 266 | 267 | /** 268 | * Handles up/end events 269 | * @param {Object} ev Normalized event 270 | */ 271 | function onUp(ev) { 272 | const event = normalizeEvent(ev); 273 | 274 | if (pointerActive && event.id === pointerId) { 275 | stopTracking(); 276 | } 277 | } 278 | 279 | /** 280 | * Stops movement tracking, starts animation 281 | */ 282 | function stopTracking() { 283 | pointerActive = false; 284 | addTrackingPoint(pointerLastX, pointerLastY); 285 | startDecelAnim(); 286 | 287 | cleanUpRuntimeEvents(); 288 | } 289 | 290 | /** 291 | * Records movement for the last 100ms 292 | * @param {number} x 293 | * @param {number} y [description] 294 | */ 295 | function addTrackingPoint(x, y) { 296 | const time = Date.now(); 297 | while (trackingPoints.length > 0) { 298 | if (time - trackingPoints[0].time <= 100) { 299 | break; 300 | } 301 | trackingPoints.shift(); 302 | } 303 | 304 | trackingPoints.push({ x, y, time }); 305 | } 306 | 307 | /** 308 | * Calculate new values, call update function 309 | */ 310 | function updateAndRender() { 311 | const pointerChangeX = pointerCurrentX - pointerLastX; 312 | const pointerChangeY = pointerCurrentY - pointerLastY; 313 | 314 | targetX += pointerChangeX * multiplier; 315 | targetY += pointerChangeY * multiplier; 316 | 317 | if (bounce) { 318 | const diff = checkBounds(); 319 | if (diff.x !== 0) { 320 | targetX -= pointerChangeX * dragOutOfBoundsMultiplier(diff.x) * multiplier; 321 | } 322 | if (diff.y !== 0) { 323 | targetY -= pointerChangeY * dragOutOfBoundsMultiplier(diff.y) * multiplier; 324 | } 325 | } else { 326 | checkBounds(true); 327 | } 328 | 329 | callUpdateCallback(); 330 | 331 | pointerLastX = pointerCurrentX; 332 | pointerLastY = pointerCurrentY; 333 | ticking = false; 334 | } 335 | 336 | /** 337 | * Returns a value from around 0.5 to 1, based on distance 338 | * @param {Number} val 339 | */ 340 | function dragOutOfBoundsMultiplier(val) { 341 | return 0.000005 * Math.pow(val, 2) + 0.0001 * val + 0.55; 342 | } 343 | 344 | /** 345 | * prevents animating faster than current framerate 346 | */ 347 | function requestTick() { 348 | if (!ticking) { 349 | requestAnimFrame(updateAndRender); 350 | } 351 | ticking = true; 352 | } 353 | 354 | /** 355 | * Determine position relative to bounds 356 | * @param {Boolean} restrict Whether to restrict target to bounds 357 | */ 358 | function checkBounds(restrict) { 359 | let xDiff = 0; 360 | let yDiff = 0; 361 | 362 | if (boundXmin !== undefined && targetX < boundXmin) { 363 | xDiff = boundXmin - targetX; 364 | } else if (boundXmax !== undefined && targetX > boundXmax) { 365 | xDiff = boundXmax - targetX; 366 | } 367 | 368 | if (boundYmin !== undefined && targetY < boundYmin) { 369 | yDiff = boundYmin - targetY; 370 | } else if (boundYmax !== undefined && targetY > boundYmax) { 371 | yDiff = boundYmax - targetY; 372 | } 373 | 374 | if (restrict) { 375 | if (xDiff !== 0) { 376 | targetX = xDiff > 0 ? boundXmin : boundXmax; 377 | } 378 | if (yDiff !== 0) { 379 | targetY = yDiff > 0 ? boundYmin : boundYmax; 380 | } 381 | } 382 | 383 | return { 384 | x: xDiff, 385 | y: yDiff, 386 | inBounds: xDiff === 0 && yDiff === 0 387 | }; 388 | } 389 | 390 | /** 391 | * Initialize animation of values coming to a stop 392 | */ 393 | function startDecelAnim() { 394 | const firstPoint = trackingPoints[0]; 395 | const lastPoint = trackingPoints[trackingPoints.length - 1]; 396 | 397 | const xOffset = lastPoint.x - firstPoint.x; 398 | const yOffset = lastPoint.y - firstPoint.y; 399 | const timeOffset = lastPoint.time - firstPoint.time; 400 | 401 | const D = timeOffset / 15 / multiplier; 402 | 403 | decVelX = xOffset / D || 0; // prevent NaN 404 | decVelY = yOffset / D || 0; 405 | 406 | const diff = checkBounds(); 407 | 408 | if (Math.abs(decVelX) > 1 || Math.abs(decVelY) > 1 || !diff.inBounds) { 409 | decelerating = true; 410 | requestAnimFrame(stepDecelAnim); 411 | } else if (stopCallback) { 412 | stopCallback(sourceEl); 413 | } 414 | } 415 | 416 | /** 417 | * Animates values slowing down 418 | */ 419 | function stepDecelAnim() { 420 | if (!decelerating) { 421 | return; 422 | } 423 | 424 | decVelX *= friction; 425 | decVelY *= friction; 426 | 427 | targetX += decVelX; 428 | targetY += decVelY; 429 | 430 | const diff = checkBounds(); 431 | 432 | if ( 433 | Math.abs(decVelX) > stopThreshold 434 | || Math.abs(decVelY) > stopThreshold 435 | || !diff.inBounds 436 | ) { 437 | if (bounce) { 438 | const reboundAdjust = 2.5; 439 | 440 | if (diff.x !== 0) { 441 | if (diff.x * decVelX <= 0) { 442 | decVelX += diff.x * bounceDeceleration; 443 | } else { 444 | const adjust = diff.x > 0 ? reboundAdjust : -reboundAdjust; 445 | decVelX = (diff.x + adjust) * bounceAcceleration; 446 | } 447 | } 448 | if (diff.y !== 0) { 449 | if (diff.y * decVelY <= 0) { 450 | decVelY += diff.y * bounceDeceleration; 451 | } else { 452 | const adjust = diff.y > 0 ? reboundAdjust : -reboundAdjust; 453 | decVelY = (diff.y + adjust) * bounceAcceleration; 454 | } 455 | } 456 | } else { 457 | if (diff.x !== 0) { 458 | if (diff.x > 0) { 459 | targetX = boundXmin; 460 | } else { 461 | targetX = boundXmax; 462 | } 463 | decVelX = 0; 464 | } 465 | if (diff.y !== 0) { 466 | if (diff.y > 0) { 467 | targetY = boundYmin; 468 | } else { 469 | targetY = boundYmax; 470 | } 471 | decVelY = 0; 472 | } 473 | } 474 | 475 | callUpdateCallback(); 476 | 477 | requestAnimFrame(stepDecelAnim); 478 | } else { 479 | decelerating = false; 480 | if (stopCallback) { 481 | stopCallback(sourceEl); 482 | } 483 | } 484 | } 485 | } 486 | } 487 | 488 | /** 489 | * @see http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ 490 | */ 491 | const requestAnimFrame = (function () { 492 | return ( 493 | window.requestAnimationFrame 494 | || window.webkitRequestAnimationFrame 495 | || window.mozRequestAnimationFrame 496 | || function (callback) { 497 | window.setTimeout(callback, 1000 / 60); 498 | } 499 | ); 500 | }()); 501 | 502 | function getPassiveSupported() { 503 | let passiveSupported = false; 504 | 505 | try { 506 | const options = Object.defineProperty({}, 'passive', { 507 | get() { 508 | passiveSupported = true; 509 | } 510 | }); 511 | 512 | window.addEventListener('test', null, options); 513 | } catch (err) {} 514 | 515 | getPassiveSupported = () => passiveSupported; 516 | return passiveSupported; 517 | } 518 | -------------------------------------------------------------------------------- /src/lib/mix.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // used by apply() and isApplicationOf() 4 | const _appliedMixin = '__mixwith_appliedMixin'; 5 | 6 | /** 7 | * A function that returns a subclass of its argument. 8 | * 9 | * @example 10 | * const M = (superclass) => class extends superclass { 11 | * getMessage() { 12 | * return "Hello"; 13 | * } 14 | * } 15 | * 16 | * @typedef {Function} MixinFunction 17 | * @param {Function} superclass 18 | * @return {Function} A subclass of `superclass` 19 | */ 20 | 21 | /** 22 | * Applies `mixin` to `superclass`. 23 | * 24 | * `apply` stores a reference from the mixin application to the unwrapped mixin 25 | * to make `isApplicationOf` and `hasMixin` work. 26 | * 27 | * This function is usefull for mixin wrappers that want to automatically enable 28 | * {@link hasMixin} support. 29 | * 30 | * @example 31 | * const Applier = (mixin) => wrap(mixin, (superclass) => apply(superclass, mixin)); 32 | * 33 | * // M now works with `hasMixin` and `isApplicationOf` 34 | * const M = Applier((superclass) => class extends superclass {}); 35 | * 36 | * class C extends M(Object) {} 37 | * let i = new C(); 38 | * hasMixin(i, M); // true 39 | * 40 | * @function 41 | * @param {Function} superclass A class or constructor function 42 | * @param {MixinFunction} mixin The mixin to apply 43 | * @return {Function} A subclass of `superclass` produced by `mixin` 44 | */ 45 | export const apply = (superclass, mixin) => { 46 | let application = mixin(superclass); 47 | application.prototype[_appliedMixin] = unwrap(mixin); 48 | return application; 49 | }; 50 | 51 | /** 52 | * Returns `true` iff `proto` is a prototype created by the application of 53 | * `mixin` to a superclass. 54 | * 55 | * `isApplicationOf` works by checking that `proto` has a reference to `mixin` 56 | * as created by `apply`. 57 | * 58 | * @function 59 | * @param {Object} proto A prototype object created by {@link apply}. 60 | * @param {MixinFunction} mixin A mixin function used with {@link apply}. 61 | * @return {boolean} whether `proto` is a prototype created by the application of 62 | * `mixin` to a superclass 63 | */ 64 | export const isApplicationOf = (proto, mixin) => 65 | proto.hasOwnProperty(_appliedMixin) && proto[_appliedMixin] === unwrap(mixin); 66 | 67 | /** 68 | * Returns `true` iff `o` has an application of `mixin` on its prototype 69 | * chain. 70 | * 71 | * @function 72 | * @param {Object} o An object 73 | * @param {MixinFunction} mixin A mixin applied with {@link apply} 74 | * @return {boolean} whether `o` has an application of `mixin` on its prototype 75 | * chain 76 | */ 77 | export const hasMixin = (o, mixin) => { 78 | while (o != null) { 79 | if (isApplicationOf(o, mixin)) return true; 80 | o = Object.getPrototypeOf(o); 81 | } 82 | return false; 83 | } 84 | 85 | 86 | // used by wrap() and unwrap() 87 | const _wrappedMixin = '__mixwith_wrappedMixin'; 88 | 89 | /** 90 | * Sets up the function `mixin` to be wrapped by the function `wrapper`, while 91 | * allowing properties on `mixin` to be available via `wrapper`, and allowing 92 | * `wrapper` to be unwrapped to get to the original function. 93 | * 94 | * `wrap` does two things: 95 | * 1. Sets the prototype of `mixin` to `wrapper` so that properties set on 96 | * `mixin` inherited by `wrapper`. 97 | * 2. Sets a special property on `mixin` that points back to `mixin` so that 98 | * it can be retreived from `wrapper` 99 | * 100 | * @function 101 | * @param {MixinFunction} mixin A mixin function 102 | * @param {MixinFunction} wrapper A function that wraps {@link mixin} 103 | * @return {MixinFunction} `wrapper` 104 | */ 105 | export const wrap = (mixin, wrapper) => { 106 | Object.setPrototypeOf(wrapper, mixin); 107 | if (!mixin[_wrappedMixin]) { 108 | mixin[_wrappedMixin] = mixin; 109 | } 110 | return wrapper; 111 | }; 112 | 113 | /** 114 | * Unwraps the function `wrapper` to return the original function wrapped by 115 | * one or more calls to `wrap`. Returns `wrapper` if it's not a wrapped 116 | * function. 117 | * 118 | * @function 119 | * @param {MixinFunction} wrapper A wrapped mixin produced by {@link wrap} 120 | * @return {MixinFunction} The originally wrapped mixin 121 | */ 122 | export const unwrap = (wrapper) => wrapper[_wrappedMixin] || wrapper; 123 | 124 | const _cachedApplications = '__mixwith_cachedApplications'; 125 | 126 | /** 127 | * Decorates `mixin` so that it caches its applications. When applied multiple 128 | * times to the same superclass, `mixin` will only create one subclass, memoize 129 | * it and return it for each application. 130 | * 131 | * Note: If `mixin` somehow stores properties its classes constructor (static 132 | * properties), or on its classes prototype, it will be shared across all 133 | * applications of `mixin` to a super class. It's reccomended that `mixin` only 134 | * access instance state. 135 | * 136 | * @function 137 | * @param {MixinFunction} mixin The mixin to wrap with caching behavior 138 | * @return {MixinFunction} a new mixin function 139 | */ 140 | export const Cached = (mixin) => wrap(mixin, (superclass) => { 141 | // Get or create a symbol used to look up a previous application of mixin 142 | // to the class. This symbol is unique per mixin definition, so a class will have N 143 | // applicationRefs if it has had N mixins applied to it. A mixin will have 144 | // exactly one _cachedApplicationRef used to store its applications. 145 | 146 | let cachedApplications = superclass[_cachedApplications]; 147 | if (!cachedApplications) { 148 | cachedApplications = superclass[_cachedApplications] = new Map(); 149 | } 150 | 151 | let application = cachedApplications.get(mixin); 152 | if (!application) { 153 | application = mixin(superclass); 154 | cachedApplications.set(mixin, application); 155 | } 156 | 157 | return application; 158 | }); 159 | 160 | /** 161 | * Decorates `mixin` so that it only applies if it's not already on the 162 | * prototype chain. 163 | * 164 | * @function 165 | * @param {MixinFunction} mixin The mixin to wrap with deduplication behavior 166 | * @return {MixinFunction} a new mixin function 167 | */ 168 | export const DeDupe = (mixin) => wrap(mixin, (superclass) => 169 | (hasMixin(superclass.prototype, mixin)) 170 | ? superclass 171 | : mixin(superclass)); 172 | 173 | /** 174 | * Adds [Symbol.hasInstance] (ES2015 custom instanceof support) to `mixin`. 175 | * 176 | * @function 177 | * @param {MixinFunction} mixin The mixin to add [Symbol.hasInstance] to 178 | * @return {MixinFunction} the given mixin function 179 | */ 180 | export const HasInstance = (mixin) => { 181 | if (Symbol && Symbol.hasInstance && !mixin[Symbol.hasInstance]) { 182 | Object.defineProperty(mixin, Symbol.hasInstance, { 183 | value(o) { 184 | return hasMixin(o, mixin); 185 | }, 186 | }); 187 | } 188 | return mixin; 189 | }; 190 | 191 | /** 192 | * A basic mixin decorator that applies the mixin with {@link apply} so that it 193 | * can be used with {@link isApplicationOf}, {@link hasMixin} and the other 194 | * mixin decorator functions. 195 | * 196 | * @function 197 | * @param {MixinFunction} mixin The mixin to wrap 198 | * @return {MixinFunction} a new mixin function 199 | */ 200 | export const BareMixin = (mixin) => wrap(mixin, (s) => apply(s, mixin)); 201 | 202 | /** 203 | * Decorates a mixin function to add deduplication, application caching and 204 | * instanceof support. 205 | * 206 | * @function 207 | * @param {MixinFunction} mixin The mixin to wrap 208 | * @return {MixinFunction} a new mixin function 209 | */ 210 | export const Mixin = (mixin) => DeDupe(Cached(BareMixin(mixin))); 211 | 212 | /** 213 | * A fluent interface to apply a list of mixins to a superclass. 214 | * 215 | * ```javascript 216 | * class X extends mix(Object).with(A, B, C) {} 217 | * ``` 218 | * 219 | * The mixins are applied in order to the superclass, so the prototype chain 220 | * will be: X->C'->B'->A'->Object. 221 | * 222 | * This is purely a convenience function. The above example is equivalent to: 223 | * 224 | * ```javascript 225 | * class X extends C(B(A(Object))) {} 226 | * ``` 227 | * 228 | * @function 229 | * @param {Function} [superclass=Object] 230 | * @return {MixinBuilder} 231 | */ 232 | export const mix = (superclass) => new MixinBuilder(superclass); 233 | 234 | class MixinBuilder { 235 | 236 | constructor(superclass) { 237 | this.superclass = superclass || class {}; 238 | } 239 | 240 | /** 241 | * Applies `mixins` in order to the superclass given to `mix()`. 242 | * 243 | * @param {Array.} mixins 244 | * @return {Function} a subclass of `superclass` with `mixins` applied 245 | */ 246 | with(...mixins) { 247 | return mixins.reduce((c, m) => m(c), this.superclass); 248 | } 249 | } -------------------------------------------------------------------------------- /src/lib/mouse-event-offset.js: -------------------------------------------------------------------------------- 1 | var rootPosition = { left: 0, top: 0 } 2 | 3 | export default mouseEventOffset 4 | function mouseEventOffset (ev, target, out) { 5 | target = target || ev.currentTarget || ev.srcElement 6 | if (!Array.isArray(out)) { 7 | out = [ 0, 0 ] 8 | } 9 | var cx = ev.clientX || 0 10 | var cy = ev.clientY || 0 11 | var rect = getBoundingClientOffset(target) 12 | out[0] = cx - rect.left 13 | out[1] = cy - rect.top 14 | return out 15 | } 16 | 17 | function getBoundingClientOffset (element) { 18 | if (element === window || 19 | element === document || 20 | element === document.body) { 21 | return rootPosition 22 | } else { 23 | return element.getBoundingClientRect() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/mouse-wheel.js: -------------------------------------------------------------------------------- 1 | import MagicScroll from './MagicScroll'; 2 | import toPX from './mumath/to-px'; 3 | 4 | export default mouseWheelListen; 5 | 6 | function mouseWheelListen(element, callback, noScroll) { 7 | if (typeof element === 'function') { 8 | noScroll = !!callback; 9 | callback = element; 10 | element = window; 11 | } 12 | 13 | const magicScroll = new MagicScroll(element, 80, 12); 14 | 15 | magicScroll.onUpdate = function (delta, ev) { 16 | callback(delta, ev); 17 | }; 18 | 19 | // const lineHeight = toPX('ex', element); 20 | // const listener = function (ev) { 21 | // if (noScroll) { 22 | // ev.preventDefault(); 23 | // } 24 | // let dx = ev.deltaX || 0; 25 | // let dy = ev.deltaY || 0; 26 | // let dz = ev.deltaZ || 0; 27 | // const mode = ev.deltaMode; 28 | // let scale = 1; 29 | // switch (mode) { 30 | // case 1: 31 | // scale = lineHeight; 32 | // break; 33 | // case 2: 34 | // scale = window.innerHeight; 35 | // break; 36 | // } 37 | // dx *= scale; 38 | // dy *= scale; 39 | // dz *= scale; 40 | // if (dx || dy || dz) { 41 | // return callback(dx, dy, dz, ev); 42 | // } 43 | // }; 44 | // element.addEventListener('wheel', listener); 45 | // return listener; 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/mumath/almost.js: -------------------------------------------------------------------------------- 1 | 2 | // Type definitions for almost-equal 1.1 3 | // Project: https://github.com/mikolalysenko/almost-equal#readme 4 | // Definitions by: Curtis Maddalozzo 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | 7 | var abs = Math.abs; 8 | var min = Math.min; 9 | 10 | function almostEqual(a, b, absoluteError, relativeError) { 11 | var d = abs(a - b) 12 | 13 | if (absoluteError == null) absoluteError = almostEqual.DBL_EPSILON; 14 | if (relativeError == null) relativeError = absoluteError; 15 | 16 | if(d <= absoluteError) { 17 | return true 18 | } 19 | if(d <= relativeError * min(abs(a), abs(b))) { 20 | return true 21 | } 22 | return a === b 23 | } 24 | 25 | export const FLT_EPSILON = 1.19209290e-7 26 | export const DBL_EPSILON = 2.2204460492503131e-16 27 | 28 | almostEqual.FLT_EPSILON = FLT_EPSILON; 29 | almostEqual.DBL_EPSILON = DBL_EPSILON; 30 | 31 | export default almostEqual; -------------------------------------------------------------------------------- /src/lib/mumath/clamp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clamp value. 3 | * Detects proper clamp min/max. 4 | * 5 | * @param {number} a Current value to cut off 6 | * @param {number} min One side limit 7 | * @param {number} max Other side limit 8 | * 9 | * @return {number} Clamped value 10 | */ 11 | 12 | function clamp(a, min, max) { 13 | return max > min ? Math.max(Math.min(a,max),min) : Math.max(Math.min(a,min),max); 14 | }; 15 | 16 | export default clamp; 17 | -------------------------------------------------------------------------------- /src/lib/mumath/closest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mumath/closest 3 | */ 4 | 'use strict'; 5 | 6 | module.exports = function closest (num, arr) { 7 | var curr = arr[0]; 8 | var diff = Math.abs (num - curr); 9 | for (var val = 0; val < arr.length; val++) { 10 | var newdiff = Math.abs (num - arr[val]); 11 | if (newdiff < diff) { 12 | diff = newdiff; 13 | curr = arr[val]; 14 | } 15 | } 16 | return curr; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/mumath/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Composed set of all math utils, wrapped 3 | * 4 | * @module mumath 5 | */ 6 | 7 | export { default as clamp } from './clamp'; 8 | export { default as len } from './len'; 9 | export { default as lerp } from './lerp'; 10 | export { default as mod } from './mod'; 11 | export { default as round } from './round'; 12 | export { default as range } from './range'; 13 | export { default as order } from './order'; 14 | export { default as normalize } from './normalize'; 15 | export { default as almost } from './almost'; 16 | export { default as lg } from './log10'; 17 | export { default as isMultiple } from './is-multiple'; 18 | export { default as scale } from './scale'; 19 | export { default as pad } from './left-pad'; 20 | export { default as parseUnit } from './parse-unit'; 21 | export { default as toPx } from './to-px'; 22 | export { default as isObj } from './is-plain-obj'; -------------------------------------------------------------------------------- /src/lib/mumath/is-multiple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if one number is multiple of other 3 | * 4 | * @module mumath/is-multiple 5 | */ 6 | 7 | import almost from './almost'; 8 | 9 | export default function isMultiple(a, b, eps) { 10 | var remainder = a % b; 11 | 12 | if (!eps) eps = almost.FLT_EPSILON; 13 | 14 | if (!remainder) return true; 15 | if (almost(0, remainder, eps, 0) || almost(Math.abs(b), Math.abs(remainder), eps, 0)) return true; 16 | 17 | return false; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/mumath/is-plain-obj.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT © Sindre Sorhus 3 | * https://github.com/sindresorhus/is-plain-obj/blob/master/index.js 4 | */ 5 | export default (value) => { 6 | if (Object.prototype.toString.call(value) !== '[object Object]') { 7 | return false; 8 | } 9 | 10 | const prototype = Object.getPrototypeOf(value); 11 | return prototype === null || prototype === Object.getPrototypeOf({}); 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/mumath/left-pad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cache = [ 4 | '', 5 | ' ', 6 | ' ', 7 | ' ', 8 | ' ', 9 | ' ', 10 | ' ', 11 | ' ', 12 | ' ', 13 | ' ' 14 | ]; 15 | 16 | export default function leftPad (str, len, ch) { 17 | // convert `str` to a `string` 18 | str = str + ''; 19 | // `len` is the `pad`'s length now 20 | len = len - str.length; 21 | // doesn't need to pad 22 | if (len <= 0) return str; 23 | // `ch` defaults to `' '` 24 | if (!ch && ch !== 0) ch = ' '; 25 | // convert `ch` to a `string` cuz it could be a number 26 | ch = ch + ''; 27 | // cache common use cases 28 | if (ch === ' ' && len < 10) return cache[len] + str; 29 | // `pad` starts with an empty string 30 | var pad = ''; 31 | // loop 32 | while (true) { 33 | // add `ch` to `pad` if `len` is odd 34 | if (len & 1) pad += ch; 35 | // divide `len` by 2, ditch the remainder 36 | len >>= 1; 37 | // "double" the `ch` so this operation count grows logarithmically on `len` 38 | // each time `ch` is "doubled", the `len` would need to be "doubled" too 39 | // similar to finding a value in binary search tree, hence O(log(n)) 40 | if (len) ch += ch; 41 | // `len` is 0, exit the loop 42 | else break; 43 | } 44 | // pad `str`! 45 | return pad + str; 46 | } -------------------------------------------------------------------------------- /src/lib/mumath/len.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return quadratic length 3 | * 4 | * @module mumath/len 5 | * 6 | */ 7 | 8 | export default function len(a, b) { 9 | return Math.sqrt(a*a + b*b); 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/mumath/lerp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mumath/lerp 3 | */ 4 | 'use strict'; 5 | export default function (x, y, a) { 6 | return x * (1.0 - a) + y * a; 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/mumath/log10.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base 10 logarithm 3 | * 4 | * @module mumath/log10 5 | */ 6 | 'use strict'; 7 | export default Math.log10 || function (a) { 8 | return Math.log(a) / Math.log(10); 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/mumath/mod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Looping function for any framesize. 3 | * Like fmod. 4 | * 5 | * @module mumath/loop 6 | * 7 | */ 8 | 9 | export default function (value, left, right) { 10 | //detect single-arg case, like mod-loop or fmod 11 | if (right === undefined) { 12 | right = left; 13 | left = 0; 14 | } 15 | 16 | //swap frame order 17 | if (left > right) { 18 | var tmp = right; 19 | right = left; 20 | left = tmp; 21 | } 22 | 23 | var frame = right - left; 24 | 25 | value = ((value + left) % frame) - left; 26 | if (value < left) value += frame; 27 | if (value > right) value -= frame; 28 | 29 | return value; 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/mumath/normalize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get rid of float remainder 3 | * 4 | * @module mumath/normalize 5 | */ 6 | 'use strict'; 7 | 8 | import { FLT_EPSILON } from './almost'; 9 | 10 | export default function(value, eps) { 11 | //ignore ints 12 | var rem = value%1; 13 | if (!rem) return value; 14 | 15 | if (eps == null) eps = Number.EPSILON || FLT_EPSILON; 16 | 17 | //pick number’s neighbour, which is way shorter, like 0.4999999999999998 → 0.5 18 | //O(20) 19 | var range = 5; 20 | var len = (rem+'').length; 21 | 22 | for (var i = 1; i < range; i+=.5) { 23 | var left = rem - eps*i, 24 | right = rem + eps*i; 25 | 26 | var leftStr = left+'', rightStr = right + ''; 27 | 28 | if (len - leftStr.length > 2) return value - eps*i; 29 | if (len - rightStr.length > 2) return value + eps*i; 30 | 31 | // if (leftStr[2] != rightStr[2]) 32 | } 33 | 34 | return value; 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/mumath/order.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mumath/order 3 | */ 4 | export default function (n) { 5 | n = Math.abs(n); 6 | var order = Math.floor(Math.log(n) / Math.LN10 + 0.000000001); 7 | return Math.pow(10,order); 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/mumath/parse-unit.js: -------------------------------------------------------------------------------- 1 | export default (str, out) => { 2 | if (!out) 3 | out = [ 0, '' ] 4 | 5 | str = String(str) 6 | var num = parseFloat(str, 10) 7 | out[0] = num 8 | out[1] = str.match(/[\d.\-\+]*\s*(.*)/)[1] || '' 9 | return out 10 | } -------------------------------------------------------------------------------- /src/lib/mumath/precision.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mumath/precision 3 | * 4 | * Get precision from float: 5 | * 6 | * @example 7 | * 1.1 → 1, 1234 → 0, .1234 → 4 8 | * 9 | * @param {number} n 10 | * 11 | * @return {number} decimap places 12 | */ 13 | 'use strict'; 14 | 15 | import almost from './almost'; 16 | import norm from './normalize'; 17 | 18 | export default function (n, eps) { 19 | n = norm(n); 20 | 21 | var str = n + ''; 22 | 23 | //1e-10 etc 24 | var e = str.indexOf('e-'); 25 | if (e >= 0) return parseInt(str.substring(e+2)); 26 | 27 | //imperfect ints, like 3.0000000000000004 or 1.9999999999999998 28 | var remainder = Math.abs(n % 1); 29 | var remStr = remainder + ''; 30 | 31 | if (almost(remainder, 1, eps) || almost(remainder, 0, eps)) return 0; 32 | 33 | //usual floats like .0123 34 | var d = remStr.indexOf('.') + 1; 35 | 36 | if (d) return remStr.length - d; 37 | 38 | //regular inte 39 | return 0; 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/mumath/pretty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Format number nicely 3 | * 4 | * @module mumath/loop 5 | * 6 | */ 7 | 'use strict'; 8 | 9 | import precision from './precision'; 10 | import almost from './almost'; 11 | 12 | export default function (v, prec) { 13 | if (almost(v, 0)) return '0'; 14 | 15 | if (prec == null) { 16 | prec = precision(v); 17 | prec = Math.min(prec, 20); 18 | } 19 | 20 | // return v.toFixed(prec); 21 | return v.toFixed(prec); 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/mumath/range.js: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2016 angus croll 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | /* 24 | range(0, 5); // [0, 1, 2, 3, 4] 25 | range(5); // [0, 1, 2, 3, 4] 26 | range(-5); // [0, -1, -2, -3, -4] 27 | range(0, 20, 5) // [0, 5, 10, 15] 28 | range(0, -20, -5) // [0, -5, -10, -15] 29 | */ 30 | 31 | export default function range(start, stop, step) { 32 | if (start != null && typeof start != 'number') { 33 | throw new Error('start must be a number or null'); 34 | } 35 | if (stop != null && typeof stop != 'number') { 36 | throw new Error('stop must be a number or null'); 37 | } 38 | if (step != null && typeof step != 'number') { 39 | throw new Error('step must be a number or null'); 40 | } 41 | if (stop == null) { 42 | stop = start || 0; 43 | start = 0; 44 | } 45 | if (step == null) { 46 | step = stop > start ? 1 : -1; 47 | } 48 | var toReturn = []; 49 | var increasing = start < stop; //← here’s the change 50 | for (; increasing ? start < stop : start > stop; start += step) { 51 | toReturn.push(start); 52 | } 53 | return toReturn; 54 | } -------------------------------------------------------------------------------- /src/lib/mumath/round.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Precision round 3 | * 4 | * @param {number} value 5 | * @param {number} step Minimal discrete to round 6 | * 7 | * @return {number} 8 | * 9 | * @example 10 | * toPrecision(213.34, 1) == 213 11 | * toPrecision(213.34, .1) == 213.3 12 | * toPrecision(213.34, 10) == 210 13 | */ 14 | 'use strict'; 15 | import precision from './precision'; 16 | 17 | export default function(value, step) { 18 | if (step === 0) return value; 19 | if (!step) return Math.round(value); 20 | value = Math.round(value / step) * step; 21 | return parseFloat(value.toFixed(precision(step))); 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/mumath/scale.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get step out of the set 3 | * 4 | * @module mumath/step 5 | */ 6 | 'use strict'; 7 | 8 | import lg from './log10'; 9 | 10 | export default function (minStep, srcSteps) { 11 | var power = Math.floor(lg(minStep)); 12 | 13 | var order = Math.pow(10, power); 14 | var steps = srcSteps.map(v => v*order); 15 | order = Math.pow(10, power+1); 16 | steps = steps.concat(srcSteps.map(v => v*order)); 17 | 18 | //find closest scale 19 | var step = 0; 20 | for (var i = 0; i < steps.length; i++) { 21 | step = steps[i]; 22 | if (step >= minStep) break; 23 | } 24 | 25 | return step; 26 | }; 27 | -------------------------------------------------------------------------------- /src/lib/mumath/to-px.js: -------------------------------------------------------------------------------- 1 | // (c) 2015 Mikola Lysenko. MIT License 2 | // https://github.com/mikolalysenko/to-px 3 | 4 | import parseUnit from './parse-unit'; 5 | 6 | var PIXELS_PER_INCH = 96 7 | 8 | var defaults = { 9 | 'ch': 8, 10 | 'ex': 7.15625, 11 | 'em': 16, 12 | 'rem': 16, 13 | 'in': PIXELS_PER_INCH, 14 | 'cm': PIXELS_PER_INCH / 2.54, 15 | 'mm': PIXELS_PER_INCH / 25.4, 16 | 'pt': PIXELS_PER_INCH / 72, 17 | 'pc': PIXELS_PER_INCH / 6, 18 | 'px': 1 19 | } 20 | 21 | export default function toPX(str) { 22 | if (!str) return null 23 | 24 | if (defaults[str]) return defaults[str] 25 | 26 | // detect number of units 27 | var parts = parseUnit(str) 28 | if (!isNaN(parts[0]) && parts[1]) { 29 | var px = toPX(parts[1]) 30 | return typeof px === 'number' ? parts[0] * px : null; 31 | } 32 | 33 | return null; 34 | } -------------------------------------------------------------------------------- /src/lib/mumath/within.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Whether element is between left & right including 3 | * 4 | * @param {number} a 5 | * @param {number} left 6 | * @param {number} right 7 | * 8 | * @return {Boolean} 9 | */ 10 | 'use strict'; 11 | module.exports = function(a, left, right){ 12 | if (left > right) { 13 | var tmp = left; 14 | left = right; 15 | right = tmp; 16 | } 17 | if (a <= right && a >= left) return true; 18 | return false; 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/panzoom.js: -------------------------------------------------------------------------------- 1 | import evPos from './ev-pos'; 2 | import Impetus from './impetus'; 3 | import touchPinch from './touch-pinch'; 4 | import raf from './raf'; 5 | import MagicScroll from './MagicScroll'; 6 | 7 | const panzoom = (target, cb) => { 8 | if (target instanceof Function) { 9 | cb = target; 10 | target = document.documentElement || document.body; 11 | } 12 | 13 | if (typeof target === 'string') target = document.querySelector(target); 14 | 15 | let cursor = { 16 | x: 0, 17 | y: 0 18 | }; 19 | 20 | const hasPassive = () => { 21 | let supported = false; 22 | 23 | try { 24 | const opts = Object.defineProperty({}, 'passive', { 25 | get() { 26 | supported = true; 27 | } 28 | }); 29 | 30 | window.addEventListener('test', null, opts); 31 | window.removeEventListener('test', null, opts); 32 | } catch (e) { 33 | supported = false; 34 | } 35 | 36 | return supported; 37 | }; 38 | 39 | let impetus; 40 | let magicScroll; 41 | 42 | let initX = 0; 43 | let initY = 0; 44 | let init = true; 45 | const initFn = function(e) { 46 | init = true; 47 | }; 48 | target.addEventListener('mousedown', initFn); 49 | 50 | const onMouseMove = e => { 51 | cursor = evPos(e); 52 | }; 53 | 54 | target.addEventListener('mousemove', onMouseMove); 55 | 56 | const wheelListener = function(e) { 57 | if (e) { 58 | cursor = evPos(e); 59 | } 60 | }; 61 | 62 | target.addEventListener('wheel', wheelListener); 63 | target.addEventListener('touchstart', initFn, hasPassive() ? { passive: true } : false); 64 | 65 | target.addEventListener( 66 | 'contextmenu', 67 | e => { 68 | e.preventDefault(); 69 | return false; 70 | }, 71 | false 72 | ); 73 | 74 | let lastY = 0; 75 | let lastX = 0; 76 | impetus = new Impetus({ 77 | source: target, 78 | update(x, y) { 79 | if (init) { 80 | init = false; 81 | initX = cursor.x; 82 | initY = cursor.y; 83 | } 84 | 85 | const e = { 86 | target, 87 | type: 'mouse', 88 | dx: x - lastX, 89 | dy: y - lastY, 90 | dz: 0, 91 | x: cursor.x, 92 | y: cursor.y, 93 | x0: initX, 94 | y0: initY, 95 | isRight: cursor.isRight 96 | }; 97 | 98 | lastX = x; 99 | lastY = y; 100 | 101 | schedule(e); 102 | }, 103 | stop() { 104 | const ev = { 105 | target, 106 | type: 'mouse', 107 | dx: 0, 108 | dy: 0, 109 | dz: 0, 110 | x: cursor.x, 111 | y: cursor.y, 112 | x0: initX, 113 | y0: initY 114 | }; 115 | schedule(ev); 116 | }, 117 | multiplier: 1, 118 | friction: 0.75 119 | }); 120 | 121 | magicScroll = new MagicScroll(target, 80, 12, 0); 122 | 123 | magicScroll.onUpdate = (dy, e) => { 124 | schedule({ 125 | target, 126 | type: 'mouse', 127 | dx: 0, 128 | dy: 0, 129 | dz: dy, 130 | x: cursor.x, 131 | y: cursor.y, 132 | x0: cursor.x, 133 | y0: cursor.y 134 | }); 135 | }; 136 | 137 | // mobile pinch zoom 138 | const pinch = touchPinch(target); 139 | const mult = 2; 140 | let initialCoords; 141 | 142 | pinch.on('start', curr => { 143 | const f1 = pinch.fingers[0]; 144 | const f2 = pinch.fingers[1]; 145 | 146 | initialCoords = [ 147 | f2.position[0] * 0.5 + f1.position[0] * 0.5, 148 | f2.position[1] * 0.5 + f1.position[1] * 0.5 149 | ]; 150 | 151 | impetus && impetus.pause(); 152 | }); 153 | pinch.on('end', () => { 154 | if (!initialCoords) return; 155 | 156 | initialCoords = null; 157 | 158 | impetus && impetus.resume(); 159 | }); 160 | pinch.on('change', (curr, prev) => { 161 | if (!pinch.pinching || !initialCoords) return; 162 | 163 | schedule({ 164 | target, 165 | type: 'touch', 166 | dx: 0, 167 | dy: 0, 168 | dz: -(curr - prev) * mult, 169 | x: initialCoords[0], 170 | y: initialCoords[1], 171 | x0: initialCoords[0], 172 | y0: initialCoords[0] 173 | }); 174 | }); 175 | 176 | // schedule function to current or next frame 177 | let planned; 178 | let frameId; 179 | function schedule(ev) { 180 | if (frameId != null) { 181 | if (!planned) planned = ev; 182 | else { 183 | planned.dx += ev.dx; 184 | planned.dy += ev.dy; 185 | planned.dz += ev.dz; 186 | 187 | planned.x = ev.x; 188 | planned.y = ev.y; 189 | } 190 | 191 | return; 192 | } 193 | 194 | // Firefox sometimes does not clear webgl current drawing buffer 195 | // so we have to schedule callback to the next frame, not the current 196 | // cb(ev) 197 | 198 | frameId = raf(() => { 199 | cb(ev); 200 | frameId = null; 201 | if (planned) { 202 | const arg = planned; 203 | planned = null; 204 | schedule(arg); 205 | } 206 | }); 207 | } 208 | 209 | return function unpanzoom() { 210 | target.removeEventListener('mousedown', initFn); 211 | target.removeEventListener('mousemove', onMouseMove); 212 | target.removeEventListener('touchstart', initFn); 213 | 214 | impetus.destroy(); 215 | 216 | target.removeEventListener('wheel', wheelListener); 217 | 218 | pinch.disable(); 219 | 220 | raf.cancel(frameId); 221 | }; 222 | }; 223 | 224 | export default panzoom; 225 | -------------------------------------------------------------------------------- /src/lib/raf.js: -------------------------------------------------------------------------------- 1 | function getPrefixed(name) { 2 | return window['webkit' + name] || window['moz' + name] || window['ms' + name]; 3 | } 4 | 5 | var lastTime = 0; 6 | 7 | // fallback for IE 7-8 8 | function timeoutDefer(fn) { 9 | var time = +new Date(), 10 | timeToCall = Math.max(0, 16 - (time - lastTime)); 11 | 12 | lastTime = time + timeToCall; 13 | return window.setTimeout(fn, timeToCall); 14 | } 15 | 16 | export function bind(fn, obj) { 17 | var slice = Array.prototype.slice; 18 | 19 | if (fn.bind) { 20 | return fn.bind.apply(fn, slice.call(arguments, 1)); 21 | } 22 | 23 | var args = slice.call(arguments, 2); 24 | 25 | return function () { 26 | return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); 27 | }; 28 | } 29 | 30 | export var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer; 31 | export var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || 32 | getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; 33 | 34 | const raf = (fn, context, immediate) =>{ 35 | if (immediate && requestFn === timeoutDefer) { 36 | fn.call(context); 37 | } else { 38 | return requestFn.call(window, bind(fn, context)); 39 | } 40 | } 41 | 42 | raf.cancel = (id) => { 43 | if (id) { 44 | cancelFn.call(window, id); 45 | } 46 | } 47 | 48 | export default raf; -------------------------------------------------------------------------------- /src/lib/touch-pinch.js: -------------------------------------------------------------------------------- 1 | import EventEmitter2 from 'eventemitter2'; 2 | import eventOffset from './mouse-event-offset'; 3 | 4 | function distance(a, b) { 5 | var x = b[0] - a[0], 6 | y = b[1] - a[1] 7 | return Math.sqrt(x*x + y*y) 8 | } 9 | 10 | export default touchPinch; 11 | function touchPinch (target) { 12 | target = target || window 13 | 14 | var emitter = new EventEmitter2() 15 | var fingers = [ null, null ] 16 | var activeCount = 0 17 | 18 | var lastDistance = 0 19 | var ended = false 20 | var enabled = false 21 | 22 | // some read-only values 23 | Object.defineProperties(emitter, { 24 | pinching(){ 25 | return activeCount === 2 26 | }, 27 | 28 | fingers() { 29 | return fingers 30 | } 31 | }) 32 | 33 | enable() 34 | emitter.enable = enable 35 | emitter.disable = disable 36 | emitter.indexOfTouch = indexOfTouch 37 | return emitter 38 | 39 | function indexOfTouch (touch) { 40 | var id = touch.identifier 41 | for (var i = 0; i < fingers.length; i++) { 42 | if (fingers[i] && 43 | fingers[i].touch && 44 | fingers[i].touch.identifier === id) { 45 | return i 46 | } 47 | } 48 | return -1 49 | } 50 | 51 | function enable () { 52 | if (enabled) return 53 | enabled = true 54 | target.addEventListener('touchstart', onTouchStart, false) 55 | target.addEventListener('touchmove', onTouchMove, false) 56 | target.addEventListener('touchend', onTouchRemoved, false) 57 | target.addEventListener('touchcancel', onTouchRemoved, false) 58 | } 59 | 60 | function disable () { 61 | if (!enabled) return 62 | enabled = false 63 | activeCount = 0 64 | fingers[0] = null 65 | fingers[1] = null 66 | lastDistance = 0 67 | ended = false 68 | target.removeEventListener('touchstart', onTouchStart, false) 69 | target.removeEventListener('touchmove', onTouchMove, false) 70 | target.removeEventListener('touchend', onTouchRemoved, false) 71 | target.removeEventListener('touchcancel', onTouchRemoved, false) 72 | } 73 | 74 | function onTouchStart (ev) { 75 | for (var i = 0; i < ev.changedTouches.length; i++) { 76 | var newTouch = ev.changedTouches[i] 77 | var id = newTouch.identifier 78 | var idx = indexOfTouch(id) 79 | 80 | if (idx === -1 && activeCount < 2) { 81 | var first = activeCount === 0 82 | 83 | // newest and previous finger (previous may be undefined) 84 | var newIndex = fingers[0] ? 1 : 0 85 | var oldIndex = fingers[0] ? 0 : 1 86 | var newFinger = new Finger() 87 | 88 | // add to stack 89 | fingers[newIndex] = newFinger 90 | activeCount++ 91 | 92 | // update touch event & position 93 | newFinger.touch = newTouch 94 | eventOffset(newTouch, target, newFinger.position) 95 | 96 | var oldTouch = fingers[oldIndex] ? fingers[oldIndex].touch : undefined 97 | emitter.emit('place', newTouch, oldTouch) 98 | 99 | if (!first) { 100 | var initialDistance = computeDistance() 101 | ended = false 102 | emitter.emit('start', initialDistance) 103 | lastDistance = initialDistance 104 | } 105 | } 106 | } 107 | } 108 | 109 | function onTouchMove (ev) { 110 | var changed = false 111 | for (var i = 0; i < ev.changedTouches.length; i++) { 112 | var movedTouch = ev.changedTouches[i] 113 | var idx = indexOfTouch(movedTouch) 114 | if (idx !== -1) { 115 | changed = true 116 | fingers[idx].touch = movedTouch // avoid caching touches 117 | eventOffset(movedTouch, target, fingers[idx].position) 118 | } 119 | } 120 | 121 | if (activeCount === 2 && changed) { 122 | var currentDistance = computeDistance() 123 | emitter.emit('change', currentDistance, lastDistance) 124 | lastDistance = currentDistance 125 | } 126 | } 127 | 128 | function onTouchRemoved (ev) { 129 | for (var i = 0; i < ev.changedTouches.length; i++) { 130 | var removed = ev.changedTouches[i] 131 | var idx = indexOfTouch(removed) 132 | 133 | if (idx !== -1) { 134 | fingers[idx] = null 135 | activeCount-- 136 | var otherIdx = idx === 0 ? 1 : 0 137 | var otherTouch = fingers[otherIdx] ? fingers[otherIdx].touch : undefined 138 | emitter.emit('lift', removed, otherTouch) 139 | } 140 | } 141 | 142 | if (!ended && activeCount !== 2) { 143 | ended = true 144 | emitter.emit('end') 145 | } 146 | } 147 | 148 | function computeDistance () { 149 | if (activeCount < 2) return 0 150 | return distance(fingers[0].position, fingers[1].position) 151 | } 152 | } 153 | 154 | function Finger () { 155 | this.position = [0, 0] 156 | this.touch = null 157 | } 158 | -------------------------------------------------------------------------------- /src/map/Map.js: -------------------------------------------------------------------------------- 1 | import panzoom from '../lib/panzoom'; 2 | import { clamp } from '../lib/mumath/index'; 3 | 4 | import Base from '../core/Base'; 5 | import { MAP, Modes } from '../core/Constants'; 6 | import Grid from '../grid/Grid'; 7 | import { Point } from '../geometry/Point'; 8 | import ModesMixin from './ModesMixin'; 9 | import Measurement from '../measurement/Measurement'; 10 | import { mix } from '../lib/mix'; 11 | 12 | export class Map extends mix(Base).with(ModesMixin) { 13 | constructor(container, options) { 14 | super(options); 15 | 16 | this.defaults = Object.assign({}, MAP); 17 | 18 | // set defaults 19 | Object.assign(this, this.defaults); 20 | 21 | // overwrite options 22 | Object.assign(this, this._options); 23 | 24 | this.center = new Point(this.center); 25 | 26 | this.container = container || document.body; 27 | 28 | const canvas = document.createElement('canvas'); 29 | this.container.appendChild(canvas); 30 | canvas.setAttribute('id', 'indoors-map-canvas'); 31 | 32 | canvas.width = this.width || this.container.clientWidth; 33 | canvas.height = this.height || this.container.clientHeight; 34 | 35 | this.canvas = new fabric.Canvas(canvas, { 36 | preserveObjectStacking: true, 37 | renderOnAddRemove: true 38 | }); 39 | this.context = this.canvas.getContext('2d'); 40 | 41 | this.on('render', () => { 42 | if (this.autostart) this.clear(); 43 | }); 44 | 45 | this.originX = -this.canvas.width / 2; 46 | this.originY = -this.canvas.height / 2; 47 | 48 | this.canvas.absolutePan({ 49 | x: this.originX, 50 | y: this.originY 51 | }); 52 | 53 | // this.center = { 54 | // x: this.canvas.width / 2.0, 55 | // y: this.canvas.height / 2.0 56 | // }; 57 | 58 | this.x = this.center.x; 59 | this.y = this.center.y; 60 | this.dx = 0; 61 | this.dy = 0; 62 | 63 | try { 64 | this.addFloorPlan(); 65 | } catch (e) { 66 | console.error(e); 67 | } 68 | 69 | if (this.showGrid) { 70 | this.addGrid(); 71 | } 72 | 73 | this.setMode(this.mode || Modes.GRAB); 74 | 75 | const vm = this; 76 | panzoom(this.container, e => { 77 | vm.panzoom(e); 78 | }); 79 | 80 | this.registerListeners(); 81 | 82 | setTimeout(() => { 83 | this.emit('ready', this); 84 | }, 300); 85 | 86 | this.measurement = new Measurement(this); 87 | } 88 | 89 | addFloorPlan() { 90 | if (!this.floorplan) return; 91 | const vm = this; 92 | this.floorplan.on('load', img => { 93 | vm.addLayer(img); 94 | }); 95 | } 96 | 97 | addLayer(layer) { 98 | // this.canvas.renderOnAddRemove = false; 99 | if (!layer.shape) { 100 | console.error('shape is undefined'); 101 | return; 102 | } 103 | this.canvas.add(layer.shape); 104 | this.canvas._objects.sort((o1, o2) => o1.zIndex - o2.zIndex); 105 | 106 | if (layer.shape.keepOnZoom) { 107 | const scale = 1.0 / this.zoom; 108 | layer.shape.set('scaleX', scale); 109 | layer.shape.set('scaleY', scale); 110 | layer.shape.setCoords(); 111 | this.emit(`${layer.class}scaling`, layer); 112 | } 113 | if (layer.class) { 114 | this.emit(`${layer.class}:added`, layer); 115 | } 116 | 117 | // this.canvas.renderOnAddRemove = true; 118 | 119 | // this.update(); 120 | this.canvas.requestRenderAll(); 121 | } 122 | 123 | removeLayer(layer) { 124 | if (!layer || !layer.shape) return; 125 | if (layer.class) { 126 | this.emit(`${layer.class}:removed`, layer); 127 | } 128 | this.canvas.remove(layer.shape); 129 | } 130 | 131 | addGrid() { 132 | this.gridCanvas = this.cloneCanvas(); 133 | this.gridCanvas.setAttribute('id', 'indoors-grid-canvas'); 134 | this.grid = new Grid(this.gridCanvas, this); 135 | this.grid.draw(); 136 | } 137 | 138 | moveTo(obj, index) { 139 | if (index !== undefined) { 140 | obj.zIndex = index; 141 | } 142 | this.canvas.moveTo(obj.shape, obj.zIndex); 143 | } 144 | 145 | cloneCanvas(canvas) { 146 | canvas = canvas || this.canvas; 147 | const clone = document.createElement('canvas'); 148 | clone.width = canvas.width; 149 | clone.height = canvas.height; 150 | canvas.wrapperEl.appendChild(clone); 151 | return clone; 152 | } 153 | 154 | setZoom(zoom) { 155 | const { width, height } = this.canvas; 156 | this.zoom = clamp(zoom, this.minZoom, this.maxZoom); 157 | this.dx = 0; 158 | this.dy = 0; 159 | this.x = width / 2.0; 160 | this.y = height / 2.0; 161 | this.update(); 162 | process.nextTick(() => { 163 | this.update(); 164 | }); 165 | } 166 | 167 | getBounds() { 168 | let minX = Infinity; 169 | let maxX = -Infinity; 170 | let minY = Infinity; 171 | let maxY = -Infinity; 172 | 173 | this.canvas.forEachObject(obj => { 174 | const coords = obj.getBounds(); 175 | 176 | coords.forEach(point => { 177 | minX = Math.min(minX, point.x); 178 | maxX = Math.max(maxX, point.x); 179 | minY = Math.min(minY, point.y); 180 | maxY = Math.max(maxY, point.y); 181 | }); 182 | }); 183 | 184 | return [new Point(minX, minY), new Point(maxX, maxY)]; 185 | } 186 | 187 | fitBounds(padding = 100) { 188 | this.onResize(); 189 | 190 | const { width, height } = this.canvas; 191 | 192 | this.originX = -this.canvas.width / 2; 193 | this.originY = -this.canvas.height / 2; 194 | 195 | const bounds = this.getBounds(); 196 | 197 | this.center.x = (bounds[0].x + bounds[1].x) / 2.0; 198 | this.center.y = -(bounds[0].y + bounds[1].y) / 2.0; 199 | 200 | const boundWidth = Math.abs(bounds[0].x - bounds[1].x) + padding; 201 | const boundHeight = Math.abs(bounds[0].y - bounds[1].y) + padding; 202 | const scaleX = width / boundWidth; 203 | const scaleY = height / boundHeight; 204 | 205 | this.zoom = Math.min(scaleX, scaleY); 206 | 207 | this.canvas.setZoom(this.zoom); 208 | 209 | this.canvas.absolutePan({ 210 | x: this.originX + this.center.x * this.zoom, 211 | y: this.originY - this.center.y * this.zoom 212 | }); 213 | 214 | this.update(); 215 | process.nextTick(() => { 216 | this.update(); 217 | }); 218 | } 219 | 220 | setCursor(cursor) { 221 | this.container.style.cursor = cursor; 222 | } 223 | 224 | reset() { 225 | const { width, height } = this.canvas; 226 | this.zoom = this._options.zoom || 1; 227 | this.center = new Point(); 228 | this.originX = -this.canvas.width / 2; 229 | this.originY = -this.canvas.height / 2; 230 | this.canvas.absolutePan({ 231 | x: this.originX, 232 | y: this.originY 233 | }); 234 | this.x = width / 2.0; 235 | this.y = height / 2.0; 236 | this.update(); 237 | process.nextTick(() => { 238 | this.update(); 239 | }); 240 | } 241 | 242 | onResize(width, height) { 243 | const oldWidth = this.canvas.width; 244 | const oldHeight = this.canvas.height; 245 | 246 | width = width || this.container.clientWidth; 247 | height = height || this.container.clientHeight; 248 | 249 | this.canvas.setWidth(width); 250 | this.canvas.setHeight(height); 251 | 252 | if (this.grid) { 253 | this.grid.setSize(width, height); 254 | } 255 | 256 | const dx = width / 2.0 - oldWidth / 2.0; 257 | const dy = height / 2.0 - oldHeight / 2.0; 258 | 259 | this.canvas.relativePan({ 260 | x: dx, 261 | y: dy 262 | }); 263 | 264 | this.update(); 265 | } 266 | 267 | update() { 268 | const canvas = this.canvas; 269 | 270 | if (this.grid) { 271 | this.grid.update2({ 272 | x: this.center.x, 273 | y: this.center.y, 274 | zoom: this.zoom 275 | }); 276 | } 277 | this.emit('update', this); 278 | if (this.grid) { 279 | this.grid.render(); 280 | } 281 | 282 | canvas.zoomToPoint(new Point(this.x, this.y), this.zoom); 283 | 284 | if (this.isGrabMode() || this.isRight) { 285 | canvas.relativePan(new Point(this.dx, this.dy)); 286 | this.emit('panning'); 287 | this.setCursor('grab'); 288 | } else { 289 | this.setCursor('pointer'); 290 | } 291 | 292 | const now = Date.now(); 293 | if (!this.lastUpdatedTime && Math.abs(this.lastUpdatedTime - now) < 100) { 294 | return; 295 | } 296 | this.lastUpdatedTime = now; 297 | 298 | const objects = canvas.getObjects(); 299 | let hasKeepZoom = false; 300 | for (let i = 0; i < objects.length; i += 1) { 301 | const object = objects[i]; 302 | if (object.keepOnZoom) { 303 | object.set('scaleX', 1.0 / this.zoom); 304 | object.set('scaleY', 1.0 / this.zoom); 305 | object.setCoords(); 306 | hasKeepZoom = true; 307 | this.emit(`${object.class}scaling`, object); 308 | } 309 | } 310 | if (hasKeepZoom) canvas.requestRenderAll(); 311 | } 312 | 313 | panzoom(e) { 314 | // enable interactions 315 | const { width, height } = this.canvas; 316 | // shift start 317 | const zoom = clamp(-e.dz, -height * 0.75, height * 0.75) / height; 318 | 319 | const prevZoom = 1 / this.zoom; 320 | let curZoom = prevZoom * (1 - zoom); 321 | curZoom = clamp(curZoom, this.minZoom, this.maxZoom); 322 | 323 | let { x, y } = this.center; 324 | 325 | // pan 326 | const oX = 0.5; 327 | const oY = 0.5; 328 | if (this.isGrabMode() || e.isRight) { 329 | x -= prevZoom * e.dx; 330 | y += prevZoom * e.dy; 331 | this.setCursor('grab'); 332 | } else { 333 | this.setCursor('pointer'); 334 | } 335 | 336 | if (this.zoomEnabled) { 337 | const tx = e.x / width - oX; 338 | x -= width * (curZoom - prevZoom) * tx; 339 | const ty = oY - e.y / height; 340 | y -= height * (curZoom - prevZoom) * ty; 341 | } 342 | this.center.setX(x); 343 | this.center.setY(y); 344 | this.zoom = 1 / curZoom; 345 | this.dx = e.dx; 346 | this.dy = e.dy; 347 | this.x = e.x0; 348 | this.y = e.y0; 349 | this.isRight = e.isRight; 350 | this.update(); 351 | } 352 | 353 | setView(view) { 354 | this.dx = 0; 355 | this.dy = 0; 356 | this.x = 0; 357 | this.y = 0; 358 | view.y *= -1; 359 | 360 | const dx = this.center.x - view.x; 361 | const dy = -this.center.y + view.y; 362 | 363 | this.center.copy(view); 364 | 365 | this.canvas.relativePan(new Point(dx * this.zoom, dy * this.zoom)); 366 | 367 | this.canvas.renderAll(); 368 | 369 | this.update(); 370 | 371 | process.nextTick(() => { 372 | this.update(); 373 | }); 374 | } 375 | 376 | registerListeners() { 377 | const vm = this; 378 | 379 | this.canvas.on('object:scaling', e => { 380 | if (e.target.class) { 381 | vm.emit(`${e.target.class}:scaling`, e.target.parent); 382 | e.target.parent.emit('scaling', e.target.parent); 383 | return; 384 | } 385 | const group = e.target; 386 | if (!group.getObjects) return; 387 | 388 | const objects = group.getObjects(); 389 | group.removeWithUpdate(); 390 | for (let i = 0; i < objects.length; i += 1) { 391 | const object = objects[i]; 392 | object.orgYaw = object.parent.yaw || 0; 393 | object.fire('moving', object.parent); 394 | vm.emit(`${object.class}:moving`, object.parent); 395 | } 396 | vm.update(); 397 | vm.canvas.requestRenderAll(); 398 | }); 399 | 400 | this.canvas.on('object:rotating', e => { 401 | if (e.target.class) { 402 | vm.emit(`${e.target.class}:rotating`, e.target.parent, e.target.angle); 403 | e.target.parent.emit('rotating', e.target.parent, e.target.angle); 404 | return; 405 | } 406 | const group = e.target; 407 | if (!group.getObjects) return; 408 | const objects = group.getObjects(); 409 | for (let i = 0; i < objects.length; i += 1) { 410 | const object = objects[i]; 411 | if (object.class === 'marker') { 412 | object._set('angle', -group.angle); 413 | object.parent.yaw = -group.angle + (object.orgYaw || 0); 414 | // object.orgYaw = object.parent.yaw; 415 | object.fire('moving', object.parent); 416 | vm.emit(`${object.class}:moving`, object.parent); 417 | object.fire('rotating', object.parent); 418 | vm.emit(`${object.class}:rotating`, object.parent); 419 | } 420 | } 421 | this.update(); 422 | }); 423 | 424 | this.canvas.on('object:moving', e => { 425 | if (e.target.class) { 426 | vm.emit(`${e.target.class}:moving`, e.target.parent); 427 | e.target.parent.emit('moving', e.target.parent); 428 | return; 429 | } 430 | const group = e.target; 431 | if (!group.getObjects) return; 432 | const objects = group.getObjects(); 433 | for (let i = 0; i < objects.length; i += 1) { 434 | const object = objects[i]; 435 | if (object.class) { 436 | object.fire('moving', object.parent); 437 | vm.emit(`${object.class}:moving`, object.parent); 438 | } 439 | } 440 | this.update(); 441 | }); 442 | 443 | this.canvas.on('object:moved', e => { 444 | if (e.target.class) { 445 | vm.emit(`${e.target.class}dragend`, e); 446 | vm.emit(`${e.target.class}:moved`, e.target.parent); 447 | e.target.parent.emit('moved', e.target.parent); 448 | this.update(); 449 | return; 450 | } 451 | const group = e.target; 452 | if (!group.getObjects) return; 453 | const objects = group.getObjects(); 454 | for (let i = 0; i < objects.length; i += 1) { 455 | const object = objects[i]; 456 | if (object.class) { 457 | object.fire('moved', object.parent); 458 | vm.emit(`${object.class}:moved`, object.parent); 459 | } 460 | } 461 | this.update(); 462 | }); 463 | 464 | this.canvas.on('selection:cleared', e => { 465 | const objects = e.deselected; 466 | if (!objects || !objects.length) return; 467 | for (let i = 0; i < objects.length; i += 1) { 468 | const object = objects[i]; 469 | if (object.class === 'marker') { 470 | object._set('angle', 0); 471 | object._set('scaleX', 1 / vm.zoom); 472 | object._set('scaleY', 1 / vm.zoom); 473 | if (object.parent) { 474 | object.parent.inGroup = false; 475 | } 476 | object.fire('moving', object.parent); 477 | } 478 | } 479 | }); 480 | this.canvas.on('selection:created', e => { 481 | const objects = e.selected; 482 | if (!objects || objects.length < 2) return; 483 | for (let i = 0; i < objects.length; i += 1) { 484 | const object = objects[i]; 485 | if (object.class && object.parent) { 486 | object.parent.inGroup = true; 487 | object.orgYaw = object.parent.yaw || 0; 488 | } 489 | } 490 | }); 491 | this.canvas.on('selection:updated', e => { 492 | const objects = e.selected; 493 | if (!objects || objects.length < 2) return; 494 | for (let i = 0; i < objects.length; i += 1) { 495 | const object = objects[i]; 496 | if (object.class && object.parent) { 497 | object.parent.inGroup = true; 498 | object.orgYaw = object.parent.yaw || 0; 499 | } 500 | } 501 | }); 502 | 503 | this.canvas.on('mouse:down', e => { 504 | vm.dragObject = e.target; 505 | }); 506 | 507 | this.canvas.on('mouse:move', e => { 508 | if (this.isMeasureMode()) { 509 | this.measurement.onMouseMove(e); 510 | } 511 | if (vm.dragObject && vm.dragObject.clickable) { 512 | if (vm.dragObject === e.target) { 513 | vm.dragObject.dragging = true; 514 | } else { 515 | vm.dragObject.dragging = false; 516 | } 517 | } 518 | this.isRight = false; 519 | if ('which' in e.e) { 520 | // Gecko (Firefox), WebKit (Safari/Chrome) & Opera 521 | this.isRight = e.e.which === 3; 522 | } else if ('button' in e.e) { 523 | // IE, Opera 524 | this.isRight = e.e.button === 2; 525 | } 526 | 527 | vm.emit('mouse:move', e); 528 | }); 529 | 530 | this.canvas.on('mouse:up', e => { 531 | if (this.isMeasureMode()) { 532 | this.measurement.onClick(e); 533 | } 534 | 535 | this.isRight = false; 536 | this.dx = 0; 537 | this.dy = 0; 538 | 539 | if (!vm.dragObject || !e.target || !e.target.selectable) { 540 | e.target = null; 541 | vm.emit('mouse:click', e); 542 | } 543 | if (vm.dragObject && vm.dragObject.clickable) { 544 | if (vm.dragObject !== e.target) return; 545 | if (!vm.dragObject.dragging && !vm.modeToggleByKey) { 546 | vm.emit(`${vm.dragObject.class}:click`, vm.dragObject.parent); 547 | } 548 | vm.dragObject.dragging = false; 549 | } 550 | vm.dragObject = null; 551 | }); 552 | 553 | window.addEventListener('resize', () => { 554 | vm.onResize(); 555 | }); 556 | 557 | // document.addEventListener('keyup', () => { 558 | // if (this.modeToggleByKey && this.isGrabMode()) { 559 | // this.setModeAsSelect(); 560 | // this.modeToggleByKey = false; 561 | // } 562 | // }); 563 | 564 | // document.addEventListener('keydown', event => { 565 | // if (event.ctrlKey || event.metaKey) { 566 | // if (this.isSelectMode()) { 567 | // this.setModeAsGrab(); 568 | // } 569 | // this.modeToggleByKey = true; 570 | // } 571 | // }); 572 | } 573 | 574 | unregisterListeners() { 575 | this.canvas.off('object:moving'); 576 | this.canvas.off('object:moved'); 577 | } 578 | 579 | getMarkerById(id) { 580 | const objects = this.canvas.getObjects(); 581 | for (let i = 0; i < objects.length; i += 1) { 582 | const obj = objects[i]; 583 | if (obj.class === 'marker' && obj.id === id) { 584 | return obj.parent; 585 | } 586 | } 587 | return null; 588 | } 589 | 590 | getMarkers() { 591 | const list = []; 592 | const objects = this.canvas.getObjects(); 593 | for (let i = 0; i < objects.length; i += 1) { 594 | const obj = objects[i]; 595 | if (obj.class === 'marker') { 596 | list.push(obj.parent); 597 | } 598 | } 599 | return list; 600 | } 601 | } 602 | 603 | export const map = (container, options) => new Map(container, options); 604 | -------------------------------------------------------------------------------- /src/map/ModesMixin.js: -------------------------------------------------------------------------------- 1 | import { Modes } from '../core/Constants'; 2 | 3 | const ModesMixin = superclass => 4 | class extends superclass { 5 | /** 6 | * MODES 7 | */ 8 | setMode(mode) { 9 | this.mode = mode; 10 | 11 | switch (mode) { 12 | case Modes.SELECT: 13 | this.canvas.isDrawingMode = false; 14 | this.canvas.interactive = true; 15 | this.canvas.selection = true; 16 | this.canvas.hoverCursor = 'default'; 17 | this.canvas.moveCursor = 'default'; 18 | break; 19 | case Modes.GRAB: 20 | this.canvas.isDrawingMode = false; 21 | this.canvas.interactive = false; 22 | this.canvas.selection = false; 23 | this.canvas.discardActiveObject(); 24 | this.canvas.hoverCursor = 'move'; 25 | this.canvas.moveCursor = 'move'; 26 | break; 27 | case Modes.MEASURE: 28 | this.canvas.isDrawingMode = true; 29 | this.canvas.freeDrawingBrush.color = 'transparent'; 30 | this.canvas.discardActiveObject(); 31 | break; 32 | case Modes.DRAW: 33 | this.canvas.isDrawingMode = true; 34 | break; 35 | 36 | default: 37 | break; 38 | } 39 | } 40 | 41 | setModeAsDraw() { 42 | this.setMode(Modes.DRAW); 43 | } 44 | 45 | setModeAsSelect() { 46 | this.setMode(Modes.SELECT); 47 | } 48 | 49 | setModeAsMeasure() { 50 | this.setMode(Modes.MEASURE); 51 | } 52 | 53 | setModeAsGrab() { 54 | this.setMode(Modes.GRAB); 55 | } 56 | 57 | isSelectMode() { 58 | return this.mode === Modes.SELECT; 59 | } 60 | 61 | isGrabMode() { 62 | return this.mode === Modes.GRAB; 63 | } 64 | 65 | isMeasureMode() { 66 | return this.mode === Modes.MEASURE; 67 | } 68 | 69 | isDrawMode() { 70 | return this.mode === Modes.DRAW; 71 | } 72 | }; 73 | 74 | export default ModesMixin; 75 | -------------------------------------------------------------------------------- /src/map/index.js: -------------------------------------------------------------------------------- 1 | export * from './Map'; 2 | -------------------------------------------------------------------------------- /src/measurement/Measurement.js: -------------------------------------------------------------------------------- 1 | import Measurer from './Measurer'; 2 | 3 | class Measurement { 4 | constructor(map) { 5 | this.map = map; 6 | this.measurer = null; 7 | } 8 | 9 | onMouseMove(e) { 10 | const point = { 11 | x: e.absolutePointer.x, 12 | y: e.absolutePointer.y, 13 | }; 14 | 15 | if (this.measurer && !this.measurer.completed) { 16 | this.measurer.setEnd(point); 17 | this.map.canvas.requestRenderAll(); 18 | } 19 | } 20 | 21 | onClick(e) { 22 | const point = { 23 | x: e.absolutePointer.x, 24 | y: e.absolutePointer.y, 25 | }; 26 | if (!this.measurer) { 27 | this.measurer = new Measurer({ 28 | start: point, 29 | end: point, 30 | map: this.map, 31 | }); 32 | 33 | // this.map.canvas.add(this.measurer); 34 | } else if (!this.measurer.completed) { 35 | this.measurer.setEnd(point); 36 | this.measurer.complete(); 37 | } 38 | } 39 | } 40 | 41 | export default Measurement; 42 | -------------------------------------------------------------------------------- /src/measurement/Measurer.js: -------------------------------------------------------------------------------- 1 | import { Point } from '../geometry/Point'; 2 | 3 | class Measurer { 4 | constructor(options) { 5 | options = options || {}; 6 | options.hasBorders = false; 7 | options.selectable = false; 8 | options.hasControls = false; 9 | // options.evented = false; 10 | options.class = 'measurer'; 11 | options.scale = options.scale || 1; 12 | 13 | // super([], options); 14 | 15 | this.options = options || {}; 16 | this.start = this.options.start; 17 | this.end = this.options.end; 18 | 19 | this.canvas = this.options.map.canvas; 20 | 21 | this.completed = false; 22 | 23 | if (!this.start || !this.end) { 24 | throw new Error('start must be defined'); 25 | } 26 | this.draw(); 27 | } 28 | 29 | clear() { 30 | if (this.objects) { 31 | this.objects.forEach((object) => { 32 | this.canvas.remove(object); 33 | }); 34 | } 35 | } 36 | 37 | draw() { 38 | this.clear(); 39 | 40 | let { start, end } = this; 41 | start = new Point(start); 42 | end = new Point(end); 43 | 44 | const center = start.add(end).multiply(0.5); 45 | 46 | this.line = new fabric.Line([start.x, start.y, end.x, end.y], { 47 | stroke: this.options.stroke || '#3e82ff', 48 | hasControls: false, 49 | hasBorders: false, 50 | selectable: false, 51 | evented: false, 52 | strokeDashArray: [5, 5], 53 | }); 54 | 55 | const lineEndOptions = { 56 | left: start.x, 57 | top: start.y, 58 | strokeWidth: 1, 59 | radius: this.options.radius || 1, 60 | fill: this.options.fill || '#3e82ff', 61 | stroke: this.options.stroke || '#3e82ff', 62 | hasControls: false, 63 | hasBorders: false, 64 | }; 65 | 66 | const lineEndOptions2 = { 67 | left: start.x, 68 | top: start.y, 69 | strokeWidth: 1, 70 | radius: this.options.radius || 5, 71 | fill: this.options.fill || '#3e82ff33', 72 | stroke: this.options.stroke || '#3e82ff', 73 | hasControls: false, 74 | hasBorders: false, 75 | }; 76 | 77 | this.circle1 = new fabric.Circle(lineEndOptions2); 78 | this.circle2 = new fabric.Circle({ 79 | ...lineEndOptions2, 80 | left: end.x, 81 | top: end.y, 82 | }); 83 | 84 | this.circle11 = new fabric.Circle(lineEndOptions); 85 | this.circle22 = new fabric.Circle({ 86 | ...lineEndOptions, 87 | left: end.x, 88 | top: end.y, 89 | }); 90 | 91 | let text = Math.round(start.distanceFrom(end)); 92 | text = `${text / 100} m`; 93 | this.text = new fabric.Text(text, { 94 | textBackgroundColor: 'black', 95 | fill: 'white', 96 | left: center.x, 97 | top: center.y - 10, 98 | fontSize: 12, 99 | hasControls: false, 100 | hasBorders: false, 101 | selectable: false, 102 | evented: false, 103 | }); 104 | 105 | this.objects = [this.line, this.text, this.circle11, this.circle22, this.circle1, this.circle2]; 106 | 107 | this.objects.forEach((object) => { 108 | this.canvas.add(object); 109 | }); 110 | 111 | this.line.hasControls = false; 112 | this.line.hasBorders = false; 113 | this.line.selectable = false; 114 | this.line.evented = false; 115 | 116 | this.registerListeners(); 117 | } 118 | 119 | setStart(start) { 120 | this.start = start; 121 | this.draw(); 122 | } 123 | 124 | setEnd(end) { 125 | this.end = end; 126 | this.draw(); 127 | } 128 | 129 | complete() { 130 | this.completed = true; 131 | } 132 | 133 | registerListeners() { 134 | this.circle2.on('moving', (e) => { 135 | this.setEnd(e.pointer); 136 | }); 137 | 138 | this.circle1.on('moving', (e) => { 139 | this.setStart(e.pointer); 140 | }); 141 | } 142 | 143 | applyScale(scale) { 144 | this.start.x *= scale; 145 | this.start.y *= scale; 146 | this.end.x *= scale; 147 | this.end.y *= scale; 148 | this.draw(); 149 | } 150 | } 151 | export default Measurer; 152 | -------------------------------------------------------------------------------- /src/paint/Arrow.js: -------------------------------------------------------------------------------- 1 | import ArrowHead from './ArrowHead'; 2 | 3 | export class Arrow extends fabric.Group { 4 | constructor(point, options) { 5 | options = options || {}; 6 | options.strokeWidth = options.strokeWidth || 5; 7 | options.stroke = options.stroke || '#7db9e8'; 8 | options.class = 'arrow'; 9 | super( 10 | [], 11 | Object.assign(options, { 12 | evented: false 13 | }) 14 | ); 15 | this.pointArray = [point, Object.assign({}, point)]; 16 | this.options = options; 17 | this.draw(); 18 | } 19 | 20 | draw() { 21 | if (this.head) { 22 | this.remove(this.head); 23 | } 24 | 25 | if (this.polyline) { 26 | this.remove(this.polyline); 27 | } 28 | 29 | this.polyline = new fabric.Polyline( 30 | this.pointArray, 31 | Object.assign(this.options, { 32 | strokeLineJoin: 'round', 33 | fill: false 34 | }) 35 | ); 36 | 37 | this.addWithUpdate(this.polyline); 38 | 39 | const lastPoints = this.getLastPoints(); 40 | 41 | const p1 = new fabric.Point(lastPoints[0], lastPoints[1]); 42 | const p2 = new fabric.Point(lastPoints[2], lastPoints[3]); 43 | const dis = p1.distanceFrom(p2); 44 | console.log(`dis = ${dis}`); 45 | 46 | this.head = new ArrowHead( 47 | lastPoints, 48 | Object.assign(this.options, { 49 | headLength: this.strokeWidth * 2, 50 | lastAngle: dis <= 10 ? this.lastAngle : undefined 51 | }) 52 | ); 53 | 54 | if (dis > 10) { 55 | this.lastAngle = this.head.angle; 56 | } 57 | this.addWithUpdate(this.head); 58 | } 59 | 60 | addPoint(point) { 61 | this.pointArray.push(point); 62 | this.draw(); 63 | } 64 | 65 | addTempPoint(point) { 66 | const len = this.pointArray.length; 67 | const lastPoint = this.pointArray[len - 1]; 68 | lastPoint.x = point.x; 69 | lastPoint.y = point.y; 70 | this.draw(); 71 | } 72 | 73 | getLastPoints() { 74 | const len = this.pointArray.length; 75 | const point1 = this.pointArray[len - 2]; 76 | const point2 = this.pointArray[len - 1]; 77 | return [point1.x, point1.y, point2.x, point2.y]; 78 | } 79 | 80 | setColor(color) { 81 | this._objects.forEach(obj => { 82 | obj.setColor(color); 83 | }); 84 | } 85 | } 86 | 87 | export const arrow = (points, options) => new Arrow(points, options); 88 | 89 | export default Arrow; 90 | -------------------------------------------------------------------------------- /src/paint/ArrowHead.js: -------------------------------------------------------------------------------- 1 | class ArrowHead extends fabric.Triangle { 2 | constructor(points, options) { 3 | options = options || {}; 4 | options.headLength = options.headLength || 10; 5 | options.stroke = options.stroke || '#207cca'; 6 | 7 | const [x1, y1, x2, y2] = points; 8 | const dx = x2 - x1; 9 | const dy = y2 - y1; 10 | let angle = Math.atan2(dy, dx); 11 | 12 | angle *= 180 / Math.PI; 13 | angle += 90; 14 | 15 | if (options.lastAngle !== undefined) { 16 | angle = options.lastAngle; 17 | console.log(`Angle: ${angle}`); 18 | } 19 | 20 | super({ 21 | angle, 22 | fill: options.stroke, 23 | top: y2, 24 | left: x2, 25 | height: options.headLength, 26 | width: options.headLength, 27 | originX: 'center', 28 | originY: 'center', 29 | selectable: false 30 | }); 31 | } 32 | } 33 | export default ArrowHead; 34 | -------------------------------------------------------------------------------- /src/paint/Canvas.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import Base from '../core/Base'; 3 | import { Arrow } from './Arrow'; 4 | 5 | const Modes = { 6 | SELECT: 'select', 7 | DRAWING: 'drawing', 8 | ARROW: 'arrow', 9 | TEXT: 'text' 10 | }; 11 | 12 | export class Canvas extends Base { 13 | constructor(container, options) { 14 | super(options); 15 | 16 | this.container = container; 17 | 18 | const canvas = document.createElement('canvas'); 19 | this.container.appendChild(canvas); 20 | canvas.setAttribute('id', 'indoorjs-canvas'); 21 | 22 | canvas.width = this.width || this.container.clientWidth; 23 | canvas.height = this.height || this.container.clientHeight; 24 | 25 | this.currentColor = this.currentColor || 'black'; 26 | this.fontFamily = this.fontFamily || 'Roboto'; 27 | 28 | this.canvas = new fabric.Canvas(canvas, { 29 | freeDrawingCursor: 'none', 30 | freeDrawingLineWidth: this.lineWidth 31 | }); 32 | this.arrows = []; 33 | 34 | this.setLineWidth(this.lineWidth || 10); 35 | this.addCursor(); 36 | this.addListeners(); 37 | 38 | this.setModeAsArrow(); 39 | } 40 | 41 | setModeAsDrawing() { 42 | this.mode = Modes.DRAWING; 43 | this.canvas.isDrawingMode = true; 44 | this.canvas.selection = false; 45 | this.onModeChanged(); 46 | } 47 | 48 | isDrawingMode() { 49 | return this.mode === Modes.DRAWING; 50 | } 51 | 52 | setModeAsSelect() { 53 | this.mode = Modes.SELECT; 54 | this.canvas.isDrawingMode = false; 55 | this.canvas.selection = true; 56 | this.onModeChanged(); 57 | } 58 | 59 | isSelectMode() { 60 | return this.mode === Modes.SELECT; 61 | } 62 | 63 | setModeAsArrow() { 64 | this.mode = Modes.ARROW; 65 | this.canvas.isDrawingMode = false; 66 | this.canvas.selection = false; 67 | this.onModeChanged(); 68 | } 69 | 70 | isArrowMode() { 71 | return this.mode === Modes.ARROW; 72 | } 73 | 74 | setModeAsText() { 75 | this.mode = Modes.TEXT; 76 | this.canvas.isDrawingMode = false; 77 | this.canvas.selection = false; 78 | this.onModeChanged(); 79 | } 80 | 81 | isTextMode() { 82 | return this.mode === Modes.TEXT; 83 | } 84 | 85 | onModeChanged() { 86 | this.updateCursor(); 87 | this.emit('mode-changed', this.mode); 88 | this.canvas._objects.forEach(obj => { 89 | obj.evented = this.isSelectMode(); 90 | }); 91 | } 92 | 93 | addListeners() { 94 | const canvas = this.canvas; 95 | canvas.on('mouse:move', evt => { 96 | const mouse = canvas.getPointer(evt.e); 97 | if (this.mousecursor) { 98 | this.mousecursor 99 | .set({ 100 | top: mouse.y, 101 | left: mouse.x 102 | }) 103 | .setCoords() 104 | .canvas.renderAll(); 105 | } 106 | 107 | if (this.isTextMode()) { 108 | console.log('text'); 109 | } else if (this.isArrowMode()) { 110 | if (this.activeArrow) { 111 | this.activeArrow.addTempPoint(mouse); 112 | } 113 | this.canvas.requestRenderAll(); 114 | } 115 | }); 116 | 117 | canvas.on('mouse:out', () => { 118 | // put circle off screen 119 | if (!this.mousecursor) return; 120 | this.mousecursor 121 | .set({ 122 | left: -1000, 123 | top: -1000 124 | }) 125 | .setCoords(); 126 | 127 | this.cursor.renderAll(); 128 | }); 129 | 130 | canvas.on('mouse:up', event => { 131 | if (canvas.mouseDown) { 132 | canvas.fire('mouse:click', event); 133 | } 134 | canvas.mouseDown = false; 135 | }); 136 | 137 | canvas.on('mouse:move', event => { 138 | canvas.mouseDown = false; 139 | }); 140 | 141 | canvas.on('mouse:down', event => { 142 | canvas.mouseDown = true; 143 | }); 144 | 145 | canvas.on('mouse:click', event => { 146 | console.log('mouse click', event); 147 | const mouse = canvas.getPointer(event.e); 148 | if (event.target) return; 149 | if (this.isTextMode()) { 150 | const text = new fabric.IText('Text', { 151 | left: mouse.x, 152 | top: mouse.y, 153 | width: 100, 154 | fontSize: 20, 155 | fontFamily: this.fontFamily, 156 | lockUniScaling: true, 157 | fill: this.currentColor, 158 | stroke: this.currentColor 159 | }); 160 | canvas 161 | .add(text) 162 | .setActiveObject(text) 163 | .renderAll(); 164 | 165 | this.setModeAsSelect(); 166 | } else if (this.isArrowMode()) { 167 | console.log('arrow mode'); 168 | if (this.activeArrow) { 169 | this.activeArrow.addPoint(mouse); 170 | } else { 171 | this.activeArrow = new Arrow(mouse, { 172 | stroke: this.currentColor, 173 | strokeWidth: this.lineWidth 174 | }); 175 | this.canvas.add(this.activeArrow); 176 | } 177 | this.canvas.requestRenderAll(); 178 | } 179 | }); 180 | 181 | canvas.on('mouse:dblclick', event => { 182 | console.log('mouse:dbclick'); 183 | if (this.isArrowMode() && this.activeArrow) { 184 | this.arrows.push(this.activeArrow); 185 | this.activeArrow = null; 186 | } 187 | }); 188 | 189 | canvas.on('selection:created', event => { 190 | this.emit('selected'); 191 | }); 192 | 193 | canvas.on('selection:cleared', event => { 194 | this.emit('unselected'); 195 | }); 196 | } 197 | 198 | removeSelected() { 199 | this.canvas.remove(this.canvas.getActiveObject()); 200 | this.canvas.getActiveObjects().forEach(obj => { 201 | this.canvas.remove(obj); 202 | }); 203 | this.canvas.discardActiveObject().renderAll(); 204 | } 205 | 206 | updateCursor() { 207 | if (!this.cursor) return; 208 | 209 | const canvas = this.canvas; 210 | 211 | if (this.mousecursor) { 212 | this.cursor.remove(this.mousecursor); 213 | this.mousecursor = null; 214 | } 215 | 216 | const cursorOpacity = 0.3; 217 | let mousecursor = null; 218 | if (this.isDrawingMode()) { 219 | mousecursor = new fabric.Circle({ 220 | left: -1000, 221 | top: -1000, 222 | radius: canvas.freeDrawingBrush.width / 2, 223 | fill: `rgba(255,0,0,${cursorOpacity})`, 224 | stroke: 'black', 225 | originX: 'center', 226 | originY: 'center' 227 | }); 228 | } else if (this.isTextMode()) { 229 | mousecursor = new fabric.Path('M0,-10 V10', { 230 | left: -1000, 231 | top: -1000, 232 | radius: canvas.freeDrawingBrush.width / 2, 233 | fill: `rgba(255,0,0,${cursorOpacity})`, 234 | stroke: `rgba(0,0,0,${cursorOpacity})`, 235 | originX: 'center', 236 | originY: 'center', 237 | scaleX: 1, 238 | scaleY: 1 239 | }); 240 | } else { 241 | mousecursor = new fabric.Path('M0,-10 V10 M-10,0 H10', { 242 | left: -1000, 243 | top: -1000, 244 | radius: canvas.freeDrawingBrush.width / 2, 245 | fill: `rgba(255,0,0,${cursorOpacity})`, 246 | stroke: `rgba(0,0,0,${cursorOpacity})`, 247 | originX: 'center', 248 | originY: 'center' 249 | }); 250 | } 251 | 252 | if (this.isSelectMode()) { 253 | mousecursor = null; 254 | this.canvas.defaultCursor = 'default'; 255 | } else { 256 | this.canvas.defaultCursor = 'none'; 257 | } 258 | if (mousecursor) { 259 | this.cursor.add(mousecursor); 260 | } 261 | this.mousecursor = mousecursor; 262 | } 263 | 264 | addCursor() { 265 | const canvas = this.canvas; 266 | const cursorCanvas = document.createElement('canvas'); 267 | this.canvas.wrapperEl.appendChild(cursorCanvas); 268 | cursorCanvas.setAttribute('id', 'indoorjs-cursor-canvas'); 269 | cursorCanvas.style.position = 'absolute'; 270 | cursorCanvas.style.top = '0'; 271 | cursorCanvas.style.pointerEvents = 'none'; 272 | cursorCanvas.width = this.width || this.container.clientWidth; 273 | cursorCanvas.height = this.height || this.container.clientHeight; 274 | this.cursorCanvas = cursorCanvas; 275 | canvas.defaultCursor = 'none'; 276 | this.cursor = new fabric.StaticCanvas(cursorCanvas); 277 | this.updateCursor(); 278 | } 279 | 280 | setColor(color) { 281 | this.currentColor = color; 282 | this.canvas.freeDrawingBrush.color = color; 283 | 284 | const obj = this.canvas.getActiveObject(); 285 | if (obj) { 286 | obj.set('stroke', color); 287 | obj.set('fill', color); 288 | this.canvas.requestRenderAll(); 289 | } 290 | 291 | if (!this.mousecursor) return; 292 | 293 | this.mousecursor 294 | .set({ 295 | left: 100, 296 | top: 100, 297 | fill: color 298 | }) 299 | .setCoords() 300 | .canvas.renderAll(); 301 | } 302 | 303 | setLineWidth(width) { 304 | this.lineWidth = width; 305 | this.canvas.freeDrawingBrush.width = width; 306 | 307 | if (!this.mousecursor) return; 308 | 309 | this.mousecursor 310 | .set({ 311 | left: 100, 312 | top: 100, 313 | radius: width / 2 314 | }) 315 | .setCoords() 316 | .canvas.renderAll(); 317 | } 318 | 319 | setFontFamily(family) { 320 | this.fontFamily = family; 321 | const obj = this.canvas.getActiveObject(); 322 | if (obj && obj.type === 'i-text') { 323 | obj.set('fontFamily', family); 324 | this.canvas.requestRenderAll(); 325 | } 326 | } 327 | 328 | clear() { 329 | this.arrows = []; 330 | this.canvas.clear(); 331 | } 332 | } 333 | 334 | export const canvas = (container, options) => new Canvas(container, options); 335 | -------------------------------------------------------------------------------- /src/paint/index.js: -------------------------------------------------------------------------------- 1 | export * from './Canvas.js'; 2 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before */ 2 | 3 | import chai from 'chai'; 4 | import { fabric } from 'fabric'; 5 | import { Map, Marker } from '../lib/indoor.js'; 6 | 7 | window.fabric = fabric; 8 | 9 | chai.expect(); 10 | 11 | const expect = chai.expect; 12 | 13 | let lib; 14 | 15 | describe('Given an instance of my Cat library', () => { 16 | before(() => { 17 | lib = new Map(); 18 | }); 19 | describe('when I need the name', () => { 20 | it('should return the name', () => { 21 | expect(lib).to.have.property('canvas'); 22 | }); 23 | }); 24 | }); 25 | 26 | describe('Given an instance of my Dog library', () => { 27 | before(() => { 28 | lib = new Marker(); 29 | }); 30 | describe('when I need the name', () => { 31 | it('should return the name', () => { 32 | expect(lib.class).to.be.equal('marker'); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | watch: true, 6 | entry: { 7 | main: './dev/index.js', 8 | draw: './dev/draw.js' 9 | }, 10 | output: { 11 | path: path.join(__dirname, 'demo'), 12 | filename: '[name].js' 13 | }, 14 | devtool: 'eval', 15 | devServer: { 16 | contentBase: path.join(__dirname, 'demo'), 17 | port: 3300 18 | // host: '0.0.0.0', 19 | // open: true, 20 | // overlay: true 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | exclude: /node_modules/, 27 | use: { 28 | loader: 'babel-loader' 29 | } 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: ['style-loader', 'css-loader'] 34 | } 35 | ] 36 | }, 37 | // optimization: { 38 | // splitChunks: { 39 | // name: 'shared', 40 | // minChunks: 2 41 | // } 42 | // }, 43 | plugins: [ 44 | new HtmlWebpackPlugin({ 45 | hash: true, 46 | title: 'Dev', 47 | template: './dev/index.html', 48 | chunks: ['main'], 49 | path: path.join(__dirname, '../demo/'), 50 | filename: 'index.html' 51 | }), 52 | new HtmlWebpackPlugin({ 53 | hash: true, 54 | title: 'Demo drawing', 55 | template: './dev/draw.html', 56 | chunks: ['draw'], 57 | path: path.join(__dirname, '../demo/'), 58 | filename: 'draw.html' 59 | }) 60 | ] 61 | }; 62 | -------------------------------------------------------------------------------- /webpack.config.lib.js: -------------------------------------------------------------------------------- 1 | /* global __dirname, require, module */ 2 | 3 | const path = require('path'); 4 | const env = require('yargs').argv.env; // use --env with webpack 2 5 | 6 | const libraryName = 'Indoor'; 7 | 8 | let outputFile; 9 | let mode; 10 | 11 | if (env === 'build') { 12 | mode = 'production'; 13 | outputFile = `${libraryName}.min.js`; 14 | } else { 15 | mode = 'development'; 16 | outputFile = `${libraryName}.js`; 17 | } 18 | 19 | const config = { 20 | mode, 21 | entry: './src/Indoor.js', 22 | output: { 23 | path: path.resolve(__dirname, './lib'), 24 | filename: outputFile, 25 | library: libraryName, 26 | libraryTarget: 'umd', 27 | umdNamedDefine: true, 28 | globalObject: 'this' 29 | }, 30 | externals: { 31 | // fabric: 'fabric' 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.(js)$/, 37 | exclude: /(node_modules|bower_components)/, 38 | use: 'babel-loader' 39 | } 40 | ] 41 | } 42 | }; 43 | 44 | module.exports = config; 45 | --------------------------------------------------------------------------------