Daniel Tea


I came across an interesting interaction that reminded me of the older web the other day. As a user moved their mouse cursor around, there would be a trail of something that followed it. I was intrigued and wanted to see how/what it worked, so I opened up the dev console for the website (Synchronized Digital Studio) and after poking around a bit, I realized that it wasn't just a plain old <div> that had some special handling. It was <canvas>, which I had never worked with before.

Using a combination of <canvas> and requestAnimationFrame(), I was able to recreate the interaction.

The idea is to attach an event listener for mouse movement to add a new Point to an array that holds the position of the user's movement.

const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')

class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
    this.timestamp = 0
  }
}

const points = []

const addPoint = (x, y) => {
  const point = new Point(x, y)
  points.push(point)
}

document.addEventListener('mousemove', ({ clientX, clientY }) => {
  addPoint(clientX - canvas.offsetLeft, clientY - canvas.offsetTop)
})

Next, using requestAnimationFrame, on each frame, clear the canvas, loop over each point and either

  1. Remove the point if enough time has passed (maxAgeMS)
  2. Draw the point and connect it to the previous point
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)

const maxAgeMS = 160

for (let i = 0; i < points.length; i++) {
  const currentPoint = points[i]
  let previousPoint

  if (points[i - 1] !== undefined) {
    previousPoint = points[i - 1]
  } else {
    previousPoint = currentPoint
  }

  if (!currentPoint.timestamp) {
    currentPoint.timestamp = timestamp
  }

  const elapsed = timestamp - currentPoint.timestamp

  if (elapsed > maxAgeMS) {
    points.shift()
  } else {
    ctx.lineJoin = 'round'
    ctx.lineWidth = 4

    ctx.strokeStyle = `rgb(152,236,60)`

    ctx.beginPath()

    ctx.moveTo(previousPoint.x, previousPoint.y)
    ctx.lineTo(currentPoint.x, currentPoint.y)

    ctx.stroke()
    ctx.closePath()
  }
}

Rinse and repeat for each frame, and this is what I ended up with: