Skip to content
Snippets Groups Projects
Commit f870028f authored by Adam Procter's avatar Adam Procter
Browse files

start of links

parent cbfa741f
Branches linksLayer
No related tags found
No related merge requests found
<template>
<canvas ref="canvas" :width="width" :height="height"></canvas>
</template>
<script>
import { getPalette } from './mixins/color.js'
import { drawLinkBezierCurve, drawArrowHead } from './mixins/canvas.js'
export default {
name: 'LinksLayer',
mixins: [getPalette, drawLinkBezierCurve, drawArrowHead],
props: {
width: {
type: Number
},
height: {
type: Number
},
nodes: {
type: Array
},
links: {
type: Array
}
},
mounted() {
this.canvas = this.$refs.canvas
this.context = this.canvas.getContext('2d')
this.draw()
},
data() {
return {
canvas: {},
context: {},
defaultLinkProps: {
// these are our initial 'default' settings for each link
hue: 'dark',
tension: 0.25,
lineWidth: 3,
lineDash: [0, 0]
}
}
},
// Watch the props for changes and, if necessary, redraw canvas
watch: {
width() {
this.draw()
},
height() {
this.draw()
},
nodes() {
this.draw()
},
links() {
this.draw()
}
},
methods: {
/**
* Clears the context, renders links and nodes on initial load
* and when new data has been provided
**/
draw() {
this.clear()
this.context.save()
this.renderAllLinks()
this.renderAllNodes()
this.context.restore()
},
/**
* Clears drawing context
**/
clear() {
this.context.clearRect(0, 0, this.width, this.height)
},
/**
* Where a node can be rendered
* @param {Node} node - Target node
**/
renderNode({ x, y }) {
this.context.setTransform(1, 0, 0, 1, x, y)
// this renders nothing, but if we wanted to we
// could render nodes in the canvas as well
this.context.setTransform(1, 0, 0, 1, 0, 0)
},
/**
* Renders a link between two nodes
**/
renderLink({ from, to, color, lineDash, lineWidth, hue, tension, arrow }) {
// fetch the nodes based on their id reference provided in the link
const fromNode = this.findNode(from)
const toNode = this.findNode(to)
if (fromNode && toNode) {
// apply the link color, falling back to defaultLinkProps
this.context.strokeStyle = getPalette(
color,
hue || this.defaultLinkProps.hue
)
// apply link styling, falling back to defaultLinkProps
this.context.setLineDash(lineDash || this.defaultLinkProps.lineDash)
this.context.lineWidth = lineWidth || this.defaultLinkProps.lineWidth
// establish link tension, falling back to defaultLinkProps
const curveTension = !isNaN(tension) || this.defaultLinkProps.tension
// add the curve to our drawing context
drawLinkBezierCurve(this.context, fromNode, toNode, curveTension)
if (arrow) drawArrowHead(this.context, fromNode, toNode)
// render the curve
this.context.stroke()
}
},
/**
* Helper to fetch a single node based on its id
*
* @param {string} id - Reference to node id
* @returns {Node}
**/
findNode(id) {
return [...this.nodes].find(pt => pt.id === id)
},
/**
* Iterate through the array of links, rendering each one
**/
renderAllLinks() {
for (let link of this.links) {
this.renderLink(link)
}
},
/**
* Included for demo only, doesn't actually render anything
* iterate through the array of nodes, rendering each one
**/
renderAllNodes() {
for (let node of this.nodes) {
this.renderNode(node)
}
}
}
}
</script>
<style scoped>
canvas {
z-index: 1;
position: absolute;
}
</style>
import { generateLinkHandles } from '../utils/nodes.js'
import { mapRange, distance } from '../utils/numbers.js'
//////////////////////////////////////////////////////////////////////
// CANVAS UTILITIES
//////////////////////////////////////////////////////////////////////
// Because these helpers directly manipulate the drawing context,
// their methods don't need to return anything
/**
* Adds a bezier curve to the drawing context
*
* @param {CanvasRenderingContext2D} context - 2D rendering context
* @param {Node} from - Origin node
* @param {Node} to - Target node
* @param {number} tension - Tension of the curve to be drawn
*
* */
export const drawLinkBezierCurve = (context, from, to, tension) => {
const [fromHandle, toHandle] = generateLinkHandles(from, to)
// This is a simple way to adjust the link tension depending
// on the distance the link covers
const adjustedTension = mapRange(
distance(fromHandle, toHandle),
0,
1000,
tension * 0.01,
tension * 2
)
context.beginPath()
context.moveTo(fromHandle.x, fromHandle.y)
context.bezierCurveTo(
fromHandle.x * (1 + adjustedTension),
fromHandle.y,
toHandle.x * (1 - adjustedTension),
toHandle.y,
toHandle.x,
toHandle.y
)
}
export const drawArrowHead = (context, from, to, size = 10) => {
const [fromHandle, toHandle] = generateLinkHandles(from, to)
var angleBetween = null
const angle = angleBetween(fromHandle, toHandle)
context.setLineDash([0, 0])
context.setTransform(1, 0, 0, 1, toHandle.x - size, toHandle.y)
context.rotate(angle * 0.65)
context.moveTo(-size, -size / 1.25)
context.lineTo(0, 0)
context.lineTo(-size, size / 1.25)
context.setTransform(1, 0, 0, 1, 0, 0)
}
export const palette = {
coral: {
light: '#FFD8E1',
dark: '#F56789'
},
lime: {
light: '#EEFFBC',
dark: '#B9DF4E'
},
blue: {
light: '#CDEAFF',
dark: '#4EB4FF'
},
purple: {
light: '#DAD9FE',
dark: '#8A80F6'
},
pink: {
light: '#FBE9FF',
dark: '#E47EFD'
},
yellow: {
light: '#FFF3CA',
dark: '#FFD84F'
},
mono: {
light: '#FFFFFF',
dark: '#A3A3A3'
}
}
/**
* Selects a HEX color value depending on a supplied key reference,
* providing a default (mono) value if none is found.
*
* @param {string} color - Target palette (from above)
* @param {string} type - Target color type (light or dark)
* @returns {string} hex color value
*
* */
export const getPalette = (color = 'mono', type) =>
palette[color] ? palette[color][type] : palette.mono[type]
//////////////////////////////////////////////////////////////////////
// TYPE DEFINITIONS
//////////////////////////////////////////////////////////////////////
/**
* A representation of a point
* @typedef {Object} Point
* @property {number} x - Horizontal (x) position in pixels
* @property {number} y - Vertical (y) position in pixels
*/
/**
* A representation of an area
* @typedef {Object} Area
* @property {number} x1 - Top left (x) position in pixels
* @property {number} y1 - Top left (y) position in pixels
* @property {number} x2 - Bottom right (x) position in pixels
* @property {number} y2 - Bottom right (y) position in pixels
*/
/**
* A representation of a node
* @typedef {Object} Node
* @property {number} x - Horizontal (x) position in pixels
* @property {number} y - Vertical (y) position in pixels
* @property {number} width - Node width in pixels
* @property {number} height - Node height in pixels
*/
//////////////////////////////////////////////////////////////////////
// NODES
//////////////////////////////////////////////////////////////////////
// Helpers for working with nodes, points and areas
/**
* Converts a @Node into the @Area it covers
*
* @param {Node} node - Origin node
* @param {number} threshold - Additional threshold area (optional)
* @return {Area} Area covered by box, including threshold
*
* */
export const nodeToArea = (node, threshold = 0.0) => {
return {
x1: node.x * (1.0 - threshold),
y1: node.y * (1.0 - threshold),
x2: (node.x + node.width) * (1.0 + threshold),
y2: (node.y + node.height) * (1.0 + threshold)
}
}
/**
* Converts an @Area into a node
*
* @param {Area} area - Origin node
* @return {Node}
*
* */
export const areaToNode = area => {
return {
x: area.x1,
y: area.y1,
width: Math.abs(area.x2 - area.x1),
height: Math.abs(area.y2 - area.y1)
}
}
/**
* Returns whether one @Area completely contains another @Area
*
* @param {Area} a
* @param {Area} b
* @return {boolean}
*
* */
export const areaContains = (a, b) =>
!(b.x1 < a.x1 || b.y1 < a.y1 || b.x2 > a.x2 || b.y2 > a.y2)
/**
* Returns whether one @Area overlaps another @Area
*
* @param {Area} a
* @param {Area} b
* @return {boolean}
*
* */
export const areaOverlaps = (a, b) => {
if (a.x1 >= b.x2 || b.x1 >= a.x2) return false
if (a.y1 >= b.y2 || b.y1 >= a.y2) return false
return true
}
/**
* Returns anchor positions for drawing links between two Nodes.
*
* @param {Node} from - Origin node
* @param {Node} from - Target node
* @return {array} Array of {x,y} positions
*
* */
export const generateLinkHandles = (from, to) => {
// const toLeft = to.x > from.x
return [
{
x: from.x + from.width,
y: from.y + from.height / 2
},
{
// x: to.x + to.width / 2,
x: to.x + 10,
y: to.y + to.height / 2
}
]
}
/**
* Returns a list of nodes which overlap with a given {x1,y1,x2,y2} @Area
*
* @param {Area} area - Origin node
* @param {array} node - List of nodes to check against
* @param {number} threshold - Additional threshold area (optional)
* @return {array} Array of @Node objects which match
*
* */
export const areaNodeIntersections = (targetNode, nodes, threshold = 0) => {
return nodes
.filter(node =>
areaOverlaps(nodeToArea(targetNode), nodeToArea(node, threshold))
)
.map(({ id }) => id)
}
/**
* Returns a list of nodes which overlap with a given {x,y} @Point
*
* @param {Point} point
* @param {array} node - List of nodes to check against
* @return {array} Array of @Node objects which match
*
* */
export const pointNodeIntersections = (point, nodes) => {
return nodes.filter(node => pointWithinNode(point, node)).map(({ id }) => id)
}
/**
* Checks whether a @Point is within a @Node
*
* @param {Point} point
* @param {Node} node
* @return {boolean}
*
* */
export const pointWithinNode = (point, node) => {
const { x1, y1, x2, y2 } = nodeToArea(node)
return x1 <= point.x && point.x <= x2 && y1 <= point.y && point.y <= y2
}
/**
* Converts an array of nodes into a single large @Area
*
* @param {array} node - List of nodes
* @return {Area}
*
* */
export const generateAreaFromNodes = nodes => {
const sum = {}
for (let node of nodes) {
const area = nodeToArea(node)
if (!sum.x1 || area.x1 < sum.x1) {
sum.x1 = area.x1
}
if (!sum.y1 || area.y1 < sum.y1) {
sum.y1 = area.y1
}
if (!sum.x2 || area.x2 > sum.x2) {
sum.x2 = area.x2
}
if (!sum.y2 || area.y2 > sum.y2) {
sum.y2 = area.y2
}
}
return sum
}
/**
* Generates a node shape from two given points
* */
export const generateNode = (origin, point) => {
const minusX = origin.x > point.x
const minusY = origin.y > point.y
const width = Math.abs(point.x - origin.x)
const height = Math.abs(point.y - origin.y)
return {
x: minusX ? origin.x - width : origin.x,
y: minusY ? origin.y - height : origin.y,
width,
height
}
}
/**
* Transforms a grouped set of nodes based on a base transformation
*
* @param {Node[]} nodes - List of nodes
* @param {Point} transform – transform
* @return {Node[]}
* */
export const transformNodeSelection = (nodes, transform) => {
const baseCoordinates = {}
// iterate through all nodes to get underlying coordinates of group
for (let node of nodes) {
if (!baseCoordinates.x || node.x < baseCoordinates.x) {
baseCoordinates.x = node.x
}
if (!baseCoordinates.y || node.y < baseCoordinates.y) {
baseCoordinates.y = node.y
}
}
return nodes.map(node => {
const relativeTransform = {
x: node.x - baseCoordinates.x,
y: node.y - baseCoordinates.y
}
return Object.assign({}, node, {
x: transform.x + relativeTransform.x,
y: transform.y + relativeTransform.y
})
})
}
//////////////////////////////////////////////////////////////////////
// MATHS AND NUMBERS
//////////////////////////////////////////////////////////////////////
/**
* Returns the distance between two {x,y} points
*
* @param {Node} from - Origin node
* @param {Node} from - Target node
* @return {number} Distance between points
*
* */
export const distance = (from, to) => Math.hypot(to.x - from.x, to.y - from.y)
/**
* Returns an interpolated value between two numbers
*
* @param {number} from
* @param {number} to
* @param {number} extent - between 0.0 and 1.0
* @return {number}
*
* */
export const lerp = (from, to, extent) => from * (1 - extent) + to * extent
/**
* Returns an interpolated value between two points
*
* @param {Point} from
* @param {Point} to
* @param {number} extent - between 0.0 and 1.0
* @return {Point}
*
* */
export const lerpPoint = (from, to, extent) => {
return {
x: lerp(from.x, to.x, extent)
}
}
/**
* Map a @number from one range to another
*
* @param {number} value - value to map
* @param {number} from1
* @param {number} to1
* @param {number} from2
* @param {number} to2
* @return {number}
*
* */
export const mapRange = (value, from1, to1, from2, to2) =>
((value - from1) * (to2 - from2)) / (to1 - from1) + from2
/**
* Calculates the angle between two points
*
* @param {Point} point1
* @param {Point} point2
* @return {number}
*
* */
export const angleBetween = (point1, point2) =>
Math.atan2(point2.y - point1.y, point2.x - point1.x)
......@@ -15,7 +15,13 @@
v-bind:nodetext="value.nodetext"
/>
<CanvasLayer />
<!-- <CanvasLayer /> -->
<LinksLayer
v-bind:width="width"
v-bind:height="height"
v-bind:nodes="nodes"
v-bind:links="links"
/>
<DeBug />
<ControlsLayer />
</div>
......@@ -26,7 +32,7 @@
<script>
// @ is an alias to /src
import OnBoard from '@/components/OnBoard.vue'
import CanvasLayer from '@/components/CanvasLayer.vue'
import LinksLayer from '@/components/LinksLayer.vue'
import NodesLayer from '@/components/NodesLayer.vue'
import OtherNodeslayer from '@/components/OtherNodeslayer.vue'
import DeBug from '@/components/DeBug.vue'
......@@ -40,13 +46,15 @@ export default {
data: function() {
return {
clientset: false,
offline: false
offline: false,
width: 1200,
height: 800
}
},
components: {
OnBoard,
CanvasLayer,
LinksLayer,
NodesLayer,
OtherNodeslayer,
DeBug,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment