Make the aircraft breathe, and send a ring out from each of them

Two things that move on their own, on the window and in the animated
pictures alike, and both turned off with a flag.

An aircraft swells from bright to dim and back.  At the top of the swell it
burns: drawn in a set of peak colours and wearing a halo, which is what a
phosphor does when the beam sits in one place a little too long.  The halo
is the aeroplane's own shape spread outward a pixel at a time rather than a
circle drawn round it -- the first attempt did draw a circle and it looked
exactly like what it was, a ring of dots -- so the glow has the shape of the
thing casting it.  It goes only where the picture was still empty, over the
ground and the grid and the background and never over another aircraft,
since a halo is what light does to the dark around a thing.

And a ring leaves each aircraft and travels outward, growing and dimming as
it goes.  What a radar repeater does, and what the eye reads as this thing
is transmitting -- which is exactly what an aeroplane on this picture is
doing, twice a second, and is how it got on the picture at all.  One ring at
a time per aircraft: a new one leaves as the last reaches the end of its
reach, so the sky has one ring per aeroplane rather than a stack of them to
read through.

Five settings, in a group of their own because the others were already at
eight: whether to pulse, how long a swell takes, whether to echo, how long
between rings, and how far a ring gets.  Both times are seconds of watching
rather than of flying, so the rhythm looks the same whatever speed an
evening is being run through.

Two decisions worth naming.  Each aircraft is offset by its own address, so
a sky full of them swells and rings separately rather than beating as one,
which would read as a display flashing rather than as a lot of separate
things transmitting; the offset comes from the address, so an aeroplane
keeps its rhythm from one frame to the next and from one drawing of the same
log to the next.  And only an aircraft still being heard pulses: one that
has gone quiet is already fading, and a thing that is fading and beating at
once says two contradictory things about itself.

The window can blend and its swell is continuous.  The animation cannot, a
GIF being indexed colour, so there the swell is the steps a palette allows
-- which family of colours the aeroplane is drawn from, and how far its halo
reaches, which is six between them and reads as a swell at a couple of
seconds a cycle.  The peak has sixteen colours of its own, sixteen being
exactly what was left of the palette once 255 is set aside as the
transparent index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
The Dust Council 2026-09-06 17:03:16 -07:00
parent 8ed01f991f
commit f01de4117f
10 changed files with 907 additions and 16 deletions

View file

@ -1919,3 +1919,120 @@ def test_nothing_in_the_window_is_drawn_over_the_flag(app):
assert is_flag(x, y), "the foot of the pole was painted over"
assert all(is_flag(x, y - up) for up in range(HOME_POLE)), \
"part of the pole was painted over"
# ---------------------------------------------------------------------------
# Pulsing aircraft, and the rings that leave them
# ---------------------------------------------------------------------------
def test_a_sky_draws_neither_effect_unless_it_is_asked_to():
plain = a_sky()
assert plain.pulse == 0.0 and plain.echo == 0.0
lively = Sky(pulse=2.2, echo=3.0, echo_reach=46)
assert lively.pulse == 2.2 and lively.echo == 3.0
def test_the_effect_settings_are_kept_inside_their_range():
quiet = Sky(pulse=-1.0, echo=-2.0, echo_reach=-5.0)
assert quiet.pulse == 0.0 and quiet.echo == 0.0 and quiet.echo_reach == 0.0
def _at_own_peak(sky, icao, want=0.5):
"""Wind the clock so this aircraft is `want` of the way round its pulse.
Each is offset by its own address, so a test cannot assume that half a
cycle on the clock is half a cycle for any particular aeroplane.
"""
from bandsaunter.flightmap import phase_of
rate = sky.pulse or sky.echo
sky.started = time.time() - rate * ((want - phase_of(icao)) % 1.0)
@qt
def test_an_aircraft_burns_at_the_top_of_its_pulse(app):
"""Brighter, and wearing a halo grown out of its own shape: what a
phosphor does when the beam sits in one place a little too long."""
from bandsaunter.livemap import SkyView
def drawn(want):
sky = a_sky(a_blip(), pulse=2.2)
_at_own_peak(sky, "A76154", want)
view = SkyView(sky)
view.show_ground = False
view.detail = 0 # the symbol alone
picture = _rendered(view)
return int(picture[:, :, :3].astype(int).sum())
assert drawn(0.5) > drawn(0.0), "it did not brighten at the top"
@qt
def test_a_pulse_turned_off_leaves_the_aircraft_alone(app):
"""Asked of the level rather than of the pixels: the header carries a
running clock, so two whole frames are never identical anyway."""
from bandsaunter.livemap import SkyView
off = SkyView(a_sky(a_blip()))
assert off._pulse_level("A76154") is None
on = SkyView(a_sky(a_blip(), pulse=2.2))
for want in (0.0, 0.25, 0.5, 0.75):
_at_own_peak(on.sky, "A76154", want)
level = on._pulse_level("A76154")
assert level is not None and 0.0 <= level <= 1.0
_at_own_peak(on.sky, "A76154", 0.5)
assert on._pulse_level("A76154") > 0.98, "it does not reach the top"
_at_own_peak(on.sky, "A76154", 0.0)
assert on._pulse_level("A76154") < 0.02, "nor the bottom"
@qt
def test_a_ring_leaves_an_aircraft_and_grows(app):
from bandsaunter.livemap import SkyView
def spread(want):
"""How far the ring has got from its aircraft.
Measured against the same view with the echo turned off, and only
within a box round the aeroplane: the header carries a running
clock and the box a running age, and neither is a ring.
"""
sky = a_sky(a_blip(), echo=3.0, echo_reach=60)
_at_own_peak(sky, "A76154", want)
view = SkyView(sky)
view.show_ground = False
view.detail = 0
with_it = _rendered(view)
sky.echo = 0.0
without = _rendered(view)
x, y = view.projection().xy(a_blip().latitude, a_blip().longitude)
top, left = max(0, y - 90), max(0, x - 90)
near = slice(top, y + 90), slice(left, x + 90)
differs = (with_it[near] != without[near]).any(axis=2)
ys, xs = np.where(differs)
if not len(xs):
return 0
return int(np.hypot(xs + left - x, ys + top - y).max())
early, late = spread(0.15), spread(0.8)
assert early > 0, "no ring at all"
assert late > early, f"the ring did not grow: {early} then {late}"
@qt
def test_the_window_keeps_asking_for_frames_while_anything_is_alive(app):
"""A pulse and an echo are never finished, so the window has to keep
repainting the way it does while a box is on its way somewhere."""
from bandsaunter.livemap import SkyView
lively = SkyView(a_sky(a_blip(), pulse=2.2))
lively.show_ground = False
_rendered(lively)
assert lively._glide_timer.isActive(), "it stopped asking for frames"
still = SkyView(a_sky(a_blip()))
still.show_ground = False
_rendered(still)
assert not still._glide_timer.isActive(), \
"it is repainting with nothing to animate"