← ziglang.inraylib-zigGitHub

animation blending

★★★★by Kirandeep (@Kirandeep-Singh-Khehra)

There is no browser build of this example yet.

Is it the same as C?

//! raylib [models] example - animation blending
//!
//! Zig-API translation of examples/models/models_animation_blending.c (raylib 6.2).
//! Example contributed by Kirandeep (@Kirandeep-Singh-Khehra) and reviewed by Ramon Santamaria (@raysan5)
//! WARNING: GPU skinning must be enabled in raylib with a compilation flag,
//! if not enabled, CPU skinning will be used instead
//! Example licensed under an unmodified zlib/libpng license.
//! Copyright (c) 2024-2026 Kirandeep (@Kirandeep-Singh-Khehra) and Ramon Santamaria (@raysan5)

const std = @import("std");
const rl = @import("raylib");
const gui = @import("raygui");

/// The animation's name, as the NUL-terminated string raygui lists.
fn animName(anim: *const rl.ModelAnimation) [*:0]const u8 {
    return anim.name[0..std.mem.indexOfScalar(u8, &anim.name, 0).? :0];
}

/// Advance `frame` by `speed`, looping over the animation's keyframes.
fn advanceFrame(frame: *f32, speed: f32, anim: rl.ModelAnimation) void {
    frame.* += speed;
    if (frame.* >= @as(f32, @floatFromInt(anim.keyframeCount))) frame.* = 0.0;
}

/// Draw a playing timeline with a tick per keyframe of `anim`.
fn drawTimeline(y: f32, label: [:0]const u8, progress: *f32, anim: rl.ModelAnimation) void {
    const screen_width = rl.getScreenWidth();
    const keyframe_count: f32 = @floatFromInt(anim.keyframeCount);

    _ = gui.progressBar(
        .{ .x = 60, .y = y, .width = @as(f32, @floatFromInt(screen_width)) - 180.0, .height = 20 },
        label,
        rl.textFormat("FRAME: {d:.2} / {d}", .{ progress.*, anim.keyframeCount }),
        progress,
        0.0,
        keyframe_count,
    );
    for (0..@intCast(anim.keyframeCount)) |i| {
        const tick = (@as(f32, @floatFromInt(screen_width - 180)) / keyframe_count) * @as(f32, @floatFromInt(i));
        rl.drawRectangle(60 + @as(i32, @intFromFloat(tick)), @intFromFloat(y), 1, 20, .blue);
    }
}

