mediocre-blog/_posts/2020-07-07-viz-3.md
Brian Picciano 8d1de40ef7 viz 3
2020-07-08 19:12:40 -06:00

155 lines
4.3 KiB
Markdown

---
title: >-
Visualization 3
description: >-
All the pixels.
series: viz
---
<canvas id="canvas" style="padding-bottom: 2rem;"></canvas>
This visualization is built from the ground up. On every frame a random set of
pixels is chosen. Each chosen pixel calculates the average of its color and the
color of a random neighbor. Some random color drift is added in as well. It
replaces its own color with that calculated color.
Choosing a neighbor is done using the "asteroid rule", ie a pixel at the very
top row is considered to be the neighbor of the pixel on the bottom row of the
same column.
Without the asteroid rule the pixels would all eventually converge into a single
uniform color, generally a light blue, due to the colors at the edge, the reds,
being quickly averaged away. With the asteroid rule in place the canvas has no
edges, thus no position on the canvas is favored and balance can be maintained.
<script type="text/javascript">
let rectSize = 12;
function randn(n) {
return Math.floor(Math.random() * n);
}
let canvas = document.getElementById("canvas");
canvas.width = window.innerWidth - (window.innerWidth % rectSize);
canvas.height = window.innerHeight- (window.innerHeight % rectSize);
let ctx = canvas.getContext("2d");
let w = canvas.width / rectSize;
let h = canvas.height / rectSize;
let matrices = new Array(2);
matrices[0] = new Array(w);
matrices[1] = new Array(w);
for (let x = 0; x < w; x++) {
matrices[0][x] = new Array(h);
matrices[1][x] = new Array(h);
for (let y = 0; y < h; y++) {
let el = {
h: 360 * (x / w),
s: "100%",
l: "50%",
};
matrices[0][x][y] = el;
matrices[1][x][y] = el;
}
}
// draw initial canvas, from here on out only individual rectangles will be
// filled as they get updated.
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
let el = matrices[0][x][y];
ctx.fillStyle = `hsl(${el.h}, ${el.s}, ${el.l})`;
ctx.fillRect(x * rectSize, y * rectSize, rectSize, rectSize);
}
}
let requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
let neighbors = [
[-1, -1], [0, -1], [1, -1],
[-1, 0], [1, 0],
[-1, 1], [0, 1], [1, 1],
];
function randNeighborAsteroid(matrix, x, y) {
let neighborCoord = neighbors[randn(neighbors.length)];
let neighborX = x+neighborCoord[0];
let neighborY = y+neighborCoord[1];
neighborX = (neighborX + w) % w;
neighborY = (neighborY + h) % h;
return matrix[neighborX][neighborY];
}
function randNeighbor(matrix, x, y) {
while (true) {
let neighborCoord = neighbors[randn(neighbors.length)];
let neighborX = x+neighborCoord[0];
let neighborY = y+neighborCoord[1];
if (neighborX < 0 || neighborX >= w || neighborY < 0 || neighborY >= h) {
continue;
}
return matrix[neighborX][neighborY];
}
}
let drift = 10;
function genChildH(elA, elB) {
// set the two h values, h1 <= h2
let h1 = elA.h;
let h2 = elB.h;
if (h1 > h2) {
h1 = elB.h;
h2 = elA.h;
}
// diff must be between 0 (inclusive) and 360 (exclusive). If it's greater
// than 180 then it's not the shortest path around, that must be the other
// way around the circle.
let hChild;
let diff = h2 - h1;
if (diff > 180) {
diff = 360 - diff;
hChild = h2 + (diff / 2);
} else {
hChild = h1 + (diff / 2);
}
hChild += (Math.random() * drift * 2) - drift;
hChild = (hChild + 360) % 360;
return hChild;
}
let tick = 0;
function doTick() {
tick++;
let currI = tick % 2;
let curr = matrices[currI];
let lastI = (tick - 1) % 2;
let last = matrices[lastI];
for (let i = 0; i < (w * h / 2); i++) {
let x = randn(w);
let y = randn(h);
if (curr[x][y].lastTick == tick) continue;
let neighbor = randNeighborAsteroid(last, x, y);
curr[x][y].h = genChildH(curr[x][y], neighbor);
curr[x][y].lastTick = tick;
ctx.fillStyle = `hsl(${curr[x][y].h}, ${curr[x][y].s}, ${curr[x][y].l})`;
ctx.fillRect(x * rectSize, y * rectSize, rectSize, rectSize);
}
matrices[currI] = curr;
requestAnimationFrame(doTick);
}
requestAnimationFrame(doTick);
</script>