summaryrefslogtreecommitdiff
path: root/static/src/_posts/2021-05-26-viz-4.md
blob: cd6054a95b2b8fc752dc2a567e57a1d3691c1cde (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
---
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>