pub fn main() !void {
    // Initialization
    const screen_width = 800;
    const screen_height = 450;

    rl.initWindow(screen_width, screen_height, "raylib [models] example - animation blending");
    defer rl.closeWindow(); // Close window and OpenGL context

    // Define the camera to look into our 3d world
    var camera: rl.Camera = .{
        .position = .{ .x = 6.0, .y = 6.0, .z = 6.0 }, // Camera position
        .target = .{ .x = 0.0, .y = 2.0, .z = 0.0 }, // Camera looking at point
        .up = .{ .x = 0.0, .y = 1.0, .z = 0.0 }, // Camera up vector (rotation towards target)
        .fovy = 45.0, // Camera field-of-view Y
        .projection = .perspective, // Camera projection type
    };

    // Load model
    const model = try rl.loadModel("resources/models/gltf/robot.glb"); // Load character model
    defer rl.unloadModel(model); // Unload model and meshes/material
    const position: rl.Vector3 = .{ .x = 0.0, .y = 0.0, .z = 0.0 }; // Set model world position

    // NOTE: The C example loads a GPU skinning shader only `#if SUPPORT_GPU_SKINNING`,
    // a raylib config.h flag that is not visible to programs, so it is never loaded.

    // Load model animations
    const anims = try rl.loadModelAnimations("resources/models/gltf/robot.glb");
    defer rl.unloadModelAnimations(anims); // Unload model animation

    // Animation playing variables
    // NOTE: Two animations are played with a smooth transition between them
    var current_anim_playing: u1 = 0; // Current animation playing (0 o 1)
    var next_anim_to_play: u1 = 1; // Next animation to play (to transition)
    var anim_transition = false; // Flag to register anim transition state

    var anim_index0: i32 = 10; // Current animation playing (walking)
    var anim_current_frame0: f32 = 0.0; // Current animation frame (supporting interpolated frames)
    var anim_frame_speed0: f32 = 0.5; // Current animation play speed
    var anim_index1: i32 = 6; // Next animation to play (running)
    var anim_current_frame1: f32 = 0.0; // Next animation frame (supporting interpolated frames)
    var anim_frame_speed1: f32 = 0.5; // Next animation play speed

    var anim_blend_factor: f32 = 0.0; // Blend factor from anim0[frame0] --> anim1[frame1], [0.0f..1.0f]
    // NOTE: 0.0f results in full anim0[] and 1.0f in full anim1[]

    const anim_blend_time: f32 = 2.0; // Time to blend from one playing animation to another (in seconds)
    var anim_blend_time_counter: f32 = 0.0; // Time counter (delta time)

    var anim_pause = false; // Pause animation

    // UI required variables
    var anim_names_buffer: [64][*:0]const u8 = undefined; // Pointers to animation names for dropdown box
    const anim_names = anim_names_buffer[0..anims.len];
    for (anim_names, anims) |*name, *anim| name.* = animName(anim);

    var dropdown_edit_mode0 = false;
    var dropdown_edit_mode1 = false;
    var anim_frame_progress0: f32 = 0.0;
    var anim_frame_progress1: f32 = 0.0;
    var anim_blend_progress: f32 = 0.0;

    rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second

    // Main game loop
    while (!rl.windowShouldClose()) { // Detect window close button or ESC key
        // Update
        rl.updateCamera(&camera, .orbital);

        if (rl.isKeyPressed(.p)) anim_pause = !anim_pause;

        if (!anim_pause and anims.len > 0) { // Animations could fail to load
            const anim0 = anims[@intCast(anim_index0)];
            const anim1 = anims[@intCast(anim_index1)];

            // Start transition from anim0[] to anim1[]
            if (rl.isKeyPressed(.space) and !anim_transition) {
                if (current_anim_playing == 0) {
                    // Transition anim0 --> anim1
                    next_anim_to_play = 1;
                    anim_current_frame1 = 0.0;
                } else {
                    // Transition anim1 --> anim0
                    next_anim_to_play = 0;
                    anim_current_frame0 = 0.0;
                }

                // Set animation transition
                anim_transition = true;
                anim_blend_time_counter = 0.0;
                anim_blend_factor = 0.0;
            }

            if (anim_transition) {
                // Playing anim0 and anim1 at the same time
                advanceFrame(&anim_current_frame0, anim_frame_speed0, anim0);
                advanceFrame(&anim_current_frame1, anim_frame_speed1, anim1);

                // Increment blend factor over time to transition from anim0 --> anim1 over time
                // NOTE: Time blending could be other than linear, using some easing
                anim_blend_factor = anim_blend_time_counter / anim_blend_time;
                anim_blend_time_counter += rl.getFrameTime();
                anim_blend_progress = anim_blend_factor;

                // Update model with animations blending
                switch (next_anim_to_play) {
                    // Blend anim0 --> anim1
                    1 => rl.updateModelAnimationEx(model, anim0, anim_current_frame0, anim1, anim_current_frame1, anim_blend_factor),
                    // Blend anim1 --> anim0
                    0 => rl.updateModelAnimationEx(model, anim1, anim_current_frame1, anim0, anim_current_frame0, anim_blend_factor),
                }

                // Check if transition completed
                if (anim_blend_factor > 1.0) {
                    // Reset frame states
                    switch (current_anim_playing) {
                        0 => anim_current_frame0 = 0.0,
                        1 => anim_current_frame1 = 0.0,
                    }
                    current_anim_playing = next_anim_to_play; // Update current animation playing

                    anim_blend_factor = 0.0; // Reset blend factor
                    anim_transition = false; // Exit transition mode
                    anim_blend_time_counter = 0.0;
                }
            } else {
                // Play only one anim, the current one, at its defined speed
                switch (current_anim_playing) {
                    0 => {
                        advanceFrame(&anim_current_frame0, anim_frame_speed0, anim0);
                        rl.updateModelAnimation(model, anim0, anim_current_frame0);
                    },
                    1 => {
                        advanceFrame(&anim_current_frame1, anim_frame_speed1, anim1);
                        rl.updateModelAnimation(model, anim1, anim_current_frame1);
                    },
                }
            }
        }

        // Update progress bars values with current frame for each animation
        anim_frame_progress0 = anim_current_frame0;
        anim_frame_progress1 = anim_current_frame1;

        // Draw
        rl.beginDrawing();
        defer rl.endDrawing();

        rl.clearBackground(.ray_white);

        {
            rl.beginMode3D(camera);
            defer rl.endMode3D();

            rl.drawModel(model, position, 1.0, .white); // Draw animated model

            rl.drawGrid(10, 1.0);
        }

        if (anim_transition) rl.drawText("ANIM TRANSITION BLENDING!", 170, 50, 30, .blue);

        const screen_w: f32 = @floatFromInt(rl.getScreenWidth());
        const screen_h: f32 = @floatFromInt(rl.getScreenHeight());

        // Draw UI elements
        if (dropdown_edit_mode0) gui.disable();
        _ = gui.slider(.{ .x = 10, .y = 38, .width = 160, .height = 12 }, null, rl.textFormat("x{d:.1}", .{anim_frame_speed0}), &anim_frame_speed0, 0.1, 2.0);
        gui.enable();
        if (dropdown_edit_mode1) gui.disable();
        _ = gui.slider(.{ .x = screen_w - 170.0, .y = 38, .width = 160, .height = 12 }, rl.textFormat("{d:.1}x", .{anim_frame_speed1}), null, &anim_frame_speed1, 0.1, 2.0);
        gui.enable();

        // Draw animation selectors for blending transition
        // NOTE: Transition does not start until requested
        gui.setStyle(.dropdownbox, gui.DropdownBoxProperty.dropdown_items_spacing, 1);
        if (gui.dropdownBox(.{ .x = 10, .y = 10, .width = 160, .height = 24 }, rl.textJoin(anim_names, ";"), &anim_index0, dropdown_edit_mode0) != 0) dropdown_edit_mode0 = !dropdown_edit_mode0;

        // Blending process progress bar
        const side: i32 = switch (next_anim_to_play) {
            1 => 0, // Left-->Right
            0 => 1, // Right-->Left
        };
        gui.setStyle(.progressbar, gui.ProgressBarProperty.side, side);
        _ = gui.progressBar(.{ .x = 180, .y = 14, .width = 440, .height = 16 }, null, null, &anim_blend_progress, 0.0, 1.0);
        gui.setStyle(.progressbar, gui.ProgressBarProperty.side, 0); // Reset to Left-->Right

        if (gui.dropdownBox(.{ .x = screen_w - 170.0, .y = 10, .width = 160, .height = 24 }, rl.textJoin(anim_names, ";"), &anim_index1, dropdown_edit_mode1) != 0) dropdown_edit_mode1 = !dropdown_edit_mode1;

        gui.setStyle(.label, gui.ControlProperty.text_alignment, gui.TextAlignment.center);
        gui.setStyle(.default, gui.DefaultProperty.text_size, gui.getFont().baseSize * 2);
        _ = gui.label(.{ .x = 0, .y = screen_h - 100.0, .width = screen_w, .height = 40 }, "PRESS SPACE to START BLENDING");
        gui.setStyle(.default, gui.DefaultProperty.text_size, gui.getFont().baseSize);
        gui.setStyle(.label, gui.ControlProperty.text_alignment, gui.TextAlignment.left);

        if (anims.len > 0) {
            // Draw playing timeline with keyframes for anim0[]
            drawTimeline(screen_h - 60.0, "ANIM 0", &anim_frame_progress0, anims[@intCast(anim_index0)]);

            // Draw playing timeline with keyframes for anim1[]
            drawTimeline(screen_h - 30.0, "ANIM 1", &anim_frame_progress1, anims[@intCast(anim_index1)]);
        }
    }
}
/*******************************************************************************************
*
*   raylib [models] example - animation blending
*
*   Example complexity rating: [★★★★] 4/4
*
*   Example originally created with raylib 5.5, last time updated with raylib 6.0
*
*   Example contributed by Kirandeep (@Kirandeep-Singh-Khehra) and reviewed by Ramon Santamaria (@raysan5)
*
*   WARNING: GPU skinning must be enabled in raylib with a compilation flag,
*   if not enabled, CPU skinning will be used instead
*
*   Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
*   BSD-like license that allows static linking with closed source software
*
*   Copyright (c) 2024-2026 Kirandeep (@Kirandeep-Singh-Khehra) and Ramon Santamaria (@raysan5)
*
********************************************************************************************/

