Last active
April 8, 2026 15:17
-
-
Save zaus/18690763e1db46715a1770ac87b6b3bf to your computer and use it in GitHub Desktop.
Save that Color-Flooded Maze
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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>); < 360</td> | |
| <td><code>0</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>sat</code></td> | |
| <td><i>hsl</i> saturation; < 1</td> | |
| <td><code>1</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>lit</code></td> | |
| <td><i>hsl</i> lightness; < 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