From f870028fad3f03e4dfd3c54656b16cc49af00ee7 Mon Sep 17 00:00:00 2001
From: Adam Procter <adamprocter@researchnot.es>
Date: Tue, 25 Feb 2020 00:38:03 +0000
Subject: [PATCH] start of links

---
 canvas-10-feb/src/components/LinksLayer.vue   | 157 ++++++++++++
 canvas-10-feb/src/components/mixins/canvas.js |  58 +++++
 canvas-10-feb/src/components/mixins/color.js  |  42 ++++
 canvas-10-feb/src/components/utils/nodes.js   | 235 ++++++++++++++++++
 canvas-10-feb/src/components/utils/numbers.js |  64 +++++
 canvas-10-feb/src/views/Home.vue              |  16 +-
 6 files changed, 568 insertions(+), 4 deletions(-)
 create mode 100644 canvas-10-feb/src/components/LinksLayer.vue
 create mode 100644 canvas-10-feb/src/components/mixins/canvas.js
 create mode 100644 canvas-10-feb/src/components/mixins/color.js
 create mode 100644 canvas-10-feb/src/components/utils/nodes.js
 create mode 100644 canvas-10-feb/src/components/utils/numbers.js

diff --git a/canvas-10-feb/src/components/LinksLayer.vue b/canvas-10-feb/src/components/LinksLayer.vue
new file mode 100644
index 0000000..e2fa2ca
--- /dev/null
+++ b/canvas-10-feb/src/components/LinksLayer.vue
@@ -0,0 +1,157 @@
+<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>
diff --git a/canvas-10-feb/src/components/mixins/canvas.js b/canvas-10-feb/src/components/mixins/canvas.js
new file mode 100644
index 0000000..6f5798e
--- /dev/null
+++ b/canvas-10-feb/src/components/mixins/canvas.js
@@ -0,0 +1,58 @@
+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)
+}
diff --git a/canvas-10-feb/src/components/mixins/color.js b/canvas-10-feb/src/components/mixins/color.js
new file mode 100644
index 0000000..44438cb
--- /dev/null
+++ b/canvas-10-feb/src/components/mixins/color.js
@@ -0,0 +1,42 @@
+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]
diff --git a/canvas-10-feb/src/components/utils/nodes.js b/canvas-10-feb/src/components/utils/nodes.js
new file mode 100644
index 0000000..11ff05b
--- /dev/null
+++ b/canvas-10-feb/src/components/utils/nodes.js
@@ -0,0 +1,235 @@
+//////////////////////////////////////////////////////////////////////
+// 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
+    })
+  })
+}
diff --git a/canvas-10-feb/src/components/utils/numbers.js b/canvas-10-feb/src/components/utils/numbers.js
new file mode 100644
index 0000000..edd9906
--- /dev/null
+++ b/canvas-10-feb/src/components/utils/numbers.js
@@ -0,0 +1,64 @@
+//////////////////////////////////////////////////////////////////////
+// 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)
diff --git a/canvas-10-feb/src/views/Home.vue b/canvas-10-feb/src/views/Home.vue
index da2ed08..351ecef 100644
--- a/canvas-10-feb/src/views/Home.vue
+++ b/canvas-10-feb/src/views/Home.vue
@@ -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,
-- 
GitLab