Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
css_animation.hpp
Go to the documentation of this file.
1// CSS-spec types — `transition` shorthand parser, easing curves
2// matching the CSS Animations Module Level 1 vocabulary, the
3// AnimatableProperty enum the dispatcher consults, and the
4// KeyframesRegistry that holds @keyframes blocks. Sits alongside the
5// existing animation.hpp (which holds Pulp's AnimationManager — used
6// for native UI animations driven via the FrameClock) without
7// colliding: this file is the CSS-driven side, that file is the
8// host-app-driven side.
9//
10#pragma once
11
12#include <cctype>
13#include <cmath>
14#include <cstdint>
15#include <cstdlib>
16#include <string>
17#include <utility>
18#include <vector>
19
20#include <pulp/view/motion.hpp>
22
23namespace pulp::view {
24
29struct CssEasing {
30 enum class Kind {
32 ease, // cubic-bezier(0.25, 0.1, 0.25, 1)
33 ease_in, // cubic-bezier(0.42, 0, 1, 1)
34 ease_out, // cubic-bezier(0, 0, 0.58, 1)
35 ease_in_out, // cubic-bezier(0.42, 0, 0.58, 1)
36 cubic_bezier, // custom 4-control-point bezier
37 steps_start, // steps(N, jump-start)
38 steps_end, // steps(N, jump-end)
39 };
40
49 float p1x = 0.25f, p1y = 0.1f, p2x = 0.25f, p2y = 1.0f; // cubic-bezier
50 int steps_count = 1; // steps()
51
53 float at(float t) const {
54 if (t <= 0.0f) return 0.0f;
55 if (t >= 1.0f) return 1.0f;
56 switch (kind) {
57 case Kind::linear:
58 return t;
59 case Kind::ease:
60 return cubic_bezier_y(t, 0.25f, 0.1f, 0.25f, 1.0f);
61 case Kind::ease_in:
62 return cubic_bezier_y(t, 0.42f, 0.0f, 1.0f, 1.0f);
63 case Kind::ease_out:
64 return cubic_bezier_y(t, 0.0f, 0.0f, 0.58f, 1.0f);
66 return cubic_bezier_y(t, 0.42f, 0.0f, 0.58f, 1.0f);
68 return cubic_bezier_y(t, p1x, p1y, p2x, p2y);
69 case Kind::steps_start: {
70 const float step = 1.0f / static_cast<float>(steps_count);
71 const float v = std::floor(t / step) * step + step;
72 return v > 1.0f ? 1.0f : v;
73 }
74 case Kind::steps_end: {
75 const float step = 1.0f / static_cast<float>(steps_count);
76 return std::floor(t / step) * step;
77 }
78 }
79 return t;
80 }
81
85 static float cubic_bezier_y(float x, float p1x, float p1y, float p2x, float p2y) {
86 const float cx = 3.0f * p1x;
87 const float bx = 3.0f * (p2x - p1x) - cx;
88 const float ax = 1.0f - cx - bx;
89 const float cy = 3.0f * p1y;
90 const float by = 3.0f * (p2y - p1y) - cy;
91 const float ay = 1.0f - cy - by;
92 float u = x;
93 for (int i = 0; i < 6; ++i) {
94 const float xu = ((ax * u + bx) * u + cx) * u;
95 const float dxu = (3.0f * ax * u + 2.0f * bx) * u + cx;
96 if (dxu == 0.0f) break;
97 const float du = (xu - x) / dxu;
98 u -= du;
99 if (std::abs(du) < 1e-5f) break;
100 }
101 return ((ay * u + by) * u + cy) * u;
102 }
103
105 static CssEasing from_keyword(const std::string& s) {
106 CssEasing e;
107 if (s == "ease") e.kind = Kind::ease;
108 else if (s == "ease-in") e.kind = Kind::ease_in;
109 else if (s == "ease-out") e.kind = Kind::ease_out;
110 else if (s == "ease-in-out") e.kind = Kind::ease_in_out;
111 else e.kind = Kind::linear;
112 return e;
113 }
114};
115
143
151 std::string property_name;
152 float duration_seconds = 0.0f;
153 float delay_seconds = 0.0f;
155
160 std::string source_file;
161 int source_line = 0;
162
165 bool is_snap() const { return duration_seconds <= 0.0f; }
166
174 if (source_file.empty() && source_line == 0 && property_name.empty()) {
175 return p;
176 }
177 p.source_kind = "css-transition";
178 p.source_id = property_name.empty() ? std::string{"all"} : property_name;
179 p.source_file = source_file;
180 p.source_line = source_line;
181 return p;
182 }
183};
184
195 float start_value = 0.0f;
196 float end_value = 0.0f;
197 float elapsed_seconds = 0.0f;
198 bool active = true;
199
206
211 if (motion_policy_applied_) return;
213 if (motion_policy_is_off()) {
214 // Snap to the end value on the next tick.
215 spec.duration_seconds = 0.0f;
216 spec.delay_seconds = 0.0f;
217 return;
218 }
220 const double scale =
222 spec.duration_seconds *= static_cast<float>(scale);
223 }
224 }
225
235 float tick(float dt) {
237 elapsed_seconds += dt;
238 const float total = spec.delay_seconds + spec.duration_seconds;
239 if (elapsed_seconds >= total) {
240 active = false;
241 return end_value;
242 }
243 if (elapsed_seconds < spec.delay_seconds) {
244 return start_value;
245 }
246 const float local = elapsed_seconds - spec.delay_seconds;
247 const float t = spec.duration_seconds > 0.0f
248 ? local / spec.duration_seconds
249 : 1.0f;
250 const float p = spec.easing.at(t);
251 return start_value + (end_value - start_value) * p;
252 }
253
258 void publish(std::string view_name,
259 std::string metric_name,
260 motion::PublishOptions opts = {}) const {
261 auto p = spec.motion_provenance();
262 if (p.is_set() && !opts.provenance.is_set()) {
263 opts.provenance = std::move(p);
264 }
265 // Re-evaluate the eased value at the current elapsed time
266 // without mutating internal state — the host already called
267 // `tick()` to advance, so reading the spec is enough.
268 float t = 0.0f;
269 const float local = elapsed_seconds - spec.delay_seconds;
270 if (local > 0.0f && spec.duration_seconds > 0.0f) {
271 t = local / spec.duration_seconds;
272 if (t > 1.0f) t = 1.0f;
273 }
274 const float eased = spec.easing.at(t);
275 const float current = start_value + (end_value - start_value) * eased;
276 motion::publish_value(std::move(view_name), std::move(metric_name),
277 static_cast<double>(current), opts);
278 }
279};
280
287 float offset = 0.0f;
289 std::vector<std::pair<std::string /*prop*/, std::string /*raw value*/>> properties;
290};
291
294 std::string name;
295 std::vector<CssKeyframe> stops;
296};
297
301 std::vector<CssKeyframesBlock> blocks;
302
304 for (auto& b : blocks) {
305 if (b.name == block.name) {
306 b = std::move(block);
307 return;
308 }
309 }
310 blocks.push_back(std::move(block));
311 }
312
313 const CssKeyframesBlock* find(const std::string& name) const {
314 for (const auto& b : blocks) {
315 if (b.name == name) return &b;
316 }
317 return nullptr;
318 }
319};
320
326 if (name == "opacity") return AnimatableProperty::opacity;
327 if (name == "background-color") return AnimatableProperty::background_color;
328 if (name == "color") return AnimatableProperty::color;
329 if (name == "border-color") return AnimatableProperty::border_color;
330 if (name == "translate-x") return AnimatableProperty::translate_x;
331 if (name == "translate-y") return AnimatableProperty::translate_y;
332 if (name == "rotate") return AnimatableProperty::rotate_deg;
333 if (name == "scale") return AnimatableProperty::scale;
334 if (name == "width") return AnimatableProperty::width;
335 if (name == "height") return AnimatableProperty::height;
336 if (name == "margin-top") return AnimatableProperty::margin_top;
337 if (name == "margin-right") return AnimatableProperty::margin_right;
338 if (name == "margin-bottom") return AnimatableProperty::margin_bottom;
339 if (name == "margin-left") return AnimatableProperty::margin_left;
340 if (name == "padding-top") return AnimatableProperty::padding_top;
341 if (name == "padding-right") return AnimatableProperty::padding_right;
342 if (name == "padding-bottom") return AnimatableProperty::padding_bottom;
343 if (name == "padding-left") return AnimatableProperty::padding_left;
344 if (name == "top") return AnimatableProperty::top;
345 if (name == "right") return AnimatableProperty::right;
346 if (name == "bottom") return AnimatableProperty::bottom;
347 if (name == "left") return AnimatableProperty::left;
349}
350
353inline float parse_css_time_seconds(const std::string& s) {
354 if (s.empty()) return 0.0f;
355 char* end = nullptr;
356 float v = std::strtof(s.c_str(), &end);
357 if (end == s.c_str() || !std::isfinite(v)) return 0.0f;
358 std::string unit = end;
359 while (!unit.empty() && std::isspace(static_cast<unsigned char>(unit.front()))) unit.erase(0, 1);
360 while (!unit.empty() && std::isspace(static_cast<unsigned char>(unit.back()))) unit.pop_back();
361 if (unit == "ms") return v / 1000.0f;
362 if (unit == "s") return v;
363 if (unit.empty()) return v; // bare number → seconds (lenient)
364 return 0.0f;
365}
366
367inline bool parse_css_float_token(const std::string& token, float& out) {
368 if (token.empty()) return false;
369 char* end = nullptr;
370 float v = std::strtof(token.c_str(), &end);
371 if (end == token.c_str() || !std::isfinite(v)) return false;
372 while (*end != '\0') {
373 if (!std::isspace(static_cast<unsigned char>(*end))) return false;
374 ++end;
375 }
376 out = v;
377 return true;
378}
379
389inline std::vector<TransitionSpec> parse_transition_shorthand(const std::string& css) {
390 std::vector<TransitionSpec> out;
391 if (css.empty() || css == "none") return out;
392
393 // Split on commas at depth 0 (cubic-bezier(...) has commas inside
394 // parens; need to skip those).
395 std::vector<std::string> entries;
396 {
397 std::string acc;
398 int depth = 0;
399 for (char c : css) {
400 if (c == '(') ++depth;
401 else if (c == ')') --depth;
402 if (c == ',' && depth == 0) {
403 entries.push_back(acc);
404 acc.clear();
405 } else {
406 acc += c;
407 }
408 }
409 if (!acc.empty()) entries.push_back(acc);
410 }
411
412 for (const auto& entry : entries) {
413 // Tokenize on whitespace (preserving cubic-bezier(...) and
414 // steps(...) parens).
415 std::vector<std::string> tokens;
416 {
417 std::string acc;
418 int depth = 0;
419 for (char c : entry) {
420 if (c == '(') { ++depth; acc += c; continue; }
421 if (c == ')') { --depth; acc += c; continue; }
422 if (depth == 0 && std::isspace(static_cast<unsigned char>(c))) {
423 if (!acc.empty()) { tokens.push_back(acc); acc.clear(); }
424 continue;
425 }
426 acc += c;
427 }
428 if (!acc.empty()) tokens.push_back(acc);
429 }
430
431 if (tokens.empty()) continue;
432
433 TransitionSpec spec{};
434 bool saw_duration = false;
435 for (const auto& tok : tokens) {
436 // Time tokens — `200ms` or `0.3s` (but NOT `ease`).
437 const bool ends_ms = (tok.size() >= 2 && tok.substr(tok.size() - 2) == "ms");
438 const bool ends_s_only = (!ends_ms && tok.size() >= 2 && tok.back() == 's'
439 && (std::isdigit(static_cast<unsigned char>(tok[tok.size() - 2]))
440 || tok[tok.size() - 2] == '.'));
441 if (ends_ms || ends_s_only) {
442 if (!saw_duration) { spec.duration_seconds = parse_css_time_seconds(tok); saw_duration = true; }
443 else spec.delay_seconds = parse_css_time_seconds(tok);
444 continue;
445 }
446 // Easing keyword
447 if (tok == "linear" || tok == "ease" || tok == "ease-in"
448 || tok == "ease-out" || tok == "ease-in-out") {
450 continue;
451 }
452 // cubic-bezier(p1x, p1y, p2x, p2y)
453 if (tok.rfind("cubic-bezier(", 0) == 0 && tok.back() == ')') {
454 std::string inner = tok.substr(13, tok.size() - 14);
455 std::vector<float> nums;
456 std::string acc;
457 auto flush = [&]() {
458 if (!acc.empty()) {
459 float parsed = 0.0f;
460 if (parse_css_float_token(acc, parsed)) nums.push_back(parsed);
461 acc.clear();
462 }
463 };
464 for (char c : inner) {
465 if (c == ',') flush();
466 else if (!std::isspace(static_cast<unsigned char>(c))) acc += c;
467 }
468 flush();
469 if (nums.size() == 4) {
471 spec.easing.p1x = nums[0]; spec.easing.p1y = nums[1];
472 spec.easing.p2x = nums[2]; spec.easing.p2y = nums[3];
473 }
474 continue;
475 }
476 // steps(N) / steps(N, end|start)
477 if (tok.rfind("steps(", 0) == 0 && tok.back() == ')') {
478 std::string inner = tok.substr(6, tok.size() - 7);
479 size_t comma = inner.find(',');
480 std::string n_str = comma == std::string::npos ? inner : inner.substr(0, comma);
481 char* end = nullptr;
482 long steps = std::strtol(n_str.c_str(), &end, 10);
483 if (end == n_str.c_str()) continue;
484 bool valid_steps = true;
485 while (*end != '\0') {
486 if (!std::isspace(static_cast<unsigned char>(*end))) {
487 valid_steps = false;
488 break;
489 }
490 ++end;
491 }
492 if (!valid_steps) continue;
493 spec.easing.steps_count = static_cast<int>(steps);
495 if (comma != std::string::npos) {
496 std::string mode = inner.substr(comma + 1);
497 while (!mode.empty() && std::isspace(static_cast<unsigned char>(mode.front()))) mode.erase(0, 1);
498 if (mode.rfind("jump-start", 0) == 0 || mode == "start") {
500 }
501 }
502 continue;
503 }
504 // Otherwise — property name. Last property name wins (CSS
505 // spec: the last unparsed token is the property).
506 spec.property_name = tok;
508 }
509 if (spec.property_name.empty()) spec.property_name = "all";
510 out.push_back(std::move(spec));
511 }
512 return out;
513}
514
520inline std::vector<TransitionSpec> parse_transition_shorthand_with_provenance(
521 const std::string& css,
522 std::string source_file,
523 int source_line) {
524 auto specs = parse_transition_shorthand(css);
525 for (auto& s : specs) {
526 s.source_file = source_file;
527 s.source_line = source_line;
528 }
529 return specs;
530}
531
532} // namespace pulp::view
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
Agent-first motion observability: sample view geometry, scroll state, and arbitrary scalar values at ...
System-level reduced-motion preferences.
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
std::vector< TransitionSpec > parse_transition_shorthand_with_provenance(const std::string &css, std::string source_file, int source_line)
Parse a transition shorthand AND stamp each emitted TransitionSpec with the source attribution (CSS f...
Definition css_animation.hpp:520
@ bottom
Definition border_style.hpp:31
@ right
Definition border_style.hpp:31
@ left
Definition border_style.hpp:31
@ top
Definition border_style.hpp:31
@ none
Definition appearance_defects.hpp:53
bool motion_policy_is_off()
Returns true when the current policy says "do not animate" — i.e.
Definition motion_preferences.hpp:133
AnimatableProperty animatable_property_from_css_name(const std::string &name)
Map CSS property name to AnimatableProperty enum.
Definition css_animation.hpp:325
bool parse_css_float_token(const std::string &token, float &out)
Definition css_animation.hpp:367
float parse_css_time_seconds(const std::string &s)
Parse a CSS time string (200ms / 0.3s / bare seconds) into seconds.
Definition css_animation.hpp:353
@ block
Filled glyph cell; the covered glyph is drawn in the bg color.
Definition caret.hpp:66
@ s
Definition input_events.hpp:41
@ c
Definition input_events.hpp:39
@ u
Definition input_events.hpp:41
@ p
Definition input_events.hpp:40
@ i
Definition input_events.hpp:40
@ b
Definition input_events.hpp:39
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ e
Definition input_events.hpp:39
@ t
Definition input_events.hpp:41
std::vector< TransitionSpec > parse_transition_shorthand(const std::string &css)
Parse a CSS transition shorthand into a list of TransitionSpecs.
Definition css_animation.hpp:389
@ end
Definition geometry.hpp:370
AnimatableProperty
Property whose values can be tweened.
Definition css_animation.hpp:118
@ scale
Definition css_animation.hpp:126
@ padding_bottom
Definition css_animation.hpp:135
@ translate_y
Definition css_animation.hpp:124
@ translate_x
Definition css_animation.hpp:123
@ background_color
Definition css_animation.hpp:120
@ margin_top
Definition css_animation.hpp:129
@ none
Definition css_animation.hpp:141
@ border_color
Definition css_animation.hpp:122
@ padding_right
Definition css_animation.hpp:134
@ color
Definition css_animation.hpp:121
@ bottom
Definition css_animation.hpp:139
@ right
Definition css_animation.hpp:138
@ left
Definition css_animation.hpp:140
@ rotate_deg
Definition css_animation.hpp:125
@ margin_right
Definition css_animation.hpp:130
@ opacity
Definition css_animation.hpp:119
@ top
Definition css_animation.hpp:137
@ height
Definition css_animation.hpp:128
@ padding_top
Definition css_animation.hpp:133
@ margin_bottom
Definition css_animation.hpp:131
@ width
Definition css_animation.hpp:127
@ padding_left
Definition css_animation.hpp:136
@ margin_left
Definition css_animation.hpp:132
@ Reduced
Animate, but scale duration by duration_scale.
Definition motion_preferences.hpp:30
One running animation.
Definition css_animation.hpp:189
bool motion_policy_applied_
Internal: have we applied the current MotionPolicy to this animation's effective duration yet?
Definition css_animation.hpp:205
void apply_motion_policy()
Apply the current MotionPolicy to spec.duration_seconds (and start_value under Off,...
Definition css_animation.hpp:210
float tick(float dt)
Advance by dt seconds.
Definition css_animation.hpp:235
TransitionSpec spec
Definition css_animation.hpp:191
void publish(std::string view_name, std::string metric_name, motion::PublishOptions opts={}) const
Publish the current eased value through the motion publish channel, stamping spec....
Definition css_animation.hpp:258
float start_value
Start + end values are property-typed; supported properties reduce to a float scalar after component ...
Definition css_animation.hpp:195
float elapsed_seconds
Definition css_animation.hpp:197
float end_value
Definition css_animation.hpp:196
bool active
Definition css_animation.hpp:198
CSS easing function — maps a normalized progress t ∈ [0,1] to an eased progress p ∈ [0,...
Definition css_animation.hpp:29
Kind kind
CSS spec — transition-timing-function defaults to ease, NOT linear.
Definition css_animation.hpp:45
static float cubic_bezier_y(float x, float p1x, float p1y, float p2x, float p2y)
Evaluate a CSS cubic-bezier(p1x, p1y, p2x, p2y) at progress x.
Definition css_animation.hpp:85
float p1y
Definition css_animation.hpp:49
int steps_count
Definition css_animation.hpp:50
float p1x
Cubic-bezier control points.
Definition css_animation.hpp:49
static CssEasing from_keyword(const std::string &s)
Parse a CSS easing keyword. Unrecognized → linear.
Definition css_animation.hpp:105
float at(float t) const
Evaluate the easing at progress t ∈ [0,1].
Definition css_animation.hpp:53
float p2y
Definition css_animation.hpp:49
float p2x
Definition css_animation.hpp:49
Kind
Definition css_animation.hpp:30
@ steps_end
Definition css_animation.hpp:38
@ ease_in
Definition css_animation.hpp:33
@ ease_out
Definition css_animation.hpp:34
@ steps_start
Definition css_animation.hpp:37
@ ease
Definition css_animation.hpp:32
@ linear
Definition css_animation.hpp:31
@ ease_in_out
Definition css_animation.hpp:35
@ cubic_bezier
Definition css_animation.hpp:36
One @keyframe stop.
Definition css_animation.hpp:284
std::vector< std::pair< std::string, std::string > > properties
Property → value snapshots.
Definition css_animation.hpp:289
float offset
Position along the timeline: 0.0 = from, 1.0 = to.
Definition css_animation.hpp:287
One @keyframes block: name + ordered stops.
Definition css_animation.hpp:293
std::string name
Definition css_animation.hpp:294
std::vector< CssKeyframe > stops
Definition css_animation.hpp:295
Application-wide registry for @keyframes blocks.
Definition css_animation.hpp:300
std::vector< CssKeyframesBlock > blocks
Definition css_animation.hpp:301
void define(CssKeyframesBlock block)
Definition css_animation.hpp:303
const CssKeyframesBlock * find(const std::string &name) const
Definition css_animation.hpp:313
One transition spec — one CSS property with its own duration / delay / easing.
Definition css_animation.hpp:147
CssEasing easing
Definition css_animation.hpp:154
std::string property_name
Original CSS property name.
Definition css_animation.hpp:151
int source_line
Definition css_animation.hpp:161
AnimatableProperty property
Definition css_animation.hpp:148
float duration_seconds
Definition css_animation.hpp:152
bool is_snap() const
CSS spec — duration <= 0 means "snap" (no tween).
Definition css_animation.hpp:165
motion::Provenance motion_provenance() const
Build a motion::Provenance envelope for this spec.
Definition css_animation.hpp:172
std::string source_file
Source attribution.
Definition css_animation.hpp:160
float delay_seconds
Definition css_animation.hpp:153
Definition motion.hpp:77
std::string source_kind
"tween" | "css-transition" | "animator-set" | "rAF" | "design-import" | "user" | "publish"
Definition motion.hpp:78
Definition motion.hpp:181