diff --git a/src/graphics/effects/SpaceWarp.cpp b/src/graphics/effects/SpaceWarp.cpp index 89d03f6..31f2cb0 100644 --- a/src/graphics/effects/SpaceWarp.cpp +++ b/src/graphics/effects/SpaceWarp.cpp @@ -321,6 +321,15 @@ void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) { return; } + // Add a small, smooth sub-pixel jitter to the visual center used for + // rendering so a single bright star/comet doesn't permanently sit exactly + // at the pixel-perfect center of the viewport. + const float jitterAmp = 1.6f; // maximum jitter in pixels + const uint32_t now = SDL_GetTicks(); + const float tms = static_cast(now) * 0.001f; + const float centerJitterX = std::sin(tms * 1.7f) * jitterAmp + std::cos(tms * 0.9f) * 0.4f; + const float centerJitterY = std::sin(tms * 1.1f + 3.7f) * (jitterAmp * 0.6f); + SDL_BlendMode previous = SDL_BLENDMODE_NONE; SDL_GetRenderDrawBlendMode(renderer, &previous); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); @@ -339,13 +348,17 @@ void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) { float trailAlphaFloat = alpha * settings.trailAlphaScale; Uint8 trailAlpha = static_cast(std::clamp(trailAlphaFloat, 0.0f, 255.0f)); SDL_SetRenderDrawColor(renderer, color, color, color, trailAlpha); - SDL_RenderLine(renderer, star.prevScreenX, star.prevScreenY, star.screenX, star.screenY); + SDL_RenderLine(renderer, + star.prevScreenX + centerJitterX, star.prevScreenY + centerJitterY, + star.screenX + centerJitterX, star.screenY + centerJitterY); } float dotSize = std::clamp(settings.minDotSize + depthFactor * (settings.maxDotSize - settings.minDotSize), settings.minDotSize, settings.maxDotSize); - SDL_FRect dot{star.screenX - dotSize * 0.5f, star.screenY - dotSize * 0.5f, dotSize, dotSize}; + SDL_FRect dot{star.screenX - dotSize * 0.5f + centerJitterX, + star.screenY - dotSize * 0.5f + centerJitterY, + dotSize, dotSize}; SDL_SetRenderDrawColor(renderer, color, color, color, alpha); SDL_RenderFillRect(renderer, &dot); } @@ -354,10 +367,14 @@ void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) { float lifeNorm = std::clamp(comet.life / comet.maxLife, 0.0f, 1.0f); Uint8 alpha = static_cast(std::clamp(220.0f * lifeNorm, 0.0f, 255.0f)); SDL_SetRenderDrawColor(renderer, comet.color.r, comet.color.g, comet.color.b, alpha); - SDL_RenderLine(renderer, comet.prevScreenX, comet.prevScreenY, comet.screenX, comet.screenY); + SDL_RenderLine(renderer, + comet.prevScreenX + centerJitterX, comet.prevScreenY + centerJitterY, + comet.screenX + centerJitterX, comet.screenY + centerJitterY); float size = comet.size * (0.8f + (1.0f - lifeNorm) * 0.6f); - SDL_FRect head{comet.screenX - size * 0.5f, comet.screenY - size * 0.5f, size, size}; + SDL_FRect head{comet.screenX - size * 0.5f + centerJitterX, + comet.screenY - size * 0.5f + centerJitterY, + size, size}; SDL_RenderFillRect(renderer, &head); } diff --git a/src/graphics/effects/Starfield3D.cpp b/src/graphics/effects/Starfield3D.cpp index f452e2d..06030c3 100644 --- a/src/graphics/effects/Starfield3D.cpp +++ b/src/graphics/effects/Starfield3D.cpp @@ -163,14 +163,22 @@ void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, SDL_Color c } void Starfield3D::draw(SDL_Renderer* renderer, float offsetX, float offsetY, float alphaScale, bool grayscale) { + // Small visual jitter applied to the visual center so a single bright star + // doesn't remain perfectly fixed at the exact pixel center of the viewport. + const float jitterAmp = 1.6f; // max pixel offset + const uint32_t now = SDL_GetTicks(); + const float tms = static_cast(now) * 0.001f; + const float centerJitterX = std::sin(tms * 1.7f) * jitterAmp + std::cos(tms * 0.9f) * 0.4f; + const float centerJitterY = std::sin(tms * 1.1f + 3.7f) * (jitterAmp * 0.6f); + const bool useMagnet = magnetActive && magnetStrength > 0.0f; for (const Star3D& star : stars) { // Calculate perspective projection factor const float k = DEPTH_FACTOR / star.z; // Calculate screen position with perspective - float px = star.x * k + centerX; - float py = star.y * k + centerY; + float px = star.x * k + centerX + centerJitterX; + float py = star.y * k + centerY + centerJitterY; if (useMagnet) { float dx = magnetX - px; diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 88222eb..18504f0 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -716,7 +716,16 @@ void GameRenderer::renderPlayingState( SDL_BlendMode oldBlend = SDL_BLENDMODE_NONE; SDL_GetRenderDrawBlendMode(renderer, &oldBlend); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - s_inGridStarfield.draw(renderer, gridX, gridY, 0.22f, true); + // Add a small, smooth sub-pixel jitter to the starfield origin so the + // brightest star doesn't permanently sit exactly at the visual center. + { + const float jitterAmp = 1.6f; // max pixels of jitter + const uint32_t now = SDL_GetTicks(); + const float tms = static_cast(now) * 0.001f; + const float jitterX = std::sin(tms * 1.7f) * jitterAmp + std::cos(tms * 0.9f) * 0.4f; + const float jitterY = std::sin(tms * 1.1f + 3.7f) * (jitterAmp * 0.6f); + s_inGridStarfield.draw(renderer, gridX + jitterX, gridY + jitterY, 0.22f, true); + } // Update and spawn ambient sparkles inside/around the grid // Use the same RNG and timing values used for impact sparks