1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
import React, { Component } from 'react'
import { mod, angle } from 'app/utils'
const DEFAULT_MEASUREMENT = { width: 16, height: 16 }
const BACKLINK_SPACING = 10
const ARROWHEAD_LENGTH = 10
const GRAPH_LINK_COLOR = "#ff88ff"
const GRAPH_BACKLINK_COLOR = "#88ffff"
const GRAPH_UNHOVER_LINK_COLOR = "#884488"
const GRAPH_UNHOVER_BACKLINK_COLOR = "#448888"
export default class GraphCanvas extends Component {
constructor(props) {
super(props)
this.canvasRef = React.createRef()
}
componentDidMount() {
if (this.props.bounds) {
this.draw({})
}
}
componentDidUpdate(prevProps) {
this.draw(prevProps)
}
draw(prevProps) {
const { current: canvas } = this.canvasRef
const { bounds, pages, currentPage, box, measurements, highlightedPageId } = this.props
const { width, height } = bounds
if (prevProps.bounds !== bounds) {
canvas.width = width
canvas.height = height
}
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, width, height)
ctx.lineWidth = 2
const coordsLookup = pages.reduce((a,b) => {
if (currentPage && box && b.id === currentPage.id) {
a[b.id] = {
x: box.x,
y: box.y,
backlinks: new Set([]),
}
} else {
a[b.id] = {
x: b.settings.x,
y: b.settings.y,
backlinks: new Set([]),
}
}
return a
}, {})
pages.map(page => {
const sourceCoord = coordsLookup[page.id]
page.backlinks.map(tile => {
if (tile.target_page_id <= 0) return
const targetCoord = coordsLookup[tile.page_id]
const isHighlightedPage = !highlightedPageId || highlightedPageId === page.id || highlightedPageId === tile.page_id
let tile_measurement = measurements[tile.page_id] || DEFAULT_MEASUREMENT
let target_measurement = measurements[tile.target_page_id] || DEFAULT_MEASUREMENT
let theta = angle(targetCoord.x, targetCoord.y, sourceCoord.x, sourceCoord.y)
let x1_offset = tile_measurement.width / 2 // * (0.5 - Math.cos(theta))
let y1_offset = tile_measurement.height / 2
let x2_offset = target_measurement.width / 2 // (0.5 - Math.cos(theta))
let y2_offset = target_measurement.height / 2
// skip duplicate links
if (sourceCoord.backlinks.has(tile.page_id)) {
return
}
/*
if it's pointing right, cos(t) is 1
if it's pointing left, cos(t) is -1
*/
// if (Math.abs(Math.cos(theta)) > 0.5) {
// x1_offset += target_measurement.width / 3 * (- Math.cos(theta))
// x1_offset += target_measurement.height / 4 * (- Math.sin(theta))
x2_offset += target_measurement.width / 3 * (- Math.cos(theta))
y2_offset += target_measurement.height / 6 * (- Math.sin(theta))
// }
// if this is the first time encountering this link...
if (!targetCoord.backlinks.has(tile.target_page_id)) {
sourceCoord.backlinks.add(tile.page_id)
ctx.strokeStyle = isHighlightedPage
? GRAPH_LINK_COLOR
: GRAPH_UNHOVER_LINK_COLOR
} else { // otherwise this is a two-way link
x1_offset += BACKLINK_SPACING * Math.sin(theta)
y1_offset += BACKLINK_SPACING * Math.cos(theta)
x2_offset += BACKLINK_SPACING * Math.sin(theta)
y2_offset += BACKLINK_SPACING * Math.cos(theta)
// x1_offset += BACKLINK_SPACING * Math.cos(theta + Math.PI /2)
// y1_offset += BACKLINK_SPACING * Math.sin(theta + Math.PI /2)
// x2_offset += BACKLINK_SPACING * Math.cos(theta + Math.PI /2)
// y2_offset += BACKLINK_SPACING * Math.sin(theta + Math.PI /2)
ctx.strokeStyle = isHighlightedPage
? GRAPH_BACKLINK_COLOR
: GRAPH_UNHOVER_BACKLINK_COLOR
}
ctx.beginPath()
const x1 = targetCoord.x * width + x1_offset
const y1 = targetCoord.y * height + y1_offset
const x2 = sourceCoord.x * width + x2_offset
const y2 = sourceCoord.y * height + y2_offset
this.arrow(ctx, x1, y1, x2, y2)
ctx.stroke()
})
})
}
arrow(ctx, x1, y1, x2, y2) {
const farOffset = 20
const endOffset = 1
const theta = angle(x1, y1, x2, y2)
x1 += Math.cos(theta) * 0
x2 -= Math.cos(theta) * farOffset
y1 += Math.sin(theta) * 0
y2 -= Math.sin(theta) * farOffset
const xEnd = x2 - Math.cos(theta) * endOffset
const yEnd = y2 - Math.sin(theta) * endOffset
const leftAngle = mod(theta - Math.PI / 6, Math.PI * 2)
const rightAngle = mod(theta + Math.PI / 6, Math.PI * 2)
ctx.moveTo(x1, y1)
ctx.lineTo(xEnd, yEnd)
ctx.moveTo(x2, y2)
ctx.lineTo(x2 - ARROWHEAD_LENGTH * Math.cos(leftAngle), y2 - ARROWHEAD_LENGTH * Math.sin(leftAngle))
ctx.moveTo(x2, y2)
ctx.lineTo(x2 - ARROWHEAD_LENGTH * Math.cos(rightAngle), y2 - ARROWHEAD_LENGTH * Math.sin(rightAngle))
}
render() {
return (
<canvas ref={this.canvasRef} />
)
}
}
|