---
title: "Componente AnimControls — zenit Zig UI"
description: "Control de animaciones: yoyo, líneas de tiempo, keyframes y muelles. Reproducir, invertir, reiniciar y buscar actúan sobre una única máquina de estados de…"
url: https://zenit.z.express/es/components/animctl
language: es
alternate_en: https://zenit.z.express/components/animctl.md
alternate_zh: https://zenit.z.express/zh/components/animctl.md
alternate_ja: https://zenit.z.express/ja/components/animctl.md
alternate_ko: https://zenit.z.express/ko/components/animctl.md
alternate_fr: https://zenit.z.express/fr/components/animctl.md
alternate_de: https://zenit.z.express/de/components/animctl.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# AnimControls

`ui.fx`

Control de animaciones: yoyo, líneas de tiempo, keyframes y muelles.

[Video](https://zenit.z.express/media/stories/animctl.mp4?v=6e61433c8b)

**BEHAVIOR CONTRACT**

Reproducir, invertir, reiniciar y buscar actúan sobre una única máquina de estados de animación.

**RECORDED INTERACTION**

El harness reproduce el yoyo y la línea de tiempo.

`examples/storybook/stories.zig:4883`

```zig
pub fn buildAnimCtl(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
    const a = cx.allocator;
    const c = try col(cx, 12);
    try c.appendChild(a, try label(cx, "Animation controls — yoyo replay / timeline reverse / keyframes catch-up / degenerate spring"));

    // yoyo tween：20 → 120 → 20（loops=2 + yoyo），完成后停在 from。
    const yoyo_ctrl = try a.create(ui.fx.AnimationController);
    yoyo_ctrl.* = ui.fx.AnimationController.initTween(.{ .from = 20, .to = 120, .duration = 0.7, .easing = .linear });
    yoyo_ctrl.yoyo = true;
    yoyo_ctrl.loops = 2;
    try scope.registerResource(@ptrCast(yoyo_ctrl), destroyAnimController);

    // timeline：单条 0→100 线性 2s。
    const tl_ctrl = try a.create(ui.fx.AnimationController);
    tl_ctrl.* = ui.fx.AnimationController.initTween(.{ .from = 0, .to = 100, .duration = 2.0, .easing = .linear });
    try scope.registerResource(@ptrCast(tl_ctrl), destroyAnimController);
    const tl = try a.create(ui.fx.Timeline);
    tl.* = ui.fx.Timeline.init(a);
    try scope.registerResource(@ptrCast(tl), destroyAnimTimeline);
    try tl.add(tl_ctrl, ui.fx.Position.start);

    // 退化 spring：负 stiffness（修复后 precompute 三参全钳，不产 NaN）。
    const spring_ctrl = try a.create(ui.fx.AnimationController);
    spring_ctrl.* = ui.fx.AnimationController.initSpring(.{ .from = 0, .to = 100, .stiffness = -50, .damping = 10, .mass = 1 });
    try scope.registerResource(@ptrCast(spring_ctrl), destroyAnimController);

    // 可视化目标（值断言走 label；箱体只是肉眼观察辅助）
    const arena = try ui.box(cx, .{
        .width = .{ .px = 560 },
        .height = .{ .px = 60 },
        .background = light.color.bg_secondary,
    }, .{});
    const yoyo_box = try ui.box(cx, .{ .width = .{ .px = 28 }, .height = .{ .px = 28 }, .background = light.color.accent }, .{});
    yoyo_box.meta.ownership.meta.test_id = "story.animctl.yoyobox";
    try arena.appendChild(a, yoyo_box);
    const spring_box = try ui.box(cx, .{ .width = .{ .px = 28 }, .height = .{ .px = 28 }, .background = light.color.border }, .{});
    spring_box.meta.ownership.meta.test_id = "story.animctl.springbox";
    try arena.appendChild(a, spring_box);
    try c.appendChild(a, arena);

    const yoyo_label = try ui.text(cx, "yoyo: state=idle value=20.0 replay_first=-1.0", .{ .font_size = 13, .color = light.color.fg_primary });
    yoyo_label.meta.ownership.meta.test_id = "story.animctl.yoyo";
    const tl_label = try ui.text(cx, "tl: state=idle t=0.00 v=0.0", .{ .font_size = 13, .color = light.color.fg_primary });
    tl_label.meta.ownership.meta.test_id = "story.animctl.tl";
    const kf_label = try ui.text(cx, "kf: loops=0 value=0.0 completed=false", .{ .font_size = 13, .color = light.color.fg_primary });
    kf_label.meta.ownership.meta.test_id = "story.animctl.kf";
    const spring_label = try ui.text(cx, "spring: ticks=0 value=0.0", .{ .font_size = 13, .color = light.color.fg_primary });
    spring_label.meta.ownership.meta.test_id = "story.animctl.spring";
    try c.appendChild(a, yoyo_label);
    try c.appendChild(a, tl_label);
    try c.appendChild(a, kf_label);
    try c.appendChild(a, spring_label);

    const st = try cx.bindState(AnimCtlStory, .{
        .cx = cx,
        .yoyo_ctrl = yoyo_ctrl,
        .tl = tl,
        .tl_ctrl = tl_ctrl,
        .spring_ctrl = spring_ctrl,
        .kf = ui.fx.KeyframeAnimation.init(&KF_FRAMES, 400, 4),
        .yoyo_box = yoyo_box,
        .spring_box = spring_box,
        .yoyo_label = yoyo_label,
        .tl_label = tl_label,
        .kf_label = kf_label,
        .spring_label = spring_label,
    });

    const btn_row = try row(cx, 8);
    const yoyo_btn = try W.Button(.{ .label = "Play yoyo", .size = .sm, .on_click = cx.on(AnimCtlStory, st, AnimCtlStory.playYoyo) }).mount(scope, cx);
    yoyo_btn.meta.ownership.meta.test_id = "story.animctl.yoyo_play";
    try btn_row.appendChild(a, yoyo_btn);
    const tl_play_btn = try W.Button(.{ .label = "Play timeline", .size = .sm, .on_click = cx.on(AnimCtlStory, st, AnimCtlStory.playTimeline) }).mount(scope, cx);
    tl_play_btn.meta.ownership.meta.test_id = "story.animctl.tl_play";
    try btn_row.appendChild(a, tl_play_btn);
    const tl_rev_btn = try W.Button(.{ .label = "Reverse timeline", .size = .sm, .on_click = cx.on(AnimCtlStory, st, AnimCtlStory.reverseTimeline) }).mount(scope, cx);
    tl_rev_btn.meta.ownership.meta.test_id = "story.animctl.tl_reverse";
    try btn_row.appendChild(a, tl_rev_btn);
    const kf_reset_btn = try W.Button(.{ .label = "KF reset", .size = .sm, .on_click = cx.on(AnimCtlStory, st, AnimCtlStory.kfReset) }).mount(scope, cx);
    kf_reset_btn.meta.ownership.meta.test_id = "story.animctl.kf_reset";
    try btn_row.appendChild(a, kf_reset_btn);
    const kf_step_btn = try W.Button(.{ .label = "KF +1s", .size = .sm, .on_click = cx.on(AnimCtlStory, st, AnimCtlStory.kfAdvance) }).mount(scope, cx);
    kf_step_btn.meta.ownership.meta.test_id = "story.animctl.kf_step";
    try btn_row.appendChild(a, kf_step_btn);
    try c.appendChild(a, btn_row);

    c.meta.per_frame.hooks.slots.anim_state = @ptrCast(st);
    c.meta.per_frame.hooks.before_render.main = AnimCtlStory.tickHook;

    // spring 自动起播（mount 即驱动，e2e 只读回值）
    spring_ctrl.play();
    cx.needs_redraw = true;
    return c;
}
```

[Ver el código en GitHubexamples/storybook/stories.zig:4883](https://github.com/version-next/zenit/blob/HEAD/examples/storybook/stories.zig#L4883)

El código literal de la story en el Storybook (zenit 5f9add5+wip 2026-09-30). `W` es `ui.widgets`; `col` / `row` / `label` son pequeños helpers de layout del Storybook.
