A fast and simple blog backend.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
mediocre-blog/static/src/_posts/2021-05-26-viz-4.md

213 lines
5.1 KiB

---
title: >-
Visualization 4
description: >-
Birth, death, and colors.
series: viz
tags: tech art
---
<canvas id="canvas" style="padding-bottom: 2rem;" width="100%" height="100%"></canvas>
This visualization is a conglomeration of ideas from all the previous ones. On
each tick up to 20 new pixels are generated. The color of each new pixel is
based on the average color of its neighbors, plus some random drift.
Each pixel dies after a certain number of ticks, `N`. A pixel's life can be
extended by up to `8N` ticks, one for each neighbor it has which is still alive.
This mechanism accounts for the strange behavior which is seen when the
visualization first loads, but also allows for more coherent clusters of pixels
to hold together as time goes on.
The asteroid rule is also in effect in this visualization, so the top row and
bottom row pixels are neighbors of each other, and similarly for the rightmost
and leftmost column pixels.
<script type="text/javascript">
function randn(n) {
return Math.floor(Math.random() * n);
}
const canvas = document.getElementById("canvas");
const parentWidth = canvas.parentElement.offsetWidth;
const rectSize = Math.floor(parentWidth /100 /2) *2; // must be even number
console.log("rectSize", rectSize);
canvas.width = parentWidth - rectSize - (parentWidth % rectSize);
canvas.height = canvas.width * 0.75;
canvas.height -= canvas.height % rectSize;
const ctx = canvas.getContext("2d");
const w = (canvas.width / rectSize) - 1;
const h = (canvas.height / rectSize) - 1;
class Elements {
constructor() {
this.els = {};
this.diff = {};
}
_normCoord(coord) {
if (typeof coord !== 'string') coord = JSON.stringify(coord);
return coord;
}
get(coord) {
return this.els[this._normCoord(coord)];
}
getAll() {
return Object.values(this.els);
}
set(coord, el) {
this.diff[this._normCoord(coord)] = {action: "set", coord: coord, ...el};
}
unset(coord) {
this.diff[this._normCoord(coord)] = {action: "unset"};
}
drawDiff(ctx) {
for (const coordStr in this.diff) {
const el = this.diff[coordStr];
const coord = JSON.parse(coordStr);
if (el.action == "set") {
ctx.fillStyle = `hsl(${el.h}, ${el.s}, ${el.l})`;
} else {
ctx.fillStyle = `#FFF`;
}
ctx.fillRect(coord[0]*rectSize, coord[1]*rectSize, rectSize, rectSize);
}
}
applyDiff() {
for (const coordStr in this.diff) {
const el = this.diff[coordStr];
delete this.diff[coordStr];
if (el.action == "set") {
delete el.action;
this.els[coordStr] = el;
} else {
delete this.els[coordStr];
}
}
}
}
const neighbors = [
[-1, -1], [0, -1], [1, -1],
[-1, 0], /* [0, 0], */ [1, 0],
[-1, 1], [0, 1], [1, 1],
];
function neighborsOf(coord) {
return neighbors.map((n) => {
let nX = coord[0]+n[0];
let nY = coord[1]+n[1];
nX = (nX + w) % w;
nY = (nY + h) % h;
return [nX, nY];
});
}
function randEmptyNeighboringCoord(els, coord) {
const neighbors = neighborsOf(coord).sort(() => Math.random() - 0.5);
for (const nCoord of neighbors) {
if (!els.get(nCoord)) return nCoord;
}
return null;
}
function neighboringElsOf(els, coord) {
const neighboringEls = [];
for (const nCoord of neighborsOf(coord)) {
const el = els.get(nCoord);
if (el) neighboringEls.push(el);
}
return neighboringEls;
}
const drift = 30;
function newEl(nEls) {
// for each h (which can be considered as degrees around a circle) break the h
// down into x and y vectors, and add those up separately. Then find the angle
// between those two resulting vectors, and that's the "average" h value.
let x = 0;
let y = 0;
nEls.forEach((el) => {
const hRad = el.h * Math.PI / 180;
x += Math.cos(hRad);
y += Math.sin(hRad);
});
let h = Math.atan2(y, x);
h = h / Math.PI * 180;
// apply some random drift, normalize
h += (Math.random() * drift * 2) - drift;
h = (h + 360) % 360;
return {
h: h,
s: "100%",
l: "50%",
};
}
const requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
const els = new Elements();
const maxNewElsPerTick = 20;
const deathThresh = 20;
let tick = 0;
function doTick() {
tick++;
const allEls = els.getAll().sort(() => Math.random() - 0.5);
if (allEls.length == 0) {
els.set([w/2, h/2], {
h: randn(360),
s: "100%",
l: "50%",
});
}
let newEls = 0;
for (const el of allEls) {
const nCoord = randEmptyNeighboringCoord(els, el.coord);
if (!nCoord) continue; // el has no empty neighboring spots
const nEl = newEl(neighboringElsOf(els, nCoord))
nEl.tick = tick;
els.set(nCoord, nEl);
newEls++;
if (newEls >= maxNewElsPerTick) break;
}
for (const el of allEls) {
const nEls = neighboringElsOf(els, el.coord);
if (tick - el.tick - (nEls.length * deathThresh) >= deathThresh) els.unset(el.coord);
}
els.drawDiff(ctx);
els.applyDiff();
requestAnimationFrame(doTick);
}
requestAnimationFrame(doTick);
</script>