Skip to main content

animation_example/
animation-example.rs

1use mittens_engine::{engine, utils};
2
3#[path = "example_util/mod.rs"]
4mod example_util;
5
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    let world = engine::ecs::World::default();
11    let mut universe = engine::Universe::new(world);
12
13    // Minimal scene with a camera so the window opens.
14    let clear = universe
15        .world
16        .add_component(engine::ecs::component::BackgroundColorComponent::new());
17    let clear_c = universe
18        .world
19        .add_component(engine::ecs::component::ColorComponent::rgba(
20            0.08, 0.08, 0.08, 1.0,
21        ));
22    let _ = universe.world.add_child(clear, clear_c);
23    universe.add(clear);
24
25    // Input-driven camera rig.
26    let input = universe
27        .world
28        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
29    let rig_transform = universe.world.add_component(
30        engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
31    );
32    let input_mode = universe.world.add_component(
33        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34    );
35    let camera3d = universe.world.add_component(
36        engine::ecs::component::Camera3DComponent::new()
37            .with_far(250.0)
38            .with_fov(70.0),
39    );
40    let _ = universe.attach(input, input_mode);
41    let _ = universe.attach(input, rig_transform);
42    let _ = universe.attach(rig_transform, camera3d);
43
44    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
45    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
46    universe.add(input);
47
48    // Light so we can see non-emissive materials (even though our cubes are emissive).
49    let light_tx = universe.world.add_component(
50        engine::ecs::component::TransformComponent::new().with_position(0.0, 2.5, 4.0),
51    );
52    let light = universe.world.add_component(
53        engine::ecs::component::PointLightComponent::new()
54            .with_distance(25.0)
55            .with_color(1.0, 1.0, 1.0),
56    );
57    let _ = universe.attach(light_tx, light);
58    universe.add(light_tx);
59
60    // ClockComponent sets global tempo.
61    let clock = universe
62        .world
63        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
64    universe.add(clock);
65
66    // Audio output + oscillators driven by scheduled actions.
67    let audio_out = universe
68        .world
69        .add_component(engine::ecs::component::AudioOutputComponent::new());
70    universe.add(audio_out);
71
72    // Noise oscillator: short white-noise hits.
73    let osc_noise = engine::ecs::component::AudioOscillator::noise()
74        .with_frequency(0.0)
75        .with_amplitude(0.06)
76        .with_enabled(false);
77    let osc_noise_comp =
78        universe
79            .world
80            .add_component(engine::ecs::component::AudioOscillatorComponent::single(
81                osc_noise,
82            ));
83    let _ = universe.attach(audio_out, osc_noise_comp);
84
85    // Drum oscillator: retriggers (phase + sweep) every enable.
86    let osc_drum = engine::ecs::component::AudioOscillator::drum()
87        // A low-ish pitch scale; actual pitch is set by scheduled notes.
88        .with_frequency(32.0)
89        .with_amplitude(0.40)
90        .with_enabled(false);
91    let osc_drum_comp =
92        universe
93            .world
94            .add_component(engine::ecs::component::AudioOscillatorComponent::single(
95                osc_drum,
96            ));
97
98    let _ = universe.attach(audio_out, osc_drum_comp);
99
100    // --- Visual layout helpers ---
101    fn spawn_text(universe: &mut engine::Universe, pos: (f32, f32, f32), scale: f32, text: &str) {
102        let tx = universe.world.add_component(
103            engine::ecs::component::TransformComponent::new()
104                .with_position(pos.0, pos.1, pos.2)
105                .with_scale(scale, scale, 1.0),
106        );
107        let t =
108            universe
109                .world
110                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
111                    text, 38,
112                ));
113        let _ = universe.attach(tx, t);
114        universe.add(tx);
115    }
116
117    fn spawn_emissive_cube(
118        universe: &mut engine::Universe,
119        parent: engine::ecs::ComponentId,
120        pos: (f32, f32, f32),
121        scale: f32,
122        rgba: [f32; 4],
123    ) {
124        let tx = universe.world.add_component(
125            engine::ecs::component::TransformComponent::new()
126                .with_position(pos.0, pos.1, pos.2)
127                .with_scale(scale, scale, scale),
128        );
129        let r = universe
130            .world
131            .add_component(engine::ecs::component::RenderableComponent::cube());
132        let c = universe
133            .world
134            .add_component(engine::ecs::component::ColorComponent::rgba(
135                rgba[0], rgba[1], rgba[2], rgba[3],
136            ));
137        let e = universe
138            .world
139            .add_component(engine::ecs::component::EmissiveComponent::on());
140        let _ = universe.attach(parent, tx);
141        let _ = universe.attach(tx, r);
142        let _ = universe.attach(r, c);
143        let _ = universe.attach(r, e);
144    }
145
146    fn spawn_op_cube(
147        universe: &mut engine::Universe,
148        parent: engine::ecs::ComponentId,
149        pos: (f32, f32, f32),
150        scale: f32,
151        base_rgba: [f32; 4],
152    ) -> engine::ecs::ComponentId {
153        let tx = universe.world.add_component(
154            engine::ecs::component::TransformComponent::new()
155                .with_position(pos.0, pos.1, pos.2)
156                .with_scale(scale, scale, scale),
157        );
158        let r = universe
159            .world
160            .add_component(engine::ecs::component::RenderableComponent::cube());
161        let c = universe
162            .world
163            .add_component(engine::ecs::component::ColorComponent::rgba(
164                base_rgba[0],
165                base_rgba[1],
166                base_rgba[2],
167                base_rgba[3],
168            ));
169        let e = universe
170            .world
171            .add_component(engine::ecs::component::EmissiveComponent::on());
172
173        let _ = universe.attach(parent, tx);
174        let _ = universe.attach(tx, r);
175        let _ = universe.attach(r, c);
176        let _ = universe.attach(r, e);
177
178        tx
179    }
180
181    // --- HUD / lanes ---
182    let lane_x = -2.6_f32;
183    let lane_title_z = -0.4_f32;
184    let lane_cfg_z = -0.4_f32;
185    let _lane_cube_z = -0.8_f32;
186    let drum_y = 0.9_f32;
187    let noise_y = -0.4_f32;
188
189    spawn_text(
190        &mut universe,
191        (lane_x, drum_y + 0.55, lane_title_z),
192        0.09,
193        "Audio Source A: Drum (kick)",
194    );
195    spawn_text(
196        &mut universe,
197        (lane_x, drum_y + 0.25, lane_cfg_z),
198        0.07,
199        "type=Drum\namp=0.40\nbase_freq=32Hz\nkick: C0 dur=0.12 vel=0.90\nlookahead=0.10s",
200    );
201
202    spawn_text(
203        &mut universe,
204        (lane_x, noise_y + 0.55, lane_title_z),
205        0.09,
206        "Audio Source B: Noise",
207    );
208    spawn_text(
209        &mut universe,
210        (lane_x, noise_y + 0.25, lane_cfg_z),
211        0.07,
212        "type=Noise\namp=0.06\nnoise: C9 dur=0.06 vel=0.25\noffset=+0.5 beats\nlookahead=0.10s",
213    );
214
215    // Representative cubes for the two audio sources.
216    let viz_root = universe.world.add_component(
217        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
218    );
219    universe.add(viz_root);
220    spawn_emissive_cube(
221        &mut universe,
222        viz_root,
223        (lane_x - 0.28, drum_y + 0.55, lane_title_z),
224        0.16,
225        [1.00, 0.90, 0.10, 1.0],
226    );
227    spawn_emissive_cube(
228        &mut universe,
229        viz_root,
230        (lane_x - 0.28, noise_y + 0.55, lane_title_z),
231        0.16,
232        [1.00, 1.00, 1.00, 1.0],
233    );
234
235    // Timeline roots so we can "reset all" via a single set_color action.
236    let kick_timeline_root = universe.world.add_component(
237        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
238    );
239    let noise_timeline_root = universe.world.add_component(
240        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
241    );
242    let _ = universe.attach(viz_root, kick_timeline_root);
243    let _ = universe.attach(viz_root, noise_timeline_root);
244
245    // Animation with 16 keyframes: each keyframe schedules a kick at offset 0.0,
246    // and a noise hit at offset 0.5.
247    let anim = universe
248        .world
249        .add_component(engine::ecs::component::AnimationComponent::new());
250
251    let kick_dur = 0.12_f32;
252    let noise_dur = 0.06_f32;
253
254    // Requested palette:
255    // - kick: dark yellow -> bright yellow
256    // - noise: grey -> white
257    let kick_dark = [0.35, 0.28, 0.02, 1.0];
258    let kick_bright = [1.00, 0.90, 0.10, 1.0];
259    let noise_dark = [0.35, 0.35, 0.35, 1.0];
260    let noise_bright = [1.00, 1.00, 1.00, 1.0];
261
262    let beat_spacing = 0.38_f32;
263    for i in 0..16 {
264        let kf_beat = i as f64;
265        let kf = universe
266            .world
267            .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
268
269        let kick = universe
270            .world
271            .add_component(engine::ecs::component::ActionComponent::new(
272                engine::ecs::IntentValue::AudioSchedulePlay {
273                    component_ids: vec![osc_drum_comp],
274                    beat_offset: 0.0,
275                    beat_context: None,
276                    note: Some(
277                        engine::ecs::component::MusicNote::c(0, kick_dur).with_velocity(0.9),
278                    ),
279                    gain: None,
280                    rate: None,
281                    duration: None,
282                },
283            ));
284
285        let noise = universe
286            .world
287            .add_component(engine::ecs::component::ActionComponent::new(
288                engine::ecs::IntentValue::AudioSchedulePlay {
289                    component_ids: vec![osc_noise_comp],
290                    beat_offset: 0.5,
291                    beat_context: None,
292                    note: Some(
293                        engine::ecs::component::MusicNote::c(9, noise_dur).with_velocity(0.25),
294                    ),
295                    gain: None,
296                    rate: None,
297                    duration: None,
298                },
299            ));
300
301        let _ = universe.attach(anim, kf);
302        let _ = universe.attach(kf, kick);
303        let _ = universe.attach(kf, noise);
304
305        // Each keyframe drives visualization purely via normal actions:
306        // 1) reset all timeline cubes to their base colors
307        // 2) brighten the current beat's cubes
308        let reset_kick_lane =
309            universe
310                .world
311                .add_component(engine::ecs::component::ActionComponent::new(
312                    engine::ecs::IntentValue::SetColor {
313                        component_ids: vec![kick_timeline_root],
314                        rgba: kick_dark,
315                    },
316                ));
317        let reset_noise_lane =
318            universe
319                .world
320                .add_component(engine::ecs::component::ActionComponent::new(
321                    engine::ecs::IntentValue::SetColor {
322                        component_ids: vec![noise_timeline_root],
323                        rgba: noise_dark,
324                    },
325                ));
326        let _ = universe.attach(kf, reset_kick_lane);
327        let _ = universe.attach(kf, reset_noise_lane);
328
329        // Timeline cubes: one cube per scheduled audio operation.
330        // Kick (offset 0.0) on the drum lane.
331        let kick_x = (kf_beat as f32) * beat_spacing;
332        let kick_cube = spawn_op_cube(
333            &mut universe,
334            kick_timeline_root,
335            (kick_x, drum_y, -1.3),
336            0.10,
337            kick_dark,
338        );
339
340        // Noise (offset 0.5) on the noise lane.
341        let noise_x = ((kf_beat + 0.5) as f32) * beat_spacing;
342        let noise_cube = spawn_op_cube(
343            &mut universe,
344            noise_timeline_root,
345            (noise_x, noise_y, -1.3),
346            0.10,
347            noise_dark,
348        );
349
350        let brighten_kick =
351            universe
352                .world
353                .add_component(engine::ecs::component::ActionComponent::new(
354                    engine::ecs::IntentValue::SetColor {
355                        component_ids: vec![kick_cube],
356                        rgba: kick_bright,
357                    },
358                ));
359        let brighten_noise =
360            universe
361                .world
362                .add_component(engine::ecs::component::ActionComponent::new(
363                    engine::ecs::IntentValue::SetColor {
364                        component_ids: vec![noise_cube],
365                        rgba: noise_bright,
366                    },
367                ));
368        let _ = universe.attach(kf, brighten_kick);
369        let _ = universe.attach(kf, brighten_noise);
370    }
371
372    universe.add(anim);
373
374    universe.systems.process_commands(
375        &mut universe.world,
376        &mut universe.visuals,
377        &mut universe.render_assets,
378        &mut universe.command_queue,
379    );
380
381    engine::Windowing::run_app(universe).expect("Windowing failed");
382}