From 29228fc9ea1813ff25816dfdf7bbb7e4608061c8 Mon Sep 17 00:00:00 2001 From: Ryan Hamamura <58859899+ryanhamamura@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:03:59 -1000 Subject: [PATCH] feat: animate fleet of container ships along bay waypoints Replace the static ship marker with three signal-backed ships that lerp along waypoint loops through SF Bay (Golden Gate, Oakland, and Sausalito ferry routes). All clients see the same ship positions via shared Go state synced every second. --- internal/examples/maplibre/main.go | 142 +++++++++++++++++++++++++---- 1 file changed, 124 insertions(+), 18 deletions(-) diff --git a/internal/examples/maplibre/main.go b/internal/examples/maplibre/main.go index 4922a6d..22f80ee 100644 --- a/internal/examples/maplibre/main.go +++ b/internal/examples/maplibre/main.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "math" "math/rand" "strconv" "sync" @@ -24,6 +26,55 @@ var ( } ) +// shipState tracks a ship lerping along a loop of waypoints. +type shipState struct { + lng, lat float64 + waypoints [][2]float64 // [lng, lat] pairs + wpIdx int // index of next target waypoint + progress float64 // 0..1 toward next waypoint + speed float64 // progress increment per tick +} + +func (s *shipState) tick() { + s.progress += s.speed + for s.progress >= 1 { + s.progress -= 1 + s.wpIdx = (s.wpIdx + 1) % len(s.waypoints) + } + from := s.waypoints[(s.wpIdx-1+len(s.waypoints))%len(s.waypoints)] + to := s.waypoints[s.wpIdx] + s.lng = from[0] + (to[0]-from[0])*s.progress + s.lat = from[1] + (to[1]-from[1])*s.progress +} + +// heading returns clockwise degrees from north (for SVG rotation). +func (s *shipState) heading() float64 { + from := s.waypoints[(s.wpIdx-1+len(s.waypoints))%len(s.waypoints)] + to := s.waypoints[s.wpIdx] + dx := to[0] - from[0] + dy := to[1] - from[1] + // atan2 gives angle from +X axis; convert to CW from north + return math.Mod(math.Atan2(dx, dy)*180/math.Pi+360, 360) +} + +var ( + fleetOnce sync.Once + fleet struct { + mu sync.RWMutex + ships [3]shipState + } +) + +const shipSVG = `` + func main() { v := via.New() v.Config(via.Options{ @@ -48,6 +99,50 @@ func main() { }() }) + // Fleet of ships following waypoint loops through SF Bay. + fleetOnce.Do(func() { + fleet.ships = [3]shipState{ + { // Golden Gate → Alcatraz → Pier 39 → back out + waypoints: [][2]float64{ + {-122.478, 37.819}, {-122.423, 37.827}, + {-122.410, 37.809}, {-122.423, 37.827}, + }, + speed: 0.03, + }, + { // Oakland → Treasure Island → Angel Island → loop + waypoints: [][2]float64{ + {-122.330, 37.795}, {-122.370, 37.823}, + {-122.432, 37.860}, {-122.370, 37.823}, + }, + speed: 0.02, + }, + { // Sausalito → Pier 39 ferry route + waypoints: [][2]float64{ + {-122.480, 37.859}, {-122.435, 37.840}, + {-122.410, 37.809}, {-122.435, 37.840}, + }, + speed: 0.025, + }, + } + // Set initial positions. + for i := range fleet.ships { + s := &fleet.ships[i] + s.lng = s.waypoints[0][0] + s.lat = s.waypoints[0][1] + s.wpIdx = 1 + } + go func() { + for { + time.Sleep(time.Second) + fleet.mu.Lock() + for i := range fleet.ships { + fleet.ships[i].tick() + } + fleet.mu.Unlock() + } + }() + }) + v.Page("/", func(c *via.Context) { m := maplibre.New(c, maplibre.Options{ Style: "https://demotiles.maplibre.org/style.json", @@ -75,24 +170,27 @@ func main() { }, }) - // Custom SVG container ship marker (static) - m.AddMarker("ship", maplibre.Marker{ - LngLat: maplibre.LngLat{Lng: -122.38, Lat: 37.80}, - Element: ``, - Anchor: "center", - Rotation: 45, - Popup: &maplibre.Popup{ - Content: "MSC Adriatica
Container vessel — heading NE
", - }, - }) + // Animated container ships following waypoint routes + shipNames := [3]string{"MSC Adriatica", "Evergreen Harmony", "Maersk Aurora"} + type shipSignals struct{ lng, lat *via.Signal } + var ships [3]shipSignals + + fleet.mu.RLock() + for i, s := range fleet.ships { + ships[i].lng = c.Signal(s.lng) + ships[i].lat = c.Signal(s.lat) + m.AddMarker(fmt.Sprintf("ship-%d", i), maplibre.Marker{ + LngSignal: ships[i].lng, + LatSignal: ships[i].lat, + Element: shipSVG, + Anchor: "center", + Rotation: s.heading(), + Popup: &maplibre.Popup{ + Content: fmt.Sprintf("%s", shipNames[i]), + }, + }) + } + fleet.mu.RUnlock() // Custom SVG vehicle marker — reads shared Go state vehicleLng := c.Signal(-122.43) @@ -113,6 +211,14 @@ func main() { vehicle.mu.RUnlock() vehicleLng.SetValue(lng) vehicleLat.SetValue(lat) + + fleet.mu.RLock() + for i, s := range fleet.ships { + ships[i].lng.SetValue(s.lng) + ships[i].lat.SetValue(s.lat) + } + fleet.mu.RUnlock() + c.SyncSignals() })