#include "raylib.h"

#define RAYGUI_IMPLEMENTATION
#include "raygui.h"             // Required for: UI controls

#if defined(PLATFORM_DESKTOP)
    #define GLSL_VERSION            330
#else   // PLATFORM_ANDROID, PLATFORM_WEB
    #define GLSL_VERSION            100
#endif

//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
    // Initialization
    //--------------------------------------------------------------------------------------
    const int screenWidth = 800;
    const int screenHeight = 450;

    InitWindow(screenWidth, screenHeight, "raylib [models] example - animation blending");

    // Define the camera to look into our 3d world
    Camera camera = { 0 };
    camera.position = (Vector3){ 6.0f, 6.0f, 6.0f }; // Camera position
    camera.target = (Vector3){ 0.0f, 2.0f, 0.0f };  // Camera looking at point
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };      // Camera up vector (rotation towards target)
    camera.fovy = 45.0f;                            // Camera field-of-view Y
    camera.projection = CAMERA_PERSPECTIVE;         // Camera projection type

    // Load model
    Model model = LoadModel("resources/models/gltf/robot.glb"); // Load character model
    Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model world position

#if SUPPORT_GPU_SKINNING
    // WARNING: SUPPORT_GPU_SKINNING is required to be enabled at raylib compile time (disabled by default)
    // It will skip loading CPU required buffers to store animation updated data, so, both modes are exclusive
     
    // Load skinning shader
    // NOTE: It must be a valid shader, following raylib attribs/uniform conventions for GPU skinning
    Shader skinningShader = LoadShader(TextFormat("resources/shaders/glsl%i/skinning.vs", GLSL_VERSION),
                                       TextFormat("resources/shaders/glsl%i/skinning.fs", GLSL_VERSION));

    // Skinning shader could be required to be assigned to all materials shaders, just to make
    // sure required uniforms are being updated for the mesh using that material (and shader)
    for (int i = 0; i < model.materialCount; i++) model.materials[i].shader = skinningShader;
