mapcreator git · master
SDL3 2.5D game and engine using assets, with map editor
C++ 99.3%git clone https://git.christianimmanuel.de/sdl-graphics/mapcreator.gitwget https://git.christianimmanuel.de/sdl-graphics/mapcreator/archive/mapcreator.tar.gzsrc/cursor/animated_cursor.cpp 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);
}