Nimbin[12]?SDL & Graphics / mapcreator / src/cursor/animated_cursor.cpp

mapcreator git · master

SDL3 2.5D game and engine using assets, with map editor

sdl3 c++ game engine map-editor cmake · first commit 2025-10-31 · last commit 2026-09-07 (1 month ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-spieleentwicklung/map_creator

C++ 99.3%
git clone https://git.christianimmanuel.de/sdl-graphics/mapcreator.gitwget https://git.christianimmanuel.de/sdl-graphics/mapcreator/archive/mapcreator.tar.gz
src/cursor/animated_cursor.cpp 9.5 KB · 283 lines raw
// g++ -std=c++23 -O2 -Wall -Wextra animated_cursor.hpp.cpp  $(pkg-config --cflags --libs sdl3 sdl3-image) -o cursor_particles && ./cursor_particles

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <vector>
#include <algorithm>
#include <random>
#include <cmath>
#include <print>

#include "cursor/animated_cursor.hpp"


AnimatedCursor::AnimatedCursor() : spriteTexture(nullptr),
    spriteSurface(nullptr), isReversing(false), reverseProgress(0.0f),
    time(0.0f), clickX(400.0f), clickY(300.0f),
    currentFrame(0), isPlayingAnimation(false), mouseX(400.0f), mouseY(300.0f) {}

    AnimatedCursor::~AnimatedCursor() {
        Cleanup();
    }

bool AnimatedCursor::Init(SDL_Renderer* renderer, const char* filename) {
    spriteSurface = IMG_Load(filename);
    if (!spriteSurface) {
        SDL_Log("IMG_Load failed: %s", SDL_GetError());
        return false;
    }

    spriteTexture = SDL_CreateTextureFromSurface(renderer, spriteSurface);
    if (!spriteTexture) {
        SDL_Log("SDL_CreateTextureFromSurface failed: %s", SDL_GetError());
        return false;
    }

    frame_ExtractFirst();
    return true;
}

void AnimatedCursor::set_State(const CursorState& state_new) {
    std::print("MOUSE STATE {0}\n", (int)state_new);

    bool is_visible = SDL_CursorVisible();
    if (state == CursorState::ANIMATED || state == CursorState::HIDDEN) {
        if (is_visible && !isReversing)
            SDL_HideCursor();
    } else if (state == CursorState::NORMAL){
        if (!is_visible)
            SDL_ShowCursor();
    }
    state = state_new;
}

void AnimatedCursor::frame_ExtractFirst() {
    pixels.clear();
    allFrames.clear();
    allFrames.resize(TOTAL_FRAMES);

    // Lock surface for pixel access
    SDL_LockSurface(spriteSurface);
    Uint32* surfacePixels = (Uint32*)spriteSurface->pixels;
    int pitch = spriteSurface->pitch / 4;

    const SDL_PixelFormatDetails* details =
        SDL_GetPixelFormatDetails(spriteSurface->format);
    if (!details) {
        SDL_UnlockSurface(spriteSurface);
        return;
    }

    // Extract non-transparent pixels from all 8 frames
    for (size_t frame = 0; frame < TOTAL_FRAMES; frame++) {
        int frameOffsetX = static_cast<int>(frame) * FRAME_WIDTH;

        for (int y = 0; y < FRAME_HEIGHT; y++) {
            for (int x = 0; x < FRAME_WIDTH; x++) {
                Uint32 pixel = surfacePixels[y * pitch + (frameOffsetX + x)];
                Uint8 r, g, b, a;
                SDL_GetRGBA(pixel, details, NULL, &r, &g, &b, &a);

                if (a > 0) { // Non-transparent pixel
                    Pixel p;
                    p.srcX = x;
                    p.srcY = y;
                    p.originalColor = {r, g, b, a};
                    allFrames[frame].push_back(p);
                }
            }
        }
    }

    SDL_UnlockSurface(spriteSurface);

    // Start with first frame
    pixels = allFrames[0];
    currentFrame = 0;

    // Generate shuffled positions and colors
    state_GenerateShuffle();
}

void AnimatedCursor::state_GenerateShuffle() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * M_PI);
    std::uniform_real_distribution<float> radiusDist(0.0f, SPHERE_RADIUS);
    std::uniform_real_distribution<float> speedDist(0.3f, 4.5f);
    std::uniform_int_distribution<> colorDist(0, 255);

    // Initialize each pixel with random position and movement
    for (auto& p : pixels) {
        // Initial position in sphere
        p.currentAngle = angleDist(gen);
        p.currentRadius = radiusDist(gen);

        // Target for smooth movement
        p.targetAngle = angleDist(gen);
        p.targetRadius = radiusDist(gen);

        // Random movement speeds
        p.angleSpeed = speedDist(gen);
        p.radiusSpeed = speedDist(gen) * 0.5f;

        // Generate random shuffled color
        p.shuffledColor = {
            (Uint8)colorDist(gen),
            (Uint8)colorDist(gen),
            (Uint8)colorDist(gen),
            p.originalColor.a
        };
    }
}

SDL_Color lerpColor(const SDL_Color& a, const SDL_Color& b, float t) {
    return {
        (Uint8)(a.r + (b.r - a.r) * t),
        (Uint8)(a.g + (b.g - a.g) * t),
        (Uint8)(a.b + (b.b - a.b) * t),
        (Uint8)(a.a + (b.a - a.a) * t)
    };
}

