Skip to main content

animation_state_machine/
animation_state_machine.rs

1use operad::native::{NativeWindowOptions, NativeWindowResult};
2use operad::{
3    root_style, widgets, AccessibilityMeta, AccessibilityRole, AnimatedValues,
4    AnimationBlendBinding, AnimationCondition, AnimationMachine, AnimationState,
5    AnimationTransition, ColorRgba, InputBehavior, LayoutStyle, ScenePrimitive, StrokeStyle,
6    TextStyle, UiDocument, UiNode, UiPoint, UiSize, UiVisual, WidgetAction,
7};
8
9const INPUT_OPEN: &str = "open";
10const INPUT_MORPH: &str = "morph";
11
12fn main() -> NativeWindowResult {
13    operad::native::run_app_with(
14        NativeWindowOptions::new("Animation state machine").with_min_size(540.0, 360.0),
15        AnimationApp::default(),
16        AnimationApp::update,
17        AnimationApp::view,
18    )
19}
20
21#[derive(Default)]
22struct AnimationApp {
23    open: bool,
24}
25
26impl AnimationApp {
27    fn update(&mut self, action: WidgetAction) {
28        if action
29            .binding
30            .action_id()
31            .is_some_and(|id| id.as_str() == "animation.toggle")
32        {
33            self.open = !self.open;
34        }
35    }
36
37    fn view(&self, viewport: UiSize) -> UiDocument {
38        let mut ui = UiDocument::new(root_style(viewport.width, viewport.height));
39        let panel = ui.add_child(
40            ui.root(),
41            UiNode::container(
42                "animation.panel",
43                LayoutStyle::column()
44                    .with_width_percent(1.0)
45                    .with_height_percent(1.0)
46                    .with_padding(16.0)
47                    .with_gap(12.0),
48            )
49            .with_visual(UiVisual::panel(ColorRgba::new(13, 17, 23, 255), None, 0.0)),
50        );
51
52        widgets::label(
53            &mut ui,
54            panel,
55            "animation.title",
56            "Animation state machine",
57            heading(),
58            LayoutStyle::new().with_width_percent(1.0).with_height(34.0),
59        );
60
61        let row = ui.add_child(
62            panel,
63            UiNode::container(
64                "animation.controls",
65                LayoutStyle::row()
66                    .with_width_percent(1.0)
67                    .with_height(40.0)
68                    .with_gap(10.0),
69            ),
70        );
71        widgets::button(
72            &mut ui,
73            row,
74            "animation.toggle",
75            if self.open { "Close" } else { "Open" },
76            widgets::ButtonOptions::default().with_action("animation.toggle"),
77        );
78        widgets::label(
79            &mut ui,
80            row,
81            "animation.state",
82            if self.open {
83                "State input: open"
84            } else {
85                "State input: closed"
86            },
87            muted(),
88            LayoutStyle::new().with_width(220.0).with_height(32.0),
89        );
90
91        let stage = ui.add_child(
92            panel,
93            UiNode::container(
94                "animation.stage",
95                LayoutStyle::row()
96                    .with_width_percent(1.0)
97                    .with_height(0.0)
98                    .with_flex_grow(1.0),
99            )
100            .with_visual(UiVisual::panel(
101                ColorRgba::new(16, 21, 28, 255),
102                Some(StrokeStyle::new(ColorRgba::new(58, 68, 84, 255), 1.0)),
103                6.0,
104            )),
105        );
106
107        ui.add_child(
108            stage,
109            UiNode::scene(
110                "animation.shape",
111                shape_primitives(),
112                LayoutStyle::new()
113                    .with_width_percent(1.0)
114                    .with_height_percent(1.0),
115            )
116            .with_input(InputBehavior::BUTTON)
117            .with_animation(shape_machine(self.open))
118            .with_accessibility(
119                AccessibilityMeta::new(AccessibilityRole::Button)
120                    .label("Animated morphing shape")
121                    .focusable(),
122            ),
123        );
124        ui
125    }
126}
127
128fn shape_machine(open: bool) -> AnimationMachine {
129    let closed = AnimatedValues::new(0.82, UiPoint::new(0.0, 0.0), 1.0).with_morph(0.0);
130    let open_values = AnimatedValues::new(1.0, UiPoint::new(160.0, 0.0), 1.08).with_morph(1.0);
131    AnimationMachine::new(
132        vec![
133            AnimationState::new("closed", closed),
134            AnimationState::new("open", open_values),
135        ],
136        vec![
137            AnimationTransition::when(
138                "closed",
139                "open",
140                AnimationCondition::bool(INPUT_OPEN, true),
141                0.24,
142            ),
143            AnimationTransition::when(
144                "open",
145                "closed",
146                AnimationCondition::bool(INPUT_OPEN, false),
147                0.18,
148            ),
149        ],
150        "closed",
151    )
152    .unwrap_or_else(|_| AnimationMachine::single_state("closed", closed))
153    .with_bool_input(INPUT_OPEN, open)
154    .with_number_input(INPUT_MORPH, if open { 1.0 } else { 0.0 })
155    .with_blend_binding(AnimationBlendBinding::new(INPUT_MORPH, "closed", "open"))
156}
157
158fn shape_primitives() -> Vec<ScenePrimitive> {
159    vec![
160        ScenePrimitive::MorphPolygon {
161            from_points: vec![
162                UiPoint::new(76.0, 54.0),
163                UiPoint::new(156.0, 54.0),
164                UiPoint::new(156.0, 134.0),
165                UiPoint::new(76.0, 134.0),
166            ],
167            to_points: pentagon_points(UiPoint::new(116.0, 94.0), 48.0),
168            amount: 0.0,
169            fill: ColorRgba::new(120, 210, 180, 255),
170            stroke: Some(StrokeStyle::new(ColorRgba::new(236, 244, 255, 255), 1.5)),
171        },
172        ScenePrimitive::Circle {
173            center: UiPoint::new(102.0, 78.0),
174            radius: 8.0,
175            fill: ColorRgba::new(244, 248, 255, 255),
176            stroke: None,
177        },
178    ]
179}
180
181fn pentagon_points(center: UiPoint, radius: f32) -> Vec<UiPoint> {
182    (0..5)
183        .map(|index| {
184            let angle = -std::f32::consts::FRAC_PI_2 + index as f32 * std::f32::consts::TAU / 5.0;
185            UiPoint::new(
186                center.x + angle.cos() * radius,
187                center.y + angle.sin() * radius,
188            )
189        })
190        .collect()
191}
192
193fn heading() -> TextStyle {
194    TextStyle {
195        font_size: 22.0,
196        line_height: 30.0,
197        color: ColorRgba::WHITE,
198        ..TextStyle::default()
199    }
200}
201
202fn muted() -> TextStyle {
203    TextStyle {
204        color: ColorRgba::new(166, 178, 196, 255),
205        ..TextStyle::default()
206    }
207}