GPU-Animated Image
Render an animated icon directly into the style image atlas with a WebGL shader.
Based on: https://maplibre.org/maplibre-gl-js/docs/examples/animate-an-icon-on-the-gpu/
<script lang="ts">
import 'svelte-maplibre-gl/vite'; // Required only for GL JS v6+
import type * as maplibregl from 'maplibre-gl';
import { MapLibre, Image, GeoJSONSource, SymbolLayer } from 'svelte-maplibre-gl';
const SIZE = 128;
const CITIES = [
[141.35, 43.06],
[139.69, 35.68],
[136.91, 35.18],
[135.5, 34.69],
[132.46, 34.4],
[130.4, 33.59]
];
const VERTEX_SHADER = `#version 300 es
const vec2 p[3] = vec2[3](vec2(-1, -1), vec2(3, -1), vec2(-1, 3));
out vec2 uv;
void main() { gl_Position = vec4(p[gl_VertexID], 0, 1); uv = p[gl_VertexID] * .5 + .5; }`;
const FRAGMENT_SHADER = `#version 300 es
precision highp float;
in vec2 uv; uniform float time; out vec4 color;
void main() {
vec2 p = uv * 2. - 1.; float r = length(p), a = atan(p.y, p.x);
float ring = .02 / abs(r - .48 - .07 * sin(a * 6. - time * 2.));
float spark = .015 / abs(length(p - .55 * vec2(cos(time), sin(time))) - .06);
float alpha = smoothstep(1., .72, r) * clamp(.08 + ring + spark, 0., 1.);
vec3 neon = .5 + .5 * cos(time + r * 7. + vec3(0., 2., 4.));
color = vec4(neon * alpha, alpha);
}`;
let map: maplibregl.Map | undefined;
let gl: WebGL2RenderingContext | undefined;
let program: WebGLProgram | null;
let framebuffer: WebGLFramebuffer | null;
let timeLocation: WebGLUniformLocation | null;
function compileShader(context: WebGL2RenderingContext, type: number, source: string) {
const shader = context.createShader(type)!;
context.shaderSource(shader, source);
context.compileShader(shader);
return shader;
}
function setup(context: WebGL2RenderingContext) {
gl = context;
program = context.createProgram();
context.attachShader(program!, compileShader(context, context.VERTEX_SHADER, VERTEX_SHADER));
context.attachShader(program!, compileShader(context, context.FRAGMENT_SHADER, FRAGMENT_SHADER));
context.linkProgram(program!);
timeLocation = context.getUniformLocation(program!, 'time');
framebuffer = context.createFramebuffer();
}
function destroy() {
if (!gl) return;
gl.deleteProgram(program);
gl.deleteFramebuffer(framebuffer);
gl = undefined;
program = framebuffer = timeLocation = null;
}
const gpuImage: maplibregl.StyleImageInterface = {
width: SIZE,
height: SIZE,
onAdd(value) {
map = value;
},
render() {
map?.triggerRepaint();
return true;
},
data: {
renderWithWebGL(target) {
if (gl !== target.gl) setup(target.gl);
target.gl.bindFramebuffer(target.gl.FRAMEBUFFER, framebuffer);
target.gl.framebufferTexture2D(
target.gl.FRAMEBUFFER,
target.gl.COLOR_ATTACHMENT0,
target.gl.TEXTURE_2D,
target.texture,
0
);
target.gl.viewport(target.x, target.y, target.width, target.height);
target.gl.useProgram(program);
target.gl.uniform1f(timeLocation, performance.now() / 1000);
target.gl.drawArrays(target.gl.TRIANGLES, 0, 3);
}
},
onRemove: destroy
};
</script>
<MapLibre
class="h-[55vh] min-h-75"
style="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
zoom={4}
center={{ lng: 136.5, lat: 37.5 }}
>
<Image id="gpu-signal" image={gpuImage} options={{ pixelRatio: 2 }} />
<GeoJSONSource
data={{
type: 'FeatureCollection',
features: CITIES.map((coordinates) => ({
type: 'Feature' as const,
geometry: { type: 'Point' as const, coordinates },
properties: {}
}))
}}
>
<SymbolLayer layout={{ 'icon-image': 'gpu-signal', 'icon-allow-overlap': true }} />
</GeoJSONSource>
</MapLibre>Our examples use Tailwind CSS and shadcn-svelte.