summaryrefslogtreecommitdiff
path: root/animise.lisp
blob: 9f2eb0ce02a8fdba545280e06b093a6ff6811270 (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
;;;; animise.lisp

(in-package #:animise)

;;; Interface generices


;; In addition to the following, both a `DURATION' method and
;; a `START-TIME' setf-able method are implemented for each class. 

(defgeneric run-tween (tween time))

;;; TWEENS

;; A TWEEN is effectively a function. A tween is used to produce a sequence of
;; values over time the purpose of animating an object.

;; START-TIME and DURATION arguments are a representation of time and must use
;; the same units. START-TIME must be supplied on TWEEN instantiation, and is
;; used to record when the tween begins running.

;; DELTA-VAL is a number, the amount by which the animated object's target
;; property should have changed by the time the animation is over.

;; Different EASE functions may be supplied to modulate the way that sequence is
;; produced.  EASE is the heart of the TWEEN's behavior.

;; Every TWEEN instance must have an EFFECTOR, which should be a closure that
;; accepts a single argument. The purpose of the EFFECTOR is to apply the values
;; generated by the TWEEN to some object.

(defclass tween ()
  ((start-time
    :accessor start-time
    :initarg :start-time
    :initform 0)
   (duration
    :reader get-duration
    :initarg :duration
    :initform 1000.0) ; 1 second
   (ease-fn
    :initarg :ease-fn
    :initform #'linear)
   (start-val
    :initform nil)
   (end-val
    :accessor end-val
    :initarg :end-val
    :initform (error "Must supply an end value."))
   (target
    :initarg :target
    :initform (error "Must have a target"))
   (rounding
    :initarg :rounding
    :initform t )
   (setter)
   (accessor
    :initarg :accessor
    :initform (error "Must supply an accessor function"))
   (on-complete
    :accessor on-complete
    :initarg :on-complete
    :initform nil)))

(defmethod initialize-instance :after ((tween tween) &key)
  (with-slots (getter setter accessor) tween
    (setf setter (eval `(function (setf ,accessor))))))


;;; TWEEN-SEQ combines tweens to run one after another.

(defclass tween-seq ()
  ((tweens
    :accessor tweens
    :initarg :tweens
    :initform (error "empty tween sequences are disallowed."))
   (loop-mode
    :accessor loop-mode
    :initarg :loop-mode
    :initform nil)))  ; :looping :reflecting (:looping max n) (:reflecting max n)

;;; TWEEN-GROUP

(defclass tween-group ()
  ((members
    :accessor members
    :initarg :members
    :initform nil
    :type list)))

;;; Some functions that use the protocol defined by the generics

(defun pause (duration &optional (start 0))
  (make-instance 'tween :target (list 0) :start-time start :duration duration
                 :accessor 'car :end-val duration :rounding nil))

(defun animate (target acc end &key (start 0) (ease #'linear) (rounding t) (duration 1000))
  (make-instance 'tween
                 :target target
                 :start-time start
                 :accessor acc
                 :end-val end
                 :ease-fn ease
                 :rounding rounding
                 :duration duration))

(defun in-sequence (t1 &rest tws)
  (let ((seq (make-instance 'tween-seq :tweens (cons t1 tws))))
    (correct-sequencing seq)
    seq))

(defun end-time (tween)
  "Some tweens dont have a duration ,and hence never end. NIL is returned to
  reflect this."
  (let-when (dur (duration tween))
            (+ (start-time tween) dur)))

(defun tween-finished-p (tween time)
  "Returns T if  TWEEN is done running."
  (let-when (end (end-time tween))
            (>= time end)))

(defun add-to-group (group tween &key (offset 0))
  "Adds TWEEN to GROUP. If TWEEN is the first tween added to GROUP, then TWEEN'S
   start time becomes the GROUP'S start time and the OFFSET is ignored.
   Otherwise, TWEEN's start time is set to the start time of the GROUP modified
   by OFFSET."
  (let-when (start-time (and (members group) (start-time group)))
    (setf (start-time tween)
          (+ offset start-time)))
  (push (members group) tween))


(defun as-group (tween &rest tweens)
  (make-instance 'tween-group :members (cons tween tweens)))

;;; Interface implementations for TWEEN class

(defmethod duration ((tween tween))
  (get-duration tween))

(defmethod run-tween ((tween tween) time)
  (with-slots (start-time duration rounding ease-fn start-val target end-val setter accessor) tween
    (when (>= time start-time)
      (when (null start-val)
        (setf start-val (funcall accessor target)))
      (let ((new-val
              (+ start-val
                 (funcall ease-fn
                          start-time
                          duration
                          time
                          (- end-val start-val)))))
        (funcall setter
                 (if rounding (round new-val) new-val)
                 target)))))

(defmethod run-tween :after ((tween tween) time)
  (when (and (on-complete tween) (tween-finished-p tween time))
    (funcall (on-complete tween))
    (setf (on-complete tween) nil)))

;;; Interface implementations for TWEEN-SEQ

(defmethod start-time ((ob tween-seq))
  (when (tweens ob)
    (start-time (car (tweens ob)))))

(defun correct-sequencing (seq)
  ;; A helper function that sets the start and stop time for each tween in a
  ;; tween sequence. It assumes the first tween is correctly configured.
  (when (tweens seq)
    (let ((end (end-time (car (tweens seq)))))
      (dolist (tween (cdr (tweens seq)))
        (setf (start-time tween) end)
        (setf end (end-time tween))))))


(defmethod (setf start-time) (val (ob tween-seq))
  (unless (tweens ob) (error "Cannot set start time of empty sequence."))
  (setf (start-time (car (tweens ob))) val)
  (correct-sequencing ob)
  val)


(defmethod duration ((ob tween-seq))
  "NIL means that the tween is infinitely looping."
  (unless (tweens ob) (error "Cannot determine the duration of an empty sequence."))
  (with-slots (tweens loop-mode) ob
    (match loop-mode
      (:looping nil)
      (:reflecting nil)
      ((list _ max _)
       (* max
          (reduce #'+ tweens :key #'duration :initial-value 0)))
      (nil
       (reduce #'+ tweens :key #'duration :initial-value 0)))))

(defun reset-child-loops (seq &optional reset-parent)
  ;; Resets LOOP-MODEs that have counts to whatever their initial count was.
  ;; Does the same for all tweens in the sequence that might themselves be
  ;; TWEEN-SEQ instances
  (dolist (sub (tweens seq))
    (when (typep sub 'tween-seq)
      (reset-child-loops seq)))
  (when reset-parent
    (with-slots (loop-mode) seq
      (when (consp loop-mode)
        (setf (nth 2 loop-mode)
              (nth 1 loop-mode))))))

;; TODO implmeent the reflecting tween behavior
(defun apply-looping (seq now)
  ;; Applies any looping in order to see if, after doing so, a runnable tween
  ;; becomes available.
  (with-slots (loop-mode) seq
    (match loop-mode

      (:looping
       ;; If you're simply looping, you need to make sure each non-infinite subloop
       ;; be reset.
       (reset-child-loops seq t)
       ;; then set the start time to right now and correct start times of the
       ;; subsequent loops
       (setf (start-time seq) now)
       (car (tweens seq)))

      ((list :looping _ n)
       (when (plusp n)
         (decf (nth 2 loop-mode))
         (reset-child-loops seq)
         (setf (start-time seq) now)
         (car (tweens seq)))) )))

      ;; (:reflecting
      ;;  (reverse-tween seq)
      ;;  (car (tweens seq)))

      ;; ((list :reflecting max n)
      ;;  (when (plusp n)
      ;;    (decf (nth 2 loop-mode))
      ;;    (reverse-tween seq)
      ;;    (car (tweens seq)))))))


(defmethod run-tween ((ob tween-seq) time)
  (let-when (tween (or
                    ;; find the first unfinished tween in the sequence
                    (find-if-not (lambda (tween)
                                   (tween-finished-p tween time))
                                 (tweens ob))
                    ;; otherwise apply any looping configuration on the sequence
                    ;; and apply the tween that results
                    (apply-looping ob time)))
            (run-tween tween time)))


;;; Interface Implementations for TWEEN-GROUP

(defmethod start-time ((ob tween-group))
  (loop :for tw :in (members ob) :minimizing (start-time tw)))

(defmethod (setf start-time) (val (ob tween-group))
  (Unless (members ob) (error "Can't setf the start time on an empty group"))
  (let* ((old-start-time (start-time ob))
         (offset (- val old-start-time)))
    (dolist (tween (members ob))
      (incf (start-time tween) offset))
    val))

(defmethod duration ((ob tween-group))
  (when (members ob)
    (let ((start-time (start-time ob))
          (end-time (loop :for tw :in (members ob) :maximizing (end-time tw))))
      (- end-time start-time))))

(defmethod run-tween ((ob tween-group) now)
  (dolist (tween (members ob))
    (run-tween tween now)))