Skip to content

Commit

Permalink
Merge pull request #43 from se-ti/master
Browse files Browse the repository at this point in the history
Precompute circle's weights
  • Loading branch information
sunng87 committed Dec 8, 2020
2 parents d5c620a + 2782112 commit c05e7c3
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 82 deletions.
228 changes: 151 additions & 77 deletions src/heatcanvas-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ onmessage = function(e){

function calc(params) {
var value = params.value;
var degree = +params.degree || 1;
var step = +params.step || 1;
var degree = params.degree || 1;
var step = params.step || 1;

if (!(value instanceof Float32Array) || value.length != params.width * params.height) {
value = new Float32Array(params.width * params.height);
}

var deg2 = degree / 2;
var cache = new Cache(params.data, params.height, params.width, step, degree);
var rows = cache.getRows();

var v = 0.0;
var hasLine1 = true, hasLine2 = true;
var dy = 0, dx = 0, rx = 0, base = 0, base2 = 0;

var v = 0.0, dySq = 0.0;
var scany = 0;
var scany2 = 0;
var dx = 0, rx = 0, base = 0, base2;
for (var pos in params.data) {
var data = params.data[pos] / step; // we don't need absolute values, and this allows us to get rid of many step*pow(...)
var radius = Math.pow(data, 1/degree);
Expand All @@ -52,155 +53,228 @@ function calc(params) {

// for all circles, lying inside the screen use fast method
if (y >= radius && y < params.height-radius && x >= radius && radius <= maxRx) {
fastCalc(params, data, value, x, y, radius, radiusSq);
fastCalc(params, data, value, x, y, radius, radiusSq, rows);
continue;
}

base = (y - radius)*params.width + x;
base2 = (y + radius)*params.width + x;

var row;
// calculate point x.y
var rLeft, rRight; // minimum of radius or distance to respective image border
var maxY = y+radius < params.height - 1 ? y+radius : (params.height - 1);
for (scany = y - radius; scany < y; scany++, base += params.width, base2 -= params.width) {
scany2 = (y + y - scany);
if (scany < 0 && scany2 > maxY) // both scanlines out of extend
continue;

dySq = Math.pow(scany-y, 2);
rx = Math.floor(Math.sqrt(radiusSq - dySq));
var rTop = radius > y ? y : radius;
var rBottom = maxY - y;

for (dy = rTop > rBottom ? rTop : rBottom, base = (y - dy) * params.width + x, base2 = (y + dy) * params.width + x;
dy > 0;
base += params.width, base2 -= params.width, dy--) {

hasLine1 = y - dy >= 0;
hasLine2 = y + dy <= maxY;

row = rows[dy];

rx = Math.floor(Math.sqrt(radiusSq - Math.pow(dy, 2)));
rLeft = rx > x ? x : rx;
rRight = rx > maxRx ? maxRx : rx;

if (rLeft < rRight) {
for (dx = rLeft + 1; dx <= rRight; dx++) {
v = data - Math.pow(Math.pow(dx, 2) + dySq, deg2);
v = data - row[dx];

if (scany >= 0)
if (hasLine1)
value[base + dx] += v;
if (scany2 <= maxY)
if (hasLine2)
value[base2 + dx] += v;
}
}
else if (rRight < rLeft) {
for (dx = rRight + 1; dx <= rLeft; dx++) {
v = data - Math.pow(Math.pow(dx, 2) + dySq, deg2);
if (scany >= 0)
v = data - row[dx];
if (hasLine1)
value[base - dx] += v;
if (scany2 <= maxY)
if (hasLine2)
value[base2 - dx] += v;
}
}

for (dx = rRight < rLeft ? rRight : rLeft; dx > 0; dx--) {
v = data - Math.pow(Math.pow(dx, 2) + dySq, deg2);
v = data - row[dx];

if (scany >= 0) {
if (hasLine1) {
value[base - dx] += v;
value[base + dx] += v;
}
if (scany2 <= maxY) {
if (hasLine2) {
value[base2 - dx] += v;
value[base2 + dx] += v;
}
}
// dx == 0
v = data - Math.pow(dySq, deg2);
if (scany >= 0) {
v = data - row[0];
if (hasLine1) {
value[base] += v;
}
if (scany2 <= maxY) {
if (hasLine2) {
value[base2] += v;
}
}

// dy == 0 && dx != 0
// attention! power (sqrt(dx^2), degree) == power(dx, degree), but dx SHOULD be non-negative, since degree can be float!
row = rows[0];
base = y*params.width + x;
rLeft = radius > x ? x : radius;
rRight = radius > maxRx ? maxRx : radius;
if (rLeft < rRight) {
for (dx = rLeft + 1; dx <= rRight; dx++)
value[base + dx] += data - Math.pow(dx, degree);
value[base + dx] += data - row[dx];
}
else if (rRight < rLeft) {
for (dx = rRight + 1; dx <= rLeft; dx++)
value[base - dx] += data - Math.pow(dx, degree);
value[base - dx] += data - row[dx];
}

for (dx = rRight < rLeft ? rRight : rLeft; dx > 0; dx--) {
v = data - Math.pow(dx, degree);
v = data - row[dx];
value[base - dx] += v;
value[base + dx] += v;
}

// dy == dx == 0
value[base] += data;
}

row = null;
rows = null;
cache.clear();
cache = null;

postMessage({'value': value, 'width': params.width, 'height': params.height});
}

/* Uses fact that circle on the screen has 4 axes of symmetry: x = 0, y = 0, y = x, y = -x,
so it computes values for ~1/8 of points and copies them to symmetrical ones
/* Knows that circle on the screen has 4 axes of symmetry (x = 0, y = 0, y = x, y = -x),
but uses just first two, because with precomputed weights cache-friendliness seems to be more important.
Does not check bounds of the image!
*/
function fastCalc(params, data, value, x, y, radius, radiusSq) {
var width = params.width;
var base = y * width + x;
var deg2 = params.degree / 2;
var radiusSq2 = radiusSq / 2;
function fastCalc(params, data, value, x, y, radius, radiusSq, cacheRows) {
var base = y * params.width + x;

var v = 0.0;
var vRow = null;
var dx = 0, rx = 0.0;

var xOffset = 0;
var xOffset2 = 0;
var dx = 0, dy = 0, rx = 0.0, dySq = 0;

var yOffset = base - radius * width;
var yOffset2 = base + radius * width;

for (dy = -radius; dy < 0; dy++, yOffset += width, yOffset2 -= width) {
dySq = Math.pow(dy, 2);
rx = Math.floor(Math.sqrt(radiusSq - dySq));
for (var dy = 1, yOffset = base - params.width, yOffset2 = base + params.width;
dy <= radius;
dy++, yOffset -= params.width, yOffset2 += params.width) {

dx = rx >= -dy ? dy + 1 : -rx;
xOffset = dx * width + dy;
xOffset2 = dx * width - dy;
for (; dx < 0; dx++, xOffset += width, xOffset2 += width) {
v = data - Math.pow(Math.pow(dx, 2) + dySq, deg2);
vRow = cacheRows[dy];
rx = Math.floor(Math.sqrt(radiusSq - Math.pow(dy, 2)));
for (dx = 1; dx <= rx; dx++) {
v = data - vRow[dx];

// main and symmetrical over x=0, y = 0;
value[yOffset + dx] += v;
value[yOffset - dx] += v;
value[yOffset2 + dx] += v;
value[yOffset + dx] += v;
value[yOffset2 - dx] += v;

// symmetrical over y = x, y = -x
value[base + xOffset] += v;
value[base + xOffset2] += v;
value[base - xOffset2] += v;
value[base - xOffset] += v;
}
//dy == dx
if (dySq <= radiusSq2) {
v = data - Math.pow(2 * dySq, deg2);
value[yOffset + dy] += v;
value[yOffset - dy] += v;
value[yOffset2 + dy] += v;
value[yOffset2 - dy] += v;
value[yOffset2 + dx] += v;
}

// dx = 0
v = data - Math.pow(dySq, deg2);
v = data - vRow[0];
value[yOffset] += v;
value[yOffset2] += v;
value[base + dy] += v;
value[base - dy] += v;
}

// dx == dy == 0
// dy == 0
vRow = cacheRows[0];
for (dx = 1; dx <= radius; dx++) {
v = data - vRow[dx];
value[base - dx] += v;
value[base + dx] += v;
}

// dy == dx == 0
value[base] += data;

}

// precomputes and stores weights for 1/4 of a circle.
// Isn't optimized for 1/8 for the sake of ease of use
var Cache = function(data, height, width, step, degree) {
this.rows = null;
this._deg2 = degree / 2;
this._maxDx = -1;
this._maxDy = -1;

this._computeStat(data, height, width, step, 1/degree);
};
Cache.prototype = {

_computeStat: function(data, height, width, step, reciprocalDegree) {
var maxV = -1;
var width1 = width - 1;
var height1 = height - 1;

for (var p in data) {
var p0 = data[p];
if (p0 <= maxV)
continue;

var r0 = Math.pow(p0 / step, reciprocalDegree);
var floor = Math.floor(r0);
var x0 = Math.floor(p%width);
var y0 = Math.floor(p/width);

var mDx = width1 - x0;
if (mDx < x0)
mDx = x0;
var mDy = height1 - y0;
if (mDy < y0)
mDy = y0;

if (mDx > floor)
mDx = floor;
if (mDy > floor)
mDy = floor;

if (mDx > this._maxDx)
this._maxDx = mDx;
if (mDy > this._maxDy)
this._maxDy = mDy;

if ((y0 >= r0 || y0 <= height1 - r0) && (x0 >= r0 || x0 <= width1 - r0)) { // this value's circle has at least a quarter inside the canvas. We aren't interested in smaller values anymore
maxV = p0;
}
}
},

getRows: function() {
if (this.rows) {
return this.rows;
}

var r, j;
var iSq;
this.rows = new Array(this._maxDy + 1);
for (var i = this._maxDy; i >= 0; i--) {
this.rows[i] = r = new Float32Array(this._maxDx + 1);
iSq = Math.pow(i, 2);
for (j = this._maxDx; j >= 0; j--) {
r[j] = Math.pow(iSq + Math.pow(j, 2), this._deg2);
}
}

return this.rows;
},

clear: function() {
if (!this.rows) {
return;
}

for (var i = this.rows.length - 1; i >= 0; i--) {
this.rows[i] = null;
}
this.rows = null;
}
};

10 changes: 5 additions & 5 deletions src/heatcanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,19 @@ HeatCanvas.prototype.render = function(step, degree, f_value_color) {
self.value = e.data.value;
self._valueWidth = e.data.width;
self._valueHeight = e.data.height;
self.data = {};
self.data = {}; // can spoil next rendering if onmessage happens between HeatCanvas.push and next render!
self._render(f_value_color);
if (self.onRenderingEnd) {
self.onRenderingEnd();
}
}
var msg = {
'data': self.data,
'width': self.width,
'height': self.height,
'data': this.data,
'width': this.width,
'height': this.height,
'step': step,
'degree': degree,
'value': self.value
'value': this.value
};
this.worker.postMessage(msg);
if (this.onRenderingStart) {
Expand Down

0 comments on commit c05e7c3

Please sign in to comment.