Skip to content

Animation Tracks

This example shows scene animation tracks driving a cube and camera.

Preview

Run And Adapt

Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.

Route Availability Command or action
C Canonical native source just example-c features/animation_tracks (build and run), or rerun ./build/examples/c/features/animation_tracks
Python Available; direct-engine adaptation python3 -m examples.python.gallery.features.animation_tracks
Browser Live WebGPU route Open live example

This example is approved as a starting point for user code and coding agents. Keep the object lifetimes and data shapes intact while adapting the data and styling.

What To Look For

The cube keeps its mesh geometry and material, while a rotation track updates its visual transform every frame. A closed Catmull-Rom keyframe track moves the camera eye around the cube and a constant track keeps the target at the origin. In the live preview, compare the cube's local spin with the slower camera flyover; user interaction pauses the camera motion so the object can still be inspected. Tracks are useful for repeatable animations, scripted camera paths, and deterministic movie captures.

Source

#!/usr/bin/env python3
"""Scene animation tracks driving a cube transform and camera path."""

from __future__ import annotations

import ctypes
import math

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


ANIMATION_LOOP_DURATION_S = 9.6


class AnimationTracksState:
    def __init__(self) -> None:
        self.rotation = None
        self.camera_eye = None
        self.camera_target = None
        self.visual_animation = None
        self.camera_animation = None

    def destroy_tracks(self) -> None:
        for track in (self.rotation, self.camera_eye, self.camera_target):
            if track:
                dvz.dvz_track_destroy(track)
        self.rotation = None
        self.camera_eye = None
        self.camera_target = None


def _animation_camera_desc():
    camera = dvz.dvz_camera_desc()
    camera.view.eye[:] = (-2.0, 2.0, 2.0)
    camera.view.up[:] = (0.0, 1.0, 0.0)
    camera.projection.fov_y = 0.66
    camera.projection.near_clip = 0.05
    camera.projection.far_clip = 100.0
    return camera


def _add_reference_grid(panel) -> None:
    desc = dvz.dvz_reference_grid_desc()
    desc.plane = dvz.DVZ_REFERENCE_GRID_XZ
    desc.origin[1] = -0.32
    desc.size[:] = (6.0, 6.0)
    desc.spacing = 0.25
    desc.major_every = 4
    desc.minor_color = dvz.DvzColor(74, 86, 98, 95)
    desc.major_color = dvz.DvzColor(116, 132, 148, 145)
    desc.axis_color = dvz.DvzColor(176, 190, 204, 185)
    desc.minor_width_px = 1.0
    desc.major_width_px = 1.5
    desc.axis_width_px = 2.0
    desc.depth_test = True
    grid = dvz.dvz_reference_grid(panel, ctypes.byref(desc))
    if not grid:
        raise RuntimeError("dvz_reference_grid() failed")


def _add_cube_mesh(scene, panel):
    face_colors = (dvz.DvzColor * 6)(ex.CYAN, ex.GREEN, ex.YELLOW, ex.TEXT, ex.BLUE, ex.RED)

    desc = dvz.dvz_geometry_cube_desc()
    desc.size = 0.56
    desc.face_colors = face_colors
    desc.face_color_count = len(face_colors)
    geometry = dvz.dvz_geometry_cube(ctypes.byref(desc))
    if not geometry:
        raise RuntimeError("dvz_geometry_cube() failed")

    mesh = dvz.dvz_mesh(scene, 0)
    if not mesh:
        dvz.dvz_geometry_destroy(geometry)
        raise RuntimeError("dvz_mesh() failed")

    try:
        if dvz.dvz_mesh_set_geometry(mesh, geometry) != 0:
            raise RuntimeError("dvz_mesh_set_geometry() failed")
    finally:
        dvz.dvz_geometry_destroy(geometry)

    material = dvz.dvz_standard_material_desc()
    material.standard.roughness = 0.52
    material.standard.specular = 0.38
    material.standard.rim_strength = 0.18
    if dvz.dvz_visual_set_material(mesh, ctypes.byref(material)) != 0:
        raise RuntimeError("dvz_visual_set_material() failed")
    ex.add_visual(panel, mesh)
    return mesh


