diff options
Diffstat (limited to 'static/src/_posts/2021-07-01-viz-7.md')
-rw-r--r-- | static/src/_posts/2021-07-01-viz-7.md | 440 |
1 files changed, 0 insertions, 440 deletions
diff --git a/static/src/_posts/2021-07-01-viz-7.md b/static/src/_posts/2021-07-01-viz-7.md deleted file mode 100644 index 5bf3e8d..0000000 --- a/static/src/_posts/2021-07-01-viz-7.md +++ /dev/null @@ -1,440 +0,0 @@ ---- -title: >- - Visualization 7 -description: >- - Feedback Loop. -series: viz -tags: tech art ---- - -<script type="text/javascript"> - -function randn(n) { - return Math.floor(Math.random() * n); -} - -const w = 100; -const h = 60; - -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 UniverseState { - constructor(layers) { - this.tick = 0; - this.layers = layers; - } - - neighboringLayers(layerIndex) { - const prevIndex = layerIndex-1; - const prev = prevIndex < 0 ? null : this.layers[prevIndex]; - - const nextIndex = layerIndex+1; - const next = nextIndex >= this.layers.length ? null : this.layers[nextIndex]; - - return [prev, next]; - } -} - -const defaultKnobs = { - maxNewElsPerTick: 10, - ageOfDeath: 30, - drift: 30, - neighborScalar: 0, - prevLayerScalar: 0, - prevLayerLikenessScalar: 0, - nextLayerScalar: 0, - nextLayerLikenessScalar: 0, - chaos: 0, -}; - -class Layer { - constructor(className, newEl, knobs = {}) { - this.className = className; - this.els = {}; - this.diff = {}; - this.newEl = newEl; - this.knobs = { ...defaultKnobs, ...knobs }; - } - - _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, thisLayerIndex) { - // 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) { - const newEl = this.newEl(this, []) - newEl.tick = state.tick; - this.set([w/2, h/2], newEl); - return; - } - - let newEls = 0; - for (const el of allEls) { - const nCoord = randEmptyNeighboringCoord(this, el.coord); - if (!nCoord) continue; // el has no empty neighboring spots - - const newEl = this.newEl(this, neighboringElsOf(this, nCoord)) - newEl.tick = state.tick; - this.set(nCoord, newEl); - - newEls++; - if (newEls >= this.knobs.maxNewElsPerTick) break; - } - - const calcLayerBonus = (el, layer, scalar, likenessScalar) => { - if (!layer) return 0; - const nEls = neighboringElsOf(layer, el.coord, true) - - const likeness = nEls.reduce((likeness, nEl) => { - const diff = Math.abs(nEl.c - el.c); - return likeness + Math.max(diff, Math.abs(1 - diff)); - }, 0); - - return (nEls.length * scalar) + (likeness * likenessScalar); - }; - - const [prevLayer, nextLayer] = state.neighboringLayers(thisLayerIndex); - - for (const el of allEls) { - const age = state.tick - el.tick; - const neighborBonus = neighboringElsOf(this, el.coord).length * this.knobs.neighborScalar; - const prevLayerBonus = calcLayerBonus(el, prevLayer, this.knobs.prevLayerScalar, this.knobs.prevLayerLikenessScalar); - const nextLayerBonus = calcLayerBonus(el, nextLayer, this.knobs.nextLayerScalar, this.knobs.nextLayerLikenessScalar); - const chaos = (this.chaos > 0) ? randn(this.knobs.chaos) : 0; - - if (age - neighborBonus - prevLayerBonus - nextLayerBonus + chaos >= this.knobs.ageOfDeath) { - 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, includeCoord = false) { - const neighboringEls = []; - - const neighboringCoords = neighborsOf(coord); - if (includeCoord) neighboringCoords.push(coord); - - for (const nCoord of neighboringCoords) { - const el = layer.get(nCoord); - if (el) neighboringEls.push(el); - } - return neighboringEls; -} - -function newEl(h, l) { - return { - h: h, - s: "100%", - l: l, - c: h / 360, // c is used to compare the element to others - }; -} - -function mkNewEl(l) { - return (layer, nEls) => { - const s = "100%"; - if (nEls.length == 0) { - const h = randn(360); - return newEl(h, 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() * layer.knobs.drift * 2) - layer.knobs.drift; - h = (h + 360) % 360; - - return newEl(h, l); - } -} - -class Universe { - constructor(canvasesByClass, layers) { - this.canvasesByClass = canvasesByClass; - this.state = new UniverseState(layers); - } - - update() { - this.state.tick++; - let prevLayer; - this.state.layers.forEach((layer, i) => { - layer.update(this.state, i); - prevLayer = layer; - }); - } - - draw() { - this.state.layers.forEach((layer) => { - if (!this.canvasesByClass[layer.className]) return; - this.canvasesByClass[layer.className].forEach((canvas) => { - layer.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"> - <h3>Bottom Layer</h3> - <div class="canvasContainer"><canvas class="layer1"></canvas></div> - <div class="layer1 layerParams"> - <label>Max New Elements Per Tick</label><input type="text" param="maxNewElsPerTick" /> - <label>Color Drift</label><input type="text" param="drift" /> - <label>Age of Death</label><input type="text" param="ageOfDeath" /> - <label>Neighbor Scalar</label><input type="text" param="neighborScalar" /> - <label>Top Layer Neighbor Scalar</label><input type="text" param="nextLayerScalar" /> - <label>Top Layer Neighbor Likeness Scalar</label><input type="text" param="nextLayerLikenessScalar" /> - </div> - </div> - - <div class="columns six"> - <h3>Top Layer</h3> - <div class="canvasContainer"><canvas class="layer2"></canvas></div> - <div class="layer2 layerParams"> - <label>Max New Elements Per Tick</label><input type="text" param="maxNewElsPerTick" /> - <label>Color Drift</label><input type="text" param="drift" /> - <label>Age of Death</label><input type="text" param="ageOfDeath" /> - <label>Neighbor Scalar</label><input type="text" param="neighborScalar" /> - <label>Bottom Layer Neighbor Scalar</label><input type="text" param="prevLayerScalar" /> - <label>Bottom Layer Neighbor Likeness Scalar</label><input type="text" param="prevLayerLikenessScalar" /> - </div> - </div> - -</div> - -Once again, this visualization iterates upon the previous. In the last one the -top layer was able to "see" the bottom, and was therefore able to bolster or -penalize its own elements which were on or near bottom layer elements, but not -vice-versa. This time both layers can see each other, and the "Layer Neighbor -Scalar" can be used to adjust lifetime of elements which are on/near elements of -the neighboring layer. - -By default, the bottom layer has a high affinity to the top, and the top layer -has a some (but not as much) affinity in return. - -Another addition is the "likeness" scalar. Likeness is defined as the degree to -which one element is like another. In this visualization likeness is determined -by color. The "Layer Neighbor Likeness Scalar" adjusts the lifetime of elements -based on how like they are to nearby elements on the neighboring layer. - -By default, the top layer has a high affinity for the bottom's color, but the -bottom doesn't care about the top's color at all (and so its color will drift -aimlessly). - -And finally "Color Drift" can be used to adjust the degree to which the color of -new elements can diverge from its parents. This has always been hardcoded, but -can now be adjusted separately across the different layers. - -In the default configuration the top layer will (eventually) converge to roughly -match the bottom both in shape and color. When I first implemented the likeness -scaling I thought it was broken, because the top would never converge to the -bottom's color. - -What I eventually realized was that the top must have a higher color drift than -the bottom in order for it to do so, otherwise the top would always be playing -catchup. However, if the drift difference is _too_ high then the top layer -becomes chaos and also doesn't really follow the color of the bottom. A -difference of 10 (degrees out of 360) is seemingly enough. - -<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 layers = [ - - new Layer("layer1", mkNewEl("90%"), { - maxNewElsPerTick: 2, - ageOfDeath: 30, - drift: 40, - neighborScalar: 50, - nextLayerScalar: 20, - }), - - new Layer("layer2", mkNewEl("50%", ), { - maxNewElsPerTick: 15, - ageOfDeath: 1, - drift: 50, - neighborScalar: 5, - prevLayerScalar: 5, - prevLayerLikenessScalar: 20, - }), - -]; - -for (const layer of layers) { - document.querySelectorAll(`.${layer.className}.layerParams > input`).forEach((input) => { - const param = input.getAttribute("param"); - - // pre-fill input values - input.value = layer.knobs[param]; - - input.onchange = () => { - console.log(`setting ${layer.className}.${param} to ${input.value}`); - layer.knobs[param] = input.value; - }; - }); -} - -const universe = new Universe(canvasesByClass, layers); - -const requestAnimationFrame = - window.requestAnimationFrame || - window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || - window.msRequestAnimationFrame; - -function doTick() { - universe.update(); - universe.draw(); - requestAnimationFrame(doTick); -} - -doTick(); - -</script> |