r15ch13
1/19/2017 - 9:05 PM

cursor trails on a blurred image

cursor trails on a blurred image

Usage

<div class="image"></div>

<script src="jquery.js"></script>
<script src="cursor-trails.js"></script>
<script>
$('.image').blurredImage({
  image: 'image-normal.jpg',
  blur: 'image-with-blur.jpg',
  lifetime: 1000, // time the trail remains on screen
  width: 1000, // image width
  height: 1000, // image height
  minimumLineWidth: 25, // trail minimum width
  maximumLineWidth: 100, // trail maximum width
  maximumSpeed: 50, // at which speed should the trail get thinner (lower = more thinner trails)
  shadowBlur: 30 // blur of the trails edges
});
</script>
/* globals jQuery */
/**
 * Based on https://codepen.io/chrisdoble/pen/WQLLVp
 */
(function ($) {
  window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame

  $.fn.blurredImage = function (options) {
    if (!this.length > 0) {
      return false
    }

    // This is the easiest way to have default options.
    let settings = $.extend({
      image: '',
      blur: '',
      lifetime: 1000,
      width: 1280,
      height: 720,
      minimumLineWidth: 25,
      maximumLineWidth: 100,
      maximumSpeed: 50,
      shadowBlur: 30
    }, options)

    let points = []
    let image = $('<img src="' + settings.image + '">').hide()

    let imageCanvas = $('<canvas>')
    let imageCanvasContext = imageCanvas[0].getContext('2d')

    let lineCanvas = $('<canvas>')
    let lineCanvasContext = lineCanvas[0].getContext('2d')
    this.append(image)
    this.append(imageCanvas)
    this.css('background', 'url(\'' + settings.blur + '\') 0 0 / cover')

    if (image[0].complete) {
      start()
    } else {
      image[0].onload = start
    }

    /**
     * Attaches event listeners and starts the effect.
     */
    function start () {
      image.width(settings.width)
      image.height(settings.height)
      $(this).parent('div').width(settings.width)
      $(this).parent('div').height(settings.height)

      $(this).parent('div')[0].addEventListener('mousemove', function (event) {
        points.push({
          time: Date.now(),
          x: event.offsetX,
          y: event.offsetY
        })
      })

      window.addEventListener('resize', function () {
        imageCanvas[0].width = $(this).parent('div').width()
        imageCanvas[0].height = $(this).parent('div').height()
        lineCanvas[0].width = $(this).parent('div').width()
        lineCanvas[0].height = $(this).parent('div').height()
      }.bind(this))
      window.dispatchEvent(new window.Event('resize'))
      tick()
    }

    /**
     * The main loop, called at ~60hz.
     */
    function tick () {
      // Remove old points
      points = points.filter(function (point) {
        return (Date.now() - point.time) < settings.lifetime
      })

      drawLineCanvas()
      drawImageCanvas()
      window.requestAnimationFrame(tick)
    }

    /**
     * Draws a line using the recorded cursor positions.
     *
     * This line is used to mask the original image.
     */
    function drawLineCanvas () {
      let minimumLineWidth = settings.minimumLineWidth
      let maximumLineWidth = settings.maximumLineWidth
      let lineWidthRange = maximumLineWidth - minimumLineWidth
      let maximumSpeed = settings.maximumSpeed

      lineCanvasContext.clearRect(0, 0, lineCanvas[0].width, lineCanvas[0].height)
      lineCanvasContext.lineCap = 'round'
      lineCanvasContext.shadowBlur = 30
      lineCanvasContext.shadowColor = '#000'

      for (let i = 1; i < points.length; i++) {
        let point = points[i]
        let previousPoint = points[i - 1]

        // Change line width based on speed
        let distance = getDistanceBetween(point, previousPoint)
        let speed = Math.max(0, Math.min(maximumSpeed, distance))
        let percentageLineWidth = (maximumSpeed - speed) / maximumSpeed
        lineCanvasContext.lineWidth = minimumLineWidth + percentageLineWidth * lineWidthRange

        // Fade points as they age
        let age = Date.now() - point.time
        let opacity = (settings.lifetime - age) / settings.lifetime
        lineCanvasContext.strokeStyle = 'rgba(0, 0, 0, ' + opacity + ')'

        lineCanvasContext.beginPath()
        lineCanvasContext.moveTo(previousPoint.x, previousPoint.y)
        lineCanvasContext.lineTo(point.x, point.y)
        lineCanvasContext.stroke()
      }
    }

    /**
     * @param {{x: number, y: number}} a
     * @param {{x: number, y: number}} b
     * @return {number} The distance between points a and b
     */
    function getDistanceBetween (a, b) {
      return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2))
    }

    /**
     * Draws the original image, masked by the line drawn in drawLineToCanvas.
     */
    function drawImageCanvas () {
      // Emulate background-size: cover
      let width = imageCanvas[0].width
      let height = imageCanvas[0].width / image[0].naturalWidth * image[0].naturalHeight

      if (height < imageCanvas[0].height) {
        width = imageCanvas[0].height / image[0].naturalHeight * image[0].naturalWidth
        height = imageCanvas[0].height
      }

      imageCanvasContext.clearRect(0, 0, imageCanvas[0].width, imageCanvas[0].height)
      imageCanvasContext.globalCompositeOperation = 'source-over'
      imageCanvasContext.drawImage(image[0], 0, 0, width, height)
      imageCanvasContext.globalCompositeOperation = 'destination-in'
      imageCanvasContext.drawImage(lineCanvas[0], 0, 0)
    }
  }
}(jQuery))