Skip to main content

AnimationComponent

Struct AnimationComponent 

Source
pub struct AnimationComponent {
    pub state: AnimationState,
    pub resolve_targets: ResolveTargetsMode,
    pub length_beats: Option<f64>,
    pub scope_source: Option<ComponentRef>,
    pub resolved_scope: Option<ComponentId>,
    /* private fields */
}

Fields§

§state: AnimationState§resolve_targets: ResolveTargetsMode§length_beats: Option<f64>

Explicit loop length in beats. None falls back to the derived default in AnimationSystem (floor(max_keyframe_beat) + 1). Authored via Animation.length(beats).

§scope_source: Option<ComponentRef>§resolved_scope: Option<ComponentId>

Implementations§

Source§

impl AnimationComponent

Source

pub fn new() -> Self

Examples found in repository?
examples/mesh-factory-example.rs (line 81)
55    fn spawn_labeled_mesh(
56        universe: &mut engine::Universe,
57        x: f32,
58        y: f32,
59        label: &str,
60        mesh: engine::graphics::primitives::CpuMeshHandle,
61        scale: [f32; 3],
62        color: [f32; 4],
63    ) {
64        use engine::ecs::component::{
65            ActionComponent, AnimationComponent, AnimationState, ColorComponent, EmissiveComponent,
66            KeyframeComponent, RenderableComponent, TextComponent, TransformComponent,
67        };
68        use engine::graphics::primitives::{MaterialHandle, Renderable};
69
70        // Mesh.
71        let root = universe.world.add_component(
72            TransformComponent::new()
73                .with_position(x, y, 0.0)
74                .with_scale(scale[0], scale[1], scale[2]),
75        );
76
77        // Spin each shape around its own +Y axis using AnimationComponent + keyframes.
78        // We fill [0, 2) beats densely so it looks smooth.
79        let anim = universe
80            .world
81            .add_component(AnimationComponent::new().with_state(AnimationState::Looping));
82        let _ = universe.attach(root, anim);
83
84        let steps: usize = 64;
85        for i in 0..steps {
86            let beat = (i as f64) * (2.0 / (steps as f64));
87            let kf = universe.world.add_component(KeyframeComponent::new(beat));
88            let _ = universe.attach(anim, kf);
89
90            // Full turn over 2 beats.
91            let angle = (std::f64::consts::TAU * (beat / 2.0)) as f32;
92            let rotation = utils::math::quat_from_axis_angle([0.0, 1.0, 0.0], angle);
93
94            let action_cid = universe.world.add_component(ActionComponent::new(
95                engine::ecs::IntentValue::UpdateTransform {
96                    component_ids: vec![root],
97                    translation: [x, y, 0.0],
98                    rotation_quat_xyzw: rotation,
99                    scale,
100                },
101            ));
102            let _ = universe.attach(kf, action_cid);
103        }
104
105        let renderable = universe
106            .world
107            .add_component(RenderableComponent::new(Renderable::new(
108                mesh,
109                MaterialHandle::TOON_MESH,
110            )));
111        let color_c = universe
112            .world
113            .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
114        let emissive = universe.world.add_component(EmissiveComponent::on());
115
116        let _ = universe.attach(root, renderable);
117        let _ = universe.attach(renderable, color_c);
118        let _ = universe.attach(renderable, emissive);
119
120        universe.add(root);
121
122        // Label (separate transform so we can scale text independently).
123        let text_root = universe.world.add_component(
124            TransformComponent::new()
125                .with_position(x, y + 0.75, 0.05)
126                .with_scale(0.09, 0.09, 1.0),
127        );
128        let text = universe
129            .world
130            .add_component(TextComponent::with_word_wrap_tokens(
131                label,
132                LABEL_WRAP_AT,
133                ["::", "(", ")", ",", "."],
134            ));
135        let text_color = universe
136            .world
137            .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
138        let text_emissive = universe.world.add_component(EmissiveComponent::on());
139        let _ = universe.attach(text_root, text);
140        let _ = universe.attach(text, text_color);
141        let _ = universe.attach(text, text_emissive);
142        universe.add(text_root);
143    }
More examples
Hide additional examples
examples/text-animation.rs (line 103)
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    // Input-driven camera rig.
14    // Topology: I { T { C3D } }
15    let input = universe
16        .world
17        .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
18    let camera3d = universe
19        .world
20        .add_component(engine::ecs::component::Camera3DComponent::new());
21    let rig_transform = universe.world.add_component(
22        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.5),
23    );
24    let input_mode = universe.world.add_component(
25        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
26    );
27    let _ = universe.attach(input, input_mode);
28    let _ = universe.attach(input, rig_transform);
29    let _ = universe.attach(rig_transform, camera3d);
30
31    // Small on-screen help.
32    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
33    universe.add(input);
34
35    // Light so we can see non-emissive materials.
36    let light_transform = universe.world.add_component(
37        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.0),
38    );
39    let light = universe.world.add_component(
40        engine::ecs::component::PointLightComponent::new()
41            .with_distance(25.0)
42            .with_color(1.0, 1.0, 1.0),
43    );
44    let _ = universe.attach(light_transform, light);
45    universe.add(light_transform);
46
47    use engine::ecs::component::{
48        ColorComponent, TextComponent, TextShadowComponent, TextureComponent,
49        TextureFilteringComponent, TransformComponent, TransparentCutoutComponent,
50    };
51
52    // Styled text root.
53    let text_root = universe.world.add_component(
54        TransformComponent::new()
55            .with_position(0.0, 0.0, 0.0)
56            .with_scale(0.25, 0.25, 1.0),
57    );
58
59    // Color must be an ancestor of the glyph renderables.
60    let color_id = universe
61        .world
62        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
63    let _ = universe.attach(text_root, color_id);
64
65    // This is the component we animate via SetText.
66    let text_id = universe.world.add_component(TextComponent::new(">w<"));
67    let _ = universe.attach(color_id, text_id);
68
69    // Route into cutout pass for cleaner edges.
70    let cutout = universe
71        .world
72        .add_component(TransparentCutoutComponent::new());
73    let _ = universe.attach(text_id, cutout);
74
75    // Font atlas.
76    let tex = universe.world.add_component(TextureComponent::with_uri(
77        "assets/textures/font_system.dds",
78    ));
79    let _ = universe.attach(text_id, tex);
80
81    let shadow = universe.world.add_component(
82        TextShadowComponent::new()
83            .with_scale(1.35)
84            .with_offset([0.06, -0.06, 0.0015]),
85    );
86    let filtering = universe
87        .world
88        .add_component(TextureFilteringComponent::nearest_magnification());
89
90    let _ = universe.attach(text_id, shadow);
91    let _ = universe.attach(text_id, filtering);
92
93    universe.add(text_root);
94
95    let clock_component = universe
96        .world
97        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
98    let _ = universe.add(clock_component);
99
100    // Looping animation: 4 beats long, 4 keyframes.
101    let anim = universe
102        .world
103        .add_component(engine::ecs::component::AnimationComponent::new());
104
105    let faces = [">w<", "^w^", "-_-", "o_o"];
106    for (i, &face) in faces.iter().enumerate() {
107        let kf = universe
108            .world
109            .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
110
111        // Each keyframe emits a SetText intent.
112        let action = universe
113            .world
114            .add_component(engine::ecs::component::ActionComponent::new(
115                engine::ecs::IntentValue::SetText {
116                    component_ids: vec![text_id],
117                    text: face.to_string(),
118                },
119            ));
120
121        let _ = universe.attach(anim, kf);
122        let _ = universe.attach(kf, action);
123    }
124
125    universe.add(anim);
126
127    // Process init-time registrations (Text expands into glyph subtrees here).
128    universe.systems.process_commands(
129        &mut universe.world,
130        &mut universe.visuals,
131        &mut universe.render_assets,
132        &mut universe.command_queue,
133    );
134
135    engine::Windowing::run_app(universe).expect("Windowing failed");
136}
examples/raycast-topology-animation.rs (line 291)
111fn main() {
112    mittens_engine::example_support::ensure_model_assets();
113    utils::logger::init();
114
115    let world = engine::ecs::World::default();
116    let mut universe = engine::Universe::new(world);
117
118    // Background.
119    let bg_color = universe
120        .world
121        .add_component(engine::ecs::component::BackgroundColorComponent::new());
122    let bg_color_c = universe
123        .world
124        .add_component(engine::ecs::component::ColorComponent::rgba(
125            0.1, 0.1, 0.1, 1.0,
126        ));
127    let _ = universe.world.add_child(bg_color, bg_color_c);
128    universe.add(bg_color);
129
130    // Camera rig so we can see the scene.
131    let input = universe
132        .world
133        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
134
135    let rig_transform = universe.world.add_component(
136        engine::ecs::component::TransformComponent::new()
137            .with_position(0.0, 2.0, 8.0)
138            .with_rotation_euler(-0.25, 0.0, 0.0),
139    );
140
141    let camera3d = universe
142        .world
143        .add_component(engine::ecs::component::Camera3DComponent::new());
144
145    let input_mode = universe.world.add_component(
146        engine::ecs::component::InputTransformModeComponent::forward_z()
147            .with_roll_axis_y()
148            .with_fps_rotation(),
149    );
150
151    let _ = universe.attach(input, input_mode);
152    let _ = universe.attach(input, rig_transform);
153    let _ = universe.attach(rig_transform, camera3d);
154
155    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
156    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
157    universe.add(input);
158
159    // Two rotating anchor transforms that the raycaster will be reparented under.
160    // Each ring has its own anchor at a different height.
161    let anchor_a = universe.world.add_component(
162        engine::ecs::component::TransformComponent::new().with_position(0.0, 1.0, 0.0),
163    );
164    let anchor_b = universe.world.add_component(
165        engine::ecs::component::TransformComponent::new().with_position(0.0, 2.2, 0.0),
166    );
167
168    // Visual markers for A/B.
169    fn marker(universe: &mut engine::Universe, parent: engine::ecs::ComponentId, rgba: [f32; 4]) {
170        let r = universe
171            .world
172            .add_component(engine::ecs::component::RenderableComponent::cube());
173        let rc = universe
174            .world
175            .add_component(engine::ecs::component::RaycastableComponent::disabled());
176        let c = universe
177            .world
178            .add_component(engine::ecs::component::ColorComponent::rgba(
179                rgba[0], rgba[1], rgba[2], rgba[3],
180            ));
181        let e = universe
182            .world
183            .add_component(engine::ecs::component::EmissiveComponent::on());
184        let t = universe.world.add_component(
185            engine::ecs::component::TransformComponent::new().with_scale(0.15, 0.15, 0.15),
186        );
187        let _ = universe.attach(parent, t);
188        let _ = universe.attach(t, r);
189        let _ = universe.attach(r, rc);
190        let _ = universe.attach(r, c);
191        let _ = universe.attach(r, e);
192    }
193
194    marker(&mut universe, anchor_a, [0.9, 0.2, 0.2, 1.0]);
195    marker(&mut universe, anchor_b, [0.2, 0.2, 0.9, 1.0]);
196
197    universe.add(anchor_a);
198    universe.add(anchor_b);
199
200    // A ring of cubes around the origin to see which one gets hit.
201    fn ring_cube(
202        universe: &mut engine::Universe,
203        ring_root: engine::ecs::ComponentId,
204        x: f32,
205        y: f32,
206        z: f32,
207        rgba: [f32; 4],
208    ) {
209        let t = universe.world.add_component(
210            engine::ecs::component::TransformComponent::new()
211                .with_position(x, y, z)
212                .with_scale(0.35, 0.35, 0.35),
213        );
214        let r = universe
215            .world
216            .add_component(engine::ecs::component::RenderableComponent::cube());
217        let rc = universe
218            .world
219            .add_component(engine::ecs::component::RaycastableComponent::enabled());
220        let c = universe
221            .world
222            .add_component(engine::ecs::component::ColorComponent::rgba(
223                rgba[0], rgba[1], rgba[2], rgba[3],
224            ));
225        let _ = universe.attach(ring_root, t);
226        let _ = universe.attach(t, r);
227        let _ = universe.attach(r, rc);
228        let _ = universe.attach(r, c);
229    }
230
231    // Two rings: one per anchor height.
232    let n = 28;
233    let (radius_a, y_a) = (4.0, 1.0);
234    let (radius_b, y_b) = (2.6, 2.2);
235
236    let ring_a_root = universe
237        .world
238        .add_component(engine::ecs::component::TransformComponent::new());
239    let ring_b_root = universe
240        .world
241        .add_component(engine::ecs::component::TransformComponent::new());
242
243    for i in 0..n {
244        let a = (i as f32) * (std::f32::consts::TAU / (n as f32));
245        let (x, z) = (radius_a * a.cos(), radius_a * a.sin());
246        let color = if i % 2 == 0 {
247            [0.55, 0.20, 0.20, 1.0]
248        } else {
249            [0.20, 0.55, 0.20, 1.0]
250        };
251        ring_cube(&mut universe, ring_a_root, x, y_a, z, color);
252    }
253
254    for i in 0..n {
255        let a = (i as f32) * (std::f32::consts::TAU / (n as f32));
256        let (x, z) = (radius_b * a.cos(), radius_b * a.sin());
257        let color = if i % 2 == 0 {
258            [0.20, 0.25, 0.60, 1.0]
259        } else {
260            [0.20, 0.55, 0.55, 1.0]
261        };
262        ring_cube(&mut universe, ring_b_root, x, y_b, z, color);
263    }
264
265    // Init rings and attach scoped interaction handlers.
266    universe.add(ring_a_root);
267    universe.add(ring_b_root);
268    universe.add_signal_handler(
269        engine::ecs::SignalKind::RayIntersected,
270        ring_a_root,
271        ring_a_handler,
272    );
273    universe.add_signal_handler(
274        engine::ecs::SignalKind::RayIntersected,
275        ring_b_root,
276        ring_b_handler,
277    );
278
279    // The raycaster component we will move between parents.
280    // Source is inferred from topology:
281    // - Under transforms A/B (no camera child) => parent-forward (-Z)
282    // - Under camera rig transform (has camera child) => cursor-through-camera
283    let raycaster = universe.world.add_component(
284        engine::ecs::component::RayCastComponent::event_driven().with_max_distance(25.0),
285    );
286
287    // Global animation: move the raycaster between anchors.
288    // Loop length is 8 beats (we include a noop keyframe at beat 7.0 to force loop_len=8).
289    let anim_global = universe
290        .world
291        .add_component(engine::ecs::component::AnimationComponent::new());
292    {
293        // beat 0: attach to A.
294        let kf0 = universe
295            .world
296            .add_component(engine::ecs::component::KeyframeComponent::new(0.0));
297        let act0 = universe
298            .world
299            .add_component(engine::ecs::component::ActionComponent::new(
300                engine::ecs::IntentValue::Attach {
301                    parents: vec![anchor_a],
302                    child: raycaster,
303                },
304            ));
305        let _ = universe.attach(kf0, act0);
306        let _ = universe.attach(anim_global, kf0);
307
308        // beat 4: attach to B.
309        let kf4 = universe
310            .world
311            .add_component(engine::ecs::component::KeyframeComponent::new(4.0));
312        let act4 = universe
313            .world
314            .add_component(engine::ecs::component::ActionComponent::new(
315                engine::ecs::IntentValue::Attach {
316                    parents: vec![anchor_b],
317                    child: raycaster,
318                },
319            ));
320        let _ = universe.attach(kf4, act4);
321        let _ = universe.attach(anim_global, kf4);
322
323        // beat 7: noop to make loop_len=8.
324        let kf7 = universe
325            .world
326            .add_component(engine::ecs::component::KeyframeComponent::new(7.0));
327        let noop = universe
328            .world
329            .add_component(engine::ecs::component::ActionComponent::new(
330                engine::ecs::IntentValue::Noop,
331            ));
332        let _ = universe.attach(kf7, noop);
333        let _ = universe.attach(anim_global, kf7);
334    }
335    universe.add(anim_global);
336
337    // Ring A animation: rotate anchor A around Y (1 rev / 8 beats).
338    // Also triggers Action::raycast(raycaster) on downbeats in the first half: beats 0,1,2,3.
339    let anim_a = universe
340        .world
341        .add_component(engine::ecs::component::AnimationComponent::new());
342
343    // Ring B animation: rotate anchor B differently (yaw + pitch).
344    // Also triggers Action::raycast(raycaster) on offbeats in the second half: beats 4.5,5.5,6.5,7.5.
345    let anim_b = universe
346        .world
347        .add_component(engine::ecs::component::AnimationComponent::new());
348
349    let loop_beats = 8.0;
350    let steps = 64;
351
352    for step in 0..=steps {
353        let t = (step as f32) / (steps as f32);
354        let beat = (t as f64) * (loop_beats as f64);
355
356        // Keyframe beats are per-animation local beats.
357        let kf_a = universe
358            .world
359            .add_component(engine::ecs::component::KeyframeComponent::new(beat));
360        let kf_b = universe
361            .world
362            .add_component(engine::ecs::component::KeyframeComponent::new(beat));
363
364        // Anchor A rotation: smooth yaw.
365        let yaw_a = t * std::f32::consts::TAU;
366        let a_set = engine::ecs::component::ActionComponent::new(
367            engine::ecs::IntentValue::UpdateTransform {
368                component_ids: vec![anchor_a],
369                translation: [0.0, 1.0, 0.0],
370                rotation_quat_xyzw: quat_from_yaw(yaw_a),
371                scale: [1.0, 1.0, 1.0],
372            },
373        );
374        let a_set_id = universe.world.add_component(a_set);
375        let _ = universe.attach(kf_a, a_set_id);
376
377        // Anchor B rotation: yaw + pitch (different pattern).
378        let yaw_b = -t * std::f32::consts::TAU * 1.5;
379        let pitch_b = (t * std::f32::consts::TAU).sin() * 0.35;
380        let rot_b = quat_mul(quat_from_yaw(yaw_b), quat_from_pitch(pitch_b));
381        let b_set = engine::ecs::component::ActionComponent::new(
382            engine::ecs::IntentValue::UpdateTransform {
383                component_ids: vec![anchor_b],
384                translation: [0.0, 2.2, 0.0],
385                rotation_quat_xyzw: rot_b,
386                scale: [1.0, 1.0, 1.0],
387            },
388        );
389        let b_set_id = universe.world.add_component(b_set);
390        let _ = universe.attach(kf_b, b_set_id);
391
392        // Per-ring raycast patterns.
393        // A: downbeats in first half (0..4): step % 8 == 0 -> beats 0,1,2,3,4.
394        if beat < 4.0 && step % 8 == 0 {
395            let act = universe
396                .world
397                .add_component(engine::ecs::component::ActionComponent::new(
398                    engine::ecs::IntentValue::RequestRaycast {
399                        component_ids: vec![raycaster],
400                    },
401                ));
402            let _ = universe.attach(kf_a, act);
403        }
404
405        // B: offbeats in second half (4..8): step % 8 == 4 -> beats 0.5,1.5,...,7.5.
406        if beat >= 4.0 && step % 8 == 4 {
407            let act = universe
408                .world
409                .add_component(engine::ecs::component::ActionComponent::new(
410                    engine::ecs::IntentValue::RequestRaycast {
411                        component_ids: vec![raycaster],
412                    },
413                ));
414            let _ = universe.attach(kf_b, act);
415        }
416
417        let _ = universe.attach(anim_a, kf_a);
418        let _ = universe.attach(anim_b, kf_b);
419    }
420
421    universe.add(anim_a);
422    universe.add(anim_b);
423
424    // Process init-time registrations.
425    universe.systems.process_commands(
426        &mut universe.world,
427        &mut universe.visuals,
428        &mut universe.render_assets,
429        &mut universe.command_queue,
430    );
431
432    engine::Windowing::run_app(universe).expect("Windowing failed");
433}
examples/animation-for-topology.rs (line 195)
80fn main() {
81    mittens_engine::example_support::ensure_model_assets();
82    utils::logger::init();
83
84    let world = engine::ecs::World::default();
85    let mut universe = engine::Universe::new(world);
86
87    // Minimal scene with a camera so the window opens.
88    let clear = universe
89        .world
90        .add_component(engine::ecs::component::BackgroundColorComponent::new());
91    let clear_c = universe
92        .world
93        .add_component(engine::ecs::component::ColorComponent::rgba(
94            0.06, 0.06, 0.07, 1.0,
95        ));
96    let _ = universe.world.add_child(clear, clear_c);
97    universe.add(clear);
98
99    // Input-driven camera rig.
100    let input = universe
101        .world
102        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
103    let rig_transform = universe.world.add_component(
104        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 9.0),
105    );
106    let input_mode = universe.world.add_component(
107        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
108    );
109    let camera3d = universe.world.add_component(
110        engine::ecs::component::Camera3DComponent::new()
111            .with_far(250.0)
112            .with_fov(70.0),
113    );
114    let _ = universe.attach(input, input_mode);
115    let _ = universe.attach(input, rig_transform);
116    let _ = universe.attach(rig_transform, camera3d);
117
118    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
119    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
120    universe.add(input);
121
122    // Light so we can see non-emissive materials.
123    let light_tx = universe.world.add_component(
124        engine::ecs::component::TransformComponent::new().with_position(2.0, 3.5, 6.0),
125    );
126    let light = universe.world.add_component(
127        engine::ecs::component::PointLightComponent::new()
128            .with_distance(30.0)
129            .with_color(1.0, 1.0, 1.0),
130    );
131    let _ = universe.attach(light_tx, light);
132    universe.add(light_tx);
133
134    // ClockComponent drives the animation timeline in beats.
135    let clock = universe
136        .world
137        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
138    universe.add(clock);
139
140    // Root for all visualization objects.
141    let viz_root = universe.world.add_component(
142        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
143    );
144    universe.add(viz_root);
145
146    let anchor_count = 16usize;
147    let cube_pool_size = 8usize;
148
149    // Three grids side-by-side.
150    let layout_a = GridLayout {
151        origin: (-6.0, 0.0, 0.0),
152        spacing: 1.1,
153    };
154    let layout_b = GridLayout {
155        origin: (0.0, 0.0, 0.0),
156        spacing: 1.1,
157    };
158    let layout_c = GridLayout {
159        origin: (6.0, 0.0, 0.0),
160        spacing: 1.1,
161    };
162
163    // --- Grid A: detach + re-attach (reparent) ---
164    let grid_a_root = universe
165        .world
166        .add_component(engine::ecs::component::TransformComponent::new());
167    let _ = universe.attach(viz_root, grid_a_root);
168
169    let mut anchors_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
170    for i in 0..anchor_count {
171        let (x, y, z) = grid_anchor_local(layout_a, i);
172        let anchor = universe.world.add_component(
173            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
174        );
175        let _ = universe.attach(grid_a_root, anchor);
176        anchors_a.push(anchor);
177        spawn_emissive_marker_cube(
178            &mut universe,
179            anchor,
180            (0.0, -0.35, 0.0),
181            0.06,
182            [0.18, 0.18, 0.22, 1.0],
183        );
184    }
185
186    let mut cubes_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
187    for i in 0..cube_pool_size {
188        let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
189        let rgba = [0.10, 0.40 + 0.50 * t, 0.90 - 0.70 * t, 1.0];
190        cubes_a.push(spawn_detached_cube_prefab(&mut universe, 0.22, rgba));
191    }
192
193    let anim_a = universe
194        .world
195        .add_component(engine::ecs::component::AnimationComponent::new());
196    for i in 0..anchor_count {
197        let cube = cubes_a[i % cube_pool_size];
198        let parent = anchors_a[i];
199
200        // Explicit detach+attach to test topology changes via animations.
201
202        // Put them on separate keyframes so ordering is time-deterministic.
203        let beat_detach = i as f64;
204        let beat_attach = i as f64 + 0.05;
205
206        let kf_detach = universe
207            .world
208            .add_component(engine::ecs::component::KeyframeComponent::new(beat_detach));
209        let _ = universe.attach(anim_a, kf_detach);
210        let detach_action =
211            universe
212                .world
213                .add_component(engine::ecs::component::ActionComponent::new(
214                    engine::ecs::IntentValue::Detach {
215                        component_ids: vec![cube],
216                    },
217                ));
218        let _ = universe.attach(kf_detach, detach_action);
219
220        let kf_attach = universe
221            .world
222            .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
223        let _ = universe.attach(anim_a, kf_attach);
224        let attach_action =
225            universe
226                .world
227                .add_component(engine::ecs::component::ActionComponent::new(
228                    engine::ecs::IntentValue::Attach {
229                        parents: vec![parent],
230                        child: cube,
231                    },
232                ));
233        let _ = universe.attach(kf_attach, attach_action);
234    }
235    universe.add(anim_a);
236
237    // --- Grid B: continuously spawn (attach_clone) + delete-behind (remove_child) ---
238    //
239    // Uses the new actions:
240    // - `Action::attach_clone(parent, prefab_root)`
241    // - `Action::remove_child(parent, index)`
242    //
243    // This avoids needing a pre-built pool of cube ComponentIds.
244    let grid_b_root = universe
245        .world
246        .add_component(engine::ecs::component::TransformComponent::new());
247    let _ = universe.attach(viz_root, grid_b_root);
248
249    let mut anchors_b: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
250    for i in 0..anchor_count {
251        let (x, y, z) = grid_anchor_local(layout_b, i);
252        let anchor = universe.world.add_component(
253            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
254        );
255        let _ = universe.attach(grid_b_root, anchor);
256        anchors_b.push(anchor);
257
258        // Marker is attached under the grid root (not under the anchor), so anchor child index 0
259        // remains reserved for the dynamic cube subtree.
260        spawn_emissive_marker_cube(
261            &mut universe,
262            grid_b_root,
263            (x, y - 0.35, z),
264            0.06,
265            [0.22, 0.18, 0.18, 1.0],
266        );
267    }
268
269    // Detached prefab that will be cloned on demand (reddish cube).
270    let prefab_b = spawn_detached_cube_prefab(&mut universe, 0.20, [0.90, 0.25, 0.15, 1.0]);
271
272    let steps_b = 32usize;
273    let window = 8usize;
274
275    let anim_b = universe
276        .world
277        .add_component(engine::ecs::component::AnimationComponent::new());
278
279    // Phase 1: each beat, spawn a cube under an anchor; delete-behind by removing child(0).
280    for i in 0..steps_b {
281        let parent = anchors_b[i % anchor_count];
282
283        let beat_attach = i as f64;
284        let beat_remove = i as f64 + 0.05;
285
286        let kf_attach = universe
287            .world
288            .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
289        let _ = universe.attach(anim_b, kf_attach);
290
291        let attach_action =
292            universe
293                .world
294                .add_component(engine::ecs::component::ActionComponent::new(
295                    engine::ecs::IntentValue::AttachClone {
296                        parents: vec![parent],
297                        prefab_root: prefab_b,
298                    },
299                ));
300        let _ = universe.attach(kf_attach, attach_action);
301
302        if i >= window {
303            let remove_parent = anchors_b[(i - window) % anchor_count];
304            let kf_remove = universe
305                .world
306                .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
307            let _ = universe.attach(anim_b, kf_remove);
308
309            let remove_action =
310                universe
311                    .world
312                    .add_component(engine::ecs::component::ActionComponent::new(
313                        engine::ecs::IntentValue::RemoveChild {
314                            parents: vec![remove_parent],
315                            index: 0,
316                        },
317                    ));
318            let _ = universe.attach(kf_remove, remove_action);
319        }
320    }
321
322    // Phase 2: cleanup tail so the loop doesn't accumulate cubes.
323    for j in 0..window {
324        let remove_parent = anchors_b[(steps_b - window + j) % anchor_count];
325        let beat_remove = steps_b as f64 + j as f64 + 0.05;
326
327        let kf_remove = universe
328            .world
329            .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
330        let _ = universe.attach(anim_b, kf_remove);
331
332        let remove_action =
333            universe
334                .world
335                .add_component(engine::ecs::component::ActionComponent::new(
336                    engine::ecs::IntentValue::RemoveChild {
337                        parents: vec![remove_parent],
338                        index: 0,
339                    },
340                ));
341        let _ = universe.attach(kf_remove, remove_action);
342    }
343
344    universe.add(anim_b);
345
346    // --- Grid C: move cubes via set_position (no topology changes) ---
347    let grid_c_root = universe
348        .world
349        .add_component(engine::ecs::component::TransformComponent::new());
350    let _ = universe.attach(viz_root, grid_c_root);
351
352    let mut anchors_c: Vec<(f32, f32, f32)> = Vec::with_capacity(anchor_count);
353    for i in 0..anchor_count {
354        let (x, y, z) = grid_anchor_local(layout_c, i);
355        anchors_c.push((x, y, z));
356
357        let anchor = universe.world.add_component(
358            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
359        );
360        let _ = universe.attach(grid_c_root, anchor);
361        spawn_emissive_marker_cube(
362            &mut universe,
363            anchor,
364            (0.0, -0.35, 0.0),
365            0.06,
366            [0.18, 0.22, 0.18, 1.0],
367        );
368    }
369
370    let mut cubes_c: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
371    for i in 0..cube_pool_size {
372        let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
373        let rgba = [0.25, 0.85 - 0.55 * t, 0.25 + 0.55 * t, 1.0];
374        let cube_root = spawn_detached_cube_prefab(&mut universe, 0.22, rgba);
375        let _ = universe.attach(grid_c_root, cube_root);
376        cubes_c.push(cube_root);
377    }
378
379    let anim_c = universe
380        .world
381        .add_component(engine::ecs::component::AnimationComponent::new());
382    for i in 0..anchor_count {
383        let kf = universe
384            .world
385            .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
386        let _ = universe.attach(anim_c, kf);
387
388        let cube = cubes_c[i % cube_pool_size];
389        let (x, y, z) = anchors_c[i];
390        let setpos_action =
391            universe
392                .world
393                .add_component(engine::ecs::component::ActionComponent::new(
394                    engine::ecs::IntentValue::SetPosition {
395                        component_ids: vec![cube],
396                        position: [x, y, z],
397                    },
398                ));
399        let _ = universe.attach(kf, setpos_action);
400    }
401    universe.add(anim_c);
402
403    universe.systems.process_commands(
404        &mut universe.world,
405        &mut universe.visuals,
406        &mut universe.render_assets,
407        &mut universe.command_queue,
408    );
409
410    engine::Windowing::run_app(universe).expect("Windowing failed");
411}
examples/animation-example.rs (line 249)
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}
examples/audio-graph-example.rs (line 627)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    // Debug toggle: allow isolating stack overflows to audio vs. non-audio init.
11    // Set `CAT_AUDIO_EXAMPLE_DISABLE_AUDIO=1` to skip creating audio components and
12    // skip scheduling audio notes. The UI/text/cube visualization will still spawn.
13    let audio_enabled = std::env::var("CAT_AUDIO_EXAMPLE_DISABLE_AUDIO")
14        .ok()
15        .as_deref()
16        != Some("1");
17
18    // If set, we still build audio graph components but we don't start the CPAL output stream.
19    // This helps distinguish "audio graph / scheduling" issues from "CPAL backend" issues.
20    let audio_output_enabled = std::env::var("CAT_AUDIO_EXAMPLE_AUDIO_OUTPUT_OFF")
21        .ok()
22        .as_deref()
23        != Some("1");
24
25    println!("[audio-graph-example] start");
26
27    let world = engine::ecs::World::default();
28    let mut universe = engine::Universe::new(world);
29
30    println!("[audio-graph-example] universe created");
31
32    // Minimal scene with a camera so the window opens (copied from animation-example).
33    let clear = universe
34        .world
35        .add_component(engine::ecs::component::BackgroundColorComponent::new());
36    let clear_c = universe
37        .world
38        .add_component(engine::ecs::component::ColorComponent::rgba(
39            0.07, 0.07, 0.07, 1.0,
40        ));
41    let _ = universe.world.add_child(clear, clear_c);
42    universe.add(clear);
43
44    // Ambient light so unlit areas aren't pitch black.
45    // Keep it dark to match the background clear color.
46    // (User request) 2.5x brighter.
47    let ambient = universe
48        .world
49        .add_component(engine::ecs::component::AmbientLightComponent::rgb(
50            0.075, 0.075, 0.075,
51        ));
52    universe.add(ambient);
53
54    // Input-driven camera rig.
55    let input = universe
56        .world
57        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
58    let rig_transform = universe.world.add_component(
59        engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
60    );
61    let input_mode = universe.world.add_component(
62        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
63    );
64    let camera3d = universe.world.add_component(
65        engine::ecs::component::Camera3DComponent::new()
66            .with_far(250.0)
67            .with_fov(70.0),
68    );
69    let _ = universe.attach(input, input_mode);
70    let _ = universe.attach(input, rig_transform);
71    let _ = universe.attach(rig_transform, camera3d);
72
73    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
74    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
75    universe.add(input);
76
77    // Directional light (sun-ish). Note: the renderer interprets the node's world position
78    // as a direction vector (see DirectionalLightComponent docs).
79    let light_tx = universe.world.add_component(
80        engine::ecs::component::TransformComponent::new().with_position(0.2, 0.7, 1.0),
81    );
82    let light = universe.world.add_component(
83        engine::ecs::component::DirectionalLightComponent::new()
84            .with_color(1.0, 1.0, 1.0)
85            .with_intensity(0.35),
86    );
87    let _ = universe.attach(light_tx, light);
88    universe.add(light_tx);
89
90    // --- Background clouds (occluded + lit) ---
91    // Mirrors the vtuber example: use a BackgroundComponent stage so the cloud volume
92    // self-occludes and is lit, but renders as background.
93    let bg_root = universe.world.add_component(
94        engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
95    );
96    universe.add(bg_root);
97    let mut cloud_params = example_util::CloudRingParams::default();
98    cloud_params.cloud_count = 7;
99    cloud_params.radius = 22.0;
100    // Move the ring up by ~one cloud height. The cloud generator uses a ~4.0 unit
101    // vertical spread for puff offsets, so +4.0 is a good "one height" bump.
102    cloud_params.center_y = 6.0;
103    cloud_params.puffs_per_cloud = 26;
104    cloud_params.angle_jitter = 0.0;
105    cloud_params.high_y_probability = 0.0;
106    cloud_params.high_y_multiplier = 1.0;
107    cloud_params.seed = 0xA0_D1_0C_01u32;
108    example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
109
110    // ClockComponent sets global tempo.
111    if audio_enabled {
112        let clock = universe
113            .world
114            .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
115        universe.add(clock);
116    }
117
118    if audio_enabled {
119        println!("[audio-graph-example] clock added");
120    } else {
121        println!("[audio-graph-example] audio disabled; skipping clock");
122    }
123
124    // Audio output + 2 oscillator sources.
125    let audio_out = if audio_enabled {
126        let audio_out_comp = if audio_output_enabled {
127            engine::ecs::component::AudioOutputComponent::new()
128        } else {
129            engine::ecs::component::AudioOutputComponent::off()
130        };
131        let audio_out = universe.world.add_component(audio_out_comp);
132        universe.add(audio_out);
133        Some(audio_out)
134    } else {
135        None
136    };
137
138    match (audio_enabled, audio_output_enabled) {
139        (false, _) => println!("[audio-graph-example] audio disabled; skipping audio output"),
140        (true, true) => println!("[audio-graph-example] audio output added (CPAL on)"),
141        (true, false) => println!("[audio-graph-example] audio output added (CPAL off)"),
142    }
143
144    // Track A: square lead.
145    let osc_a_comp =
146        if audio_enabled {
147            let osc_a = engine::ecs::component::AudioOscillator::square()
148                .with_frequency(110.0)
149                .with_amplitude(0.12)
150                .with_enabled(false);
151            let osc_a_comp = universe.world.add_component(
152                engine::ecs::component::AudioOscillatorComponent::single(osc_a),
153            );
154            if let Some(audio_out) = audio_out {
155                let _ = universe.attach(audio_out, osc_a_comp);
156            }
157            Some(osc_a_comp)
158        } else {
159            None
160        };
161
162    if audio_enabled {
163        println!("[audio-graph-example] track A created");
164    } else {
165        println!("[audio-graph-example] audio disabled; skipping track A");
166    }
167
168    // Effect tree A (single chain):
169    // osc_a
170    //   Gain
171    //     BandPass
172    //       Limiter
173    let mut bp_a_comp: Option<engine::ecs::ComponentId> = None;
174    if let Some(osc_a_comp) = osc_a_comp {
175        let gain_a = universe
176            .world
177            .add_component(engine::ecs::component::AudioGainComponent::new(3.2));
178        let bp_a = universe.world.add_component(
179            engine::ecs::component::AudioBandPassFilterComponent::new(120.0, 3.0, 0.40),
180        );
181        bp_a_comp = Some(bp_a);
182        let lim_a =
183            universe
184                .world
185                .add_component(engine::ecs::component::AudioLimiterComponent::new(
186                    4.0, 80.0, 0.90,
187                ));
188
189        let _ = universe.attach(osc_a_comp, gain_a);
190        let _ = universe.attach(gain_a, bp_a);
191        let _ = universe.attach(bp_a, lim_a);
192    }
193
194    if audio_enabled {
195        println!("[audio-graph-example] track A effects attached");
196    }
197
198    // --- Visual layout helpers (copied/adapted from animation-example) ---
199    fn spawn_text(
200        universe: &mut engine::Universe,
201        pos: (f32, f32, f32),
202        scale: f32,
203        wrap_cols: usize,
204        text: &str,
205    ) {
206        let tx = universe.world.add_component(
207            engine::ecs::component::TransformComponent::new()
208                .with_position(pos.0, pos.1, pos.2)
209                .with_scale(scale, scale, 1.0),
210        );
211        let t =
212            universe
213                .world
214                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
215                    text, wrap_cols,
216                ));
217        let _ = universe.attach(tx, t);
218
219        // TextSystem looks for an immediate TextureFilteringComponent child.
220        let filtering = universe
221            .world
222            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223        let _ = universe.attach(t, filtering);
224
225        // TextSystem also supports styling from immediate Emissive children.
226        let emissive = universe
227            .world
228            .add_component(engine::ecs::component::EmissiveComponent::on());
229        let _ = universe.attach(t, emissive);
230
231        universe.add(tx);
232    }
233
234    fn spawn_emissive_cube(
235        universe: &mut engine::Universe,
236        parent: engine::ecs::ComponentId,
237        pos: (f32, f32, f32),
238        scale: f32,
239        rgba: [f32; 4],
240    ) {
241        let tx = universe.world.add_component(
242            engine::ecs::component::TransformComponent::new()
243                .with_position(pos.0, pos.1, pos.2)
244                .with_scale(scale, scale, scale),
245        );
246        let r = universe
247            .world
248            .add_component(engine::ecs::component::RenderableComponent::cube());
249        let c = universe
250            .world
251            .add_component(engine::ecs::component::ColorComponent::rgba(
252                rgba[0], rgba[1], rgba[2], rgba[3],
253            ));
254        let e = universe
255            .world
256            .add_component(engine::ecs::component::EmissiveComponent::on());
257        let _ = universe.attach(parent, tx);
258        let _ = universe.attach(tx, r);
259        let _ = universe.attach(r, c);
260        let _ = universe.attach(r, e);
261    }
262
263    fn spawn_op_cube(
264        universe: &mut engine::Universe,
265        parent: engine::ecs::ComponentId,
266        pos: (f32, f32, f32),
267        scale: f32,
268        base_rgba: [f32; 4],
269    ) -> engine::ecs::ComponentId {
270        let tx = universe.world.add_component(
271            engine::ecs::component::TransformComponent::new()
272                .with_position(pos.0, pos.1, pos.2)
273                .with_scale(scale, scale, scale),
274        );
275        let r = universe
276            .world
277            .add_component(engine::ecs::component::RenderableComponent::cube());
278        let c = universe
279            .world
280            .add_component(engine::ecs::component::ColorComponent::rgba(
281                base_rgba[0],
282                base_rgba[1],
283                base_rgba[2],
284                base_rgba[3],
285            ));
286        let e = universe
287            .world
288            .add_component(engine::ecs::component::EmissiveComponent::on());
289
290        let _ = universe.attach(parent, tx);
291        let _ = universe.attach(tx, r);
292        let _ = universe.attach(r, c);
293        let _ = universe.attach(r, e);
294
295        tx
296    }
297
298    // --- HUD / lane (single track) ---
299    let lane_x = -2.6_f32;
300    let lane_title_z = -0.4_f32;
301    let lane_cfg_z = -0.4_f32;
302    let lane_pat_z = -1.3_f32;
303    let lane_graph_z = -1.15_f32;
304
305    // Content for pattern/chain/graph starts at x=0; keep section labels aligned there.
306    // This also keeps them from overlapping the config block (which is anchored at lane_x).
307    let lane_labels_x = lane_x + 1.6_f32;
308
309    let track_a_y = 0.9_f32;
310
311    spawn_text(
312        &mut universe,
313        (lane_x, track_a_y + 0.55, lane_title_z),
314        0.09,
315        42,
316        "Track A: AudioOscillator::square()",
317    );
318    spawn_text(
319        &mut universe,
320        (lane_x, track_a_y + 0.25, lane_cfg_z),
321        0.07,
322        48,
323        "oscillators=1\nfrequency_hz=110.0\namplitude=0.12\nenabled=false\nlookahead=0.10s",
324    );
325
326    let viz_root = universe.world.add_component(
327        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
328    );
329    universe.add(viz_root);
330
331    println!("[audio-graph-example] viz root added");
332
333    // Small identifier cubes next to titles.
334    spawn_emissive_cube(
335        &mut universe,
336        viz_root,
337        (lane_x - 0.28, track_a_y + 0.55, lane_title_z),
338        0.16,
339        [0.85, 0.40, 1.00, 1.0],
340    );
341
342    // Pattern roots so we can reset via one SetColor action.
343    let track_a_pattern_root = universe.world.add_component(
344        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
345    );
346    let _ = universe.attach(viz_root, track_a_pattern_root);
347
348    // Labels for pattern/chain/graph sections.
349    spawn_text(
350        &mut universe,
351        (lane_labels_x, track_a_y, lane_title_z),
352        0.06,
353        42,
354        "pattern",
355    );
356    spawn_text(
357        &mut universe,
358        (lane_labels_x, track_a_y - 0.40, lane_title_z),
359        0.06,
360        42,
361        "graph",
362    );
363
364    // --- Processing chain + graph visualization (compiled graph → cubes + labels) ---
365    use engine::ecs::system::audio_graph_compiler::{
366        AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367    };
368
369    fn effect_grey_for_depth(depth: usize) -> f32 {
370        // depth=1 => medium grey; deeper => lighter, capped.
371        let base = 0.55;
372        let step = 0.13;
373        let d = depth.saturating_sub(1) as f32;
374        (base + step * d).min(0.92)
375    }
376
377    fn node_rgba(node: &AudioGraphNode, depth: usize) -> [f32; 4] {
378        match node.kind {
379            AudioGraphNodeKind::OscillatorSource { .. } => [1.0, 0.78, 0.22, 1.0],
380            _ => {
381                let g = effect_grey_for_depth(depth);
382                [g, g, g, 1.0]
383            }
384        }
385    }
386
387    fn node_label(node: &AudioGraphNode) -> String {
388        match &node.kind {
389            AudioGraphNodeKind::OscillatorSource { voices } => {
390                format!("OscillatorSource voices={voices}")
391            }
392            AudioGraphNodeKind::Gain { gain } => {
393                format!("Gain gain={gain:.3}")
394            }
395            AudioGraphNodeKind::LowPass {
396                cutoff_hz,
397                resonance,
398            } => {
399                format!("LowPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
400            }
401            AudioGraphNodeKind::BandPass {
402                center_hz,
403                bandwidth_octaves,
404                resonance,
405            } => {
406                format!(
407                    "BandPass center={center_hz:.1}Hz bw={bandwidth_octaves:.3}oct res={resonance:.3}"
408                )
409            }
410            AudioGraphNodeKind::HighPass {
411                cutoff_hz,
412                resonance,
413            } => {
414                format!("HighPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
415            }
416            AudioGraphNodeKind::Limiter {
417                attack_ms,
418                release_ms,
419                threshold,
420            } => {
421                format!("Limiter atk={attack_ms:.1}ms rel={release_ms:.1}ms thr={threshold:.3}")
422            }
423            AudioGraphNodeKind::ClipSource => "ClipSource".to_string(),
424        }
425    }
426
427    fn compute_layout(
428        node: &AudioGraphNode,
429        depth: usize,
430        x_cursor: &mut i32,
431        out: &mut std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
432    ) -> i32 {
433        // Depth-only layout:
434        // - keep children directly below their parent on X
435        // - use Z sibling offsets (in spawn_graph_tree) to show branching
436        let _ = x_cursor;
437        for ch in node.children.iter() {
438            let _ = compute_layout(ch, depth + 1, x_cursor, out);
439        }
440
441        let my_x = 0;
442        out.insert(node as *const AudioGraphNode, (my_x, depth));
443        my_x
444    }
445
446    fn spawn_graph_tree(
447        universe: &mut engine::Universe,
448        parent: engine::ecs::ComponentId,
449        node: &AudioGraphNode,
450        bp_component: Option<engine::ecs::ComponentId>,
451        bp_label_out: &mut Option<engine::ecs::ComponentId>,
452        origin: (f32, f32, f32),
453        layout: &std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
454        dx: f32,
455        dy: f32,
456        cube_scale: f32,
457        sibling_index: usize,
458        sibling_count: usize,
459    ) {
460        let Some((x_unit, depth)) = layout.get(&(node as *const AudioGraphNode)).copied() else {
461            return;
462        };
463
464        let x = origin.0 + (x_unit as f32) * dx;
465        let y = origin.1 - (depth as f32) * dy;
466
467        // Push siblings “behind” each other along Z to reduce overlap between
468        // a sibling's cube and another node's label.
469        let dz_sibling = 0.18;
470        let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472        // Keep text slightly in front of its cube.
473        let z_text = z + 0.03;
474
475        let rgba = node_rgba(node, depth);
476        let cube_tx = spawn_op_cube(universe, parent, (x, y, z), cube_scale, rgba);
477        let _ = cube_tx;
478
479        // Label next to the cube.
480        let label = node_label(node);
481        let tx = universe.world.add_component(
482            engine::ecs::component::TransformComponent::new()
483                .with_position(x + 0.14, y + 0.02, z_text)
484                .with_scale(0.06, 0.06, 1.0),
485        );
486        let t =
487            universe
488                .world
489                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
490                    &label, 25,
491                ));
492        let _ = universe.attach(parent, tx);
493        let _ = universe.attach(tx, t);
494
495        if bp_component == Some(node.component) {
496            *bp_label_out = Some(t);
497        }
498
499        let filtering = universe
500            .world
501            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
502        let _ = universe.attach(t, filtering);
503
504        let emissive = universe
505            .world
506            .add_component(engine::ecs::component::EmissiveComponent::on());
507        let _ = universe.attach(t, emissive);
508
509        universe.add(tx);
510
511        // Mix/branch label if branching.
512        if node.children.len() > 1 {
513            let mut weights: Vec<f32> = Vec::with_capacity(node.children.len());
514            for i in 0..node.children.len() {
515                let w = node
516                    .mix
517                    .as_ref()
518                    .map(|m| m.weights.get(i).copied().unwrap_or(1.0))
519                    .unwrap_or(1.0);
520                weights.push(w);
521            }
522            let mix_label = if node.mix.is_some() {
523                format!("Mix weights={weights:?}")
524            } else {
525                format!("Mix <implicit> w={weights:?}")
526            };
527            let mix_tx = universe.world.add_component(
528                engine::ecs::component::TransformComponent::new()
529                    .with_position(x + 0.14, y - 0.11, z_text)
530                    .with_scale(0.05, 0.05, 1.0),
531            );
532            let mix_t = universe.world.add_component(
533                engine::ecs::component::TextComponent::with_word_wrap(&mix_label, 25),
534            );
535            let _ = universe.attach(parent, mix_tx);
536            let _ = universe.attach(mix_tx, mix_t);
537
538            let filtering = universe
539                .world
540                .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
541            let _ = universe.attach(mix_t, filtering);
542
543            let emissive = universe
544                .world
545                .add_component(engine::ecs::component::EmissiveComponent::on());
546            let _ = universe.attach(mix_t, emissive);
547
548            universe.add(mix_tx);
549        }
550
551        let child_count = node.children.len().max(1);
552        for (i, ch) in node.children.iter().enumerate() {
553            // Each node's children form a sibling group; offset them in Z.
554            // For the root node, sibling_index is 0.
555            let _ = sibling_count;
556            spawn_graph_tree(
557                universe,
558                parent,
559                ch,
560                bp_component,
561                bp_label_out,
562                origin,
563                layout,
564                dx,
565                dy,
566                cube_scale,
567                i,
568                child_count,
569            );
570        }
571    }
572
573    // Compile + display chain and graph for the track.
574    let compiled_a = if audio_enabled {
575        println!("[audio-graph-example] compiling graph...");
576        let Some(osc_a_comp) = osc_a_comp else {
577            panic!("audio_enabled but track A was not created");
578        };
579        let compiled_a =
580            AudioGraphCompiler::compile(&universe.world, osc_a_comp).expect("compile A");
581        println!("[audio-graph-example] graph compiled");
582        Some(compiled_a)
583    } else {
584        println!("[audio-graph-example] audio disabled; skipping graph compilation");
585        None
586    };
587
588    // Full compiled graph visualization (tree layout + mix labels).
589    let mut bp_graph_label_text: Option<engine::ecs::ComponentId> = None;
590    if let Some(compiled_a) = &compiled_a {
591        let mut x_cursor = 0;
592        let mut layout: std::collections::HashMap<*const AudioGraphNode, (i32, usize)> =
593            std::collections::HashMap::new();
594        let _root_x = compute_layout(&compiled_a.root, 0, &mut x_cursor, &mut layout);
595
596        // With depth-only X layout, we keep the tree centered at x=0.
597        // Pull the graph up now that we don't show the separate chain view.
598        let origin = (0.0, track_a_y - 0.40, lane_graph_z);
599
600        spawn_graph_tree(
601            &mut universe,
602            viz_root,
603            &compiled_a.root,
604            bp_a_comp,
605            &mut bp_graph_label_text,
606            origin,
607            &layout,
608            0.45,
609            0.28,
610            0.08,
611            0,
612            1,
613        );
614    } else {
615        spawn_text(
616            &mut universe,
617            (0.0, track_a_y - 0.40, lane_graph_z),
618            0.06,
619            60,
620            "(audio disabled)",
621        );
622    }
623
624    // Animation with 16 keyframes drives scheduled notes + pattern highlights.
625    let anim = universe
626        .world
627        .add_component(engine::ecs::component::AnimationComponent::new());
628
629    let dur_a = 0.85_f32;
630    let beat_spacing = 0.38_f32;
631
632    let a_dark = [0.18, 0.08, 0.22, 1.0];
633    let a_bright = [0.85, 0.40, 1.00, 1.0];
634
635    for i in 0..16 {
636        let kf_beat = i as f64;
637        let kf = universe
638            .world
639            .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
640
641        let _ = universe.attach(anim, kf);
642
643        // Scheduled audio note (sample-accurate via lookahead).
644        // Pulse every beat.
645        if audio_enabled {
646            let Some(osc_a_comp) = osc_a_comp else {
647                panic!("audio_enabled but track A was not created");
648            };
649
650            let note_a = engine::ecs::component::MusicNote::c(1, dur_a).with_velocity(0.80);
651
652            let act_a = universe
653                .world
654                .add_component(engine::ecs::component::ActionComponent::new(
655                    engine::ecs::IntentValue::AudioSchedulePlay {
656                        component_ids: vec![osc_a_comp],
657                        beat_offset: 0.0,
658                        beat_context: None,
659                        note: Some(note_a),
660                        gain: None,
661                        rate: None,
662                        duration: None,
663                    },
664                ));
665
666            let _ = universe.attach(kf, act_a);
667        }
668
669        // Keyframed band-pass center.
670        // This is an immediate parameter update applied RT-side (no graph rebuild).
671        if audio_enabled {
672            if let Some(bp_a_comp) = bp_a_comp {
673                let t = (i as f32) / 15.0;
674                let center_hz = 10.0 + t * (1000.0 - 10.0);
675
676                let bp_center =
677                    universe
678                        .world
679                        .add_component(engine::ecs::component::ActionComponent::new(
680                            engine::ecs::IntentValue::AudioBandPassSetCenterHz {
681                                component_ids: vec![bp_a_comp],
682                                center_hz,
683                            },
684                        ));
685                let _ = universe.attach(kf, bp_center);
686
687                // Update the BandPass node label in the graph visualization.
688                if let Some(text_id) = bp_graph_label_text {
689                    let label =
690                        universe
691                            .world
692                            .add_component(engine::ecs::component::ActionComponent::new(
693                                engine::ecs::IntentValue::SetText {
694                                    component_ids: vec![text_id],
695                                    text: format!("BandPass center={center_hz:.1}Hz"),
696                                },
697                            ));
698                    let _ = universe.attach(kf, label);
699                }
700            }
701        }
702
703        // Visualization reset per keyframe.
704        let reset_a = universe
705            .world
706            .add_component(engine::ecs::component::ActionComponent::new(
707                engine::ecs::IntentValue::SetColor {
708                    component_ids: vec![track_a_pattern_root],
709                    rgba: a_dark,
710                },
711            ));
712        let _ = universe.attach(kf, reset_a);
713
714        // Pattern cube per step for each track.
715        let x = (kf_beat as f32) * beat_spacing;
716        let cube_a = spawn_op_cube(
717            &mut universe,
718            track_a_pattern_root,
719            (x, track_a_y, lane_pat_z),
720            0.10,
721            a_dark,
722        );
723
724        let bright_a = universe
725            .world
726            .add_component(engine::ecs::component::ActionComponent::new(
727                engine::ecs::IntentValue::SetColor {
728                    component_ids: vec![cube_a],
729                    rgba: a_bright,
730                },
731            ));
732        let _ = universe.attach(kf, bright_a);
733    }
734    universe.add(anim);
735
736    println!("[audio-graph-example] animation created");
737
738    // Keep window open.
739    println!("[audio-graph-example] processing commands...");
740    universe.systems.process_commands(
741        &mut universe.world,
742        &mut universe.visuals,
743        &mut universe.render_assets,
744        &mut universe.command_queue,
745    );
746
747    println!("[audio-graph-example] commands processed; launching window");
748
749    engine::Windowing::run_app(universe).expect("Windowing failed");
750}
Source

