Skip to content

Commit

Permalink
Added PropertyAnimation.direction property for controlling animatio…
Browse files Browse the repository at this point in the history
…n direction (#6261)

Closes #6260

ChangeLog: Added property for controlling animation direction
  • Loading branch information
SK83RJOSH authored Sep 24, 2024
1 parent f772cb8 commit 6a131e2
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 12 deletions.
1 change: 1 addition & 0 deletions api/cpp/cbindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ fn gen_corelib(
"Slice",
"WindowAdapterRcOpaque",
"PropertyAnimation",
"AnimationDirection",
"EasingCurve",
"TextHorizontalAlignment",
"TextVerticalAlignment",
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/src/language/syntax/animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ Fine-tune animations using the following parameters:
- `iteration-count`: The number of times an animation should run. A negative value specifies
infinite reruns. Fractual values are possible.
For permanently running animations, see [`animation-tick()`](../builtins/functions.md#animation-tick-duration).
- `direction`: Can be any of the following. See [`developer.mozilla.org`](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction) for reference:
- `normal`
- `reverse`
- `alternate`
- `alternate-reverse`
- `easing`: can be any of the following. See [`easings.net`](https://easings.net/) for a visual reference:

- `linear`
Expand Down
1 change: 1 addition & 0 deletions docs/reference/src/language/syntax/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ All properties in Slint have a type. Slint knows these basic types:
| `brush` | A brush is a special type that can be either initialized from a color or a gradient specification. See the [Colors and Brushes Section](#colors-and-brushes) for more information. | transparent |
| `color` | RGB color with an alpha channel, with 8 bit precision for each channel. CSS color names as well as the hexadecimal color encodings are supported, such as `#RRGGBBAA` or `#RGB`. | transparent |
| `duration` | Type for the duration of animations. A suffix like `ms` (millisecond) or `s` (second) is used to indicate the precision. | 0ms |
| `direction` | Type for the direction of animations. See [animations](animations.md) for list of values. | normal |
| `easing` | Property animation allow specifying an easing curve. See [animations](animations.md) for list of values. | linear |
| `float` | Signed, 32-bit floating point number. Numbers with a `%` suffix are automatically divided by 100, so for example `30%` is the same as `0.30`. | 0 |
| `image` | A reference to an image, can be initialized with the `@image-url("...")` construct | empty image |
Expand Down
2 changes: 1 addition & 1 deletion editors/tree-sitter-slint/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ module.exports = grammar({
animate_statement: ($) => seq("animate", $.expression, $.animate_body),

animate_option_identifier: (_) =>
choice("delay", "duration", "iteration-count", "easing"),
choice("delay", "duration", "iteration-count", "direction", "easing"),

animate_option: ($) =>
seq(
Expand Down
12 changes: 12 additions & 0 deletions internal/common/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,18 @@ macro_rules! for_each_enums {
/// The style chooses dark colors for the background and light for the foreground.
Light,
}

/// This enum describes the direction of an animation.
enum AnimationDirection {
/// The ["normal" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#normal).
Normal,
/// The ["reverse" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#reverse).
Reverse,
/// The ["alternate" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#alternate).
Alternate,
/// The ["alternate reverse" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#alternate-reverse).
AlternateReverse,
}
];
};
}
1 change: 1 addition & 0 deletions internal/compiler/builtins.slint
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ export component Dialog inherits WindowItem { }
component PropertyAnimation {
in property <duration> delay;
in property <duration> duration;
in property <AnimationDirection> direction;
in property <easing> easing;
in property <float> iteration-count: 1.0;
//-is_non_item_type
Expand Down
9 changes: 8 additions & 1 deletion internal/compiler/llr/lower_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ use itertools::Either;

use super::lower_to_item_tree::{LoweredElement, LoweredSubComponentMapping, LoweringState};
use super::{Animation, PropertyReference};
use crate::expression_tree::{BuiltinFunction, Expression as tree_Expression};
use crate::langtype::{EnumerationValue, Type};
use crate::layout::Orientation;
use crate::llr::Expression as llr_Expression;
use crate::namedreference::NamedReference;
use crate::object_tree::{Element, ElementRc, PropertyAnimation};
use crate::{
expression_tree::{BuiltinFunction, Expression as tree_Expression},
typeregister::BUILTIN_ENUMS,
};

pub struct ExpressionContext<'a> {
pub component: &'a Rc<crate::object_tree::Component>,
Expand Down Expand Up @@ -407,6 +410,10 @@ pub fn lower_animation(a: &PropertyAnimation, ctx: &ExpressionContext<'_>) -> An
IntoIterator::into_iter([
("duration".to_string(), Type::Int32),
("iteration-count".to_string(), Type::Float32),
(
"direction".to_string(),
Type::Enumeration(BUILTIN_ENUMS.with(|e| e.AnimationDirection.clone())),
),
("easing".to_string(), Type::Easing),
("delay".to_string(), Type::Int32),
])
Expand Down
10 changes: 9 additions & 1 deletion internal/core/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,14 +922,22 @@ pub struct PropertyAnimation {
#[rtti_field]
pub iteration_count: f32,
#[rtti_field]
pub direction: AnimationDirection,
#[rtti_field]
pub easing: crate::animations::EasingCurve,
}

impl Default for PropertyAnimation {
fn default() -> Self {
// Defaults for PropertyAnimation are defined here (for internal Rust code doing programmatic animations)
// as well as in `builtins.slint` (for generated C++ and Rust code)
Self { delay: 0, duration: 0, iteration_count: 1., easing: Default::default() }
Self {
delay: 0,
duration: 0,
iteration_count: 1.,
direction: Default::default(),
easing: Default::default(),
}
}
}

Expand Down
87 changes: 79 additions & 8 deletions internal/core/properties/properties_animations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

use super::*;
use crate::{items::PropertyAnimation, lengths::LogicalLength};
use crate::{
items::{AnimationDirection, PropertyAnimation},
lengths::LogicalLength,
};
#[cfg(not(feature = "std"))]
use num_traits::Float;

enum AnimationState {
Delaying,
Animating { current_iteration: u64 },
Done,
Done { interation_count: u64 },
}

pub(super) struct PropertyValueAnimationData<T> {
Expand All @@ -30,6 +33,14 @@ impl<T: InterpolatedPropertyValue + Clone> PropertyValueAnimationData<T> {
pub fn compute_interpolated_value(&mut self) -> (T, bool) {
let new_tick = crate::animations::current_tick();
let mut time_progress = new_tick.duration_since(self.start_time).as_millis() as u64;
let reversed = |iteration: u64| -> bool {
match self.details.direction {
AnimationDirection::Normal => false,
AnimationDirection::Reverse => true,
AnimationDirection::Alternate => iteration % 2 == 1,
AnimationDirection::AlternateReverse => iteration % 2 == 0,
}
};

match self.state {
AnimationState::Delaying => {
Expand All @@ -41,7 +52,11 @@ impl<T: InterpolatedPropertyValue + Clone> PropertyValueAnimationData<T> {
let delay = self.details.delay as u64;

if time_progress < delay {
(self.from_value.clone(), false)
if reversed(0) {
(self.to_value.clone(), false)
} else {
(self.from_value.clone(), false)
}
} else {
self.start_time =
new_tick - core::time::Duration::from_millis(time_progress - delay);
Expand All @@ -53,7 +68,7 @@ impl<T: InterpolatedPropertyValue + Clone> PropertyValueAnimationData<T> {
}
AnimationState::Animating { mut current_iteration } => {
if self.details.duration <= 0 || self.details.iteration_count == 0. {
self.state = AnimationState::Done;
self.state = AnimationState::Done { interation_count: 0 };
return self.compute_interpolated_value();
}

Expand All @@ -71,18 +86,32 @@ impl<T: InterpolatedPropertyValue + Clone> PropertyValueAnimationData<T> {
{
self.state = AnimationState::Animating { current_iteration };

let progress =
(time_progress as f32 / self.details.duration as f32).clamp(0., 1.);
let progress = {
let progress =
(time_progress as f32 / self.details.duration as f32).clamp(0., 1.);
if reversed(current_iteration) {
1. - progress
} else {
progress
}
};
let t = crate::animations::easing_curve(&self.details.easing, progress);
let val = self.from_value.interpolate(&self.to_value, t);

(val, false)
} else {
self.state = AnimationState::Done;
self.state =
AnimationState::Done { interation_count: current_iteration.max(1) - 1 };
self.compute_interpolated_value()
}
}
AnimationState::Done => (self.to_value.clone(), true),
AnimationState::Done { interation_count } => {
if reversed(interation_count) {
(self.from_value.clone(), true)
} else {
(self.to_value.clone(), true)
}
}
}
}

Expand Down Expand Up @@ -742,6 +771,48 @@ mod animation_tests {
compo.width.handle.access(|binding| assert!(binding.is_some()));
}

#[test]
fn properties_test_animation_direction_triggered_by_set() {
let compo = Component::new_test_component();

let animation_details = PropertyAnimation {
delay: -25,
duration: DURATION.as_millis() as _,
direction: AnimationDirection::AlternateReverse,
iteration_count: 1.,
..PropertyAnimation::default()
};

compo.width.set(100);
assert_eq!(get_prop_value(&compo.width), 100);
assert_eq!(get_prop_value(&compo.width_times_two), 200);

let start_time = crate::animations::current_tick();

compo.width.set_animated_value(200, animation_details);
assert_eq!(get_prop_value(&compo.width), 200);
assert_eq!(get_prop_value(&compo.width_times_two), 400);

crate::animations::CURRENT_ANIMATION_DRIVER
.with(|driver| driver.update_animations(start_time + DURATION / 2));
assert_eq!(get_prop_value(&compo.width), 150);
assert_eq!(get_prop_value(&compo.width_times_two), 300);

crate::animations::CURRENT_ANIMATION_DRIVER
.with(|driver| driver.update_animations(start_time + DURATION));
assert_eq!(get_prop_value(&compo.width), 100);
assert_eq!(get_prop_value(&compo.width_times_two), 200);

// Overshoot: Always from_value.
crate::animations::CURRENT_ANIMATION_DRIVER
.with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
assert_eq!(get_prop_value(&compo.width), 100);
assert_eq!(get_prop_value(&compo.width_times_two), 200);

// the binding should be removed
compo.width.handle.access(|binding| assert!(binding.is_none()));
}

#[test]
fn properties_test_animation_triggered_by_binding() {
let compo = Component::new_test_component();
Expand Down
31 changes: 30 additions & 1 deletion tests/cases/properties/property_animation.slint
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ TestCase := Rectangle {
duration: 1200ms;
}

property<int> direction: 100;
animate direction {
duration: 600ms;
direction: alternate-reverse;
iteration-count: 2;
}
}

/*
Expand All @@ -30,34 +36,42 @@ TestCase := Rectangle {
let instance = TestCase::new().unwrap();
assert_eq!(instance.get_hello(), 40);
assert_eq!(instance.get_binding_dep(), 100);
assert_eq!(instance.get_direction(), 100);
instance.set_condition(false);
instance.set_hello(60);
instance.set_direction(200);
// no time has elapsed yet
assert_eq!(instance.get_hello(), 40);
assert_eq!(instance.get_binding_dep(), 100);
assert_eq!(instance.get_direction(), 200);
// Half the animation
slint_testing::mock_elapsed_time(600);
assert_eq!(instance.get_hello(), 50);
assert_eq!(instance.get_binding_dep(), 125);
assert_eq!(instance.get_direction(), 100);
// Remaining half
slint_testing::mock_elapsed_time(600);
assert_eq!(instance.get_hello(), 60);
assert_eq!(instance.get_binding_dep(), 150);
assert_eq!(instance.get_direction(), 200);
slint_testing::mock_elapsed_time(100);
assert_eq!(instance.get_hello(), 60);
assert_eq!(instance.get_binding_dep(), 150);
assert_eq!(instance.get_direction(), 200);
// Changing the value and waiting should have effect without
// querying the value (because te dirty event should cause the animation to start)
instance.set_condition(true);
instance.set_hello(30);
instance.set_direction(100);
slint_testing::mock_elapsed_time(600);
assert_eq!(instance.get_hello(), 45);
assert_eq!(instance.get_binding_dep(), 125);
assert_eq!(instance.get_direction(), 200);
```
Expand All @@ -67,65 +81,80 @@ auto handle = TestCase::create();
const TestCase &instance = *handle;
assert_eq(instance.get_hello(), 40);
assert_eq(instance.get_binding_dep(), 100);
assert_eq(instance.get_direction(), 100);
instance.set_condition(false);
instance.set_hello(60);
instance.set_direction(200);
// no time has elapsed yet
assert_eq(instance.get_hello(), 40);
assert_eq(instance.get_binding_dep(), 100);
assert_eq(instance.get_direction(), 200);
// Half the animation
slint_testing::mock_elapsed_time(600);
assert_eq(instance.get_hello(), 50);
assert_eq(instance.get_binding_dep(), 125);
assert_eq(instance.get_direction(), 100);
// Remaining half
slint_testing::mock_elapsed_time(600);
assert_eq(instance.get_hello(), 60);
assert_eq(instance.get_binding_dep(), 150);
assert_eq(instance.get_direction(), 200);
slint_testing::mock_elapsed_time(100);
assert_eq(instance.get_hello(), 60);
assert_eq(instance.get_binding_dep(), 150);
assert_eq(instance.get_direction(), 200);
// Changing the value and waiting should have effect without
// querying the value (because te dirty event should cause the animation to start)
instance.set_condition(true);
instance.set_hello(30);
instance.set_direction(100);
slint_testing::mock_elapsed_time(600);
assert_eq(instance.get_hello(), 45);
assert_eq(instance.get_binding_dep(), 125);
assert_eq(instance.get_direction(), 200);
```
```js
var instance = new slint.TestCase({});
assert.equal(instance.hello, 40);
assert.equal(instance.binding_dep, 100);
assert.equal(instance.direction, 100);
instance.condition = false;
instance.hello = 60;
instance.direction = 200;
// no time has elapsed yet
assert.equal(instance.hello, 40);
assert.equal(instance.binding_dep, 100);
assert.equal(instance.direction, 200);
// Half the animation
slintlib.private_api.mock_elapsed_time(600);
assert.equal(instance.hello, 50);
assert.equal(instance.binding_dep, 125);
assert.equal(instance.direction, 100);
// Remaining half
slintlib.private_api.mock_elapsed_time(600);
assert.equal(instance.hello, 60);
assert.equal(instance.binding_dep, 150);
assert.equal(instance.direction, 200);
slintlib.private_api.mock_elapsed_time(100);
assert.equal(instance.hello, 60);
assert.equal(instance.binding_dep, 150);
assert.equal(instance.direction, 200);
// Changing the value and waiting should have effect without
// querying the value (because te dirty event should cause the animation to start)
instance.condition = true;
instance.hello = 30;
instance.direction = 100;
slintlib.private_api.mock_elapsed_time(600);
assert.equal(instance.hello, 45);
assert.equal(instance.binding_dep, 125);
assert.equal(instance.direction, 200);
```
*/
Loading

0 comments on commit 6a131e2

Please sign in to comment.