def _add_visual_animation(scene, mesh, state: AnimationTracksState) -> None:
    rotation_desc = dvz.dvz_track_rotation_desc()
    rotation_desc.axis[:] = (0.35, 0.85, 0.25)
    rotation_desc.speed_rad_per_sec = -math.tau / ANIMATION_LOOP_DURATION_S
    rotation = dvz.dvz_track_rotation(ctypes.byref(rotation_desc))
    if not rotation:
        raise RuntimeError("dvz_track_rotation() failed")
    state.rotation = rotation

    transform_desc = dvz.dvz_transform_motion_desc()
    transform_desc.rotation = rotation
    animation = dvz.dvz_anim_visual_transform(scene, mesh, ctypes.byref(transform_desc))
    if not animation:
        raise RuntimeError("dvz_anim_visual_transform() failed")
    state.visual_animation = animation
    if dvz.dvz_anim_set_speed(animation, 1.0) != 0:
        raise RuntimeError("dvz_anim_set_speed(visual) failed")
    if dvz.dvz_anim_start(animation, 0.0) != 0:
        raise RuntimeError("dvz_anim_start(visual) failed")


def _add_camera_animation(scene, camera, state: AnimationTracksState) -> None:
    times = np.array([0.0, 1.2, 2.4, 3.6, 4.8], dtype=np.float64)
    eyes = np.array(
        [
            [-2.0, 2.0, +2.0],
            [+2.0, 2.0, +2.0],
            [+2.0, 2.0, -2.0],
            [-2.0, 2.0, -2.0],
            [-2.0, 2.0, +2.0],
        ],
        dtype=np.float32,
    )

    eye_desc = dvz.dvz_track_keyframes_desc()
    eye_desc.type = dvz.DVZ_TRACK_VEC3
    eye_desc.count = len(times)
    eye_desc.times = times.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
    eye_desc.values = eyes.ctypes.data_as(ctypes.c_void_p)
    eye_desc.topology = dvz.DVZ_TRACK_TOPOLOGY_CLOSED
    eye_desc.repeat = dvz.DVZ_TRACK_REPEAT_LOOP
    eye_desc.interpolation = dvz.DVZ_TRACK_INTERP_CATMULL_ROM
    camera_eye = dvz.dvz_track_keyframes(ctypes.byref(eye_desc))
    if not camera_eye:
        raise RuntimeError("dvz_track_keyframes(camera eye) failed")
    state.camera_eye = camera_eye

    target_value = (ctypes.c_float * 3)(0.0, 0.0, 0.0)
    target_desc = dvz.dvz_track_constant_desc()
    target_desc.type = dvz.DVZ_TRACK_VEC3
    target_desc.value = ctypes.cast(target_value, ctypes.c_void_p)
    camera_target = dvz.dvz_track_constant(ctypes.byref(target_desc))
    if not camera_target:
        raise RuntimeError("dvz_track_constant(camera target) failed")
    state.camera_target = camera_target

    camera_motion = dvz.dvz_camera_motion_desc()
    camera_motion.eye = camera_eye
    camera_motion.target = camera_target
    camera_motion.up_mode = dvz.DVZ_CAMERA_UP_WORLD
    camera_motion.up[:] = (0.0, 1.0, 0.0)
    animation = dvz.dvz_anim_camera_motion(scene, camera, ctypes.byref(camera_motion))
    if not animation:
        raise RuntimeError("dvz_anim_camera_motion() failed")
    state.camera_animation = animation
    if dvz.dvz_anim_set_speed(animation, 0.5) != 0:
        raise RuntimeError("dvz_anim_set_speed(camera) failed")
    if dvz.dvz_anim_start(animation, 0.0) != 0:
        raise RuntimeError("dvz_anim_start(camera) failed")


def _build_scene():
    scene, figure, panel = ex.scene_panel()
    state = AnimationTracksState()

    camera_desc = _animation_camera_desc()
    if dvz.dvz_panel_set_camera_desc(panel, ctypes.byref(camera_desc)) != 0:
        raise RuntimeError("dvz_panel_set_camera_desc() failed")
    camera = dvz.dvz_panel_camera(panel)
    if not camera:
        raise RuntimeError("dvz_panel_camera() failed")

    _add_reference_grid(panel)
    mesh = _add_cube_mesh(scene, panel)
    _add_visual_animation(scene, mesh, state)
    _add_camera_animation(scene, camera, state)
    return scene, figure, panel, state


def _configure_view(view, scene, panel, state: AnimationTracksState) -> None:
    controller = dvz.dvz_turntable(scene, None)
    if not controller:
        raise RuntimeError("dvz_turntable() failed")
    if dvz.dvz_view_bind_controller(view, panel, controller, dvz.DVZ_DIM_MASK_XYZ) != 0:
        raise RuntimeError("dvz_view_bind_controller() failed")
    if (
        dvz.dvz_anim_set_interaction_policy(
            state.camera_animation,
            controller,
            dvz.DVZ_ANIM_INTERACTION_PAUSE,
            0.0,
        )
        != 0
    ):
        raise RuntimeError("dvz_anim_set_interaction_policy() failed")


