diff options
Diffstat (limited to 'static/src/_posts/2021-05-28-viz-5.md')
-rw-r--r-- | static/src/_posts/2021-05-28-viz-5.md | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/static/src/_posts/2021-05-28-viz-5.md b/static/src/_posts/2021-05-28-viz-5.md new file mode 100644 index 0000000..ea2f9e9 --- /dev/null +++ b/static/src/_posts/2021-05-28-viz-5.md @@ -0,0 +1,306 @@ +--- +title: >- + Visualization 5 +description: >- + Seeing double. +series: viz +tags: tech art +--- + +<script type="text/javascript"> + +function randn(n) { + return Math.floor(Math.random() * n); +} + +const w = 100; +const h = 50; + +const maxNewElsPerTick = 10; +const deathThresh = 10; + +class Canvas { + constructor(canvasDOM) { + this.dom = canvasDOM; + this.ctx = canvasDOM.getContext("2d"); + + // expand canvas element's width to match parent. + this.dom.width = this.dom.parentElement.offsetWidth; + + // rectSize must be an even number or the pixels don't display nicely. + this.rectSize = Math.floor(this.dom.width / w /2) * 2; + + this.dom.width = w * this.rectSize; + this.dom.height = h * this.rectSize; + } + + rectSize() { + return Math.floor(this.dom.width / w); + } +} + +class Layer { + constructor(newEl) { + this.els = {}; + this.diff = {}; + this.newEl = newEl; + } + + _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"}; + } + + 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]; + } + } + } + + update(state) { + // Apply diff from previous update first. The diff can't be applied last + // because it needs to be present during the draw phase. + this.applyDiff(); + + const allEls = this.getAll().sort(() => Math.random() - 0.5); + + if (allEls.length == 0) { + this.set([w/2, h/2], this.newEl([])); + } + + let newEls = 0; + for (const el of allEls) { + const nCoord = randEmptyNeighboringCoord(this, el.coord); + if (!nCoord) continue; // el has no empty neighboring spots + + const nEl = this.newEl(neighboringElsOf(this, nCoord)) + nEl.tick = state.tick; + this.set(nCoord, nEl); + + newEls++; + if (newEls >= maxNewElsPerTick) break; + } + + for (const el of allEls) { + const nEls = neighboringElsOf(this, el.coord); + if (state.tick - el.tick - (nEls.length * deathThresh) >= deathThresh) this.unset(el.coord); + } +} + + draw(canvas) { + for (const coordStr in this.diff) { + const el = this.diff[coordStr]; + const coord = JSON.parse(coordStr); + + if (el.action == "set") { + canvas.ctx.fillStyle = `hsl(${el.h}, ${el.s}, ${el.l})`; + canvas.ctx.fillRect( + coord[0]*canvas.rectSize, coord[1]*canvas.rectSize, + canvas.rectSize, canvas.rectSize, + ); + + } else { + canvas.ctx.clearRect( + coord[0]*canvas.rectSize, coord[1]*canvas.rectSize, + canvas.rectSize, canvas.rectSize, + ); + } + } + } +} + +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(layer, coord) { + const neighbors = neighborsOf(coord).sort(() => Math.random() - 0.5); + for (const nCoord of neighbors) { + if (!layer.get(nCoord)) return nCoord; + } + return null; +} + +function neighboringElsOf(layer, coord) { + const neighboringEls = []; + for (const nCoord of neighborsOf(coord)) { + const el = layer.get(nCoord); + if (el) neighboringEls.push(el); + } + return neighboringEls; +} + +const drift = 30; +function mkNewEl(l) { + return (nEls) => { + const s = "100%"; + if (nEls.length == 0) { + return { + h: randn(360), + s: s, + l: l, + }; + } + + // 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: s, + l: l, + }; + } +} + +class Universe { + constructor(canvasesByClass, layersByClass) { + this.canvasesByClass = canvasesByClass; + this.state = { + tick: 0, + layers: layersByClass, + }; + } + + update() { + this.state.tick++; + Object.values(this.state.layers).forEach((layer) => layer.update(this.state)); + } + + draw() { + for (const layerName in this.state.layers) { + if (!this.canvasesByClass[layerName]) return; + this.canvasesByClass[layerName].forEach((canvas) => { + this.state.layers[layerName].draw(canvas); + }); + } + } +} + +</script> + +<style> + +.canvasContainer { + display: grid; + margin-bottom: 2rem; + text-align: center; +} + +canvas { + border: 1px dashed #AAA; + width: 100%; + grid-area: 1/1/2/2; +} + +</style> + +<div class="canvasContainer"> + <canvas class="layer1"></canvas> + <canvas class="layer2"></canvas> +</div> + +<div class="row"> + <div class="columns six"> + <div class="canvasContainer"><canvas class="layer1"></canvas></div> + </div> + <div class="columns six"> + <div class="canvasContainer"><canvas class="layer2"></canvas></div> + </div> +</div> + +This visualization combines two distinct layers, each of them borrowing their +behavior from [Visualization 4][viz4]. Neither layer has any effect on the +other, one is merely super-imposed on top of the other in the top canvas. You +can see each layer individually in the two lower canvases. + +Despite their not affecting each other, the code is set up so that each layer +_could_ be affected by the other. This will likely be explored more in a future +post. + +[viz4]: {% post_url 2021-05-26-viz-4 %} + +<script> + +const canvasesByClass = {}; +[...document.getElementsByTagName("canvas")].forEach((canvasDOM) => { + + const canvas = new Canvas(canvasDOM); + canvasDOM.classList.forEach((name) => { + if (!canvasesByClass[name]) canvasesByClass[name] = []; + canvasesByClass[name].push(canvas); + }) +}); + + +const universe = new Universe(canvasesByClass, { + "layer1": new Layer(mkNewEl("90%")), + "layer2": new Layer(mkNewEl("50%")), +}); + +const requestAnimationFrame = + window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + window.msRequestAnimationFrame; + +function doTick() { + universe.update(); + universe.draw(); + requestAnimationFrame(doTick); +} + +doTick(); + +</script> |