#endif

    // Load model animations
    int animCount = 0;
    ModelAnimation *anims = LoadModelAnimations("resources/models/gltf/robot.glb", &animCount);

    // Animation playing variables
    // NOTE: Two animations are played with a smooth transition between them
    int currentAnimPlaying = 0;         // Current animation playing (0 o 1)
    int nextAnimToPlay = 1;             // Next animation to play (to transition)
    bool animTransition = false;        // Flag to register anim transition state

    int animIndex0 = 10;                // Current animation playing (walking)
    float animCurrentFrame0 = 0.0f;     // Current animation frame (supporting interpolated frames)
    float animFrameSpeed0 = 0.5f;       // Current animation play speed
    int animIndex1 = 6;                 // Next animation to play (running)
    float animCurrentFrame1 = 0.0f;     // Next animation frame (supporting interpolated frames)
    float animFrameSpeed1 = 0.5f;       // Next animation play speed

    float animBlendFactor = 0.0f;       // Blend factor from anim0[frame0] --> anim1[frame1], [0.0f..1.0f]
                                        // NOTE: 0.0f results in full anim0[] and 1.0f in full anim1[]

    float animBlendTime = 2.0f;         // Time to blend from one playing animation to another (in seconds)
    float animBlendTimeCounter = 0.0f;  // Time counter (delta time)

    bool animPause = false;             // Pause animation

    // UI required variables
    char *animNames[64] = { 0 };        // Pointers to animation names for dropdown box
    for (int i = 0; i < animCount; i++) animNames[i] = anims[i].name;

    bool dropdownEditMode0 = false;
    bool dropdownEditMode1 = false;
    float animFrameProgress0 = 0.0f;
    float animFrameProgress1 = 0.0f;
    float animBlendProgress = 0.0f;

    SetTargetFPS(60);                   // Set our game to run at 60 frames-per-second
    //--------------------------------------------------------------------------------------

    // Main game loop
    while (!WindowShouldClose())        // Detect window close button or ESC key
    {
        // Update
        //----------------------------------------------------------------------------------
        UpdateCamera(&camera, CAMERA_ORBITAL);

        if (IsKeyPressed(KEY_P)) animPause = !animPause;

        if (!animPause && (animCount > 0)) // Animations could fail to load
        {
            // Start transition from anim0[] to anim1[]
            if (IsKeyPressed(KEY_SPACE) && !animTransition)
            {
                if (currentAnimPlaying == 0)
                {
                    // Transition anim0 --> anim1
                    nextAnimToPlay = 1;
                    animCurrentFrame1 = 0.0f;
                }
                else
                {
                    // Transition anim1 --> anim0
                    nextAnimToPlay = 0;
                    animCurrentFrame0 = 0.0f;
                }

                // Set animation transition
                animTransition = true;
                animBlendTimeCounter = 0.0f;
                animBlendFactor = 0.0f;
            }

            if (animTransition)
            {
                // Playing anim0 and anim1 at the same time
                animCurrentFrame0 += animFrameSpeed0;
                if (animCurrentFrame0 >= anims[animIndex0].keyframeCount) animCurrentFrame0 = 0.0f;
                animCurrentFrame1 += animFrameSpeed1;
                if (animCurrentFrame1 >= anims[animIndex1].keyframeCount) animCurrentFrame1 = 0.0f;

                // Increment blend factor over time to transition from anim0 --> anim1 over time
                // NOTE: Time blending could be other than linear, using some easing
                animBlendFactor = animBlendTimeCounter/animBlendTime;
                animBlendTimeCounter += GetFrameTime();
                animBlendProgress = animBlendFactor;

                // Update model with animations blending
                if (nextAnimToPlay == 1)
                {
                    // Blend anim0 --> anim1
                    UpdateModelAnimationEx(model, anims[animIndex0], animCurrentFrame0,
                        anims[animIndex1], animCurrentFrame1, animBlendFactor);
                }
                else
                {
                    // Blend anim1 --> anim0
                    UpdateModelAnimationEx(model, anims[animIndex1], animCurrentFrame1,
                        anims[animIndex0], animCurrentFrame0, animBlendFactor);
                }

                // Check if transition completed
                if (animBlendFactor > 1.0f)
                {
                    // Reset frame states
                    if (currentAnimPlaying == 0) animCurrentFrame0 = 0.0f;
                    else if (currentAnimPlaying == 1) animCurrentFrame1 = 0.0f;
                    currentAnimPlaying = nextAnimToPlay; // Update current animation playing

                    animBlendFactor = 0.0f; // Reset blend factor
                    animTransition = false; // Exit transition mode
                    animBlendTimeCounter = 0.0f;
                }
            }
            else
            {
                // Play only one anim, the current one
                if (currentAnimPlaying == 0)
                {
                    // Playing anim0 at defined speed
                    animCurrentFrame0 += animFrameSpeed0;
                    if (animCurrentFrame0 >= anims[animIndex0].keyframeCount) animCurrentFrame0 = 0.0f;
                    UpdateModelAnimation(model, anims[animIndex0], animCurrentFrame0);
                    //UpdateModelAnimationEx(model, anims[animIndex0], animCurrentFrame0,
                    //    anims[animIndex1], animCurrentFrame1, 0.0f); // Same as above, first animation frame blend
                }
                else if (currentAnimPlaying == 1)
                {
                    // Playing anim1 at defined speed
                    animCurrentFrame1 += animFrameSpeed1;
                    if (animCurrentFrame1 >= anims[animIndex1].keyframeCount) animCurrentFrame1 = 0.0f;
                    UpdateModelAnimation(model, anims[animIndex1], animCurrentFrame1);
                    //UpdateModelAnimationEx(model, anims[animIndex0], animCurrentFrame0,
                    //    anims[animIndex1], animCurrentFrame1, 1.0f); // Same as above, second animation frame blend
                }
            }
        }

        // Update progress bars values with current frame for each animation
        animFrameProgress0 = animCurrentFrame0;
        animFrameProgress1 = animCurrentFrame1;
        //----------------------------------------------------------------------------------

        // Draw
        //----------------------------------------------------------------------------------
        BeginDrawing();

            ClearBackground(RAYWHITE);

            BeginMode3D(camera);

                DrawModel(model, position, 1.0f, WHITE); // Draw animated model

                DrawGrid(10, 1.0f);

            EndMode3D();

            if (animTransition) DrawText("ANIM TRANSITION BLENDING!", 170, 50, 30, BLUE);

            // Draw UI elements
            //---------------------------------------------------------------------------------------------
            if (dropdownEditMode0) GuiDisable();
            GuiSlider((Rectangle){ 10, 38, 160, 12 },
                NULL, TextFormat("x%.1f", animFrameSpeed0), &animFrameSpeed0, 0.1f, 2.0f);
            GuiEnable();
            if (dropdownEditMode1) GuiDisable();
            GuiSlider((Rectangle){ GetScreenWidth() - 170.0f, 38, 160, 12 },
                TextFormat("%.1fx", animFrameSpeed1), NULL, &animFrameSpeed1, 0.1f, 2.0f);
            GuiEnable();

            // Draw animation selectors for blending transition
            // NOTE: Transition does not start until requested
            GuiSetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING, 1);
            if (GuiDropdownBox((Rectangle){ 10, 10, 160, 24 }, TextJoin(animNames, animCount, ";"),
                &animIndex0, dropdownEditMode0)) dropdownEditMode0 = !dropdownEditMode0;

            // Blending process progress bar
            if (nextAnimToPlay == 1) GuiSetStyle(PROGRESSBAR, PROGRESS_SIDE, 0); // Left-->Right
            else GuiSetStyle(PROGRESSBAR, PROGRESS_SIDE, 1); // Right-->Left
            GuiProgressBar((Rectangle){ 180, 14, 440, 16 }, NULL, NULL, &animBlendProgress, 0.0f, 1.0f);
            GuiSetStyle(PROGRESSBAR, PROGRESS_SIDE, 0); // Reset to Left-->Right

            if (GuiDropdownBox((Rectangle){ GetScreenWidth() - 170.0f, 10, 160, 24 }, TextJoin(animNames, animCount, ";"),
                &animIndex1, dropdownEditMode1)) dropdownEditMode1 = !dropdownEditMode1;

            GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
            GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetFont().baseSize*2);
            GuiLabel((Rectangle){ 0, GetScreenHeight() - 100.0f, (float)GetScreenWidth(), 40 }, "PRESS SPACE to START BLENDING");
            GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetFont().baseSize);
            GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);

            if (animCount > 0)
            {
                // Draw playing timeline with keyframes for anim0[]
                GuiProgressBar((Rectangle){ 60, GetScreenHeight() - 60.0f, GetScreenWidth() - 180.0f, 20 }, "ANIM 0",
                    TextFormat("FRAME: %.2f / %i", animFrameProgress0, anims[animIndex0].keyframeCount),
                    &animFrameProgress0, 0.0f, (float)anims[animIndex0].keyframeCount);
                for (int i = 0; i < anims[animIndex0].keyframeCount; i++)
                    DrawRectangle(60 + (int)(((float)(GetScreenWidth() - 180)/(float)anims[animIndex0].keyframeCount)*(float)i),
                        GetScreenHeight() - 60, 1, 20, BLUE);

                // Draw playing timeline with keyframes for anim1[]
                GuiProgressBar((Rectangle){ 60, GetScreenHeight() - 30.0f, GetScreenWidth() - 180.0f, 20 }, "ANIM 1",
                    TextFormat("FRAME: %.2f / %i", animFrameProgress1, anims[animIndex1].keyframeCount),
                    &animFrameProgress1, 0.0f, (float)anims[animIndex1].keyframeCount);
                for (int i = 0; i < anims[animIndex1].keyframeCount; i++)
                    DrawRectangle(60 + (int)(((float)(GetScreenWidth() - 180)/(float)anims[animIndex1].keyframeCount)*(float)i),
                        GetScreenHeight() - 30, 1, 20, BLUE);
            }
            //---------------------------------------------------------------------------------------------

        EndDrawing();
        //----------------------------------------------------------------------------------
    }

    // De-Initialization
    //--------------------------------------------------------------------------------------
    UnloadModelAnimations(anims, animCount); // Unload model animation
    UnloadModel(model);             // Unload model and meshes/material
#if SUPPORT_GPU_SKINNING
    UnloadShader(skinningShader);   // Unload GPU skinning shader
#endif

    CloseWindow();                  // Close window and OpenGL context
    //--------------------------------------------------------------------------------------

    return 0;
}

From raylib 6.2. The original on GitHub.