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}