// g++ -std=c++23 -O2 -Wall -Wextra animated_cursor.hpp.cpp $(pkg-config --cflags --libs sdl3 sdl3-image) -o cursor_particles && ./cursor_particles #include #include #include #include #include #include #include #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(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 angleDist(0.0f, 2.0f * M_PI); std::uniform_real_distribution radiusDist(0.0f, SPHERE_RADIUS); std::uniform_real_distribution 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 angleDist(0.0f, 2.0f * M_PI); std::uniform_real_distribution 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); }