#+TITLE: Tweens 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. This doc covers the =downstroke-tween= module: =make-tween=, the =step-tweens= pipeline step, and every easing curve shipped in the =*ease-table*=. * The minimum you need A one-shot slide to the right, 500 ms, linear ease: #+BEGIN_SRC scheme (import downstroke-tween) (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.