Animated Mapbox Vector Fields (aka Wind Maps)
People often want to create a "Windy-style" map using Mapbox. A few years ago, Mapboxer @mourner wrote a blog post on how to create a map in that style using WebGL. It's an effective method, but the resulting map had a static, global zoom. This notebook shows an approach for taking the final step, rendering a vector field over an interactive Mapbox map using a Custom Layer. Only a few tweaks to the original blog-post code were needed to produce the map below.
viewof map = Error: Failed to initialize WebGL.
resizeTrigger = RuntimeError: Failed to initialize WebGL.
Implementation details
Most demonstrations of an animated vector field are of winds at various levels of the atmosphere. For a change of pace, I hunted down a different vector field, model output of tectonic plate motions. That data file is attached to this notebook. I had never dealt with this data before, so to make sure I was interpreting it correctly, I referenced a UNAVCO viewer and selected the Modeled, IGS08/NNR, GSRM data source for comparison.
The Mapbox map above animates particles moving in the direction of the plate motion. The map also shows all magnitude 6+ earthquakes over the past ten years. As expected, the earthquakes focus in areas of particle (plate) convergence.
Like in the original blog post, this notebook requires the vector field to be encoded in the channels of an image. I encoded the u (east/west) and v (north/south) plate-motion vector components in the R and G channels of the RGBA image visible below.
When preparing vector data for use with this method, it is important to make sure the output is in degrees (the WGS 84 projection). The method also requires knowing the latitude and longitude bounds of the data. The tectonic plate motion data is global, so to wrap around the dateline it goes from -180 to 180 longitude. The latitude does not extend all the way to the poles (it's only -80 to 80), but that's okay. The vectors will simply not render outside the specified bounds.
data = Uint8Array(232484) [139, 115, 0, 255, 139, 116, 0, 255, 139, 116, 0, 255, 139, 116, 0, 255, 139, 116, 0, 255, … ]
The VectorField cell contains the bulk of the code. It does all of the preparation of the WebGL programs required to render the vectors and contains the methods for starting and stopping the animation. To reduce WebGL boilerplate required, the twgl.js library was used. The library was created by greggman, who also created the fantastic webglfundamentals.org for learning all things WebGL.
Included in the block are variables that will affect the appearance of the map, including number of particles, how fast the trails fade, and the speed of the particles.
That's all there is to it! With this code, you'll be able to animate any vector field on a Mapbox map.
VectorField = ƒ(map, gl)
Shader code
vs = ` precision highp float;
attribute float a_index;
uniform sampler2D u_particles; uniform float u_particles_res;
varying vec2 v_particle_pos;
void main() { vec4 color = texture2D(u_particles, vec2( fract(a_index / u_particles_res), floor(a_index / u_particles_res) / u_particles_res));
// decode current particle position from the pixel's RGBA value v_particle_pos = vec2( color.r / 255.0 + color.b, color.g / 255.0 + color.a); } `
fs = ` precision highp float;
uniform sampler2D u_vector; uniform vec2 u_vector_min; uniform vec2 u_vector_max; //uniform sampler2D u_color_ramp;
uniform vec4 u_bounds; uniform vec4 u_data_bounds;
varying vec2 v_particle_pos;
vec2 returnLonLat(float x_domain, float y_domain, vec2 pos) {
//need value between 0 and 1, which fract accomplishes float mercator_x = fract(u_bounds.x + pos.x * x_domain); float mercator_y = u_bounds.w + pos.y * y_domain;
float lon = mercator_x * 360.0 - 180.0; } `
vsQuad = ` precision highp float;
attribute vec2 a_pos;
varying vec2 v_tex_pos;
void main() { v_tex_pos = a_pos; gl_Position = vec4(1.0 - 2.0 * a_pos, 0, 1); } `
fsScreen = ` precision highp float;
uniform sampler2D u_screen; uniform float u_opacity;
varying vec2 v_tex_pos;
void main() { vec4 color = texture2D(u_screen, 1.0 - v_tex_pos); // a hack to guarantee opacity fade out even with a value close to 1.0 gl_FragColor = vec4(floor(255.0 * color * u_opacity) / 255.0); } `
fsUpdate = ` precision highp float;
uniform sampler2D u_particles; uniform sampler2D u_vector; uniform vec2 u_vector_res; uniform vec2 u_vector_min; uniform vec2 u_vector_max; uniform float u_rand_seed; uniform float u_speed_factor; uniform float u_drop_rate; uniform float u_drop_rate_bump; uniform vec4 u_bounds; uniform vec4 u_data_bounds;
varying vec2 v_tex_pos;
// pseudo-random generator const vec3 rand_constants = vec3(12.9898, 78.233, 4375.85453); float rand(const vec2 co) { } `
Odds and ends
VectorImage = ƒ(url, bounds, range)
modeledData = Array(57960) [Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), Array(7), …]
dataWidth = 361
dataHeight = 161
minLon = -180
minLat = -80
container = Object {element: HTMLDivElement}
mapDimensions = Array(2) [1889, 944]
JavaScript libraries
mapboxgl = Object {version: "2.9.2", supported: ƒ(e), setRTLTextPlugin: ƒ(…), getRTLTextPluginStatus: ƒ(), Map: class, NavigationControl: class, GeolocateControl: class, AttributionControl: class, ScaleControl: class, FullscreenControl: class, Popup: class, Marker: class, Style: class, LngLat: class, LngLatBounds: class, Point: ƒ(t, e), MercatorCoordinate: class, FreeCameraOptions: class, Evented: class, config: Object, …}
twgl = Object {__esModule: true, m4: Object, primitives: Object, v3: Object, addExtensionsToContext: ƒ(gl), attributes: Object, draw: Object, framebuffers: Object, getContext: ƒ(canvas, opt_attribs), getWebGLContext: ƒ(canvas, opt_attribs), programs: Object, resizeCanvasToDisplaySize: ƒ(canvas, multiplier), setDefaults: ƒ(newDefaults), textures: Object, typedarrays: Object, utils: Object, vertexArrays: Object, createAttribsFromArrays: ƒ(gl, arrays), createBufferFromArray: ƒ(gl, array, arrayName), createBufferFromTypedArray: ƒ(gl, typedArray, type, drawType), …}`