├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
├── assets
│ ├── index-38dce45a.css
│ └── index-df007825.js
├── favicon.ico
└── index.html
├── index.html
├── index.js
├── lib
├── svg-pan-zoom
│ ├── browserify.js
│ ├── control-icons.js
│ ├── shadow-viewport.js
│ ├── stand-alone.js
│ ├── svg-pan-zoom.js
│ ├── svg-utilities.js
│ ├── uniwheel.js
│ └── utilities.js
└── victor.js
├── package-lock.json
├── package.json
├── public
└── favicon.ico
├── src
├── components
│ ├── Edge.vue
│ ├── Group.vue
│ ├── Label.vue
│ ├── Marker.vue
│ ├── Node.vue
│ ├── Port.vue
│ └── Screen.vue
├── demo
│ ├── Benchmark.vue
│ ├── Demo.vue
│ ├── Edit.vue
│ ├── Groups.vue
│ ├── Labels.vue
│ ├── Markers.vue
│ ├── Ports.vue
│ ├── PortsPort.vue
│ ├── Sink.vue
│ ├── SinkSidebar.vue
│ └── Styles.vue
├── graph.js
├── main.js
├── mixins
│ └── drag.js
├── themes
│ └── cssutil.js
└── util.js
├── vite.config.js
└── vue.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 |
4 | # local env files
5 | .env.local
6 | .env.*.local
7 |
8 | # Log files
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 |
13 | # Editor directories and files
14 | .idea
15 | .vscode
16 | *.suo
17 | *.ntvs*
18 | *.njsproj
19 | *.sln
20 | *.sw?
21 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /docs
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | 2.0.0
3 | - New method of rendering nodes in a div layer instead of old foreignObject inside svg
4 | - Dropped vue 2 support
5 | - Improved ports offset calculation method
6 | - Fixed ports continuous updates by using a computed property instead to trigger updates
7 | - Allow middle click on nodes to go through
8 | - Other - default drag threshold reduced to 2
9 | - simplified nodes and groups structure (less divs)
10 | - removed margin from groups and nodes as its no longer needed
11 | - removed node text select as this can be achieved using CSS
12 | - fixed edge producing errors if assigned nodes cannot be found
13 |
14 | 1.4.0
15 | - Fix safari issues for initial node dimensions and styles demo
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a copy
2 | of this software and associated documentation files (the "Software"), to deal
3 | in the Software without restriction, including without limitation the rights
4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5 | copies of the Software, and to permit persons to whom the Software is
6 | furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all
9 | copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17 | SOFTWARE.
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vnodes
2 |
3 | Vue components to create svg interactive graphs, diagrams or node visual tools.
4 |
5 | ### Demo
6 |
7 | https://tiagolr.github.io/vnodes/
8 |
9 | ### Install
10 |
11 | ```bash
12 | npm install vnodes
13 | ```
14 |
15 | ## Vnodes 2.0 is here!
16 |
17 | With 2.0 the rendering method was changed, instead of having foreignObjects inside svg, the `screen` now uses different layers for svg and html while sinchronizing the transforms between them. This fixes issues with Safari that were partially patched by @metanas, the layout of nodes becomes simpler as there is no need to account for margins, clipping, lack of support of absolute positioning inside nodes or opacity in browsers based on WebKit.
18 |
19 | Markers were also revamped among other changes, see [CHANGELOG.md](./CHANGELOG.md) for more details.
20 |
21 | ### Get started
22 | ```html
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ```
40 |
41 | Previously all svg and html nodes were placed inside screen default slot, in 2.0 that changed and it uses different layers for different types like `#nodes` (html), `#edges` (svg) and `#overlay` (svg).
42 |
43 | The rest of the API remains the same but there were a few minor tweaks.
44 |
45 | ```js
46 | import { Screen, Node, Edge, graph } from 'vnodes'
47 | export default {
48 | components: {
49 | Screen,
50 | Node,
51 | Edge
52 | }
53 | data () {
54 | return {
55 | graph: new graph()
56 | }
57 | }
58 | created () {
59 | this.graph.createNode('a')
60 | this.graph.createNode('b')
61 | this.graph.createEdge('a', 'b')
62 | this.graph.graphNodes()
63 | }
64 | }
65 | ```
66 |
67 | ## Components
68 |
69 | ### Screen
70 |
71 | Main container of html and svg content, handles zoom panning and applies the same transforms to all its layers.
72 |
73 | ```html
74 |
75 |
76 |
77 |
78 |
79 | ```
80 |
81 | #### Screen Options
82 | Screen component uses [svg-pan-zoom](https://www.npmjs.com/package/svg-pan-zoom) under the hood
83 | and screen takes options prop like this
84 | ```html
85 |
86 |
87 |
88 |
89 |
90 | ```
91 | you can refer to available options [here](https://www.npmjs.com/package/svg-pan-zoom#how-to-use)
92 | ```javascript
93 | {
94 | viewportSelector: string,
95 | panEnabled: boolean,
96 | controlIconsEnabled: boolean,
97 | zoomEnabled: boolean,
98 | dblClickZoomEnabled: boolean,
99 | mouseWheelZoomEnabled: boolean,
100 | preventMouseEventsDefault: boolean,
101 | zoomScaleSensitivity: number,
102 | minZoom: number,
103 | maxZoom: number,
104 | fit: boolean,
105 | contain: boolean,
106 | center: boolean,
107 | refreshRate: 'auto',
108 | beforeZoom: function(){},
109 | onZoom: function(){},
110 | beforePan: function(){},
111 | onPan: function(){},
112 | onUpdatedCTM: function(){},
113 | customEventsHandler: {},
114 | eventsListenerElement: null
115 | }
116 | ```
117 |
118 | ### Node
119 |
120 | Div containers with handlers for data updates based on dimensions and positioning, also provides dragging by default which can be disabled.
121 |
122 | ```html
123 |
131 |
My First Node!
132 |
133 | ```
134 |
135 | ### Edge
136 |
137 | Connects nodes using svg paths
138 |
139 | ```html
140 |
144 | ```
145 |
146 | Edges require node references `{ from: id|Object, to: String|Object }`, if nodes are refered by `id(String)` an array `nodes` must be passed:
147 |
148 | ```html
149 |
152 |
153 | ```
154 |
155 | Edges can take **anchor** information to offset their position relative to a node,
156 |
157 | ```html
158 |
164 | ```
165 | anchors format can be:
166 |
167 | * String `'center', 'left', 'right', 'top', 'top-left', 'top-right', 'bottom', 'bottom-left', 'bottom-right', 'cirlce', 'rect'`
168 | * Object `{ x?:Number|String, y?: Number|String, align?: String, snap?: String }`
169 |
170 | Examples of valid anchors:
171 |
172 | ```js
173 | null
174 | { x: 0, y: 0}
175 | { x: 10, y: 10 }
176 | { x: '50%', '50%' }
177 | { x: '50%', '50%', snap: 'rect' }
178 | { align: 'bottom-right' }
179 | 'center'
180 | 'top-left'
181 | 'circle' // snaps offset to circle with radius node.width/2
182 | 'rect' // snaps offset to node rectangle
183 | ```
184 |
185 | ### Group
186 |
187 | Surrounds a group of nodes with a rectangle, allows dragging multiple nodes.
188 |
189 | ```html
190 |
191 |
Group Label
192 |
193 | ```
194 |
195 | ### Port
196 |
197 | Placed inside a node, automatically offsets edges to a their position inside the nodes.
198 |
199 | ### Label
200 |
201 | Create a label node that is positioned along an edge
202 |
203 | ```html
204 |
205 |
Content
206 |
207 | ```
208 |
209 | ### graph.js
210 |
211 | Can be used to store edges and nodes.
212 | Contains utility methods to build graphs, layouts, remove and create nodes, edges and so on.
213 |
214 | ## Styling
215 |
216 | The simplest way to style nodes and edges is using CSS
217 |
218 | ```css
219 |
231 | ```
232 |
233 | ### Markers
234 |
235 | There are two ways to create makers for eges, one is using [SVG markers](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker), creating definitions `` in `#edges` slot and then assign them to edges using CSS.
236 |
237 | In 2.0 the old markers helper was removed and a new `Marker.vue` component was added that provides embed svg markers which are more versatile than SVG markers, here is how it can be used:
238 |
239 | ```html
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | ```
250 |
251 | The marker can be any svg content, the component handles rotations and translations to the correct place along the edge given a percentage `perc` and the edge to place on.
252 | These markers are more versatile then defining them in svg using `` although probably more expensive in terms of computation.
253 | The svg content should be centered at the origin for the transforms to work properly, the `offset` property can be used to correct alignments.
254 |
--------------------------------------------------------------------------------
/docs/assets/index-38dce45a.css:
--------------------------------------------------------------------------------
1 | .screen{width:100%;height:100%;position:relative;overflow:hidden}.screen .edges,.screen .nodes,.screen .overlay{position:absolute;left:0;top:0;width:100%;height:100%}.screen .nodes,.screen .overlay,.screen .nodes .nodes-inner{pointer-events:none}.screen .nodes-inner *,.screen .overlay *{pointer-events:auto}.nodes-inner{transform-origin:top left;position:relative}.node{display:inline-flex;flex-direction:column;position:absolute;background-color:#64c864e6;border-radius:7px;user-select:none;-webkit-user-select:none}.node .default-label{font-weight:700;width:auto;height:auto;min-width:30px;min-height:30px;line-height:30px;padding:10px;text-align:center}.edge{stroke-width:4;stroke:green;fill:transparent}.node-group{position:absolute;display:inline-flex;border-radius:7px;background-color:#64646440;display:inline-block}.label{background-color:#bbe4bb}#edit-demo .CodeMirror{width:100%;height:500px;margin:0;overflow:hidden;position:relative;background-color:#f1f1f1;border:1px solid #f1f1f1}#edit-demo .node .content>div{padding:25px}#edit-demo .node .content h4,h5,p{margin:0}#edit-demo .node:hover .background{background-color:#5ac85a}#edit-demo .node.selected .content{background-color:#64c864;box-shadow:0 0 0 2px #333}#edit-demo .node .content,#edit-demo .edge{cursor:pointer}#edit-demo .edge:hover{stroke:#5ac85a}#edit-demo .edge.selected{stroke:#333}.port-inner[data-v-9cca1433]{width:15px;height:15px;border-radius:10px;background-color:#abc;display:inline-block;cursor:crosshair}.port-inner[data-v-9cca1433]:hover,.port-inner.connected[data-v-9cca1433]{background-color:#02db43}.node-header[data-v-9cca1433]{text-align:left;padding-left:10px;background-color:#28965f;border-radius:5px 5px 0 0;color:#fff}#ports-demo .node{background-color:#eee;box-shadow:2px 2px 2px 2px #64646480}#ports-demo .edge{stroke:#757575;stroke-linejoin:round;marker-start:none;marker-end:none;stroke-dasharray:5px 10px;stroke-dashoffset:1000;stroke-linecap:round;animation:dash 20s linear infinite}#benchmark-demo .node{background-color:#47696e;color:#fff}#benchmark-demo .node:hover{background-color:red}#benchmark-demo .edge{stroke:#ccc;stroke-width:4;marker-end:none}#benchmark-demo .edge:hover{stroke:red}.demo[data-v-d82b42a2] .v-codemirror .cm-gutters{display:none}@keyframes dash{to{stroke-dashoffset:0}}.checkboxes input[data-v-3f231c7d]{display:inline}.checkboxes label[data-v-3f231c7d]{display:inline;margin-right:10px}.group-label{font-weight:700;color:#fff;padding:10px;margin:0;background-color:#183e5280;border-top-left-radius:10px;border-top-right-radius:10px}#markers-demo .edge{stroke:var(--094c57ec)}#markers-demo .node{background:none}h2[data-v-0f69632e]{margin-top:50px;width:calc(100% - 225px)}h2 a[data-v-0f69632e]{color:#4a4a4a;font-size:.75em;float:right}.screen{background-color:#fff;border:1px solid #ccc}h2{font-size:1.5em}body{background-color:#f9f9f9;max-width:56em}.demo{display:flex}.viewport{height:500px;flex-grow:1}.sidebar{padding-left:20px;width:200px;max-width:200px;flex-shrink:0}@media only screen and (max-width: 1000px){.demo{flex-wrap:wrap}.viewport{width:100%}.sidebar{padding-left:0;display:flex;flex-wrap:wrap;min-width:100%;gap:10px;margin-top:20px}h2{width:100%!important}}
2 |
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiagolr/vnodes/7e6444e5cb5f5ad283b54457085fd6ad13f56923/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | vnodes
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | vnodes
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import graph from './src/graph'
2 | import Screen from './src/components/Screen.vue'
3 | import Node from './src/components/Node.vue'
4 | import Edge from './src/components/Edge.vue'
5 | import Group from './src/components/Group.vue'
6 | import Port from './src/components/Port.vue'
7 | import Label from './src/components/Label.vue'
8 | import Marker from './src/components/Marker.vue'
9 |
10 | export { graph }
11 | export { Screen }
12 | export { Node }
13 | export { Edge }
14 | export { Group }
15 | export { Port }
16 | export { Label }
17 | export { Marker }
18 | export default {
19 | graph,
20 | Screen,
21 | Node,
22 | Edge,
23 | Group,
24 | Port,
25 | Label,
26 | Marker
27 | }
--------------------------------------------------------------------------------
/lib/svg-pan-zoom/browserify.js:
--------------------------------------------------------------------------------
1 | import SvgPanZoom from './svg-pan-zoom.js'
2 |
3 | export default SvgPanZoom;
4 |
--------------------------------------------------------------------------------
/lib/svg-pan-zoom/control-icons.js:
--------------------------------------------------------------------------------
1 | import SvgUtils from './svg-utilities'
2 |
3 | export default {
4 | enable: function(instance) {
5 | // Select (and create if necessary) defs
6 | var defs = instance.svg.querySelector('defs')
7 | if (!defs) {
8 | defs = document.createElementNS(SvgUtils.svgNS, 'defs')
9 | instance.svg.appendChild(defs)
10 | }
11 |
12 | // Check for style element, and create it if it doesn't exist
13 | var styleEl = defs.querySelector('style#svg-pan-zoom-controls-styles');
14 | if (!styleEl) {
15 | var style = document.createElementNS(SvgUtils.svgNS, 'style')
16 | style.setAttribute('id', 'svg-pan-zoom-controls-styles')
17 | style.setAttribute('type', 'text/css')
18 | style.textContent = '.svg-pan-zoom-control { cursor: pointer; fill: black; fill-opacity: 0.333; } .svg-pan-zoom-control:hover { fill-opacity: 0.8; } .svg-pan-zoom-control-background { fill: white; fill-opacity: 0.5; } .svg-pan-zoom-control-background { fill-opacity: 0.8; }'
19 | defs.appendChild(style)
20 | }
21 |
22 | // Zoom Group
23 | var zoomGroup = document.createElementNS(SvgUtils.svgNS, 'g');
24 | zoomGroup.setAttribute('id', 'svg-pan-zoom-controls');
25 | zoomGroup.setAttribute('transform', 'translate(' + ( instance.width - 70 ) + ' ' + ( instance.height - 76 ) + ') scale(0.75)');
26 | zoomGroup.setAttribute('class', 'svg-pan-zoom-control');
27 |
28 | // Control elements
29 | zoomGroup.appendChild(this._createZoomIn(instance))
30 | zoomGroup.appendChild(this._createZoomReset(instance))
31 | zoomGroup.appendChild(this._createZoomOut(instance))
32 |
33 | // Finally append created element
34 | instance.svg.appendChild(zoomGroup)
35 |
36 | // Cache control instance
37 | instance.controlIcons = zoomGroup
38 | }
39 |
40 | , _createZoomIn: function(instance) {
41 | var zoomIn = document.createElementNS(SvgUtils.svgNS, 'g');
42 | zoomIn.setAttribute('id', 'svg-pan-zoom-zoom-in');
43 | zoomIn.setAttribute('transform', 'translate(30.5 5) scale(0.015)');
44 | zoomIn.setAttribute('class', 'svg-pan-zoom-control');
45 | zoomIn.addEventListener('click', function() {instance.getPublicInstance().zoomIn()}, false)
46 | zoomIn.addEventListener('touchstart', function() {instance.getPublicInstance().zoomIn()}, false)
47 |
48 | var zoomInBackground = document.createElementNS(SvgUtils.svgNS, 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier
49 | zoomInBackground.setAttribute('x', '0');
50 | zoomInBackground.setAttribute('y', '0');
51 | zoomInBackground.setAttribute('width', '1500'); // larger than expected because the whole group is transformed to scale down
52 | zoomInBackground.setAttribute('height', '1400');
53 | zoomInBackground.setAttribute('class', 'svg-pan-zoom-control-background');
54 | zoomIn.appendChild(zoomInBackground);
55 |
56 | var zoomInShape = document.createElementNS(SvgUtils.svgNS, 'path');
57 | zoomInShape.setAttribute('d', 'M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z');
58 | zoomInShape.setAttribute('class', 'svg-pan-zoom-control-element');
59 | zoomIn.appendChild(zoomInShape);
60 |
61 | return zoomIn
62 | }
63 |
64 | , _createZoomReset: function(instance){
65 | // reset
66 | var resetPanZoomControl = document.createElementNS(SvgUtils.svgNS, 'g');
67 | resetPanZoomControl.setAttribute('id', 'svg-pan-zoom-reset-pan-zoom');
68 | resetPanZoomControl.setAttribute('transform', 'translate(5 35) scale(0.4)');
69 | resetPanZoomControl.setAttribute('class', 'svg-pan-zoom-control');
70 | resetPanZoomControl.addEventListener('click', function() {instance.getPublicInstance().reset()}, false);
71 | resetPanZoomControl.addEventListener('touchstart', function() {instance.getPublicInstance().reset()}, false);
72 |
73 | var resetPanZoomControlBackground = document.createElementNS(SvgUtils.svgNS, 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier
74 | resetPanZoomControlBackground.setAttribute('x', '2');
75 | resetPanZoomControlBackground.setAttribute('y', '2');
76 | resetPanZoomControlBackground.setAttribute('width', '182'); // larger than expected because the whole group is transformed to scale down
77 | resetPanZoomControlBackground.setAttribute('height', '58');
78 | resetPanZoomControlBackground.setAttribute('class', 'svg-pan-zoom-control-background');
79 | resetPanZoomControl.appendChild(resetPanZoomControlBackground);
80 |
81 | var resetPanZoomControlShape1 = document.createElementNS(SvgUtils.svgNS, 'path');
82 | resetPanZoomControlShape1.setAttribute('d', 'M33.051,20.632c-0.742-0.406-1.854-0.609-3.338-0.609h-7.969v9.281h7.769c1.543,0,2.701-0.188,3.473-0.562c1.365-0.656,2.048-1.953,2.048-3.891C35.032,22.757,34.372,21.351,33.051,20.632z');
83 | resetPanZoomControlShape1.setAttribute('class', 'svg-pan-zoom-control-element');
84 | resetPanZoomControl.appendChild(resetPanZoomControlShape1);
85 |
86 | var resetPanZoomControlShape2 = document.createElementNS(SvgUtils.svgNS, 'path');
87 | resetPanZoomControlShape2.setAttribute('d', 'M170.231,0.5H15.847C7.102,0.5,0.5,5.708,0.5,11.84v38.861C0.5,56.833,7.102,61.5,15.847,61.5h154.384c8.745,0,15.269-4.667,15.269-10.798V11.84C185.5,5.708,178.976,0.5,170.231,0.5z M42.837,48.569h-7.969c-0.219-0.766-0.375-1.383-0.469-1.852c-0.188-0.969-0.289-1.961-0.305-2.977l-0.047-3.211c-0.03-2.203-0.41-3.672-1.142-4.406c-0.732-0.734-2.103-1.102-4.113-1.102h-7.05v13.547h-7.055V14.022h16.524c2.361,0.047,4.178,0.344,5.45,0.891c1.272,0.547,2.351,1.352,3.234,2.414c0.731,0.875,1.31,1.844,1.737,2.906s0.64,2.273,0.64,3.633c0,1.641-0.414,3.254-1.242,4.84s-2.195,2.707-4.102,3.363c1.594,0.641,2.723,1.551,3.387,2.73s0.996,2.98,0.996,5.402v2.32c0,1.578,0.063,2.648,0.19,3.211c0.19,0.891,0.635,1.547,1.333,1.969V48.569z M75.579,48.569h-26.18V14.022h25.336v6.117H56.454v7.336h16.781v6H56.454v8.883h19.125V48.569z M104.497,46.331c-2.44,2.086-5.887,3.129-10.34,3.129c-4.548,0-8.125-1.027-10.731-3.082s-3.909-4.879-3.909-8.473h6.891c0.224,1.578,0.662,2.758,1.316,3.539c1.196,1.422,3.246,2.133,6.15,2.133c1.739,0,3.151-0.188,4.236-0.562c2.058-0.719,3.087-2.055,3.087-4.008c0-1.141-0.504-2.023-1.512-2.648c-1.008-0.609-2.607-1.148-4.796-1.617l-3.74-0.82c-3.676-0.812-6.201-1.695-7.576-2.648c-2.328-1.594-3.492-4.086-3.492-7.477c0-3.094,1.139-5.664,3.417-7.711s5.623-3.07,10.036-3.07c3.685,0,6.829,0.965,9.431,2.895c2.602,1.93,3.966,4.73,4.093,8.402h-6.938c-0.128-2.078-1.057-3.555-2.787-4.43c-1.154-0.578-2.587-0.867-4.301-0.867c-1.907,0-3.428,0.375-4.565,1.125c-1.138,0.75-1.706,1.797-1.706,3.141c0,1.234,0.561,2.156,1.682,2.766c0.721,0.406,2.25,0.883,4.589,1.43l6.063,1.43c2.657,0.625,4.648,1.461,5.975,2.508c2.059,1.625,3.089,3.977,3.089,7.055C108.157,41.624,106.937,44.245,104.497,46.331z M139.61,48.569h-26.18V14.022h25.336v6.117h-18.281v7.336h16.781v6h-16.781v8.883h19.125V48.569z M170.337,20.14h-10.336v28.43h-7.266V20.14h-10.383v-6.117h27.984V20.14z');
88 | resetPanZoomControlShape2.setAttribute('class', 'svg-pan-zoom-control-element');
89 | resetPanZoomControl.appendChild(resetPanZoomControlShape2);
90 |
91 | return resetPanZoomControl
92 | }
93 |
94 | , _createZoomOut: function(instance){
95 | // zoom out
96 | var zoomOut = document.createElementNS(SvgUtils.svgNS, 'g');
97 | zoomOut.setAttribute('id', 'svg-pan-zoom-zoom-out');
98 | zoomOut.setAttribute('transform', 'translate(30.5 70) scale(0.015)');
99 | zoomOut.setAttribute('class', 'svg-pan-zoom-control');
100 | zoomOut.addEventListener('click', function() {instance.getPublicInstance().zoomOut()}, false);
101 | zoomOut.addEventListener('touchstart', function() {instance.getPublicInstance().zoomOut()}, false);
102 |
103 | var zoomOutBackground = document.createElementNS(SvgUtils.svgNS, 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier
104 | zoomOutBackground.setAttribute('x', '0');
105 | zoomOutBackground.setAttribute('y', '0');
106 | zoomOutBackground.setAttribute('width', '1500'); // larger than expected because the whole group is transformed to scale down
107 | zoomOutBackground.setAttribute('height', '1400');
108 | zoomOutBackground.setAttribute('class', 'svg-pan-zoom-control-background');
109 | zoomOut.appendChild(zoomOutBackground);
110 |
111 | var zoomOutShape = document.createElementNS(SvgUtils.svgNS, 'path');
112 | zoomOutShape.setAttribute('d', 'M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z');
113 | zoomOutShape.setAttribute('class', 'svg-pan-zoom-control-element');
114 | zoomOut.appendChild(zoomOutShape);
115 |
116 | return zoomOut
117 | }
118 |
119 | , disable: function(instance) {
120 | if (instance.controlIcons) {
121 | instance.controlIcons.parentNode.removeChild(instance.controlIcons)
122 | instance.controlIcons = null
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/lib/svg-pan-zoom/shadow-viewport.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import SvgUtils from './svg-utilities'
3 | import Utils from './utilities'
4 |
5 | var ShadowViewport = function(viewport, options){
6 | this.init(viewport, options)
7 | }
8 |
9 | /**
10 | * Initialization
11 | *
12 | * @param {SVGElement} viewport
13 | * @param {Object} options
14 | */
15 | ShadowViewport.prototype.init = function(viewport, options) {
16 | // DOM Elements
17 | this.viewport = viewport
18 | this.options = options
19 |
20 | // State cache
21 | this.originalState = {zoom: 1, x: 0, y: 0}
22 | this.activeState = {zoom: 1, x: 0, y: 0}
23 |
24 | this.updateCTMCached = Utils.proxy(this.updateCTM, this)
25 |
26 | // Create a custom requestAnimationFrame taking in account refreshRate
27 | this.requestAnimationFrame = Utils.createRequestAnimationFrame(this.options.refreshRate)
28 |
29 | // ViewBox
30 | this.viewBox = {x: 0, y: 0, width: 0, height: 0}
31 | this.cacheViewBox()
32 |
33 | // Process CTM
34 | var newCTM = this.processCTM()
35 |
36 | // Update viewport CTM and cache zoom and pan
37 | this.setCTM(newCTM)
38 |
39 | // Update CTM in this frame
40 | this.updateCTM()
41 | }
42 |
43 | /**
44 | * Cache initial viewBox value
45 | * If no viewBox is defined, then use viewport size/position instead for viewBox values
46 | */
47 | ShadowViewport.prototype.cacheViewBox = function() {
48 | var svgViewBox = this.options.svg.getAttribute('viewBox')
49 |
50 | if (svgViewBox) {
51 | var viewBoxValues = svgViewBox.split(/[\s\,]/).filter(function(v){return v}).map(parseFloat)
52 |
53 | // Cache viewbox x and y offset
54 | this.viewBox.x = viewBoxValues[0]
55 | this.viewBox.y = viewBoxValues[1]
56 | this.viewBox.width = viewBoxValues[2]
57 | this.viewBox.height = viewBoxValues[3]
58 |
59 | var zoom = Math.min(this.options.width / this.viewBox.width, this.options.height / this.viewBox.height)
60 |
61 | // Update active state
62 | this.activeState.zoom = zoom
63 | this.activeState.x = (this.options.width - this.viewBox.width * zoom) / 2
64 | this.activeState.y = (this.options.height - this.viewBox.height * zoom) / 2
65 |
66 | // Force updating CTM
67 | this.updateCTMOnNextFrame()
68 |
69 | this.options.svg.removeAttribute('viewBox')
70 | } else {
71 | this.simpleViewBoxCache()
72 | }
73 | }
74 |
75 | /**
76 | * Recalculate viewport sizes and update viewBox cache
77 | */
78 | ShadowViewport.prototype.simpleViewBoxCache = function() {
79 | var bBox = this.viewport.getBBox()
80 |
81 | this.viewBox.x = bBox.x
82 | this.viewBox.y = bBox.y
83 | this.viewBox.width = bBox.width
84 | this.viewBox.height = bBox.height
85 | }
86 |
87 | /**
88 | * Returns a viewbox object. Safe to alter
89 | *
90 | * @return {Object} viewbox object
91 | */
92 | ShadowViewport.prototype.getViewBox = function() {
93 | return Utils.extend({}, this.viewBox)
94 | }
95 |
96 | /**
97 | * Get initial zoom and pan values. Save them into originalState
98 | * Parses viewBox attribute to alter initial sizes
99 | *
100 | * @return {CTM} CTM object based on options
101 | */
102 | ShadowViewport.prototype.processCTM = function() {
103 | var newCTM = this.getCTM()
104 |
105 | if (this.options.fit || this.options.contain) {
106 | var newScale;
107 | if (this.options.fit) {
108 | newScale = Math.min(this.options.width/this.viewBox.width, this.options.height/this.viewBox.height);
109 | } else {
110 | newScale = Math.max(this.options.width/this.viewBox.width, this.options.height/this.viewBox.height);
111 | }
112 |
113 | newCTM.a = newScale; //x-scale
114 | newCTM.d = newScale; //y-scale
115 | newCTM.e = -this.viewBox.x * newScale; //x-transform
116 | newCTM.f = -this.viewBox.y * newScale; //y-transform
117 | }
118 |
119 | if (this.options.center) {
120 | var offsetX = (this.options.width - (this.viewBox.width + this.viewBox.x * 2) * newCTM.a) * 0.5
121 | , offsetY = (this.options.height - (this.viewBox.height + this.viewBox.y * 2) * newCTM.a) * 0.5
122 |
123 | newCTM.e = offsetX
124 | newCTM.f = offsetY
125 | }
126 |
127 | // Cache initial values. Based on activeState and fix+center opitons
128 | this.originalState.zoom = newCTM.a
129 | this.originalState.x = newCTM.e
130 | this.originalState.y = newCTM.f
131 |
132 | return newCTM
133 | }
134 |
135 | /**
136 | * Return originalState object. Safe to alter
137 | *
138 | * @return {Object}
139 | */
140 | ShadowViewport.prototype.getOriginalState = function() {
141 | return Utils.extend({}, this.originalState)
142 | }
143 |
144 | /**
145 | * Return actualState object. Safe to alter
146 | *
147 | * @return {Object}
148 | */
149 | ShadowViewport.prototype.getState = function() {
150 | return Utils.extend({}, this.activeState)
151 | }
152 |
153 | /**
154 | * Get zoom scale
155 | *
156 | * @return {Float} zoom scale
157 | */
158 | ShadowViewport.prototype.getZoom = function() {
159 | return this.activeState.zoom
160 | }
161 |
162 | /**
163 | * Get zoom scale for pubilc usage
164 | *
165 | * @return {Float} zoom scale
166 | */
167 | ShadowViewport.prototype.getRelativeZoom = function() {
168 | return this.activeState.zoom / this.originalState.zoom
169 | }
170 |
171 | /**
172 | * Compute zoom scale for pubilc usage
173 | *
174 | * @return {Float} zoom scale
175 | */
176 | ShadowViewport.prototype.computeRelativeZoom = function(scale) {
177 | return scale / this.originalState.zoom
178 | }
179 |
180 | /**
181 | * Get pan
182 | *
183 | * @return {Object}
184 | */
185 | ShadowViewport.prototype.getPan = function() {
186 | return {x: this.activeState.x, y: this.activeState.y}
187 | }
188 |
189 | /**
190 | * Return cached viewport CTM value that can be safely modified
191 | *
192 | * @return {SVGMatrix}
193 | */
194 | ShadowViewport.prototype.getCTM = function() {
195 | var safeCTM = this.options.svg.createSVGMatrix()
196 |
197 | // Copy values manually as in FF they are not itterable
198 | safeCTM.a = this.activeState.zoom
199 | safeCTM.b = 0
200 | safeCTM.c = 0
201 | safeCTM.d = this.activeState.zoom
202 | safeCTM.e = this.activeState.x
203 | safeCTM.f = this.activeState.y
204 |
205 | return safeCTM
206 | }
207 |
208 | /**
209 | * Set a new CTM
210 | *
211 | * @param {SVGMatrix} newCTM
212 | */
213 | ShadowViewport.prototype.setCTM = function(newCTM) {
214 | var willZoom = this.isZoomDifferent(newCTM)
215 | , willPan = this.isPanDifferent(newCTM)
216 |
217 | if (willZoom || willPan) {
218 | // Before zoom
219 | if (willZoom) {
220 | // If returns false then cancel zooming
221 | if (this.options.beforeZoom(this.getRelativeZoom(), this.computeRelativeZoom(newCTM.a)) === false) {
222 | newCTM.a = newCTM.d = this.activeState.zoom
223 | willZoom = false
224 | } else {
225 | this.updateCache(newCTM);
226 | this.options.onZoom(this.getRelativeZoom())
227 | }
228 | }
229 |
230 | // Before pan
231 | if (willPan) {
232 | var preventPan = this.options.beforePan(this.getPan(), {x: newCTM.e, y: newCTM.f})
233 | // If prevent pan is an object
234 | , preventPanX = false
235 | , preventPanY = false
236 |
237 | // If prevent pan is Boolean false
238 | if (preventPan === false) {
239 | // Set x and y same as before
240 | newCTM.e = this.getPan().x
241 | newCTM.f = this.getPan().y
242 |
243 | preventPanX = preventPanY = true
244 | } else if (Utils.isObject(preventPan)) {
245 | // Check for X axes attribute
246 | if (preventPan.x === false) {
247 | // Prevent panning on x axes
248 | newCTM.e = this.getPan().x
249 | preventPanX = true
250 | } else if (Utils.isNumber(preventPan.x)) {
251 | // Set a custom pan value
252 | newCTM.e = preventPan.x
253 | }
254 |
255 | // Check for Y axes attribute
256 | if (preventPan.y === false) {
257 | // Prevent panning on x axes
258 | newCTM.f = this.getPan().y
259 | preventPanY = true
260 | } else if (Utils.isNumber(preventPan.y)) {
261 | // Set a custom pan value
262 | newCTM.f = preventPan.y
263 | }
264 | }
265 |
266 | // Update willPan flag
267 | // Check if newCTM is still different
268 | if ((preventPanX && preventPanY) || !this.isPanDifferent(newCTM)) {
269 | willPan = false
270 | } else {
271 | this.updateCache(newCTM);
272 | this.options.onPan(this.getPan());
273 | }
274 | }
275 |
276 | // Check again if should zoom or pan
277 | if (willZoom || willPan) {
278 | this.updateCTMOnNextFrame()
279 | }
280 | }
281 | }
282 |
283 | ShadowViewport.prototype.isZoomDifferent = function(newCTM) {
284 | return this.activeState.zoom !== newCTM.a
285 | }
286 |
287 | ShadowViewport.prototype.isPanDifferent = function(newCTM) {
288 | return this.activeState.x !== newCTM.e || this.activeState.y !== newCTM.f
289 | }
290 |
291 |
292 | /**
293 | * Update cached CTM and active state
294 | *
295 | * @param {SVGMatrix} newCTM
296 | */
297 | ShadowViewport.prototype.updateCache = function(newCTM) {
298 | this.activeState.zoom = newCTM.a
299 | this.activeState.x = newCTM.e
300 | this.activeState.y = newCTM.f
301 | }
302 |
303 | ShadowViewport.prototype.pendingUpdate = false
304 |
305 | /**
306 | * Place a request to update CTM on next Frame
307 | */
308 | ShadowViewport.prototype.updateCTMOnNextFrame = function() {
309 | if (!this.pendingUpdate) {
310 | // Lock
311 | this.pendingUpdate = true
312 |
313 | // Throttle next update
314 | this.requestAnimationFrame.call(window, this.updateCTMCached)
315 | }
316 | }
317 |
318 | /**
319 | * Update viewport CTM with cached CTM
320 | */
321 | ShadowViewport.prototype.updateCTM = function() {
322 | var ctm = this.getCTM()
323 |
324 | // Updates SVG element
325 | SvgUtils.setCTM(this.viewport, ctm, this.defs)
326 |
327 | // Free the lock
328 | this.pendingUpdate = false
329 |
330 | // Notify about the update
331 | if(this.options.onUpdatedCTM) {
332 | this.options.onUpdatedCTM(ctm)
333 | }
334 | }
335 |
336 | export default function(viewport, options){
337 | return new ShadowViewport(viewport, options)
338 | }
339 |
--------------------------------------------------------------------------------
/lib/svg-pan-zoom/stand-alone.js:
--------------------------------------------------------------------------------
1 | import svgPanZoom from './svg-pan-zoom.js'
2 |
3 | // UMD module definition
4 | (function(window, document){
5 | // AMD
6 | if (typeof define === 'function' && define.amd) {
7 | define('svg-pan-zoom', function () {
8 | return svgPanZoom;
9 | });
10 | // CMD
11 | } else if (typeof module !== 'undefined' && module.exports) {
12 | module.exports = svgPanZoom;
13 |
14 | // Browser
15 | // Keep exporting globally as module.exports is available because of browserify
16 | window.svgPanZoom = svgPanZoom;
17 | }
18 | })(window, document)
19 |
--------------------------------------------------------------------------------
/lib/svg-pan-zoom/svg-pan-zoom.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import Wheel from './uniwheel'
3 | import ControlIcons from './control-icons'
4 | import Utils from './utilities'
5 | import SvgUtils from './svg-utilities'
6 | import ShadowViewport from './shadow-viewport'
7 |
8 | var SvgPanZoom = function(svg, options) {
9 | this.init(svg, options)
10 | }
11 |
12 | var optionsDefaults = {
13 | viewportSelector: '.svg-pan-zoom_viewport' // Viewport selector. Can be querySelector string or SVGElement
14 | , panEnabled: true // enable or disable panning (default enabled)
15 | , controlIconsEnabled: false // insert icons to give user an option in addition to mouse events to control pan/zoom (default disabled)
16 | , zoomEnabled: true // enable or disable zooming (default enabled)
17 | , dblClickZoomEnabled: true // enable or disable zooming by double clicking (default enabled)
18 | , mouseWheelZoomEnabled: true // enable or disable zooming by mouse wheel (default enabled)
19 | , preventMouseEventsDefault: true // enable or disable preventDefault for mouse events
20 | , zoomScaleSensitivity: 0.1 // Zoom sensitivity
21 | , minZoom: 0.5 // Minimum Zoom level
22 | , maxZoom: 10 // Maximum Zoom level
23 | , fit: true // enable or disable viewport fit in SVG (default true)
24 | , contain: false // enable or disable viewport contain the svg (default false)
25 | , center: true // enable or disable viewport centering in SVG (default true)
26 | , refreshRate: 'auto' // Maximum number of frames per second (altering SVG's viewport)
27 | , beforeZoom: null
28 | , onZoom: null
29 | , beforePan: null
30 | , onPan: null
31 | , onUserPan: null // custom event, return false to cancel
32 | , onUserZoom: null // custom event, return false to cancel
33 | , onDoubleClick: null // custom event
34 | , customEventsHandler: null
35 | , eventsListenerElement: null
36 | , onUpdatedCTM: null
37 | }
38 |
39 | var passiveListenerOption = {passive: true};
40 |
41 | SvgPanZoom.prototype.init = function(svg, options) {
42 | var that = this
43 |
44 | this.svg = svg
45 | this.defs = svg.querySelector('defs')
46 |
47 | // Add default attributes to SVG
48 | SvgUtils.setupSvgAttributes(this.svg)
49 |
50 | // Set options
51 | this.options = Utils.extend(Utils.extend({}, optionsDefaults), options)
52 |
53 | // Set default state
54 | this.state = 'none'
55 |
56 | // Get dimensions
57 | var boundingClientRectNormalized = SvgUtils.getBoundingClientRectNormalized(svg)
58 | this.width = boundingClientRectNormalized.width
59 | this.height = boundingClientRectNormalized.height
60 |
61 | // Init shadow viewport
62 | this.viewport = ShadowViewport(SvgUtils.getOrCreateViewport(this.svg, this.options.viewportSelector), {
63 | svg: this.svg
64 | , width: this.width
65 | , height: this.height
66 | , fit: this.options.fit
67 | , contain: this.options.contain
68 | , center: this.options.center
69 | , refreshRate: this.options.refreshRate
70 | // Put callbacks into functions as they can change through time
71 | , beforeZoom: function(oldScale, newScale) {
72 | if (that.viewport && that.options.beforeZoom) {return that.options.beforeZoom(oldScale, newScale)}
73 | }
74 | , onZoom: function(scale) {
75 | if (that.viewport && that.options.onZoom) {return that.options.onZoom(scale)}
76 | }
77 | , beforePan: function(oldPoint, newPoint) {
78 | if (that.viewport && that.options.beforePan) {return that.options.beforePan(oldPoint, newPoint)}
79 | }
80 | , onPan: function(point) {
81 | if (that.viewport && that.options.onPan) {return that.options.onPan(point)}
82 | }
83 | , onUpdatedCTM: function(ctm) {
84 | if (that.viewport && that.options.onUpdatedCTM) {return that.options.onUpdatedCTM(ctm)}
85 | }
86 | })
87 |
88 | // Wrap callbacks into public API context
89 | var publicInstance = this.getPublicInstance()
90 | publicInstance.setBeforeZoom(this.options.beforeZoom)
91 | publicInstance.setOnZoom(this.options.onZoom)
92 | publicInstance.setBeforePan(this.options.beforePan)
93 | publicInstance.setOnPan(this.options.onPan)
94 | publicInstance.setOnUpdatedCTM(this.options.onUpdatedCTM)
95 |
96 | if (this.options.controlIconsEnabled) {
97 | ControlIcons.enable(this)
98 | }
99 |
100 | // Init events handlers
101 | this.lastMouseWheelEventTime = Date.now()
102 | this.setupHandlers()
103 | }
104 |
105 | /**
106 | * Register event handlers
107 | */
108 | SvgPanZoom.prototype.setupHandlers = function() {
109 | var that = this
110 | , prevEvt = null // use for touchstart event to detect double tap
111 | ;
112 |
113 | this.eventListeners = {
114 | // Mouse down group
115 | mousedown: function(evt) {
116 | var result = that.handleMouseDown(evt, prevEvt);
117 | prevEvt = evt
118 | return result;
119 | }
120 | , touchstart: function(evt) {
121 | var result = that.handleMouseDown(evt, prevEvt);
122 | prevEvt = evt
123 | return result;
124 | }
125 |
126 | // Mouse up group
127 | , mouseup: function(evt) {
128 | return that.handleMouseUp(evt);
129 | }
130 | , touchend: function(evt) {
131 | return that.handleMouseUp(evt);
132 | }
133 |
134 | // Mouse move group
135 | , mousemove: function(evt) {
136 | return that.handleMouseMove(evt);
137 | }
138 | , touchmove: function(evt) {
139 | return that.handleMouseMove(evt);
140 | }
141 |
142 | // Mouse leave group
143 | , mouseleave: function(evt) {
144 | return that.handleMouseUp(evt);
145 | }
146 | , touchleave: function(evt) {
147 | return that.handleMouseUp(evt);
148 | }
149 | , touchcancel: function(evt) {
150 | return that.handleMouseUp(evt);
151 | }
152 | }
153 |
154 | // Init custom events handler if available
155 | if (this.options.customEventsHandler != null) { // jshint ignore:line
156 | this.options.customEventsHandler.init({
157 | svgElement: this.svg
158 | , eventsListenerElement: this.options.eventsListenerElement
159 | , instance: this.getPublicInstance()
160 | })
161 |
162 | // Custom event handler may halt builtin listeners
163 | var haltEventListeners = this.options.customEventsHandler.haltEventListeners
164 | if (haltEventListeners && haltEventListeners.length) {
165 | for (var i = haltEventListeners.length - 1; i >= 0; i--) {
166 | if (this.eventListeners.hasOwnProperty(haltEventListeners[i])) {
167 | delete this.eventListeners[haltEventListeners[i]]
168 | }
169 | }
170 | }
171 | }
172 |
173 | // Bind eventListeners
174 | for (var event in this.eventListeners) {
175 | // Attach event to eventsListenerElement or SVG if not available
176 | (this.options.eventsListenerElement || this.svg)
177 | .addEventListener(event, this.eventListeners[event], !this.options.preventMouseEventsDefault ? passiveListenerOption : false)
178 | }
179 |
180 | // Zoom using mouse wheel
181 | if (this.options.mouseWheelZoomEnabled) {
182 | this.options.mouseWheelZoomEnabled = false // set to false as enable will set it back to true
183 | this.enableMouseWheelZoom()
184 | }
185 | }
186 |
187 | /**
188 | * Enable ability to zoom using mouse wheel
189 | */
190 | SvgPanZoom.prototype.enableMouseWheelZoom = function() {
191 | if (!this.options.mouseWheelZoomEnabled) {
192 | var that = this
193 |
194 | // Mouse wheel listener
195 | this.wheelListener = function(evt) {
196 | return that.handleMouseWheel(evt);
197 | }
198 |
199 | // Bind wheelListener
200 | var isPassiveListener = !this.options.preventMouseEventsDefault
201 | Wheel.on(this.options.eventsListenerElement || this.svg, this.wheelListener, isPassiveListener)
202 |
203 | this.options.mouseWheelZoomEnabled = true
204 | }
205 | }
206 |
207 | /**
208 | * Disable ability to zoom using mouse wheel
209 | */
210 | SvgPanZoom.prototype.disableMouseWheelZoom = function() {
211 | if (this.options.mouseWheelZoomEnabled) {
212 | var isPassiveListener = !this.options.preventMouseEventsDefault
213 | Wheel.off(this.options.eventsListenerElement || this.svg, this.wheelListener, isPassiveListener)
214 | this.options.mouseWheelZoomEnabled = false
215 | }
216 | }
217 |
218 | /**
219 | * Handle mouse wheel event
220 | *
221 | * @param {Event} evt
222 | */
223 | SvgPanZoom.prototype.handleMouseWheel = function(evt) {
224 | if (!this.options.zoomEnabled || this.state !== 'none') {
225 | return;
226 | }
227 |
228 | if (this.options.preventMouseEventsDefault){
229 | if (evt.preventDefault) {
230 | evt.preventDefault();
231 | } else {
232 | evt.returnValue = false;
233 | }
234 | }
235 |
236 | // custom evt
237 | if (this.options.onUserZoom) {
238 | if (this.options.onUserZoom(evt) === false) {
239 | return
240 | }
241 | }
242 |
243 | // Default delta in case that deltaY is not available
244 | var delta = evt.deltaY || 1
245 | , timeDelta = Date.now() - this.lastMouseWheelEventTime
246 | , divider = 3 + Math.max(0, 30 - timeDelta)
247 |
248 | // Update cache
249 | this.lastMouseWheelEventTime = Date.now()
250 |
251 | // Make empirical adjustments for browsers that give deltaY in pixels (deltaMode=0)
252 | if ('deltaMode' in evt && evt.deltaMode === 0 && evt.wheelDelta) {
253 | delta = evt.deltaY === 0 ? 0 : Math.abs(evt.wheelDelta) / evt.deltaY
254 | }
255 |
256 | delta = -0.3 < delta && delta < 0.3 ? delta : (delta > 0 ? 1 : -1) * Math.log(Math.abs(delta) + 10) / divider
257 |
258 | var inversedScreenCTM = this.svg.getScreenCTM().inverse()
259 | , relativeMousePoint = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(inversedScreenCTM)
260 | , zoom = Math.pow(1 + this.options.zoomScaleSensitivity, (-1) * delta); // multiplying by neg. 1 so as to make zoom in/out behavior match Google maps behavior
261 |
262 | this.zoomAtPoint(zoom, relativeMousePoint)
263 | }
264 |
265 | /**
266 | * Zoom in at a SVG point
267 | *
268 | * @param {SVGPoint} point
269 | * @param {Float} zoomScale Number representing how much to zoom
270 | * @param {Boolean} zoomAbsolute Default false. If true, zoomScale is treated as an absolute value.
271 | * Otherwise, zoomScale is treated as a multiplied (e.g. 1.10 would zoom in 10%)
272 | */
273 | SvgPanZoom.prototype.zoomAtPoint = function(zoomScale, point, zoomAbsolute) {
274 | var originalState = this.viewport.getOriginalState()
275 |
276 | if (!zoomAbsolute) {
277 | // Fit zoomScale in set bounds
278 | if (this.getZoom() * zoomScale < this.options.minZoom * originalState.zoom) {
279 | zoomScale = (this.options.minZoom * originalState.zoom) / this.getZoom()
280 | } else if (this.getZoom() * zoomScale > this.options.maxZoom * originalState.zoom) {
281 | zoomScale = (this.options.maxZoom * originalState.zoom) / this.getZoom()
282 | }
283 | } else {
284 | // Fit zoomScale in set bounds
285 | zoomScale = Math.max(this.options.minZoom * originalState.zoom, Math.min(this.options.maxZoom * originalState.zoom, zoomScale))
286 | // Find relative scale to achieve desired scale
287 | zoomScale = zoomScale/this.getZoom()
288 | }
289 |
290 | var oldCTM = this.viewport.getCTM()
291 | , relativePoint = point.matrixTransform(oldCTM.inverse())
292 | , modifier = this.svg.createSVGMatrix().translate(relativePoint.x, relativePoint.y).scale(zoomScale).translate(-relativePoint.x, -relativePoint.y)
293 | , newCTM = oldCTM.multiply(modifier)
294 |
295 | if (newCTM.a !== oldCTM.a) {
296 | this.viewport.setCTM(newCTM)
297 | }
298 | }
299 |
300 | /**
301 | * Zoom at center point
302 | *
303 | * @param {Float} scale
304 | * @param {Boolean} absolute Marks zoom scale as relative or absolute
305 | */
306 | SvgPanZoom.prototype.zoom = function(scale, absolute) {
307 | this.zoomAtPoint(scale, SvgUtils.getSvgCenterPoint(this.svg, this.width, this.height), absolute)
308 | }
309 |
310 | /**
311 | * Zoom used by public instance
312 | *
313 | * @param {Float} scale
314 | * @param {Boolean} absolute Marks zoom scale as relative or absolute
315 | */
316 | SvgPanZoom.prototype.publicZoom = function(scale, absolute) {
317 | if (absolute) {
318 | scale = this.computeFromRelativeZoom(scale)
319 | }
320 |
321 | this.zoom(scale, absolute)
322 | }
323 |
324 | /**
325 | * Zoom at point used by public instance
326 | *
327 | * @param {Float} scale
328 | * @param {SVGPoint|Object} point An object that has x and y attributes
329 | * @param {Boolean} absolute Marks zoom scale as relative or absolute
330 | */
331 | SvgPanZoom.prototype.publicZoomAtPoint = function(scale, point, absolute) {
332 | if (absolute) {
333 | // Transform zoom into a relative value
334 | scale = this.computeFromRelativeZoom(scale)
335 | }
336 |
337 | // If not a SVGPoint but has x and y then create a SVGPoint
338 | if (Utils.getType(point) !== 'SVGPoint') {
339 | if('x' in point && 'y' in point) {
340 | point = SvgUtils.createSVGPoint(this.svg, point.x, point.y)
341 | } else {
342 | throw new Error('Given point is invalid')
343 | }
344 | }
345 |
346 | this.zoomAtPoint(scale, point, absolute)
347 | }
348 |
349 | /**
350 | * Get zoom scale
351 | *
352 | * @return {Float} zoom scale
353 | */
354 | SvgPanZoom.prototype.getZoom = function() {
355 | return this.viewport.getZoom()
356 | }
357 |
358 | /**
359 | * Get zoom scale for public usage
360 | *
361 | * @return {Float} zoom scale
362 | */
363 | SvgPanZoom.prototype.getRelativeZoom = function() {
364 | return this.viewport.getRelativeZoom()
365 | }
366 |
367 | /**
368 | * Compute actual zoom from public zoom
369 | *
370 | * @param {Float} zoom
371 | * @return {Float} zoom scale
372 | */
373 | SvgPanZoom.prototype.computeFromRelativeZoom = function(zoom) {
374 | return zoom * this.viewport.getOriginalState().zoom
375 | }
376 |
377 | /**
378 | * Set zoom to initial state
379 | */
380 | SvgPanZoom.prototype.resetZoom = function() {
381 | var originalState = this.viewport.getOriginalState()
382 |
383 | this.zoom(originalState.zoom, true);
384 | }
385 |
386 | /**
387 | * Set pan to initial state
388 | */
389 | SvgPanZoom.prototype.resetPan = function() {
390 | this.pan(this.viewport.getOriginalState());
391 | }
392 |
393 | /**
394 | * Set pan and zoom to initial state
395 | */
396 | SvgPanZoom.prototype.reset = function() {
397 | this.resetZoom()
398 | this.resetPan()
399 | }
400 |
401 | /**
402 | * Handle double click event
403 | * See handleMouseDown() for alternate detection method
404 | *
405 | * @param {Event} evt
406 | */
407 | SvgPanZoom.prototype.handleDblClick = function(evt) {
408 | if (this.options.preventMouseEventsDefault) {
409 | if (evt.preventDefault) {
410 | evt.preventDefault()
411 | } else {
412 | evt.returnValue = false
413 | }
414 | }
415 |
416 | // Check if target was a control button
417 | if (this.options.controlIconsEnabled) {
418 | var targetClass = evt.target.getAttribute('class') || ''
419 | if (targetClass.indexOf('svg-pan-zoom-control') > -1) {
420 | return false
421 | }
422 | }
423 |
424 | var zoomFactor
425 |
426 | if (evt.shiftKey) {
427 | zoomFactor = 1/((1 + this.options.zoomScaleSensitivity) * 2) // zoom out when shift key pressed
428 | } else {
429 | zoomFactor = (1 + this.options.zoomScaleSensitivity) * 2
430 | }
431 |
432 | var point = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.svg.getScreenCTM().inverse())
433 | this.zoomAtPoint(zoomFactor, point)
434 | }
435 |
436 | /**
437 | * Handle click event
438 | *
439 | * @param {Event} evt
440 | */
441 | SvgPanZoom.prototype.handleMouseDown = function(evt, prevEvt) {
442 | if (this.options.preventMouseEventsDefault) {
443 | if (evt.preventDefault) {
444 | evt.preventDefault()
445 | } else {
446 | evt.returnValue = false
447 | }
448 | }
449 |
450 | Utils.mouseAndTouchNormalize(evt, this.svg)
451 | const isDoubleClick = Utils.isDblClick(evt, prevEvt)
452 | // Double click detection; more consistent than ondblclick
453 | if (this.options.onDoubleClick && isDoubleClick) {
454 | this.options.onDoubleClick()
455 | }
456 | if (this.options.dblClickZoomEnabled && isDoubleClick){
457 | this.handleDblClick(evt)
458 | } else {
459 | // Pan mode
460 | this.state = 'pan'
461 | this.firstEventCTM = this.viewport.getCTM()
462 | this.stateOrigin = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.firstEventCTM.inverse())
463 | }
464 | }
465 |
466 | /**
467 | * Handle mouse move event
468 | *
469 | * @param {Event} evt
470 | */
471 | SvgPanZoom.prototype.handleMouseMove = function(evt) {
472 | if (this.options.preventMouseEventsDefault) {
473 | if (evt.preventDefault) {
474 | evt.preventDefault()
475 | } else {
476 | evt.returnValue = false
477 | }
478 | }
479 |
480 | if (this.state === 'pan' && this.options.panEnabled) {
481 | if (this.options.onUserPan) {
482 | if (this.options.onUserPan(evt) === false) {
483 | return
484 | }
485 | }
486 | var point = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.firstEventCTM.inverse())
487 | , viewportCTM = this.firstEventCTM.translate(point.x - this.stateOrigin.x, point.y - this.stateOrigin.y)
488 |
489 | this.viewport.setCTM(viewportCTM)
490 | }
491 | }
492 |
493 | /**
494 | * Handle mouse button release event
495 | *
496 | * @param {Event} evt
497 | */
498 | SvgPanZoom.prototype.handleMouseUp = function(evt) {
499 | if (this.options.preventMouseEventsDefault) {
500 | if (evt.preventDefault) {
501 | evt.preventDefault()
502 | } else {
503 | evt.returnValue = false
504 | }
505 | }
506 |
507 | if (this.state === 'pan') {
508 | // Quit pan mode
509 | this.state = 'none'
510 | }
511 | }
512 |
513 | /**
514 | * Adjust viewport size (only) so it will fit in SVG
515 | * Does not center image
516 | */
517 | SvgPanZoom.prototype.fit = function() {
518 | var viewBox = this.viewport.getViewBox()
519 | , newScale = Math.min(this.width/viewBox.width, this.height/viewBox.height)
520 |
521 | this.zoom(newScale, true)
522 | }
523 |
524 | /**
525 | * Adjust viewport size (only) so it will contain the SVG
526 | * Does not center image
527 | */
528 | SvgPanZoom.prototype.contain = function() {
529 | var viewBox = this.viewport.getViewBox()
530 | , newScale = Math.max(this.width/viewBox.width, this.height/viewBox.height)
531 |
532 | this.zoom(newScale, true)
533 | }
534 |
535 | /**
536 | * Adjust viewport pan (only) so it will be centered in SVG
537 | * Does not zoom/fit/contain image
538 | */
539 | SvgPanZoom.prototype.center = function() {
540 | var viewBox = this.viewport.getViewBox()
541 | , offsetX = (this.width - (viewBox.width + viewBox.x * 2) * this.getZoom()) * 0.5
542 | , offsetY = (this.height - (viewBox.height + viewBox.y * 2) * this.getZoom()) * 0.5
543 |
544 | this.getPublicInstance().pan({x: offsetX, y: offsetY})
545 | }
546 |
547 | /**
548 | * Update content cached BorderBox
549 | * Use when viewport contents change
550 | */
551 | SvgPanZoom.prototype.updateBBox = function() {
552 | this.viewport.simpleViewBoxCache()
553 | }
554 |
555 | /**
556 | * Pan to a rendered position
557 | *
558 | * @param {Object} point {x: 0, y: 0}
559 | */
560 | SvgPanZoom.prototype.pan = function(point) {
561 | var viewportCTM = this.viewport.getCTM()
562 | viewportCTM.e = point.x
563 | viewportCTM.f = point.y
564 | this.viewport.setCTM(viewportCTM)
565 | }
566 |
567 | /**
568 | * Relatively pan the graph by a specified rendered position vector
569 | *
570 | * @param {Object} point {x: 0, y: 0}
571 | */
572 | SvgPanZoom.prototype.panBy = function(point) {
573 | var viewportCTM = this.viewport.getCTM()
574 | viewportCTM.e += point.x
575 | viewportCTM.f += point.y
576 | this.viewport.setCTM(viewportCTM)
577 | }
578 |
579 | /**
580 | * Get pan vector
581 | *
582 | * @return {Object} {x: 0, y: 0}
583 | */
584 | SvgPanZoom.prototype.getPan = function() {
585 | var state = this.viewport.getState()
586 |
587 | return {x: state.x, y: state.y}
588 | }
589 |
590 | /**
591 | * Recalculates cached svg dimensions and controls position
592 | */
593 | SvgPanZoom.prototype.resize = function() {
594 | // Get dimensions
595 | var boundingClientRectNormalized = SvgUtils.getBoundingClientRectNormalized(this.svg)
596 | this.width = boundingClientRectNormalized.width
597 | this.height = boundingClientRectNormalized.height
598 |
599 | // Recalculate original state
600 | var viewport = this.viewport
601 | viewport.options.width = this.width
602 | viewport.options.height = this.height
603 | viewport.processCTM()
604 |
605 | // Reposition control icons by re-enabling them
606 | if (this.options.controlIconsEnabled) {
607 | this.getPublicInstance().disableControlIcons()
608 | this.getPublicInstance().enableControlIcons()
609 | }
610 | }
611 |
612 | /**
613 | * Unbind mouse events, free callbacks and destroy public instance
614 | */
615 | SvgPanZoom.prototype.destroy = function() {
616 | var that = this
617 |
618 | // Free callbacks
619 | this.beforeZoom = null
620 | this.onZoom = null
621 | this.beforePan = null
622 | this.onPan = null
623 | this.onUpdatedCTM = null
624 |
625 | // Destroy custom event handlers
626 | if (this.options.customEventsHandler != null) { // jshint ignore:line
627 | this.options.customEventsHandler.destroy({
628 | svgElement: this.svg
629 | , eventsListenerElement: this.options.eventsListenerElement
630 | , instance: this.getPublicInstance()
631 | })
632 | }
633 |
634 | // Unbind eventListeners
635 | for (var event in this.eventListeners) {
636 | (this.options.eventsListenerElement || this.svg)
637 | .removeEventListener(event, this.eventListeners[event], !this.options.preventMouseEventsDefault ? passiveListenerOption : false)
638 | }
639 |
640 | // Unbind wheelListener
641 | this.disableMouseWheelZoom()
642 |
643 | // Remove control icons
644 | this.getPublicInstance().disableControlIcons()
645 |
646 | // Reset zoom and pan
647 | this.reset()
648 |
649 | // Remove instance from instancesStore
650 | instancesStore = instancesStore.filter(function(instance){
651 | return instance.svg !== that.svg
652 | })
653 |
654 | // Delete options and its contents
655 | delete this.options
656 |
657 | // Delete viewport to make public shadow viewport functions uncallable
658 | delete this.viewport
659 |
660 | // Destroy public instance and rewrite getPublicInstance
661 | delete this.publicInstance
662 | delete this.pi
663 | this.getPublicInstance = function(){
664 | return null
665 | }
666 | }
667 |
668 | /**
669 | * Returns a public instance object
670 | *
671 | * @return {Object} Public instance object
672 | */
673 | SvgPanZoom.prototype.getPublicInstance = function() {
674 | var that = this
675 |
676 | // Create cache
677 | if (!this.publicInstance) {
678 | this.publicInstance = this.pi = {
679 | options: this.options,
680 | // Pan
681 | enablePan: function() {that.options.panEnabled = true; return that.pi}
682 | , disablePan: function() {that.options.panEnabled = false; return that.pi}
683 | , isPanEnabled: function() {return !!that.options.panEnabled}
684 | , pan: function(point) {that.pan(point); return that.pi}
685 | , panBy: function(point) {that.panBy(point); return that.pi}
686 | , getPan: function() {return that.getPan()}
687 | // Pan event
688 | , setBeforePan: function(fn) {that.options.beforePan = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
689 | , setOnPan: function(fn) {that.options.onPan = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
690 | // Zoom and Control Icons
691 | , enableZoom: function() {that.options.zoomEnabled = true; return that.pi}
692 | , disableZoom: function() {that.options.zoomEnabled = false; return that.pi}
693 | , isZoomEnabled: function() {return !!that.options.zoomEnabled}
694 | , enableControlIcons: function() {
695 | if (!that.options.controlIconsEnabled) {
696 | that.options.controlIconsEnabled = true
697 | ControlIcons.enable(that)
698 | }
699 | return that.pi
700 | }
701 | , disableControlIcons: function() {
702 | if (that.options.controlIconsEnabled) {
703 | that.options.controlIconsEnabled = false;
704 | ControlIcons.disable(that)
705 | }
706 | return that.pi
707 | }
708 | , isControlIconsEnabled: function() {return !!that.options.controlIconsEnabled}
709 | // Double click zoom
710 | , enableDblClickZoom: function() {that.options.dblClickZoomEnabled = true; return that.pi}
711 | , disableDblClickZoom: function() {that.options.dblClickZoomEnabled = false; return that.pi}
712 | , isDblClickZoomEnabled: function() {return !!that.options.dblClickZoomEnabled}
713 | // Mouse wheel zoom
714 | , enableMouseWheelZoom: function() {that.enableMouseWheelZoom(); return that.pi}
715 | , disableMouseWheelZoom: function() {that.disableMouseWheelZoom(); return that.pi}
716 | , isMouseWheelZoomEnabled: function() {return !!that.options.mouseWheelZoomEnabled}
717 | // Zoom scale and bounds
718 | , setZoomScaleSensitivity: function(scale) {that.options.zoomScaleSensitivity = scale; return that.pi}
719 | , setMinZoom: function(zoom) {that.options.minZoom = zoom; return that.pi}
720 | , setMaxZoom: function(zoom) {that.options.maxZoom = zoom; return that.pi}
721 | // Zoom event
722 | , setBeforeZoom: function(fn) {that.options.beforeZoom = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
723 | , setOnZoom: function(fn) {that.options.onZoom = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
724 | // Zooming
725 | , zoom: function(scale) {that.publicZoom(scale, true); return that.pi}
726 | , zoomBy: function(scale) {that.publicZoom(scale, false); return that.pi}
727 | , zoomAtPoint: function(scale, point) {that.publicZoomAtPoint(scale, point, true); return that.pi}
728 | , zoomAtPointBy: function(scale, point) {that.publicZoomAtPoint(scale, point, false); return that.pi}
729 | , zoomIn: function() {this.zoomBy(1 + that.options.zoomScaleSensitivity); return that.pi}
730 | , zoomOut: function() {this.zoomBy(1 / (1 + that.options.zoomScaleSensitivity)); return that.pi}
731 | , getZoom: function() {return that.getRelativeZoom()}
732 | // CTM update
733 | , setOnUpdatedCTM: function(fn) {that.options.onUpdatedCTM = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
734 | // Reset
735 | , resetZoom: function() {that.resetZoom(); return that.pi}
736 | , resetPan: function() {that.resetPan(); return that.pi}
737 | , reset: function() {that.reset(); return that.pi}
738 | // Fit, Contain and Center
739 | , fit: function() {that.fit(); return that.pi}
740 | , contain: function() {that.contain(); return that.pi}
741 | , center: function() {that.center(); return that.pi}
742 | // Size and Resize
743 | , updateBBox: function() {that.updateBBox(); return that.pi}
744 | , resize: function() {that.resize(); return that.pi}
745 | , getSizes: function() {
746 | return {
747 | width: that.width
748 | , height: that.height
749 | , realZoom: that.getZoom()
750 | , viewBox: that.viewport.getViewBox()
751 | }
752 | }
753 | // Destroy
754 | , destroy: function() {that.destroy(); return that.pi}
755 | }
756 | }
757 |
758 | return this.publicInstance
759 | }
760 |
761 | /**
762 | * Stores pairs of instances of SvgPanZoom and SVG
763 | * Each pair is represented by an object {svg: SVGSVGElement, instance: SvgPanZoom}
764 | *
765 | * @type {Array}
766 | */
767 | var instancesStore = []
768 |
769 | var svgPanZoom = function(elementOrSelector, options){
770 | var svg = Utils.getSvg(elementOrSelector)
771 |
772 | if (svg === null) {
773 | return null
774 | } else {
775 | // Look for existent instance
776 | for(var i = instancesStore.length - 1; i >= 0; i--) {
777 | if (instancesStore[i].svg === svg) {
778 | return instancesStore[i].instance.getPublicInstance()
779 | }
780 | }
781 |
782 | // If instance not found - create one
783 | instancesStore.push({
784 | svg: svg
785 | , instance: new SvgPanZoom(svg, options)
786 | })
787 |
788 | // Return just pushed instance
789 | return instancesStore[instancesStore.length - 1].instance.getPublicInstance()
790 | }
791 | }
792 |
793 | export default svgPanZoom
794 |
--------------------------------------------------------------------------------
/lib/svg-pan-zoom/svg-utilities.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import Utils from './utilities'
3 | let _browser = 'unknown'
4 |
5 | // http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
6 | if (/*@cc_on!@*/false || !!document.documentMode) { // internet explorer
7 | _browser = 'ie';
8 | }
9 |
10 | export default {
11 | svgNS: 'http://www.w3.org/2000/svg'
12 | , xmlNS: 'http://www.w3.org/XML/1998/namespace'
13 | , xmlnsNS: 'http://www.w3.org/2000/xmlns/'
14 | , xlinkNS: 'http://www.w3.org/1999/xlink'
15 | , evNS: 'http://www.w3.org/2001/xml-events'
16 |
17 | /**
18 | * Get svg dimensions: width and height
19 | *
20 | * @param {SVGSVGElement} svg
21 | * @return {Object} {width: 0, height: 0}
22 | */
23 | , getBoundingClientRectNormalized: function(svg) {
24 | if (svg.clientWidth && svg.clientHeight) {
25 | return {width: svg.clientWidth, height: svg.clientHeight}
26 | } else if (!!svg.getBoundingClientRect()) {
27 | return svg.getBoundingClientRect();
28 | } else {
29 | throw new Error('Cannot get BoundingClientRect for SVG.');
30 | }
31 | }
32 |
33 | /**
34 | * Gets g element with class of "viewport" or creates it if it doesn't exist
35 | *
36 | * @param {SVGSVGElement} svg
37 | * @return {SVGElement} g (group) element
38 | */
39 | , getOrCreateViewport: function(svg, selector) {
40 | var viewport = null
41 |
42 | if (Utils.isElement(selector)) {
43 | viewport = selector
44 | } else {
45 | viewport = svg.querySelector(selector)
46 | }
47 |
48 | // Check if there is just one main group in SVG
49 | if (!viewport) {
50 | var childNodes = Array.prototype.slice.call(svg.childNodes || svg.children).filter(function(el){
51 | return el.nodeName !== 'defs' && el.nodeName !== '#text'
52 | })
53 |
54 | // Node name should be SVGGElement and should have no transform attribute
55 | // Groups with transform are not used as viewport because it involves parsing of all transform possibilities
56 | if (childNodes.length === 1 && childNodes[0].nodeName === 'g' && childNodes[0].getAttribute('transform') === null) {
57 | viewport = childNodes[0]
58 | }
59 | }
60 |
61 | // If no favorable group element exists then create one
62 | if (!viewport) {
63 | var viewportId = 'viewport-' + new Date().toISOString().replace(/\D/g, '');
64 | viewport = document.createElementNS(this.svgNS, 'g');
65 | viewport.setAttribute('id', viewportId);
66 |
67 | // Internet Explorer (all versions?) can't use childNodes, but other browsers prefer (require?) using childNodes
68 | var svgChildren = svg.childNodes || svg.children;
69 | if (!!svgChildren && svgChildren.length > 0) {
70 | for (var i = svgChildren.length; i > 0; i--) {
71 | // Move everything into viewport except defs
72 | if (svgChildren[svgChildren.length - i].nodeName !== 'defs') {
73 | viewport.appendChild(svgChildren[svgChildren.length - i]);
74 | }
75 | }
76 | }
77 | svg.appendChild(viewport);
78 | }
79 |
80 | // Parse class names
81 | var classNames = [];
82 | if (viewport.getAttribute('class')) {
83 | classNames = viewport.getAttribute('class').split(' ')
84 | }
85 |
86 | // Set class (if not set already)
87 | if (!~classNames.indexOf('svg-pan-zoom_viewport')) {
88 | classNames.push('svg-pan-zoom_viewport')
89 | viewport.setAttribute('class', classNames.join(' '))
90 | }
91 |
92 | return viewport
93 | }
94 |
95 | /**
96 | * Set SVG attributes
97 | *
98 | * @param {SVGSVGElement} svg
99 | */
100 | , setupSvgAttributes: function(svg) {
101 | // Setting default attributes
102 | svg.setAttribute('xmlns', this.svgNS);
103 | svg.setAttributeNS(this.xmlnsNS, 'xmlns:xlink', this.xlinkNS);
104 | svg.setAttributeNS(this.xmlnsNS, 'xmlns:ev', this.evNS);
105 |
106 | // Needed for Internet Explorer, otherwise the viewport overflows
107 | if (svg.parentNode !== null) {
108 | var style = svg.getAttribute('style') || '';
109 | if (style.toLowerCase().indexOf('overflow') === -1) {
110 | svg.setAttribute('style', 'overflow: hidden; ' + style);
111 | }
112 | }
113 | }
114 |
115 | /**
116 | * How long Internet Explorer takes to finish updating its display (ms).
117 | */
118 | , internetExplorerRedisplayInterval: 300
119 |
120 | /**
121 | * Forces the browser to redisplay all SVG elements that rely on an
122 | * element defined in a 'defs' section. It works globally, for every
123 | * available defs element on the page.
124 | * The throttling is intentionally global.
125 | *
126 | * This is only needed for IE. It is as a hack to make markers (and 'use' elements?)
127 | * visible after pan/zoom when there are multiple SVGs on the page.
128 | * See bug report: https://connect.microsoft.com/IE/feedback/details/781964/
129 | * also see svg-pan-zoom issue: https://github.com/ariutta/svg-pan-zoom/issues/62
130 | */
131 | , refreshDefsGlobal: Utils.throttle(function() {
132 | var allDefs = document.querySelectorAll('defs');
133 | var allDefsCount = allDefs.length;
134 | for (var i = 0; i < allDefsCount; i++) {
135 | var thisDefs = allDefs[i];
136 | thisDefs.parentNode.insertBefore(thisDefs, thisDefs);
137 | }
138 | }, this ? this.internetExplorerRedisplayInterval : null)
139 |
140 | /**
141 | * Sets the current transform matrix of an element
142 | *
143 | * @param {SVGElement} element
144 | * @param {SVGMatrix} matrix CTM
145 | * @param {SVGElement} defs
146 | */
147 | , setCTM: function(element, matrix, defs) {
148 | var that = this
149 | , s = 'matrix(' + matrix.a + ',' + matrix.b + ',' + matrix.c + ',' + matrix.d + ',' + matrix.e + ',' + matrix.f + ')';
150 |
151 | // element.setAttributeNS(null, 'transform', s);
152 | element.style.transform = s; // https://github.com/ariutta/svg-pan-zoom/issues/101 implement
153 | if ('transform' in element.style) {
154 | element.style.transform = s;
155 | } else if ('-ms-transform' in element.style) {
156 | element.style['-ms-transform'] = s;
157 | } else if ('-webkit-transform' in element.style) {
158 | element.style['-webkit-transform'] = s;
159 | }
160 |
161 | // IE has a bug that makes markers disappear on zoom (when the matrix "a" and/or "d" elements change)
162 | // see http://stackoverflow.com/questions/17654578/svg-marker-does-not-work-in-ie9-10
163 | // and http://srndolha.wordpress.com/2013/11/25/svg-line-markers-may-disappear-in-internet-explorer-11/
164 | if (_browser === 'ie' && !!defs) {
165 | // this refresh is intended for redisplaying the SVG during zooming
166 | defs.parentNode.insertBefore(defs, defs);
167 | // this refresh is intended for redisplaying the other SVGs on a page when panning a given SVG
168 | // it is also needed for the given SVG itself, on zoomEnd, if the SVG contains any markers that
169 | // are located under any other element(s).
170 | window.setTimeout(function() {
171 | that.refreshDefsGlobal();
172 | }, that.internetExplorerRedisplayInterval);
173 | }
174 | }
175 |
176 | /**
177 | * Instantiate an SVGPoint object with given event coordinates
178 | *
179 | * @param {Event} evt
180 | * @param {SVGSVGElement} svg
181 | * @return {SVGPoint} point
182 | */
183 | , getEventPoint: function(evt, svg) {
184 | var point = svg.createSVGPoint()
185 |
186 | Utils.mouseAndTouchNormalize(evt, svg)
187 |
188 | point.x = evt.clientX
189 | point.y = evt.clientY
190 |
191 | return point
192 | }
193 |
194 | /**
195 | * Get SVG center point
196 | *
197 | * @param {SVGSVGElement} svg
198 | * @return {SVGPoint}
199 | */
200 | , getSvgCenterPoint: function(svg, width, height) {
201 | return this.createSVGPoint(svg, width / 2, height / 2)
202 | }
203 |
204 | /**
205 | * Create a SVGPoint with given x and y
206 | *
207 | * @param {SVGSVGElement} svg
208 | * @param {Number} x
209 | * @param {Number} y
210 | * @return {SVGPoint}
211 | */
212 | , createSVGPoint: function(svg, x, y) {
213 | var point = svg.createSVGPoint()
214 | point.x = x
215 | point.y = y
216 |
217 | return point
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/lib/svg-pan-zoom/uniwheel.js:
--------------------------------------------------------------------------------
1 | // uniwheel 0.1.2 (customized)
2 | // A unified cross browser mouse wheel event handler
3 | // https://github.com/teemualap/uniwheel
4 |
5 | export default (function(){
6 |
7 | //Full details: https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel
8 |
9 | var prefix = "", _addEventListener, _removeEventListener, support, fns = [];
10 | var passiveOption = {passive: true};
11 |
12 | // detect event model
13 | if ( window.addEventListener ) {
14 | _addEventListener = "addEventListener";
15 | _removeEventListener = "removeEventListener";
16 | } else {
17 | _addEventListener = "attachEvent";
18 | _removeEventListener = "detachEvent";
19 | prefix = "on";
20 | }
21 |
22 | // detect available wheel event
23 | support = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel"
24 | document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel"
25 | "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox
26 |
27 |
28 | function createCallback(element,callback) {
29 |
30 | var fn = function(originalEvent) {
31 |
32 | !originalEvent && ( originalEvent = window.event );
33 |
34 | // create a normalized event object
35 | var event = {
36 | // keep a ref to the original event object
37 | originalEvent: originalEvent,
38 | target: originalEvent.target || originalEvent.srcElement,
39 | type: "wheel",
40 | deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1,
41 | deltaX: 0,
42 | delatZ: 0,
43 | preventDefault: function() {
44 | originalEvent.preventDefault ?
45 | originalEvent.preventDefault() :
46 | originalEvent.returnValue = false;
47 | }
48 | };
49 |
50 | // calculate deltaY (and deltaX) according to the event
51 | if ( support == "mousewheel" ) {
52 | event.deltaY = - 1/40 * originalEvent.wheelDelta;
53 | // Webkit also support wheelDeltaX
54 | originalEvent.wheelDeltaX && ( event.deltaX = - 1/40 * originalEvent.wheelDeltaX );
55 | } else {
56 | event.deltaY = originalEvent.detail;
57 | }
58 |
59 | // it's time to fire the callback
60 | return callback( event );
61 |
62 | };
63 |
64 | fns.push({
65 | element: element,
66 | fn: fn,
67 | });
68 |
69 | return fn;
70 | }
71 |
72 | function getCallback(element) {
73 | for (var i = 0; i < fns.length; i++) {
74 | if (fns[i].element === element) {
75 | return fns[i].fn;
76 | }
77 | }
78 | return function(){};
79 | }
80 |
81 | function removeCallback(element) {
82 | for (var i = 0; i < fns.length; i++) {
83 | if (fns[i].element === element) {
84 | return fns.splice(i,1);
85 | }
86 | }
87 | }
88 |
89 | function _addWheelListener(elem, eventName, callback, isPassiveListener ) {
90 | var cb;
91 |
92 | if (support === "wheel") {
93 | cb = callback;
94 | } else {
95 | cb = createCallback(elem, callback);
96 | }
97 |
98 | elem[_addEventListener](prefix + eventName, cb, isPassiveListener ? passiveOption : false);
99 | }
100 |
101 | function _removeWheelListener(elem, eventName, callback, isPassiveListener ) {
102 |
103 | var cb;
104 |
105 | if (support === "wheel") {
106 | cb = callback;
107 | } else {
108 | cb = getCallback(elem);
109 | }
110 |
111 | elem[_removeEventListener](prefix + eventName, cb, isPassiveListener ? passiveOption : false);
112 |
113 | removeCallback(elem);
114 | }
115 |
116 | function addWheelListener( elem, callback, isPassiveListener ) {
117 | _addWheelListener(elem, support, callback, isPassiveListener );
118 |
119 | // handle MozMousePixelScroll in older Firefox
120 | if( support == "DOMMouseScroll" ) {
121 | _addWheelListener(elem, "MozMousePixelScroll", callback, isPassiveListener );
122 | }
123 | }
124 |
125 | function removeWheelListener(elem, callback, isPassiveListener){
126 | _removeWheelListener(elem, support, callback, isPassiveListener);
127 |
128 | // handle MozMousePixelScroll in older Firefox
129 | if( support == "DOMMouseScroll" ) {
130 | _removeWheelListener(elem, "MozMousePixelScroll", callback, isPassiveListener);
131 | }
132 | }
133 |
134 | return {
135 | on: addWheelListener,
136 | off: removeWheelListener
137 | };
138 |
139 | })();
140 |
--------------------------------------------------------------------------------
/lib/svg-pan-zoom/utilities.js:
--------------------------------------------------------------------------------
1 | export default {
2 | /**
3 | * Extends an object
4 | *
5 | * @param {Object} target object to extend
6 | * @param {Object} source object to take properties from
7 | * @return {Object} extended object
8 | */
9 | extend: function(target, source) {
10 | target = target || {};
11 | for (var prop in source) {
12 | // Go recursively
13 | if (this.isObject(source[prop])) {
14 | target[prop] = this.extend(target[prop], source[prop])
15 | } else {
16 | target[prop] = source[prop]
17 | }
18 | }
19 | return target;
20 | }
21 |
22 | /**
23 | * Checks if an object is a DOM element
24 | *
25 | * @param {Object} o HTML element or String
26 | * @return {Boolean} returns true if object is a DOM element
27 | */
28 | , isElement: function(o){
29 | return (
30 | o instanceof HTMLElement || o instanceof SVGElement || o instanceof SVGSVGElement || //DOM2
31 | (o && typeof o === 'object' && o !== null && o.nodeType === 1 && typeof o.nodeName === 'string')
32 | );
33 | }
34 |
35 | /**
36 | * Checks if an object is an Object
37 | *
38 | * @param {Object} o Object
39 | * @return {Boolean} returns true if object is an Object
40 | */
41 | , isObject: function(o){
42 | return Object.prototype.toString.call(o) === '[object Object]';
43 | }
44 |
45 | /**
46 | * Checks if variable is Number
47 | *
48 | * @param {Integer|Float} n
49 | * @return {Boolean} returns true if variable is Number
50 | */
51 | , isNumber: function(n) {
52 | return !isNaN(parseFloat(n)) && isFinite(n);
53 | }
54 |
55 | /**
56 | * Search for an SVG element
57 | *
58 | * @param {Object|String} elementOrSelector DOM Element or selector String
59 | * @return {Object|Null} SVG or null
60 | */
61 | , getSvg: function(elementOrSelector) {
62 | var element
63 | , svg;
64 |
65 | if (!this.isElement(elementOrSelector)) {
66 | // If selector provided
67 | if (typeof elementOrSelector === 'string' || elementOrSelector instanceof String) {
68 | // Try to find the element
69 | element = document.querySelector(elementOrSelector)
70 |
71 | if (!element) {
72 | throw new Error('Provided selector did not find any elements. Selector: ' + elementOrSelector)
73 | }
74 | } else {
75 | throw new Error('Provided selector is not an HTML object nor String')
76 | }
77 | } else {
78 | element = elementOrSelector
79 | }
80 |
81 | if (element.tagName.toLowerCase() === 'svg') {
82 | svg = element;
83 | } else {
84 | if (element.tagName.toLowerCase() === 'object') {
85 | svg = element.contentDocument.documentElement;
86 | } else {
87 | if (element.tagName.toLowerCase() === 'embed') {
88 | svg = element.getSVGDocument().documentElement;
89 | } else {
90 | if (element.tagName.toLowerCase() === 'img') {
91 | throw new Error('Cannot script an SVG in an "img" element. Please use an "object" element or an in-line SVG.');
92 | } else {
93 | throw new Error('Cannot get SVG.');
94 | }
95 | }
96 | }
97 | }
98 |
99 | return svg
100 | }
101 |
102 | /**
103 | * Attach a given context to a function
104 | * @param {Function} fn Function
105 | * @param {Object} context Context
106 | * @return {Function} Function with certain context
107 | */
108 | , proxy: function(fn, context) {
109 | return function() {
110 | return fn.apply(context, arguments)
111 | }
112 | }
113 |
114 | /**
115 | * Returns object type
116 | * Uses toString that returns [object SVGPoint]
117 | * And than parses object type from string
118 | *
119 | * @param {Object} o Any object
120 | * @return {String} Object type
121 | */
122 | , getType: function(o) {
123 | return Object.prototype.toString.apply(o).replace(/^\[object\s/, '').replace(/\]$/, '')
124 | }
125 |
126 | /**
127 | * If it is a touch event than add clientX and clientY to event object
128 | *
129 | * @param {Event} evt
130 | * @param {SVGSVGElement} svg
131 | */
132 | , mouseAndTouchNormalize: function(evt, svg) {
133 | // If no clientX then fallback
134 | if (evt.clientX === void 0 || evt.clientX === null) {
135 | // Fallback
136 | evt.clientX = 0
137 | evt.clientY = 0
138 |
139 | // If it is a touch event
140 | if (evt.touches !== void 0 && evt.touches.length) {
141 | if (evt.touches[0].clientX !== void 0) {
142 | evt.clientX = evt.touches[0].clientX
143 | evt.clientY = evt.touches[0].clientY
144 | } else if (evt.touches[0].pageX !== void 0) {
145 | var rect = svg.getBoundingClientRect();
146 |
147 | evt.clientX = evt.touches[0].pageX - rect.left
148 | evt.clientY = evt.touches[0].pageY - rect.top
149 | }
150 | // If it is a custom event
151 | } else if (evt.originalEvent !== void 0) {
152 | if (evt.originalEvent.clientX !== void 0) {
153 | evt.clientX = evt.originalEvent.clientX
154 | evt.clientY = evt.originalEvent.clientY
155 | }
156 | }
157 | }
158 | }
159 |
160 | /**
161 | * Check if an event is a double click/tap
162 | * TODO: For touch gestures use a library (hammer.js) that takes in account other events
163 | * (touchmove and touchend). It should take in account tap duration and traveled distance
164 | *
165 | * @param {Event} evt
166 | * @param {Event} prevEvt Previous Event
167 | * @return {Boolean}
168 | */
169 | , isDblClick: function(evt, prevEvt) {
170 | // Double click detected by browser
171 | if (evt.detail === 2) {
172 | return true;
173 | }
174 | // Try to compare events
175 | else if (prevEvt !== void 0 && prevEvt !== null) {
176 | var timeStampDiff = evt.timeStamp - prevEvt.timeStamp // should be lower than 250 ms
177 | , touchesDistance = Math.sqrt(Math.pow(evt.clientX - prevEvt.clientX, 2) + Math.pow(evt.clientY - prevEvt.clientY, 2))
178 |
179 | return timeStampDiff < 250 && touchesDistance < 10
180 | }
181 |
182 | // Nothing found
183 | return false;
184 | }
185 |
186 | /**
187 | * Returns current timestamp as an integer
188 | *
189 | * @return {Number}
190 | */
191 | , now: Date.now || function() {
192 | return new Date().getTime();
193 | }
194 |
195 | // From underscore.
196 | // Returns a function, that, when invoked, will only be triggered at most once
197 | // during a given window of time. Normally, the throttled function will run
198 | // as much as it can, without ever going more than once per `wait` duration;
199 | // but if you'd like to disable the execution on the leading edge, pass
200 | // `{leading: false}`. To disable execution on the trailing edge, ditto.
201 | // jscs:disable
202 | // jshint ignore:start
203 | , throttle: function(func, wait, options) {
204 | var that = this;
205 | var context, args, result;
206 | var timeout = null;
207 | var previous = 0;
208 | if (!options) options = {};
209 | var later = function() {
210 | previous = options.leading === false ? 0 : that.now();
211 | timeout = null;
212 | result = func.apply(context, args);
213 | if (!timeout) context = args = null;
214 | };
215 | return function() {
216 | var now = that.now();
217 | if (!previous && options.leading === false) previous = now;
218 | var remaining = wait - (now - previous);
219 | context = this;
220 | args = arguments;
221 | if (remaining <= 0 || remaining > wait) {
222 | clearTimeout(timeout);
223 | timeout = null;
224 | previous = now;
225 | result = func.apply(context, args);
226 | if (!timeout) context = args = null;
227 | } else if (!timeout && options.trailing !== false) {
228 | timeout = setTimeout(later, remaining);
229 | }
230 | return result;
231 | };
232 | }
233 | // jshint ignore:end
234 | // jscs:enable
235 |
236 | /**
237 | * Create a requestAnimationFrame simulation
238 | *
239 | * @param {Number|String} refreshRate
240 | * @return {Function}
241 | */
242 | , createRequestAnimationFrame: function(refreshRate) {
243 | var timeout = null
244 |
245 | // Convert refreshRate to timeout
246 | if (refreshRate !== 'auto' && refreshRate < 60 && refreshRate > 1) {
247 | timeout = Math.floor(1000 / refreshRate)
248 | }
249 |
250 | if (timeout === null) {
251 | return window.requestAnimationFrame || requestTimeout(33)
252 | } else {
253 | return requestTimeout(timeout)
254 | }
255 | }
256 | }
257 |
258 | /**
259 | * Create a callback that will execute after a given timeout
260 | *
261 | * @param {Function} timeout
262 | * @return {Function}
263 | */
264 | function requestTimeout(timeout) {
265 | return function(callback) {
266 | window.setTimeout(callback, timeout)
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/lib/victor.js:
--------------------------------------------------------------------------------
1 | export default Victor
2 | /**
3 | * # Victor - A JavaScript 2D vector class with methods for common vector operations
4 | */
5 |
6 | /**
7 | * Constructor. Will also work without the `new` keyword
8 | *
9 | * ### Examples:
10 | * var vec1 = new Victor(100, 50);
11 | * var vec2 = Victor(42, 1337);
12 | *
13 | * @param {Number} x Value of the x axis
14 | * @param {Number} y Value of the y axis
15 | * @return {Victor}
16 | * @api public
17 | */
18 | function Victor (x, y) {
19 | if (!(this instanceof Victor)) {
20 | return new Victor(x, y);
21 | }
22 |
23 | /**
24 | * The X axis
25 | *
26 | * ### Examples:
27 | * var vec = new Victor.fromArray(42, 21);
28 | *
29 | * vec.x;
30 | * // => 42
31 | *
32 | * @api public
33 | */
34 | this.x = x || 0;
35 |
36 | /**
37 | * The Y axis
38 | *
39 | * ### Examples:
40 | * var vec = new Victor.fromArray(42, 21);
41 | *
42 | * vec.y;
43 | * // => 21
44 | *
45 | * @api public
46 | */
47 | this.y = y || 0;
48 | };
49 |
50 | /**
51 | * # Static
52 | */
53 |
54 | /**
55 | * Creates a new instance from an array
56 | *
57 | * ### Examples:
58 | * var vec = Victor.fromArray([42, 21]);
59 | *
60 | * vec.toString();
61 | * // => x:42, y:21
62 | *
63 | * @name Victor.fromArray
64 | * @param {Array} array Array with the x and y values at index 0 and 1 respectively
65 | * @return {Victor} The new instance
66 | * @api public
67 | */
68 | Victor.fromArray = function (arr) {
69 | return new Victor(arr[0] || 0, arr[1] || 0);
70 | };
71 |
72 | /**
73 | * Creates a new instance from an object
74 | *
75 | * ### Examples:
76 | * var vec = Victor.fromObject({ x: 42, y: 21 });
77 | *
78 | * vec.toString();
79 | * // => x:42, y:21
80 | *
81 | * @name Victor.fromObject
82 | * @param {Object} obj Object with the values for x and y
83 | * @return {Victor} The new instance
84 | * @api public
85 | */
86 | Victor.fromObject = function (obj) {
87 | return new Victor(obj.x || 0, obj.y || 0);
88 | };
89 |
90 | /**
91 | * # Manipulation
92 | *
93 | * These functions are chainable.
94 | */
95 |
96 | /**
97 | * Adds another vector's X axis to this one
98 | *
99 | * ### Examples:
100 | * var vec1 = new Victor(10, 10);
101 | * var vec2 = new Victor(20, 30);
102 | *
103 | * vec1.addX(vec2);
104 | * vec1.toString();
105 | * // => x:30, y:10
106 | *
107 | * @param {Victor} vector The other vector you want to add to this one
108 | * @return {Victor} `this` for chaining capabilities
109 | * @api public
110 | */
111 | Victor.prototype.addX = function (vec) {
112 | this.x += vec.x;
113 | return this;
114 | };
115 |
116 | /**
117 | * Adds another vector's Y axis to this one
118 | *
119 | * ### Examples:
120 | * var vec1 = new Victor(10, 10);
121 | * var vec2 = new Victor(20, 30);
122 | *
123 | * vec1.addY(vec2);
124 | * vec1.toString();
125 | * // => x:10, y:40
126 | *
127 | * @param {Victor} vector The other vector you want to add to this one
128 | * @return {Victor} `this` for chaining capabilities
129 | * @api public
130 | */
131 | Victor.prototype.addY = function (vec) {
132 | this.y += vec.y;
133 | return this;
134 | };
135 |
136 | /**
137 | * Adds another vector to this one
138 | *
139 | * ### Examples:
140 | * var vec1 = new Victor(10, 10);
141 | * var vec2 = new Victor(20, 30);
142 | *
143 | * vec1.add(vec2);
144 | * vec1.toString();
145 | * // => x:30, y:40
146 | *
147 | * @param {Victor} vector The other vector you want to add to this one
148 | * @return {Victor} `this` for chaining capabilities
149 | * @api public
150 | */
151 | Victor.prototype.add = function (vec) {
152 | this.x += vec.x;
153 | this.y += vec.y;
154 | return this;
155 | };
156 |
157 | /**
158 | * Adds the given scalar to both vector axis
159 | *
160 | * ### Examples:
161 | * var vec = new Victor(1, 2);
162 | *
163 | * vec.addScalar(2);
164 | * vec.toString();
165 | * // => x: 3, y: 4
166 | *
167 | * @param {Number} scalar The scalar to add
168 | * @return {Victor} `this` for chaining capabilities
169 | * @api public
170 | */
171 | Victor.prototype.addScalar = function (scalar) {
172 | this.x += scalar;
173 | this.y += scalar;
174 | return this;
175 | };
176 |
177 | /**
178 | * Adds the given scalar to the X axis
179 | *
180 | * ### Examples:
181 | * var vec = new Victor(1, 2);
182 | *
183 | * vec.addScalarX(2);
184 | * vec.toString();
185 | * // => x: 3, y: 2
186 | *
187 | * @param {Number} scalar The scalar to add
188 | * @return {Victor} `this` for chaining capabilities
189 | * @api public
190 | */
191 | Victor.prototype.addScalarX = function (scalar) {
192 | this.x += scalar;
193 | return this;
194 | };
195 |
196 | /**
197 | * Adds the given scalar to the Y axis
198 | *
199 | * ### Examples:
200 | * var vec = new Victor(1, 2);
201 | *
202 | * vec.addScalarY(2);
203 | * vec.toString();
204 | * // => x: 1, y: 4
205 | *
206 | * @param {Number} scalar The scalar to add
207 | * @return {Victor} `this` for chaining capabilities
208 | * @api public
209 | */
210 | Victor.prototype.addScalarY = function (scalar) {
211 | this.y += scalar;
212 | return this;
213 | };
214 |
215 | /**
216 | * Subtracts the X axis of another vector from this one
217 | *
218 | * ### Examples:
219 | * var vec1 = new Victor(100, 50);
220 | * var vec2 = new Victor(20, 30);
221 | *
222 | * vec1.subtractX(vec2);
223 | * vec1.toString();
224 | * // => x:80, y:50
225 | *
226 | * @param {Victor} vector The other vector you want subtract from this one
227 | * @return {Victor} `this` for chaining capabilities
228 | * @api public
229 | */
230 | Victor.prototype.subtractX = function (vec) {
231 | this.x -= vec.x;
232 | return this;
233 | };
234 |
235 | /**
236 | * Subtracts the Y axis of another vector from this one
237 | *
238 | * ### Examples:
239 | * var vec1 = new Victor(100, 50);
240 | * var vec2 = new Victor(20, 30);
241 | *
242 | * vec1.subtractY(vec2);
243 | * vec1.toString();
244 | * // => x:100, y:20
245 | *
246 | * @param {Victor} vector The other vector you want subtract from this one
247 | * @return {Victor} `this` for chaining capabilities
248 | * @api public
249 | */
250 | Victor.prototype.subtractY = function (vec) {
251 | this.y -= vec.y;
252 | return this;
253 | };
254 |
255 | /**
256 | * Subtracts another vector from this one
257 | *
258 | * ### Examples:
259 | * var vec1 = new Victor(100, 50);
260 | * var vec2 = new Victor(20, 30);
261 | *
262 | * vec1.subtract(vec2);
263 | * vec1.toString();
264 | * // => x:80, y:20
265 | *
266 | * @param {Victor} vector The other vector you want subtract from this one
267 | * @return {Victor} `this` for chaining capabilities
268 | * @api public
269 | */
270 | Victor.prototype.subtract = function (vec) {
271 | this.x -= vec.x;
272 | this.y -= vec.y;
273 | return this;
274 | };
275 |
276 | /**
277 | * Subtracts the given scalar from both axis
278 | *
279 | * ### Examples:
280 | * var vec = new Victor(100, 200);
281 | *
282 | * vec.subtractScalar(20);
283 | * vec.toString();
284 | * // => x: 80, y: 180
285 | *
286 | * @param {Number} scalar The scalar to subtract
287 | * @return {Victor} `this` for chaining capabilities
288 | * @api public
289 | */
290 | Victor.prototype.subtractScalar = function (scalar) {
291 | this.x -= scalar;
292 | this.y -= scalar;
293 | return this;
294 | };
295 |
296 | /**
297 | * Subtracts the given scalar from the X axis
298 | *
299 | * ### Examples:
300 | * var vec = new Victor(100, 200);
301 | *
302 | * vec.subtractScalarX(20);
303 | * vec.toString();
304 | * // => x: 80, y: 200
305 | *
306 | * @param {Number} scalar The scalar to subtract
307 | * @return {Victor} `this` for chaining capabilities
308 | * @api public
309 | */
310 | Victor.prototype.subtractScalarX = function (scalar) {
311 | this.x -= scalar;
312 | return this;
313 | };
314 |
315 | /**
316 | * Subtracts the given scalar from the Y axis
317 | *
318 | * ### Examples:
319 | * var vec = new Victor(100, 200);
320 | *
321 | * vec.subtractScalarY(20);
322 | * vec.toString();
323 | * // => x: 100, y: 180
324 | *
325 | * @param {Number} scalar The scalar to subtract
326 | * @return {Victor} `this` for chaining capabilities
327 | * @api public
328 | */
329 | Victor.prototype.subtractScalarY = function (scalar) {
330 | this.y -= scalar;
331 | return this;
332 | };
333 |
334 | /**
335 | * Divides the X axis by the x component of given vector
336 | *
337 | * ### Examples:
338 | * var vec = new Victor(100, 50);
339 | * var vec2 = new Victor(2, 0);
340 | *
341 | * vec.divideX(vec2);
342 | * vec.toString();
343 | * // => x:50, y:50
344 | *
345 | * @param {Victor} vector The other vector you want divide by
346 | * @return {Victor} `this` for chaining capabilities
347 | * @api public
348 | */
349 | Victor.prototype.divideX = function (vector) {
350 | this.x /= vector.x;
351 | return this;
352 | };
353 |
354 | /**
355 | * Divides the Y axis by the y component of given vector
356 | *
357 | * ### Examples:
358 | * var vec = new Victor(100, 50);
359 | * var vec2 = new Victor(0, 2);
360 | *
361 | * vec.divideY(vec2);
362 | * vec.toString();
363 | * // => x:100, y:25
364 | *
365 | * @param {Victor} vector The other vector you want divide by
366 | * @return {Victor} `this` for chaining capabilities
367 | * @api public
368 | */
369 | Victor.prototype.divideY = function (vector) {
370 | this.y /= vector.y;
371 | return this;
372 | };
373 |
374 | /**
375 | * Divides both vector axis by a axis values of given vector
376 | *
377 | * ### Examples:
378 | * var vec = new Victor(100, 50);
379 | * var vec2 = new Victor(2, 2);
380 | *
381 | * vec.divide(vec2);
382 | * vec.toString();
383 | * // => x:50, y:25
384 | *
385 | * @param {Victor} vector The vector to divide by
386 | * @return {Victor} `this` for chaining capabilities
387 | * @api public
388 | */
389 | Victor.prototype.divide = function (vector) {
390 | this.x /= vector.x;
391 | this.y /= vector.y;
392 | return this;
393 | };
394 |
395 | /**
396 | * Divides both vector axis by the given scalar value
397 | *
398 | * ### Examples:
399 | * var vec = new Victor(100, 50);
400 | *
401 | * vec.divideScalar(2);
402 | * vec.toString();
403 | * // => x:50, y:25
404 | *
405 | * @param {Number} The scalar to divide by
406 | * @return {Victor} `this` for chaining capabilities
407 | * @api public
408 | */
409 | Victor.prototype.divideScalar = function (scalar) {
410 | if (scalar !== 0) {
411 | this.x /= scalar;
412 | this.y /= scalar;
413 | } else {
414 | this.x = 0;
415 | this.y = 0;
416 | }
417 |
418 | return this;
419 | };
420 |
421 | /**
422 | * Divides the X axis by the given scalar value
423 | *
424 | * ### Examples:
425 | * var vec = new Victor(100, 50);
426 | *
427 | * vec.divideScalarX(2);
428 | * vec.toString();
429 | * // => x:50, y:50
430 | *
431 | * @param {Number} The scalar to divide by
432 | * @return {Victor} `this` for chaining capabilities
433 | * @api public
434 | */
435 | Victor.prototype.divideScalarX = function (scalar) {
436 | if (scalar !== 0) {
437 | this.x /= scalar;
438 | } else {
439 | this.x = 0;
440 | }
441 | return this;
442 | };
443 |
444 | /**
445 | * Divides the Y axis by the given scalar value
446 | *
447 | * ### Examples:
448 | * var vec = new Victor(100, 50);
449 | *
450 | * vec.divideScalarY(2);
451 | * vec.toString();
452 | * // => x:100, y:25
453 | *
454 | * @param {Number} The scalar to divide by
455 | * @return {Victor} `this` for chaining capabilities
456 | * @api public
457 | */
458 | Victor.prototype.divideScalarY = function (scalar) {
459 | if (scalar !== 0) {
460 | this.y /= scalar;
461 | } else {
462 | this.y = 0;
463 | }
464 | return this;
465 | };
466 |
467 | /**
468 | * Inverts the X axis
469 | *
470 | * ### Examples:
471 | * var vec = new Victor(100, 50);
472 | *
473 | * vec.invertX();
474 | * vec.toString();
475 | * // => x:-100, y:50
476 | *
477 | * @return {Victor} `this` for chaining capabilities
478 | * @api public
479 | */
480 | Victor.prototype.invertX = function () {
481 | this.x *= -1;
482 | return this;
483 | };
484 |
485 | /**
486 | * Inverts the Y axis
487 | *
488 | * ### Examples:
489 | * var vec = new Victor(100, 50);
490 | *
491 | * vec.invertY();
492 | * vec.toString();
493 | * // => x:100, y:-50
494 | *
495 | * @return {Victor} `this` for chaining capabilities
496 | * @api public
497 | */
498 | Victor.prototype.invertY = function () {
499 | this.y *= -1;
500 | return this;
501 | };
502 |
503 | /**
504 | * Inverts both axis
505 | *
506 | * ### Examples:
507 | * var vec = new Victor(100, 50);
508 | *
509 | * vec.invert();
510 | * vec.toString();
511 | * // => x:-100, y:-50
512 | *
513 | * @return {Victor} `this` for chaining capabilities
514 | * @api public
515 | */
516 | Victor.prototype.invert = function () {
517 | this.invertX();
518 | this.invertY();
519 | return this;
520 | };
521 |
522 | /**
523 | * Multiplies the X axis by X component of given vector
524 | *
525 | * ### Examples:
526 | * var vec = new Victor(100, 50);
527 | * var vec2 = new Victor(2, 0);
528 | *
529 | * vec.multiplyX(vec2);
530 | * vec.toString();
531 | * // => x:200, y:50
532 | *
533 | * @param {Victor} vector The vector to multiply the axis with
534 | * @return {Victor} `this` for chaining capabilities
535 | * @api public
536 | */
537 | Victor.prototype.multiplyX = function (vector) {
538 | this.x *= vector.x;
539 | return this;
540 | };
541 |
542 | /**
543 | * Multiplies the Y axis by Y component of given vector
544 | *
545 | * ### Examples:
546 | * var vec = new Victor(100, 50);
547 | * var vec2 = new Victor(0, 2);
548 | *
549 | * vec.multiplyX(vec2);
550 | * vec.toString();
551 | * // => x:100, y:100
552 | *
553 | * @param {Victor} vector The vector to multiply the axis with
554 | * @return {Victor} `this` for chaining capabilities
555 | * @api public
556 | */
557 | Victor.prototype.multiplyY = function (vector) {
558 | this.y *= vector.y;
559 | return this;
560 | };
561 |
562 | /**
563 | * Multiplies both vector axis by values from a given vector
564 | *
565 | * ### Examples:
566 | * var vec = new Victor(100, 50);
567 | * var vec2 = new Victor(2, 2);
568 | *
569 | * vec.multiply(vec2);
570 | * vec.toString();
571 | * // => x:200, y:100
572 | *
573 | * @param {Victor} vector The vector to multiply by
574 | * @return {Victor} `this` for chaining capabilities
575 | * @api public
576 | */
577 | Victor.prototype.multiply = function (vector) {
578 | this.x *= vector.x;
579 | this.y *= vector.y;
580 | return this;
581 | };
582 |
583 | /**
584 | * Multiplies both vector axis by the given scalar value
585 | *
586 | * ### Examples:
587 | * var vec = new Victor(100, 50);
588 | *
589 | * vec.multiplyScalar(2);
590 | * vec.toString();
591 | * // => x:200, y:100
592 | *
593 | * @param {Number} The scalar to multiply by
594 | * @return {Victor} `this` for chaining capabilities
595 | * @api public
596 | */
597 | Victor.prototype.multiplyScalar = function (scalar) {
598 | this.x *= scalar;
599 | this.y *= scalar;
600 | return this;
601 | };
602 |
603 | /**
604 | * Multiplies the X axis by the given scalar
605 | *
606 | * ### Examples:
607 | * var vec = new Victor(100, 50);
608 | *
609 | * vec.multiplyScalarX(2);
610 | * vec.toString();
611 | * // => x:200, y:50
612 | *
613 | * @param {Number} The scalar to multiply the axis with
614 | * @return {Victor} `this` for chaining capabilities
615 | * @api public
616 | */
617 | Victor.prototype.multiplyScalarX = function (scalar) {
618 | this.x *= scalar;
619 | return this;
620 | };
621 |
622 | /**
623 | * Multiplies the Y axis by the given scalar
624 | *
625 | * ### Examples:
626 | * var vec = new Victor(100, 50);
627 | *
628 | * vec.multiplyScalarY(2);
629 | * vec.toString();
630 | * // => x:100, y:100
631 | *
632 | * @param {Number} The scalar to multiply the axis with
633 | * @return {Victor} `this` for chaining capabilities
634 | * @api public
635 | */
636 | Victor.prototype.multiplyScalarY = function (scalar) {
637 | this.y *= scalar;
638 | return this;
639 | };
640 |
641 | /**
642 | * Normalize
643 | *
644 | * @return {Victor} `this` for chaining capabilities
645 | * @api public
646 | */
647 | Victor.prototype.normalize = function () {
648 | var length = this.length();
649 |
650 | if (length === 0) {
651 | this.x = 1;
652 | this.y = 0;
653 | } else {
654 | this.divide(Victor(length, length));
655 | }
656 | return this;
657 | };
658 |
659 | Victor.prototype.norm = Victor.prototype.normalize;
660 |
661 | /**
662 | * If the absolute vector axis is greater than `max`, multiplies the axis by `factor`
663 | *
664 | * ### Examples:
665 | * var vec = new Victor(100, 50);
666 | *
667 | * vec.limit(80, 0.9);
668 | * vec.toString();
669 | * // => x:90, y:50
670 | *
671 | * @param {Number} max The maximum value for both x and y axis
672 | * @param {Number} factor Factor by which the axis are to be multiplied with
673 | * @return {Victor} `this` for chaining capabilities
674 | * @api public
675 | */
676 | Victor.prototype.limit = function (max, factor) {
677 | if (Math.abs(this.x) > max){ this.x *= factor; }
678 | if (Math.abs(this.y) > max){ this.y *= factor; }
679 | return this;
680 | };
681 |
682 | /**
683 | * Randomizes both vector axis with a value between 2 vectors
684 | *
685 | * ### Examples:
686 | * var vec = new Victor(100, 50);
687 | *
688 | * vec.randomize(new Victor(50, 60), new Victor(70, 80`));
689 | * vec.toString();
690 | * // => x:67, y:73
691 | *
692 | * @param {Victor} topLeft first vector
693 | * @param {Victor} bottomRight second vector
694 | * @return {Victor} `this` for chaining capabilities
695 | * @api public
696 | */
697 | Victor.prototype.randomize = function (topLeft, bottomRight) {
698 | this.randomizeX(topLeft, bottomRight);
699 | this.randomizeY(topLeft, bottomRight);
700 |
701 | return this;
702 | };
703 |
704 | /**
705 | * Randomizes the y axis with a value between 2 vectors
706 | *
707 | * ### Examples:
708 | * var vec = new Victor(100, 50);
709 | *
710 | * vec.randomizeX(new Victor(50, 60), new Victor(70, 80`));
711 | * vec.toString();
712 | * // => x:55, y:50
713 | *
714 | * @param {Victor} topLeft first vector
715 | * @param {Victor} bottomRight second vector
716 | * @return {Victor} `this` for chaining capabilities
717 | * @api public
718 | */
719 | Victor.prototype.randomizeX = function (topLeft, bottomRight) {
720 | var min = Math.min(topLeft.x, bottomRight.x);
721 | var max = Math.max(topLeft.x, bottomRight.x);
722 | this.x = random(min, max);
723 | return this;
724 | };
725 |
726 | /**
727 | * Randomizes the y axis with a value between 2 vectors
728 | *
729 | * ### Examples:
730 | * var vec = new Victor(100, 50);
731 | *
732 | * vec.randomizeY(new Victor(50, 60), new Victor(70, 80`));
733 | * vec.toString();
734 | * // => x:100, y:66
735 | *
736 | * @param {Victor} topLeft first vector
737 | * @param {Victor} bottomRight second vector
738 | * @return {Victor} `this` for chaining capabilities
739 | * @api public
740 | */
741 | Victor.prototype.randomizeY = function (topLeft, bottomRight) {
742 | var min = Math.min(topLeft.y, bottomRight.y);
743 | var max = Math.max(topLeft.y, bottomRight.y);
744 | this.y = random(min, max);
745 | return this;
746 | };
747 |
748 | /**
749 | * Randomly randomizes either axis between 2 vectors
750 | *
751 | * ### Examples:
752 | * var vec = new Victor(100, 50);
753 | *
754 | * vec.randomizeAny(new Victor(50, 60), new Victor(70, 80));
755 | * vec.toString();
756 | * // => x:100, y:77
757 | *
758 | * @param {Victor} topLeft first vector
759 | * @param {Victor} bottomRight second vector
760 | * @return {Victor} `this` for chaining capabilities
761 | * @api public
762 | */
763 | Victor.prototype.randomizeAny = function (topLeft, bottomRight) {
764 | if (!! Math.round(Math.random())) {
765 | this.randomizeX(topLeft, bottomRight);
766 | } else {
767 | this.randomizeY(topLeft, bottomRight);
768 | }
769 | return this;
770 | };
771 |
772 | /**
773 | * Rounds both axis to an integer value
774 | *
775 | * ### Examples:
776 | * var vec = new Victor(100.2, 50.9);
777 | *
778 | * vec.unfloat();
779 | * vec.toString();
780 | * // => x:100, y:51
781 | *
782 | * @return {Victor} `this` for chaining capabilities
783 | * @api public
784 | */
785 | Victor.prototype.unfloat = function () {
786 | this.x = Math.round(this.x);
787 | this.y = Math.round(this.y);
788 | return this;
789 | };
790 |
791 | /**
792 | * Rounds both axis to a certain precision
793 | *
794 | * ### Examples:
795 | * var vec = new Victor(100.2, 50.9);
796 | *
797 | * vec.unfloat();
798 | * vec.toString();
799 | * // => x:100, y:51
800 | *
801 | * @param {Number} Precision (default: 8)
802 | * @return {Victor} `this` for chaining capabilities
803 | * @api public
804 | */
805 | Victor.prototype.toFixed = function (precision) {
806 | if (typeof precision === 'undefined') { precision = 8; }
807 | this.x = this.x.toFixed(precision);
808 | this.y = this.y.toFixed(precision);
809 | return this;
810 | };
811 |
812 | /**
813 | * Performs a linear blend / interpolation of the X axis towards another vector
814 | *
815 | * ### Examples:
816 | * var vec1 = new Victor(100, 100);
817 | * var vec2 = new Victor(200, 200);
818 | *
819 | * vec1.mixX(vec2, 0.5);
820 | * vec.toString();
821 | * // => x:150, y:100
822 | *
823 | * @param {Victor} vector The other vector
824 | * @param {Number} amount The blend amount (optional, default: 0.5)
825 | * @return {Victor} `this` for chaining capabilities
826 | * @api public
827 | */
828 | Victor.prototype.mixX = function (vec, amount) {
829 | if (typeof amount === 'undefined') {
830 | amount = 0.5;
831 | }
832 |
833 | this.x = (1 - amount) * this.x + amount * vec.x;
834 | return this;
835 | };
836 |
837 | /**
838 | * Performs a linear blend / interpolation of the Y axis towards another vector
839 | *
840 | * ### Examples:
841 | * var vec1 = new Victor(100, 100);
842 | * var vec2 = new Victor(200, 200);
843 | *
844 | * vec1.mixY(vec2, 0.5);
845 | * vec.toString();
846 | * // => x:100, y:150
847 | *
848 | * @param {Victor} vector The other vector
849 | * @param {Number} amount The blend amount (optional, default: 0.5)
850 | * @return {Victor} `this` for chaining capabilities
851 | * @api public
852 | */
853 | Victor.prototype.mixY = function (vec, amount) {
854 | if (typeof amount === 'undefined') {
855 | amount = 0.5;
856 | }
857 |
858 | this.y = (1 - amount) * this.y + amount * vec.y;
859 | return this;
860 | };
861 |
862 | /**
863 | * Performs a linear blend / interpolation towards another vector
864 | *
865 | * ### Examples:
866 | * var vec1 = new Victor(100, 100);
867 | * var vec2 = new Victor(200, 200);
868 | *
869 | * vec1.mix(vec2, 0.5);
870 | * vec.toString();
871 | * // => x:150, y:150
872 | *
873 | * @param {Victor} vector The other vector
874 | * @param {Number} amount The blend amount (optional, default: 0.5)
875 | * @return {Victor} `this` for chaining capabilities
876 | * @api public
877 | */
878 | Victor.prototype.mix = function (vec, amount) {
879 | this.mixX(vec, amount);
880 | this.mixY(vec, amount);
881 | return this;
882 | };
883 |
884 | /**
885 | * # Products
886 | */
887 |
888 | /**
889 | * Creates a clone of this vector
890 | *
891 | * ### Examples:
892 | * var vec1 = new Victor(10, 10);
893 | * var vec2 = vec1.clone();
894 | *
895 | * vec2.toString();
896 | * // => x:10, y:10
897 | *
898 | * @return {Victor} A clone of the vector
899 | * @api public
900 | */
901 | Victor.prototype.clone = function () {
902 | return new Victor(this.x, this.y);
903 | };
904 |
905 | /**
906 | * Copies another vector's X component in to its own
907 | *
908 | * ### Examples:
909 | * var vec1 = new Victor(10, 10);
910 | * var vec2 = new Victor(20, 20);
911 | * var vec2 = vec1.copyX(vec1);
912 | *
913 | * vec2.toString();
914 | * // => x:20, y:10
915 | *
916 | * @return {Victor} `this` for chaining capabilities
917 | * @api public
918 | */
919 | Victor.prototype.copyX = function (vec) {
920 | this.x = vec.x;
921 | return this;
922 | };
923 |
924 | /**
925 | * Copies another vector's Y component in to its own
926 | *
927 | * ### Examples:
928 | * var vec1 = new Victor(10, 10);
929 | * var vec2 = new Victor(20, 20);
930 | * var vec2 = vec1.copyY(vec1);
931 | *
932 | * vec2.toString();
933 | * // => x:10, y:20
934 | *
935 | * @return {Victor} `this` for chaining capabilities
936 | * @api public
937 | */
938 | Victor.prototype.copyY = function (vec) {
939 | this.y = vec.y;
940 | return this;
941 | };
942 |
943 | /**
944 | * Copies another vector's X and Y components in to its own
945 | *
946 | * ### Examples:
947 | * var vec1 = new Victor(10, 10);
948 | * var vec2 = new Victor(20, 20);
949 | * var vec2 = vec1.copy(vec1);
950 | *
951 | * vec2.toString();
952 | * // => x:20, y:20
953 | *
954 | * @return {Victor} `this` for chaining capabilities
955 | * @api public
956 | */
957 | Victor.prototype.copy = function (vec) {
958 | this.copyX(vec);
959 | this.copyY(vec);
960 | return this;
961 | };
962 |
963 | /**
964 | * Sets the vector to zero (0,0)
965 | *
966 | * ### Examples:
967 | * var vec1 = new Victor(10, 10);
968 | * var1.zero();
969 | * vec1.toString();
970 | * // => x:0, y:0
971 | *
972 | * @return {Victor} `this` for chaining capabilities
973 | * @api public
974 | */
975 | Victor.prototype.zero = function () {
976 | this.x = this.y = 0;
977 | return this;
978 | };
979 |
980 | /**
981 | * Calculates the dot product of this vector and another
982 | *
983 | * ### Examples:
984 | * var vec1 = new Victor(100, 50);
985 | * var vec2 = new Victor(200, 60);
986 | *
987 | * vec1.dot(vec2);
988 | * // => 23000
989 | *
990 | * @param {Victor} vector The second vector
991 | * @return {Number} Dot product
992 | * @api public
993 | */
994 | Victor.prototype.dot = function (vec2) {
995 | return this.x * vec2.x + this.y * vec2.y;
996 | };
997 |
998 | Victor.prototype.cross = function (vec2) {
999 | return (this.x * vec2.y ) - (this.y * vec2.x );
1000 | };
1001 |
1002 | /**
1003 | * Projects a vector onto another vector, setting itself to the result.
1004 | *
1005 | * ### Examples:
1006 | * var vec = new Victor(100, 0);
1007 | * var vec2 = new Victor(100, 100);
1008 | *
1009 | * vec.projectOnto(vec2);
1010 | * vec.toString();
1011 | * // => x:50, y:50
1012 | *
1013 | * @param {Victor} vector The other vector you want to project this vector onto
1014 | * @return {Victor} `this` for chaining capabilities
1015 | * @api public
1016 | */
1017 | Victor.prototype.projectOnto = function (vec2) {
1018 | var coeff = ( (this.x * vec2.x)+(this.y * vec2.y) ) / ((vec2.x*vec2.x)+(vec2.y*vec2.y));
1019 | this.x = coeff * vec2.x;
1020 | this.y = coeff * vec2.y;
1021 | return this;
1022 | };
1023 |
1024 |
1025 | Victor.prototype.horizontalAngle = function () {
1026 | return Math.atan2(this.y, this.x);
1027 | };
1028 |
1029 | Victor.prototype.horizontalAngleDeg = function () {
1030 | return radian2degrees(this.horizontalAngle());
1031 | };
1032 |
1033 | Victor.prototype.verticalAngle = function () {
1034 | return Math.atan2(this.x, this.y);
1035 | };
1036 |
1037 | Victor.prototype.verticalAngleDeg = function () {
1038 | return radian2degrees(this.verticalAngle());
1039 | };
1040 |
1041 | Victor.prototype.angle = Victor.prototype.horizontalAngle;
1042 | Victor.prototype.angleDeg = Victor.prototype.horizontalAngleDeg;
1043 | Victor.prototype.direction = Victor.prototype.horizontalAngle;
1044 |
1045 | Victor.prototype.rotate = function (angle) {
1046 | var nx = (this.x * Math.cos(angle)) - (this.y * Math.sin(angle));
1047 | var ny = (this.x * Math.sin(angle)) + (this.y * Math.cos(angle));
1048 |
1049 | this.x = nx;
1050 | this.y = ny;
1051 |
1052 | return this;
1053 | };
1054 |
1055 | Victor.prototype.rotateDeg = function (angle) {
1056 | angle = degrees2radian(angle);
1057 | return this.rotate(angle);
1058 | };
1059 |
1060 | Victor.prototype.rotateTo = function(rotation) {
1061 | return this.rotate(rotation-this.angle());
1062 | };
1063 |
1064 | Victor.prototype.rotateToDeg = function(rotation) {
1065 | rotation = degrees2radian(rotation);
1066 | return this.rotateTo(rotation);
1067 | };
1068 |
1069 | Victor.prototype.rotateBy = function (rotation) {
1070 | var angle = this.angle() + rotation;
1071 |
1072 | return this.rotate(angle);
1073 | };
1074 |
1075 | Victor.prototype.rotateByDeg = function (rotation) {
1076 | rotation = degrees2radian(rotation);
1077 | return this.rotateBy(rotation);
1078 | };
1079 |
1080 | /**
1081 | * Calculates the distance of the X axis between this vector and another
1082 | *
1083 | * ### Examples:
1084 | * var vec1 = new Victor(100, 50);
1085 | * var vec2 = new Victor(200, 60);
1086 | *
1087 | * vec1.distanceX(vec2);
1088 | * // => -100
1089 | *
1090 | * @param {Victor} vector The second vector
1091 | * @return {Number} Distance
1092 | * @api public
1093 | */
1094 | Victor.prototype.distanceX = function (vec) {
1095 | return this.x - vec.x;
1096 | };
1097 |
1098 | /**
1099 | * Same as `distanceX()` but always returns an absolute number
1100 | *
1101 | * ### Examples:
1102 | * var vec1 = new Victor(100, 50);
1103 | * var vec2 = new Victor(200, 60);
1104 | *
1105 | * vec1.absDistanceX(vec2);
1106 | * // => 100
1107 | *
1108 | * @param {Victor} vector The second vector
1109 | * @return {Number} Absolute distance
1110 | * @api public
1111 | */
1112 | Victor.prototype.absDistanceX = function (vec) {
1113 | return Math.abs(this.distanceX(vec));
1114 | };
1115 |
1116 | /**
1117 | * Calculates the distance of the Y axis between this vector and another
1118 | *
1119 | * ### Examples:
1120 | * var vec1 = new Victor(100, 50);
1121 | * var vec2 = new Victor(200, 60);
1122 | *
1123 | * vec1.distanceY(vec2);
1124 | * // => -10
1125 | *
1126 | * @param {Victor} vector The second vector
1127 | * @return {Number} Distance
1128 | * @api public
1129 | */
1130 | Victor.prototype.distanceY = function (vec) {
1131 | return this.y - vec.y;
1132 | };
1133 |
1134 | /**
1135 | * Same as `distanceY()` but always returns an absolute number
1136 | *
1137 | * ### Examples:
1138 | * var vec1 = new Victor(100, 50);
1139 | * var vec2 = new Victor(200, 60);
1140 | *
1141 | * vec1.distanceY(vec2);
1142 | * // => 10
1143 | *
1144 | * @param {Victor} vector The second vector
1145 | * @return {Number} Absolute distance
1146 | * @api public
1147 | */
1148 | Victor.prototype.absDistanceY = function (vec) {
1149 | return Math.abs(this.distanceY(vec));
1150 | };
1151 |
1152 | /**
1153 | * Calculates the euclidean distance between this vector and another
1154 | *
1155 | * ### Examples:
1156 | * var vec1 = new Victor(100, 50);
1157 | * var vec2 = new Victor(200, 60);
1158 | *
1159 | * vec1.distance(vec2);
1160 | * // => 100.4987562112089
1161 | *
1162 | * @param {Victor} vector The second vector
1163 | * @return {Number} Distance
1164 | * @api public
1165 | */
1166 | Victor.prototype.distance = function (vec) {
1167 | return Math.sqrt(this.distanceSq(vec));
1168 | };
1169 |
1170 | /**
1171 | * Calculates the squared euclidean distance between this vector and another
1172 | *
1173 | * ### Examples:
1174 | * var vec1 = new Victor(100, 50);
1175 | * var vec2 = new Victor(200, 60);
1176 | *
1177 | * vec1.distanceSq(vec2);
1178 | * // => 10100
1179 | *
1180 | * @param {Victor} vector The second vector
1181 | * @return {Number} Distance
1182 | * @api public
1183 | */
1184 | Victor.prototype.distanceSq = function (vec) {
1185 | var dx = this.distanceX(vec),
1186 | dy = this.distanceY(vec);
1187 |
1188 | return dx * dx + dy * dy;
1189 | };
1190 |
1191 | /**
1192 | * Calculates the length or magnitude of the vector
1193 | *
1194 | * ### Examples:
1195 | * var vec = new Victor(100, 50);
1196 | *
1197 | * vec.length();
1198 | * // => 111.80339887498948
1199 | *
1200 | * @return {Number} Length / Magnitude
1201 | * @api public
1202 | */
1203 | Victor.prototype.length = function () {
1204 | return Math.sqrt(this.lengthSq());
1205 | };
1206 |
1207 | /**
1208 | * Squared length / magnitude
1209 | *
1210 | * ### Examples:
1211 | * var vec = new Victor(100, 50);
1212 | *
1213 | * vec.lengthSq();
1214 | * // => 12500
1215 | *
1216 | * @return {Number} Length / Magnitude
1217 | * @api public
1218 | */
1219 | Victor.prototype.lengthSq = function () {
1220 | return this.x * this.x + this.y * this.y;
1221 | };
1222 |
1223 | Victor.prototype.magnitude = Victor.prototype.length;
1224 |
1225 | /**
1226 | * Returns a true if vector is (0, 0)
1227 | *
1228 | * ### Examples:
1229 | * var vec = new Victor(100, 50);
1230 | * vec.zero();
1231 | *
1232 | * // => true
1233 | *
1234 | * @return {Boolean}
1235 | * @api public
1236 | */
1237 | Victor.prototype.isZero = function() {
1238 | return this.x === 0 && this.y === 0;
1239 | };
1240 |
1241 | /**
1242 | * Returns a true if this vector is the same as another
1243 | *
1244 | * ### Examples:
1245 | * var vec1 = new Victor(100, 50);
1246 | * var vec2 = new Victor(100, 50);
1247 | * vec1.isEqualTo(vec2);
1248 | *
1249 | * // => true
1250 | *
1251 | * @return {Boolean}
1252 | * @api public
1253 | */
1254 | Victor.prototype.isEqualTo = function(vec2) {
1255 | return this.x === vec2.x && this.y === vec2.y;
1256 | };
1257 |
1258 | /**
1259 | * # Utility Methods
1260 | */
1261 |
1262 | /**
1263 | * Returns an string representation of the vector
1264 | *
1265 | * ### Examples:
1266 | * var vec = new Victor(10, 20);
1267 | *
1268 | * vec.toString();
1269 | * // => x:10, y:20
1270 | *
1271 | * @return {String}
1272 | * @api public
1273 | */
1274 | Victor.prototype.toString = function () {
1275 | return 'x:' + this.x + ', y:' + this.y;
1276 | };
1277 |
1278 | /**
1279 | * Returns an array representation of the vector
1280 | *
1281 | * ### Examples:
1282 | * var vec = new Victor(10, 20);
1283 | *
1284 | * vec.toArray();
1285 | * // => [10, 20]
1286 | *
1287 | * @return {Array}
1288 | * @api public
1289 | */
1290 | Victor.prototype.toArray = function () {
1291 | return [ this.x, this.y ];
1292 | };
1293 |
1294 | /**
1295 | * Returns an object representation of the vector
1296 | *
1297 | * ### Examples:
1298 | * var vec = new Victor(10, 20);
1299 | *
1300 | * vec.toObject();
1301 | * // => { x: 10, y: 20 }
1302 | *
1303 | * @return {Object}
1304 | * @api public
1305 | */
1306 | Victor.prototype.toObject = function () {
1307 | return { x: this.x, y: this.y };
1308 | };
1309 |
1310 |
1311 | var degrees = 180 / Math.PI;
1312 |
1313 | function random (min, max) {
1314 | return Math.floor(Math.random() * (max - min + 1) + min);
1315 | }
1316 |
1317 | function radian2degrees (rad) {
1318 | return rad * degrees;
1319 | }
1320 |
1321 | function degrees2radian (deg) {
1322 | return deg / degrees;
1323 | }
1324 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vnodes",
3 | "version": "2.0.0",
4 | "scripts": {
5 | "dev": "vite --host",
6 | "build": "vite build",
7 | "pages": "vite build && rm -rf docs && mv dist docs && git add . && git commit -m pages && git push",
8 | "start": "npm run dev"
9 | },
10 | "license": "MIT",
11 | "repository": "github:txlabs/vnodes",
12 | "author": {
13 | "name": "txlabs",
14 | "email": "txlabs@protonmail.com"
15 | },
16 | "keywords": [
17 | "vue",
18 | "graph",
19 | "nodes",
20 | "diagram",
21 | "dag",
22 | "visualization",
23 | "tree",
24 | "visual tools",
25 | "svg",
26 | "flowchart"
27 | ],
28 | "dependencies": {
29 | "d3-dag": "^0.2.6",
30 | "d3-flextree": "^2.1.2",
31 | "tiny-emitter": "^2.1.0"
32 | },
33 | "devDependencies": {
34 | "@codemirror/lang-css": "^6.0.2",
35 | "@vitejs/plugin-vue": "^4.0.0",
36 | "css-parse": "^2.0.0",
37 | "javascript-stringify": "^1.6.0",
38 | "pretty": "^2.0.0",
39 | "stats.js": "^0.17.0",
40 | "vite": "^4.1.4",
41 | "vue": "^3.2.47",
42 | "vue-codemirror": "^6.1.1",
43 | "vue-template-compiler": "^2.6.10"
44 | },
45 | "browserslist": [
46 | "> 1%",
47 | "last 2 versions"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiagolr/vnodes/7e6444e5cb5f5ad283b54457085fd6ad13f56923/public/favicon.ico
--------------------------------------------------------------------------------
/src/components/Edge.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
177 |
178 |
185 |
--------------------------------------------------------------------------------
/src/components/Group.vue:
--------------------------------------------------------------------------------
1 |
2 |