├── 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 | 23 | 24 | 26 | 27 | 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 | --------------------------------------------------------------------------------