├── .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 | 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 | 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 | 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 | 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 | 5 | 6 | 177 | 178 | 185 | -------------------------------------------------------------------------------- /src/components/Group.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/Label.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 119 | 120 | 125 | -------------------------------------------------------------------------------- /src/components/Marker.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 87 | 88 | -------------------------------------------------------------------------------- /src/components/Node.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 77 | 78 | -------------------------------------------------------------------------------- /src/components/Port.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 72 | 73 | -------------------------------------------------------------------------------- /src/components/Screen.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 168 | 169 | 194 | -------------------------------------------------------------------------------- /src/demo/Benchmark.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 78 | 79 | -------------------------------------------------------------------------------- /src/demo/Demo.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 63 | 64 | 75 | 76 | 122 | -------------------------------------------------------------------------------- /src/demo/Edit.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 161 | 162 | -------------------------------------------------------------------------------- /src/demo/Groups.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 96 | 97 | 106 | 107 | -------------------------------------------------------------------------------- /src/demo/Labels.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 108 | -------------------------------------------------------------------------------- /src/demo/Markers.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 179 | 180 | -------------------------------------------------------------------------------- /src/demo/Ports.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 229 | 230 | 256 | 257 | 275 | 276 | -------------------------------------------------------------------------------- /src/demo/PortsPort.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiagolr/vnodes/7e6444e5cb5f5ad283b54457085fd6ad13f56923/src/demo/PortsPort.vue -------------------------------------------------------------------------------- /src/demo/Sink.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 51 | 52 | -------------------------------------------------------------------------------- /src/demo/SinkSidebar.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 223 | -------------------------------------------------------------------------------- /src/demo/Styles.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 194 | 195 | 200 | 201 | -------------------------------------------------------------------------------- /src/graph.js: -------------------------------------------------------------------------------- 1 | import util from './util' 2 | import { flextree } from 'd3-flextree' 3 | 4 | export default class Graph { 5 | constructor () { 6 | this.nodes = [] 7 | this.edges = [] 8 | } 9 | 10 | positionNode ({ node, parent, dir = 'right', spacing = 40, invertOffset = false } = {}) { 11 | node = typeof node === 'string' ? this.nodes.find(n => n.id === node) : node 12 | parent = typeof parent === 'string' ? this.nodes.find(n => n.id === parent) : parent 13 | const pos = util.findPosition(node, parent, dir, this.nodes, spacing, invertOffset) 14 | this.updateNode(node, { x: pos.x, y: pos.y }) 15 | } 16 | 17 | graphNodes ({ nodes, edges, type = 'basic', dir = 'right', spacing = 40 } = {}) { 18 | nodes = nodes || this.nodes 19 | edges = edges || this.edges 20 | 21 | const dag = util.createDAG(nodes, edges) // removes cycles if any 22 | if (!dag.length) { 23 | return 24 | } 25 | 26 | if (type === 'basic' || type === 'basic-invert') { 27 | const visited = {} 28 | const findPos = (node, parent) => { 29 | if (visited[node.id]) { 30 | return 31 | } 32 | const collisions = nodes.filter(n => !!visited[n.id]) 33 | const pos = util.findPosition(node, parent, dir, collisions, spacing, type === 'basic-invert') 34 | node.x = pos.x 35 | node.y = pos.y 36 | this.updateNode(node.id, { 37 | x: node.x, 38 | y: node.y 39 | }) 40 | visited[node.id] = true 41 | node.children.forEach(n => findPos(n, node)) 42 | } 43 | dag 44 | .filter(node => !node.parentIds.length) 45 | .forEach(node => findPos(node, null)) 46 | } else 47 | 48 | if (type === 'tree') { 49 | const layout = flextree() 50 | const flipH = (dir === 'left' || dir === 'right') 51 | const roots = dag.filter(n => !n.parentIds.length) 52 | roots.forEach(root => { 53 | const graph = [] 54 | const offsetX = root.x 55 | const offsetY = root.y 56 | util.dagToFlextree(root, graph, flipH, spacing) 57 | const tree = layout.hierarchy(graph[0]) 58 | layout(tree) 59 | // apply layout to nodes 60 | const invertX = dir === 'left' ? -1 : 1 61 | const invertY = dir === 'up' ? -1 : 1 62 | const applyChanges = n => { 63 | this.updateNode(n.data.id, { 64 | x: (flipH ? n.y : n.x) * invertX + offsetX, 65 | y: (flipH ? n.x : n.y) * invertY + offsetY 66 | }) 67 | n.children && n.children.forEach(applyChanges) 68 | } 69 | applyChanges(tree) 70 | }) 71 | } else { 72 | throw new Error('unknown layout type ' + type) 73 | } 74 | } 75 | 76 | reset () { 77 | this.edges = [] 78 | this.nodes = [] 79 | } 80 | 81 | createNode (fields = {}) { 82 | if (typeof fields === 'string') { 83 | fields = { id: fields } // support a single id string or an object as params 84 | } 85 | const node = Object.assign({ 86 | id: Math.random().toString(36).slice(2), 87 | x: 0, 88 | y: 0, 89 | width: 50, 90 | height: 50, 91 | }, fields) 92 | 93 | this.nodes.push(node) 94 | return node 95 | } 96 | 97 | updateNode (node, fields = {}) { 98 | if (typeof node === 'string') node = this.nodes.find(n => n.id === node) 99 | if (!node) throw new Error(`node ${node} does not exist`) 100 | return Object.assign(node, fields) 101 | } 102 | 103 | removeNode (node) { 104 | const index = this.nodes.indexOf(node) 105 | if (index > -1) { 106 | this.nodes.splice(index, 1) 107 | } 108 | return index 109 | } 110 | 111 | createEdge (from, to, fields = {}) { 112 | if (arguments.length === 1) { 113 | // support calling with single argument 114 | fields = arguments[0] 115 | from = fields.from 116 | to = fields.to 117 | } else { 118 | // support passing node objects instead of ids 119 | if (typeof from === 'object') from = from.id 120 | if (typeof to === 'object') to = to.id 121 | } 122 | if (!from) throw new Error('orig required') 123 | if (!to) throw new Error('dest required') 124 | 125 | const edge = Object.assign({ 126 | id: Math.random().toString(36).slice(2), 127 | from, 128 | to, 129 | fromAnchor: { x: '50%', y: '50%' }, 130 | toAnchor: { x: '50%', y: '50%' }, 131 | type: 'linear', 132 | pathd: '', // reactive path 133 | }, fields) 134 | 135 | this.edges.push(edge) 136 | return edge 137 | } 138 | 139 | updateEdge (edge, fields) { 140 | return Object.assign(edge, fields) 141 | } 142 | 143 | removeEdge (edge) { 144 | const index = this.edges.indexOf(edge) 145 | if (index > -1) { 146 | this.edges.splice(index, 1) 147 | } 148 | return index 149 | } 150 | 151 | /** 152 | * Force-directed layout by @emeric254 153 | */ 154 | reorderGraph() { 155 | for(let i = 0; i < 200; i++){ 156 | let n = 0 157 | for(let node of this.nodes){ 158 | for (let otherNode of this.nodes) { 159 | if(otherNode.id === node.id){ 160 | continue 161 | } 162 | else{ 163 | // distance(offset) between two nodes 164 | let dx = otherNode.x - node.x; 165 | let dy = otherNode.y - node.y; 166 | let offset = Math.sqrt(dx * dx + dy * dy); 167 | // if nodes is linked 168 | if(this.edges.find(edge => { 169 | return (edge.to === node.id && edge.from === otherNode.id) || 170 | (edge.from === node.id && edge.to === otherNode.id) 171 | })){ 172 | if(offset < 500){ 173 | if (offset < 400) { 174 | if (offset < 200) { 175 | // if linked nodes is so close, up distance between them 176 | node.x -= dx; 177 | node.y -= dy; 178 | } else { 179 | node.x -= dx / 3; 180 | node.y -= dy / 3; 181 | } 182 | } else { 183 | //if linked nodes have medium distance, make them little closer and up force count 184 | n++ 185 | node.x += dx / 6; 186 | node.y += dy / 6; 187 | } 188 | } 189 | else{ 190 | //if linked nodes have long distance, make them closer 191 | node.x += dx / 3; 192 | node.y += dy / 3; 193 | } 194 | } 195 | // if nodes isn't linked 196 | else{ 197 | if(offset < 1000){ 198 | if(offset < 100){ 199 | // if unlinked nodes is very close, make them much further 200 | node.x -= dx * 3; 201 | node.y -= dy * 3; 202 | } 203 | else{ 204 | // if unlinked nodes is medium close, make them further 205 | if(offset < 500){ 206 | node.x -= dx / 3; 207 | node.y -= dy / 3; 208 | } 209 | else{ 210 | // if unlinked nodes is not so close, make them little further 211 | node.x -= dx / 9; 212 | node.y -= dy / 9; 213 | } 214 | } 215 | } 216 | else{ 217 | // if distance between nodes is so long, up force count 218 | n++ 219 | } 220 | } 221 | } 222 | } 223 | // move all nodes to start of coordinates 224 | let dx = 0 - node.x 225 | let dy = 0 - node.y 226 | node.x += dx / 15 227 | node.y += dy / 15 228 | } 229 | //if force of the nodes is 70% of all graph, graph is reorder 230 | if(n / (this.nodes.length * this.nodes.length) > 0.7){ 231 | break 232 | } 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import Demo from './demo/Demo.vue' 3 | 4 | const demo = createApp(Demo) 5 | demo.mount('#app') 6 | -------------------------------------------------------------------------------- /src/mixins/drag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds drag behavior to Vue component 3 | * @drag event emmited 4 | */ 5 | export default { 6 | props: { 7 | dragThreshold: { 8 | type: Number, 9 | default: 2 10 | } 11 | }, 12 | data () { 13 | return { 14 | drag: { 15 | zoom: 1, 16 | active: false, 17 | prev: { x: 0, y: 0 }, 18 | threshold: { x: 0, y: 0, crossed: false } 19 | } 20 | } 21 | }, 22 | methods: { 23 | preventClicks (e) { 24 | if (this.drag.threshold.crossed) { 25 | e.preventDefault() 26 | e.stopPropagation() 27 | e.stopImmediatePropagation() 28 | document.removeEventListener('click', this.preventClicks, true) 29 | } 30 | }, 31 | startDrag (e) { 32 | let parent = this.$parent 33 | while (parent) { 34 | if (parent.panzoom) { 35 | this.drag.zoom = parent.panzoom.getZoom() 36 | break; 37 | } 38 | parent = parent.$parent 39 | } 40 | // touch normalize 41 | if (e.touches && e.touches.length) { 42 | e.clientX = e.touches[0].clientX 43 | e.clientY = e.touches[0].clientY 44 | } 45 | this.drag.active = true 46 | this.drag.prev = { x: e.clientX, y: e.clientY } 47 | this.drag.threshold = {x: 0, y: 0, crossed: false} 48 | document.addEventListener('mouseup', this.stopDrag) 49 | document.addEventListener('touchend', this.stopDrag) 50 | document.addEventListener('mousemove', this.applyDrag, { passive: true }) 51 | document.addEventListener('touchmove', this.applyDrag, { passive: true }) 52 | document.addEventListener('click', this.preventClicks, true) 53 | }, 54 | stopDrag () { 55 | this.drag.active = false 56 | document.removeEventListener('mouseup', this.stopDrag) 57 | document.removeEventListener('touchend', this.stopDrag) 58 | document.removeEventListener('mousemove', this.applyDrag, { passive: true }) 59 | document.removeEventListener('touchmove', this.applyDrag, { passive: true }) 60 | }, 61 | applyDrag (e) { 62 | if (e.touches && e.touches.length) { 63 | e.clientX = e.touches[0].clientX 64 | e.clientY = e.touches[0].clientY 65 | } 66 | let x = (e.clientX - this.drag.prev.x) / this.drag.zoom 67 | let y = (e.clientY - this.drag.prev.y) / this.drag.zoom 68 | this.drag.prev = {x: e.clientX, y: e.clientY} 69 | 70 | if (!this.drag.threshold.crossed) { 71 | if (Math.abs(this.drag.threshold.x) < this.dragThreshold && Math.abs(this.drag.threshold.y) < this.dragThreshold) { 72 | this.drag.threshold.x += x 73 | this.drag.threshold.y += y 74 | return // don't apply drag until threshold is reached 75 | } else { 76 | this.drag.threshold.crossed = true 77 | x += this.drag.threshold.x 78 | y += this.drag.threshold.y 79 | } 80 | } 81 | this.onDrag({ x, y }) 82 | }, 83 | }, 84 | beforeDestroy () { 85 | document.removeEventListener('mousemove', this.applyDrag) 86 | document.removeEventListener('mouseup', this.stopDrag) 87 | }, 88 | } 89 | -------------------------------------------------------------------------------- /src/themes/cssutil.js: -------------------------------------------------------------------------------- 1 | import parse from 'css-parse' 2 | 3 | module.exports = function (name="") { 4 | const themes = { 5 | // load themes here 6 | } 7 | 8 | /** 9 | * Applies a set of css rules on elements of the current page 10 | * @id the name of the theme 11 | */ 12 | 13 | // many ways to swap stylesheets 14 | // https://www.rainbodesign.com/pub/css/css-javascript.html 15 | function applyTheme (id='light', rootComponent=null, rootSel='body') { 16 | let rules 17 | try { 18 | rules = parse(this.theme) 19 | .stylesheet.rules 20 | .filter(r => r.type === 'rule') 21 | } catch (e) { 22 | return; 23 | } 24 | 25 | // TRY 26 | // ref.$forceUpdate() 27 | // ref.$mount() 28 | // await this.forceRender(); - clear last applied theme 29 | 30 | // no need, just apply/remove the stylesheet 31 | 32 | rules.forEach(rule => { 33 | const sel = rule.selectors.length ? '#styles-demo ' + rule.selectors.join(', ') : '' 34 | const els = [...document.querySelectorAll(sel)] 35 | rule.declarations 36 | .filter(dec => dec.type === 'declaration') 37 | .forEach(dec => { 38 | els.filter(el => el).forEach(el => { 39 | const prop = dec.property.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); 40 | el.style[prop] = dec.value 41 | }) 42 | }) 43 | }) 44 | } 45 | 46 | return { 47 | themes, 48 | applyTheme 49 | } 50 | } 51 | 52 | // LAZY LOADING 53 | // main.js 54 | // const getCat = () => import('./cat.js') 55 | // later in the code as a response to some user interaction like click or route change 56 | // getCat() 57 | // .then({ meow } => meow()) -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple algorithm to position node on graph 3 | * Starts looking for position in front of the parent 4 | * While collisions are detected find empty position perpendicular to collision 5 | */ 6 | function findPosition (node, parent, align="right", nodes, sep={ x: 40, y: 40 }, invertOffset=false) { 7 | const sepX = sep.x || sep 8 | const sepY = sep.y || sep 9 | const startX = !parent ? 0 10 | : (align === 'down' || align === 'up') 11 | ? parent.x 12 | : align === 'right' 13 | ? parent.x + parent.width + sepX 14 | : align === 'left' 15 | ? parent.x - node.width - sepX 16 | : -1 17 | 18 | const startY = !parent ? 0 19 | : (align === 'right' || align === 'left') 20 | ? parent.y 21 | : align === 'down' 22 | ? parent.y + parent.height + sepY 23 | : align === 'up' 24 | ? parent.y - node.height - sepY 25 | : -1 26 | 27 | const alignV = align === 'down' || align === 'up' 28 | const alignH = align === 'right' || align === 'left' 29 | const offsetX = alignV ? sepX * (invertOffset ? -1 : 1) : 0 30 | const offsetY = alignH ? sepY * (invertOffset ? -1 : 1) : 0 31 | 32 | const boxes = (nodes).filter(n => n.id !== node.id) 33 | const box = { x: startX, y: startY, width: node.width, height: node.height } 34 | 35 | let cols = boxBoxes(box, boxes) 36 | while (cols.length) { 37 | const col = cols[0] 38 | if (offsetX) { 39 | box.x = col.x + offsetX + (offsetX > 0 ? col.width : -node.width) 40 | } else { 41 | box.y = col.y + offsetY + (offsetY > 0 ? col.height : -node.height) 42 | } 43 | cols = boxBoxes(box, boxes) 44 | } 45 | 46 | return { x: box.x, y: box.y } 47 | } 48 | 49 | function boxBox(a, b) { 50 | return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y 51 | } 52 | 53 | function boxBoxes(box, boxes) { 54 | return boxes.filter(b => boxBox(b, box)) 55 | } 56 | 57 | function createDAG (nodes, edges) { 58 | const _nodes = nodes.map(node => ({ 59 | id: node.id, 60 | x: node.x, 61 | y: node.y, 62 | width: node.width, 63 | height: node.height, 64 | parentIds: [], 65 | children: [] 66 | })) 67 | 68 | const visited = {} 69 | edges.forEach(c => { 70 | if (visited[c.from+c.to]) return 71 | visited[c.from+c.to] = true 72 | 73 | const from = _nodes.find(node => node.id === c.from) 74 | const to = _nodes.find(node => node.id === c.to) 75 | if (from && to) { 76 | from.children.push(to) 77 | to.parentIds.push(from.id) 78 | } 79 | }) 80 | 81 | removeCycles(_nodes) 82 | return _nodes 83 | } 84 | 85 | /** 86 | * Detect and remove cycles from graph 87 | */ 88 | const removeCycles = (nodes) => { 89 | const cycles = [] 90 | const unvisited = node => node.state === 0 91 | const visiting = node => node.state === 1 92 | const visited = node => node.state === 2 93 | nodes.forEach(node => { node.state = 0 }) 94 | 95 | const visit = (node) => { 96 | node.state = 1 // visiting 97 | node.children.forEach(child => { 98 | if (visited(child)) return 99 | if (visiting(child)) { 100 | // FOUND CYCLE 101 | node.children.splice(node.children.indexOf(child), 1) 102 | child.parentIds.splice(child.parentIds.indexOf(node.id), 1) 103 | cycles.push(`${child.id}:${node.id}`) 104 | } else { 105 | visit(child) 106 | } 107 | }) 108 | node.state = 2 // visited 109 | } 110 | 111 | nodes.forEach(node => { 112 | if (unvisited(node)) { 113 | visit(node) 114 | } 115 | }) 116 | 117 | return cycles 118 | } 119 | 120 | // convert dag into structure for flextree layout 121 | const dagToFlextree = (node, graph, flipXY=false, spacing=40) => { 122 | const entry = { 123 | id: node.id, 124 | size: [ 125 | (flipXY ? node.height : node.width) + spacing, 126 | (flipXY ? node.width: node.height) + spacing 127 | ], 128 | children: [] 129 | } 130 | graph.push(entry) 131 | node.children.forEach(child => { 132 | dagToFlextree(child, entry.children, flipXY, spacing) 133 | }) 134 | } 135 | 136 | function lineRect(x1, x2, y1, y2, rect) { 137 | const box = [ rect.x, rect.y, rect.x + rect.width, rect.y + rect.height] 138 | const intersections = [ 139 | lineLine(x1, y1, x2, y2, box[0], box[1], box[0], box[3]), // left 140 | lineLine(x1, y1, x2, y2, box[0], box[1], box[2], box[1]), // top 141 | lineLine(x1, y1, x2, y2, box[2], box[1], box[2], box[3]), // right 142 | lineLine(x1, y1, x2, y2, box[0], box[3], box[2], box[3]) // bottom 143 | ].filter(i => i) 144 | 145 | return intersections 146 | .map(i => Object.assign(i, { distance: Math.sqrt((x1 - i.x) ** 2 + (y1 - i.y) ** 2) })) 147 | .sort((a, b) => a.distance < b.distance ? 1 : -1) // order intersections by distance 148 | .pop() 149 | } 150 | 151 | const eps = 0.0000001; 152 | function between(a, b, c) { 153 | return a - eps <= b && b <= c + eps; 154 | } 155 | function lineLine(x1, y1, x2, y2, x3, y3, x4, y4) { 156 | var x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / 157 | ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)); 158 | var y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / 159 | ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)); 160 | if (isNaN(x) || isNaN(y)) { 161 | return false; 162 | } else { 163 | if (x1 >= x2) { 164 | if (!between(x2, x, x1)) { 165 | return false; 166 | } 167 | } else { 168 | if (!between(x1, x, x2)) { 169 | return false; 170 | } 171 | } 172 | if (y1 >= y2) { 173 | if (!between(y2, y, y1)) { 174 | return false; 175 | } 176 | } else { 177 | if (!between(y1, y, y2)) { 178 | return false; 179 | } 180 | } 181 | if (x3 >= x4) { 182 | if (!between(x4, x, x3)) { 183 | return false; 184 | } 185 | } else { 186 | if (!between(x3, x, x4)) { 187 | return false; 188 | } 189 | } 190 | if (y3 >= y4) { 191 | if (!between(y4, y, y3)) { 192 | return false; 193 | } 194 | } else { 195 | if (!between(y3, y, y4)) { 196 | return false; 197 | } 198 | } 199 | } 200 | return { x, y } 201 | } 202 | function isSafari() { 203 | return window 204 | ? /constructor/i.test(window.HTMLElement) || 205 | (function (p) { 206 | return p?.toString() === "[object SafariRemoteNotification]"; 207 | })( 208 | !window["safari"] || 209 | (typeof safari !== "undefined" && window["safari"].pushNotification), 210 | ) || /iPad|iPhone|iPod/.test(navigator.userAgent) && !window["MSStream"] 211 | 212 | : false; 213 | } 214 | 215 | export default { 216 | findPosition, 217 | createDAG, 218 | dagToFlextree, 219 | boxBox, 220 | boxBoxes, 221 | lineLine, 222 | lineRect, 223 | isSafari 224 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | base: './', 7 | plugins: [vue()], 8 | }) -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: process.env.NODE_ENV === 'production' 3 | ? '/vnodes/' 4 | : '/', 5 | // devServer: { 6 | // host: '192.168.1.64', 7 | // } 8 | } 9 | --------------------------------------------------------------------------------