aboutsummaryrefslogtreecommitdiff
path: root/docs/tweens.org
blob: d1ee966cdaf3ae0f561904d131dcdb509c546770 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#+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.