├── .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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 | [](http://github.com/badges/stability-badges)
3 | 
4 |
5 |
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 | 
12 |
13 | ## Usage
14 |
15 | [](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 |
--------------------------------------------------------------------------------