def _run(scene, figure, panel, state: AnimationTracksState) -> None:
    app = dvz.dvz_app(scene)
    if not app:
        dvz.dvz_scene_destroy(scene)
        state.destroy_tracks()
        raise RuntimeError("dvz_app() failed")
    try:
        view = dvz.dvz_view_window(app, figure, ex.WIDTH, ex.HEIGHT, b"Animation Tracks")
        if not view:
            raise RuntimeError("dvz_view_window() failed")
        _configure_view(view, scene, panel, state)
        ex.run_app(app, view)
    finally:
        dvz.dvz_app_destroy(app)
        dvz.dvz_scene_destroy(scene)
        state.destroy_tracks()


def main() -> None:
    scene, figure, panel, state = _build_scene()
    _run(scene, figure, panel, state)


if __name__ == "__main__":
    main()
/*
 * Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
 * Licensed under the MIT license. See LICENSE file in the project root for details.
 * SPDX-License-Identifier: MIT
 */

/* animation_tracks - This example shows scene animation tracks driving a cube and camera.
 *
 * Scenario: features_animation_tracks
 * Style: features, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c features/animation_tracks
 * Run:    ./build/examples/c/features/animation_tracks --live
 * Smoke:  ./build/examples/c/features/animation_tracks --png
 *
 * What to look for: the cube keeps its mesh geometry and material, while a rotation track updates
 * its visual transform every frame. A closed Catmull-Rom keyframe track moves the camera eye
 * around the cube and a constant track keeps the target at the origin. In the live preview, compare
 * the cube's local spin with the slower camera flyover; user interaction pauses the camera motion
 * so the object can still be inspected. Tracks are useful for repeatable animations, scripted
 * camera paths, and deterministic movie captures.
 */



/*************************************************************************************************/
/*  Includes                                                                                     */
/*************************************************************************************************/

#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

#include "_alloc.h"
#include "datoviz/geom.h"
#include "datoviz/scene.h"
#include "example_common.h"
#include "example_style.h"
#include "runner/scenario_runner.h"


DvzScenarioSpec dvz_example_animation_tracks_scenario(void);



/*************************************************************************************************/
/*  Constants                                                                                    */
/*************************************************************************************************/

#define WIDTH  EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT

static const float TAU = 6.28318530718f;
static const float ANIMATION_LOOP_DURATION_S = 9.6f;



/*************************************************************************************************/
/*  Structs                                                                                      */
/*************************************************************************************************/

typedef struct AnimationTracksState
{
    DvzGeometry* geometry;
    DvzTrack* rotation;
    DvzTrack* camera_eye;
    DvzTrack* camera_target;
    DvzAnimation* visual_animation;
    DvzAnimation* camera_animation;
} AnimationTracksState;



/*************************************************************************************************/
/*  Helpers                                                                                      */
/*************************************************************************************************/

/**
 * Create a mesh visual backed by one graphite-cyan colored cube geometry.
 *
 * @param scene scene owning the visual
 * @param size cube edge length
 * @param face_roles six graphite-cyan face color roles
 * @param out_geometry geometry handle for cleanup
 * @return uploaded mesh visual, or NULL on error
 */
static DvzVisual* _graphite_cyan_cube_mesh(
    DvzScene* scene, double size, const ExampleStyleColorRole face_roles[6],
    DvzGeometry** out_geometry)
{
    if (out_geometry != NULL)
        *out_geometry = NULL;
    if (scene == NULL || face_roles == NULL)
        return NULL;

    DvzVisual* visual = dvz_mesh(scene, 0);
    if (visual == NULL)
        return NULL;

    DvzColor face_colors[DVZ_GEOM_CUBE_FACE_COUNT] = {0};
    for (uint32_t i = 0; i < DVZ_GEOM_CUBE_FACE_COUNT; i++)
        face_colors[i] = example_graphite_cyan_color(face_roles[i]);

    DvzGeometry* cube = dvz_geometry_cube(&(DvzGeometryCubeDesc){
        DVZ_STRUCT_INIT_FIELDS(DvzGeometryCubeDesc),
        .size = size,
        .face_colors = face_colors,
        .face_color_count = DVZ_GEOM_CUBE_FACE_COUNT,
    });
    if (cube == NULL)
        return NULL;
    if (out_geometry != NULL)
        *out_geometry = cube;

    if (dvz_mesh_set_geometry(visual, cube) != 0)
        return NULL;
    return visual;
}



