From 38eee24832fe6da4f135cae455881ab97953b23a Mon Sep 17 00:00:00 2001 From: Gene Pasquet Date: Sat, 18 Apr 2026 02:47:10 +0100 Subject: Refresh docs and re-indent --- docs/tweens.org | 432 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 307 insertions(+), 125 deletions(-) (limited to 'docs/tweens.org') diff --git a/docs/tweens.org b/docs/tweens.org index 11bf510..d1ee966 100644 --- a/docs/tweens.org +++ b/docs/tweens.org @@ -1,132 +1,314 @@ -#+title: Tweens -#+author: Downstroke Contributors +#+TITLE: Tweens -* Overview +Tweens smoothly interpolate numeric entity properties over time: an +=#:x= that slides, a =#:y= that bobs, a custom numeric field that ramps +up for use inside your =update:= hook. You build a tween with +=make-tween=, attach it to an entity under the =#:tween= key, and the +engine advances it for you every frame. When it finishes, it removes +itself. -The =downstroke-tween= module interpolates **numeric** entity properties over wall-clock time. It is **decoupled** from the engine: you create tween values, call =tween-step= each frame from your =update:= hook, and store the returned entity back into the scene. +This doc covers the =downstroke-tween= module: =make-tween=, the +=step-tweens= pipeline step, and every easing curve shipped in the +=*ease-table*=. -Durations and delays are in **milliseconds**, matching the =dt= argument to =update:=. +* The minimum you need -* Import +A one-shot slide to the right, 500 ms, linear ease: -#+begin_src scheme +#+BEGIN_SRC scheme (import downstroke-tween) -#+end_src -* Core API - -** ~make-tween~ - -#+begin_src scheme -(make-tween entity #!key props duration (delay 0) ease - (on-complete #f) (repeat 0) (yoyo? #f)) -#+end_src - -| Keyword | Meaning | -|---------+---------| -| ~props~ | Alist =((#:x . 200) (#:y . 40))= — keyword keys, numeric targets | -| ~duration~ | Positive integer, milliseconds of interpolation (after ~delay~) | -| ~delay~ | Non-negative integer ms before interpolation starts (first cycle only) | -| ~ease~ | Easing symbol (see table below) or ~(lambda (t) ...)= with ~t~ in $[0,1]$ | -| ~on-complete~ | Optional ~(lambda (entity) ...)=, called **once** when the tween fully ends (not called with ~repeat: -1~) | -| ~repeat~ | ~0~ = play once (default), ~N~ = replay N additional times, ~-1~ = loop forever | -| ~yoyo?~ | ~#f~ (default) = replay same direction, ~#t~ = reverse direction each cycle | - -Start values are captured from ~entity~ at construction time. While the tween runs, intermediate values may be **inexact** (flonums) even if starts and ends are integers. - -*** Repeat and yoyo - -~repeat:~ controls how many extra times the tween replays after the initial play. ~yoyo?: #t~ swaps start and end values on each cycle, creating a ping-pong effect. - -| ~repeat~ | ~yoyo?~ | Behavior | -|------+-------+----------| -| ~0~ | either | Play once and finish (default) | -| ~1~ | ~#f~ | Play forward twice, then finish | -| ~1~ | ~#t~ | Play forward, then backward, then finish | -| ~-1~ | ~#f~ | Play forward forever | -| ~-1~ | ~#t~ | Ping-pong forever | - -~delay:~ only applies before the first cycle. ~on-complete~ fires once when the last repeat finishes; it never fires with ~repeat: -1~. Overflow time from a completed cycle carries into the next cycle for smooth transitions. - -** ~tween-step~ - -#+begin_src scheme -(tween-step tween entity dt) -#+end_src - -Returns ~(values new-tween new-entity)~. Advance time by ~dt~ (ms). Before ~delay~ elapses, ~entity~ is unchanged. After completion, further steps return the same values (idempotent). When the tween completes, ~on-complete~ runs with the **final** entity (targets applied), then the callback slot is cleared. With ~repeat:~ / ~yoyo?:~, the tween automatically resets for the next cycle. - -** ~tween-finished?~ / ~tween-active?~ - -Predicates on the tween struct. - -** ~step-tweens~ (pipeline step) - -#+begin_src scheme -(step-tweens entity scene dt) -#+end_src - -Auto-advances ~#:tween~ on an entity. If the entity has no ~#:tween~ key (guard fails), returns the entity unchanged. When the tween finishes (no more repeats), ~#:tween~ is removed from the entity. Respects ~#:skip-pipelines~: skipped when ~'tweens~ is in the list. - -~step-tweens~ is part of ~default-engine-update~ and runs automatically each frame. No manual wiring is needed — just attach a ~#:tween~ to an entity and the engine handles the rest. - -** Entity key: ~#:tween~ - -Attach a tween directly to an entity for automatic advancement by ~step-tweens~: - -#+begin_src scheme -(list #:type 'platform - #:x 100 #:y 300 #:width 48 #:height 16 - #:solid? #t #:immovable? #t #:gravity? #f - #:tween (make-tween (list #:x 100) - props: '((#:x . 300)) - duration: 3000 ease: 'sine-in-out - repeat: -1 yoyo?: #t)) -#+end_src - -The platform ping-pongs between x=100 and x=300 forever. No manual tween management needed. - -* Easing - -Each ease maps normalized time ~t ∈ [0,1]~ to an interpolation factor (usually in ~[0,1]~; ~back-out~ may exceed ~1~ briefly). - -| Symbol | Procedure | -|--------|-----------| -| ~linear~ | ~ease-linear~ | -| ~quad-in~, ~quad-out~, ~quad-in-out~ | quadratic | -| ~cubic-in~, ~cubic-out~, ~cubic-in-out~ | cubic | -| ~sine-in-out~ | smooth sine | -| ~expo-in~, ~expo-out~, ~expo-in-out~ | exponential | -| ~back-out~ | overshoot then settle (Robert Penner–style) | - -** ~ease-named~ / ~ease-resolve~ - -~ease-named~ turns a symbol into a procedure. ~ease-resolve~ accepts a symbol or procedure (identity for procedures) for use in custom tooling. - -All easing procedures are exported if you want to compose curves manually. - -* Order of operations with physics - -Tweens usually **fight** velocity and gravity if both update ~#:x~ / ~#:y~. Typical pattern: - -1. Set the entity’s ~#:skip-pipelines~ to skip integration steps you do not want (see [[physics.org][Physics]]). -2. Run ~tween-step~ for that entity. -3. Run your normal physics pipeline (collisions can still run). - -Clear ~#:skip-pipelines~ in ~on-complete~ when the tween ends. - -Example skip list for “kinematic shove” while keeping tile collisions: - -#+begin_src scheme -(entity-set player #:skip-pipelines - '(jump acceleration gravity velocity-x velocity-y)) -#+end_src - -* Demo - -=bin/demo-tweens= (source =demo/tweens.scm=) shows one row per easing and a crate that tweens horizontally while integration is skipped and tile resolution still runs. - -* Limitations (current version) - -- Single segment per tween (no built-in chains or sequences). -- Numeric properties only. +(let ((e (entity-set (make-entity 0 100 16 16) #:type 'slider))) + (entity-set e #:tween + (make-tween e props: '((#:x . 300))))) +#+END_SRC + +=make-tween= reads current values off the entity you pass in (starts), +stores your =props= as the targets (ends), and returns an opaque tween +struct. Put that struct on the entity under =#:tween= and the engine's +default update pipeline will advance it — no further wiring. + +No entity key? No problem. =make-tween= treats any missing source key +as =0= (see =entity-ref= in the source), so tweening a custom numeric +key that isn't on the entity yet starts from zero. + +* Core concepts + +** Creating a tween + +=make-tween= signature (from [[file:../tween.scm][tween.scm]]): + +#+BEGIN_SRC scheme +(make-tween entity #!key + props ; alist of (#:key . target-number) — REQUIRED + (duration 500) ; ms, must be a positive integer + (delay 0) ; ms before interpolation starts + (ease 'linear) ; symbol (see ease table) or a procedure + (on-complete #f) ; (lambda (entity) ...) called once at end + (repeat 0) ; 0 = no repeats, -1 = infinite, N = N more cycles + (yoyo? #f)) ; swap starts/ends on every repeat +#+END_SRC + +- =entity= is the entity the tween is about to animate. Its current + values for every key in =props= are captured as the starting + values — so call =make-tween= /after/ you've built the entity, not + before. +- =props= must be a non-empty alist whose keys are keywords: + =((#:x . 200) (#:y . 40))=. Any numeric entity key is valid — the + standard =#:x= / =#:y= / =#:width= / =#:height=, a physics field like + =#:vx=, or a custom key you inspect inside your own hooks. +- =ease= accepts either a symbol from the ease table or a raw + procedure of signature =(number -> number)= mapping + \([0,1] \to [0,1]\). +- =repeat= counts /additional/ cycles after the first. =repeat: 2= plays + three times total. +- =yoyo?= only matters when =repeat= is non-zero. It flips starts and + ends on each cycle so the tween plays forward, backward, forward, etc. + +A full call: + +#+BEGIN_SRC scheme +(make-tween player + props: '((#:x . 200) (#:y . 40)) + duration: 800 + delay: 100 + ease: 'cubic-in-out + on-complete: (lambda (e) (print "arrived")) + repeat: 0 + yoyo?: #f) +#+END_SRC + +Violating the contracts — duration not a positive integer, props not a +non-empty alist, a non-keyword key, a bad =repeat= value — raises an +error immediately from =make-tween=. + +** The tween lifecycle + +Attach a tween by storing it under =#:tween= on an entity. The engine's +default update pipeline runs =step-tweens= as its first per-entity +step (see [[file:../engine.scm][engine.scm]], =default-engine-update=): + +#+BEGIN_SRC scheme +(chain scene + (scene-map-entities _ (cut step-tweens <> <> dt)) + (scene-map-entities _ (cut apply-acceleration <> <> dt)) + ;; ... +) +#+END_SRC + +=step-tweens= is a =define-pipeline= step with the pipeline name +=tweens=. On each frame it: + +1. Looks at =#:tween= on the entity. If it's =#f= (or absent), it + returns the entity untouched. +2. Advances the tween by =dt= milliseconds. If the tween is still in + its =delay= window, nothing interpolates yet. +3. For each =(key . target)= pair in =props=, linearly interpolates + between the start and target using the eased progress factor, and + writes the result back with =entity-set=. +4. When the tween completes (progress reaches 1.0): + - If =repeat= is 0, invokes the =on-complete= callback (once) with + the final entity, and returns an entity with =#:tween= cleared + to =#f=. + - Otherwise, it decrements =repeat= and starts a new cycle. If + =yoyo?= is true, it swaps starts and ends so the next cycle plays + backward. + +The "clear to =#f=" behaviour is hard-coded in =step-tweens= (see +[[file:../tween.scm][tween.scm]]): + +#+BEGIN_SRC scheme +(if (tween-finished? tw2) + (entity-set ent2 #:tween #f) + (entity-set ent2 #:tween tw2)) +#+END_SRC + +So you never have to clean up finished tweens — just check whether +=#:tween= is =#f= if you need to know whether one's running. + +Note: =on-complete= fires only when the tween truly ends (=repeat= +exhausted), not on every cycle of a repeating tween. For an infinite +tween (=repeat: -1=), =on-complete= never fires. + +** Easing functions + +The ease symbol you pass to =make-tween= is looked up in =*ease-table*= +and resolved to a procedure. The full table, copied from +[[file:../tween.scm][tween.scm]]: + +| symbol | shape | +|---------------+---------------------------------------------| +| =linear= | straight line, no easing | +| =quad-in= | slow start, quadratic | +| =quad-out= | slow end, quadratic | +| =quad-in-out= | slow start and end, quadratic | +| =cubic-in= | slow start, cubic (sharper than quad) | +| =cubic-out= | slow end, cubic | +| =cubic-in-out= | slow start and end, cubic | +| =sine-in-out= | smooth half-cosine | +| =expo-in= | slow start, exponential | +| =expo-out= | slow end, exponential | +| =expo-in-out= | slow start and end, exponential | +| =back-out= | overshoots past the target then settles | + +Passing an unknown symbol raises =ease-named: unknown ease symbol= +immediately from =make-tween=. + +You can also pass a /procedure/ directly as =ease:=. =ease-resolve= +accepts any procedure of signature =(number -> number)= mapping the +normalised time \(t \in [0,1]\) to an eased factor. A trivial example: + +#+BEGIN_SRC scheme +;; A "step" ease: snap halfway through. +(make-tween e + props: '((#:x . 100)) + duration: 400 + ease: (lambda (t) (if (< t 0.5) 0.0 1.0))) +#+END_SRC + +The returned factor is used for a straight linear interpolation +between the start and target, so values outside =[0,1]= are legal and +will overshoot or undershoot (that's exactly how =back-out= works). + +** Interaction with the pipeline + +=step-tweens= runs as a =define-pipeline= step named =tweens=. You can +opt out per-entity by adding the step name to =#:skip-pipelines=: + +#+BEGIN_SRC scheme +(entity-set entity #:skip-pipelines '(tweens)) +#+END_SRC + +While that's set, any =#:tween= on the entity is frozen. Remove the +symbol from the list to resume. + +Because =step-tweens= is the /first/ step in =default-engine-update=, +a tween that writes to =#:x= or =#:y= runs /before/ physics. That +matters in practice: + +- If you tween an entity's =#:x= and the physics tile-collision step + decides to snap the entity back out of a wall that same frame, the + tween's write is overwritten for the frame. This is a real + scenario — see [[file:physics.org][physics.org]] for the full + pipeline order. +- For purely-visual tweens (decorative entities, menu widgets, the demo + boxes in =bin/demo-tweens=) you usually also want =#:gravity?= and + =#:solid?= off so physics leaves the entity alone. + +If you need a tween to "win" against physics, either disable the +physics steps you don't want on that entity via =#:skip-pipelines=, or +set =#:gravity?= and =#:solid?= to =#f= so the physics steps are no-ops. + +* Common patterns + +** One-shot move-to-position + +Slide the camera-locked player to a checkpoint, then print a message: + +#+BEGIN_SRC scheme +(entity-set player #:tween + (make-tween player + props: '((#:x . 640) (#:y . 200)) + duration: 600 + ease: 'cubic-in-out + on-complete: (lambda (e) + (print "checkpoint reached")))) +#+END_SRC + +=on-complete= receives the /final/ entity (with =props= written to +their targets). The engine takes care of clearing =#:tween= to =#f= +afterwards. + +** Yoyoing bob animation + +An enemy that hovers up and down forever: + +#+BEGIN_SRC scheme +(entity-set bat #:tween + (make-tween bat + props: `((#:y . ,(+ (entity-ref bat #:y 0) 8))) + duration: 500 + ease: 'sine-in-out + repeat: -1 + yoyo?: #t)) +#+END_SRC + +This is exactly the pattern the tweens demo uses +(=+ease-duration+= = 2600 ms, =repeat: -1=, =yoyo?: #t= — one entity +per ease, all bouncing in parallel). Because =repeat= is =-1=, the +tween never finishes and =#:tween= stays set for the lifetime of the +entity. + +** Chaining actions with =on-complete= + +The callback is invoked with the entity in its /final/ interpolated +state. The signature is just =(lambda (entity) ...)= and — importantly — +its return value is /discarded/. Look at =tween-complete= in +[[file:../tween.scm][tween.scm]]: the callback runs for side-effects +only, and the entity returned to the pipeline is the unmodified +=final=. So =on-complete= is for triggering world-level actions +(playing a sound, logging, enqueueing a command, mutating a global +flag), not for returning a new version of the entity. + +A "slide off screen, then log" example: + +#+BEGIN_SRC scheme +(entity-set popup #:tween + (make-tween popup + props: '((#:y . -40)) + duration: 400 + ease: 'quad-out + on-complete: (lambda (final) + (print "popup dismissed: " (entity-ref final #:id #f))))) +#+END_SRC + +=on-complete= fires once, only when =repeat= is exhausted — for a +=repeat: -1= tween it never fires. + +To chain a /new/ tween after this one, start it from the user =update:= +hook by inspecting whether =#:tween= is =#f= on the entity (remember, +=step-tweens= clears it automatically on completion). That keeps the +chain inside the normal pipeline data flow instead of trying to +smuggle a new tween through the discarded-callback-return. + +** Easing preview recipe + +The easing curves are easiest to compare side-by-side. =bin/demo-tweens= +renders one horizontal-sliding box per ease symbol, labelled with its +name. If you want to eyeball a single curve, copy the per-entity +recipe from the demo: + +#+BEGIN_SRC scheme +;; One entity, one ease. Ping-pongs forever between x=20 and x=140. +(define (make-ease-entity ease-sym y rgb) + (let* ((left 20) + (right (+ left 120)) + (base (plist->alist (list #:x left #:y y)))) + (plist->alist + (list #:type 'tween-demo #:x left #:y y + #:width 14 #:height 14 + #:vx 0 #:vy 0 #:gravity? #f #:solid? #f + #:color rgb + #:ease-name ease-sym + #:tween (make-tween base props: `((#:x . ,right)) + duration: 2600 + ease: ease-sym + repeat: -1 yoyo?: #t))))) +#+END_SRC + +See the full source — including the rendering loop and the label +layout — in =demo/tweens.scm=. The demo is the canonical visual +reference for every ease name. + +*Tweens demo* — run with =bin/demo-tweens=, source in =demo/tweens.scm=. + +* See also + +- [[file:guide.org][guide.org]] — getting started with =make-game= and + the main loop. +- [[file:entities.org][entities.org]] — the entity alist model, + =entity-ref=, =entity-set=, and =#:skip-pipelines=. +- [[file:physics.org][physics.org]] — the rest of the default update + pipeline that runs right after =step-tweens=. +- [[file:animation.org][animation.org]] — the complementary sprite-frame + animation system; frequently paired with tweens on the same entity. -- cgit v1.2.3