├── README.md
├── assets
├── main.css
├── emulator.js
├── main.js
└── grid.js
└── index.html
/README.md:
--------------------------------------------------------------------------------
1 | # heatgrid
2 |
3 | This will simulate Newton's universal law of cooling for a grid of interlocked particles.
4 |
--------------------------------------------------------------------------------
/assets/main.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | -moz-user-select: none;
3 | -webkit-user-select: none;
4 | -ms-user-select:none;
5 | user-select:none;
6 | -o-user-select:none;
7 |
8 | text-align: center;
9 | }
10 |
11 | #grid {
12 | border: 1px solid #d5d5d5;
13 | width: 290px;
14 | height: 290px;
15 | }
16 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 | heatgrid
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 | Clicks are hot (not cold)
21 |
22 |
28 |
29 | Click anywhere in the box.
30 |
31 | You can even spam your mouse.
32 |
33 |
34 |
--------------------------------------------------------------------------------
/assets/emulator.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var TIME_STEP = 1;
4 | var TRANSFER_CONSTANT = 1e-3;
5 |
6 | function Emulator(grid) {
7 | this._temperatures = [];
8 | this._neighbors = [];
9 | for (var i = 0, len = grid.cellCount(); i < len; ++i) {
10 | this._temperatures[i] = 0;
11 | this._neighbors[i] = grid.cellNeighbors(i);
12 | }
13 | }
14 |
15 | Emulator.prototype.emulate = function(time) {
16 | var count = Math.round(time / TIME_STEP);
17 | for (var i = 0; i < count; ++i) {
18 | this._step(TIME_STEP);
19 | }
20 | this._step(time - count*TIME_STEP);
21 | };
22 |
23 | Emulator.prototype.getTemp = function(cell) {
24 | return this._temperatures[cell];
25 | };
26 |
27 | Emulator.prototype.setTemp = function(cell, temp) {
28 | this._temperatures[cell] = temp;
29 | };
30 |
31 | Emulator.prototype._step = function(step) {
32 | var derivative = [];
33 | for (var i = 0, len = this._temperatures.length; i < len; ++i) {
34 | var neighbors = this._neighbors[i];
35 | var temp = this._temperatures[i];
36 | var velocity = 0;
37 | for (var j = 0, len1 = neighbors.length; j < len1; ++j) {
38 | var neighbor = neighbors[j];
39 | var neighborTemp = this._temperatures[neighbor];
40 | velocity += TRANSFER_CONSTANT * (neighborTemp - temp);
41 | }
42 | derivative[i] = velocity;
43 | }
44 | for (var i = 0, len = this._temperatures.length; i < len; ++i) {
45 | this._temperatures[i] += derivative[i] * step;
46 | }
47 | };
48 |
49 | window.app.Emulator = Emulator;
50 |
51 | })();
52 |
--------------------------------------------------------------------------------
/assets/main.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var emulator = null;
4 | var grid = null;
5 | var lastAnimationTime = null;
6 | var hotClickCheckbox = null;
7 |
8 | function main() {
9 | hotClickCheckbox = document.getElementById('hot-click');
10 | grid = new window.app.Grid();
11 | emulator = new window.app.Emulator(grid);
12 |
13 | grid.onClick = function(idx) {
14 | if (hotClickCheckbox.checked) {
15 | emulator.setTemp(idx, 2);
16 | } else {
17 | emulator.setTemp(idx, -2);
18 | }
19 | updateGridColors();
20 | };
21 |
22 | window.requestAnimationFrame(animationFrame);
23 | }
24 |
25 | function animationFrame(time) {
26 | window.requestAnimationFrame(animationFrame);
27 | if (lastAnimationTime === null) {
28 | lastAnimationTime = time;
29 | return;
30 | }
31 | var elapsed = Math.abs(time - lastAnimationTime);
32 | lastAnimationTime = time;
33 | emulator.emulate(elapsed);
34 |
35 | updateGridColors();
36 | }
37 |
38 | function updateGridColors() {
39 | for (var i = 0, len = grid.cellCount(); i < len; ++i) {
40 | temp = emulator.getTemp(i);
41 | var rgba;
42 | if (temp > 0) {
43 | var otherColors = Math.round(Math.min(1, Math.max(1-temp, 0)) * 255);
44 | rgba = 'rgba(255,' + otherColors + ',' + otherColors + ',1)';
45 | } else {
46 | var otherColors = Math.round(Math.min(1, Math.max(temp+1, 0)) * 255);
47 | rgba = 'rgba(' + otherColors + ',' + otherColors + ',255,1)';
48 | }
49 | grid.colorCell(i, rgba);
50 | }
51 | }
52 |
53 | window.addEventListener('load', main);
54 |
55 | })();
56 |
--------------------------------------------------------------------------------
/assets/grid.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
4 | var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
5 |
6 | var GRID_SIZE = 290;
7 | var RADIUS = 10;
8 | var SQRT_3 = Math.sqrt(3);
9 | var COLUMN_COUNT = Math.round((GRID_SIZE-RADIUS*2)/(RADIUS*3)+1);
10 | var ROW_COUNT = Math.ceil(GRID_SIZE / (SQRT_3 * RADIUS))-1;
11 |
12 | function Grid() {
13 | this._cells = [];
14 | this._element = document.getElementById('grid');
15 |
16 | var yValue = SQRT_3*RADIUS/2 + 1;
17 | for (var row = 0; row < ROW_COUNT; ++row) {
18 | var xValue = RADIUS;
19 | for (var col = 0; col < COLUMN_COUNT; ++col) {
20 | this._addCell(xValue, yValue);
21 | if (col+1 < COLUMN_COUNT) {
22 | this._addCell(xValue+RADIUS*1.5, yValue+SQRT_3*RADIUS/2);
23 | }
24 | xValue += RADIUS*3;
25 | }
26 | yValue += SQRT_3*RADIUS;
27 | }
28 |
29 | this.onClick = null;
30 | }
31 |
32 | Grid.prototype.cellCount = function() {
33 | return this._cells.length;
34 | };
35 |
36 | Grid.prototype.colorCell = function(idx, color) {
37 | this._cells[idx].setAttribute('fill', color);
38 | };
39 |
40 | Grid.prototype.cellNeighbors = function(idx) {
41 | var columnCount = COLUMN_COUNT*2 - 1;
42 | var row = Math.floor(idx / columnCount);
43 | var col = idx % columnCount;
44 | var res = [];
45 | if (col > 0) {
46 | res.push(row*columnCount + col - 1);
47 | }
48 | if (col+1 < columnCount) {
49 | res.push(row*columnCount + col + 1);
50 | }
51 | if (row > 0) {
52 | res.push((row-1)*columnCount + col);
53 | if (col%2 === 0) {
54 | if (col > 0) {
55 | res.push((row-1)*columnCount + col-1);
56 | }
57 | if (col+1 < columnCount) {
58 | res.push((row-1)*columnCount + col+1);
59 | }
60 | }
61 | }
62 | if (row+1 < ROW_COUNT) {
63 | res.push((row+1)*columnCount + col);
64 | if (col%2 === 1) {
65 | if (col > 0) {
66 | res.push((row+1)*columnCount + col-1);
67 | }
68 | if (col+1 < columnCount) {
69 | res.push((row+1)*columnCount + col+1);
70 | }
71 | }
72 | }
73 | return res;
74 | };
75 |
76 | Grid.prototype._addCell = function(x, y) {
77 | var element = document.createElementNS(SVG_NAMESPACE, 'use');
78 | element.setAttributeNS(XLINK_NAMESPACE, 'href', '#hexagon');
79 | var xStr = x.toFixed(3);
80 | var yStr = y.toFixed(3);
81 | element.setAttribute('transform', 'translate(' + xStr + ' ' + yStr + ')');
82 | this._cells.push(element);
83 | this._element.appendChild(element);
84 |
85 | var idx = this._cells.length - 1;
86 | this.colorCell(idx, 'white');
87 | element.addEventListener('click', function() {
88 | this.onClick(idx);
89 | }.bind(this));
90 | };
91 |
92 | window.app.Grid = Grid;
93 |
94 | })();
95 |
--------------------------------------------------------------------------------