block by johnburnmurdoch 7af8e35731b35851e72beaf770cd58ae

Watercolour affect using HTML5 canvas

Full Screen

A rough first attempt at recreating Tyler Hobbs’ wonderful generative watercolour effect, this time in Javascript rather than Processing as in Tyler’s original. Uses Rob Britton’s randgen.js to generate the Gaussian random numbers required in the polygon deformation step.

Instructions

Double-click anywhere in the canvas to “paint” a blotch of colour (currently set to use hues from blue-green to pink-purple, see line 108 of index.html).

index.html

<!doctype html>
<html lang="">
    <head>
        <meta charset="utf-8">
        <title>Canvas watercolour</title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <script src="https://unpkg.com/d3/build/d3.min.js"></script>
        <script src="https://unpkg.com/d3-selection-multi"></script>
        <script src="randgen.js"></script>

        <style>
          canvas{border: 1px solid #aaaaaa;}
        </style>
    </head>
    <body>
       <canvas></canvas>
       <script type='text/javascript'>

       		let width = 600,
       			height = 600,
       			PR = window.devicePixelRatio || 1,
						scaledWidth = width*PR,
						scaledHeight = height*PR;

					const canvas = d3.select('canvas')
					.attrs({
						width: scaledWidth,
						height: scaledHeight,
					})
					.styles({
						width: `${width}px`,
						height: `${height}px`
					});

					const context = d3.select('canvas').node().getContext("2d");
					context.scale(PR, PR);
					context.clearRect(0,0, scaledWidth, scaledHeight);

     			context.lineWidth = 1;

          context.globalCompositeOperation = 'multiply;'

          function makeShape(x, y, radius){
            return [
              [x, y-radius],
              [x+(radius*.65), y-(radius*.65)],
              [x+radius, y],
              [x+(radius*.65), y+(radius*.65)],
              [x, y+radius],
              [x-(radius*.65), y+(radius*.65)],
              [x-radius, y],
              [x-(radius*.65), y-(radius*.65)],
            ]
          }

          function deform(points, factor){
            let newPoints = [],
              midX, midY, rangeX, rangeY;
            for(let p=0; p<points.length; p++){
              newPoints.push(points[p]);
              if(p == points.length-1){
                midX = d3.mean([points[p][0], points[0][0]]);
                midY = d3.mean([points[p][1], points[0][1]]);
                rangeX = Math.abs(points[p][0]-points[0][0]);
                rangeY = Math.abs(points[p][1]-points[0][1]);
              }else{
                midX = d3.mean([points[p][0], points[p+1][0]]);
                midY = d3.mean([points[p][1], points[p+1][1]]);
                rangeX = Math.abs(points[p][0]-points[p+1][0]);
                rangeY = Math.abs(points[p][1]-points[p+1][1]);
              }
              let newX = rnorm(midX, rangeX*factor),
                newY = rnorm(midY, rangeY*factor);
              newPoints.push([newX, newY]);
            }
            newPoints.push(points[0]);
            return newPoints;
          }

          function draw(points){
            context.beginPath();
            context.moveTo(points[0][0], points[0][1]);
            for(let v=1; v<points.length; v++){
              context.lineTo(points[v][0], points[v][1]);
            }
            context.closePath();
            context.fill();
          }

          function paint(shape, colour, deforms, layers){
            context.fillStyle = colour;
            for(let l=0; l<layers; l++){
              let layer = shape;
              for(let it=0; it<=deforms; it++){
                layer = deform(layer, 0.8);
                if(it == deforms){
                  draw(layer);
                }
              }
            }
          }

          d3.select(window).on('dblclick', () => {
            let trans = d3.mouse(canvas.node());
            let shape = makeShape(trans[0], trans[1], 50),
              maxIt = 1;
            for(let it=0; it<=maxIt; it++){
              shape = deform(shape, 0.5);
              if(it == maxIt){
                paint(shape, `hsla(${Math.random()*150+150},70%,50%,0.005)`, 8, 50);
              }
            }
          });          

       </script>
    </body>
</html>

randgen.js

/*jslint indent: 2, plusplus: true, sloppy: true */
// Generate uniformly distributed random numbers
// Gives a random number on the interval [min, max).
// If discrete is true, the number will be an integer.
function runif(min, max, discrete) {
  if (min === undefined) {
    min = 0;
  }
  if (max === undefined) {
    max = 1;
  }
  if (discrete === undefined) {
    discrete = false;
  }
  if (discrete) {
    return Math.floor(runif(min, max, false));
  }
  return Math.random() * (max - min) + min;
}

// Generate normally-distributed random nubmers
// Algorithm adapted from:
// http://c-faq.com/lib/gaussian.html
function rnorm(mean, stdev) {
  var u1, u2, v1, v2, s;
  if (mean === undefined) {
    mean = 0.0;
  }
  if (stdev === undefined) {
    stdev = 1.0;
  }
  if (rnorm.v2 === null) {
    do {
      u1 = Math.random();
      u2 = Math.random();

      v1 = 2 * u1 - 1;
      v2 = 2 * u2 - 1;
      s = v1 * v1 + v2 * v2;
    } while (s === 0 || s >= 1);

    rnorm.v2 = v2 * Math.sqrt(-2 * Math.log(s) / s);
    return stdev * v1 * Math.sqrt(-2 * Math.log(s) / s) + mean;
  }

  v2 = rnorm.v2;
  rnorm.v2 = null;
  return stdev * v2 + mean;
}

rnorm.v2 = null;

// Generate Chi-square distributed random numbers
function rchisq(degreesOfFreedom) {
  if (degreesOfFreedom === undefined) {
    degreesOfFreedom = 1;
  }
  var i, z, sum = 0.0;
  for (i = 0; i < degreesOfFreedom; i++) {
    z = rnorm();
    sum += z * z;
  }

  return sum;
}

// Generate Poisson distributed random numbers
function rpoisson(lambda) {
  if (lambda === undefined) {
    lambda = 1;
  }
  var l = Math.exp(-lambda),
    k = 0,
    p = 1.0;
  do {
    k++;
    p *= Math.random();
  } while (p > l);

  return k - 1;
}

// Generate Cauchy distributed random numbers
function rcauchy(loc, scale) {
  if (loc === undefined) {
    loc = 0.0;
  }
  if (scale === undefined) {
    scale = 1.0;
  }
  var n2, n1 = rnorm();
  do {
    n2 = rnorm();
  } while (n2 === 0.0);

  return loc + scale * n1 / n2;
}

// Bernoulli distribution: gives 1 with probability p
function rbernoulli(p) {
  return Math.random() < p ? 1 : 0;
}

// Vectorize a random generator
function vectorize(generator) {
  return function () {
    var n, result, i, args;
    args = [].slice.call(arguments)
    n = args.shift();
    result = [];
    for (i = 0; i < n; i++) {
      result.push(generator.apply(this, args));
    }
    return result;
  };
}

// Generate a histogram from a list of numbers
function histogram(data, binCount) {
  binCount = binCount || 10;

  var bins, i, scaled,
    max = Math.max.apply(this, data),
    min = Math.min.apply(this, data);

  // edge case: max == min
  if (max === min) {
    return [data.length];
  }

  bins = [];

  // zero each bin
  for (i = 0; i < binCount; i++) {
    bins.push(0);
  }

  for (i = 0; i < data.length; i++) {
    // scale it to be between 0 and 1
    scaled = (data[i] - min) / (max - min);

    // scale it up to the histogram size
    scaled *= binCount;

    // drop it in a bin
    scaled = Math.floor(scaled);

    // edge case: the max
    if (scaled === binCount) { scaled--; }

    bins[scaled]++;
  }

  return bins;
}

/**
 * Get a random element from a list
 */
function rlist(list) {
  return list[runif(0, list.length, true)];
}