Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic styles and animations #113

Closed
wants to merge 7 commits into from
Closed

Dynamic styles and animations #113

wants to merge 7 commits into from

Conversation

maun
Copy link
Contributor

@maun maun commented Oct 2, 2023

Replace previous Style, ComputedStyle, Animation and AnimatedProp with dynamically computed styles and animations using closures.

First some API usage examples.

Examples

The API for static styles is just as before

empty().style(|s| s.background(Color::TEAL).size(300, 200))

but you can run any code, also using the previous style

fn golden_width(s: StyleAnimCtx) -> StyleAnimCtx {
    let phi = 1.61803398874989484820;
    let width = match s.style.height {
        PxPctAuto::Px(v) => PxPctAuto::Px(v / phi),
        PxPctAuto::Pct(v) => PxPctAuto::Pct(v / phi),
        PxPctAuto::Auto => PxPctAuto::Auto,
    };
    s.width(width)
}

empty().style(|s| {
    let mut s = s.background(Color::TEAL).height(200);
    s = golden_width(s); # Could also be in an extension trait from a style library
    s
})

Styles and style animations are implemented using the same closures, taking and modifying an StyleAnimCtx. The value holds the style, a boolean blend_style whether to blend with the previous style, and an animation_value, typically representing the animation progress from 0.0 to 1.0.

This allows styles like this, with a fixed animation_value:

fn pinkify(s: StyleAnimCtx) -> StyleAnimCtx {
    s.blend().background(Color::PINK)
}

empty().style(|s| {
    pinkify(
        s.size(200, 300)
            .background(Color::GRAY)
            .animation_value(0.5),
    )
})

But can also be used for state animations:

empty()
    .style(|s| s.size(100, 50).background(Color::GRAY))
    .hover_style_anim(anim(0.5), |s| s.blend().background(Color::PINK))
1_hover.mov

By using code we can also have more complicated logic, like keyframes:

empty()
    .style(|s| s.size(100, 50).background(Color::GRAY))
    .hover_style_anim(anim(1.0), |s| {
        let mut s = s.blend();
        let v = s.animation_value;
        // first increase the width
        s = s.rescale_anim(0.0, 0.5).clamp(0.0, 1.0).width(200);
        // then blend to pink, setting animation_value is required because the first call to rescale_anim overwrote it.
        if v > 0.5 {
            s = s
                .animation_value(v)
                .rescale_anim(0.5, 1.0)
                .background(Color::PINK);
        }
        s
    })
keyframes.mov

I also implemented

  • .passes(count) to run the following animation count times
  • .alternating_anim() to animate back to the initial style, after reaching an animation_value of 0.5.
  • .ease(mut self, mode: EasingMode, func: EasingFn) to apply the previously implemented easing functions to the animation_value

It is also possible to animate more (all) values of the style, like the font_size. Values which can not be interpolated, like the flex_direction could also be changed in the middle of the animation.

There is the trait AnimDriver, which produces animation_values, and currently 2 implementors:

  • FixedAnimDriver for fixed animations, used for static styles.
  • TimedAnimDriver for animating between 0.0 and 1.0 based on whether it is enabled or not.

Looping is also possible with the TimedAnimDriver:

empty().style_anim(looping(2.0), |s| {
    s.size(100, 50)
        .background(Color::GRAY)
        .blend()
        .alternating_anim()
        .background(Color::PINK)
})

Notice the position of .blend(). Everything before is applied all the time, everything afterwards is animated.

looping.mov

Motivation and implementation notes

Modern UIs use animations a lot. Ideally I would like floem to support the following:

  • Simple state transitions, like in CSS with transition
  • Complicated animations with keyframes, and easing ✅
  • Animations defined in libraries, like in Framer Motion
  • Animations based on different values, like scroll position AOS, or view screen intersection ratio
    • Not easily possible, but this is the reason why I added a separate AnimDriver and named the animation_value not animation_progress and forced it to be within [0.0, 1.0]. This could be used right now to animate some other value like, download_speed when implementing AnimDriver
  • User defined styles and functions which can be enabled/disabled
    • I did not add this, as the PR is big enough, but there could be an StyleAnimGroupId which can be used to set animations
      .custom_anim(id, driver, |s|...) and then be enabled, disabled for all views. Maybe there should be additional triggers which wake up the animation, like when the scroll position changes. The animation driver could decide whether it starts animating.

I think the previous the implementation made sense, coming from CSS/HTML. Define what should be done, and let the core do it. Because we are using rust, and not CSS, doing it directly is as simple, more flexible, and should be just as fast. This is similar to the way views are defined with code, and not markup.

Interaction with reactivity system

I am not sure how much the reactivity system can/should be used here. To me a pull based system makes sense, since animations should run once per frame, not once a value like download_speed is updated, but I think this could also be achieved in a different way. They should also be able to run without changes in a signal. Currently when using reactive values in animations and styles they are replaced when it changes, as the calls are wrapped in create_effect.

Reviewing guide

Best start with checking the following files for the most important changes:

Unrelated changes

  • Renamed BoxShadow to Shadow, as it is shorter and just as clear
  • Support multiple Shadows, this also changes the (box)_shadow_x API
  • Shadows for Circle shapes, due to a missing implementation in vger the blur_radius has no effect.
  • Extended and used StyleSelector for the base, main, and override style. This felt easier, and extends nicely to custom animations.

Hope you like this, no worries if not, I can also work on this for my own projects. Feel free to change what you like, or tell me what should be different. There is not much wich could be done in a separate PR, as this all works together.

@codecov
Copy link

codecov bot commented Oct 2, 2023

Codecov Report

Merging #113 (a79588d) into main (eb5b680) will increase coverage by 0.76%.
The diff coverage is 16.38%.

@@           Coverage Diff            @@
##            main    #113      +/-   ##
========================================
+ Coverage   3.46%   4.23%   +0.76%     
========================================
  Files         52      51       -1     
  Lines       8792    8548     -244     
========================================
+ Hits         305     362      +57     
+ Misses      8487    8186     -301     
Files Coverage Δ
src/views/svg.rs 0.00% <ø> (ø)
src/update.rs 0.00% <0.00%> (ø)
src/views/label.rs 0.00% <0.00%> (ø)
src/views/rich_text.rs 0.00% <0.00%> (ø)
src/views/text_input.rs 7.96% <0.00%> (+0.01%) ⬆️
src/id.rs 0.00% <0.00%> (ø)
src/window_handle.rs 0.00% <0.00%> (ø)
src/views/scroll.rs 0.00% <0.00%> (ø)
src/views/tab.rs 0.00% <0.00%> (ø)
src/style/timed.rs 92.81% <92.81%> (ø)
... and 8 more

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@maun maun closed this Oct 3, 2023
@dzhou121
Copy link
Contributor

dzhou121 commented Oct 3, 2023

I think we'll try to extract the parts that we want from this PR. The part that's immediately very valuable is StyleAnimCtx I think. Do you think you can extract that in a new PR and use it for style as the first step since it will be the easiest.

@maun
Copy link
Contributor Author

maun commented Oct 3, 2023

I think we'll try to extract the parts that we want from this PR. The part that's immediately very valuable is StyleAnimCtx I think. Do you think you can extract that in a new PR and use it for style as the first step since it will be the easiest.

I am not sure how that would interact/not interact with the proposed animation API, and easing between different animation „tracks“. Would this still use AnimatedProps?
When no animation is done in normal styles the StyleAnimCtx is not really required, although it allows some blending which can be nice.
Before starting an implementation I would first like to better understand this. Also not sure what @presiyan-ivanov is going to do, any implementation ideas/plans? 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants