Skip to content

Instantly share code, notes, and snippets.

@zaus
Last active April 8, 2026 15:17
Show Gist options
  • Select an option

  • Save zaus/18690763e1db46715a1770ac87b6b3bf to your computer and use it in GitHub Desktop.

Select an option

Save zaus/18690763e1db46715a1770ac87b6b3bf to your computer and use it in GitHub Desktop.
Save that Color-Flooded Maze
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Color-flooded Maze</title>
<link rel="canonical" href="https://codepen.io/zaus/pen/abwwVG" />
<style>
body { background: #fff; }
table, td, th { border:1px solid black; border-collapse: collapse; text-align:center; }
canvas { border:none; background-color:#fff; }
#instructions { position:absolute; right:0; top: 1em; max-width:40%; visibility:collapse; background-color:white; padding:1em; }
#instructions.open { visibility:visible; }
#instructions::before { content: '?'; border:1px solid darkblue; background-color:blue; color:white; font-family:sans-serif; font-weight:bold; border-radius:50%; padding:0.5em; visibility:visible; position:absolute; right: 0; }
#instructions.open::before {content: 'X'; background-color:red; border-color:darkred; }
</style>
</head>
<body>
<div id="instructions">
<p>Generates a color-flooded maze, then lets you save the resulting image. Use <a
href="?w=500&h=500&bg=fc0&size=10&spacing=3" title="for example...">url parameters</a> to change some of the
settings.</p>
<table>
<tr>
<th>param</th>
<th>descr</th>
<th>default</th>
</tr>
<tr>
<td><code>w</code></td>
<td>width of generated canvas, in pixels</td>
<td><code>i * (cell size + cell spacing) + cell spacing</code></td>
</tr>
<tr>
<td><code>h</code></td>
<td>height of generated canvas, in pixels</td>
<td><code>j * (cell size + cell spacing) + cell spacing</code></td>
</tr>
<tr>
<td><code>i</code></td>
<td>width of generated canvas, in "cells+spacing"</td>
<td><code>125</code></td>
</tr>
<tr>
<td><code>j</code></td>
<td>height of generated canvas, in "cells+spacing"</td>
<td><code>i</code></td>
</tr>
<tr>
<td><code>bg</code></td>
<td>maze background/wall color (given in hex, without <code>#</code>)</td>
<td><code>000</code> (black)</td>
</tr>
<tr>
<td><code>size</code></td>
<td>cell size</td>
<td><code>4</code></td>
</tr>
<tr>
<td><code>spacing</code></td>
<td>cell spacing</td>
<td><code>cell width</code></td>
</tr>
<tr>
<td><code>fit</code></td>
<td>round height/width to fit cell/padding exactly (no excess border)</td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>animate</code></td>
<td>cool looking or instantly</td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>hue</code></td>
<td><i>hsl</i> starting color offset (<code>(distance+hue) % 360</code>); &lt; 360</td>
<td><code>0</code></td>
</tr>
<tr>
<td><code>sat</code></td>
<td><i>hsl</i> saturation; &lt; 1</td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>lit</code></td>
<td><i>hsl</i> lightness; &lt; 1</td>
<td><code>0.5</code></td>
</tr>
<tr>
<td><code>huerange</code></td>
<td>How much the hue (color) can vary</td>
<td><code>360</code></td>
</tr>
</table>
<p>I take no credit other than the ability to save it, although for gigantic images newer browsers can save it via
context-menu. See <a href="https://bl.ocks.org/mbostock/061b3929ba0f3964d335">Source</a>. Originally created on <a href="https://codepen.io/zaus/pen/abwwVG">CodePen</a>.</p>
<p><a href="javascript:saveToImage('png')">Save as PNG</a> | <a href="javascript:saveToImage('jpg')">Save as JPG</a>
</p>
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
// #region ----------- utilities ----------
function qp(k) {
if(this._qp) return this._qp[k];
// cache for repeat use
this._qp = {};
var q, qs = document.location.search.substring(1)
if(!qs) return null;
qs = qs.split('&');
for(var i = qs.length; i > 0;) {
q = qs[--i].split('=');
this._qp[decodeURIComponent(q[0])] = decodeURIComponent(q[1]);
//if(decodeURIComponent(q[0]) == k) return decodeURIComponent(q[1]);
}
return qp(k);
}
function saveToImage(format) {
// http://stackoverflow.com/questions/923885/capture-html-canvas-as-gif-jpg-png-pdf
var c = document.getElementsByTagName('canvas')[0];
var w = c.width, h = c.height, bg = c.dataset.bg, size = c.dataset.cellSize, spacing = c.dataset.cellSpacing;
var img = c.toDataURL('image/' + (format||'png'));
var d = new Date();
var date = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
var link = document.createElement('a');
link.href = img;
link.download = `color-flooded-maze_${w}x${h}_s${size},${spacing}_${date}.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
var $instructions = document.getElementById('instructions');
$instructions.addEventListener('click', function(e) {
$instructions.classList.toggle('open');
})
// #endregion ----------- utilities ----------
// #region ----------- the wilson thingy ----------
var N = 1 << 0,
S = 1 << 1,
W = 1 << 2,
E = 1 << 3;
var bg = '#' + (qp('bg') || '000'),
cellSize = parseInt(qp('size')) || 4,
cellSpacing = parseInt(qp('spacing')) || cellSize,
fw = parseInt(qp('i')) || 125,
fh = parseInt(qp('j')) || fw,
width = parseInt(qp('w')) || fw * (cellSize + cellSpacing) + cellSpacing,
height = parseInt(qp('h')) || fh * (cellSize + cellSpacing) + cellSpacing,
cellWidth = Math.floor((width - cellSpacing) / (cellSize + cellSpacing)),
cellHeight = Math.floor((height - cellSpacing) / (cellSize + cellSpacing)),
cells = generateMaze(cellWidth, cellHeight), // each cell’s edge bits
distance = d3.range(cellWidth * cellHeight).map(function() { return 0; }),
frontier = [(cellHeight - 1) * cellWidth],
animate = qp('animate') == 'true',
// offset for determining color
hueMod = Math.max(parseInt(qp('hue')) || 0, 0),
saturationMod = Math.min(parseFloat(qp('sat')) || 1, 1),
lightnessMod = Math.min(parseFloat(qp('lit')) || 0.5, 1),
hueRange = Math.min(parseInt(qp('huerange')) || 360, 360)
;
// fix for actual width/height so we don't have any excess borders
if(qp('fit')) {
width = cellWidth * (cellSize + cellSpacing) + cellSpacing;
height = cellHeight * (cellSize + cellSpacing) + cellSpacing;
}
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height)
.attr("data-cell-size", cellSize)
.attr("data-cell-spacing", cellSpacing)
.attr("data-bg", bg);
var context = canvas.node().getContext("2d");
// fill the background
context.fillStyle = bg;
//context.fillRect(0,0, width/*-cellSize*/, height/*-cellSize*/); // slightly adjust fill so far edge isn't too thick? or is that only in the UI and not when saved?
context.fillRect(0,0, width, height);
// center within background so borders match if not fitting
context.translate(
Math.round((width - cellWidth * (cellSize + cellSpacing) - cellSpacing) / 2),
Math.round((height - cellHeight * (cellSize + cellSpacing) - cellSpacing) / 2)
);
context.fillStyle = '#fff'; // the corridors of the maze
for (var y = 0, i = 0; y < cellHeight; ++y) {
for (var x = 0; x < cellWidth; ++x, ++i) {
fillCell(i);
if (cells[i] & S) fillSouth(i);
if (cells[i] & E) fillEast(i);
}
}
function exploreLoop() {
for (var i = 0; i < 50; ++i) {
if (exploreFrontier()) {
return true;
}
}
}
if(animate) {
d3.timer(exploreLoop);
} else {
while(!exploreLoop());
}
function bellCurve(x, m, b) {
return (1 / (m * Math.sqrt(2 * Math.PI))) * Math.exp(- Math.pow( (x % (2*b)) - b, 2) / (2 * Math.pow(m, 2)))
}
function getColor(offset) {
var offsetHue = offset % hueRange,
offsetHue2 = offset % (2 * hueRange),
scaledHue = ((
offsetHue == 0 && offsetHue2 != 0
? hueRange
: offsetHue2 > hueRange
? hueRange - offsetHue
: offsetHue
) + hueMod) % 360;
// (bellCurve(offset, 50, 180) / bellCurve(180, 50, 180) * hueRange + hueMod) % 360;
// Math.round((offset % 360)/360 * hueRange + hueMod)%360
return d3.hsl( scaledHue, saturationMod, lightnessMod) + "";
}
function exploreFrontier() {
if ((i0 = popRandom(frontier)) == null) return true;
var i0,
i1,
d0 = distance[i0],
d1 = d0 + 1;
context.fillStyle = getColor(d0);
fillCell(i0);
context.fillStyle = getColor(d1);
if (cells[i0] & E && !distance[i1 = i0 + 1]) distance[i1] = d1, fillEast(i0), frontier.push(i1);
if (cells[i0] & W && !distance[i1 = i0 - 1]) distance[i1] = d1, fillEast(i1), frontier.push(i1);
if (cells[i0] & S && !distance[i1 = i0 + cellWidth]) distance[i1] = d1, fillSouth(i0), frontier.push(i1);
if (cells[i0] & N && !distance[i1 = i0 - cellWidth]) distance[i1] = d1, fillSouth(i1), frontier.push(i1);
}
function fillCell(i) {
var x = i % cellWidth, y = i / cellWidth | 0;
context.fillRect(x * cellSize + (x + 1) * cellSpacing, y * cellSize + (y + 1) * cellSpacing, cellSize, cellSize);
}
function fillEast(i) {
var x = i % cellWidth, y = i / cellWidth | 0;
context.fillRect((x + 1) * (cellSize + cellSpacing), y * cellSize + (y + 1) * cellSpacing, cellSpacing, cellSize);
}
function fillSouth(i) {
var x = i % cellWidth, y = i / cellWidth | 0;
context.fillRect(x * cellSize + (x + 1) * cellSpacing, (y + 1) * (cellSize + cellSpacing), cellSize, cellSpacing);
}
function generateMaze(width, height) {
var cells = new Array(width * height), // each cell’s edge bits
remaining = d3.range(width * height), // cell indexes to visit
previous = new Array(width * height); // current random walk
// Add the starting cell.
var start = remaining.pop();
cells[start] = 0;
// While there are remaining cells,
// add a loop-erased random walk to the maze.
while (!loopErasedRandomWalk());
return cells;
function loopErasedRandomWalk() {
var i0,
i1,
x0,
y0;
// Pick a location that’s not yet in the maze (if any).
do if ((i0 = remaining.pop()) == null) return true;
while (cells[i0] >= 0);
// Perform a random walk starting at this location,
previous[i0] = i0;
while (true) {
x0 = i0 % width;
y0 = i0 / width | 0;
// picking a legal random direction at each step.
i1 = Math.random() * 4 | 0;
if (i1 === 0) { if (y0 <= 0) continue; --y0, i1 = i0 - width; }
else if (i1 === 1) { if (y0 >= height - 1) continue; ++y0, i1 = i0 + width; }
else if (i1 === 2) { if (x0 <= 0) continue; --x0, i1 = i0 - 1; }
else { if (x0 >= width - 1) continue; ++x0, i1 = i0 + 1; }
// If this new cell was visited previously during this walk,
// erase the loop, rewinding the path to its earlier state.
if (previous[i1] >= 0) eraseWalk(i0, i1);
// Otherwise, just add it to the walk.
else previous[i1] = i0;
// If this cell is part of the maze, we’re done walking.
if (cells[i1] >= 0) {
// Add the random walk to the maze by backtracking to the starting cell.
// Also erase this walk’s history to not interfere with subsequent walks.
while ((i0 = previous[i1]) !== i1) {
if (i1 === i0 + 1) cells[i0] |= E, cells[i1] |= W;
else if (i1 === i0 - 1) cells[i0] |= W, cells[i1] |= E;
else if (i1 === i0 + width) cells[i0] |= S, cells[i1] |= N;
else cells[i0] |= N, cells[i1] |= S;
previous[i1] = NaN;
i1 = i0;
}
previous[i1] = NaN;
return;
}
i0 = i1;
}
}
function eraseWalk(i0, i2) {
var i1;
do i1 = previous[i0], previous[i0] = NaN, i0 = i1; while (i1 !== i2);
}
}
function popRandom(array) {
if (!array.length) return;
var n = array.length, i = Math.random() * n | 0, t;
t = array[i], array[i] = array[n - 1], array[n - 1] = t;
return array.pop();
}
d3.select(self.frameElement).style("height", height + "px");
// #endregion ----------- the wilson thingy ----------
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment