aboutsummaryrefslogtreecommitdiff
path: root/docs/tweens.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tweens.org')
-rw-r--r--docs/tweens.org432
1 files changed, 307 insertions, 125 deletions
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.