/**
 * Create one cube whose local transform is driven by retained scene tracks.
 *
 * @param ctx scenario context
 * @param panel target panel
 * @param state scenario state
 * @return true on success
 */
static bool
_add_animated_cube(DvzScenarioContext* ctx, DvzPanel* panel, AnimationTracksState* state)
{
    const ExampleStyleColorRole face_roles[6] = {
        EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY, EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY,
        EXAMPLE_STYLE_COLOR_WARNING,        EXAMPLE_STYLE_COLOR_TEXT,
        EXAMPLE_STYLE_COLOR_GRID,           EXAMPLE_STYLE_COLOR_MINOR_TICK,
    };
    DvzVisual* cube = _graphite_cyan_cube_mesh(ctx->scene, 0.56, face_roles, &state->geometry);
    if (cube == NULL)
        return false;

    DvzMaterialDesc material = example_default_standard_material_desc();
    if (dvz_visual_set_material(cube, &material) != 0)
        return false;
    if (dvz_panel_add_visual(panel, cube, NULL) != 0)
        return false;

    DvzTrackRotationDesc rotation_desc = dvz_track_rotation_desc();
    rotation_desc.axis[0] = 0.35f;
    rotation_desc.axis[1] = 0.85f;
    rotation_desc.axis[2] = 0.25f;
    rotation_desc.speed_rad_per_sec = -TAU / ANIMATION_LOOP_DURATION_S;
    state->rotation = dvz_track_rotation(&rotation_desc);
    if (state->rotation == NULL)
        return false;

    DvzTransformMotionDesc transform_desc = dvz_transform_motion_desc();
    transform_desc.rotation = state->rotation;
    state->visual_animation = dvz_anim_visual_transform(ctx->scene, cube, &transform_desc);
    if (state->visual_animation == NULL)
        return false;

    dvz_anim_set_speed(state->visual_animation, 1.0f);
    dvz_anim_start(state->visual_animation, 0.0);
    return true;
}



/**
 * Create a keyframed camera flyover that keeps looking at the cube.
 *
 * @param ctx scenario context
 * @param camera panel-owned camera
 * @param state scenario state
 * @return true on success
 */
static bool
_add_camera_hover(DvzScenarioContext* ctx, DvzCamera* camera, AnimationTracksState* state)
{
    if (ctx == NULL || camera == NULL || state == NULL)
        return false;

    static const double times[] = {0.0, 1.2, 2.4, 3.6, 4.8};
    static const vec3 eyes[] = {
        {-2, 2, +2}, //
        {+2, 2, +2}, //
        {+2, 2, -2}, //
        {-2, 2, -2}, //
        {-2, 2, +2},
    };

    DvzTrackKeyframesDesc eye_desc = dvz_track_keyframes_desc();
    eye_desc.type = DVZ_TRACK_VEC3;
    eye_desc.count = DVZ_ARRAY_COUNT(times);
    eye_desc.times = times;
    eye_desc.values = eyes;
    eye_desc.topology = DVZ_TRACK_TOPOLOGY_CLOSED;
    eye_desc.repeat = DVZ_TRACK_REPEAT_LOOP;
    eye_desc.interpolation = DVZ_TRACK_INTERP_CATMULL_ROM;
    state->camera_eye = dvz_track_keyframes(&eye_desc);
    if (state->camera_eye == NULL)
        return false;

    DvzTrackConstantDesc target_desc = dvz_track_constant_desc();
    target_desc.type = DVZ_TRACK_VEC3;
    target_desc.value = (float[3]){0.0f, 0.0f, 0.0f};
    state->camera_target = dvz_track_constant(&target_desc);
    if (state->camera_target == NULL)
        return false;

    DvzCameraMotionDesc camera_motion = dvz_camera_motion_desc();
    camera_motion.eye = state->camera_eye;
    camera_motion.target = state->camera_target;
    camera_motion.up_mode = DVZ_CAMERA_UP_WORLD;
    camera_motion.up[0] = 0.0f;
    camera_motion.up[1] = 1.0f;
    camera_motion.up[2] = 0.0f;
    state->camera_animation = dvz_anim_camera_motion(ctx->scene, camera, &camera_motion);
    if (state->camera_animation == NULL)
        return false;

    dvz_anim_set_speed(state->camera_animation, 0.5f);
    dvz_anim_start(state->camera_animation, 0.0);
    return true;
}



/*************************************************************************************************/
/*  Scenario callbacks                                                                           */
/*************************************************************************************************/

