@painekillers/tree-builder-core 0.0.0-stage → 1.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/graph.js +339 -0
- package/index.js +5 -0
- package/package.json +7 -4
- package/themes/dark.js +154 -0
- package/themes/default.js +240 -0
- package/world/bridge.js +150 -0
- package/world/camera.js +54 -0
- package/world/objects.js +175 -0
- package/world/world.js +240 -0
- package/README.md +0 -3
package/graph.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
export class Node {
|
|
2
|
+
constructor(id, pa, ch, val){
|
|
3
|
+
this.id = id
|
|
4
|
+
this.pa = pa
|
|
5
|
+
this.ch = ch
|
|
6
|
+
this.val = val
|
|
7
|
+
|
|
8
|
+
this.parents = []
|
|
9
|
+
this.children = []
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Graph {
|
|
14
|
+
constructor(nodes){
|
|
15
|
+
this.nodes = new Map(nodes.map(n => [n.id, n]))
|
|
16
|
+
|
|
17
|
+
if (this.nodes.size !== nodes.length) {
|
|
18
|
+
console.warn(`Duplicate node IDs`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// resolve parent and child references
|
|
22
|
+
for (const node of this.nodes.values()) {
|
|
23
|
+
const parentIds = node.pa;
|
|
24
|
+
const childIds = node.ch;
|
|
25
|
+
|
|
26
|
+
// node says X is its parent
|
|
27
|
+
for (const parentId of parentIds) {
|
|
28
|
+
const parent = this.nodeFromId(parentId);
|
|
29
|
+
if(!parent) {
|
|
30
|
+
console.warn(`Parent with id ${parentId} not found for node ${node.id}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!node.parents.includes(parent)) {
|
|
35
|
+
node.parents.push(parent);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!parent.children.includes(node)) {
|
|
39
|
+
parent.children.push(node);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// node says X is its child
|
|
44
|
+
for (const childId of childIds) {
|
|
45
|
+
const child = this.nodeFromId(childId);
|
|
46
|
+
if(!child) {
|
|
47
|
+
console.warn(`Child with id ${childId} not found for node ${node.id}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!node.children.includes(child)) {
|
|
52
|
+
node.children.push(child);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!child.parents.includes(node)) {
|
|
56
|
+
child.parents.push(node);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
nodeFromId(id) {
|
|
63
|
+
return this.nodes.get(id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class LayoutNode {
|
|
69
|
+
constructor(node, l, original = null) {
|
|
70
|
+
this.node = node
|
|
71
|
+
this.connected = []
|
|
72
|
+
|
|
73
|
+
this.layer = l
|
|
74
|
+
this.original = original
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
addConnection(c){
|
|
78
|
+
this.connected.push(c)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
Recursion settings
|
|
84
|
+
Null: No recursion
|
|
85
|
+
False: Creates fake nodes when encountering recursion
|
|
86
|
+
True: Connects back to existing instance of a node when recursing
|
|
87
|
+
#: Real recursion to depths #
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
Strategies
|
|
92
|
+
- [X] Current BFS (First) — Layer by shortest distance from root; first discovery determines placement.
|
|
93
|
+
- [X] Undirected BFS (Undirected) — Shortest distance to root; layering is on directional.
|
|
94
|
+
- [X] Directed BFS, outward only (Outwards) — Follow prerequisite-to-dependent edges only.
|
|
95
|
+
- [X] Furthest-parent layering (Furthest) — Place each node one layer below its deepest prerequisite.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
export class LayoutGraph {
|
|
99
|
+
constructor(graph, root, recurse, strategy = "First") {
|
|
100
|
+
if(strategy == "First" && typeof recurse == "number") {
|
|
101
|
+
throw new Error(`Cannot use strategy ${strategy} with numeric recursion setting`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.layers = new Map()
|
|
105
|
+
|
|
106
|
+
const rootNode = graph.nodeFromId(root)
|
|
107
|
+
if(!rootNode) {
|
|
108
|
+
throw new Error(`Root node with id ${root} not found in graph`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if(recurse === 0) {
|
|
112
|
+
this.layers.set(0, [new LayoutNode(rootNode, 0)])
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const visited = new Map()
|
|
117
|
+
const connections = new Set()
|
|
118
|
+
|
|
119
|
+
const addConnection = (ln, from) => {
|
|
120
|
+
const fromNode =
|
|
121
|
+
this.layers.get(from.layer)?.[from.ind]
|
|
122
|
+
|
|
123
|
+
if(!fromNode) return
|
|
124
|
+
|
|
125
|
+
const a = `${ln.layer}:${ln.ind}`
|
|
126
|
+
const b = `${from.layer}:${from.ind}`
|
|
127
|
+
|
|
128
|
+
const key = [a, b].sort().join("|")
|
|
129
|
+
|
|
130
|
+
if(connections.has(key)) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
connections.add(key)
|
|
135
|
+
ln.addConnection(from)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const queue = [{
|
|
139
|
+
node: rootNode,
|
|
140
|
+
layer: 0,
|
|
141
|
+
from: null
|
|
142
|
+
}]
|
|
143
|
+
|
|
144
|
+
let qi = 0
|
|
145
|
+
|
|
146
|
+
while (qi < queue.length) {
|
|
147
|
+
const { node, layer, from } = queue[qi++]
|
|
148
|
+
|
|
149
|
+
if(typeof recurse == "number" && Math.abs(layer) > recurse) {
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if(!this.layers.has(layer)) {
|
|
154
|
+
this.layers.set(layer, [])
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const key = typeof recurse == "number"
|
|
158
|
+
? `${node.id}|${layer}`
|
|
159
|
+
: node.id
|
|
160
|
+
|
|
161
|
+
if(visited.has(key)) {
|
|
162
|
+
if(from) {
|
|
163
|
+
const pos = visited.get(key)
|
|
164
|
+
const ln = this.layers.get(pos.layer)[pos.ind]
|
|
165
|
+
|
|
166
|
+
//holy crap i cannot think bruh
|
|
167
|
+
if(((recurse === false) || (recurse === null)) && pos.layer !== layer) {
|
|
168
|
+
if (
|
|
169
|
+
recurse == null ||
|
|
170
|
+
strategy == "Outwards" &&
|
|
171
|
+
from !== null &&
|
|
172
|
+
Math.abs(layer) < Math.abs(from.layer)
|
|
173
|
+
) { continue }
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
strategy == "Furthest" &&
|
|
177
|
+
(
|
|
178
|
+
(layer > 0 && !node.parents.every(val => visited.has(val.id)))
|
|
179
|
+
||
|
|
180
|
+
(layer < 0 && !node.children.every(val => visited.has(val.id)))
|
|
181
|
+
)
|
|
182
|
+
) { continue }
|
|
183
|
+
|
|
184
|
+
const fn = new LayoutNode(node, layer, this.layers.get(pos.layer)[pos.ind])
|
|
185
|
+
this.layers.get(layer).push(fn)
|
|
186
|
+
|
|
187
|
+
addConnection(fn, from)
|
|
188
|
+
} else {
|
|
189
|
+
addConnection(ln, from)
|
|
190
|
+
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (
|
|
198
|
+
strategy == "Outwards" &&
|
|
199
|
+
from !== null &&
|
|
200
|
+
Math.abs(layer) < Math.abs(from.layer)
|
|
201
|
+
) { continue }
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
strategy == "Furthest" &&
|
|
205
|
+
(
|
|
206
|
+
(layer > 0 && !node.parents.every(val => visited.has(val.id)))
|
|
207
|
+
||
|
|
208
|
+
(layer < 0 && !node.children.every(val => visited.has(val.id)))
|
|
209
|
+
)
|
|
210
|
+
) { continue }
|
|
211
|
+
|
|
212
|
+
const ln = new LayoutNode(node, layer)
|
|
213
|
+
|
|
214
|
+
this.layers.get(layer).push(ln)
|
|
215
|
+
|
|
216
|
+
const pos = {
|
|
217
|
+
layer,
|
|
218
|
+
ind: this.layers.get(layer).length - 1
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
visited.set(key, pos)
|
|
222
|
+
|
|
223
|
+
if(from) {
|
|
224
|
+
addConnection(ln, from)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const child of node.children) {
|
|
228
|
+
queue.push({
|
|
229
|
+
node: child,
|
|
230
|
+
layer: layer + 1,
|
|
231
|
+
from: pos
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const parent of node.parents) {
|
|
236
|
+
queue.push({
|
|
237
|
+
node: parent,
|
|
238
|
+
layer: strategy !== "Undirected" ? layer - 1: layer + 1,
|
|
239
|
+
from: pos
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export class DrawNode {
|
|
248
|
+
constructor(layoutNode, x, y, width, height) {
|
|
249
|
+
this.layoutNode = layoutNode
|
|
250
|
+
this.x = x
|
|
251
|
+
this.y = y
|
|
252
|
+
this.width = width
|
|
253
|
+
this.height = height
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export class DrawConnection {
|
|
258
|
+
constructor(fromNode, toNode) {
|
|
259
|
+
this.fromNode = fromNode
|
|
260
|
+
this.toNode = toNode
|
|
261
|
+
|
|
262
|
+
if (fromNode.y <= toNode.y) {
|
|
263
|
+
this.from = {
|
|
264
|
+
x: fromNode.x,
|
|
265
|
+
y: fromNode.y + fromNode.height / 2
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.to = {
|
|
269
|
+
x: toNode.x,
|
|
270
|
+
y: toNode.y - toNode.height / 2
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
this.from = {
|
|
274
|
+
x: fromNode.x,
|
|
275
|
+
y: fromNode.y - fromNode.height / 2
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.to = {
|
|
279
|
+
x: toNode.x,
|
|
280
|
+
y: toNode.y + toNode.height / 2
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export class DrawGraph {
|
|
287
|
+
constructor(layoutGraph, nodeWidth, nodeHeight, layerSpacing, nodeSpacing) {
|
|
288
|
+
this.nodes = []
|
|
289
|
+
this.connections = []
|
|
290
|
+
|
|
291
|
+
const drawNodes = new Map()
|
|
292
|
+
|
|
293
|
+
const layers = Array.from(layoutGraph.layers.entries())
|
|
294
|
+
.sort((a, b) => a[0] - b[0])
|
|
295
|
+
|
|
296
|
+
for (const [layerIndex, layerNodes] of layers) {
|
|
297
|
+
const layerWidth =
|
|
298
|
+
layerNodes.length * nodeWidth +
|
|
299
|
+
(layerNodes.length - 1) * nodeSpacing
|
|
300
|
+
|
|
301
|
+
let x = -layerWidth / 2 + nodeWidth / 2
|
|
302
|
+
|
|
303
|
+
const y =
|
|
304
|
+
layerIndex * (nodeHeight + layerSpacing)
|
|
305
|
+
|
|
306
|
+
for (const layoutNode of layerNodes) {
|
|
307
|
+
const drawNode = new DrawNode(
|
|
308
|
+
layoutNode,
|
|
309
|
+
x,
|
|
310
|
+
y,
|
|
311
|
+
nodeWidth,
|
|
312
|
+
nodeHeight
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
this.nodes.push(drawNode)
|
|
316
|
+
drawNodes.set(layoutNode, drawNode)
|
|
317
|
+
|
|
318
|
+
x += nodeWidth + nodeSpacing
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const drawNode of this.nodes) {
|
|
323
|
+
for (const connection of drawNode.layoutNode.connected) {
|
|
324
|
+
const toLayoutNode =
|
|
325
|
+
layoutGraph.layers
|
|
326
|
+
.get(connection.layer)[connection.ind]
|
|
327
|
+
|
|
328
|
+
const toDrawNode =
|
|
329
|
+
drawNodes.get(toLayoutNode)
|
|
330
|
+
|
|
331
|
+
if (toDrawNode) {
|
|
332
|
+
this.connections.push(
|
|
333
|
+
new DrawConnection(drawNode, toDrawNode)
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@painekillers/tree-builder-core",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./index.js",
|
|
7
|
+
"./themes/*": "./themes/*"
|
|
8
|
+
}
|
|
9
|
+
}
|
package/themes/dark.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
background: Object.assign(new Image(),
|
|
3
|
+
{
|
|
4
|
+
src: "data:image/svg+xml," + encodeURIComponent(`
|
|
5
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080">
|
|
6
|
+
<defs>
|
|
7
|
+
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
|
8
|
+
<path
|
|
9
|
+
d="M 40 0 L 0 0 0 40"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="#3f4147"
|
|
12
|
+
stroke-width="1"
|
|
13
|
+
opacity="0.35"
|
|
14
|
+
/>
|
|
15
|
+
</pattern>
|
|
16
|
+
</defs>
|
|
17
|
+
|
|
18
|
+
<rect
|
|
19
|
+
width="1920"
|
|
20
|
+
height="1080"
|
|
21
|
+
fill="#111112"
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<rect
|
|
25
|
+
width="1920"
|
|
26
|
+
height="1080"
|
|
27
|
+
fill="url(#grid)"
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
`)
|
|
31
|
+
}
|
|
32
|
+
),
|
|
33
|
+
|
|
34
|
+
draw: {
|
|
35
|
+
line(ctx, object) {
|
|
36
|
+
ctx.save()
|
|
37
|
+
|
|
38
|
+
ctx.lineCap = "round"
|
|
39
|
+
|
|
40
|
+
ctx.beginPath()
|
|
41
|
+
|
|
42
|
+
ctx.moveTo(
|
|
43
|
+
object.from.x,
|
|
44
|
+
object.from.y
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
ctx.lineTo(
|
|
48
|
+
object.to.x,
|
|
49
|
+
object.to.y
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
ctx.lineWidth = object.width
|
|
53
|
+
ctx.strokeStyle = "#4b4d52"
|
|
54
|
+
ctx.stroke()
|
|
55
|
+
|
|
56
|
+
ctx.restore()
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
node(ctx, object) {
|
|
60
|
+
const pos = object.pos
|
|
61
|
+
const size = object.size
|
|
62
|
+
|
|
63
|
+
const width = size.width * object.scale
|
|
64
|
+
const height = size.height * object.scale
|
|
65
|
+
|
|
66
|
+
const x = pos.x - width / 2
|
|
67
|
+
const y = pos.y - height / 2
|
|
68
|
+
|
|
69
|
+
const radius = 10
|
|
70
|
+
|
|
71
|
+
const fake = object.drawNode.layoutNode.original !== null
|
|
72
|
+
|
|
73
|
+
ctx.save()
|
|
74
|
+
|
|
75
|
+
ctx.shadowColor = "rgba(0, 0, 0, 0.35)"
|
|
76
|
+
ctx.shadowBlur = object.hovered ? 14 : 7
|
|
77
|
+
ctx.shadowOffsetY = object.hovered ? 3 : 2
|
|
78
|
+
|
|
79
|
+
ctx.fillStyle = "#2b2d31"
|
|
80
|
+
|
|
81
|
+
ctx.beginPath()
|
|
82
|
+
ctx.roundRect(
|
|
83
|
+
x,
|
|
84
|
+
y,
|
|
85
|
+
width,
|
|
86
|
+
height,
|
|
87
|
+
radius
|
|
88
|
+
)
|
|
89
|
+
ctx.fill()
|
|
90
|
+
|
|
91
|
+
ctx.shadowColor = "transparent"
|
|
92
|
+
|
|
93
|
+
ctx.save()
|
|
94
|
+
|
|
95
|
+
ctx.beginPath()
|
|
96
|
+
ctx.roundRect(
|
|
97
|
+
x,
|
|
98
|
+
y,
|
|
99
|
+
width,
|
|
100
|
+
height,
|
|
101
|
+
radius
|
|
102
|
+
)
|
|
103
|
+
ctx.clip()
|
|
104
|
+
|
|
105
|
+
ctx.globalAlpha = object.hovered ? 1 : 0.92
|
|
106
|
+
|
|
107
|
+
ctx.drawImage(
|
|
108
|
+
object.image,
|
|
109
|
+
x,
|
|
110
|
+
y,
|
|
111
|
+
width,
|
|
112
|
+
height
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
ctx.restore()
|
|
116
|
+
|
|
117
|
+
ctx.beginPath()
|
|
118
|
+
ctx.roundRect(
|
|
119
|
+
x,
|
|
120
|
+
y,
|
|
121
|
+
width,
|
|
122
|
+
height,
|
|
123
|
+
radius
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
ctx.lineWidth = 1
|
|
127
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.08)"
|
|
128
|
+
|
|
129
|
+
if(fake) {
|
|
130
|
+
ctx.setLineDash([4, 4])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ctx.stroke()
|
|
134
|
+
ctx.setLineDash([])
|
|
135
|
+
|
|
136
|
+
if (object.root) {
|
|
137
|
+
ctx.beginPath()
|
|
138
|
+
|
|
139
|
+
ctx.arc(
|
|
140
|
+
x + width - 9,
|
|
141
|
+
y + 9,
|
|
142
|
+
3,
|
|
143
|
+
0,
|
|
144
|
+
Math.PI * 2
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.35)"
|
|
148
|
+
ctx.fill()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
ctx.restore()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const animate = node => {
|
|
2
|
+
if(node.animation) return
|
|
3
|
+
|
|
4
|
+
const animate = () => {
|
|
5
|
+
const target = node.hovered ? 1.04 : 1
|
|
6
|
+
const difference = target - node.scale
|
|
7
|
+
|
|
8
|
+
if(Math.abs(difference) < 0.001) {
|
|
9
|
+
node.scale = target
|
|
10
|
+
node.animation = null
|
|
11
|
+
node.world.update()
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
node.scale += difference * 0.18
|
|
16
|
+
|
|
17
|
+
node.world.update()
|
|
18
|
+
|
|
19
|
+
node.animation = requestAnimationFrame(animate)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
node.animation = requestAnimationFrame(animate)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
background: Object.assign(new Image(),
|
|
27
|
+
{src: "data:image/svg+xml," + encodeURIComponent(`
|
|
28
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080">
|
|
29
|
+
<defs>
|
|
30
|
+
<linearGradient id="base" x1="0" y1="0" x2="0" y2="1">
|
|
31
|
+
<stop offset="0%" stop-color="#f4f5f7"/>
|
|
32
|
+
<stop offset="100%" stop-color="#e8eaed"/>
|
|
33
|
+
</linearGradient>
|
|
34
|
+
|
|
35
|
+
<radialGradient id="light" cx="50%" cy="42%" r="65%">
|
|
36
|
+
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
|
|
37
|
+
<stop offset="60%" stop-color="#ffffff" stop-opacity="0.3"/>
|
|
38
|
+
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
|
|
39
|
+
</radialGradient>
|
|
40
|
+
|
|
41
|
+
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
|
42
|
+
<path
|
|
43
|
+
d="M 40 0 L 0 0 0 40"
|
|
44
|
+
fill="none"
|
|
45
|
+
stroke="#9da3ad"
|
|
46
|
+
stroke-width="1"
|
|
47
|
+
opacity="0.28"
|
|
48
|
+
/>
|
|
49
|
+
</pattern>
|
|
50
|
+
|
|
51
|
+
<radialGradient id="fade" cx="50%" cy="50%" r="70%">
|
|
52
|
+
<stop offset="60%" stop-color="#ffffff" stop-opacity="0"/>
|
|
53
|
+
<stop offset="100%" stop-color="#c8ccd3" stop-opacity="0.2"/>
|
|
54
|
+
</radialGradient>
|
|
55
|
+
</defs>
|
|
56
|
+
|
|
57
|
+
<rect width="1920" height="1080" fill="url(#base)"/>
|
|
58
|
+
<rect width="1920" height="1080" fill="url(#light)"/>
|
|
59
|
+
<rect width="1920" height="1080" fill="url(#grid)"/>
|
|
60
|
+
<rect width="1920" height="1080" fill="url(#fade)"/>
|
|
61
|
+
</svg>
|
|
62
|
+
`)}
|
|
63
|
+
),
|
|
64
|
+
|
|
65
|
+
focusButton: `
|
|
66
|
+
<svg viewBox="0 0 24 24">
|
|
67
|
+
<circle cx="12" cy="12" r="7"/>
|
|
68
|
+
<circle cx="12" cy="12" r="2"/>
|
|
69
|
+
<path d="M12 2V5"/>
|
|
70
|
+
<path d="M12 19V22"/>
|
|
71
|
+
<path d="M2 12H5"/>
|
|
72
|
+
<path d="M19 12H22"/>
|
|
73
|
+
</svg>
|
|
74
|
+
`,
|
|
75
|
+
|
|
76
|
+
rootButton: `
|
|
77
|
+
<svg viewBox="0 0 24 24">
|
|
78
|
+
<circle cx="12" cy="5" r="2.5"/>
|
|
79
|
+
<circle cx="6" cy="19" r="2.5"/>
|
|
80
|
+
<circle cx="18" cy="19" r="2.5"/>
|
|
81
|
+
<path d="M12 7.5V12"/>
|
|
82
|
+
<path d="M12 12L6 16.5"/>
|
|
83
|
+
<path d="M12 12L18 16.5"/>
|
|
84
|
+
</svg>
|
|
85
|
+
`,
|
|
86
|
+
|
|
87
|
+
searchButton: `
|
|
88
|
+
<svg viewBox="0 0 24 24">
|
|
89
|
+
<circle cx="11" cy="11" r="6.5"></circle>
|
|
90
|
+
<line x1="16" y1="16" x2="21" y2="21"></line>
|
|
91
|
+
</svg>
|
|
92
|
+
`,
|
|
93
|
+
|
|
94
|
+
themeButton: `
|
|
95
|
+
<svg
|
|
96
|
+
viewBox="0 0 24 24"
|
|
97
|
+
fill="none"
|
|
98
|
+
stroke="currentColor"
|
|
99
|
+
stroke-width="2"
|
|
100
|
+
stroke-linecap="round"
|
|
101
|
+
stroke-linejoin="round"
|
|
102
|
+
>
|
|
103
|
+
<circle cx="13.5" cy="6.5" r=".5"></circle>
|
|
104
|
+
<circle cx="17.5" cy="10.5" r=".5"></circle>
|
|
105
|
+
<circle cx="8.5" cy="7.5" r=".5"></circle>
|
|
106
|
+
<circle cx="6.5" cy="12.5" r=".5"></circle>
|
|
107
|
+
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"></path>
|
|
108
|
+
</svg>
|
|
109
|
+
`,
|
|
110
|
+
|
|
111
|
+
draw: {
|
|
112
|
+
line(ctx, object) {
|
|
113
|
+
ctx.save()
|
|
114
|
+
|
|
115
|
+
ctx.lineCap = "round"
|
|
116
|
+
|
|
117
|
+
ctx.shadowColor = "rgba(0, 0, 0, 0.12)"
|
|
118
|
+
ctx.shadowBlur = 4
|
|
119
|
+
ctx.shadowOffsetY = 1
|
|
120
|
+
|
|
121
|
+
ctx.beginPath()
|
|
122
|
+
|
|
123
|
+
ctx.moveTo(
|
|
124
|
+
object.from.x,
|
|
125
|
+
object.from.y
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
ctx.lineTo(
|
|
129
|
+
object.to.x,
|
|
130
|
+
object.to.y
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
ctx.lineWidth = object.width
|
|
134
|
+
ctx.strokeStyle = "#999"
|
|
135
|
+
ctx.stroke()
|
|
136
|
+
|
|
137
|
+
ctx.restore()
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
node(ctx, object) {
|
|
141
|
+
const pos = object.pos
|
|
142
|
+
const size = object.size
|
|
143
|
+
|
|
144
|
+
const width = size.width * object.scale
|
|
145
|
+
const height = size.height * object.scale
|
|
146
|
+
|
|
147
|
+
const x = pos.x - width / 2
|
|
148
|
+
const y = pos.y - height / 2
|
|
149
|
+
|
|
150
|
+
const radius = 10
|
|
151
|
+
|
|
152
|
+
const fake = object.drawNode.layoutNode.original !== null
|
|
153
|
+
|
|
154
|
+
ctx.save()
|
|
155
|
+
|
|
156
|
+
ctx.shadowColor = "rgba(0, 0, 0, 0.14)"
|
|
157
|
+
ctx.shadowBlur = object.hovered ? 16 : 8
|
|
158
|
+
ctx.shadowOffsetY = object.hovered ? 4 : 2
|
|
159
|
+
|
|
160
|
+
ctx.fillStyle = "#f8f8f8"
|
|
161
|
+
|
|
162
|
+
ctx.beginPath()
|
|
163
|
+
ctx.roundRect(x, y, width, height, radius)
|
|
164
|
+
ctx.fill()
|
|
165
|
+
|
|
166
|
+
ctx.shadowColor = "transparent"
|
|
167
|
+
|
|
168
|
+
ctx.save()
|
|
169
|
+
|
|
170
|
+
ctx.beginPath()
|
|
171
|
+
ctx.roundRect(x, y, width, height, radius)
|
|
172
|
+
ctx.clip()
|
|
173
|
+
|
|
174
|
+
ctx.globalAlpha = object.hovered ? 1 : 0.92
|
|
175
|
+
|
|
176
|
+
ctx.drawImage(
|
|
177
|
+
object.image,
|
|
178
|
+
x,
|
|
179
|
+
y,
|
|
180
|
+
width,
|
|
181
|
+
height
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
ctx.restore()
|
|
185
|
+
|
|
186
|
+
ctx.beginPath()
|
|
187
|
+
ctx.roundRect(x, y, width, height, radius)
|
|
188
|
+
|
|
189
|
+
ctx.lineWidth = 1
|
|
190
|
+
ctx.strokeStyle = "rgba(0, 0, 0, 0.08)"
|
|
191
|
+
|
|
192
|
+
if(fake) {
|
|
193
|
+
ctx.setLineDash([4, 4])
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
ctx.stroke()
|
|
197
|
+
ctx.setLineDash([])
|
|
198
|
+
|
|
199
|
+
if(object.root) {
|
|
200
|
+
ctx.beginPath()
|
|
201
|
+
ctx.arc(
|
|
202
|
+
x + width - 9,
|
|
203
|
+
y + 9,
|
|
204
|
+
3,
|
|
205
|
+
0,
|
|
206
|
+
Math.PI * 2
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
|
|
210
|
+
ctx.fill()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
ctx.restore()
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
focus(node) {
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
node.hovered = true
|
|
220
|
+
animate(node)
|
|
221
|
+
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
if(node.world.hover !== node) {
|
|
224
|
+
node.hovered = false
|
|
225
|
+
animate(node)
|
|
226
|
+
}
|
|
227
|
+
}, 600)
|
|
228
|
+
}, 150)
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
mouseEnter(node) {
|
|
232
|
+
node.hovered = true
|
|
233
|
+
animate(node)
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
mouseLeave(node) {
|
|
237
|
+
node.hovered = false
|
|
238
|
+
animate(node)
|
|
239
|
+
}
|
|
240
|
+
}
|
package/world/bridge.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { LayoutGraph, DrawGraph } from "../graph.js"
|
|
2
|
+
import { NodeInstance, Line } from "./objects.js"
|
|
3
|
+
|
|
4
|
+
export default class Bridge {
|
|
5
|
+
constructor(graph, world) {
|
|
6
|
+
this.graph = graph
|
|
7
|
+
this.world = world
|
|
8
|
+
|
|
9
|
+
this.layout = null
|
|
10
|
+
this.draw = null
|
|
11
|
+
this.obj = null
|
|
12
|
+
this.toObj = null
|
|
13
|
+
this.toNode = null
|
|
14
|
+
|
|
15
|
+
this.root = null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#cleanWorld() {
|
|
19
|
+
if(this.obj) {
|
|
20
|
+
this.world.bulkRemove(obj => this.obj.includes(obj))
|
|
21
|
+
|
|
22
|
+
this.obj = null
|
|
23
|
+
this.toObj = null
|
|
24
|
+
this.toNode = null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
calculateLayout(root, recurse, strategy = "First"){ // Real maxdepth, since we cant actually make it infinite
|
|
29
|
+
this.layout = new LayoutGraph(this.graph, root, recurse, strategy)
|
|
30
|
+
// Since JS is goofy like this ill just have recurse be a number when we want recursive
|
|
31
|
+
this.root = this.graph.nodes.get(root)
|
|
32
|
+
|
|
33
|
+
// Invalidate everything else
|
|
34
|
+
this.draw = null
|
|
35
|
+
this.#cleanWorld()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
calculateDraw(width, height, padX, padY){
|
|
39
|
+
if(!this.layout){
|
|
40
|
+
console.warn("calculateLayout must be called prior to this")
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.draw = new DrawGraph(this.layout, width, height, padX, padY)
|
|
45
|
+
this.#cleanWorld()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
createObjects(maxDepth){
|
|
49
|
+
if(!this.draw){
|
|
50
|
+
console.warn("calculateDraw must be called prior to this")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.#cleanWorld()
|
|
55
|
+
|
|
56
|
+
this.obj = []
|
|
57
|
+
this.toObj = new Map()
|
|
58
|
+
this.toNode = new Map()
|
|
59
|
+
|
|
60
|
+
// Add edges first so nodes render on top
|
|
61
|
+
for (const connection of this.draw.connections) {
|
|
62
|
+
if(
|
|
63
|
+
maxDepth !== null &&
|
|
64
|
+
(
|
|
65
|
+
Math.abs(connection.fromNode.layoutNode.layer) > maxDepth ||
|
|
66
|
+
Math.abs(connection.toNode.layoutNode.layer) > maxDepth
|
|
67
|
+
)
|
|
68
|
+
){
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let obj = new Line(
|
|
73
|
+
this.world,
|
|
74
|
+
connection.from,
|
|
75
|
+
connection.to,
|
|
76
|
+
2
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
this.obj.push(obj)
|
|
80
|
+
this.world.addObject(obj)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add nodes
|
|
84
|
+
for (const drawNode of this.draw.nodes) {
|
|
85
|
+
if(
|
|
86
|
+
maxDepth !== null &&
|
|
87
|
+
Math.abs(drawNode.layoutNode.layer) > maxDepth
|
|
88
|
+
){
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let obj = new NodeInstance(
|
|
93
|
+
this.world,
|
|
94
|
+
drawNode.x,
|
|
95
|
+
drawNode.y,
|
|
96
|
+
drawNode.width,
|
|
97
|
+
drawNode.height,
|
|
98
|
+
drawNode.layoutNode.node,
|
|
99
|
+
drawNode.layoutNode.node.val?.icon,
|
|
100
|
+
drawNode
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
obj.root =
|
|
104
|
+
drawNode.layoutNode.node === this.root
|
|
105
|
+
|
|
106
|
+
this.obj.push(obj)
|
|
107
|
+
|
|
108
|
+
if (drawNode.layoutNode.original === null) {
|
|
109
|
+
this.toNode.set(
|
|
110
|
+
obj,
|
|
111
|
+
drawNode.layoutNode.node
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
this.toObj.set(
|
|
115
|
+
drawNode.layoutNode.node,
|
|
116
|
+
obj
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.world.addObject(obj)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.focus(this.root)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
focus(node) {
|
|
127
|
+
if(!this.toObj) {
|
|
128
|
+
console.warn("createObjects must be called prior to this")
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const obj = this.toObj.get(node)
|
|
133
|
+
|
|
134
|
+
if(!obj) {
|
|
135
|
+
console.warn(`Node ${node.id} is not currently visible`)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.world.camera.pos = [
|
|
140
|
+
obj.pos.x,
|
|
141
|
+
obj.pos.y
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
this.world.camera.zoom = 1.5
|
|
145
|
+
|
|
146
|
+
obj.focus()
|
|
147
|
+
|
|
148
|
+
this.world.update?.()
|
|
149
|
+
}
|
|
150
|
+
}
|
package/world/camera.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export default class Camera {
|
|
2
|
+
#world
|
|
3
|
+
|
|
4
|
+
#x = 0
|
|
5
|
+
#y = 0
|
|
6
|
+
#width = 0
|
|
7
|
+
#height = 0
|
|
8
|
+
#zoom = 1
|
|
9
|
+
|
|
10
|
+
constructor(world) {
|
|
11
|
+
this.#world = world
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
set pos(p){
|
|
15
|
+
if (this.#world.paused) return
|
|
16
|
+
|
|
17
|
+
this.#x = p[0]
|
|
18
|
+
this.#y = p[1]
|
|
19
|
+
this.#world.update()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
set viewport(v){
|
|
23
|
+
if (this.#world.paused) return
|
|
24
|
+
|
|
25
|
+
this.#width = v[0]
|
|
26
|
+
this.#height = v[1]
|
|
27
|
+
this.#world.update()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set zoom(val) {
|
|
31
|
+
if (this.#world.paused) return
|
|
32
|
+
|
|
33
|
+
this.#zoom = val
|
|
34
|
+
this.#world.update()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get pos(){
|
|
38
|
+
return {
|
|
39
|
+
x: this.#x,
|
|
40
|
+
y: this.#y
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get viewport(){
|
|
45
|
+
return {
|
|
46
|
+
width: this.#width,
|
|
47
|
+
height: this.#height
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get zoom(){
|
|
52
|
+
return this.#zoom
|
|
53
|
+
}
|
|
54
|
+
}
|
package/world/objects.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
class Object {
|
|
2
|
+
#world
|
|
3
|
+
|
|
4
|
+
constructor(world) {
|
|
5
|
+
this.#world = world
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get world() {
|
|
9
|
+
return this.#world
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class PSInstance extends Object {
|
|
14
|
+
#x
|
|
15
|
+
#y
|
|
16
|
+
|
|
17
|
+
#width
|
|
18
|
+
#height
|
|
19
|
+
|
|
20
|
+
constructor(world, x, y, width, height) {
|
|
21
|
+
super(world)
|
|
22
|
+
|
|
23
|
+
this.#x = x
|
|
24
|
+
this.#y = y
|
|
25
|
+
this.#width = width
|
|
26
|
+
this.#height = height
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set pos(p) {
|
|
30
|
+
this.#x = p[0]
|
|
31
|
+
this.#y = p[1]
|
|
32
|
+
this.world.update()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
set size(s) {
|
|
36
|
+
this.#width = s[0]
|
|
37
|
+
this.#height = s[1]
|
|
38
|
+
this.world.update()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get pos() {
|
|
42
|
+
return {
|
|
43
|
+
x: this.#x,
|
|
44
|
+
y: this.#y
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get size() {
|
|
49
|
+
return {
|
|
50
|
+
width: this.#width,
|
|
51
|
+
height: this.#height
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*export class test extends PSInstance {
|
|
57
|
+
constructor(world, x, y, width, height, value) {
|
|
58
|
+
super(world, x, y, width, height)
|
|
59
|
+
|
|
60
|
+
this.value = value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
draw(ctx) {
|
|
64
|
+
let pos = this.pos
|
|
65
|
+
let size = this.size
|
|
66
|
+
|
|
67
|
+
ctx.fillStyle = "red"
|
|
68
|
+
ctx.fillRect(
|
|
69
|
+
pos.x - size.width / 2,
|
|
70
|
+
pos.y - size.height / 2,
|
|
71
|
+
size.width,
|
|
72
|
+
size.height
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
ctx.fillStyle = "white"
|
|
76
|
+
ctx.textAlign = "center"
|
|
77
|
+
ctx.textBaseline = "middle"
|
|
78
|
+
ctx.font = "16px sans-serif"
|
|
79
|
+
ctx.fillText(
|
|
80
|
+
this.value,
|
|
81
|
+
pos.x,
|
|
82
|
+
pos.y
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}*/
|
|
86
|
+
|
|
87
|
+
const placeholder = new Image()
|
|
88
|
+
|
|
89
|
+
placeholder.src = "data:image/svg+xml," + encodeURIComponent(`
|
|
90
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
|
|
91
|
+
<text x="100" y="105"
|
|
92
|
+
text-anchor="middle"
|
|
93
|
+
font-family="sans-serif"
|
|
94
|
+
font-size="20"
|
|
95
|
+
fill="#b0b0b5">
|
|
96
|
+
No Image
|
|
97
|
+
</text>
|
|
98
|
+
</svg>
|
|
99
|
+
`)
|
|
100
|
+
|
|
101
|
+
export class ImageInstance extends PSInstance {
|
|
102
|
+
#image
|
|
103
|
+
|
|
104
|
+
constructor(world, x, y, width, height, image) {
|
|
105
|
+
super(world, x, y, width, height)
|
|
106
|
+
|
|
107
|
+
this.image = image
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
set image(img) {
|
|
111
|
+
this.#image = img
|
|
112
|
+
this.world.update()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get image() {
|
|
116
|
+
return this.#image ?? placeholder
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
draw(ctx, p, s) {
|
|
120
|
+
let pos = p || this.pos
|
|
121
|
+
let size = s || this.size
|
|
122
|
+
|
|
123
|
+
ctx.drawImage(
|
|
124
|
+
this.image,
|
|
125
|
+
pos.x - size.width / 2,
|
|
126
|
+
pos.y - size.height / 2,
|
|
127
|
+
size.width,
|
|
128
|
+
size.height
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class Line extends Object {
|
|
134
|
+
constructor(world, from, to, width = 2) {
|
|
135
|
+
super(world)
|
|
136
|
+
|
|
137
|
+
this.from = from
|
|
138
|
+
this.to = to
|
|
139
|
+
this.width = width
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
draw(ctx) {
|
|
143
|
+
this.world.theme.draw.line(ctx, this)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class NodeInstance extends ImageInstance {
|
|
148
|
+
constructor(world, x, y, width, height, node, image, drawNode) {
|
|
149
|
+
super(world, x, y, width, height, image)
|
|
150
|
+
|
|
151
|
+
this.node = node
|
|
152
|
+
this.hovered = false
|
|
153
|
+
this.scale = 1
|
|
154
|
+
this.animation = null
|
|
155
|
+
this.root = false
|
|
156
|
+
|
|
157
|
+
this.drawNode = drawNode
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
focus() {
|
|
161
|
+
this.world.theme.focus(this)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
mouseEnter() {
|
|
165
|
+
this.world.theme.mouseEnter(this)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
mouseLeave() {
|
|
169
|
+
this.world.theme.mouseLeave(this)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
draw(ctx) {
|
|
173
|
+
this.world.theme.draw.node(ctx, this)
|
|
174
|
+
}
|
|
175
|
+
}
|
package/world/world.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
export default class World {
|
|
2
|
+
/** @type {HTMLCanvasElement} */
|
|
3
|
+
#canvas
|
|
4
|
+
#ctx
|
|
5
|
+
|
|
6
|
+
#camera
|
|
7
|
+
#theme
|
|
8
|
+
|
|
9
|
+
#paused = false
|
|
10
|
+
|
|
11
|
+
updateRate = 1/60
|
|
12
|
+
|
|
13
|
+
#objects = []
|
|
14
|
+
#background
|
|
15
|
+
|
|
16
|
+
constructor(canvas, theme) {
|
|
17
|
+
this.#theme = theme
|
|
18
|
+
this.#canvas = canvas
|
|
19
|
+
this.#ctx = canvas.getContext("2d")
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set paused(value) {
|
|
24
|
+
this.#paused = value
|
|
25
|
+
|
|
26
|
+
if (value) {
|
|
27
|
+
this.#hoveredObject?.mouseLeave?.()
|
|
28
|
+
this.#hoveredObject = null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get paused() {
|
|
33
|
+
return this.#paused
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set theme(value) {
|
|
37
|
+
this.#theme = value
|
|
38
|
+
this.update()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get theme() {
|
|
42
|
+
return this.#theme
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
set camera(cam) {
|
|
46
|
+
this.#camera = cam
|
|
47
|
+
this.update()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get camera() {
|
|
51
|
+
return this.#camera
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
set background(bg) {
|
|
55
|
+
this.#background = bg
|
|
56
|
+
this.update()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get hover() {
|
|
60
|
+
return this.#hoveredObject
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
addObject(obj) {
|
|
64
|
+
if (this.#paused) return
|
|
65
|
+
|
|
66
|
+
let index = this.#objects.findIndex(e => e.z > obj.z)
|
|
67
|
+
|
|
68
|
+
if (index === -1) {
|
|
69
|
+
this.#objects.push(obj)
|
|
70
|
+
} else {
|
|
71
|
+
this.#objects.splice(index, 0, obj)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.update()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
removeObject(obj) {
|
|
78
|
+
if (this.#paused) return
|
|
79
|
+
|
|
80
|
+
const index = this.#objects.indexOf(obj)
|
|
81
|
+
|
|
82
|
+
if (index !== -1) {
|
|
83
|
+
this.#objects.splice(index, 1)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.#hoveredObject === obj) {
|
|
87
|
+
this.#hoveredObject = null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.update()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
bulkRemove(func) {
|
|
94
|
+
if (this.#paused) return
|
|
95
|
+
|
|
96
|
+
this.#objects = this.#objects.filter(obj => {
|
|
97
|
+
if (func(obj)) {
|
|
98
|
+
if (this.#hoveredObject === obj) {
|
|
99
|
+
this.#hoveredObject = null
|
|
100
|
+
}
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
this.update()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
clear() {
|
|
111
|
+
if (this.#paused) return
|
|
112
|
+
|
|
113
|
+
this.#objects = []
|
|
114
|
+
this.#hoveredObject = null
|
|
115
|
+
|
|
116
|
+
this.update()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get objects() {
|
|
120
|
+
return this.#objects
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#draw() {
|
|
124
|
+
this.#ctx.clearRect(
|
|
125
|
+
0,
|
|
126
|
+
0,
|
|
127
|
+
this.#canvas.width,
|
|
128
|
+
this.#canvas.height
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if (this.#background) {
|
|
132
|
+
this.#ctx.drawImage(
|
|
133
|
+
this.#background,
|
|
134
|
+
0,
|
|
135
|
+
0,
|
|
136
|
+
this.#canvas.width,
|
|
137
|
+
this.#canvas.height
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.#ctx.save()
|
|
142
|
+
|
|
143
|
+
const cpos = this.#camera.pos
|
|
144
|
+
|
|
145
|
+
this.#ctx.translate(
|
|
146
|
+
this.#canvas.width / 2,
|
|
147
|
+
this.#canvas.height / 2
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
this.#ctx.scale(
|
|
151
|
+
this.#camera.zoom,
|
|
152
|
+
this.#camera.zoom
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
this.#ctx.translate(
|
|
156
|
+
-cpos.x,
|
|
157
|
+
-cpos.y
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
this.#objects.forEach(e => e.draw(this.#ctx))
|
|
161
|
+
|
|
162
|
+
this.#ctx.restore()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#frameRequested = false
|
|
166
|
+
|
|
167
|
+
update() {
|
|
168
|
+
if (this.#paused) return
|
|
169
|
+
if (this.#frameRequested) return
|
|
170
|
+
|
|
171
|
+
this.#frameRequested = true
|
|
172
|
+
|
|
173
|
+
requestAnimationFrame(() => {
|
|
174
|
+
this.#frameRequested = false
|
|
175
|
+
|
|
176
|
+
this.#draw()
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
resize(width, height) {
|
|
181
|
+
this.#canvas.width = width
|
|
182
|
+
this.#canvas.height = height
|
|
183
|
+
|
|
184
|
+
this.#draw()
|
|
185
|
+
|
|
186
|
+
this.#camera.viewport = [width, height]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#hoveredObject = null
|
|
190
|
+
|
|
191
|
+
mouseWorldPosition(e) {
|
|
192
|
+
if (!e) {
|
|
193
|
+
return {
|
|
194
|
+
x: null,
|
|
195
|
+
y: null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const rect = this.#canvas.getBoundingClientRect()
|
|
200
|
+
|
|
201
|
+
const x =
|
|
202
|
+
e.clientX -
|
|
203
|
+
rect.left -
|
|
204
|
+
this.#canvas.width / 2
|
|
205
|
+
|
|
206
|
+
const y =
|
|
207
|
+
e.clientY -
|
|
208
|
+
rect.top -
|
|
209
|
+
this.#canvas.height / 2
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
x: x / this.#camera.zoom + this.#camera.pos.x,
|
|
213
|
+
y: y / this.#camera.zoom + this.#camera.pos.y
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
objectAt(x, y) {
|
|
218
|
+
for (let i = this.#objects.length - 1; i >= 0; i--) {
|
|
219
|
+
const object = this.#objects[i]
|
|
220
|
+
|
|
221
|
+
if (!object.pos || !object.size) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const pos = object.pos
|
|
226
|
+
const size = object.size
|
|
227
|
+
|
|
228
|
+
if (
|
|
229
|
+
x >= pos.x - size.width / 2 &&
|
|
230
|
+
x <= pos.x + size.width / 2 &&
|
|
231
|
+
y >= pos.y - size.height / 2 &&
|
|
232
|
+
y <= pos.y + size.height / 2
|
|
233
|
+
) {
|
|
234
|
+
return object
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
240
|
+
}
|
package/README.md
DELETED