libgd-gis v0.2.7.pre.alpha.1

January 16, 2026

Testing GIS animations in Ruby (exploratory work)

Today, early in the morning, after releasing GIF and animation support in ruby-libgd, together with updated documentation, versioning, and examples, I decided to do something very concrete:

spend the entire day stress-testing the alpha version of libgd-gis.

And what better way to test animations than to play a bit with real cities, casually moving around familiar maps… perhaps with a Pac-Man wandering through them.

The outcome was surprisingly solid. Not because everything is finished, but because the foundations are already strong enough that next week this can realistically evolve into a beta, or at least something very close to production from an architectural point of view.

Article content

The real challenge: animating maps

GIS animation is not just about “moving an icon”.

Each frame requires:

  • Reprojecting coordinates
  • Rendering complex vector layers
  • Preserving visual consistency
  • Repeating all of the above for every single frame

In practice:

The complexity of a map is multiplied by the number of frames.

This is precisely why v0.2.7.pre.alpha.1 exists. It is not a feature release — it is a testing ground.


Technical approach

The experiment was built around a very clear idea:

  • Render the base map once
  • Cache it
  • Render only dynamic layers per frame

This approach made it possible to generate smooth animations even over dense urban maps such as Manhattan or Buenos Aires.


Core structure

The entire experiment relies on three simple concepts:

  1. A cached static base
  2. Mutable dynamic layers
  3. A normalized time value (t from 0 to 1)
require "json"
require "gd/gis"

Simple UI overlay (legend)

Article content

Each frame includes a small legend rendered directly onto the image. No HTML, no canvas, no external rendering engine.

def draw_legend(img)
x = 24
y = 24
w = 520
h = 90
pad = 18
bg = GD::Color.rgba(0, 0, 0, 190)
border = GD::Color.rgb(255, 140, 0)
title_col = GD::Color.rgb(255, 160, 40)
sub_col = GD::Color.rgb(255, 210, 160)
img.filled_rectangle(x, y, x + w, y + h, bg)
img.rectangle(x, y, x + w, y + h, border)
img.text(
"libgd-gis v0.2.7.pre.alpha.1",
x: x + pad,
y: y + 38,
size: 26,
color: title_col,
font: "fonts/GoogleSans-Bold.ttf"
)
img.text(
"Animated GIS engine for Ruby",
x: x + pad,
y: y + 68,
size: 16,
color: sub_col,
font: "fonts/GoogleSans-SemiBold.ttf"
)
end
This alone demonstrates something important: libgd-gis does not depend on a frontend to produce presentable results.

Animation configuration

MANHATTAN = [-74.02, 40.70, -73.97, 40.78]
FRAMES = 40
DELAY = 8
PELLET_T_OFFSET = 0.01

All animation logic is driven by a normalized t, which makes it trivial to:

  • Interpolate positions
  • Synchronize multiple layers
  • Avoid visual tunneling issues
Article content

Rendering the base map (once)

This is arguably the most important optimization in the entire experiment.

map = GD::GIS::Map.new(
bbox: MANHATTAN,
zoom: 14,
basemap: :osm
)
map.style = GD::GIS::Style.load("dark")
map.add_geojson "nyc_roads.geojson"
map.render_base

From this point on, the base map is reused for every frame.


Defining the Pac-Man path

The movement is not arbitrary. It is sampled from a real GeoJSON path.

path = GD::GIS::PathSampler.from_geojson("bicicle.json")

This opens the door to real GIS use cases: routes, tracks, trajectories, fleets, and timelines.


Pellets as mutable state

Each pellet carries its own progress value.

pellets = (0...PELLET_COUNT).map do |i|
t = i.to_f / (PELLET_COUNT - 1)
lon, lat = path.point_at(t)
{ "lon" => lon, "lat" => lat, "t" => t }
end

As Pac-Man advances, pellets are removed in a robust and deterministic way.


Pac-Man as a dynamic layer

pacman_layer = map.add_points(
[{ "lon" => path.point_at(0)[0], "lat" => path.point_at(0)[1] }],
lon: ->(f){ f["lon"] },
lat: ->(f){ f["lat"] },
icon: "pacman.png"
)

Nothing special here — just another point layer synchronized to global time.

Article content

Animation loop

This is where everything comes together.

gif = GD::Gif.new("pacman_manhattan.gif")
FRAMES.times do |i|
t = i.to_f / (FRAMES - 1)
px, py = path.point_at(t)
pacman_layer.data = [{ "lon" => px, "lat" => py }]
pellets.reject! { |p| p["t"] <= t + PELLET_T_OFFSET }
pellet_layer.data = pellets
img = map.render_with_base
draw_legend(img)
gif.add_frame(img, delay: DELAY)
end
gif.close

The key detail is simple but powerful:

Only dynamic layers are rendered per frame, on top of a cached base image.


Final thoughts

This release is intentionally labeled pre-alpha.

The goal was not API stability or performance tuning, but to answer a single question:

Can Ruby realistically drive GIS animations without external engines?

Based on these experiments, the answer is yes.

The next step is to harden the API, formalize the animation layer model, and move toward a proper beta.

More to come soon.

Article content

Leave a comment