leaflet-canvaslayer-field
Version:
A set of layers using canvas to draw ASCIIGrid or GeoTIFF files. This includes a basic raster layer (*ScalaField*) and an animated layer for vector fields, such as wind or currents (*VectorFieldAnim*)
173 lines (144 loc) • 5.43 kB
JavaScript
/**
* Animated VectorField on canvas
*/
L.CanvasLayer.VectorFieldAnim = L.CanvasLayer.Field.extend({
options: {
paths: 800,
color: 'white', // html-color | function colorFor(value) [e.g. chromajs.scale]
width: 1.0, // number | function widthFor(value)
fade: 0.96, // 0 to 1
duration: 20, // milliseconds per 'frame'
maxAge: 200, // number of maximum frames per path
velocityScale: 1 / 5000
},
initialize: function(vectorField, options) {
L.CanvasLayer.Field.prototype.initialize.call(
this,
vectorField,
options
);
L.Util.setOptions(this, options);
this.timer = null;
},
onLayerDidMount: function() {
L.CanvasLayer.Field.prototype.onLayerDidMount.call(this);
this._map.on('move resize', this._stopAnimation, this);
},
onLayerWillUnmount: function() {
L.CanvasLayer.Field.prototype.onLayerWillUnmount.call(this);
this._map.off('move resize', this._stopAnimation, this);
this._stopAnimation();
},
_hideCanvas: function _showCanvas() {
L.CanvasLayer.Field.prototype._hideCanvas.call(this);
this._stopAnimation();
},
onDrawLayer: function(viewInfo) {
if (!this._field || !this.isVisible()) return;
this._updateOpacity();
let ctx = this._getDrawingContext();
let paths = this._prepareParticlePaths();
this.timer = d3.timer(function() {
_moveParticles();
_drawParticles();
}, this.options.duration);
let self = this;
/**
* Builds the paths, adding 'particles' on each animation step, considering
* their properties (age / position source > target)
*/
function _moveParticles() {
// let screenFactor = 1 / self._map.getZoom(); // consider using a 'screenFactor' to ponderate velocityScale
paths.forEach(function(par) {
if (par.age > self.options.maxAge) {
// restart, on a random x,y
par.age = 0;
self._field.randomPosition(par);
}
let vector = self._field.valueAt(par.x, par.y);
if (vector === null) {
par.age = self.options.maxAge;
} else {
// the next point will be...
let xt = par.x + vector.u * self.options.velocityScale; //* screenFactor;
let yt = par.y + vector.v * self.options.velocityScale; //* screenFactor;
if (self._field.hasValueAt(xt, yt)) {
par.xt = xt;
par.yt = yt;
par.m = vector.magnitude();
} else {
// not visible anymore...
par.age = self.options.maxAge;
}
}
par.age += 1;
});
}
/**
* Draws the paths on each step
*/
function _drawParticles() {
// Previous paths...
let prev = ctx.globalCompositeOperation;
ctx.globalCompositeOperation = 'destination-in';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//ctx.globalCompositeOperation = 'source-over';
ctx.globalCompositeOperation = prev;
// fading paths...
ctx.fillStyle = `rgba(0, 0, 0, ${self.options.fade})`;
ctx.lineWidth = self.options.width;
ctx.strokeStyle = self.options.color;
// New paths
paths.forEach(function(par) {
self._drawParticle(viewInfo, ctx, par);
});
}
},
_drawParticle(viewInfo, ctx, par) {
let source = new L.latLng(par.y, par.x);
let target = new L.latLng(par.yt, par.xt);
if (
viewInfo.bounds.contains(source) &&
par.age <= this.options.maxAge
) {
let pA = viewInfo.layer._map.latLngToContainerPoint(source);
let pB = viewInfo.layer._map.latLngToContainerPoint(target);
ctx.beginPath();
ctx.moveTo(pA.x, pA.y);
ctx.lineTo(pB.x, pB.y);
// next-step movement
par.x = par.xt;
par.y = par.yt;
// colormap vs. simple color
let color = this.options.color;
if (typeof color === 'function') {
ctx.strokeStyle = color(par.m);
}
let width = this.options.width;
if (typeof width === 'function') {
ctx.lineWidth = width(par.m);
}
ctx.stroke();
}
},
_prepareParticlePaths: function() {
let paths = [];
for (var i = 0; i < this.options.paths; i++) {
let p = this._field.randomPosition();
p.age = this._randomAge();
paths.push(p);
}
return paths;
},
_randomAge: function() {
return Math.floor(Math.random() * this.options.maxAge);
},
_stopAnimation: function() {
if (this.timer) {
this.timer.stop();
}
}
});
L.canvasLayer.vectorFieldAnim = function(vectorField, options) {
return new L.CanvasLayer.VectorFieldAnim(vectorField, options);
};