@shawcx/avispa 2.0.0 → 2.1.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/README.md +2 -1
- package/js/Avispa.js +127 -18
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -83,7 +83,8 @@ $(() => {
|
|
|
83
83
|
|
|
84
84
|
On screen:
|
|
85
85
|
|
|
86
|
-
- Drag the background to pan, scroll to zoom
|
|
86
|
+
- Drag the background to pan, scroll or pinch to zoom in on the spot under the
|
|
87
|
+
pointer or between your fingers, and middle-click to recenter.
|
|
87
88
|
- Drag a node or group to move it; a group's children come along. A node stays
|
|
88
89
|
inside its group, and a nested group dragged past its parent's edge grows it.
|
|
89
90
|
- Drag a group's bottom-right corner to resize it, down to what its contents need.
|
package/js/Avispa.js
CHANGED
|
@@ -13,6 +13,8 @@ const Link = require('./Link.js')
|
|
|
13
13
|
const Socket = require('./Socket.js')
|
|
14
14
|
|
|
15
15
|
class Avispa extends Backbone.View {
|
|
16
|
+
static TOUCH_EVENTS = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel']
|
|
17
|
+
|
|
16
18
|
events() {
|
|
17
19
|
return {
|
|
18
20
|
'pointerdown.avispa' : 'OnMouseDown',
|
|
@@ -32,7 +34,7 @@ class Avispa extends Backbone.View {
|
|
|
32
34
|
this.$el.css({'touch-action': 'none', 'user-select': 'none'})
|
|
33
35
|
|
|
34
36
|
_.bindAll(this, 'render',
|
|
35
|
-
'OnMouseDown', 'OnMouseMove', 'OnMouseUp', 'OnMouseWheel', 'OnContextMenu')
|
|
37
|
+
'OnMouseDown', 'OnMouseMove', 'OnMouseUp', 'OnMouseWheel', 'OnContextMenu', 'OnTouch')
|
|
36
38
|
|
|
37
39
|
this.scale = 1.0
|
|
38
40
|
this.links = {}
|
|
@@ -40,6 +42,15 @@ class Avispa extends Backbone.View {
|
|
|
40
42
|
this.dragItem = null
|
|
41
43
|
this.arrow = null
|
|
42
44
|
|
|
45
|
+
// touch points by pointerId, as client [x, y], and the pinch two of
|
|
46
|
+
// them make. Watched in the capture phase, since items stop pointerdown
|
|
47
|
+
// from bubbling up to the surface
|
|
48
|
+
this.touches = new Map()
|
|
49
|
+
this.pinch = null
|
|
50
|
+
for (const type of Avispa.TOUCH_EVENTS) {
|
|
51
|
+
this.el.addEventListener(type, this.OnTouch, true)
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
this.position = {
|
|
44
55
|
x: 0,
|
|
45
56
|
y: 0
|
|
@@ -61,10 +72,10 @@ class Avispa extends Backbone.View {
|
|
|
61
72
|
this.$objects = this.$el.find('g.objects')
|
|
62
73
|
this.$labels = this.$el.find('g.labels')
|
|
63
74
|
|
|
64
|
-
this.$pan.x =
|
|
65
|
-
this.$pan.y =
|
|
75
|
+
this.$pan.x = 0
|
|
76
|
+
this.$pan.y = 0
|
|
66
77
|
|
|
67
|
-
this.
|
|
78
|
+
this.Center()
|
|
68
79
|
|
|
69
80
|
// opt-in WebSocket transport: `socket` may be a path string or a
|
|
70
81
|
// Socket options object. Incoming messages are routed to OnMessage
|
|
@@ -86,6 +97,9 @@ class Avispa extends Backbone.View {
|
|
|
86
97
|
if (this.socket) {
|
|
87
98
|
this.socket.close()
|
|
88
99
|
}
|
|
100
|
+
for (const type of Avispa.TOUCH_EVENTS) {
|
|
101
|
+
this.el.removeEventListener(type, this.OnTouch, true)
|
|
102
|
+
}
|
|
89
103
|
return super.remove()
|
|
90
104
|
}
|
|
91
105
|
|
|
@@ -105,25 +119,66 @@ class Avispa extends Backbone.View {
|
|
|
105
119
|
return this
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
// put the surface's origin in the middle of the SVG
|
|
123
|
+
Center() {
|
|
124
|
+
let [x, y] = this.Middle()
|
|
125
|
+
return this.Pan(parseInt(x) - this.$pan.x, parseInt(y) - this.$pan.y)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// zoom by `delta` steps (0 resets to 1), keeping what's drawn at `at`
|
|
129
|
+
// where it is (see ZoomTo)
|
|
130
|
+
Zoom(delta, at) {
|
|
131
|
+
return this.ZoomTo((delta == 0) ? 1.0 : this.scale + delta * this.zoom.step, at)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// zoom to `scale` within the limits, keeping what's drawn at `at` (an
|
|
135
|
+
// [x, y] from Local) where it is; without `at`, the origin stays put
|
|
136
|
+
ZoomTo(scale, at) {
|
|
110
137
|
scale = Math.min(this.zoom.max, Math.max(this.zoom.min, scale))
|
|
138
|
+
if (!at) {
|
|
139
|
+
return this.Scale(scale)
|
|
140
|
+
}
|
|
141
|
+
let point = this.Surface(at)
|
|
111
142
|
this.Scale(scale)
|
|
112
|
-
return this
|
|
143
|
+
return this.PanTo(point, at)
|
|
113
144
|
}
|
|
114
145
|
|
|
115
|
-
|
|
116
|
-
|
|
146
|
+
// pan so the surface point `point` is drawn at `at` (an [x, y] from Local)
|
|
147
|
+
PanTo(point, at) {
|
|
148
|
+
return this.Pan(
|
|
149
|
+
at[0] - point[0] * this.scale - this.$pan.x,
|
|
150
|
+
at[1] - point[1] * this.scale - this.$pan.y)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// translates a client x,y into the SVG's own coordinates, before the pan
|
|
154
|
+
// and zoom; an SVG that isn't laid out (display: none) has no transform
|
|
155
|
+
Local(x, y) {
|
|
156
|
+
let ctm = this.el.getScreenCTM()
|
|
157
|
+
if (!ctm) {
|
|
158
|
+
return [x, y]
|
|
159
|
+
}
|
|
117
160
|
let point = this.el.createSVGPoint()
|
|
118
|
-
point.x =
|
|
119
|
-
point.y =
|
|
120
|
-
point = point.matrixTransform(
|
|
161
|
+
point.x = x
|
|
162
|
+
point.y = y
|
|
163
|
+
point = point.matrixTransform(ctm.inverse())
|
|
164
|
+
return [point.x, point.y]
|
|
165
|
+
}
|
|
121
166
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
// the middle of the SVG, from Local
|
|
168
|
+
Middle() {
|
|
169
|
+
let box = this.el.getBoundingClientRect()
|
|
170
|
+
return this.Local(box.left + box.width / 2, box.top + box.height / 2)
|
|
171
|
+
}
|
|
125
172
|
|
|
126
|
-
|
|
173
|
+
// the surface point drawn at `at` (an [x, y] from Local)
|
|
174
|
+
Surface(at) {
|
|
175
|
+
return [(at[0] - this.$pan.x) / this.scale, (at[1] - this.$pan.y) / this.scale]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Point(event) {
|
|
179
|
+
// translates the client x,y into the surface x,y
|
|
180
|
+
let [x, y] = this.Surface(this.Local(event.clientX, event.clientY))
|
|
181
|
+
return [parseInt(x), parseInt(y)]
|
|
127
182
|
}
|
|
128
183
|
|
|
129
184
|
OnMouseDown(event) {
|
|
@@ -156,12 +211,19 @@ class Avispa extends Backbone.View {
|
|
|
156
211
|
}
|
|
157
212
|
|
|
158
213
|
MiddleDown(event) {
|
|
159
|
-
this.
|
|
214
|
+
this.Center()
|
|
160
215
|
this.Zoom(0)
|
|
161
216
|
//this.$('#zoomslider').slider('option', 'value', 1)
|
|
162
217
|
}
|
|
163
218
|
|
|
164
219
|
OnMouseMove(event) {
|
|
220
|
+
// once a drag is moving, capture its pointer so the drag carries on
|
|
221
|
+
// outside the SVG and its release comes back here. Not on pointerdown:
|
|
222
|
+
// a plain click or double-click would then go to the SVG, not the item
|
|
223
|
+
if ((this.offset || this.dragItem) && !this.el.hasPointerCapture(event.pointerId)) {
|
|
224
|
+
this.el.setPointerCapture(event.pointerId)
|
|
225
|
+
}
|
|
226
|
+
|
|
165
227
|
// drag the entire scene around
|
|
166
228
|
if (this.offset) {
|
|
167
229
|
this.Pan(event.clientX - this.offset[0], event.clientY - this.offset[1])
|
|
@@ -226,11 +288,58 @@ class Avispa extends Backbone.View {
|
|
|
226
288
|
this.dragItem = null
|
|
227
289
|
}
|
|
228
290
|
|
|
291
|
+
// A second finger turns whatever the first one started into a pinch,
|
|
292
|
+
// which zooms by how far apart the fingers move and pans with their
|
|
293
|
+
// midpoint. It lasts until every finger is up, and until then the touches
|
|
294
|
+
// go no further than here: lifting the last finger isn't a click.
|
|
295
|
+
OnTouch(event) {
|
|
296
|
+
// only touches that started here: one whose release never comes would
|
|
297
|
+
// turn every later single touch into a pinch
|
|
298
|
+
if (event.pointerType !== 'touch' ||
|
|
299
|
+
(event.type !== 'pointerdown' && !this.touches.has(event.pointerId))) {
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (event.type === 'pointerup' || event.type === 'pointercancel') {
|
|
304
|
+
this.touches.delete(event.pointerId)
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
this.touches.set(event.pointerId, [event.clientX, event.clientY])
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let [a, b] = this.touches.values()
|
|
311
|
+
let middle = () => this.Local((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
|
|
312
|
+
|
|
313
|
+
if (event.type !== 'pointermove' && this.touches.size >= 2) {
|
|
314
|
+
// a pinch starts, or its fingers changed: start over from here
|
|
315
|
+
this.offset = null
|
|
316
|
+
this.dragItem = null
|
|
317
|
+
this.pinch = {
|
|
318
|
+
distance : Math.max(Math.hypot(b[0] - a[0], b[1] - a[1]), 1),
|
|
319
|
+
scale : this.scale,
|
|
320
|
+
point : this.Surface(middle()),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (event.type === 'pointermove' && this.pinch && this.touches.size >= 2) {
|
|
324
|
+
let distance = Math.hypot(b[0] - a[0], b[1] - a[1])
|
|
325
|
+
this.ZoomTo(this.pinch.scale * distance / this.pinch.distance)
|
|
326
|
+
this.PanTo(this.pinch.point, middle())
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (this.pinch) {
|
|
330
|
+
if (this.touches.size === 0) {
|
|
331
|
+
this.pinch = null
|
|
332
|
+
}
|
|
333
|
+
event.preventDefault()
|
|
334
|
+
event.stopImmediatePropagation()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
229
338
|
OnMouseWheel(event) {
|
|
230
339
|
// a zero delta (a horizontal-only scroll) would reset the zoom to 1.0
|
|
231
340
|
let delta = normalizeWheel(event)
|
|
232
341
|
if (delta) {
|
|
233
|
-
this.Zoom(delta)
|
|
342
|
+
this.Zoom(delta, this.Local(event.clientX, event.clientY))
|
|
234
343
|
}
|
|
235
344
|
//this.$('#zoomslider').slider('option', 'value', this.scale)
|
|
236
345
|
return cancelEvent(event)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shawcx/avispa",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Draw node/link graphs in SVG and drag, pan, zoom, and collapse them; Backbone views with an optional WebSocket feed",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svg",
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
"build": "npx sass scss/Avispa.scss css/Avispa.css",
|
|
23
23
|
"example": "npm --prefix example install && npm --prefix example start",
|
|
24
24
|
"test": "playwright test",
|
|
25
|
-
"pretest": "npm --prefix example install --no-audit --no-fund && npm --prefix example run build"
|
|
26
|
-
"publish:skunk": "SKUNK_NPM_TOKEN=$(glab config get token --host code.skunk.tools) npm publish --@shawcx:registry=https://code.skunk.tools/api/v4/projects/69/packages/npm/"
|
|
25
|
+
"pretest": "npm --prefix example install --no-audit --no-fund && npm --prefix example run build"
|
|
27
26
|
},
|
|
28
27
|
"repository": {
|
|
29
28
|
"type": "git",
|
|
@@ -44,7 +43,7 @@
|
|
|
44
43
|
"@playwright/test": "^1.64.1",
|
|
45
44
|
"backbone": "^1.4.1",
|
|
46
45
|
"jquery": "^3.6.3 || ^4.0.0",
|
|
47
|
-
"sass": "^1.
|
|
46
|
+
"sass": "^1.106.1",
|
|
48
47
|
"underscore": "^1.13.6"
|
|
49
48
|
}
|
|
50
49
|
}
|