pub fn with_state(self, state: AnimationState) -> Self

Examples found in repository?
examples/mesh-factory-example.rs (line 81)
55    fn spawn_labeled_mesh(
56        universe: &mut engine::Universe,
57        x: f32,
58        y: f32,
59        label: &str,
60        mesh: engine::graphics::primitives::CpuMeshHandle,
61        scale: [f32; 3],
62        color: [f32; 4],
63    ) {
64        use engine::ecs::component::{
65            ActionComponent, AnimationComponent, AnimationState, ColorComponent, EmissiveComponent,
66            KeyframeComponent, RenderableComponent, TextComponent, TransformComponent,
67        };
68        use engine::graphics::primitives::{MaterialHandle, Renderable};
69
70        // Mesh.
71        let root = universe.world.add_component(
72            TransformComponent::new()
73                .with_position(x, y, 0.0)
74                .with_scale(scale[0], scale[1], scale[2]),
75        );
76
77        // Spin each shape around its own +Y axis using AnimationComponent + keyframes.
78        // We fill [0, 2) beats densely so it looks smooth.
79        let anim = universe
80            .world
81            .add_component(AnimationComponent::new().with_state(AnimationState::Looping));
82        let _ = universe.attach(root, anim);
83
84        let steps: usize = 64;
85        for i in 0..steps {
86            let beat = (i as f64) * (2.0 / (steps as f64));
87            let kf = universe.world.add_component(KeyframeComponent::new(beat));
88            let _ = universe.attach(anim, kf);
89
90            // Full turn over 2 beats.
91            let angle = (std::f64::consts::TAU * (beat / 2.0)) as f32;
92            let rotation = utils::math::quat_from_axis_angle([0.0, 1.0, 0.0], angle);
93
94            let action_cid = universe.world.add_component(ActionComponent::new(
95                engine::ecs::IntentValue::UpdateTransform {
96                    component_ids: vec![root],
97                    translation: [x, y, 0.0],
98                    rotation_quat_xyzw: rotation,
99                    scale,
100                },
101            ));
102            let _ = universe.attach(kf, action_cid);
103        }
104
105        let renderable = universe
106            .world
107            .add_component(RenderableComponent::new(Renderable::new(
108                mesh,
109                MaterialHandle::TOON_MESH,
110            )));
111        let color_c = universe
112            .world
113            .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
114        let emissive = universe.world.add_component(EmissiveComponent::on());
115
116        let _ = universe.attach(root, renderable);
117        let _ = universe.attach(renderable, color_c);
118        let _ = universe.attach(renderable, emissive);
119
120        universe.add(root);
121
122        // Label (separate transform so we can scale text independently).
123        let text_root = universe.world.add_component(
124            TransformComponent::new()
125                .with_position(x, y + 0.75, 0.05)
126                .with_scale(0.09, 0.09, 1.0),
127        );
128        let text = universe
129            .world
130            .add_component(TextComponent::with_word_wrap_tokens(
131                label,
132                LABEL_WRAP_AT,
133                ["::", "(", ")", ",", "."],
134            ));
135        let text_color = universe
136            .world
137            .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
138        let text_emissive = universe.world.add_component(EmissiveComponent::on());
139        let _ = universe.attach(text_root, text);
140        let _ = universe.attach(text, text_color);
141        let _ = universe.attach(text, text_emissive);
142        universe.add(text_root);
143    }
Source

