Skip to content

Instantly share code, notes, and snippets.

@miracle2k
Forked from johnburnmurdoch/.block
Created December 21, 2020 00:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save miracle2k/6bbbb30535360a1f410665335de33fe5 to your computer and use it in GitHub Desktop.
Save miracle2k/6bbbb30535360a1f410665335de33fe5 to your computer and use it in GitHub Desktop.
Watercolour affect using HTML5 canvas
license: mit
height: 620
scrolling: no
border: no

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).

<!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@5"></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>
/*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)];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment