From 07b94b749c2514a893c5279684ceb255ece2d990 Mon Sep 17 00:00:00 2001
From: Jerome Humbert <djeedai@gmail.com>
Date: Fri, 28 Jan 2022 20:45:30 +0000
Subject: [PATCH] Add sequence and tracks for complex animations

- Add `Sequence<T>` for chained tweens
- Add `Tracks<T>` for tracks of sequences running in parallel
- Move most animation-related properties to the new `Tweens<T>` struct
- Add `sequence` example
---
 CHANGELOG             |  13 +-
 README.md             |  64 ++++++--
 examples/sequence.gif | Bin 0 -> 59972 bytes
 examples/sequence.rs  |  65 ++++++++
 src/lens.rs           |   3 +-
 src/lib.rs            | 365 ++++++++++++++++++++++++++++++++++--------
 src/plugin.rs         | 106 ++----------
 7 files changed, 436 insertions(+), 180 deletions(-)
 create mode 100644 examples/sequence.gif
 create mode 100644 examples/sequence.rs

diff --git a/CHANGELOG b/CHANGELOG
index 2c999d7..e74da40 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,9 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
-- Add `Animator<T>::is_paused()` and `AssetAnimator<T>::is_paused()` to query when a tweening animation is in its pause phase, if any.
-- Add `Animator<T>::direction()` and `AssetAnimator<T>::direction()` to query the playback direction of a tweening animation (forward or backward).
-- Add `Animator<T>::progress()` and `AssetAnimator<T>::progress()` to query the progres ratio in [0:1] of a tweening animation.
+- Add `Tween<T>` describing a single tween animation, independently of its target (asset or component).
+- Add `Tween<T>::is_paused()` to query when a tweening animation is in its pause phase, if any.
+- Add `Tween<T>::direction()` to query the playback direction of a tweening animation (forward or backward).
+- Add `Tween<T>::progress()` to query the progres ratio in [0:1] of a tweening animation.
+- Enable multiple lenses per animator via "tracks", the ability to add multiple tween animations running in parallel on the same component.
+- Enable sequences of tween animations running serially, one after the other, for each track of an animator, allowing to create more complex animations.
+
+### Fixed
+
+- Perform spherical linear interpolation (slerp) for `Quat` rotation of `Transform` animation via `TransformRotationLens`, instead of mere linear interpolation leaving the quaternion non-normalized.
 
 ## [0.2.0] - 2022-01-09
 
diff --git a/README.md b/README.md
index d7d5f50..8a0e7f3 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,12 @@
 
 Tweening animation plugin for the Bevy game engine.
 
+## Features
+
+- [x] Versatile customizable lens system to animate any field of any component or asset.
+- [x] Sequence of tweening animations chained together, running one after the other, to create complex animations.
+- [x] Multiple tweening animations per component, running in parallel, to animate different fields with different parameters.
+
 ## Usage
 
 ### System setup
@@ -33,12 +39,13 @@ commands
         },
         ..Default::default()
     })
-    // Add an Animator component to perform the animation
+    // Add an Animator component to perform the animation. This is a shortcut to
+    // create both an Animator and a Tween, and assign the Tween to the Animator.
     .insert(Animator::new(
         // Use a quadratic easing on both endpoints
         EaseFunction::QuadraticInOut,
         // Loop animation back and forth over 1 second, with a 0.5 second
-        // pause after each cycle (start -> end -> start).
+        // pause after each cycle (start -> end -> start -> pause -> ...).
         TweeningType::PingPong {
             duration: Duration::from_secs(1),
             pause: Some(Duration::from_millis(500)),
@@ -55,7 +62,9 @@ commands
 
 ## Predefined Lenses
 
-The naming scheme for predefined lenses is `"<TargetName><FieldName>Lens"`, where `<TargetName>` is the name of the target component or asset which is queried, and `<FieldName>` is the field which is mutated in place.
+A small number of predefined lenses are available for the most common use cases, which also serve as examples. Users are encouraged to write their own lens to tailor the animation to their use case.
+
+The naming scheme for predefined lenses is `"<TargetName><FieldName>Lens"`, where `<TargetName>` is the name of the target Bevy component or asset type which is queried by the internal animation system to be modified, and `<FieldName>` is the field which is mutated in place by the lens. All predefined lenses modify a single field. Custom lenses can be written which modify multiple fields at once.
 
 ### Bevy Components
 
@@ -74,27 +83,26 @@ The naming scheme for predefined lenses is `"<TargetName><FieldName>Lens"`, wher
 |---|---|---|
 | [`ColorMaterial`](https://docs.rs/bevy/0.6.0/bevy/sprite/struct.ColorMaterial.html) | [`color`](https://docs.rs/bevy/0.6.0/bevy/sprite/struct.ColorMaterial.html#structfield.color) | [`ColorMaterialColorLens`](https://docs.rs/bevy_tweening/latest/bevy_tweening/struct.ColorMaterialColorLens.html) |
 
-### Custom component support
+### Custom lens
 
-To be able to animate some fields of a custom component, a custom lens need to be implemented for that component, which **linearly** interpolates the field(s) of that component.
+A custom lens allows animating any field or group of fields of a Bevy component or asset. A custom lens is a type implementing the `Lens` trait, which is generic over the type of component or asset.
 
 ```rust
-#[derive(Component)]
-struct CustomComponent(f32);
-
-struct CustomLens {
+struct MyXAxisLens {
     start: f32,
     end: f32,
 }
 
-impl Lens<CustomComponent> for CustomLens {
-    fn lerp(&self, target: &mut CustomComponent, ratio: f32) -> f32 {
-        target.0 = self.start + (self.end - self.start) * ratio;
+impl Lens<Tranform> for MyXAxisLens {
+    fn lerp(&self, target: &mut Tranform, ratio: f32) -> f32 {
+        let start = Vec3::new(self.start, 0., 0.);
+        let end = Vec3::new(self.end, 0., 0.);
+        target.translation = start + (end - start) * ratio;
     }
 }
 ```
 
-This process can also be used to interpolate fields of existing Bevy built-in components for which a predfined lens is not provided.
+Note that the lens always **linearly** interpolates the field(s) of the component or asset. The type of easing applied modifies the rate at which the `ratio` parameter evolves, and is applied before the `lerp()` function is invoked.
 
 The basic formula for lerp (linear interpolation) is either of:
 
@@ -103,7 +111,27 @@ The basic formula for lerp (linear interpolation) is either of:
 
 The two formulations are mathematically equivalent, but one may be more suited than the other depending on the type interpolated and the operations available, and the potential floating-point precision errors.
 
-Then, the system `component_animator_system::<CustomComponent>` needs to be added to the application.
+### Custom component support
+
+Custom components are animated like built-in Bevy ones, via a lens.
+
+```rust
+#[derive(Component)]
+struct MyCustomComponent(f32);
+
+struct MyCustomLens {
+    start: f32,
+    end: f32,
+}
+
+impl Lens<MyCustomComponent> for MyCustomLens {
+    fn lerp(&self, target: &mut MyCustomComponent, ratio: f32) -> f32 {
+        target.0 = self.start + (self.end - self.start) * ratio;
+    }
+}
+```
+
+Then, in addition, the system `component_animator_system::<CustomComponent>` needs to be added to the application. This system will extract each frame all `CustomComponent` instances with an `Animator<CustomComponent>` on the same entity, and animate the component via its animator.
 
 ## Custom asset support
 
@@ -153,6 +181,14 @@ cargo run --example ui_position --features="bevy/bevy_winit"
 
 ![ui_position](https://raw.githubusercontent.com/djeedai/bevy_tweening/main/examples/ui_position.gif)
 
+### [`sequence`](examples/sequence.rs)
+
+```rust
+cargo run --example sequence --features="bevy/bevy_winit"
+```
+
+![sequence](https://raw.githubusercontent.com/djeedai/bevy_tweening/main/examples/sequence.gif)
+
 ## Ease Functions
 
 Many [ease functions](https://docs.rs/interpolation/0.2.0/interpolation/enum.EaseFunction.html) are available:
diff --git a/examples/sequence.gif b/examples/sequence.gif
new file mode 100644
index 0000000000000000000000000000000000000000..cd5783c828fe7c4bb643c5eea33b04ff7e6c66c7
GIT binary patch
literal 59972
zcmeIb1z40@+crFaq`)>1kP-~K1ZhDOl@38V6{$f$kw#ilO1is-?k+)cm_fR`Q|S`?
zZVAQx>?gnP{g3y57!J?@l!JBM>pa(0>pBI6A9Hd&b3w^L*#d#Iw6wJU&byA*bKMsj
zU`^$>sxm%L#S_E?EAI2QaoibVW}2j?S|%pi#=_c1LOK9}j^6h{pm8)bFa-rG0|OHy
zBNG!7GxKdWHa5<~H!dzNZf>5tcX@$#US2-Fd-op*3X46JQ_*+^{0={U4Q*{r9pDdb
zeSLjfTic+Z(3I%7l;qsZ{My_Kcx6*>NB_jc+`^ZYmA##f{cl?b2j3134)%eM|CzV%
z-@l*!!N8|~`pTcad-~JUum6Ajzkr4OKmYyz@z4Cn-~9)_%#qFit$zUgV~X$RB_2JM
z5x6h)h=q|E1qlQKQS6*MCj<OV4}5@rzyySG0J?`kB9tBT(Fx-+tx98dY_02s8zK=x
zIdOIG30c8|jUO~zeaHnoNQ84I8~>nv`U(_#-MJBuR?Ng6vnBx+&LQ0SsVPrKD3Z@<
z^&D+}ig4Uxe;2Wp8m*=)OmQNz7p3PreQDDlo9?clcKNC0R1&V-PU|M1dhnp2G%l$x
zkM_RO6qa#@?3HVL{RJX&8Q52`Me2PoItg?UTz~lZRvU%>NTxb*Ei7$o5tAi^Re!jp
zByXlQM&Lb}cxnDzM~Y0E8mzQnp(k4t8YNy<xHM4wW?>jsR`g}0LQ4frqP%!zqS2pD
z9bR6tHuE`NG&)m-b7P@5Pk#hnQMUDEwD#b=tD#N#&f088ntE$x#oiVSS{p4{Rk^>n
zy|yqiqQv+8I|z+>)czd4ZjAl`Y09W0Hv8<T(}j8tm3Me@x?|3S+OrytL}s&NuH-J%
z*G+DUagMuFC#8&g&|Aqpf6rV`J>hxh()M*-p3#^I@4K6`6S!<>G?Ts$@wo=PqQ`t|
z{lwVkCUK-5>k0hvSWa&$PyyFHK}UppZYo%`yILUVMUdWfD4}8BRa|b1@#*kaR><%E
zFsXq!h3HO0W};Z&qchR=XhcKU-0zZVv2!3zSHro!Db~jO^jD+&!Tq$^iHhZd2UT>0
z**wI>%>#e_jVu1VBE&tcI0={Q+Xg|*0ox)!Br$5+awa5VgC8Y^^X>dXR`oCkt!^#1
zE3HmLP13Bkr#8_nb`CtXHj(x7E4p0Pb&pWoezb+9qEQ$5be#$I;KjmsWRUr|bhLSJ
zexSqxCQi3uSZF54yR<Kr-?Zf!b-uF%FP1cCLg~}kT1Mx~a_g<<GJC)-Mpbz7FIQe8
z=?ASCr(9BbrRzys{#E^JJ2<T(^!{QWZW&y|=@Ub%FA7eDgl8!1MfTzhG^9<#3Eo%p
zO{bEpsAa8Ah)Q*$KIC?W3S_RkFkKtwEoNLxsqc~YSx~3m7<T6ZjHRs<%_|S4?*D8p
z^X|1U6m89sLH&MCI)kmDcHXq2gXmVSrS*;{q6W-e=}I<ik-RtIPTm*abf-YKTq3Wg
z^65V=n&c4mW}SMHHB`_a;y-qKI3d_DcBJ>d0j+_Ij$A?4oyBZ{@?7Y!g^mwF^XkB>
z4qvCtAxB*HwwRf+vYtx+Czj1j3(Y*Mt=Eip*E<E=xJ^mX8I>18D)@{S^W>HG7OBmX
z7NGnnyF4)FKdzOm-2T3^xyl<KT|NaI25&dMZQUE)Xule=$%jVtrGyr3^nEZc*qo<z
zG6wC@fcmr}dii(-lVfCT^y(tj{kfelPUP4ZK(PYZZ8<L1FEsEH!2C>Bb+m8C^Hg_O
zFg})QW$L@O<L^^y*Y4^i86U7OKErSodbsWRSpnr?&%Bz`19r>jZ>2o1qd<F5FeDgt
z<NW(zZf4XuOCrjKC_~uADUS*+iaS9U;s$dY%g8QdxxMn7u<)kNzNz-wJwy28s7f#j
z>h6jU*4xo;7J`&pWFoeWYce<{)qE{?CA4FC5)R%rmN!zcyblhDr{pM!te7*9viKzT
zk~9A*S+9n3ir6jMH=|DdPksHfJrn791)5>&8q&oriHzobBJ%9ULDkqvl=@te@>pvj
zjo-*`Cu+95;9d*0P^P?-EDQsOp*eTAB(WE`!wjs}!W#o9IU9r}6g=`GX2o<c${=vF
z(ma#Jx5;;B6mu-v*P^x$u!DHlnp$n=^`Z~3Q}{E^eRd#Pk71>~h>hR;*@b&O_JUW+
zLn@Ir52f`uLRiXUj^;LRtMzzt9EgyhNV{LydIGgLL`1&1J*ad&k=Y9(_RPxha?5%W
zCk!HC-rNzjyPnLClPcvZ(iun8mCVMQDjnS1nZ&&Tk@8A?nk>?lipvI(gQdz9HFsrN
zZKP@7q$xOXBZqr$r0a^ODfKjW7nW{hP%zM`%!u@q_HSfbz|z!&X?rSmH?r(;(lyXU
zdt)~XvfaefpA$fP>#;Voy}i<3P>J?6D;1{rR-|ilK>OfUn|T*t^tytg{q13!`ERZ;
zfaRh6-KCoak7N%V6rYI>^!INT7GHt9GKLNe?`{?q3^BZN6&)NWlFzN-%`^^%4o=sw
zmB76+O_D{2QXUkQ_QEpFis(8PjJL{0ak4CK&kwDHZI#b<8CrHghc`>jix+IN-p+`Q
zbfp$nZo{%{8LUV4cekoQxG(Kc#72>cLo!hKvK<LpG}0)GKVI<8ewW2PfT_G)LpV$$
z$J#Q6Yt5NRe30!XDE8cwulN(S#BEo}^l|*MkdO4XIi5O;BZM8>^_*r|UT<0^$ehd@
z_;7Q56VFdl5brd8VqozLZh0|4Thb)uy{Z>YKSir-nfJ6ZH>hZ^m;TKTR0IE&bFJ7k
zi@9ZswnW}RSY7!vdzodHnr&Xh%-t@|jvcrKUuxu9%M9O^Nvkbxe#|qh+55!1pD7vc
z#1X)By=hC^e7*A@USpUOkq>JRg5Qb1oiQizM_5M`E?e|{hI#4M(9R@@jj$&f^K$;7
zU76k+LC+W#lte?j3*j4nuQL|Z&WH9?;+lCoFf2ZYhV<4;n0b82Sk!(W(g*i8bBSeG
z0*i+9cf&Uwb263;FNX{a<8Il0WLP%t2p*i4Ft={YST+j}9$NO^G9P33Vx`GZxd|_F
zUdi}k%MmhsfNSv@iSeuBUeG9-<hH@Z%&#t0L1VZ+oL~yZ6%U`Far{<Jo!gl!-ts{c
zWPg-s9^7YK^`i=!q?FvzY)fAa;vkq}^x08;#<&(%8;C?-R06krzZSXot%&z~$>-O^
z>#@Y7g^%u)w#$dFC-Ow*i`$iU@(^!8l!tO<t4h0Xgl}Y6tLCVFFYQGq-pmOf%F?=5
z*8e4Jv!HB$n#-<iaEx!Obf93#td&P$C3CCNDDaEzA9rQ-7`JQW1HQUQzSW(`+^*-j
zvf^!Yck)K`c9Try*PyDmqpVpwm2WE7EWh8KU0~d8gI263ne)ucWNmik4KHTeRrG2y
zS!Z=rZYs@iESuX&53L<+U9aH%8p-%=jILt4o`ZYUpJ{!Hbz}x^R|$`2+7FYd+Pz)H
zx2b%PxxbWLzBkOowOxB{b;ZAGV)~w4eMi=TM_$$bMZ0_7h#9}{P?aBSGI1WPUHiJf
zRyA_4Ww(QGkE~#HPQiY~%pRlH5UbdJuG=10!SDj1!x*~*UatYZg+pJE1CfFONv}iO
zrUQBJ%j=wuO>&Nu3NNWc96uI1()a2!ZabFXJFzI}-%)VNF>~VV1#=fWLAssz6u|t1
z?_$~CJ?zzcZ1FBM=$(j#o@dXykga!8#g@{X&QbEta)ee2A<hX!&MFpGYTM4K1TN2e
ztzIa&WShI_a$19nT?%_#3{y<S2wiR1T}?uC%q(2ZgIujFz^{5;KW(|%TfBATbZeG(
zJ8<cJ>lWhnxya3v)5d$-t((BzFT^H5!F|x&J!IP^tk`|L$305HHipn+mct{V*fz<+
zV>#F(mC!D|*JFLlBiq6*m-GFu{QJUQyW)`dpyK!Coc5L5@6iZ7YeMWlDR^RAcs6d^
zHy3+e>h**xID97bBINYyEOzL&@FENG>L+v@?De|2?KNuQIL_%!tKdD|>o^<Y&0Org
z$mz7a?afZ;vl`;GuHbXm!e@KiX}8$tL9fq&!aLAaUm;H4b0zQ4EPW+Xe2E%dv3Fi#
z?)Z95`(EmMhNt+U1OEe|r7l6q2mJ02WR?a*SN$B&{BG_T-LUkF5AvgR*P-n5L*G2`
zV^-8);qph4^Jm{tzu}9_1u^33^G9j)=hw4;z!iY05b(IkUMMu+kKzC^8V8A;fGdQ7
z((VpV6$6Pa0u|;Qlu80AdIQyj9W|~7QE>*nfH-Pf1~G&Lfiaz4_64zS2ffmBGUf{A
zQV2F{a<T{wzE>Rlmgb%9PVghb5J&fS?-WBsEkfKZ^*l;KK5Y7Xmjr#F3H6@~3e*b?
z#`Fn=gobnZM9zgq>-ogegvE#YB<h7FH~ByyVQD))8FOJ-G`=}B;dzR_1$yB{?!G0E
z@Ujx$3K~7mo$%}{5jAs4pA;j~O(Pn)UelLETuzFBD=K}y8cEO-(dq8jqo><(5E>a-
z5IN|s)x8smHXS+Mq&}t?#bFsW3-O;XiJIt+S`JmW!izSawqD27-n5KX35wo@XnpI8
zmfMU5S?Pb}idm40K?_qu4vP^hjKTJJF}V{%M-{7uAB(puk8c(G$|Z(~+lZt;b|5g8
zocql+?zrAtag-wR)M0V23gYPJ<rsJ4Fy!J`=2dPh#p{{Ib6Ux9m&QNuis!?U<0nc`
zV@r70Ec@6hK`AgnWM1P=e?oO#f|QbuBzI!PtwcF2S%t7fANfR;(pPG`i3J6T&r_ei
zP)d4inxrf86kMA0pexBxMD7_;@+Mod3AdJ+RdRc1vXxS#P3g<GyU9_g$p?-eGVhd9
zSWQ#hh-5rUQ<%C^y!)kni6C@rkbp4hAS(z}AS6slI-(zPV*?UHB=5kTs-vCSs+gLR
zst{M2>YbFDsicrWly;RZE#K-%p;ekIdRi%#LP>v`e|=hYX>6^Ce~nVQ#YS3Vzf^N+
z`uVPOI93LfHltA_qYj)=i)8_~QtL0xm~hUh#>%Xq%`6qkECOfddt~OMW@a{Lrp;%j
zU}YuIX2pwS#elOSJ+i`5vx4VihIdtmcC+r8r6Zq@Kc}3{>XE%smle>Q{b4@a8!P8M
zZH}8rjx#vN(Idw$HOHno$7(*u94ps^8wEWrTklaWC6D@b;$*sogCvHsB&PRCx0RDv
z>5{0!bHAA8aazalq-ow7$a}Dx$FH35fG1x_DgSX<f>3zAM05U^!h8wQ#3yuWY<r2a
z&_wz0L`D5XWu8RU^NH#M3D43JG_4b~L=$v~6Z971^~>T7-p3m$$G@hFf3p{73XL-l
zkF(T|v*wAjIUi>?5bKZ@>tr44EE?-d7wf(k^Bx-G6&~ZGAM=4H#{Yaw;6QY6T6Cy&
zbhv1AByn`~LR4&7RQ&s>MCGVtx+utAWEwOwBRn$8Ix<HzGLJa2U?HNYETZInM456#
z1zkkdUiim>@Y=HQy72G@>+mM!a41hWj5xe?FRX1KtfMTf>mWR=$2zP}Ic$I@Y=}5)
zWG{4VAatTEbSgY_#yWIPIdp+1bcs0h%R<OXS;*S^kPYRKExM4Mz2H4)@P2skcl}_b
zyTK^f!Ki~lNIV7gfz?<MFQf*lRk1z}npT5_KkDdxeBu7_8RVl{(?{jGj|!MIvNSc)
z!Znh5HDc~H!jPKBb6J-bm9f9o$nRFOr0d*KsSPo$<$S5YU0!>?u0{a!6F<!-Ug1w%
zdY{<cKe0kSF*ki;nEOPFSw}@vcT>2ILa&a@y^a`CN7z(%Wv=cLX8j*D_1MDoSbFv7
z?)B#&^+-+i2eb8i7!BLh4I4rYtGW$e+!_{B8s-`sre_-tCNLUDsT+rc8vAt{d)yj3
zQySYE8{xB!P>iNV>ZUrOrW)O*Dz~Qcl%|r#ro!2#JdEaS>gEig=2YG0WVhyol;+sR
z=BU}`a13Y&H8fBN>Zc3!af5oMK;0XmF0)W4j23(978{`!E8P}zw-%F>me-9fhO;gD
z7%*LGn3fRixh_oI4W^O;Q*4CE&BA0b;8N6ZaUr;fE?m$J{xAi8zY)$i3+KUT<)m(9
z6KZAAZDn$6rB7+4X>6sOZM}i<nVkAFsnBO4-OmJWpYc*YUu^u0JNx-OMjHlo8>&zn
zvToaV*S2rTZ95HZn=@@|=<Q#r+Lr{|=XKg=To2kOliSA{+J|S_2hclusXDp@JKA+R
zT3tI@k~^9jI_hUSYSBBZsXD6!JIi%C%UnB)lRFC<I&)__v(UTJsk$J7T}e7!@vdDl
z$z72RU12j_!RXxqRNWs0yS;U~{d#KozRBojSQw~Uf-Ac1@Oz9aWQ+_f%t@V14dP8I
zdR+Q@Y$H?bNqarHd*AJ+xY+dih4p%<LOh3hLw0+8D<FQneNjq%L8Pf6k$nlxedtAf
zQA<xO87$((onv9jiA&}X8|UOm<+Kd*tPJN2FXbEq^MWDgJe;JGr57<31I7IVl@)0p
zc_XTM2S0)b>lFtZ_oa$>&0#n$(8!491JWVvjX@)wAx)v77RZoA^H7rWkUsjb_Uw?g
z$Z*1~VI9|DJ>6m3=HckXVNLXrm$SoPc!&BKM$}zLjD$v<z$2X*Bg*Kb#<L@B45RWo
zqjIjJGRdRT4Wp7XqYjNDL&Bp_oH0?Iv3JyC#DZguUSonYW3DM<qcr1TsN(`U;~qle
z!Or9N8pgd+#-}kSt~HF+FOBnXM6>WIQu9yL8cg_xO!!Ytuv1O?V@&!v>;G{9Nu+X8
zY<N=QU{dM;^jJc-fDnoB)s#Zilv3rC%J9@h<|#J@8M)!97i!aLxYLZz`kIE*FAb;F
zy{G9Drw#aKUK~un%9J*in6ZkQIxw@Hp$?p}uAH%1o^h9(av+;^l9+v~HS3x=<DNPD
zv0>JWZ^j3AjvRH?A3hyuI2Xq-7rHzhVcYloV9ua&F1GSXh{U{(?feJ6`Bb&(B=|h#
z^jucuR1V_;e&Booe5TlTF28a?!FHj7?@1}{A|d%=E!mSg+{MPI#g_er`r$=5JpD6#
zvBP`5t8$^;c8NKAu^+xv5VllMxzw|~G=aOEw>)1DUzm+r7(bYw<Xf7?{j#*YJYTsi
zPxb}Q_a!y+OB4J{!t$3|+^^A$Un?ZOh8lh?_WtUh`85~*)ob}{I_`=q<4Uskq*T-j
zQS^$m+84Y>lPu_~L9;6cw@g<Vbyh=#Rx#67!Op9+GpiAes|>9ZA92<wb=IP(*SJL1
zD!taO&#c9ztm&ezlL@XTQm@}_UMn+L$APSuz}8dT)-_Q#@C7&0s5kD-ujd(TTx!_J
zblZ3~wSh~unS-%;Uw>2m(I!^%X1?3z!-Y*%He@8EA6Z8vj2~G?TF`S8Y7iyJl?BEE
zWW&FMK=s1HQ>?5pii$rHl885i1<r;AFg!sh=Ro{NS$E2t{4w_Y&KGXb;*o1q3%C>B
zlFsL3j&FEJ%w$U<%=|*o?}m8XO^ESSLl7ftjVB~0PAKf6&Ul3G+N4AjiQV)f4PG2P
z4%O#6Y7CxUN1UYd<9#ku9oYzZJ#DX&F0GzG^(QV`!X|Scb7-Jp;;Ci~9fkUBmGAWB
zG6l=7Ud6@0I0h&(h@Kt*DAx~w9-$L}DS%D@qX>YWjeUt&<*@}YdO=7(%3|dAk&!O}
zWijS4`yXYo0Nw1k`|ogAX|bzw;v0OfF?is)J<)vZOZ~(fPqF=GZ2&V*W`1w`RDB4u
zj$FE)3<w^<eSS56hgC}|mRB7g)pA{<-B~o0{npO9_R~-qBy3a5E={=%*~?~i+@@(w
zIV4qKU2g35{n>nNl{f=thFztKJy8MS1dPMEwmZm4bj6Ti7dsquGW{*R@p#ujmSs_k
z)zLC%3Z~_L&bf)053$;46+7UWsAyA{!sbvGZKwxbQRSjV{(SqBDtvpM9P=;shhRqs
z0bsuefc^1rU<Y6h;2p6E5EeL77Qpy+1a_bXj)IP%(CBM_R0H3B1*&;Zh)MgRnAyJJ
zum--<_PpL7ox}KMQ|>Jxe`-yvr%j&|#r>GI;u88+l3M+^wN~-I=IO#?2wm~_E%9g`
zB#Iibn}RQZp9af7z>~deU}(CMj`6Me4gKEta!~Jc#OxXUzAp<(ZO*U1Y|b(1>92sY
zWi=K*otZ{me9Ej^BZO1+h~#OGQav|murobg)=02*no{NB2V=1&HQq0SEsE>HpXJ4M
z9+JwMO1B5A#yc;`Z_4yLu@bzf=wu%WjNn-q?&q)ojfA~N7px@S8XLomWO8AYv(%dh
zxkwMfNyqJv2m#{<A*ca_@a#82I6D2$)Mh{|7GVLPDdzC>|IrkKj->!J#R#~bLlZh~
ziov`&*m!%a*5?}fIYQywgeHG#8Qj^%+*^&o*pHGTgk`j#;l$5s0u=LfV1b-Y_?<g`
z$*u8J4p^!pcnMDuq)@0;iSqRtQxp=VwZL6t9qGKuhNgW5WAfQ|>b#c}y6Ke?z&+D1
z%f2ou2fUuTo}j#y*;;OevwB6N$4I!^0TY+;rNzzhPsjuu66ZtXC!5&mtS6zxE1HeL
zx2d|yLk)Fmqc6MS+k~vE!P5oH@UeN!7WxGA2h2QqispuH%Al(mC>N`Z=XgOM!IgHD
zrsE=goV^uy<>vh}Wa^enik=UZ6;Qm{$5Bb!Y8-tZEhtG=Y!N&7Bn$n*$d|p;w>O>-
zw%*g^|K&x50;Hk!8)+cC2n2%8pao8o2I?Vc)VgA$UkEwwq`O4un;kFs?gG$BcOmAG
zG*VTr+D|kDUwhPSBIK6{3#U=78MJ(rB#FfK8mj=}pZvu6o)#KuGoDsk{9S)q1CfnM
znH1(k@AT#k+U^h~3dSk!&V?TDXLWvw;7$6*{5w7D1`FNfgT=<sZkQFvbF~WVkDI8w
z^3+4sLg+pW>8s4+AI}r8ntAlzcv0i8$c_6}(RkV~e9P>hoW*3QE#Zya<MVxMIvs{j
zk_$_@V;B2k^s8h&!wU_|GOWYj*=({Kj;njMKK7Qi1W!vv`n%XZ-CY=|wmfg-UJ<j=
z(&=@}HkyPF{B>!{+*>CiD$8taNj;N^ME#X|&uH1@#q^viJ5Q8z=YAyvdjR5a`#~H(
zlYxj_!x@APV#xq;pq#Yeq9gzCHjcuN-dFa=Ex51w9o~@(P$mfFCJ4JDOQ)(d<t9Rd
zkulsNgx7!;T)b!VgLBf9u;?4Fu}HD(lb=NKIndq`%7?(>rPL`^OFJR5eoyi;BSfyu
zc4b{G@R;f^pzO`Vt^?mvmb=+s1kTGfIKRr!U;JdI{gLh#y;_RZ7SZDQ%`a`$j-i(-
z!M)T!ybV^v^0nJ9n&fEYT^8}*$iTQ6s5ZY{&3_7~5WdW2Lu4w|#i_VuvQM-r(?|d4
zd?6|EmR$c^%aP4U=`FRfT6f~)_up>54ulS{bL^CJes$^)mGiN*d9@kQ70(~Mk9$zE
zojFNmcu$gzxB2VP`O-hm->a<I`J-o`%BL#!U>|9H=Cj=%=B&C6DpBzThfSJdaz`BQ
z-nMsmh7b9VoDnQwBhhmqJx3`-a0&ubXWas)IR*6;ryM#XAPnr@#vl1hasgZ+q1>Af
zJgH>>PGO0+4`$YEju6gE0{L^kE*xyiqY^=8cfv{x&C`7lE#yy&%AF69ikCW18!2LX
zOD0)C=kX9xOlnWo<t%MdYk#_@*}5I2H$}S_6$@TLJhT=%GunzR{Naf_orZ1Y_Lj?2
z#GOX)YPXC_^<V{^@dg5PCd&Teo0CwyTarPFeJs<hf3O+3^o8D>?ZAHI!>GJV4(?19
zEaKZCGFgP>srDSawKg_%0va|MzlCeOYN;R~;IJhzHm-r9vzzk`YMVHBT)sDBU7>IG
zX<$y#>^p9y6>Mb^$yMh)AMfynK~`DodApZ;JKzPK3ZH8kw)LwU!O@lhkEnp&eq;Pm
z2aSFC9!G4eDGix#k2t&D+1v+u`!Q*CM_pu}Z3noJUzfEPqE5n)K8bt)vS|Xy<{g4;
z&N|c(>;HRXgJEB7e{_c^XTlzEV6OjgV47Udol7}#V3<+eBDj7yFc)R&2b(%>!J;u=
zV_iwjOKywg_s1t`UVqURFQrTyNfZN-^^lAQC#J5a%DX8RJeUewPg8VxR!4N}{Q6>F
zKG-sMf3BN;u=w^&d-~o6<8ZK*{%XbiR#s;<1^Q+7w|K9g*NLBJm762HIoarMEr)Am
znKIcFE@(y>rf8_^6fYbMx3<!o50AcDWVYWw2ky?h(p_jn{K{~sOthZ;8?nW!^a`8r
ze2jNXTE|<2alb2<gV)_VR^+O@hjzxM2H)^n>6Gx^?OmaRhMR}*X6|n2w#m5QRz0&W
z__8oU5Pt38AY;9&=(F9EjUqA!O;l=fM=u%$btJM!)D(iJ+asuu5fKprO$ZD8xRm%a
zB0BB8%>F4NIwF+cBcek>xp_JwD)l{zh)9L<=Z+$x)J{m(QAD&c)0K7UBqGB35fQNi
z5fP|UTWf%wLM7igzCFYG>vCVY)YdA)ECKy!ydyd*!`qGQwoe`?%!Z2=R4?lMGvo?M
z`V6L%!cEo4zFjrZg+$&DqO>A1osUU-UAVkMWcD&VPuY9gTG?zlqhN2deqVWNxwO`u
zz~}0q#;QM5hRkSh@P)}V3{$Rw$i}K~iK~oXrw5lTaeD;|>dNb*w!b?yTvqC1!OPdR
zb73J~(&rw--pW2#!iX)>F8YYA-xY;UyFQxYQHOKCM?{B3UUz^~5b0S2rp~+t08aT?
z(?O(X|4DilC<ZqG#PNGE7*W?b^B_8l1~^R|N5$Y;7Zi-2#b9iq#vIKWSMqDP82m@Y
z;8g7Fws_HC3Qb0mlH4Tm00#B>ht#Z;uyB6;=|SPA&m?{CInnZR<x{uC3k8d?^>sod
zQm!Y1Gr(6U<=m8#jhFgY=z6n78~pZl%_#fx_00=S_t;bVi_vDVgp}l|f#_yuQ`T}T
ztF6!xed5mI?d<2_&R2PC%UN>AngV2$4&HOI%G7JkP;QoAHPUIlCECF26?RM4JN-$T
z8gcl#exaMPU8PO7*>v9v8yU1=*=@~nIWM}$%TISSrXNLaIUhXDH%u+JR<4rpD;-=K
ztRPiKwNt&Tv04hVhQWqRHyzs-QjO@j<M;On(2wdlpu^^FB$VeUV!sN(2(~zjU_q=I
z1j1PS<!K;c5LO@8b4bX^W9#0dqn<m-y`rVK<xD>Ex-OIYO{hIV)QMWYS*&k0QQVpJ
zjZ3;^hn83*zu;#14y&$2l)!C#HrmU2((gq>Q%!d9o;`(JhsZg*Z_IZ4$(QAROMUuM
zCYK79+3A6wtQ2N&*~NzLj^X1{#iil8^L`BdB}V&xsOfTF<!iKYj2_X;y_T(4CB5hD
zDQhg<EY0voUCYxqlJLj8m!}5gndIBa9u1r$eYzpq&9A!mFtXp|elNFH_@||=P5xn4
zQ>VnCCl-q1#9q-6jpddPk&e?B=)iNQrvwP#&Tsip#O)3Q9?qZzFxn2&7rvta7yOv1
zbb-PFXdZt|FZ6o!qYEK17C-=+4IWpRv`O4Q#|wLt(|FtrU72hOK$Xw!oNd1$=EN%4
zoo^`<(-MJYZo7DOl<KqJ1CC1!Rsq`WiI06KWkv4Leuf~&uxq)=E_BDpRM<Y+&7bdm
zrwWTg^({>9iZC#$jJ!%PtrBD^GkhzQU{WpeSy$8&u`$CywIR-BIS66SV7=DmELD=r
zxl%crG!qevy_r*yAyweQ?Go+TjCh+MGs{vva8ksL5qb@UI0LuSNy<AYYQP;X+P`Vr
zID^j_vE;LC0gPG@5{R5R=6G_N5dSC$>11*m5-tOHFy-P6dsh+x7m^F)AkYVDo}hK=
zY@ls}S+fR@p9Lfu%K4@;p+q`aGWxE;0SPVZtazM|*L77|DVmN1HXpvOIg|O;R5{3F
zn3XA=d<JjT^e0-gq@LW{O>qVgv!$jy!@e4?J$Cw`q&E|Vs$~QQBe`~UXb0RO`eW&?
z8I)0_+j^Qw_c0kG#6q7nK@TUV?c`R&V+7tD(3j@xwkOFL;S*l?b@huK<uM8+h!1h~
z>n!hS#M*%)yAX8#0^#vQ#7l%1VvgIfIPJ$iy(gqmYH7T!S?5j8WQ#4lI@aiSOEL}{
zVyp$+&Ez%oY`RD(?!}=FZ*ljhdK}J2ii|^~p(_>7_hI#EnE#x3@}1cFfO%tk`!t0-
z@KAF>T2Gcntp}-SVR~PlZbzyrv@m0!=+#7XBpreMP?^Qr{1CJ#Yoy8^<vf{KarRiP
z+hsbnmg1a=2HzW^QDP;zQ%AT4CL&h;bi{$w9XrYm0LsD$C?mG>*|q@2r~hFn`^}4_
zQRgH9Sv~5W%tuvbT_S$;qB##?j!*Zb?T?@=T9i3dW`;ETFGKlH!%aEJPoL3K$dsTL
z2w<P>ZH-tx#xe-Fe06+A<6Z~>E@(MpUlVy-{3@=_<q8v+KIHX8y*C+?BR1FTo5CI!
zB}MK)E<K0((WsXO%=z9DcVst0Yqa#I5|3tasKo2{OA(2u`r!AJ$PXfzd`)xy6OHLS
zJXIzZ+(~;WO(GNjV@<x5)S?LT@fod*jC!sy@(|en;u#G;AM-CxEv|Mtf9+3Gi=Ut$
z-I_S8=d+{40TiHjiUJUcb_5>Ipan2CK<Fp8CQvV7oMhQ=W=jEs%P-SCA9VY0y}<{c
z*O6pS>ZY(CrKF=6#P0?)h?K2naNAh>aX6#aI4~6aIV=jt4jG3WPq!^f(44Zaq*J>i
zRPxzl11?#uE`Rwa^CUzjbn<a(CEBE1W?H?mTKAG*J6Fb)!Z0VAA5q5uXZycM0mp;O
zDNqRi+2HcOpa5WSxkN>&Y0v3I96JgxzyzL%MBEwN{D>u=vH(z^$HYEy2C(o>n1F;D
zkTCGTV*+=EypC^hBfaPGg3x%f!H<$7PR#Q4Em2PtwJ1*-nOm?h2BYzJC9FD<?-}i|
zFn0ZQCF6NMj6YTKhlnLJ#pVWt`K1b{-N^Ci<d&Iz7^y~VyusP6t>C7`<;cs!uTfL<
z>I!nmzvB%2V+jMZS<p`jgTKoJzeyP2zP~0}c`RWN&36(m00lXAlo}*TQ0ys&K==x$
z_6_3YS+M}X5T|^F3nvWmpZN+`{xe?z<KOoc{soZ}fFYtuV*V_00{9BQF$C!;hBy>C
z`8okT7CDLi8C)C^h892=?@kd0VgSrp!3biVr!4T_<5GOV;r~q)>0fgxu#O313I+Kv
z`*?V{njK{o=<W$U#SRE};?$l&ygVxw_^)**T96$7Ci{4ldic$qApA>rBHHrkZuH?O
zId+sZfGZ47r701f#94s{Vx6ZffU$KtIe~o=di+Pp3AFzxIf3<`B`3_VWB#$e;y>~v
zGJZxChwTUs02fI84Hp33!zn`{ULq{;Uv2@QFZh49v*_=)Bcxq^`LK`;^>3<{NGOE=
zq_gNx?Fgr|Rt*|{mLm>P{^PdbV+6|o%QrD%dH=)$Kwt3xXT1q<B;=Yu_YWFjo}e5P
z4^;gx_7DDsa(vRGzTm@KJ?toQ0IqK%qW*vK*b(3UFSh_jz<*vu5CYl5!a3i}10&Uv
zTci1%ut-JnQ-JHg{<Nyi`H+resd$k{k%H8&Q+>fXlq90~&+LEi3;t7?u@&vqJXIzD
zs0<*gQ$R(`Q~fXBl8EK~l?5>BPlepLKxfc15Hj+05j;=~&>2L?3QnKRrugK3k;8*T
zFXu+B54mt^r|v36qc5GN@f~;LII&O$eUb93tTUn>cV&0qUVTapi+&`GD<1lEQaW6m
zgKy|+;M9{iSyl<>(2a4q7*$a<Eg~~*!AKRTEd$*qy>ua{yCx7Cl-ZvnwJ@HT+QZOY
ziQDE_u@Fo@R&9i2E%w%8Wu%_!s%rs{rBQ#g^V$Qm9YWS|xT-QfCkB7)Tn9r&p$e>&
zt=9fm-|uAr+7EzJ7$M8_SI-;qwKHP@jNzYWFv>Y<m6UAIp)6B`(Cy<<$%OV40?2p*
z9-fUk3BvEm2pmb8jK`aTiDYP1`f@2jA)HT|6ZHJGBqCT)zb?|OQnkbgxXazKx&)Dq
z78B>TQMv<ef;<gY8ggSx`;<XaX61|#v;faxglglxms{%3(-khQXf#^{l!qty%F|f2
zq|4qeSWOMw&Qhy!%)8D`VxFU3@dAa_kZ$MpWOyL&0cpg}gjS3BWf{}-9ag>8V9B&Q
zLzbrV-K>hPQP>uRT8*iiRfVd%EHxw5s2;XNhi*6M(CuDCmII0Zf;^(qc4qcCVxhlT
z0GOtM`m?(a_X6nTI$p07|H*JsO^^!!`2gLv_$Ghi``#)vxi_1GDP?mbuD0E32`AEO
zAH=Mtghz8*!q<1!w54L{-0^KJ@${gHkHWbVQ}DnokUMEgOVn5NVHuArayzlulY0}i
zLZe89*_Y}v^{q>JG0hE>i%eUG#lz(nhjVQz4p4~+7e+IjlgSFhdgq>}czxhgBT}55
zOnEJ!h87k&KbRINY`dutI-?ztXdZ=2yt|<54auu?8S39y^vrP`W>?-D1Ba9}EY}YW
zY`yaQ=ufKxCwaE|;dDMN6(9h;Uj%@VV*G7aAU-)W7C2pPN5?y9-a|goRgpfDVk8&v
zz~(29Yx2isOI3lqPJjkeh{i==egJe;nBUX{2<Ii+hx5zMlh6WUOriJv)owO-#<#^&
z1&T!W=FiKdTuP?R=wGMpc2z1mRA;Nr7H;rkBfi2QRUiP(HQmEElqk`j8_zHE&k(L)
z-`U(yzWi0>Bh!j4+grjFp*n@D=S@`tUOj6fq>(R-xb#LFMouBH0mviBz$1m_@9aNW
zf6|4o82uP_VH4a(qPmw~agjr~zrYgZ>ppSu*Re{|t-BH`T;kJB4&U*HRk%$C;UA=Z
z4BrCMO~YCwI)av}cP)my%Hn4v%I+?#7+6w8NPZ{(R#kJ{opU&T>j}UYc0c$6A*XU?
zpn+KEUs&LGz5tz6+#@hfVuZ%FIQv62M?ky{gp3DBAM9kuLxV}g0qKKFx1iyi$~A+S
z{z;Ny>~FBNn(}lTqJ=~O9VU~ww&bh+A~+@a5Ls8LJhYJJg88mArC8%gK+UluTe3lf
zLEns8DIV;fYY@I>&{ixo(_Wz$Of?*BwUxP8wwBde?I?3;S<ixU{F4Mh*~>*j7MUi0
zn(((TORi6cgaw-6?_M>T{SYC@L0_?AA`Zx@=$m*Ev8(qH>b)1kAvQN0BDYN2w7F>U
zYT%)}5u5F$9gV4$a2|Q=$da#Z&B4S~!(7%T%Rv=)l%pjgx6N1a>Wl=h^?kG0yx4_;
z{)b|v;ja5qM?I55mHvVCjyaLNiB|Au2Mn@@8p?gBCl?HX9^hRhIgky4NB&}CA-;5`
zEbv<oFzzoNnPonaWoo;Qgar5<Bmj>HBmLl!zC#|l7ZoAwe-q#lI)F!Hv|-V#M?4}J
z&F}9`(j4%EM?TO-_UF?+Oi@4)iVW*am3L8$HJsXAOX~r6#4k~=n-<`ao~W0~8vu_O
zi?$np0UjwA-NIceTVopabws~xtGCVE{D}+Db5pr&{Gv&aCa>V_Wx(%v!*zM1JZI%u
zTfDF!Dk+hvNEeYfidn{`O>noB9#4VI6En4;GE4osNMehJV`T0FcWg;Yj7nO<&)Ze9
z@5;_JqbM7>^YV-@4JNZ%f5#;Qt&UAx3%(~A`O-3bsG*AE+Q{D9&ABe8hUf#^{lx?K
zo)PPrD=`3%T*o6f`We0eG-3<TNZfBUf=FSUDfl23dt`wh?#QWeLO=5the}MBP;>{6
zl$h9XFPw}I#;d*nxFgJ((%8azl#L(g-poJjlTHH4h7|oI+;}tacrI%+)n>dKZOIf~
z*CVZ@^*YiNNFNOKKcVf)lFRc@Z7z5T%2H2uA%)5%DQ4<+kf{z7W_A=wO*D52_hgQ^
zTdXak?iGEltCrlrmFw%te)j3%6{hLAV&jL6kEm`KyzFDt4)<dVo*XP$nhA@2lt(|b
za|hNLC!B}Bz?}-N%%kh6wB*q<Y$&@|(@no;-Z4t%D#>UVL2vBcOu%Y{yUw*fOqVRP
zrDvnL{;|JEt6`Dx5|8GJX;U;?G|OG9;Q6XArr!^kEq9FPda?+kd8hUjH)f)RcacKc
zY)~%glFK1e7WX?gP|S`xUEuB^wbi?#@Js-QGG)w#6nFcXGsP~)^>?>&&Bq;AXeP(s
zV`S*YSRE73VdC^AKs-PC4H0n=0)c1R0)NkcA^aH!t^C=4!OQ`OhxD}nQgZCSz?$+>
zM34Lzlf<@UqC@{>9`IinM1J@$J+6v5hyIHq=vl1yEzz~bo_xukT;92EI+b|inV@w2
z4L}0UDtOgkKA39shbj{z_8Rl^01sCd!*9jfFPi*)<P5Dtb*DqZoy_WY01*{`K$U5D
zmhkm_cp8v5+3(j|^vpo%DYO}wSga_M*{lyyHhMKyTX0Zl`3<}Zs+(aqkGjh}Fn1|h
zK%J40r+22iiOb3eGpej{Wh5<J(z7yl^Q+208B-Nm?zgRtrJ@FPbv}WEg&jaB8xT>6
zu|x*T+dsf&caw5#Jr0HrlPV1$Igk^AOwKfp5Q_&a@b{7`KMSvan^fWYK_-8hRJqdp
zE2#pI$w^Yh{UoW9g#$>uad*-6W<8Jg3j}YbcjZ6sp)^<sq8W@c{?MKwy1ooAzqhqo
zH@C&45##g`f7#L^Y4kX$vMT#qQpNB%sUmokR1r8#s+g(w5dcXQ10MS2!P5Jy%Q&MP
zUq`9jNA4_Nv^1Xj;KgHy%X@1r1Ma$2$){Rou-uo%o_5Y>kmu#<M_Ah9EaiRO&HN#A
zmoG|{TD!?h0`=FF?4BQl?Qjs<2Vge#m}1^a8NH9zZK`_eLX;iAB!)kjL|a??I|zg*
z=OAEpHZAbGTXIs)d2o&&nAY;6J1aqm-2R6$Es$1WvH#>xeZx?tNm{ePk6F`0Oqf**
z8qT5KJcyZ>BpJx*R5~dfFxeW9<4jwTl9wXmFXY1*i4`*^6Za%hS{A%94^O{WWV|tt
zmoAy}pv-ScFNm@~2HZnrpuc7yQF3>#y8z0<IP7cnWYu79>q|p575Ww%$>p4fb#&)h
z&9L!|A2+$sNanu{y{!xr=F<H1jUZp3EuJ^{4v<!P)NY~3YGQD)P^Ax9w@QvTxWLf6
zTyXqAm2F$B?~_3m>47qr;&f@GwX=lsuIzkf#=SakC2Pe=nAc4dv}>w&g_h$g2p+X6
z^2)7t!LGSPt5ynb2A!uKY`sQd+xRn&0$i&Q2Y5pP!5e22D~MJ9vcTW+hJXv`x=6%f
z9_1qDFI5!_A!$&(KQ$8=uPJw`!S4^5=75s+MDZYIQuv^I&NM8V><v4>8*!2`Le3B9
zo7bt^qeaA780Oa@uoM}Hl8px&U2CfHYub%nHu}$*FZ9Bkx!E)N3*-k%A7SDfN|n5^
z?l*O3&uXq<*xSueAowa&?&L*;Ps5q>sm7j|2MuJI^`aqgNlhZ+>hko*$R68G;{L_i
z^2FMxsBo(Va8X7^r37&}-FQ;~fop_#p;`NYzfCl%w<3qou$pt;6%XZ{m9m!bd$zc|
z$?J2ST-RGuzfEsKQ1Q?`uK9fB={VIDdK;jIzv>G8Wi!Mpgayuk1u(XMHGHE2U7@}w
zU7@c}TEsq3)FS=p3f27pw1^S%GM?kgo%-ZQtqFb#@lAw=gEX24g=Or;BN$AVJ^R+E
zq&_@wjiR^e)RIml)gv<4y_71J^z?($En2p;?hMf~s|*bG^k#_Wn;<rKHio8xC#|J{
z+a?ABCEzhQ-g}O$;efX*-3->|%Np6vLuA8YoJ-Hsyc+mii24>@BnIZGkpV4Y+EETT
z-(kHab2DkNSA5XufqPnQi9S)&{iR=C2IM#lvoBoCnCv2WGcgEbFVGxCdN*>Dtf*+A
z$1bo8&n>&u)RC986c^*LMf@=BNehqx!(U`7&cNeGED>RWUlzb<JS2lxAbY1H@g<-d
zaN!0TA$d&PBP1MF@Lf*+=MOyy=nP3pvf~@vDIWQ$U{)sx`cf!nn$TRFeiV9B&sNO+
z;>_d73kFI+zv#{8fQM|zyhN;ewTTa8xf%5CfLkG0v8$g%HWFkr71In+-%HU+=aSS#
zJ<>0vQ7$lSDy(oXyroiVRN)QVGtL?;6Ig)bv=k+&XFDYxbd{OC9;tUjqVsOyWSnXU
zDxs{L?`3`g^W#$AjJUcmU1}zYMbCL*qt^dt5C9ZYe}J13fcLMO20~fw*Aqv)Ix`l)
zc=t;f{)e*Kv9#i05a0r00UsArk9sni{HcMSjHcW}%_X3>D=+CU?qGg6caHk_4kqSJ
zXORMMCqyn!DKNEwPBv4mmIakgj=nEf4HH}8Sepfx`obyEBwCOv0L%w}lhr#j4+ODL
zgav+B0OR8?&C@>v^G8<iFM+8gh>Y_57?|hI&HmlX(UyR;^6}+pF9;x-l&MnXafR3{
zy)Rv>H8nBJY^kr%umKQ5Gi6lGmiZD^kVZf^QYrTxSv%aES*=zL^$$7hkk{%BRum(F
zV!_5LP{Z3<UFkt<12t~@j<}?R)3A<|*GD(0kM3U4f-C`4i=ChvF}Vz}$Iqq(Fsl9o
zRR26~wQG@(jgLnjaH3!wk34vDUA5T+(w-z0%jYGsG25A{;H(ByGNtQDQLAFdq3ed&
z=c=~Gp{AKF^%fe!>TP&}Y5Gc$mb<%nf{j$F43V;~cm};vtbKmbPPQfRwOoS&MRHv@
z9^+K3A4FT*x^Gq+62x=9dM7VmFXpEU4dl^jLD2xFPwS>2rX!rqO?}2!jj{TFg=qn#
ze{YP*4|zi~J)~dKNhruC<B8Be)j^AlhAV7Q_PXebnbm7tE_XaiSGYY<?{ZB5pWbp+
zqdEvh9{w$uRjVb6$91{Pa(%Ep2_=TT!pdafa~5yI^j)4e2GAm^8JF_1*G3I7GSRq<
zDr~REsxDLcyuG`zTGM)upom!Y%}E*lFp?ezXu$4NB#oefGY*KexqP6UhH@gG^e;Q?
zzm9eR(7#Uuf1`4$TnKVFmQP}GKte`7RyjohT~liz%o(ruC8qKgvs{^M4nbFkM_97X
zG)LVN#AQ%gooM$JjyEf}TA%Aqf5s}upv=6~k8Ie}%@EG<awNkc3*S4;LT@7OgPj~a
z)N-b$*g(K1%G%0gCM3tH&|8^D>q{mCl>jZ8oBVYCoClCTut5;R*@OdP)h8DCUqB26
zO7<u6$HCPgq!amLBs&~BwYQwNCmK923PhzVuuMT688qwDEm^1FQ4~(Nq<y#N8sa56
z<QV$zEOccNBzu!uup1y1>dkmp?3o&MSIMH^<Bc#``P{%uHBugtw=hr;EzrO~YPAOK
zxhG#0X=A+A-GO<|PVjjA;b8|bEyxexg*XH+oa_(8-)GYT{|k8GuO(NXL4Z+E$Fk_?
zH(kU^j3=9dSe1j)OIWqwQ39-J3`%$A+M^`nyVI>Uv|BP+i@a?@+4Xx1^=C5gT5pXH
zSE7Qpc;A|@3^o{0C0E=vd(+b@FDS@cVPe`*?DQR;smf`-)FVLbD!I?Qy$zo+GB=M<
zV|Um((oo`aK>nF#IE*iF0a3=lQ*?p2_INh&a>iHvzmF~y(J{!5W}Ty6zEEA9GtuCC
zLzFUGA$JPuPp3KT8Ok!#8pP{Fwxn=JuRTf9Ng^YRb)h?wt#<gEm6`THrq)Dd#h&rf
zu$LVQ0Xd1q%5Z}@RYS#~*;-%gGeIL>Rnv`*o+k@rWN&%Q$A?Ssb*}N+nr@Fx-AiuW
zCtbDbcb|X?lAO#{K1@iy1(?F<q{x7{5OBr;b7q$hD5N0$=b4CUvHu_;d6<dt!6&aS
z%or$oHPL*FnfxT+Fy~jTv`5+6rz_6ZsB*cC@;JTtwpzV6sepI71&dZ~5Ysiv^paQ8
zaHsp4)AZaXbJfw3BjOp{ApNc^$p%JVWfOzXMLaXz<$K0P%~dzhgT+cW=Ev(*Z*cIH
zzg}yGD&Ld5Yhz+u(ZzeLkaDP&L<_0|xB#J+bY?MtSoo0zfKJ~33og+9Pl^*qTyTl-
zgbP%Oe+splWsOwX?je)iRbU>gwZgffrXqh^^ON_r%ii=?nWvf^&^27rE!QUDQ4CJF
zk)ipz9d=CdxG_Yg`W;!q4Y<4mZ<b(1w`VfTm5skNR9!_^zZ<azu8j8?V6Y9@(V4Q0
zB#^+hsa7iK&1=Ou>Eu0h3~*3>mrFu~5D0vnK?`8C|34*z^%?tP!G*ae$VhR=f(z$j
z)P|a`q;+R%lxgFLnl1F^>Ly2thnq1B<Qk5+c+tr(_LOSOjc=5iFAr6|MS9akr$9Sa
zgM>>UM;~%qqrryaOQ2`4u`1N~c2!qP(3^oOHynTrEKGD;b)VF-c`2|d_Sg!`J4g0F
zmIlOs&TBk`IXeJz!N2kU&)|bbEcq;30FZ0=56^tFo6IrCJ-x#G_J7t@37E47o#?8B
z$>o7z9;RvXS*leY8AN93iYZ#H-r{MSbOVLTBQ;aICYcH)W{`0hohg%gzUlY&E9qTd
z)N9{foRNJQyrR<ZilQhmJ!n-Cs(m{PM@)ff25xiD3GHtGn>k3>m-fM?QnuEki+Rkz
zlLUH=<N;`(mP|sl5T4~7erDH>5%SNP3_ege{2ZZ(43hq*?fSn-CaH4q+aF6NUAftl
zrBP*tBO0^_oZv-K;?VB-{zAQx8U{LZ!`@P}Iyu;Td?xh_6uvMLvF#T?N6^6)@n<iz
Y$j$Xkbjf8jjkUB4pBf43X@hwF57{1{DgXcg

literal 0
HcmV?d00001

diff --git a/examples/sequence.rs b/examples/sequence.rs
new file mode 100644
index 0000000..3b72778
--- /dev/null
+++ b/examples/sequence.rs
@@ -0,0 +1,65 @@
+use bevy::prelude::*;
+use bevy_tweening::*;
+use std::time::Duration;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    App::default()
+        .insert_resource(WindowDescriptor {
+            title: "Sequence".to_string(),
+            width: 600.,
+            height: 600.,
+            vsync: true,
+            ..Default::default()
+        })
+        .add_plugins(DefaultPlugins)
+        .add_plugin(TweeningPlugin)
+        .add_startup_system(setup)
+        .run();
+
+    Ok(())
+}
+
+fn setup(mut commands: Commands) {
+    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
+
+    let size = 25.;
+
+    let margin = 40.;
+    let screen_x = 600.;
+    let screen_y = 600.;
+    let center = Vec3::new(screen_x / 2., screen_y / 2., 0.);
+
+    let dests = &[
+        Vec3::new(margin, margin, 0.),
+        Vec3::new(screen_x - margin, margin, 0.),
+        Vec3::new(screen_x - margin, screen_y - margin, 0.),
+        Vec3::new(margin, screen_y - margin, 0.),
+        Vec3::new(margin, margin, 0.),
+    ];
+    let tweens = dests
+        .windows(2)
+        .map(|pair| {
+            Tween::new(
+                EaseFunction::QuadraticInOut,
+                TweeningType::Once {
+                    duration: Duration::from_secs(1),
+                },
+                TransformPositionLens {
+                    start: pair[0] - center,
+                    end: pair[1] - center,
+                },
+            )
+        })
+        .collect();
+
+    commands
+        .spawn_bundle(SpriteBundle {
+            sprite: Sprite {
+                color: Color::RED,
+                custom_size: Some(Vec2::new(size, size)),
+                ..Default::default()
+            },
+            ..Default::default()
+        })
+        .insert(Animator::new_seq(tweens));
+}
diff --git a/src/lens.rs b/src/lens.rs
index c5470fc..fa6056c 100644
--- a/src/lens.rs
+++ b/src/lens.rs
@@ -90,8 +90,7 @@ pub struct TransformRotationLens {
 
 impl Lens<Transform> for TransformRotationLens {
     fn lerp(&mut self, target: &mut Transform, ratio: f32) {
-        let value = self.start + (self.end - self.start) * ratio;
-        target.rotation = value;
+        target.rotation = self.start.slerp(self.end, ratio); // FIXME - This slerps the shortest path only! https://docs.rs/bevy/latest/bevy/math/struct.Quat.html#method.slerp
     }
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index ec49d23..22b8728 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -226,30 +226,18 @@ impl std::ops::Not for TweeningDirection {
     }
 }
 
-/// Component to control the animation of another component.
-#[derive(Component)]
-pub struct Animator<T> {
+/// Single tweening animation instance.
+pub struct Tween<T> {
     ease_function: EaseMethod,
     timer: Timer,
-    /// Control if this animation is played or not.
-    pub state: AnimatorState,
     paused: bool,
     tweening_type: TweeningType,
     direction: TweeningDirection,
     lens: Box<dyn Lens<T> + Send + Sync + 'static>,
 }
 
-impl<T: std::fmt::Debug> std::fmt::Debug for Animator<T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("Animator")
-            .field("state", &self.state)
-            .finish()
-    }
-}
-
-impl<T> Animator<T> {
-    /// Create a new animator component from an easing function, tweening type, and a lens.
-    /// The type `T` of the component to animate can generally be deducted from the lens type itself.
+impl<T> Tween<T> {
+    /// Create a new tween animation.
     pub fn new<L>(
         ease_function: impl Into<EaseMethod>,
         tweening_type: TweeningType,
@@ -258,14 +246,13 @@ impl<T> Animator<T> {
     where
         L: Lens<T> + Send + Sync + 'static,
     {
-        Animator {
+        Tween {
             ease_function: ease_function.into(),
             timer: match tweening_type {
                 TweeningType::Once { duration } => Timer::new(duration, false),
                 TweeningType::Loop { duration, .. } => Timer::new(duration, false),
                 TweeningType::PingPong { duration, .. } => Timer::new(duration, false),
             },
-            state: AnimatorState::Playing,
             paused: false,
             tweening_type,
             direction: TweeningDirection::Forward,
@@ -302,23 +289,175 @@ impl<T> Animator<T> {
         }
     }
 
+    fn tick(&mut self, delta: Duration, target: &mut T) {
+        self.timer.tick(delta);
+        if self.paused {
+            if self.timer.just_finished() {
+                match self.tweening_type {
+                    TweeningType::Once { duration } => {
+                        self.timer.set_duration(duration);
+                    }
+                    TweeningType::Loop { duration, .. } => {
+                        self.timer.set_duration(duration);
+                    }
+                    TweeningType::PingPong { duration, .. } => {
+                        self.timer.set_duration(duration);
+                    }
+                }
+                self.timer.reset();
+                self.paused = false;
+            }
+        } else {
+            if self.timer.duration().as_secs_f32() != 0. {
+                let progress = self.progress();
+                let factor = self.ease_function.sample(progress);
+                self.apply(target, factor);
+            }
+            if self.timer.finished() {
+                match self.tweening_type {
+                    TweeningType::Once { .. } => {
+                        //commands.entity(entity).remove::<Animator>();
+                    }
+                    TweeningType::Loop { pause, .. } => {
+                        if let Some(pause) = pause {
+                            self.timer.set_duration(pause);
+                            self.paused = true;
+                        }
+                        self.timer.reset();
+                    }
+                    TweeningType::PingPong { pause, .. } => {
+                        if let Some(pause) = pause {
+                            self.timer.set_duration(pause);
+                            self.paused = true;
+                        }
+                        self.timer.reset();
+                        self.direction = !self.direction;
+                    }
+                }
+            }
+        }
+    }
+
     #[inline(always)]
     fn apply(&mut self, target: &mut T, ratio: f32) {
         self.lens.lerp(target, ratio);
     }
 }
 
+struct Sequence<T> {
+    tweens: Vec<Tween<T>>,
+    index: usize,
+}
+
+impl<T> Sequence<T> {
+    pub fn new<I>(tweens: I) -> Self
+    where
+        I: IntoIterator<Item = Tween<T>>,
+    {
+        Sequence {
+            tweens: tweens.into_iter().collect(),
+            index: 0,
+        }
+    }
+
+    pub fn from_single(tween: Tween<T>) -> Self {
+        Sequence {
+            tweens: vec![tween],
+            index: 0,
+        }
+    }
+    fn tick(&mut self, delta: Duration, target: &mut T) {
+        if self.index < self.tweens.len() {
+            let tween = &mut self.tweens[self.index];
+            tween.tick(delta, target);
+            if tween.progress() >= 1.0 {
+                self.index += 1;
+            }
+        }
+    }
+}
+
+struct Tracks<T> {
+    tracks: Vec<Sequence<T>>,
+}
+
+/// Component to control the animation of another component.
+#[derive(Component)]
+pub struct Animator<T: Component> {
+    /// Control if this animation is played or not.
+    pub state: AnimatorState,
+    tracks: Tracks<T>,
+}
+
+impl<T: Component + std::fmt::Debug> std::fmt::Debug for Animator<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Animator")
+            .field("state", &self.state)
+            .finish()
+    }
+}
+
+impl<T: Component> Animator<T> {
+    /// Create a new animator component from an easing function, tweening type, and a lens.
+    /// The type `T` of the component to animate can generally be deducted from the lens type itself.
+    /// This creates a new [`Tween`] instance then assign it to a newly created animator.
+    pub fn new<L>(
+        ease_function: impl Into<EaseMethod>,
+        tweening_type: TweeningType,
+        lens: L,
+    ) -> Self
+    where
+        L: Lens<T> + Send + Sync + 'static,
+    {
+        let tween = Tween::new(ease_function, tweening_type, lens);
+        Animator {
+            state: AnimatorState::Playing,
+            tracks: Tracks {
+                tracks: vec![Sequence::from_single(tween)],
+            },
+        }
+    }
+
+    /// Create a new animator component from a single tween instance.
+    pub fn new_single(tween: Tween<T>) -> Self {
+        Animator {
+            state: AnimatorState::Playing,
+            tracks: Tracks {
+                tracks: vec![Sequence::from_single(tween)],
+            },
+        }
+    }
+
+    /// Create a new animator component from a sequence of tween instances.
+    /// The tweens are played in order, one after the other. They all must be non-looping.
+    pub fn new_seq(tweens: Vec<Tween<T>>) -> Self {
+        for t in &tweens {
+            assert!(matches!(t.tweening_type, TweeningType::Once { .. }));
+        }
+        Animator {
+            state: AnimatorState::Playing,
+            tracks: Tracks {
+                tracks: vec![Sequence::new(tweens)],
+            },
+        }
+    }
+
+    #[allow(dead_code)]
+    fn tracks(&self) -> &Tracks<T> {
+        &self.tracks
+    }
+
+    fn tracks_mut(&mut self) -> &mut Tracks<T> {
+        &mut self.tracks
+    }
+}
+
 /// Component to control the animation of an asset.
 #[derive(Component)]
 pub struct AssetAnimator<T: Asset> {
-    ease_function: EaseMethod,
-    timer: Timer,
     /// Control if this animation is played or not.
     pub state: AnimatorState,
-    paused: bool,
-    tweening_type: TweeningType,
-    direction: TweeningDirection,
-    lens: Box<dyn Lens<T> + Send + Sync + 'static>,
+    tracks: Tracks<T>,
     handle: Handle<T>,
 }
 
@@ -343,48 +482,39 @@ impl<T: Asset> AssetAnimator<T> {
     where
         L: Lens<T> + Send + Sync + 'static,
     {
+        let tween = Tween::new(ease_function, tweening_type, lens);
         AssetAnimator {
-            ease_function: ease_function.into(),
-            timer: match tweening_type {
-                TweeningType::Once { duration } => Timer::new(duration, false),
-                TweeningType::Loop { duration, .. } => Timer::new(duration, false),
-                TweeningType::PingPong { duration, .. } => Timer::new(duration, false),
-            },
             state: AnimatorState::Playing,
-            paused: false,
-            tweening_type,
-            direction: TweeningDirection::Forward,
-            lens: Box::new(lens),
+            tracks: Tracks {
+                tracks: vec![Sequence::from_single(tween)],
+            },
             handle,
         }
     }
 
-    /// A boolean indicating whether the animation is currently in the pause phase of a loop.
-    ///
-    /// The [`TweeningType::Loop`] and [`TweeningType::PingPong`] tweening types are looping over
-    /// infinitely, with an optional pause between each loop. This function returns `true` if the
-    /// animation is currently under such pause. For [`TweeningType::Once`], which has no pause,
-    /// this always returns `false`.
-    pub fn is_paused(&self) -> bool {
-        self.paused
-    }
-
-    /// The current animation direction.
-    ///
-    /// See [`TweeningDirection`] for details.
-    pub fn direction(&self) -> TweeningDirection {
-        self.direction
+    /// Create a new animator component from a single tween instance.
+    pub fn new_single(handle: Handle<T>, tween: Tween<T>) -> Self {
+        AssetAnimator {
+            state: AnimatorState::Playing,
+            tracks: Tracks {
+                tracks: vec![Sequence::from_single(tween)],
+            },
+            handle,
+        }
     }
 
-    /// Current animation progress ratio between 0 and 1.
-    ///
-    /// For reversed playback ([`TweeningDirection::Backward`]), the ratio goes from 0 at the
-    /// end point (beginning of backward playback) to 1 at the start point (end of backward
-    /// playback).
-    pub fn progress(&self) -> f32 {
-        match self.direction {
-            TweeningDirection::Forward => self.timer.percent(),
-            TweeningDirection::Backward => self.timer.percent_left(),
+    /// Create a new animator component from a sequence of tween instances.
+    /// The tweens are played in order, one after the other. They all must be non-looping.
+    pub fn new_seq(handle: Handle<T>, tweens: Vec<Tween<T>>) -> Self {
+        for t in &tweens {
+            assert!(matches!(t.tweening_type, TweeningType::Once { .. }));
+        }
+        AssetAnimator {
+            state: AnimatorState::Playing,
+            tracks: Tracks {
+                tracks: vec![Sequence::new(tweens)],
+            },
+            handle,
         }
     }
 
@@ -392,15 +522,106 @@ impl<T: Asset> AssetAnimator<T> {
         self.handle.clone()
     }
 
-    #[inline(always)]
-    fn apply(&mut self, target: &mut T, ratio: f32) {
-        self.lens.lerp(target, ratio);
+    #[allow(dead_code)]
+    fn tracks(&self) -> &Tracks<T> {
+        &self.tracks
+    }
+
+    fn tracks_mut(&mut self) -> &mut Tracks<T> {
+        &mut self.tracks
     }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
+    #[test]
+    fn tween_tick() {
+        let mut tween = Tween {
+            ease_function: EaseMethod::Linear,
+            timer: Timer::from_seconds(1.0, false),
+            paused: false,
+            tweening_type: TweeningType::Once {
+                duration: Duration::from_secs_f32(1.0),
+            },
+            direction: TweeningDirection::Forward,
+            lens: Box::new(TransformPositionLens {
+                start: Vec3::ZERO,
+                end: Vec3::ONE,
+            }),
+        };
+        let mut transform = Transform::default();
+        tween.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform, Transform::from_translation(Vec3::splat(0.2)));
+        tween.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform, Transform::from_translation(Vec3::splat(0.4)));
+        tween.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform, Transform::from_translation(Vec3::splat(0.6)));
+        tween.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform, Transform::from_translation(Vec3::splat(0.8)));
+        tween.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0)));
+        tween.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0)));
+    }
+
+    #[test]
+    fn seq_tick() {
+        let tween1 = Tween {
+            ease_function: EaseMethod::Linear,
+            timer: Timer::from_seconds(1.0, false),
+            paused: false,
+            tweening_type: TweeningType::Once {
+                duration: Duration::from_secs_f32(1.0),
+            },
+            direction: TweeningDirection::Forward,
+            lens: Box::new(TransformPositionLens {
+                start: Vec3::ZERO,
+                end: Vec3::ONE,
+            }),
+        };
+        let tween2 = Tween {
+            ease_function: EaseMethod::Linear,
+            timer: Timer::from_seconds(1.0, false),
+            paused: false,
+            tweening_type: TweeningType::Once {
+                duration: Duration::from_secs_f32(1.0),
+            },
+            direction: TweeningDirection::Forward,
+            lens: Box::new(TransformRotationLens {
+                start: Quat::IDENTITY,
+                end: Quat::from_rotation_x(180_f32.to_radians()),
+            }),
+        };
+        let mut seq = Sequence::new([tween1, tween2]);
+        let mut transform = Transform::default();
+        // First, translation alone (0->1)
+        seq.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform, Transform::from_translation(Vec3::splat(0.2)));
+        seq.tick(Duration::from_secs_f32(0.8), &mut transform);
+        assert_eq!(transform, Transform::from_translation(Vec3::splat(1.0)));
+        // Then, rotation alone, on top of final translation (1->2)
+        seq.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform.translation, Vec3::splat(1.0));
+        assert!(transform
+            .rotation
+            .abs_diff_eq(Quat::from_rotation_x(36_f32.to_radians()), 1e-5));
+        seq.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform.translation, Vec3::splat(1.0));
+        assert!(transform
+            .rotation
+            .abs_diff_eq(Quat::from_rotation_x(72_f32.to_radians()), 1e-5));
+        seq.tick(Duration::from_secs_f32(0.6), &mut transform);
+        assert_eq!(transform.translation, Vec3::splat(1.0));
+        assert!(transform
+            .rotation
+            .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5));
+        seq.tick(Duration::from_secs_f32(0.2), &mut transform);
+        assert_eq!(transform.translation, Vec3::splat(1.0));
+        assert!(transform
+            .rotation
+            .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5));
+    }
 
     #[test]
     fn animator_new() {
@@ -415,9 +636,14 @@ mod tests {
                 end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.),
             },
         );
-        assert_eq!(animator.is_paused(), false);
-        assert_eq!(animator.direction(), TweeningDirection::Forward);
-        assert_eq!(animator.progress(), 0.);
+        let tracks = animator.tracks();
+        assert_eq!(tracks.tracks.len(), 1);
+        let seq = &tracks.tracks[0];
+        assert_eq!(seq.tweens.len(), 1);
+        let tween = &seq.tweens[0];
+        assert_eq!(tween.is_paused(), false);
+        assert_eq!(tween.direction(), TweeningDirection::Forward);
+        assert_eq!(tween.progress(), 0.);
     }
 
     #[test]
@@ -434,8 +660,13 @@ mod tests {
                 end: Color::BLUE,
             },
         );
-        assert_eq!(animator.is_paused(), false);
-        assert_eq!(animator.direction(), TweeningDirection::Forward);
-        assert_eq!(animator.progress(), 0.);
+        let tracks = animator.tracks();
+        assert_eq!(tracks.tracks.len(), 1);
+        let seq = &tracks.tracks[0];
+        assert_eq!(seq.tweens.len(), 1);
+        let tween = &seq.tweens[0];
+        assert_eq!(tween.is_paused(), false);
+        assert_eq!(tween.direction(), TweeningDirection::Forward);
+        assert_eq!(tween.progress(), 0.);
     }
 }
diff --git a/src/plugin.rs b/src/plugin.rs
index e477d19..3204862 100644
--- a/src/plugin.rs
+++ b/src/plugin.rs
@@ -1,6 +1,6 @@
 use bevy::{asset::Asset, ecs::component::Component, prelude::*};
 
-use crate::{Animator, AnimatorState, AssetAnimator, TweeningType};
+use crate::{Animator, AnimatorState, AssetAnimator};
 
 /// Plugin to add systems related to tweening
 #[derive(Debug, Clone, Copy)]
@@ -21,53 +21,12 @@ pub fn component_animator_system<T: Component>(
     mut query: Query<(&mut T, &mut Animator<T>)>,
 ) {
     for (ref mut target, ref mut animator) in query.iter_mut() {
-        if animator.state == AnimatorState::Playing {
-            animator.timer.tick(time.delta());
+        if animator.state == AnimatorState::Paused {
+            continue;
         }
-        if animator.paused {
-            if animator.timer.just_finished() {
-                match animator.tweening_type {
-                    TweeningType::Once { duration } => {
-                        animator.timer.set_duration(duration);
-                    }
-                    TweeningType::Loop { duration, .. } => {
-                        animator.timer.set_duration(duration);
-                    }
-                    TweeningType::PingPong { duration, .. } => {
-                        animator.timer.set_duration(duration);
-                    }
-                }
-                animator.timer.reset();
-                animator.paused = false;
-            }
-        } else {
-            if animator.timer.duration().as_secs_f32() != 0. {
-                let progress = animator.progress();
-                let factor = animator.ease_function.sample(progress);
-                animator.apply(target, factor);
-            }
-            if animator.timer.finished() {
-                match animator.tweening_type {
-                    TweeningType::Once { .. } => {
-                        //commands.entity(entity).remove::<Animator>();
-                    }
-                    TweeningType::Loop { pause, .. } => {
-                        if let Some(pause) = pause {
-                            animator.timer.set_duration(pause);
-                            animator.paused = true;
-                        }
-                        animator.timer.reset();
-                    }
-                    TweeningType::PingPong { pause, .. } => {
-                        if let Some(pause) = pause {
-                            animator.timer.set_duration(pause);
-                            animator.paused = true;
-                        }
-                        animator.timer.reset();
-                        animator.direction = !animator.direction;
-                    }
-                }
-            }
+        // Play all tracks in parallel
+        for seq in &mut animator.tracks_mut().tracks {
+            seq.tick(time.delta(), target);
         }
     }
 }
@@ -78,54 +37,13 @@ pub fn asset_animator_system<T: Asset>(
     mut query: Query<&mut AssetAnimator<T>>,
 ) {
     for ref mut animator in query.iter_mut() {
-        if animator.state == AnimatorState::Playing {
-            animator.timer.tick(time.delta());
+        if animator.state == AnimatorState::Paused {
+            continue;
         }
-        if animator.paused {
-            if animator.timer.just_finished() {
-                match animator.tweening_type {
-                    TweeningType::Once { duration } => {
-                        animator.timer.set_duration(duration);
-                    }
-                    TweeningType::Loop { duration, .. } => {
-                        animator.timer.set_duration(duration);
-                    }
-                    TweeningType::PingPong { duration, .. } => {
-                        animator.timer.set_duration(duration);
-                    }
-                }
-                animator.timer.reset();
-                animator.paused = false;
-            }
-        } else {
-            if animator.timer.duration().as_secs_f32() != 0. {
-                let progress = animator.progress();
-                let factor = animator.ease_function.sample(progress);
-                if let Some(target) = assets.get_mut(animator.handle()) {
-                    animator.apply(target, factor);
-                }
-            }
-            if animator.timer.finished() {
-                match animator.tweening_type {
-                    TweeningType::Once { .. } => {
-                        //commands.entity(entity).remove::<Animator>();
-                    }
-                    TweeningType::Loop { pause, .. } => {
-                        if let Some(pause) = pause {
-                            animator.timer.set_duration(pause);
-                            animator.paused = true;
-                        }
-                        animator.timer.reset();
-                    }
-                    TweeningType::PingPong { pause, .. } => {
-                        if let Some(pause) = pause {
-                            animator.timer.set_duration(pause);
-                            animator.paused = true;
-                        }
-                        animator.timer.reset();
-                        animator.direction = !animator.direction;
-                    }
-                }
+        if let Some(target) = assets.get_mut(animator.handle()) {
+            // Play all tracks in parallel
+            for seq in &mut animator.tracks_mut().tracks {
+                seq.tick(time.delta(), target);
             }
         }
     }
-- 
GitLab