pub fn with_resolve_targets(self, mode: ResolveTargetsMode) -> Self

Source

pub fn with_length_beats(self, beats: f64) -> Self

Source

pub fn with_scope_source(self, source: ComponentRef) -> Self

Source

pub fn with_playing(self, playing: bool) -> Self

Backward-compatible helper for older callers.

Source

pub fn id(&self) -> Option<ComponentId>

Trait Implementations§

Source§

impl Clone for AnimationComponent

Source§

fn clone(&self) -> AnimationComponent

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Component for AnimationComponent

Source§

fn set_id(&mut self, component: ComponentId)

Source§

fn name(&self) -> &'static str

Short debug/type name for this component kind (e.g. “transform”, “camera”).
Source§

fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)

Called when component is added to the World
Source§

fn as_any(&self) -> &dyn Any

Source§

fn as_any_mut(&mut self) -> &mut dyn Any

Source§

fn to_mms_ast(&self, _world: &World) -> ComponentExpression

Encode this component as an MMS Component Expression AST node. Read more
Source§

fn cleanup(&mut self, _emit: &mut dyn SignalEmitter, _component: ComponentId)

Called when component is removed from the World.
Source§

fn encode(&self) -> HashMap<String, Value>

Encode component data to a HashMap for serialization. Read more
Source§

fn decode(&mut self, _data: &HashMap<String, Value>) -> Result<(), String>

Decode component data from a HashMap after deserialization. Read more
Source§

impl Debug for AnimationComponent

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for AnimationComponent

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<F, T> IntoSample<T> for F
where T: FromSample<F>,

Source§

fn into_sample(self) -> T

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

Source§

fn to_sample_(self) -> U

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more