Compare commits

...

33 Commits

Author SHA1 Message Date
Francisco Dans
7fed0cd757 line stuff 2015-03-16 11:14:22 +01:00
Francisco Dans
9b761ffd4c Merge branch 'master' into aequus
Conflicts:
	lib/torque/renderer/point.js
2015-03-09 12:48:35 +01:00
Francisco Dans
49d7d5b300 adds gradient function to ballrenderer 2015-03-09 10:29:51 +01:00
Francisco Dans
c524df2f01 comments out cumulative in example 2015-02-27 10:32:08 +01:00
Francisco Dans
6ded5dafc9 makes isopleth work again in spito of lack of movement 2015-02-27 10:31:23 +01:00
Francisco Dans
aa79eeda20 adds getballindices 2015-02-26 18:19:48 +01:00
Francisco Dans
dc7516e9e6 modifies navy example to try renderer 2015-02-26 16:07:39 +01:00
Francisco Dans
3f9a8f4a18 full invalidation only on animation end 2015-02-26 16:06:39 +01:00
Francisco Dans
deacb1ae00 adds repeat checks in isopleth 2015-02-26 16:05:58 +01:00
Francisco Dans
83b6b3d058 invalidates layers a bit smarter 2015-02-26 16:05:25 +01:00
Francisco Dans
a8a8d79d86 caches balls 2015-02-26 16:03:20 +01:00
Francisco Dans
204df7829d makes sure that animation is paused when steps is one 2015-02-26 16:02:06 +01:00
Francisco Dans
98a37771ca gets rid of unnecessary stuff in isopleth 2015-02-25 18:10:31 +01:00
Francisco Dans
6a0fe4beb7 removes map_range 2015-02-25 18:08:33 +01:00
Francisco Dans
68ff448f78 invalidates pointarray when not cumulative 2015-02-25 18:02:41 +01:00
Francisco Dans
171a5b6e0c optimizes horizontalLine 2015-02-25 18:01:38 +01:00
Francisco Dans
7f72ae01af conveys draw calls in flush method 2015-02-25 12:47:55 +01:00
Francisco Dans
8e8ae30dc8 adds isInvalid to check for layers with wrong values 2015-02-25 12:47:12 +01:00
Francisco Dans
3fe1c60d61 simplifies invalidate 2015-02-25 12:46:45 +01:00
Francisco Dans
51fc104793 changes name to pointLayer 2015-02-25 12:46:26 +01:00
Francisco Dans
d048652ba9 optimizes drawnTemp a bit more 2015-02-25 12:45:45 +01:00
Francisco Dans
9479b214fa initialises layers JIT, instead of in the constructor 2015-02-25 12:42:21 +01:00
Francisco Dans
43e6cb7a37 invalidates the canvas properly 2015-02-25 12:40:43 +01:00
Francisco Dans
4f41136d24 calls draw at the end of each tile render instead of each canvas 2015-02-24 12:12:04 +01:00
Francisco Dans
a36fd0936a comments out comp ops 2015-02-24 12:11:37 +01:00
Francisco Dans
bc37a754a8 constructs the ballRenderer JIT 2015-02-24 12:11:04 +01:00
Francisco Dans
7ad4f21cc7 removes normalisation to 255 since native already does that 2015-02-24 12:10:21 +01:00
Francisco Dans
ea7f4efb23 checks for repeated lines using a simple array, optimizing the hell of horizontalLine 2015-02-24 12:09:46 +01:00
Francisco Dans
169de36bcf uses only canvas object, renames it to avoid future problems 2015-02-24 12:08:11 +01:00
Francisco Dans
4b240a4485 passes position on renderTile 2015-02-24 12:07:11 +01:00
Francisco Dans
d7d7396ba8 properly invalidates ballrenderer with each tile 2015-02-24 12:06:47 +01:00
Francisco Dans
cd396b19c3 prepares point renderer for ball renderer 2015-02-23 15:16:57 +01:00
Francisco Dans
82c5ef2b96 adds ball renderer 2015-02-23 15:15:43 +01:00
4 changed files with 325 additions and 31 deletions

