<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();
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.