<div id="container">
  <canvas id="sand" width="700" height="400"></canvas>
  <canvas id="text" width="700" height="400"></canvas>
</div>
body {
  background: #222;
  margin: 0;
}
#container {
  position: relative;
}
canvas {
  display: block;
  margin: auto;
}
#sand {
  left: 0;
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
}
// inspired by https://jsfiddle.net/sdfx/0sk1svu9/

function init() {
  // configure text for canvas
  var headlineText = 'How far can you walk in 6.5 minutes?';
  textCtx.fillStyle = '#eee';

  var options = {
    font: 'bold 55px sans-serif',
    lineHeight: 1,
    paddingX: 80,
    paddingY: 80,
    sizeToFill: true,
    renderHDPI: false,
  }
  
  // add text to canvas
  CanvasTextWrapper(textCanvas, headlineText, options);
  
  // show fps stats
  if (typeof Stats === 'function') {
    document.body.appendChild((stats = new Stats()).domElement);
  }
  
  // generate sand particles
  generateSandEffect();
}

function generateSandEffect() {
  var imageData = textCtx.getImageData(0, 0, a, s),
    imageDataArray = imageData.data,
    pixelArray = [];
  
  // push fully visible pixels to pixelArray using alpha values
  for(n = 3; n < imageDataArray.length; n+=4) {
    if (imageDataArray[n] !== 0) {
      pixelArray.push(imageDataArray[n]);
    }
  }

  pixelCount = pixelArray.length;

  var t = new Float32Array(5 * pixelCount),
    i = 0,
    m = 0,
    g = 0;

  for (; s > g; g++) {
    for (var v = 0; a > v; v++) {
      if (imageDataArray[4 * m + 3] > 0) {
        t[i++] = v + (a - u) / 2;
        t[i++] = g + (s - c) / 2;
        t[i++] = 0, t[i++] = 0;
        t[i++] = imageDataArray[4 * m + 3];
      }
      m++;
    }
  }

  var mouse = {
    x: 0,
    y: 0
  },
  xPos = 0,
  yPos = 0,
  xDistance = 0,
  yDistance = 0;

  sandCanvas.onmousemove = function (e) {
    xPos = e.clientX - sandCanvas.offsetLeft;
    yPos = e.clientY - sandCanvas.offsetTop + window.pageYOffset;
    xDistance += xPos - mouse.x;
    yDistance += yPos - mouse.y;
    mouse.x = xPos;
    mouse.y = yPos;
    requestTick();
  };
  
  sandCanvas.ontouchmove = function (e) {
    e.preventDefault();
    sandCanvas.onmousemove(e.touches[0]);
  };
  sandCtx.globalCompositeOperation = "source-over";

  function requestTick() {
    if(!ticking) {
      requestAnimationFrame(step);
      ticking = true;
    }
  }

  function step() {
    ticking = false;

    var r, i, o, l;
    sandCtx.clearRect(0, 0, a, s);
    var imageData = sandCtx.createImageData(a, s), 
      c = 0, 
      d = 0, 
      h = t.length;
    for (; h > d; d += 5) {
      r = t[d];
      i = t[d + 1];
      if (!(0 > r || r > a || 0 > i || i > s)) {
        o = t[d + 2];
        l = t[d + 3];
        var m = f2(r, i, xPos, yPos);
        if (f > m) {
          o += xDistance * (f - m) * Math.random() * 0.03;
          l += yDistance * (f - m) * Math.random() * 0.03;
        }
        r += o;
        i += l;
        if (!(0 > r || r > a || 0 > i || i > s)) {
          c = 4 * ((Math.floor(i) - 1) * a + Math.floor(r));
          imageData.data[c] = p;
          imageData.data[c + 1] = p;
          imageData.data[c + 2] = p;
          imageData.data[c + 3] = t[d + 4];
          o *= 0.5;
          l *= 0.5;
          t[d] = r;
          t[d + 1] = i;
          t[d + 2] = o;
          t[d + 3] = l;
        }
      }
    }

    sandCtx.putImageData(imageData, 0, 0);
    xDistance *= 0.5;
    yDistance *= 0.5;
  }
  step();
}

function f2(e, t, n, r) {
  var i = 0,
      o = 0;
  i = n - e;
  i *= i;
  o = r - t;
  o *= o;
  return Math.sqrt(i + o);
}

window.Float32Array = window.Float32Array || Array;
var sandCanvas = document.getElementById("sand"),
  textCanvas = document.getElementById("text"),
  sandCtx = sandCanvas.getContext("2d"),
  textCtx = textCanvas.getContext("2d"),
  a = sandCanvas.clientWidth,
  s = sandCanvas.clientHeight,
  l = sandCanvas.parentNode.clientWidth,
  u = a, // source image data width
  c = s, // source image data height
  pixelCount = 0, // number of visible pixels
  f = 15,
  p = 255,
  ticking = false;

sandCanvas.width = a;
sandCanvas.height = s;
textCanvas.width = a;
textCanvas.height = s;

init();

function monitor() {
	stats.begin();
	stats.end();
	requestAnimationFrame(monitor);
}
requestAnimationFrame(monitor);
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://rawgit.com/mrdoob/stats.js/master/build/stats.min.js
  2. https://unpkg.com/canvas-text-wrapper@0.10.2/canvas-text-wrapper.min.js