summaryrefslogtreecommitdiff
path: root/animise.lisp
blob: 1de9f33d25929848446ebbc943cea8e7d8022fc3 (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
;;;; 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 (error "Must supply a start time."))
   (duration
    :reader duration
    :initarg :duration
    :initform 1000.0) ; 1 second
   (ease-fn
    :initarg :ease-fn
    :initform #'linear)
   (start-val
    :accessor start-val
    :initarg start-val
    :initform (error "Must supply a start value."))
   (end-val
    :accessor end-val
    :initarg :end-val
    :initform (error "Must supply an end value."))
   (effector
    :initarg :effector
    :initform (error "Must supply an effector function"))))

;;; 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 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 &keys (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 run-tween ((tween tween) time)
  (with-slots (start-time duration ease start-val end-val effector) tween
    (when (>= time start-time)
      (funcall effector
               (+ start-val
                  (funcall ease
                           start-time
                           duration
                           time
                           (- end-val start-val)))))))

;;; 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)
  ;; 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)))

  (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)
       ;; 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 (- now 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)))

;;; Sequencing operations