<canvas class="voronoi"></canvas>
<div class="time"></div>
html,
body {
	height: 100%;
	overflow: hidden;
	font-family: sans-serif;
}

canvas.voronoi {
	display: block;
}

.time {
	position: absolute;
	top: 8px;
	left: 12px;
	font-size: 11px;
	letter-spacing: 0.05em;
	color: #fff;
	text-shadow: 0 0 4px #000
}
console.clear();

const config = {
	pointDensity: 2
};


// DOM references
const canvasNode = document.querySelector('.voronoi');
const timeNode = document.querySelector('.time');

// Reference to canvas context
const context = canvasNode.getContext('2d');


// Timing helper
let _startTime;

function startTime() {
	_startTime = Date.now();
}

function endTime() {
	const totalTime = Date.now() - _startTime;
	timeNode.textContent = `${totalTime}ms`;
}

startTime();
endTime();


// Helper to resize canvas to viewport dimensions.
function resizeCanvas() {
	// Not high-DPI
	canvasNode.width = window.innerWidth;
	canvasNode.height = window.innerHeight;
}

// Helper to generate random colors, inspired by the colors seen on bubbles or oil slicks.
// See: https://codepen.io/MillerTime/pen/NXxxma
const colorSampler = (function GradientSamplerFactory() {
	// The instance to be returned.
	const sampler = {};

	// Gradient color stops in RGB format.
	const colors = [
		{ r: 144, g:  18, b:  96 },
		{ r:  12, g:  43, b: 179 },
		{ r:   0, g: 196, b: 247 },
		{ r: 190, g: 255, b: 255 },
		{ r: 255, g: 232, b:   0 },
		{ r: 255, g: 103, b:   0 },
		{ r: 191, g:  26, b: 156 },
		{ r:   0, g:  79, b: 229 },
		{ r:   0, g: 196, b:   9 }
	];

	const colorCount = colors.length;
	const colorSpans = colorCount - 1;
	const spanSize = 1 / colorSpans;

	// Helper to interpolate between two numbers
	function interpolate(a, b, p) {
		return (b - a) * p + a;
	};

	sampler.sample = function sample(position) {
		// Normalize position to 0..1 scale (inclusive of 0, exlusive of 1).
		position -= position | 0;
		if (position < 0) position = 1 - position * -1;

		const startIndex = position * colorSpans | 0;
		const startColor = colors[startIndex];
		const endColor = colors[startIndex + 1];
		// Compute relative position between two chosen color stops.
		const innerPosition = (position - (startIndex / colorSpans)) / spanSize;

		const r = interpolate(startColor.r, endColor.r, innerPosition) | 0;
		const g = interpolate(startColor.g, endColor.g, innerPosition) | 0;
		const b = interpolate(startColor.b, endColor.b, innerPosition) | 0;

		return { r, g, b };
	};

	return sampler;
})();

// Helper to shade a point color based on distance.
function getPointColorAtDistance(point, distance, maxDistance) {
	const { r, g, b } = point.color;
	const scale = 1 - Math.min(1, distance / maxDistance);
	return {
		r: r * scale | 0,
		g: g * scale | 0,
		b: b * scale | 0
	};
}

// Helper to generate a random 2D point instance.
function makeRandomPoint() {
	return {
		x: Math.floor(Math.random() * canvasNode.width),
		y: Math.floor(Math.random() * canvasNode.height),
		color: colorSampler.sample(Math.random())
	};
}

// Helper to find the point in a set of points which is closest to a given coordinate.
// Returns an object with two properties: `point`, and `distance`.
function findNearestPoint(x, y, points) {
	let winningPoint = null;
	let winningDistance = Infinity;
	for (let p of points) {
		// Pythagorean Theorem
		const a = p.x - x;
		const b = p.y - y;
		const distance = Math.sqrt(a * a + b * b);
		if (distance < winningDistance) {
			winningPoint = p;
			winningDistance = distance;
		}
	}
	return {
		point: winningPoint,
		distance: winningDistance
	};
}


// The brute-force algorithm.
function drawVoronoi() {
	startTime();
	// Compute some helpful numbers.
	const width = canvasNode.width;
	const height = canvasNode.height;
	const canvasArea = width * height;
	
	// Generate points for cells.
	const pointCount = Math.floor(canvasArea / 20000 * config.pointDensity);
	const points = [];
	for (let i=0; i<pointCount; i++) {
		points.push(makeRandomPoint());
	}
	
	// For each pixel, find the nearest point, and compute color from that point.
	// Distance where darkest color is used.
	const maxDistance = Math.sqrt(canvasArea) / 4;
	// Pixel buffer
	const pixelData = [];
	for (let i=0; i<canvasArea; i++) {
		const x = i % width;
		const y = i / width | 0;
		
		const nearest = findNearestPoint(x, y, points);
		const color = getPointColorAtDistance(nearest.point, nearest.distance, maxDistance);

		pixelData.push(color.r);
		pixelData.push(color.g);
		pixelData.push(color.b);
		pixelData.push(255); // Alpha always full
	}
	
	// Draw pixels to canvas.
	const imgData = new ImageData(
		new Uint8ClampedArray(pixelData),
		width,
		height
	);
	context.putImageData(imgData, 0, 0);
	
	endTime();
}


// Init
resizeCanvas();
drawVoronoi();


window.addEventListener('click', () => {
	resizeCanvas();
	drawVoronoi();
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.