1 /** bindings for tweens to common object properties to allow them to be interpolated */
2 
3 module re.util.tweens.tween;
4 
5 import re.util.tweens.tween_manager;
6 import re.util.tweens.ease;
7 import re.gfx.raytypes;
8 
9 public enum TweenState {
10     Running,
11     Paused,
12     Complete
13 }
14 
15 interface ITween {
16     @property TweenState state();
17     void update(float dt);
18     void start(bool attach = true);
19     void add_chain(ITween[] tw...);
20     @property ITween[] get_chain();
21     @property void delegate(ITween) callback();
22     void set_callback(void delegate(ITween) cb);
23 }
24 
25 /// represents a tween, to be used for easings/interpolation
26 class Tween(T) : ITween {
27     alias TRef = T*;
28     private TRef[] _datas;
29     public const(float[]) from;
30     public const(float[]) to;
31     public const(float) duration;
32     public const(float) delay;
33     public const(EaseFunction) ease;
34     private ITween[] _chain;
35     private float elapsed = 0;
36     private TweenState _state;
37     private void delegate(ITween) _callback;
38 
39     this(TRef[] data, T[] from, T[] to, float duration, EaseFunction ease, float delay = 0) {
40         import std.algorithm : map;
41         import std.array : array;
42 
43         this._datas = data;
44         this.from = from.map!(x => cast(float) x).array;
45         this.to = to.map!(x => cast(float) x).array;
46         this.duration = duration;
47         this.ease = ease;
48         this.delay = delay;
49     }
50 
51     @property TweenState state() {
52         return _state;
53     }
54 
55     @property ITween[] get_chain() {
56         return _chain;
57     }
58 
59     @property void delegate(ITween) callback() {
60         return _callback;
61     }
62 
63     public void set_callback(void delegate(ITween) cb) {
64         _callback = cb;
65     }
66 
67     public void update(float dt) {
68         if (_state == TweenState.Paused)
69             return;
70 
71         import std.algorithm.comparison : clamp;
72 
73         elapsed += dt;
74 
75         // check delay
76         if (elapsed < delay)
77             return;
78 
79         auto run_time = elapsed - delay;
80 
81         // clamp the elapsed time (t) value
82         auto t = clamp(run_time, 0, duration);
83 
84         // - update values
85         foreach (i, data; _datas) {
86             // get value from function
87             immutable auto v = ease(t, from[i], to[i] - from[i], duration);
88             // set the value of our data pointer
89             *data = cast(T) v;
90 
91         }
92 
93         if (run_time >= duration) {
94             _state = TweenState.Complete;
95         }
96 
97         // import std.stdio : writefln;
98 
99         // writefln("F: %s, T: %s, E: %s, V: %s", from, to, run_time, v);
100     }
101 
102     public void start(bool attach) {
103         _state = TweenState.Running;
104         if (attach) {
105             import re.core : Core;
106 
107             // attach tween to global manager
108             auto tween_mgr = Core.get_manager!TweenManager;
109             assert(!tween_mgr.isNull, "global tween manager not available");
110             tween_mgr.get.register(this);
111         }
112     }
113 
114     public void add_chain(ITween[] tw...) {
115         _chain ~= tw;
116     }
117 }
118 
119 /// utility class for starting tweens
120 class Tweener {
121     public static ITween tween(T)(ref T data, T from, T to, float duration,
122         EaseFunction ease, float delay = 0) {
123         import re.math;
124 
125         ITween res;
126         static if (is(T == float)) {
127             res = new Tween!float([&data], [from], [to], duration, ease, delay);
128         } else static if (is(T == int)) {
129             res = new Tween!int([&data], [from], [to], duration, ease, delay);
130         } else static if (is(T == Color)) {
131             res = new Tween!ubyte([&data.r, &data.g, &data.b, &data.a],
132                 [from.r, from.g, from.b, from.a], [to.r, to.g, to.b, to.a],
133                 duration, ease, delay);
134         } else static if (is(T == Vector3)) {
135             res = new Tween!float([&data.x, &data.y, &data.z], [
136                     from.x, from.y, from.z
137                 ], [to.x, to.y, to.z], duration, ease, delay);
138         } else {
139             assert(0, "tweening this type is not supported");
140         }
141         return res;
142     }
143 }
144 
145 @("tween-basic")
146 unittest {
147     import std.math : abs;
148     import std.string : format;
149 
150     float start = 0;
151     float data = start;
152     float goal = 1;
153     float duration = 1;
154     auto tw = new Tween!float([&data], [start], [goal], duration, &Ease.LinearNone);
155     assert(abs(data - start) < float.epsilon, "tween did not match start");
156     tw.update(duration);
157     assert(abs(data - goal) < float.epsilon, format("tween did not match goal (was %f)", data));
158     assert(tw.state == TweenState.Complete);
159 }
160 
161 @("tween-int")
162 unittest {
163     import std.string : format;
164 
165     int start = 0;
166     int data = start;
167     int goal = 100;
168     float duration = 1;
169     auto tw = Tweener.tween(data, start, goal, duration, &Ease.LinearNone, 0);
170     tw.start(false); // start, but do not attach
171     assert(data == start, "tween did not match start");
172     tw.update(duration);
173     assert(data == goal, format("tween did not match goal (was %f)", data));
174 }
175 
176 @("tween-float")
177 unittest {
178     import std.string : format;
179     import std.math : abs;
180 
181     float start = 0;
182     float data = start;
183     float goal = 1;
184     float duration = 1;
185     auto tw = Tweener.tween(data, start, goal, duration, &Ease.LinearNone, 0);
186     tw.start(false); // start, but do not attach
187     assert(abs(data - start) < float.epsilon, "tween did not match start");
188     tw.update(duration);
189     assert(abs(data - goal) < float.epsilon, format("tween did not match goal (was %f)", data));
190 }
191 
192 @("tween-color")
193 unittest {
194     import std.string : format;
195 
196     Color start = Colors.BLACK;
197     Color data = start;
198     Color goal = Colors.RAYWHITE;
199     float duration = 1;
200     auto tw = Tweener.tween(data, start, goal, duration, &Ease.LinearNone, 0);
201     tw.start(false); // start, but do not attach
202     assert(data.r == start.r && data.g == start.g && data.b == start.b
203             && data.a == start.a, "tween did not match start");
204     tw.update(duration);
205     assert(data.r == goal.r && data.g == goal.g && data.b == goal.b
206             && data.a == goal.a, format("tween did not match goal (was %f)", data));
207 }
208 
209 @("tween-vector3")
210 unittest {
211     import std.string : format;
212     import raylib: Vector3;
213 
214     Vector3 start = Vector3(0, 0, 0);
215     Vector3 data = start;
216     Vector3 goal = Vector3(8, 4, 2);
217     float duration = 1;
218     auto tw = Tweener.tween(data, start, goal, duration, &Ease.LinearNone, 0);
219     tw.start(false); // start, but do not attach
220     assert(data.x == start.x && data.y == start.y && data.z == start.z,
221         "tween did not match start");
222     tw.update(duration);
223     assert(data.x == goal.x && data.y == goal.y && data.z == goal.z,
224         format("tween did not match goal (was %f)", data));
225 }