Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
animation.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <cmath>
4#include <functional>
5#include <vector>
6#include <algorithm>
7#include <string>
8#include <utility>
9
10#include <pulp/view/motion.hpp> // for motion::Provenance + publish_value
12
13#if defined(__has_include)
14# if __has_include(<source_location>)
15# include <source_location>
16# endif
17#endif
18
19namespace pulp::view {
20
21class FrameClock; // forward declaration
22
23// ── Easing functions ─────────────────────────────────────────────────────────
24
25namespace easing {
26 inline float linear(float t) { return t; }
27 inline float ease_in_quad(float t) { return t * t; }
28 inline float ease_out_quad(float t) { return t * (2.0f - t); }
29 inline float ease_in_out_quad(float t) {
30 return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
31 }
32 inline float ease_in_cubic(float t) { return t * t * t; }
33 inline float ease_out_cubic(float t) { float u = t - 1.0f; return u * u * u + 1.0f; }
34 inline float ease_in_out_cubic(float t) {
35 return t < 0.5f ? 4.0f * t * t * t : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f;
36 }
37 inline float ease_in_expo(float t) { return t == 0 ? 0 : std::pow(2.0f, 10.0f * (t - 1.0f)); }
38 inline float ease_out_expo(float t) { return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t); }
39 inline float ease_out_elastic(float t) {
40 if (t == 0 || t == 1) return t;
41 return std::pow(2.0f, -10.0f * t) * std::sin((t - 0.075f) * (2.0f * 3.14159f) / 0.3f) + 1.0f;
42 }
43 inline float ease_out_bounce(float t) {
44 if (t < 1.0f / 2.75f) return 7.5625f * t * t;
45 if (t < 2.0f / 2.75f) { t -= 1.5f / 2.75f; return 7.5625f * t * t + 0.75f; }
46 if (t < 2.5f / 2.75f) { t -= 2.25f / 2.75f; return 7.5625f * t * t + 0.9375f; }
47 t -= 2.625f / 2.75f; return 7.5625f * t * t + 0.984375f;
48 }
49}
50
51using EasingFunction = float(*)(float);
52
56 float min_value = 0.0f;
57 float max_value = 1.0f;
58 float min_duration = 0.0f; // 0 = instant allowed
59 float max_duration = 60.0f; // 60 seconds max
60 bool clamp_value = true; // Whether to clamp output to [min, max]
61};
62
63// ── Tween ────────────────────────────────────────────────────────────────────
64
65// Animates a float from start to end over a duration.
66//
67// Honors `MotionPreferences::current()` at construction and `reset()`:
68// - Full → animate over the configured duration (default).
69// - Reduced → scale the configured duration by `duration_scale`.
70// - Off → finish immediately at `to`; no intermediate values
71// are produced.
72class Tween {
73public:
74 Tween() = default;
75 Tween(float from, float to, float duration_seconds, EasingFunction ease = easing::linear)
76 : from_(from), to_(to),
77 configured_duration_(duration_seconds),
78 duration_(duration_seconds), ease_(ease) {
79 apply_motion_policy_at_start();
80 }
81
82 // Advance by dt seconds. Returns current value.
83 float advance(float dt) {
84 elapsed_ += dt;
85 if (duration_ <= 0.0f) {
86 current_ = to_;
87 return current_;
88 }
89 float t = std::clamp(elapsed_ / duration_, 0.0f, 1.0f);
90 float eased = ease_(t);
91 current_ = from_ + (to_ - from_) * eased;
92 return current_;
93 }
94
95 float current() const { return current_; }
96 bool finished() const { return elapsed_ >= duration_; }
97
98 void reset() {
99 elapsed_ = 0;
100 current_ = from_;
101 duration_ = configured_duration_;
102 apply_motion_policy_at_start();
103 }
104
105 // ── Motion provenance ────────────────────────────────────────────
106 //
107 // Attach a `motion::Provenance` envelope to this tween. When
108 // `publish(view, metric)` is called, the publish channel stamps
109 // the envelope onto every emitted event so an offline reader can
110 // answer "what code created this tween?". Off by default; tweens that
111 // don't call this method behave identically.
112 //
113 // The convenience `PULP_MOTION_TWEEN(...)` macro auto-fills
114 // `source_file` / `source_line` from `std::source_location` at the
115 // construction site.
116 void set_motion_provenance(std::string source_kind,
117 std::string source_id,
118 std::string source_file = {},
119 int source_line = 0) {
120 provenance_.source_kind = std::move(source_kind);
121 provenance_.source_id = std::move(source_id);
122 provenance_.source_file = std::move(source_file);
123 provenance_.source_line = source_line;
124 }
125
129 provenance_ = std::move(p);
130 }
131
132 const motion::Provenance& motion_provenance() const noexcept {
133 return provenance_;
134 }
135
141 void publish(std::string view_name,
142 std::string metric_name,
143 motion::PublishOptions opts = {}) const {
144 if (provenance_.is_set() && !opts.provenance.is_set()) {
145 opts.provenance = provenance_;
146 }
147 motion::publish_value(std::move(view_name), std::move(metric_name),
148 static_cast<double>(current_), opts);
149 }
150
151private:
156 void apply_motion_policy_at_start() {
157 if (motion_policy_is_off()) {
158 duration_ = 0.0f;
159 elapsed_ = 0.0f; // advance() returns to_ immediately
160 current_ = to_;
161 return;
162 }
165 duration_ = configured_duration_ * static_cast<float>(scale);
166 }
167 }
168
169 float from_ = 0, to_ = 0;
170 float configured_duration_ = 0;
172 float duration_ = 0;
173 float elapsed_ = 0;
174 float current_ = 0;
176 motion::Provenance provenance_;
177};
178
179// ── PULP_MOTION_TWEEN ────────────────────────────────────────────────────
180//
181// Convenience macro: constructs a `Tween` and stamps a `motion::Provenance`
182// envelope with `source_kind = "tween"`, the supplied `source_id`, and
183// `source_file` / `source_line` captured from `std::source_location` at
184// the call site. The resulting tween is returned by value so the call
185// site looks like `auto t = PULP_MOTION_TWEEN("hover-glow", 0.0f, 1.0f, 0.2f);`.
186//
187// `__FILE__` / `__LINE__` fall back if `<source_location>` isn't available
188// (e.g. exotic toolchains); Pulp targets C++20 so the modern path is the
189// default.
190#ifdef __cpp_lib_source_location
191#define PULP_MOTION_TWEEN(source_id, ...) \
192 ([](::pulp::view::Tween&& __pulp_tween_, \
193 const ::std::source_location __pulp_loc_ = \
194 ::std::source_location::current()) \
195 -> ::pulp::view::Tween { \
196 __pulp_tween_.set_motion_provenance( \
197 ::std::string{"tween"}, \
198 ::std::string{(source_id)}, \
199 ::std::string{__pulp_loc_.file_name()}, \
200 static_cast<int>(__pulp_loc_.line())); \
201 return __pulp_tween_; \
202 }(::pulp::view::Tween(__VA_ARGS__)))
203#else
204#define PULP_MOTION_TWEEN(source_id, ...) \
205 ([](::pulp::view::Tween&& __pulp_tween_) -> ::pulp::view::Tween { \
206 __pulp_tween_.set_motion_provenance( \
207 ::std::string{"tween"}, \
208 ::std::string{(source_id)}, \
209 ::std::string{__FILE__}, \
210 static_cast<int>(__LINE__)); \
211 return __pulp_tween_; \
212 }(::pulp::view::Tween(__VA_ARGS__)))
213#endif
214
215// ── AnimationManager ─────────────────────────────────────────────────────────
216
217// Manages active animations. Call tick() once per frame.
219public:
220 struct AnimationId { int value = -1; };
221
222 // Start a new animation with a callback
223 AnimationId animate(float from, float to, float duration,
224 EasingFunction ease,
225 std::function<void(float)> on_update,
226 std::function<void()> on_complete = {}) {
227 int id = next_id_++;
228 animations_.push_back({id, {from, to, duration, ease},
229 std::move(on_update), std::move(on_complete)});
230 return {id};
231 }
232
233 // Cancel an animation
235 animations_.erase(
236 std::remove_if(animations_.begin(), animations_.end(),
237 [id](const auto& a) { return a.id == id.value; }),
238 animations_.end());
239 }
240
241 // Advance all animations by dt seconds
242 void tick(float dt) {
243 for (auto it = animations_.begin(); it != animations_.end();) {
244 it->tween.advance(dt);
245 if (it->on_update) it->on_update(it->tween.current());
246
247 if (it->tween.finished()) {
248 if (it->on_complete) it->on_complete();
249 it = animations_.erase(it);
250 } else {
251 ++it;
252 }
253 }
254 }
255
256 int active_count() const { return static_cast<int>(animations_.size()); }
257 bool has_active() const { return !animations_.empty(); }
258
259private:
260 struct Animation {
261 int id;
262 Tween tween;
263 std::function<void(float)> on_update;
264 std::function<void()> on_complete;
265 };
266
267 std::vector<Animation> animations_;
268 int next_id_ = 0;
269};
270
271// ── easing_by_name ──────────────────────────────────────────────────────────
272
274inline EasingFunction easing_by_name(const std::string& name) {
275 if (name == "linear") return easing::linear;
276 if (name == "ease_in_quad") return easing::ease_in_quad;
277 if (name == "ease_out_quad") return easing::ease_out_quad;
278 if (name == "ease_in_out_quad") return easing::ease_in_out_quad;
279 if (name == "ease_in_cubic") return easing::ease_in_cubic;
280 if (name == "ease_out_cubic") return easing::ease_out_cubic;
281 if (name == "ease_in_out_cubic") return easing::ease_in_out_cubic;
282 if (name == "ease_in_expo") return easing::ease_in_expo;
283 if (name == "ease_out_expo") return easing::ease_out_expo;
284 if (name == "ease_out_elastic") return easing::ease_out_elastic;
285 if (name == "ease_out_bounce") return easing::ease_out_bounce;
286 return easing::linear;
287}
288
289// ── ValueAnimation ──────────────────────────────────────────────────────────
290
294public:
295 ValueAnimation() = default;
296 explicit ValueAnimation(float initial) : current_(initial), target_(initial), from_(initial) {}
297
304 void animate_to(float target, float duration, EasingFunction ease = easing::ease_out_quad) {
305 from_ = current_;
306 target_ = target;
307 configured_duration_ = duration;
308 duration_ = duration;
309 elapsed_ = 0;
310 ease_ = ease;
311 if (motion_policy_is_off()) {
312 current_ = target;
313 duration_ = 0.0f;
314 animating_ = false;
315 return;
316 }
319 duration_ = duration * static_cast<float>(scale);
320 }
321 if (duration_ <= 0) {
322 current_ = target;
323 animating_ = false;
324 } else {
325 animating_ = true;
326 }
327 }
328
330 void set(float value) {
331 current_ = value;
332 target_ = value;
333 from_ = value;
334 elapsed_ = 0;
335 animating_ = false;
336 }
337
339 bool advance(float dt) {
340 if (!animating_) return false;
341 elapsed_ += dt;
342 float t = std::clamp(elapsed_ / duration_, 0.0f, 1.0f);
343 current_ = from_ + (target_ - from_) * ease_(t);
344 if (t >= 1.0f) {
345 current_ = target_;
346 animating_ = false;
347 }
348 return animating_;
349 }
350
351 float value() const { return current_; }
352 float target() const { return target_; }
353 bool animating() const { return animating_; }
354
356 void cancel() {
357 target_ = current_;
358 from_ = current_;
359 animating_ = false;
360 }
361
362private:
363 float current_ = 0;
364 float target_ = 0;
365 float from_ = 0;
366 float configured_duration_ = 0;
367 float duration_ = 0;
368 float elapsed_ = 0;
369 bool animating_ = false;
371};
372
373// ── Keyframe Animation (CSS @keyframes equivalent) ──────────────────────────
374
376struct Keyframe {
377 float offset;
378 float value;
379};
380
383public:
386
387 KeyframeAnimation() = default;
388
389 void set_keyframes(std::vector<Keyframe> kf) {
390 keyframes_ = std::move(kf);
391 std::sort(keyframes_.begin(), keyframes_.end(),
392 [](const Keyframe& a, const Keyframe& b) { return a.offset < b.offset; });
393 }
394
395 void set_duration(float seconds) { duration_ = seconds; }
396 void set_iterations(float count) { iterations_ = count; } // 0 = infinite
397 void set_direction(Direction d) { direction_ = d; }
398 void set_fill_mode(FillMode f) { fill_ = f; }
399 void set_easing(EasingFunction e) { ease_ = e; }
400
401 void start() { elapsed_ = 0; running_ = true; finished_ = false; completed_iterations_ = 0; }
402 void stop() { running_ = false; }
403 void pause() { running_ = false; }
404 void resume() { running_ = true; }
405 bool is_running() const { return running_; }
406 bool is_finished() const { return finished_; }
407
409 float advance(float dt) {
410 if (!running_ || keyframes_.size() < 2 || duration_ <= 0) return value_;
411
412 elapsed_ += dt;
413 float iteration_progress = elapsed_ / duration_;
414
415 if (iterations_ > 0 && iteration_progress >= iterations_) {
416 finished_ = true;
417 running_ = false;
418 iteration_progress = iterations_;
419 }
420
421 // Current iteration number and progress within it. When a finite
422 // animation finishes exactly on an iteration boundary, keep the
423 // terminal frame at t=1 for the final iteration instead of wrapping
424 // to t=0 of the next iteration.
425 float iter_num = std::floor(iteration_progress);
426 float t = iteration_progress - iter_num;
427 if (finished_ && iterations_ > 0.0f && t == 0.0f && iteration_progress > 0.0f) {
428 iter_num -= 1.0f;
429 t = 1.0f;
430 }
431 completed_iterations_ = static_cast<int>(iter_num);
432
433 // Direction
434 bool reversed = false;
435 if (direction_ == Direction::reverse) reversed = true;
436 else if (direction_ == Direction::alternate) reversed = (static_cast<int>(iter_num) % 2) == 1;
437
438 if (reversed) t = 1.0f - t;
439
440 // Apply easing
441 t = ease_(std::clamp(t, 0.0f, 1.0f));
442
443 // Interpolate between keyframes
444 value_ = interpolate(t);
445
446 // Fill mode
447 if (finished_) {
448 if (fill_ == FillMode::forwards || fill_ == FillMode::both)
449 value_ = interpolate(reversed ? 0.0f : 1.0f);
450 }
451
452 return value_;
453 }
454
455 float value() const { return value_; }
456
457private:
458 float interpolate(float t) const {
459 if (keyframes_.empty()) return 0;
460 if (t <= keyframes_.front().offset) return keyframes_.front().value;
461 if (t >= keyframes_.back().offset) return keyframes_.back().value;
462
463 for (size_t i = 1; i < keyframes_.size(); ++i) {
464 if (t <= keyframes_[i].offset) {
465 float local_t = (t - keyframes_[i-1].offset) /
466 (keyframes_[i].offset - keyframes_[i-1].offset);
467 return keyframes_[i-1].value + local_t * (keyframes_[i].value - keyframes_[i-1].value);
468 }
469 }
470 return keyframes_.back().value;
471 }
472
473 std::vector<Keyframe> keyframes_;
474 float duration_ = 1.0f;
475 float iterations_ = 1.0f;
476 float elapsed_ = 0;
477 float value_ = 0;
478 int completed_iterations_ = 0;
479 bool running_ = false;
480 bool finished_ = false;
481 Direction direction_ = Direction::normal;
482 FillMode fill_ = FillMode::none;
484};
485
486} // namespace pulp::view
Definition animation.hpp:218
AnimationId animate(float from, float to, float duration, EasingFunction ease, std::function< void(float)> on_update, std::function< void()> on_complete={})
Definition animation.hpp:223
void tick(float dt)
Definition animation.hpp:242
int active_count() const
Definition animation.hpp:256
bool has_active() const
Definition animation.hpp:257
void cancel(AnimationId id)
Definition animation.hpp:234
Definition frame_clock.hpp:14
void set_iterations(float count)
Definition animation.hpp:396
void pause()
Definition animation.hpp:403
bool is_finished() const
Definition animation.hpp:406
void start()
Definition animation.hpp:401
float advance(float dt)
Advance by dt seconds, return current interpolated value.
Definition animation.hpp:409
Direction
Definition animation.hpp:384
@ reverse
Definition animation.hpp:384
@ alternate
Definition animation.hpp:384
@ normal
Definition animation.hpp:384
void stop()
Definition animation.hpp:402
void set_duration(float seconds)
Definition animation.hpp:395
float value() const
Definition animation.hpp:455
void resume()
Definition animation.hpp:404
void set_fill_mode(FillMode f)
Definition animation.hpp:398
void set_keyframes(std::vector< Keyframe > kf)
Definition animation.hpp:389
void set_easing(EasingFunction e)
Definition animation.hpp:399
void set_direction(Direction d)
Definition animation.hpp:397
bool is_running() const
Definition animation.hpp:405
FillMode
Definition animation.hpp:385
@ none
Definition animation.hpp:385
@ backwards
Definition animation.hpp:385
@ forwards
Definition animation.hpp:385
@ both
Definition animation.hpp:385
static MotionPolicy current()
Convenience: the effective policy right now.
Definition motion_preferences.hpp:45
static double current_duration_scale()
Convenience: the effective duration scale right now.
Definition motion_preferences.hpp:48
Definition animation.hpp:72
const motion::Provenance & motion_provenance() const noexcept
Definition animation.hpp:132
bool finished() const
Definition animation.hpp:96
float advance(float dt)
Definition animation.hpp:83
void publish(std::string view_name, std::string metric_name, motion::PublishOptions opts={}) const
Publish the tween's current value through the motion publish channel under the given (view,...
Definition animation.hpp:141
float current() const
Definition animation.hpp:95
Tween(float from, float to, float duration_seconds, EasingFunction ease=easing::linear)
Definition animation.hpp:75
void set_motion_provenance(std::string source_kind, std::string source_id, std::string source_file={}, int source_line=0)
Definition animation.hpp:116
void reset()
Definition animation.hpp:98
void set_motion_provenance(motion::Provenance p)
Set the full envelope.
Definition animation.hpp:128
float value() const
Definition animation.hpp:351
bool animating() const
Definition animation.hpp:353
ValueAnimation(float initial)
Definition animation.hpp:296
void cancel()
Cancel in-flight animation, keep current value.
Definition animation.hpp:356
float target() const
Definition animation.hpp:352
bool advance(float dt)
Advance by dt. Returns true if still animating.
Definition animation.hpp:339
void set(float value)
Snap to value immediately (no animation).
Definition animation.hpp:330
void animate_to(float target, float duration, EasingFunction ease=easing::ease_out_quad)
Set a new target.
Definition animation.hpp:304
Agent-first motion observability: sample view geometry, scroll state, and arbitrary scalar values at ...
System-level reduced-motion preferences.
Definition animation.hpp:25
float ease_out_expo(float t)
Definition animation.hpp:38
float ease_in_out_cubic(float t)
Definition animation.hpp:34
float ease_in_expo(float t)
Definition animation.hpp:37
float ease_in_quad(float t)
Definition animation.hpp:27
float ease_out_quad(float t)
Definition animation.hpp:28
float ease_out_elastic(float t)
Definition animation.hpp:39
float ease_out_bounce(float t)
Definition animation.hpp:43
float linear(float t)
Definition animation.hpp:26
float ease_in_out_quad(float t)
Definition animation.hpp:29
float ease_in_cubic(float t)
Definition animation.hpp:32
float ease_out_cubic(float t)
Definition animation.hpp:33
void publish_value(std::string view_name, std::string metric_name, double value, PublishOptions opts={})
Single-component publish.
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
bool motion_policy_is_off()
Returns true when the current policy says "do not animate" — i.e.
Definition motion_preferences.hpp:133
@ a
Definition input_events.hpp:39
@ u
Definition input_events.hpp:41
@ d
Definition input_events.hpp:39
@ p
Definition input_events.hpp:40
@ i
Definition input_events.hpp:40
@ f
Definition input_events.hpp:39
@ b
Definition input_events.hpp:39
@ e
Definition input_events.hpp:39
@ t
Definition input_events.hpp:41
@ scale
Definition css_animation.hpp:126
EasingFunction easing_by_name(const std::string &name)
Resolve an easing function by name string. Returns linear for unknown names.
Definition animation.hpp:274
float(*)(float) EasingFunction
Definition animation.hpp:51
@ Reduced
Animate, but scale duration by duration_scale.
Definition motion_preferences.hpp:30
Definition animation.hpp:220
int value
Definition animation.hpp:220
A single keyframe: offset (0-1) and value.
Definition animation.hpp:376
float value
Definition animation.hpp:378
float offset
0.0 = start, 1.0 = end
Definition animation.hpp:377
Compile-time animation limits for optimization.
Definition animation.hpp:55
bool clamp_value
Definition animation.hpp:60
float min_value
Definition animation.hpp:56
float max_duration
Definition animation.hpp:59
float max_value
Definition animation.hpp:57
float min_duration
Definition animation.hpp:58
Definition motion.hpp:77
std::string source_file
Optional, e.g. FILE at attach site.
Definition motion.hpp:82
std::string source_kind
"tween" | "css-transition" | "animator-set" | "rAF" | "design-import" | "user" | "publish"
Definition motion.hpp:78
int source_line
Optional, e.g. LINE at attach site.
Definition motion.hpp:83
bool is_set() const noexcept
Definition motion.hpp:85
std::string source_id
Free-form id, e.g.
Definition motion.hpp:80
Definition motion.hpp:181