/**
 * Initialize the animation-tracks feature scenario.
 *
 * @param ctx scenario context
 * @param out_user scenario state output
 * @return true on success
 */
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
    if (ctx == NULL || out_user == NULL)
        return false;
    *out_user = NULL;

    AnimationTracksState* state = (AnimationTracksState*)dvz_calloc(1, sizeof(AnimationTracksState));
    if (state == NULL)
        return false;
    *out_user = state;

    ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
    if (ctx->figure == NULL)
        return false;

    DvzPanel* panel = dvz_panel_full(ctx->figure);
    if (panel == NULL)
        return false;
    example_graphite_cyan_set_panel_background(panel);

    DvzCameraDesc camera = dvz_camera_desc();
    camera.view.eye[0] = -2;
    camera.view.eye[1] = 2;
    camera.view.eye[2] = +2;
    camera.view.up[1] = 1.0f;
    camera.view.up[2] = 0.0f;
    camera.projection.fov_y = 0.66f;
    camera.projection.near_clip = 0.05f;
    camera.projection.far_clip = 100.0f;
    if (dvz_panel_set_camera_desc(panel, &camera) != 0)
        return false;
    DvzCamera* panel_camera = dvz_panel_camera(panel);
    if (panel_camera == NULL)
        return false;

    DvzReferenceGridDesc grid = dvz_reference_grid_desc();
    grid.plane = DVZ_REFERENCE_GRID_XZ;
    grid.origin[1] = -0.32f;
    grid.size[0] = EXAMPLE_XZ_REFERENCE_GRID_SIZE;
    grid.size[1] = EXAMPLE_XZ_REFERENCE_GRID_SIZE;
    grid.spacing = EXAMPLE_XZ_REFERENCE_GRID_SPACING;
    grid.major_every = EXAMPLE_XZ_REFERENCE_GRID_MAJOR_EVERY;
    grid.minor_color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_GRID);
    grid.minor_color.a = EXAMPLE_XZ_REFERENCE_GRID_MINOR_ALPHA;
    grid.major_color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_MINOR_TICK);
    grid.major_color.a = EXAMPLE_XZ_REFERENCE_GRID_MAJOR_ALPHA;
    grid.axis_color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_TEXT);
    grid.axis_color.a = EXAMPLE_XZ_REFERENCE_GRID_AXIS_ALPHA;
    grid.minor_width_px = EXAMPLE_XZ_REFERENCE_GRID_MINOR_WIDTH;
    grid.major_width_px = EXAMPLE_XZ_REFERENCE_GRID_MAJOR_WIDTH;
    grid.axis_width_px = EXAMPLE_XZ_REFERENCE_GRID_AXIS_WIDTH;
    grid.depth_test = true;
    if (dvz_reference_grid(panel, &grid) == NULL)
        return false;
    if (!_add_animated_cube(ctx, panel, state))
        return false;
    if (!_add_camera_hover(ctx, panel_camera, state))
        return false;

    DvzController* controller = dvz_turntable(ctx->scene, NULL);
    if (controller == NULL)
        return false;
    if (dvz_scenario_bind_controller(ctx, panel, controller, DVZ_DIM_MASK_XYZ) != 0)
        return false;
    dvz_anim_set_interaction_policy(
        state->camera_animation, controller, DVZ_ANIM_INTERACTION_PAUSE, 0);
    return true;
}



/**
 * Destroy the animation-tracks feature state.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
    (void)ctx;
    AnimationTracksState* state = (AnimationTracksState*)user;
    if (state == NULL)
        return;

    dvz_track_destroy(state->rotation);
    dvz_track_destroy(state->camera_eye);
    dvz_track_destroy(state->camera_target);
    if (state->geometry != NULL)
        dvz_geometry_destroy(state->geometry);
    dvz_free(state);
}



/**
 * Return the animation-tracks scenario specification.
 *
 * @return scenario specification
 */
DvzScenarioSpec dvz_example_animation_tracks_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "features_animation_tracks",
        .title = "Animation Tracks",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .requirements = DVZ_SCENARIO_REQ_MESH_VISUAL | DVZ_SCENARIO_REQ_FRAME_CALLBACKS |
                        DVZ_SCENARIO_REQ_CONTROLLER,
        .init = _scenario_init,
        .destroy = _scenario_destroy,
    };
}



/*************************************************************************************************/
/*  Functions                                                                                    */
/*************************************************************************************************/

#ifndef DVZ_EXAMPLE_NO_MAIN
/**
 * Run the animation-tracks feature example through the native scenario runner.
 *
 * @param argc command-line argument count
 * @param argv command-line argument vector
 * @return process exit code
 */
int main(int argc, char** argv)
{
    DvzScenarioSpec spec = dvz_example_animation_tracks_scenario();
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details

Data

Field Value
kind synthetic