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 }