@shawcx/avispa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/css/Avispa.css +57 -0
- package/css/Avispa.css.map +1 -0
- package/js/Avispa.js +230 -0
- package/js/BaseObject.js +92 -0
- package/js/Context.js +7 -0
- package/js/Group.js +97 -0
- package/js/Link.js +154 -0
- package/js/Node.js +90 -0
- package/js/Types.js +9 -0
- package/js/Utils.js +16 -0
- package/package.json +29 -0
- package/scss/Avispa.scss +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014-2023 Matthew Shaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Avispa
|
|
2
|
+
======
|
|
3
|
+
|
|
4
|
+
Javascript Graph Layout and Interaction
|
|
5
|
+
|
|
6
|
+
Avispa (spanish for wasp) is a visualization framework. It is intended to be a
|
|
7
|
+
general purpose Javascript library for rendering graphs.
|
|
8
|
+
|
|
9
|
+
Depends on jQuery, Underscore.js, and Backbone.js
|
|
10
|
+
|
|
11
|
+
Demo at [http://shawcx.github.io/avispa/](http://shawcx.github.io/avispa/)
|
package/css/Avispa.css
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
g.group rect {
|
|
2
|
+
cursor: pointer;
|
|
3
|
+
fill: #fff;
|
|
4
|
+
opacity: 0.75;
|
|
5
|
+
stroke: #999;
|
|
6
|
+
stroke-width: 1px;
|
|
7
|
+
}
|
|
8
|
+
g.group rect.hover {
|
|
9
|
+
stroke: #333;
|
|
10
|
+
stroke-width: 2px;
|
|
11
|
+
}
|
|
12
|
+
g.group text {
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
font: bold 8pt "Andale Mono", AndaleMono, monospace;
|
|
15
|
+
fill: #000;
|
|
16
|
+
text-anchor: left;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
g.node circle {
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
fill: #fff;
|
|
22
|
+
stroke: #999;
|
|
23
|
+
stroke-width: 1px;
|
|
24
|
+
}
|
|
25
|
+
g.node circle:hover {
|
|
26
|
+
stroke: #333;
|
|
27
|
+
stroke-width: 2px;
|
|
28
|
+
}
|
|
29
|
+
g.node text {
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
font: bold 8pt "Andale Mono", AndaleMono, monospace;
|
|
32
|
+
fill: #000;
|
|
33
|
+
text-anchor: middle;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
g.link line {
|
|
37
|
+
fill: none;
|
|
38
|
+
stroke: #666;
|
|
39
|
+
stroke-width: 3px;
|
|
40
|
+
stroke-linecap: round;
|
|
41
|
+
}
|
|
42
|
+
g.link path {
|
|
43
|
+
fill: none;
|
|
44
|
+
stroke: #999;
|
|
45
|
+
stroke-width: 3px;
|
|
46
|
+
stroke-linecap: round;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
stroke-dasharray: 8, 4;
|
|
49
|
+
}
|
|
50
|
+
g.link text {
|
|
51
|
+
font: bold 6pt "Andale Mono", AndaleMono, monospace;
|
|
52
|
+
fill: #666;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
text-anchor: middle;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*# sourceMappingURL=Avispa.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sourceRoot":"","sources":["../scss/Avispa.scss"],"names":[],"mappings":"AAMI;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EAEA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA","file":"Avispa.css"}
|
package/js/Avispa.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const _ = require('underscore')
|
|
4
|
+
const Backbone = require('backbone')
|
|
5
|
+
|
|
6
|
+
const { Position } = require('./Types.js')
|
|
7
|
+
const { cancelEvent, $SVG } = require('./Utils.js')
|
|
8
|
+
|
|
9
|
+
const context = require('./Context.js')
|
|
10
|
+
const Group = require('./Group.js')
|
|
11
|
+
const Node = require('./Node.js')
|
|
12
|
+
const Link = require('./Link.js')
|
|
13
|
+
|
|
14
|
+
let normalizeWheel = function(event) {
|
|
15
|
+
if (event.wheelDelta) {
|
|
16
|
+
return event.wheelDelta / 120
|
|
17
|
+
}
|
|
18
|
+
if (event.detail) {
|
|
19
|
+
return event.detail / -3
|
|
20
|
+
}
|
|
21
|
+
return 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const $SVG = function(name) {
|
|
26
|
+
return $(document.createElementNS('http://www.w3.org/2000/svg', name))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Avispa extends Backbone.View {
|
|
31
|
+
events() {
|
|
32
|
+
return {
|
|
33
|
+
'mousedown.avispa' : 'OnMouseDown',
|
|
34
|
+
'mousemove.avispa' : 'OnMouseMove',
|
|
35
|
+
'mouseup.avispa' : 'OnMouseUp',
|
|
36
|
+
'mousewheel.avispa' : 'OnMouseWheel',
|
|
37
|
+
'DOMMouseScroll.avispa' : 'OnMouseWheel',
|
|
38
|
+
'contextmenu.avispa' : 'OnContextMenu',
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
initialize(options) {
|
|
43
|
+
context.a = this
|
|
44
|
+
|
|
45
|
+
_.bindAll(this, 'render',
|
|
46
|
+
'OnMouseDown', 'OnMouseMove', 'OnMouseUp', 'OnMouseWheel', 'OnContextMenu')
|
|
47
|
+
|
|
48
|
+
this.scale = 1.0
|
|
49
|
+
this.links = {}
|
|
50
|
+
this.offset = null
|
|
51
|
+
this.dragItem = null
|
|
52
|
+
this.arrow = null
|
|
53
|
+
|
|
54
|
+
this.position = {
|
|
55
|
+
x: 0,
|
|
56
|
+
y: 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.zoom = {
|
|
60
|
+
step : 0.125,
|
|
61
|
+
min : 0.125,
|
|
62
|
+
max : 2.5,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.$parent = this.$el.parent()
|
|
66
|
+
|
|
67
|
+
this.$pan = this.$el.find('g.pan')
|
|
68
|
+
this.$zoom = this.$el.find('g.zoom')
|
|
69
|
+
|
|
70
|
+
this.$groups = this.$el.find('g.groups')
|
|
71
|
+
this.$links = this.$el.find('g.links')
|
|
72
|
+
this.$objects = this.$el.find('g.objects')
|
|
73
|
+
this.$labels = this.$el.find('g.labels')
|
|
74
|
+
|
|
75
|
+
this.$pan.x = parseInt(window.innerWidth / 2)
|
|
76
|
+
this.$pan.y = parseInt(window.innerHeight / 2)
|
|
77
|
+
|
|
78
|
+
this.Pan(0,0)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Pan(dx, dy) {
|
|
82
|
+
this.$pan.x += dx
|
|
83
|
+
this.$pan.y += dy
|
|
84
|
+
|
|
85
|
+
this.$pan.attr('transform', `translate(${this.$pan.x}, ${this.$pan.y})`)
|
|
86
|
+
this.$parent.css('background-position', `${this.$pan.x}px ${this.$pan.y}px`)
|
|
87
|
+
|
|
88
|
+
return this
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Scale(scale) {
|
|
92
|
+
this.scale = scale
|
|
93
|
+
this.$zoom.attr('transform', `scale(${scale})`)
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Zoom(delta) {
|
|
98
|
+
let scale = (delta == 0) ? 1.0 : this.scale + delta * this.zoom.step
|
|
99
|
+
if (scale >= this.zoom.min || scale <= this.zoom.max) {
|
|
100
|
+
this.Scale(scale)
|
|
101
|
+
}
|
|
102
|
+
return this
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Point(event) {
|
|
106
|
+
// translates the client x,y into the surface x,y
|
|
107
|
+
let point = this.el.createSVGPoint()
|
|
108
|
+
point.x = event.clientX
|
|
109
|
+
point.y = event.clientY
|
|
110
|
+
point = point.matrixTransform(this.el.getScreenCTM().inverse())
|
|
111
|
+
|
|
112
|
+
// account for the current pan and scale
|
|
113
|
+
point.x = parseInt((point.x - this.$pan.x) / this.scale)
|
|
114
|
+
point.y = parseInt((point.y - this.$pan.y) / this.scale)
|
|
115
|
+
|
|
116
|
+
return [point.x, point.y]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
OnMouseDown(event) {
|
|
120
|
+
if (this.arrow) {
|
|
121
|
+
this.arrow.Remove()
|
|
122
|
+
this.arrow = null
|
|
123
|
+
return cancelEvent(event)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
switch (event.which) {
|
|
127
|
+
case 1:
|
|
128
|
+
this.LeftDown(event)
|
|
129
|
+
break
|
|
130
|
+
case 2:
|
|
131
|
+
this.MiddleDown(event)
|
|
132
|
+
break
|
|
133
|
+
case 3:
|
|
134
|
+
if (this.RightDown) {
|
|
135
|
+
this.RightDown(event)
|
|
136
|
+
}
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return cancelEvent(event)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
LeftDown(event) {
|
|
144
|
+
//if event.shiftKey
|
|
145
|
+
this.offset = [event.clientX, event.clientY]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
MiddleDown(event) {
|
|
149
|
+
this.Pan(-this.$pan.x + window.innerWidth / 2, -this.$pan.y + window.innerHeight / 2)
|
|
150
|
+
this.Zoom(0)
|
|
151
|
+
//this.$('#zoomslider').slider('option', 'value', 1)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
OnMouseMove(event) {
|
|
155
|
+
// drag the entire scene around
|
|
156
|
+
if (this.offset) {
|
|
157
|
+
this.Pan(event.clientX - this.offset[0], event.clientY - this.offset[1])
|
|
158
|
+
this.offset = [event.clientX, event.clientY]
|
|
159
|
+
}
|
|
160
|
+
else if (this.arrow) {
|
|
161
|
+
this.arrow.Drag(event)
|
|
162
|
+
}
|
|
163
|
+
else if (this.dragItem) {
|
|
164
|
+
this.dragItem.jitter++
|
|
165
|
+
if (this.dragItem.Drag) {
|
|
166
|
+
this.dragItem.Drag(event)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return cancelEvent(event)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
OnMouseUp(event) {
|
|
174
|
+
this.offset = null
|
|
175
|
+
|
|
176
|
+
if (this.dragItem) {
|
|
177
|
+
if (this.dragItem.jitter < 3) {
|
|
178
|
+
switch (event.which) {
|
|
179
|
+
case 1:
|
|
180
|
+
this.dragItem.LeftClick(event)
|
|
181
|
+
break
|
|
182
|
+
case 2:
|
|
183
|
+
this.dragItem.MiddleClick(event)
|
|
184
|
+
break
|
|
185
|
+
case 3:
|
|
186
|
+
this.dragItem.RightClick(event)
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if(this.dragItem.MouseUp) {
|
|
191
|
+
this.dragItem.MouseUp(event)
|
|
192
|
+
}
|
|
193
|
+
this.dragItem = null
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
switch (event.which) {
|
|
197
|
+
case 1:
|
|
198
|
+
this.LeftClick(event)
|
|
199
|
+
break
|
|
200
|
+
case 2:
|
|
201
|
+
this.MiddleClick(event)
|
|
202
|
+
break
|
|
203
|
+
case 3:
|
|
204
|
+
this.RightClick(event)
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return cancelEvent(event)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
OnMouseWheel(event) {
|
|
213
|
+
this.Zoom(normalizeWheel(event))
|
|
214
|
+
//this.$('#zoomslider').slider('option', 'value', this.scale)
|
|
215
|
+
return cancelEvent(event)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
LeftClick(event) {}
|
|
219
|
+
MiddleClick(event) {}
|
|
220
|
+
RightClick(event) {}
|
|
221
|
+
OnContextMenu(event) {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
exports.Avispa = Avispa
|
|
225
|
+
exports.Position = Position
|
|
226
|
+
exports.Group = Group
|
|
227
|
+
exports.Node = Node
|
|
228
|
+
exports.Link = Link
|
|
229
|
+
exports.$SVG = $SVG
|
|
230
|
+
exports.cancelEvent = cancelEvent
|
package/js/BaseObject.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const _ = require('underscore')
|
|
4
|
+
const Backbone = require('backbone')
|
|
5
|
+
|
|
6
|
+
const { cancelEvent } = require('./Utils.js')
|
|
7
|
+
const context = require('./Context.js')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseObject extends Backbone.View {
|
|
11
|
+
events() {
|
|
12
|
+
return {
|
|
13
|
+
'mousedown' : 'OnMouseDown',
|
|
14
|
+
'mouseenter' : 'OnMouseEnter',
|
|
15
|
+
'mouseleave' : 'OnMouseLeave',
|
|
16
|
+
'mouseup' : 'OnMouseUp',
|
|
17
|
+
'contextmenu' : 'OnContextMenu',
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
delegateEvents(events) {
|
|
22
|
+
super.delegateEvents(events)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
initialize(options) {
|
|
26
|
+
_.bindAll(this, 'OnMouseDown', 'OnMouseUp', 'OnContextMenu', 'render')
|
|
27
|
+
|
|
28
|
+
this.parent = options.parent
|
|
29
|
+
this.position = options.position
|
|
30
|
+
if (this.position) {
|
|
31
|
+
this.position.bind('change', this.render, this)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.parent) {
|
|
35
|
+
this.offset = {
|
|
36
|
+
x: this.position.get('x'),
|
|
37
|
+
y: this.position.get('y'),
|
|
38
|
+
}
|
|
39
|
+
this.ParentDrag(this.parent.position)
|
|
40
|
+
this.parent.position.bind('change', this.ParentDrag, this)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
size(w,h) {
|
|
45
|
+
this.position.set({w:w,h:h})
|
|
46
|
+
return this
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ParentDrag(parentPosition) {
|
|
50
|
+
this.position.set({
|
|
51
|
+
x: this.offset.x + parentPosition.get('x'),
|
|
52
|
+
y: this.offset.y + parentPosition.get('y'),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
OnMouseDown(event) {
|
|
57
|
+
this.jitter = 0
|
|
58
|
+
|
|
59
|
+
this.x1 = (event.clientX / context.a.scale) - this.position.get('x')
|
|
60
|
+
this.y1 = (event.clientY / context.a.scale) - this.position.get('y')
|
|
61
|
+
|
|
62
|
+
if (this.parent) {
|
|
63
|
+
this.ox1 = this.offset.x - this.position.get('x')
|
|
64
|
+
this.oy1 = this.offset.y - this.position.get('y')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// TODO: calculate the bounds of the parent element
|
|
68
|
+
|
|
69
|
+
if (event.shiftKey) {
|
|
70
|
+
this.$el.parent().append(this.$el)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
context.a.dragItem = this
|
|
74
|
+
|
|
75
|
+
return cancelEvent(event)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Drag(event) {
|
|
79
|
+
let x = (event.clientX / context.a.scale) - this.x1
|
|
80
|
+
let y = (event.clientY / context.a.scale) - this.y1
|
|
81
|
+
this.position.set({x:x,y:y})
|
|
82
|
+
return cancelEvent(event)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
OnMouseUp(event) {}
|
|
86
|
+
OnContextMenu(event) {}
|
|
87
|
+
LeftClick(event) {}
|
|
88
|
+
MiddleClick(event) {}
|
|
89
|
+
RightClick(event) {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = BaseObject
|
package/js/Context.js
ADDED
package/js/Group.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const _ = require('underscore')
|
|
4
|
+
const Backbone = require('backbone')
|
|
5
|
+
|
|
6
|
+
const { cancelEvent, $SVG } = require('./Utils.js')
|
|
7
|
+
const context = require('./Context.js')
|
|
8
|
+
const BaseObject = require('./BaseObject.js')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Group extends BaseObject {
|
|
12
|
+
el() {
|
|
13
|
+
return $SVG('g').attr('class', 'group')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
initialize(options) {
|
|
17
|
+
this.$rect = $SVG('rect')
|
|
18
|
+
.attr('width', options.position.get('w'))
|
|
19
|
+
.attr('height', options.position.get('h'))
|
|
20
|
+
.css('fill', options.position.get('fill'))
|
|
21
|
+
.appendTo(this.$el)
|
|
22
|
+
|
|
23
|
+
super.initialize(options)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render() {
|
|
27
|
+
this.$rect
|
|
28
|
+
.attr('width', this.position.get('w'))
|
|
29
|
+
.attr('height', this.position.get('h'))
|
|
30
|
+
.attr('x', this.position.get('x'))
|
|
31
|
+
.attr('y', this.position.get('y'))
|
|
32
|
+
return this
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
OnMouseEnter(event) {
|
|
36
|
+
if (!context.a.dragItem) {
|
|
37
|
+
this.$rect.attr('class', 'hover')
|
|
38
|
+
}
|
|
39
|
+
return cancelEvent(event)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
OnMouseLeave(event) {
|
|
43
|
+
if (!context.a.dragItem) {
|
|
44
|
+
this.$rect.removeAttr('class')
|
|
45
|
+
}
|
|
46
|
+
return cancelEvent(event)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Drag(event) {
|
|
50
|
+
let x = (event.clientX / context.a.scale) - this.x1
|
|
51
|
+
let y = (event.clientY / context.a.scale) - this.y1
|
|
52
|
+
|
|
53
|
+
let newparentw = 0
|
|
54
|
+
let newparenth = 0
|
|
55
|
+
|
|
56
|
+
if (this.offset) {
|
|
57
|
+
this.offset.x = this.ox1 + x
|
|
58
|
+
this.offset.y = this.oy1 + y
|
|
59
|
+
|
|
60
|
+
let boundsx = this.parent.position.get('w') - this.position.get('w') - 10
|
|
61
|
+
let boundsy = this.parent.position.get('h') - this.position.get('h') - 10
|
|
62
|
+
|
|
63
|
+
if (this.offset.x < 10) {
|
|
64
|
+
this.offset.x = 10
|
|
65
|
+
x = this.parent.position.get('x') + 10
|
|
66
|
+
}
|
|
67
|
+
else if (this.offset.x > boundsx) {
|
|
68
|
+
//this.offset.x = boundsx
|
|
69
|
+
//x = this.parent.position.get('x') + boundsx
|
|
70
|
+
newparentw = this.offset.x - boundsx
|
|
71
|
+
x = this.offset.x //this.parent.position.get('x') + boundsx
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.offset.y < 30) {
|
|
75
|
+
this.offset.y = 30
|
|
76
|
+
y = this.parent.position.get('y') + 30
|
|
77
|
+
}
|
|
78
|
+
else if (this.offset.y > boundsy) {
|
|
79
|
+
//this.offset.y = boundsy
|
|
80
|
+
//y = this.parent.position.get('y') + boundsy
|
|
81
|
+
newparenth = this.offset.y - boundsy
|
|
82
|
+
y = this.offset.y //this.parent.position.get('x') + boundsx
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.position.set({x:x,y:y})
|
|
87
|
+
if (newparentw || newparenth) {
|
|
88
|
+
newparentw = this.parent.position.get('w') + newparentw
|
|
89
|
+
newparenth = this.parent.position.get('h') + newparenth
|
|
90
|
+
this.parent.Size(newparentw, newparenth)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return cancelEvent(event)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = Group
|
package/js/Link.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const _ = require('underscore')
|
|
4
|
+
const Backbone = require('backbone')
|
|
5
|
+
|
|
6
|
+
const { cancelEvent, $SVG } = require('./Utils.js')
|
|
7
|
+
const context = require('./Context.js')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Link extends Backbone.View {
|
|
11
|
+
static RAD = 180.0 / Math.PI
|
|
12
|
+
|
|
13
|
+
el() {
|
|
14
|
+
return $SVG('g').attr('class', 'link')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
events() {
|
|
18
|
+
return {
|
|
19
|
+
'mousedown' : 'OnMouseDown',
|
|
20
|
+
'mouseenter' : 'OnMouseEnter',
|
|
21
|
+
'mouseleave' : 'OnMouseLeave',
|
|
22
|
+
'contextmenu' : 'OnContextMenu',
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
initialize(options) {
|
|
27
|
+
this.path = $SVG('path')
|
|
28
|
+
.css('marker-end', 'url(#Arrow)')
|
|
29
|
+
.css('opacity', '0.5')
|
|
30
|
+
.appendTo(this.$el)
|
|
31
|
+
|
|
32
|
+
_.bindAll(this, 'render', 'OnMouseDown', 'OnMouseEnter', 'OnMouseLeave', 'OnContextMenu')
|
|
33
|
+
|
|
34
|
+
this.arc = new Backbone.Model({arc:10})
|
|
35
|
+
this.arc.bind('change', this.render, this)
|
|
36
|
+
|
|
37
|
+
this.left = options.left
|
|
38
|
+
this.right = options.right
|
|
39
|
+
this.left.position.bind('change', this.render, this)
|
|
40
|
+
this.right.position.bind('change', this.render, this)
|
|
41
|
+
|
|
42
|
+
this.render()
|
|
43
|
+
|
|
44
|
+
if (options.parent) {
|
|
45
|
+
options.parent.$links.append(this.$el)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
update() {
|
|
50
|
+
//this.label.text(name)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render() {
|
|
55
|
+
if (!this.arc) { return this }
|
|
56
|
+
|
|
57
|
+
let arc = this.arc.get('arc')
|
|
58
|
+
let lx = this.left.position.get('x')
|
|
59
|
+
let ly = this.left.position.get('y')
|
|
60
|
+
let rx = this.right.position.get('x')
|
|
61
|
+
let ry = this.right.position.get('y')
|
|
62
|
+
|
|
63
|
+
// calculate the angle between 2 nodes
|
|
64
|
+
let ang = Math.atan2(rx - lx, ry - ly)
|
|
65
|
+
|
|
66
|
+
// bound the offset to about half the circle
|
|
67
|
+
let offset = Math.max(-1.5, Math.min(1.5, arc / 100))
|
|
68
|
+
|
|
69
|
+
// draw to the edge of the node
|
|
70
|
+
lx += 30 * Math.sin(ang + offset)
|
|
71
|
+
ly += 30 * Math.cos(ang + offset)
|
|
72
|
+
rx += -33 * Math.sin(ang - offset)
|
|
73
|
+
ry += -33 * Math.cos(ang - offset)
|
|
74
|
+
|
|
75
|
+
// calculate the the position for the quadratic bezier curve
|
|
76
|
+
let xc = ((lx + rx) >> 1) + arc * Math.cos(ang)
|
|
77
|
+
let yc = ((ly + ry) >> 1) - arc * Math.sin(ang)
|
|
78
|
+
|
|
79
|
+
let mx = xc - (arc>>1) * Math.cos(ang)
|
|
80
|
+
let my = yc + (arc>>1) * Math.sin(ang)
|
|
81
|
+
|
|
82
|
+
let rot = -(this.RAD * ang)
|
|
83
|
+
rot += (rot > 0 && rot < 180) ? -90 : 90
|
|
84
|
+
|
|
85
|
+
this.path.attr('d', "M #{lx} #{ly} Q #{xc} #{yc} #{rx} #{ry}")
|
|
86
|
+
//this.label.attr('x', mx).attr('y', my).attr('transform', "rotate(#{rot}, #{mx} #{my})")
|
|
87
|
+
|
|
88
|
+
return this
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Events
|
|
92
|
+
|
|
93
|
+
Drag(event) {
|
|
94
|
+
let [x,y] = context.a.Point(event)
|
|
95
|
+
|
|
96
|
+
let from_x = this.left.position.get('x')
|
|
97
|
+
let from_y = this.left.position.get('y')
|
|
98
|
+
let to_x = this.right.position.get('x')
|
|
99
|
+
let to_y = this.right.position.get('y')
|
|
100
|
+
|
|
101
|
+
let d = (to_x - from_x) * (y - from_y) - (to_y - from_y) * (x - from_x)
|
|
102
|
+
|
|
103
|
+
if (d) {
|
|
104
|
+
d = Math.pow(Math.abs(d), 0.5) * (d > 0) ? -1 : 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!this.od && this.od != 0) {
|
|
108
|
+
this.od = d
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// will trigger a call to render
|
|
112
|
+
this.arc.set('arc', Math.max(0, this.oarc + d - this.od))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
OnMouseDown(event) {
|
|
116
|
+
this.jitter = 0
|
|
117
|
+
|
|
118
|
+
context.a.dragItem = this
|
|
119
|
+
this.oarc = this.arc.get('arc')
|
|
120
|
+
this.od = null
|
|
121
|
+
|
|
122
|
+
return cancelEvent(event)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
MouseUp(event) {
|
|
126
|
+
if (this.jitter > 3) {
|
|
127
|
+
this.path.css('stroke-width', '3px')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
OnMouseEnter() {
|
|
132
|
+
if (!context.a.dragItem) {
|
|
133
|
+
this.path.css('stroke-width', '6px')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
OnMouseLeave() {
|
|
138
|
+
if (!context.a.dragItem) {
|
|
139
|
+
this.path.css('stroke-width', '3px')
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
LeftClick(event) {
|
|
144
|
+
if (event.shiftKey) {
|
|
145
|
+
this.arc.set('arc', 0)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
OnContextMenu(event) {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = Link
|
|
154
|
+
|
package/js/Node.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const _ = require('underscore')
|
|
4
|
+
const Backbone = require('backbone')
|
|
5
|
+
|
|
6
|
+
const { cancelEvent, $SVG } = require('./Utils.js')
|
|
7
|
+
const context = require('./Context.js')
|
|
8
|
+
const BaseObject = require('./BaseObject.js')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Node extends BaseObject {
|
|
12
|
+
el() {
|
|
13
|
+
return $SVG('g').attr('class', 'node')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
initialize(options) {
|
|
17
|
+
_.bindAll(this, 'OnMouseDown', 'OnMouseUp', 'OnContextMenu', 'render')
|
|
18
|
+
|
|
19
|
+
this.$circle = $SVG('circle')
|
|
20
|
+
.attr('r', options.position.get('radius'))
|
|
21
|
+
.css('fill', options.position.get('fill'))
|
|
22
|
+
.appendTo(this.$el)
|
|
23
|
+
|
|
24
|
+
this.$label = $SVG('text')
|
|
25
|
+
.attr('dy', '0.5em')
|
|
26
|
+
.text(options.label)
|
|
27
|
+
.appendTo(this.$el)
|
|
28
|
+
|
|
29
|
+
super.initialize(options)
|
|
30
|
+
|
|
31
|
+
this.render()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
this.$circle
|
|
36
|
+
.attr('cx', this.position.get('x'))
|
|
37
|
+
.attr('cy', this.position.get('y'))
|
|
38
|
+
this.$label
|
|
39
|
+
.attr('x', this.position.get('x'))
|
|
40
|
+
.attr('y', this.position.get('y'))
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
OnMouseEnter(event) {
|
|
45
|
+
if (!context.a.dragItem) {
|
|
46
|
+
this.$circle.attr('class', 'hover')
|
|
47
|
+
}
|
|
48
|
+
return cancelEvent(event)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
OnMouseLeave(event) {
|
|
52
|
+
if (!context.a.dragItem) {
|
|
53
|
+
this.$circle.removeAttr('class')
|
|
54
|
+
}
|
|
55
|
+
return cancelEvent(event)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Drag(event) {
|
|
59
|
+
let x = (event.clientX / context.a.scale) - this.x1
|
|
60
|
+
let y = (event.clientY / context.a.scale) - this.y1
|
|
61
|
+
|
|
62
|
+
if (this.offset) {
|
|
63
|
+
this.offset.x = this.ox1 + x
|
|
64
|
+
this.offset.y = this.oy1 + y
|
|
65
|
+
|
|
66
|
+
if (this.offset.x < 0) {
|
|
67
|
+
this.offset.x = 0
|
|
68
|
+
x = this.parent.position.get('x')
|
|
69
|
+
}
|
|
70
|
+
else if (this.offset.x > this.parent.position.get('w')) {
|
|
71
|
+
this.offset.x = this.parent.position.get('w')
|
|
72
|
+
x = this.parent.position.get('x') + this.parent.position.get('w')
|
|
73
|
+
}
|
|
74
|
+
if (this.offset.y < 0) {
|
|
75
|
+
this.offset.y = 0
|
|
76
|
+
y = this.parent.position.get('y')
|
|
77
|
+
}
|
|
78
|
+
else if (this.offset.y > this.parent.position.get('h')) {
|
|
79
|
+
this.offset.y = this.parent.position.get('h')
|
|
80
|
+
y = this.parent.position.get('y') + this.parent.position.get('h')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.position.set({x:x,y:y})
|
|
85
|
+
|
|
86
|
+
return cancelEvent(event)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = Node
|
package/js/Types.js
ADDED
package/js/Utils.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const $ = require('jquery')
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
exports.cancelEvent = function(event) {
|
|
7
|
+
event.preventDefault()
|
|
8
|
+
event.stopPropagation()
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
exports.normalizeWheel = function(event) {
|
|
13
|
+
if (event.wheelDelta) return event.wheelDelta / 120
|
|
14
|
+
if (event.detail) return event.detail / -3
|
|
15
|
+
return 0
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shawcx/avispa",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SVG + Websockets",
|
|
5
|
+
"main": "js/Avispa.js",
|
|
6
|
+
"sass": "scss/Avispa.scss",
|
|
7
|
+
"style": "css/Avispa.css",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "npx sass scss/Avispa.scss css/Avispa.css"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/shawcx/avispa.git"
|
|
14
|
+
},
|
|
15
|
+
"author": "Matthew Shaw",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/shawcx/avispa/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/shawcx/avispa#readme",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"backbone": "^1.4.1",
|
|
23
|
+
"jquery": "^3.6.3",
|
|
24
|
+
"underscore": "^1.13.6"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"sass": "^1.58.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/scss/Avispa.scss
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
|
|
2
|
+
$mono: "Andale Mono", AndaleMono, monospace;
|
|
3
|
+
|
|
4
|
+
g.group {
|
|
5
|
+
//TODO: elements .user-select();
|
|
6
|
+
|
|
7
|
+
rect {
|
|
8
|
+
cursor: pointer;
|
|
9
|
+
fill: #fff;
|
|
10
|
+
opacity: 0.75;
|
|
11
|
+
stroke: #999;
|
|
12
|
+
stroke-width: 1px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
rect.hover {
|
|
16
|
+
stroke: #333;
|
|
17
|
+
stroke-width: 2px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
text {
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
font: bold 8pt $mono;
|
|
23
|
+
fill: #000;
|
|
24
|
+
text-anchor: left;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
g.node {
|
|
29
|
+
circle {
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
fill: #fff;
|
|
32
|
+
stroke: #999;
|
|
33
|
+
stroke-width: 1px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
circle:hover {
|
|
37
|
+
stroke: #333;
|
|
38
|
+
stroke-width: 2px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
text {
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
font: bold 8pt $mono;
|
|
44
|
+
fill: #000;
|
|
45
|
+
text-anchor: middle;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
g.link {
|
|
50
|
+
line {
|
|
51
|
+
fill: none;
|
|
52
|
+
stroke: #666;
|
|
53
|
+
stroke-width: 3px;
|
|
54
|
+
stroke-linecap: round;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
path {
|
|
58
|
+
fill: none;
|
|
59
|
+
//opacity: 0.5;
|
|
60
|
+
stroke: #999;
|
|
61
|
+
stroke-width: 3px;
|
|
62
|
+
stroke-linecap: round;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
stroke-dasharray: 8,4;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
text {
|
|
68
|
+
font: bold 6pt $mono;
|
|
69
|
+
fill: #666;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
text-anchor: middle;
|
|
72
|
+
}
|
|
73
|
+
}
|