View File

@ -16,11 +16,12 @@
// define the torque layer style using cartocss
var CARTOCSS = [
'Map {',
'-torque-time-attribute: "date";',
'-torque-time-attribute: "cartodb_id";',
'-torque-aggregation-function: "count(cartodb_id)";',
'-torque-frame-count: 760;',
'-torque-frame-count: 512;',
'-torque-animation-duration: 15;',
'-torque-resolution: 2',
'-torque-resolution: 10',
//'-torque-data-aggregation:cumulative;',
'}',
'#layer {',
' marker-width: 3;',
@ -33,29 +34,27 @@
' [value > 5] { marker-fill: #CC4C02; }',
' [value > 6] { marker-fill: #993404; }',
' [value > 7] { marker-fill: #662506; }',
' [frame-offset = 1] { marker-width: 10; marker-fill-opacity: 0.05;}',
' [frame-offset = 2] { marker-width: 15; marker-fill-opacity: 0.02;}',
'}'
].join('\n');
var map = new L.Map('map', {
zoomControl: true,
center: [40, 0],
zoom: 3
center: [41.65649719441145,-73.3447265625],
zoom: 6
});
L.tileLayer('http://{s}.api.cartocdn.com/base-dark/{z}/{x}/{y}.png', {
L.tileLayer('http://{s}.api.cartocdn.com/base-light/{z}/{x}/{y}.png', {
attribution: 'CartoDB'
}).addTo(map);
var torqueLayer = new L.TorqueLayer({
user : 'viz2',
table : 'ow',
user : 'fdansv',
table : 'snow',
cartocss: CARTOCSS
});
torqueLayer.addTo(map);
torqueLayer.play()
torqueLayer.play();
</script>
</body>
</html>

View File

@ -60,6 +60,8 @@ L.TorqueLayer = L.CanvasLayer.extend({
this.setDuration = this.animator.duration.bind(this.animator);
this.isRunning = this.animator.isRunning.bind(this.animator);
this.lastStep = 0;
L.CanvasLayer.prototype.initialize.call(this, options);
@ -210,7 +212,9 @@ L.TorqueLayer = L.CanvasLayer.extend({
var canvas = this.getCanvas();
this.renderer.clearCanvas();
var ctx = canvas.getContext('2d');
var brrr = false;
for(t in this._tiles) {
tile = this._tiles[t];
if (tile) {
@ -226,12 +230,17 @@ L.TorqueLayer = L.CanvasLayer.extend({
// when the tile has a cached image just render it and avoid to render
// all the points
this.renderer._ctx.drawImage(tile._tileCache, 0, 0);
brrr = true;
} else {
this.renderer.renderTile(tile, this.key);
this.renderer.renderTile(tile, this.key, pos);
brrr = false;
}
}
}
this.renderer.applyFilters();
var full = this.getStep() < this.lastStep;
this.lastStep = this.getStep();
if(!brrr) this.renderer.flush(full);
//this.renderer.applyFilters();
// prepare caches if the animation is not running
// don't cache if the key has just changed, this avoids to cache

271
lib/torque/renderer/ball.js Normal file
View File

@ -0,0 +1,271 @@
////////////////////////////////
// Torque BallRenderer
// CartoDB, 2015
// developed by Francisco Dans
////////////////////////////////
function BallRenderer(thecanvas){
this.canvas = thecanvas;
this.ctx = this.canvas.getContext("2d");
this.width = this.canvas.width;
this.height = this.canvas.height;
this.size = this.width * this.height;
this.pointLayer = new Uint8ClampedArray(this.size * 4);
this.radius = 30;
this.drawnTemp = {};
this.prof = 0;
this.gradient = {};
this.ballsSoFar = 0;
this.balls = 0;
this.cumulative = true;
this.RW4 = this.radius * this.width *4;
}
BallRenderer.prototype = {
getBallIndices: function(x, y){
var indices = new Int8Array(2*r+1);
return indices;
},
addBall: function(x0, y0){
if(this.cachedBall){
var startingPoint = this.getRIndexPos(x0, y0) - this.RW4 - this.radius*4;
var i = 0,
pointer = startingPoint,
ballWidth = (this.radius*2)*4
linemax = startingPoint + 2 * this.radius*4,
endPoint = this.getRIndexPos(x0, y0) + this.RW4 + this.radius;
while (pointer <= endPoint){
while (pointer <= linemax){
this.pointLayer[pointer+3] += this.cachedBall[i+3];
i+=4;
pointer+=4;
}
linemax += this.width * 4;
pointer += this.width * 4 - ballWidth -4;
}
}
else{
this.cachedBall = new Uint8ClampedArray(Math.pow(2 * this.radius + 1, 2)*4);
x0 = this.radius;
y0 = this.radius;
var orad = this.radius;
var x = this.radius;
var y = 0;
var radiusError = 1 - x;
while (x >= y){
// Try not to touch the following, it's a pain in the ass to write
this.horizontalLine(-x + x0, x + x0, y + y0, x0,y0);
this.horizontalLine(-y + x0, y + x0, -x + y0, x0,y0);
this.horizontalLine(-x + x0, x + x0, -y + y0, x0,y0);
this.horizontalLine(-y + x0, y + x0, x + y0, x0,y0);
++y;
if (radiusError<0){
radiusError += 2 * y + 1;
}
else{
--x;
radiusError += 2 * (y - x) + 1;
}
}
this.drawnTemp = {};
}
},
addLinePoint: function(x, y){
if(!this.line){
this.line = [];
}
line.push({x,y});
},
horizontalLine: function(xi, xf, yi, x0, y0){
// Assumes xi is on the left and xf is on the right
if(typeof this.drawnTemp[yi] === "undefined"){
while (xi <= xf){
this.addPoint(xi, yi, 30 - ((30 * this.lineDistance(xi, yi,x0,y0)) / this.radius));
++xi;
}
this.drawnTemp[yi]=true;
}
},
addPoint: function(x, y, alpha){
var indexPos = (y*(this.radius*2+1)+x)*4;
this.cachedBall[indexPos + 3] = this.cachedBall[indexPos + 3] + alpha;
},
map_range: function(value, low1, high1, low2, high2) {
return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
},
lineDistance: function(x,y,x0,y0){
var xs = Math.pow(x - x0, 2);
var ys = Math.pow(y - y0, 2);
return Math.sqrt( xs + ys );
},
draw: function(dataArray){
if(this.line){
ctx.beginPath()
for (i in this.line){
if(i === 0) this.ctx.moveTo(this.line[i].x, this.line[i].x);
else{
this.ctx.lineTo(this.line[i].x, this.line[i].x);
}
}
ctx.endPath();
}
ctx.stroke();
if (!dataArray){
if (this.isoplethLayer) dataArray = this.isoplethLayer;
else if (this.contourLayer) dataArray = this.contourLayer;
else dataArray = this.pointLayer;
}
if(!this.imageData) this.imageData = this.ctx.createImageData(this.width, this.height);
this.imageData.data.set(dataArray);
this.ctx.putImageData(this.imageData, 0, 0);
},
mergeLayers: function(from, to){
if (from.length !== to.length) throw("layers aren't of the same size"); return;
for (var i = 0; i<to.length; i++){
if(from[i+3]>0){
// There's a better way of doing this but I was lazy.
to[i] = from[i];
to[i+1] = from[i+1];
to[i+2] = from[i+2];
to[i+3] = from[i+3];
}
}
},
reduceArray: function(){
},
expandArray: function(){
},
invalidate: function(full){
if(full)this.pointLayer = new Uint8ClampedArray(this.size * 4);
if(this.contourLayer){
this.contourLayer = new Uint8ClampedArray(this.size * 4);
if(this.isoplethLayer){
this.isoplethLayer = new Uint8ClampedArray(this.size * 4);
}
}
},
getRIndexPos: function(x,y){
var rIndexPos = (y*this.width+x)*4;
return rIndexPos;
},
getXYFromRIndex: function(index){
var x = (index % (this.width*4))/4;
var y = (index - 4 * x) / (4 * this.width);
return [x,y];
},
// Clockwise. Again, there definitely is a better way.
getNeighbors: function(index){
var tw = this.width*4;
var n = index - tw;
var s = index + tw;
return [n, n + 4, index + 4, s + 4, s, s - 4, index - 4, n -4];
},
isEmpty: function(layer) {
for (var i = 0; i<layer.length; i+=4){
if(layer[i+3] > 0) return false;
}
return true;
},
isInvalid: function(layer) {
for (var i = 0; i<layer.length; i+=4){
if(layer[i+3]!==0 && !layer[i+3]) return true;
}
return false;
},
createArray: function(){
return new Uint8ClampedArray(this.size * 4);
},
contour: function(granularity){
var step = 255/granularity;
var i = 0, a = new Uint8ClampedArray(granularity+1), c=0;
while (i<255){
a[c] = i;
i += step;
c++;
}
a[a.length-1] = 255;
var l = -step/2;
var gradient = new Uint8ClampedArray(1024);
for(var i = 0; i<a.length; i++){
var y = Math.round(i*step);
var thisAlpha = a[i];
while(y<step*(i+1)){
gradient[y*4+3] = thisAlpha;
y++;
}
}
if(!this.contourLayer) this.contourLayer = new Uint8ClampedArray(this.size * 4);
for (var i = 0; i< this.pointLayer.length; i+=4){
if(this.pointLayer[i+3] === 0) continue;
var currentAlpha = this.pointLayer[i+3];
this.contourLayer[i+3] = gradient[currentAlpha*4+3];
}
},
isopleth: function(){
var iso = this.createArray();
var contour = this.contourLayer;
var eq = false;
for (var i = 0, len = contour.length; i<len; i+=4){
if(!eq){
var alpha = contour[i + 3];
if (alpha > 0 && alpha < 255){
var neighbors = this.getNeighbors(i);
var refCol = contour[neighbors[0]+3];
for (var n = 1, ln = neighbors.length; n<ln; ++n){
if (contour[neighbors[n]+3] !== refCol) break;
if (n === ln-1) eq = true;
}
if(!eq){
for (var n = 0, ln = neighbors.length; n<ln; ++n){
var index = neighbors[n];
if(index>0 && (contour[index+3] === 0 || contour[index+3] !== contour[i+3])){
iso[index+3] = contour[i+3];
iso[index] = 150;
}
}
}
}
}
else{
eq = false;
}
}
this.isoplethLayer = iso;
},
colorize: function () {
for (var i = 3, len = this.pointLayer.length, j; i < len; i += 4) {
j = this.pointLayer[i] * 4; // get gradient color from opacity value
if (j) {
this.pointLayer[i - 3] = this._grad[j];
this.pointLayer[i - 2] = this._grad[j + 1];
this.pointLayer[i - 1] = this._grad[j + 2];
}
}
},
gradient: function (grad) {
// create a 256x1 gradient that we'll use to turn a grayscale heatmap into a colored one
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
gradient = ctx.createLinearGradient(0, 0, 0, 256);
canvas.width = 1;
canvas.height = 256;
for (var i in grad) {
gradient.addColorStop(i, grad[i]);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1, 256);
this._grad = ctx.getImageData(0, 0, 1, 256).data;
return this;
}
}
module.exports = BallRenderer;

View File

@ -2,6 +2,8 @@ var torque = require('../');
var cartocss = require('./cartocss_render');
var Profiler = require('../profiler');
var carto = global.carto || require('carto');
var filters = require('./torque_filters');
var BallRenderer = require('./ball.js');
var Filters = require('./torque_filters');
var TAU = Math.PI * 2;
@ -172,7 +174,7 @@ var Filters = require('./torque_filters');
//
// renders all the layers (and frames for each layer) from cartocss
//
renderTile: function(tile, key, callback) {
renderTile: function(tile, key, pos, callback) {
if (this._iconsToLoad > 0) {
this.on('allIconsLoaded', function() {
this.renderTile.apply(this, [tile, key, callback]);
@ -189,11 +191,10 @@ var Filters = require('./torque_filters');
for(var fr = 0; fr < layer.frames().length; ++fr) {
var frame = layer.frames()[fr];
var fr_sprites = sprites[frame] || (sprites[frame] = []);
this._renderTile(tile, key - frame, frame, fr_sprites, layer);
this._renderTile(tile, key - frame, frame, fr_sprites, layer, null, pos);
}
}
}
prof.end(true);
return callback && callback(null);
@ -240,15 +241,15 @@ var Filters = require('./torque_filters');
// renders a tile in the canvas for key defined in
// the torque tile
//
_renderTile: function(tile, key, frame_offset, sprites, shader, shaderVars) {
_renderTile: function(tile, key, frame_offset, sprites, shader, shaderVars, pos) {
if (!this._canvas) return;
var prof = Profiler.metric('torque.renderer.point.renderTile').start();
var ctx = this._ctx;
var blendMode = compop2canvas(shader.eval('comp-op')) || this.options.blendmode;
if (blendMode) {
ctx.globalCompositeOperation = blendMode;
}
// var blendMode = compop2canvas(shader.eval('comp-op')) || this.options.blendmode;
// if (blendMode) {
// ctx.globalCompositeOperation = blendMode;
// }
if (this.options.cumulative && key > tile.maxDate) {
//TODO: precache because this tile is not going to change
key = tile.maxDate;
@ -262,20 +263,24 @@ var Filters = require('./torque_filters');
var posIdx = tile.renderDataPos[pixelIndex + p];
var c = tile.renderData[pixelIndex + p];
if (c) {
var sp = sprites[c];
if (sp === undefined) {
sp = sprites[c] = this.generateSprite(shader, c, torque.extend({ zoom: tile.z, 'frame-offset': frame_offset }, shaderVars));
}
if (sp) {
var x = tile.x[posIdx]- (sp.width >> 1) + anchor;
var y = tileMax - tile.y[posIdx] + anchor; // flip mercator
ctx.drawImage(sp, x, y - (sp.height >> 1));
}
var x = tile.x[posIdx];
var y = tileMax - tile.y[posIdx]; // flip mercator
if(!this.ballRenderer) this.ballRenderer = new BallRenderer(this._canvas);
//this.ballRenderer.addBall(pos.x + x, pos.y + y);
this.ballRenderer.addLinePoint(pos.x + x, pos.y + y);
// var sp = sprites[c];
// if (sp === undefined) {
// sp = sprites[c] = this.generateSprite(shader, c, torque.extend({ zoom: tile.z, 'frame-offset': frame_offset }, shaderVars));
// }
// if (sp) {
// var x = tile.x[posIdx]- (sp.width >> 1);
// var y = tileMax - tile.y[posIdx]; // flip mercator
// ctx.drawImage(sp, x, y - (sp.height >> 1));
// }
}
}
}
prof.end(true);
},
@ -439,6 +444,16 @@ var Filters = require('./torque_filters');
this._filters.draw();
}
}
},
flush: function(full){
this._ctx.setTransform(1, 0, 0, 1, 0, 0);
if(!this.ballRenderer) return;
//this.ballRenderer.contour(6);
//this.ballRenderer.isopleth();
//this.ballRenderer.gradient({0.1:"#21313E",0.3:"#20575F",0.5:"#268073",0.7:"#53A976",0.9:"#98CF6F",1:"#EFEE69"});
//mthis.ballRenderer.colorize();
this.ballRenderer.draw();
this.ballRenderer.invalidate(full);
}
});