summaryrefslogtreecommitdiff
path: root/frontend/app/views/graph/components/graph.canvas.js
blob: 2896c6bf528dde3802898ff7e300d0ce2c33f58e (plain)
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} />
    )
  }
}