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