Chapter 8 - Motion Curves
The ramp from chapter 7 gives a program a clock of its own: a byte that walks from 0 to its last step, once per frame, marked changed the whole way. Point a compute block at it and a position starts moving. Divide a 64-step ramp by eight and a dot crosses the matrix:
compute TrackComet
on Travel
updates CometX
begin
ld a,(Travel)
srl a
srl a
srl a ; column = Travel / 8
ld (CometX),a
end
Sixty-four frames, eight columns, eight frames on each: the crossing is perfectly even. Watch it and you see a position being updated on schedule.
Motion in the world keeps a different schedule. A thrown ball leaves the hand fast and slows as it climbs; a landing bird glides in; a released spring shoots past its rest point and settles back. On an 8-wide matrix the columns are fixed, so all of that character lives in the timing: how many frames the dot dwells on each column before moving on. Equal steps show position. Shaped steps show motion.
Shaping the steps by hand means arithmetic every frame - squares,
roots, eight-bit fractions - inside a frame that also has input and
drawing to run. Glimmer computes the shape once, at build time. A
curve declaration names a motion shape, and the compiler turns it
into a table of bytes inside the generated program. Shaping the flight
costs your block one table read per frame.
Comet
This chapter’s program is Comet. The dot rests at the left edge of the middle row. GO launches it across the row: quick off the pad, slowing all the way, gliding in to land near the right edge. GO again - anytime, even mid-flight - launches it again.
program Comet
platform tec1g-mon3
display matrix8x8
state CometX : byte = 0 changed
pulse Go
pulse Landed
ramp Travel : byte steps 64 -> Landed
curve Glide ease_out steps 64 from 0 to 6
bind key KEY_GO rising -> Go
effect Launch
on Go
updates Travel
begin
xor a
ld (Travel),a ; rewind the ramp: the flight begins
end
compute TrackComet
on Travel
updates CometX
begin
ld a,(Travel)
ld e,a
ld d,0
ld hl,Curve_Glide
add hl,de ; HL points at this step's position
ld a,(hl)
ld (CometX),a
end
render DrawComet
on CometX
begin
call FbClear
ld a,(CometX)
ld b,a
ld c,3
ld a,COLOR_WHITE
call FbPlot
end
One declaration is new:
curve Glide ease_out steps 64 from 0 to 6
Read it aloud: Glide is an ease-out curve, 64 steps, running from 0
to 6. At build time, Glimmer traces an ease-out path - fast at first,
slowing toward the end - and writes the 64 positions it passes through
into the program as a table of bytes named Curve_Glide. A curve is a
resource: it declares data, so it owns no cell, carries no change
flag, and adds no work to the frame.
TrackComet is where the table earns its place. Each frame of a
flight the ramp steps, Travel is marked changed, and the compute
runs. Its body indexes the table with the current step:
ld a,(Travel)
ld e,a
ld d,0
ld hl,Curve_Glide
add hl,de ; HL points at this step's position
ld a,(hl)
ld (CometX),a
The step goes into DE, the table’s base into HL, and the sum points at the byte holding this step’s column. The whole ease-out calculation happened before the program ever ran; the frame pays for one load.
Every ramp names an arrival pulse, so Landed fires as the flight
ends. Comet lands quietly for now; chapter 9 puts sound on moments
like this one.
Build it and press GO. The launch is brisk and the landing is soft, and between the two the dot slows column by column - motion with a shape, from a compute block seven instructions long.
What a curve declares
The full form:
curve <Name> <preset> steps <N> from <A> to <B>
steps runs from 2 to 256 and sets the table’s length: one byte per
step. from and to are byte values, the positions at the start and
end of the run; left out, they default to 0 and steps - 1, so
curve Fade linear steps 16 counts 0 through 15. The preset names the
shape:
linear- equal spacing, the straight line the other six bend.ease_in- sets off slowly, arrives at speed.ease_out- sets off at speed, arrives slowly.ease_in_out- gentle at both ends, quick through the middle.sine- half a cosine wave; ease_in_out with rounder shoulders.overshoot- runs pastto, then settles back onto it.anticipation- pulls back behindfrombefore setting off.
The last two step outside the from..to run on purpose; that is
their character. Table values clamp to the byte range 0 to 255, so
give them headroom: an overshoot aimed at to 6 borrows column 7 for
its peak, and an anticipation launched from 1 has room to dip to 0.
Glide lands on column 6 for the same reason - Comet is about to grow
two more curves over the same run, and one of them springs.
The ramp is the clock, the curve is the path
Comet’s motion is two declarations sized to each other:
ramp Travel : byte steps 64 -> Landed
curve Glide ease_out steps 64 from 0 to 6
The ramp keeps time. Its cell answers one question - how far along is
the flight? - and it knows nothing about columns. The curve holds the
geometry. Its table answers the other question - where does the flight
pass at each step? - and it knows nothing about frames. TrackComet
joins them, clock in, path out, one byte read per step.
Matching steps counts are the whole coupling. Sixty-four ramp steps
index sixty-four table entries; the final step reads the final byte,
so the dot stands on its landing column on the very frame Landed
fires.
The pairing gives you two independent dials. Duration is the steps
count in frames: raise both numbers to 128 and the same glide takes
twice as long. Feel is the preset name: change ease_out to sine
and rebuild, and the same 64-frame flight arrives with a different
character. Either way, the block joining them stays untouched.
The table in the generated file
Open comet.main.asm and find the resource the declaration became:
; --- curve resources ---
.align 256
Curve_Glide:
.db 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3
.db 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4
.db 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6
.db 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6
Sixty-four bytes, one per ramp step, and the ease-out is readable straight off the rows: the dot spends three frames on column 0 and nineteen on column 6, each dwell longer than the last, give or take a rounding step. The declaration’s whole runtime cost sits in front of you - 64 bytes of data.
.align 256, the line above the label, moves the assembler to the
next 256-byte page boundary before laying the table down, so
Curve_Glide starts at an address whose low byte is zero: a
page-aligned table. Page alignment pays off in the indexing. A curve
holds at most 256 bytes, every entry lives in the base’s own page, and
the base’s low byte is zero - so base plus step collapses into
writing the step straight into L:
ld a,(Travel)
ld l,a ; page-aligned table: the step is the low byte
ld a,(hl)
With a table base already in HL, three instructions read the path, DE untouched. The final version of Comet leans on this.
Switching curves in flight
The point of naming a motion is choosing between motions. Here is the
full Comet: three curves over the same run, a Preset fact naming the
current one, and PLUS cycling through them.
program Comet
platform tec1g-mon3
display matrix8x8
state CometX : byte = 0 changed
state Preset : byte = 0 changed
pulse Go
pulse Landed
pulse Switch
ramp Travel : byte steps 64 -> Landed
curve Straight linear steps 64 from 0 to 6
curve Glide ease_out steps 64 from 0 to 6
curve Spring overshoot steps 64 from 0 to 6
bind key KEY_GO rising -> Go
bind key KEY_PLUS rising -> Switch
effect Launch
on Go
updates Travel
begin
xor a
ld (Travel),a ; rewind the ramp: the flight begins
end
effect NextPreset
on Switch
updates Preset
begin
ld a,(Preset)
inc a
cp 3
jr c,_store ; past the last preset: back to the first
xor a
_store:
ld (Preset),a
end
compute TrackComet
on Travel, Preset
updates CometX
begin
ld hl,Curve_Straight
ld a,(Preset)
or a
jr z,_index
ld hl,Curve_Glide
dec a
jr z,_index
ld hl,Curve_Spring
_index:
ld a,(Travel)
ld l,a ; page-aligned table: the step is the low byte
ld a,(hl)
ld (CometX),a
end
render DrawComet
on CometX
begin
call FbClear
ld a,(CometX)
ld b,a
ld c,3
ld a,COLOR_WHITE
call FbPlot
end
render ShowPreset
on Preset
begin
ld a,(Preset)
inc a ; show 1..3, matching the list above
ld l,a
ld h,0
call HudWriteU16
end
The three curve lines differ in one word each. Straight is the
reference, Glide the soft arrival, Spring the overshoot. Preset
is an ordinary fact holding 0, 1 or 2, and changed puts its number
on the seven-segment display from the first frame; NextPreset cycles
it, with cp 3 carrying the curve count. ShowPreset adds one so the
display reads 1 to 3, matching the order the curves are declared in.
TrackComet now starts with a selection:
ld hl,Curve_Straight
ld a,(Preset)
or a
jr z,_index
ld hl,Curve_Glide
dec a
jr z,_index
ld hl,Curve_Spring
_index:
A chain of loads leaves HL holding the base of whichever table
Preset names, and from _index the page-alignment idiom finishes
the job. Every curve table is page-aligned, so one ld l,a serves all
three; picking a motion at runtime costs the frame a few loads and
branches before the same single table read.
Spring is worth reading in the generated file:
.align 256
Curve_Spring:
.db 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5
.db 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7
.db 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6
.db 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6
Halfway through the flight the dot climbs onto column 7 - the spare column the headroom rule kept free - holds it for thirteen frames, then settles back onto 6 for the landing. All three tables end on 6, so pressing PLUS while the comet rests redraws it where it stands.
On the very first frame, Preset is already changed, so TrackComet
runs before any launch, reads the idle ramp’s final step, and the dot
appears on its landing column, waiting for GO.
TrackComet triggers on Travel, Preset, so a switch lands
immediately: press PLUS during a flight and the dot jumps to the new
path’s position at the same step, then finishes the journey on the new
curve. Launch under preset 1 and you are watching this chapter’s
opening motion again, equal dwell on every column. Preset 2 glides in;
preset 3 springs past and settles. The difference between the three is
one word in a declaration and zero instructions in the blocks.
Summary
curve <Name> <preset> steps <N> from <A> to <B>declares a build-time byte table. Presets:linear,ease_in,ease_out,ease_in_out,sine,overshoot,anticipation;stepsruns 2 to 256;fromandtodefault to 0 andsteps - 1.- A curve is a resource: no cell, no change flag, no frame work. The
name compiles to a page-aligned AZM table
Curve_<Name>, emitted as.align 256and.dbrows you can read the motion from. - The ramp is the clock, the curve is the path: a compute block joins
them with one table read per step. Matching
stepscounts line the table up with the ramp, and the final step lands ontoas the arrival pulse fires. - Duration is the steps count in frames; feel is the preset name. Each changes in a declaration, and the joining block stays untouched.
overshootruns pasttoandanticipationdips behindfrom; table values clamp to 0..255, so leave headroom at the borrowed end.- Page alignment collapses indexing to
ld l,a: with a table base in HL, the step is the low byte. - A state cell choosing between table bases switches motion at runtime, by name.
The comet flies well. Next it earns a body, a voice, and a scoreboard: shapes, sound, and the board’s displays.