日本語

Animation

Keyframes are Rust values too. When an element uses keyframes, their rule is written to the page along with the element.

Keyframes

Keyframes::new(name) creates an animation, and at(percent, style) adds a step. Style::keyframes sets an element's animation-name and includes the @keyframes rule in the style, so the page or patch that contains the element also contains the rule. A keyframe writes only the unconditioned declarations of its style, because a keyframe takes no selector or media query.

use hypersig::html::span;
use hypersig::style::value::{AnimationIterationCount, AnimationTimingFunction, ms, raw};
use hypersig::style::{Keyframes, Media, Style};
use hypersig::{Page, Signals};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Upload {
    busy: bool,
}

let spin = Keyframes::new("spin")
    .at(0, Style::new().transform(raw("rotate(0deg)")))
    .at(100, Style::new().transform(raw("rotate(360deg)")));

let spinner = span::<Upload>()
    .attr("aria-hidden", "true")
    .css(
        Style::new()
            .keyframes(&spin)
            .animation_duration(ms(800))
            .animation_timing_function(AnimationTimingFunction::Linear)
            .animation_iteration_count(AnimationIterationCount::Infinite)
            // Slower for a visitor who asks for reduced motion.
            .on(Media::ReducedMotion, Style::new().animation_duration(ms(4000))),
    )
    .show(Upload::busy())
    .text("◌");

let html = Page::new("Upload", Upload { busy: true }).child(spinner).render();
assert!(html.contains("@keyframes spin"));

Result

spinner.rs
use hypersig::html::{div, span};
use hypersig::style::pattern::hstack;
use hypersig::style::value::{
    AnimationIterationCount, AnimationTimingFunction, BorderStyle, ms, px, raw, rem,
};
use hypersig::style::{Keyframes, Media, Style};
use hypersig::{Page, Signals, if_else};
use hypersig_ui::{ButtonSize, ButtonVariant, button, token};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
pub struct Upload {
    busy: bool,
}

pub fn page() -> Page<Upload> {
    let busy = Upload::busy();
    let spin = Keyframes::new("spin")
        .at(0, Style::new().transform(raw("rotate(0deg)")))
        .at(100, Style::new().transform(raw("rotate(360deg)")));
    let ring = Style::new()
        .width(rem(1.5))
        .height(rem(1.5))
        .border_width(px(3))
        .border_style(BorderStyle::Solid)
        .border_color(token::muted().var())
        .border_top_color(token::primary().var())
        .border_radius(px(999))
        .keyframes(&spin)
        .animation_duration(ms(800))
        .animation_timing_function(AnimationTimingFunction::Linear)
        .animation_iteration_count(AnimationIterationCount::Infinite)
        .on(
            Media::ReducedMotion,
            Style::new().animation_duration(ms(4000)),
        );
    Page::new(
        "Upload",
        Upload { busy: true },
    )
    .child(
        div()
            .css(hstack(rem(1)))
            .child(span().attr("aria-hidden", "true").css(ring).show(busy))
            .child(span().attr("role", "status").bind_text(if_else(
                busy,
                "Uploading…",
                "Done",
            )))
            .child(
                button(ButtonVariant::Outline, ButtonSize::Sm)
                    .on_click(busy.toggle())
                    .bind_text(if_else(
                        busy,
                        "Stop",
                        "Start",
                    )),
            ),
    )
}

Reduced motion

Media::ReducedMotion applies a style for a visitor who asks for reduced motion. In that case the components of hypersig_ui change colors without a transition.

Two Keyframes with the same name are treated as one rule by the browser, and the one later in the stylesheet is used. Duplicate names are not checked.