CYD-UI
A C++ library for building native graphic user interfaces
Loading...
Searching...
No Matches
animations.cppm
Go to the documentation of this file.
1//
2// Created by castle on 2/18/25.
3//
4module;
5#include <tracy/Tracy.hpp>
6
7#define ANONYMOUS_STRUCT(...) \
8 decltype([&] { \
9 struct _anon_ __VA_ARGS__; \
10 return _anon_{}; \
11 }())
12
13#define KEYFRAME(POS, ...) {POS, keyframe::make(__VA_ARGS__)}
14#define AUTO_KEYFRAME(POS, ...) {POS, keyframe::make(ANONYMOUS_STRUCT(__VA_ARGS__){})}
15// Alternate - if the above one doensn't work
16// #define AUTO_KEYFRAME(POS, ...) {POS, keyframe::make(ANONYMOUS_STRUCT __VA_ARGS__)}
17
18export module cydui.animations;
19
20import std;
21
22import fabric.logging;
23import fabric.async;
24import fabric.wiring.signals;
25export import cydui.components.base;
26export import cydui.easing_functions;
27export import cydui.interpolation;
28export import cydui.animations.complexity;
29
30using namespace std::chrono_literals;
31
32export namespace cydui {
33 using property_id_t = std::pair<refl::type_id_t, std::string>;
34}
35
36template <>
37struct std::hash<cydui::property_id_t> {
38 std::size_t operator()(const cydui::property_id_t& x) const noexcept {
39 return std::hash<std::size_t>{}(x.first) ^ (std::hash<std::string>{}(x.second) << 1);
40 }
41};
42
43namespace cydui {
45 refl::any interpolate(double x, refl::any initial_value) const {
46 if (keyframes.empty()) {
47 return initial_value;
48 }
49
50 if (x >= std::get<0>(keyframes.back())) {
51 return std::get<1>(keyframes.back());
52 }
53
54 const refl::any* prev = &initial_value;
55 double prev_x = 0.0;
56 for (const auto& [x_f, kf, interpolator, easing_fun]: keyframes) {
57 if (x > x_f) {
58 prev = &kf;
59 prev_x = x_f;
60 } else {
61 float z = (x - prev_x) / (x_f - prev_x);
62 return interpolator(*prev, kf, easing_fun(z));
63 }
64 }
65
66 return initial_value;
67 }
68
69 std::list<std::tuple<
70 double,
71 refl::any,
72 std::function<refl::any(refl::any, refl::any, double)>,
75 };
76
77 using property_timeline_map_t = std::unordered_map<property_id_t, property_timeline_t>;
78
79 export class animation_data;
80
81 export class keyframe {
82 keyframe() = default;
83
84 public:
86 friend class keyframe;
87
88 friend class keyframe;
89 virtual ~properties_base_t() = default;
90 virtual void add_to_timeline_map(
91 double x, property_timeline_map_t& map, const easing::function_type& easing_fun
92 ) = 0;
93
94 virtual void
95 set_property_interpolator(const std::string& property_name, interpolator_base::sptr i) = 0;
96 virtual void
97 set_property_interpolator(const property_id_t& property_id, interpolator_base::sptr i) = 0;
98 };
99
100 template <class PropertiesType>
101 class properties_t final: public properties_base_t {
102 friend class keyframe;
103
104 public:
105 explicit properties_t(PropertiesType value = {})
107 properties_obj_(value) {}
108
109 private:
110 void add_to_timeline_map(
111 double x, property_timeline_map_t& map, const easing::function_type& easing_fun
112 ) override {
113 for_each_prop([&]<typename Field>(auto& prop) {
114 add_prop_to_timeline<Field>(x, map, easing_fun);
115 });
116 }
117
118 template <typename Field>
119 void add_prop_to_timeline(
120 double x, property_timeline_map_t& map, const easing::function_type& easing_fun
121 ) {
122 using PropType = typename Field::type;
123
124 property_id_t prop_id{refl::type_id<PropType>, Field::name};
125
126 auto interpolator_iter = property_interp_map_.find(prop_id);
127 std::shared_ptr<interpolator_base> interpolator_ptr;
128 if (interpolator_iter == property_interp_map_.end()) {
129 interpolator_ptr = std::make_shared<interp::lerp>();
130 } else {
131 interpolator_ptr = interpolator_iter->second;
132 }
133
134 property_timeline_t& prop_timeline = map[prop_id];
135
136 prop_timeline.keyframes.push_back(
137 {x,
138 refl::any::make(
139 refl::type_info::from<PropType>(), &Field::from_instance(properties_obj_)
140 ),
141 [=](refl::any from_a, refl::any to_a, float x) -> refl::any {
142 PropType& from = from_a.as<PropType>();
143 PropType& to = to_a.as<PropType>();
144 if constexpr (std::is_same_v<PropType, float>) {
145 PropType result = interpolator_ptr->interpolate(from, to, x);
146 return refl::any::make(result);
147 } else if constexpr (HasInterpolationMapping<PropType>) {
148 float from_f = interp_mapping<PropType>::to_float(from);
149 float to_f = interp_mapping<PropType>::to_float(to);
150 float result_f = interpolator_ptr->interpolate(from_f, to_f, x);
151 PropType result = interp_mapping<PropType>::from_float(result_f);
152 return refl::any::make(result);
153 } else {
154 return from_a;
155 }
156 },
157 easing_fun}
158 );
159 }
160
161 void set_property_interpolator(
162 const std::string& property_name, interpolator_base::sptr i
163 ) override {
164 for_each_prop([&]<typename Field>(auto& prop) {
165 if (Field::name == property_name) {
166 refl::type_id_t property_type_name = refl::type_id<typename Field::type>;
167 property_id_t property_id{property_type_name, property_name};
168 property_interp_map_[property_id] = i;
169 }
170 });
171 }
172 void set_property_interpolator(
173 const property_id_t& property_id, interpolator_base::sptr i
174 ) override {
175 const auto& [property_type_name, property_name] = property_id;
176 for_each_prop([&]<typename Field>(auto& prop) {
177 if (Field::name == property_name
178 and refl::type_id<typename Field::type> == property_type_name) {
179 property_interp_map_[property_id] = i;
180 }
181 });
182 }
183
184 void for_each_prop(auto&& fun) {
185 [&]<std::size_t... I>(std::index_sequence<I...>) {
186 (for_each_prop_i<refl::field<PropertiesType, I>>(std::forward<decltype(fun)>(fun)), ...);
187 }(std::make_index_sequence<refl::field_count<PropertiesType>>{});
188 }
189 template <typename Field>
190 void for_each_prop_i(auto&& fun) {
191 fun.template operator()<Field>(Field::from_instance(properties_obj_));
192 }
193
194 PropertiesType properties_obj_;
195 std::unordered_map<property_id_t, interpolator_base::sptr> property_interp_map_{};
196 };
197
198 public:
199 template <class PropertiesType>
200 static keyframe make(float position, PropertiesType value = {}) {
201 keyframe kf;
202 kf.position_ = position;
203 kf.properties_ = std::make_shared<properties_t<PropertiesType>>(value);
204 return kf;
205 }
206
207 friend class animation_data;
208
210 easing_function_ = fun;
211 return *this;
212 }
213
214 template <typename I>
215 requires(std::derived_from<I, interpolator_base>)
216 keyframe& interp(const std::string& property_name, I interpolator) {
217 properties_->set_property_interpolator(property_name, std::make_shared<I>(interpolator));
218 return *this;
219 }
220
221 template <typename I>
222 requires(std::derived_from<I, interpolator_base>)
223 keyframe& interp(property_id_t property, I interpolator) {
224 properties_->set_property_interpolator(property, std::make_shared<I>(interpolator));
225 return *this;
226 }
227
228 keyframe& interp(const std::string& property_name, easing::function_type fun) {
229 properties_->set_property_interpolator(property_name, std::make_shared<interp::easing>(fun));
230 return *this;
231 }
232
233 keyframe& interp(property_id_t property, easing::function_type fun) {
234 properties_->set_property_interpolator(property, std::make_shared<interp::easing>(fun));
235 return *this;
236 }
237
238 keyframe& config(auto&& fun) {
239 fun(*this);
240 return *this;
241 }
242
243 private:
244 void add_to_timeline_map(property_timeline_map_t& map) {
245 properties_->add_to_timeline_map(position_, map, easing_function_);
246 }
247
248 private:
249 float position_{0.0f};
250 easing::function_type easing_function_{easing::linear};
251 std::shared_ptr<properties_base_t> properties_{};
252 // std::unordered_map<std::pair<refl::type_id_t, std::string>,
253 // std::shared_ptr<properties_base_t>> properties_{};
254 };
255
256 export class AnimationSystem;
257 export class animation;
258
259 export struct animation_opts {
260 using duration_t = std::chrono::system_clock::duration;
261
262 animation_opts() = default;
263
265 easing_function_ = fun;
266 return *this;
267 }
268
270 duration_ = d;
271 return *this;
272 }
273
276 };
277
278 class animation_data {
279 friend class animation;
280 friend class AnimationSystem;
281
282 using duration = std::chrono::system_clock::duration;
283
284 animation_data() = default;
285
286 explicit animation_data(std::initializer_list<keyframe> keyframes, animation_opts& opts)
287 : options_(opts) {
288 for (const auto& kf: keyframes) {
289 keyframes_.emplace_back(kf.position_, kf);
290 }
291 compile_timelines();
292 }
293
294 private:
295 void compile_timelines() {
296 timelines_.clear();
297 for (auto& [x, kf]: keyframes_) {
298 kf.add_to_timeline_map(timelines_);
299 }
300 }
301
302 private:
303 std::vector<std::pair<double, keyframe>> keyframes_;
304 property_timeline_map_t timelines_{};
305
306 animation_opts options_{};
307 };
308
309 export using keyframe_list = std::initializer_list<keyframe>;
310
311 class animation {
312 public:
313 friend class AnimationSystem;
314
315 using duration = animation_data::duration;
316 using frame_iterator_t = decltype(animation_data::keyframes_.begin());
317
318 explicit animation(std::initializer_list<keyframe> keyframes, animation_opts opts = {})
319 : data_(std::shared_ptr<animation_data>(new animation_data{keyframes, opts})) {}
320
321
322 private:
323 std::shared_ptr<animation_data> data_;
324 };
325
326 export struct animation_state {
327 friend class AnimationSystem;
328
330 animation anim_,
332 std::chrono::system_clock::time_point started_
333 )
334 : anim(anim_),
335 component(component_),
336 started(started_) {}
337
340 std::chrono::system_clock::time_point started;
341
342 private:
344 std::unordered_map<
345 std::pair<refl::type_id_t, std::string>,
346 std::tuple<const refl::field_info*, refl::any>>
347 property_map{};
348 };
349
351 fabric::tasks::executor::sptr executor_;
352 std::atomic_flag enabled_{false};
353 std::atomic_flag stop_flag_{false};
354
355 public:
356 fabric::wiring::output_signal<AnimationSystem, components::component_base_t::sptr> s_repaint{};
357 fabric::wiring::output_signal<AnimationSystem, bool&> s_render_all{};
358 fabric::wiring::output_signal<AnimationSystem> s_compose_all{};
359
360 explicit AnimationSystem(const fabric::tasks::executor::sptr& executor)
361 : executor_(executor) {}
362
363 void enable() {
364 if (enabled_.test_and_set()) {
365 return;
366 }
367
368 stop_flag_.clear();
369 executor_->schedule([&] -> fabric::task<> {
370 while (not this->stop_flag_.test()) {
371 this->run();
372 co_await 16ms;
373 }
374 co_return;
375 });
376 }
377 void disable() {
378 if (not enabled_.test()) {
379 return;
380 }
381 stop_flag_.test_and_set();
382 enabled_.clear();
383 }
384
385 private:
386 void run() {
387 ZoneScopedN("AnimationSystem");
388 auto now = std::chrono::system_clock::now();
389
390 std::forward_list<components::component_base_t::sptr> c_pending_compose{};
391 std::forward_list<components::component_base_t::sptr> c_pending_repaint{};
392 std::forward_list<components::component_base_t::sptr> c_pending_reflow{};
393 std::forward_list<components::component_base_t::sptr> c_pending_full_update{};
394 std::forward_list<components::component_base_t::sptr> c_pending_deanimation{};
395
396 {
397 ZoneScopedN("advance");
398 for (auto anim = active_animations_.begin(); anim != active_animations_.end();) {
399 switch (anim->complexity) {
401 c_pending_reflow.push_front(anim->component.lock());
402 break;
404 c_pending_repaint.push_front(anim->component.lock());
405 break;
407 c_pending_compose.push_front(anim->component.lock());
408 break;
410 c_pending_full_update.push_front(anim->component.lock());
411 break;
412 }
413
414 if (not advance_animation(anim, now)) {
415 c_pending_deanimation.push_front(anim->component.lock());
416 anim = active_animations_.erase(anim);
417 } else {
418 ++anim;
419 }
420 }
421 // LOG::print {DEBUG}("Animations ON");
422 }
423
424 // De-animate components that need it
425 for (const auto& c: c_pending_deanimation) {
427 }
428
429 bool needs_compositing = false;
430
431 // TODO - Recompute dimensions if needed
432
433 // Repaint components that need it
434 if (not c_pending_repaint.empty()) {
435 for (const auto& c: c_pending_repaint) {
436 s_repaint.emit(c);
437 }
438 s_render_all.emit(needs_compositing);
439 }
440
441 // Recompose components that need it
442 if (not c_pending_compose.empty() or needs_compositing) {
443 s_compose_all.emit();
444 }
445
446 if (active_animations_.empty()) {
447 // LOG::print{DEBUG}("Animations OFF");
448 this->disable();
449 }
450 }
451
452 public:
453 void
455 components::component_state_delegate_t::set_animated(component->state().get(), true);
456 active_animations_.push_back(make_animation_state(anim, component));
457 }
458
459 private:
460 animation_state make_animation_state(
462 ) {
463 animation_state state{anim, component, std::chrono::system_clock::now()};
464
465 const refl::type_info& style_ti = component->get_style_type_info();
466
467 for (const auto& [prop_id, prop_timeline]: anim.data_->timelines_) {
468 auto& [type_id, prop_name] = prop_id;
469
470 for (const auto& field_ti: style_ti.fields()) {
471 if (field_ti.type().id() == type_id and field_ti.name == prop_name) {
473
474 void* field_ptr = field_ti.get_ptr(component->get_style_data().as_raw());
475 refl::any initial_value = refl::any::make(field_ti.type(), field_ptr);
476
477 state.property_map[prop_id] = {&field_ti, initial_value};
478 if (complexity > state.complexity) {
479 state.complexity = complexity;
480 }
481
482 break;
483 }
484 }
485 }
486
487 return state;
488 }
489
490 bool advance_animation(
491 std::list<animation_state>::iterator& anim, std::chrono::system_clock::time_point now
492 ) {
493 auto x = (now - anim->started).count()
494 / static_cast<float>(anim->anim.data_->options_.duration_.count());
495 x = anim->anim.data_->options_.easing_function_(x);
496
497 const bool is_complete = x > 1.0;
498 if (is_complete or anim->component.expired()) {
499 if (not anim->component.expired()) {
500 apply_animation_frame(anim, 1.0);
501 }
502 return false;
503 }
504
505 apply_animation_frame(anim, x);
506 return true;
507 }
508
509 void apply_animation_frame(std::list<animation_state>::iterator& anim, float frame_x) {
510 auto component = anim->component.lock();
511 for (const auto& [prop_id, prop_info]: anim->property_map) {
512 const auto& [field_info, initial_value] = prop_info;
513 auto& prop_timeline = anim->anim.data_->timelines_.at(prop_id);
514
515 refl::any interpolated_value = prop_timeline.interpolate(frame_x, initial_value);
516 void* field_ptr = field_info->get_ptr(component->get_style_data().as_raw());
517 field_info->type().assign_copy_of(interpolated_value.data(), field_ptr);
518 }
519 }
520
521 private:
522 std::list<animation_state> active_animations_{};
523 };
524
525 export void animate(const components::component_base_t::sptr& component, animation& anim) {
526 auto c_state = component->state();
527 if (c_state != nullptr) {
528 auto win = c_state->window;
529
530 auto& resource_context = *win->get_executor()->get_spawn_context();
531 auto& anim_sys = *resource_context.get_resource<AnimationSystem>();
532
533 anim_sys.enable();
534
535
536 anim_sys.start_animation(anim, component);
537 }
538 }
539} // namespace cydui
void start_animation(animation &anim, const cydui::components::component_base_t::sptr &component)
AnimationSystem(const fabric::tasks::executor::sptr &executor)
fabric::wiring::output_signal< AnimationSystem, bool & > s_render_all
fabric::wiring::output_signal< AnimationSystem, components::component_base_t::sptr > s_repaint
fabric::wiring::output_signal< AnimationSystem > s_compose_all
friend class AnimationSystem
decltype(animation_data::keyframes_.begin()) frame_iterator_t
animation(std::initializer_list< keyframe > keyframes, animation_opts opts={})
friend class AnimationSystem
animation_data::duration duration
std::weak_ptr< component_base_t > wptr
std::shared_ptr< component_base_t > sptr
static void set_animated(component_state_t *it, bool animated)
std::shared_ptr< interpolator_base > sptr
properties_t(PropertiesType value={})
keyframe & easing(easing::function_type fun)
friend class animation_data
static keyframe make(float position, PropertiesType value={})
keyframe & interp(property_id_t property, easing::function_type fun)
keyframe & interp(const std::string &property_name, easing::function_type fun)
keyframe & config(auto &&fun)
keyframe & interp(const std::string &property_name, I interpolator)
keyframe & interp(property_id_t property, I interpolator)
constexpr float_type linear(const float_type &t)
std::function< float_type(const float_type &)> function_type
std::pair< refl::type_id_t, std::string > property_id_t
void animate(const components::component_base_t::sptr &component, animation &anim)
std::unordered_map< property_id_t, property_timeline_t > property_timeline_map_t
std::initializer_list< keyframe > keyframe_list
AnimationComplexity
animation_opts & duration(duration_t d)
std::chrono::system_clock::duration duration_t
animation_opts & easing(easing::function_type fun)
easing::function_type easing_function_
friend class AnimationSystem
components::component_base_t::wptr component
animation_state(animation anim_, components::component_base_t::wptr component_, std::chrono::system_clock::time_point started_)
std::chrono::system_clock::time_point started
std::list< std::tuple< double, refl::any, std::function< refl::any(refl::any, refl::any, double)>, easing::function_type > > keyframes
refl::any interpolate(double x, refl::any initial_value) const
std::size_t operator()(const cydui::property_id_t &x) const noexcept