void AnimatedCursor::Render(SDL_Renderer* renderer) {
    if (state != CursorState::ANIMATED && !isReversing) return;
    SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255);

    // Calculate current state based on reverse progress
    float progress = isReversing ? reverseProgress : 0.0f;

    // Ease function for smooth animation
    float eased = progress * progress * (3.0f - 2.0f * progress); // Smoothstep

    // Position where animation happens (click position or following mouse)
    // float centerX = isReversing ? clickX : mouseX;
    // float centerY = isReversing ? clickY : mouseY;
    float centerX =  mouseX;
    float centerY =  mouseY;

    for (const auto& p : pixels) {
        // Calculate position in sphere
        float sphereX = centerX + cos(p.currentAngle) * p.currentRadius;
        float sphereY = centerY + sin(p.currentAngle) * p.currentRadius;

        // Interpolate position
        int x, y;
        if (isReversing) {
            // During animation: sphere -> sprite at click position
            x = (int)(sphereX + (centerX - FRAME_WIDTH/2 + p.srcX - sphereX) * eased);
            y = (int)(sphereY + (centerY - FRAME_HEIGHT/2 + p.srcY - sphereY) * eased);
        } else {
            // Shuffled state (sphere following mouse)
            x = (int)sphereX;
            y = (int)sphereY;
        }

        // Interpolate color
        SDL_Color color;
        if (isReversing) {
            color = lerpColor(p.shuffledColor, p.originalColor, eased);
        } else {
            color = p.shuffledColor;
        }

        SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
        SDL_RenderPoint(renderer, x, y);
    }

}

void AnimatedCursor::Update(float deltaTime, float mx, float my) {
    if (state != CursorState::ANIMATED && !isReversing) return;
    mouseX = mx; mouseY = my;
    time += deltaTime;

    // Update pixel positions (movement within sphere)
    if (!isReversing) {
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * M_PI);
        std::uniform_real_distribution<float> radiusDist(0.0f, SPHERE_RADIUS);

        for (auto& p : pixels) {
            // Smoothly move toward target
            float angleDiff = p.targetAngle - p.currentAngle;
            // Normalize angle difference to [-PI, PI]
            while (angleDiff > M_PI) angleDiff -= 2.0f * M_PI;
            while (angleDiff < -M_PI) angleDiff += 2.0f * M_PI;

            p.currentAngle  += angleDiff * p.angleSpeed * deltaTime;
            p.currentRadius += (p.targetRadius - p.currentRadius) * p.radiusSpeed * deltaTime;

            // Check if close to target, if so pick new target
            if (fabs(angleDiff) < 0.1f && fabs(p.targetRadius - p.currentRadius) < 1.0f) {
                p.targetAngle  = angleDist(gen);
                p.targetRadius = radiusDist(gen);
            }
        }
    }

    // Handle reverse animation and frame playback
    if (isReversing) {
        reverseProgress += deltaTime / ANIMATION_DURATION;

        if (reverseProgress >= 0.2f) {
            // Unshuffle complete, now play through frames
            if (!isPlayingAnimation) {
                isPlayingAnimation = true;
                currentFrame       = 0;
                reverseProgress    = 0.0f;
            }
        }

        // Play through animation frames
        if (isPlayingAnimation) {
            // Play all 8 frames over remaining time
            float frameTime = ANIMATION_DURATION / TOTAL_FRAMES;
            currentFrame = (int)(reverseProgress / frameTime);

            if (currentFrame >= (int)TOTAL_FRAMES) {
                // Animation complete, go back to frame 0 and reshuffle
                currentFrame = 0;
                pixels = allFrames[0];
                state_GenerateShuffle();
                isReversing        = false;
                isPlayingAnimation = false;
                reverseProgress    = 0.0f;
            } else {
                // Switch to current frame
                if (currentFrame < (int)TOTAL_FRAMES && currentFrame >= 0) {
                    pixels = allFrames[(size_t)currentFrame];
                    // Keep the shuffle state for smooth transition
                    for (size_t i = 0; i < pixels.size() && i < allFrames[0].size(); i++) {
                        pixels[i].currentAngle  = allFrames[0][i].currentAngle;
                        pixels[i].currentRadius = allFrames[0][i].currentRadius;
                        pixels[i].targetAngle   = allFrames[0][i].targetAngle;
                        pixels[i].targetRadius  = allFrames[0][i].targetRadius;
                        pixels[i].angleSpeed    = allFrames[0][i].angleSpeed;
                        pixels[i].radiusSpeed   = allFrames[0][i].radiusSpeed;
                        pixels[i].shuffledColor = allFrames[0][i].shuffledColor;
                    }
                }
            }

            reverseProgress += deltaTime / ANIMATION_DURATION;
        }
    }
}

void AnimatedCursor::mouse_OnClick() {
    if (state != CursorState::ANIMATED) return;
    if (isReversing) return;

    isReversing = true;
    reverseProgress = 0.0f;
    clickX = mouseX;
    clickY = mouseY;
}


void AnimatedCursor::Cleanup() {
    if (spriteTexture) SDL_DestroyTexture(spriteTexture);
    if (spriteSurface) SDL_DestroySurface(spriteSurface);
}