#include "Renderer.hpp" #include "Font.hpp" #include #include #include #include #include // Playboard grid rendering layout variables static constexpr int CELL_SIZE = 25; static constexpr int BOARD_X = 275; static constexpr int BOARD_Y = 50; static constexpr int BOARD_W = 10 * CELL_SIZE; static constexpr int BOARD_H = 20 * CELL_SIZE; // Sleek neon color constants static const SDL_Color COLOR_CYAN = {0, 240, 255, 255}; static const SDL_Color COLOR_PINK = {255, 0, 150, 255}; static const SDL_Color COLOR_WHITE = {255, 255, 255, 255}; static const SDL_Color COLOR_ORANGE = {255, 140, 0, 255}; static const SDL_Color COLOR_RED = {245, 0, 50, 255}; // Helper function to draw a single pixel with optional alpha blending onto an RGBA32 surface static inline void drawPixelSoftware(SDL_Surface* surface, int x, int y, SDL_Color color, bool blend) { if (x < 0 || x >= surface->w || y < 0 || y >= surface->h) return; uint32_t* pixels = static_cast(surface->pixels); int pitch = surface->pitch / 4; // 32-bit words per row uint32_t& dstPixel = pixels[y * pitch + x]; if (!blend || color.a == 255) { dstPixel = SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); } else if (color.a > 0) { Uint8 dstR, dstG, dstB, dstA; SDL_GetRGBA(dstPixel, surface->format, &dstR, &dstG, &dstB, &dstA); float alpha = color.a / 255.0f; float invAlpha = 1.0f - alpha; Uint8 outR = static_cast(color.r * alpha + dstR * invAlpha); Uint8 outG = static_cast(color.g * alpha + dstG * invAlpha); Uint8 outB = static_cast(color.b * alpha + dstB * invAlpha); Uint8 outA = static_cast(color.a + dstA * invAlpha); dstPixel = SDL_MapRGBA(surface->format, outR, outG, outB, outA); } } // Helper function to fill a rectangle with optional alpha blending on an RGBA32 surface static void fillRectSoftware(SDL_Surface* surface, const SDL_Rect* rect, SDL_Color color, bool blend) { SDL_Rect clippedRect; SDL_Rect surfaceRect = {0, 0, surface->w, surface->h}; if (!SDL_IntersectRect(rect, &surfaceRect, &clippedRect)) return; uint32_t* pixels = static_cast(surface->pixels); int pitch = surface->pitch / 4; if (!blend || color.a == 255) { uint32_t mappedColor = SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); for (int y = clippedRect.y; y < clippedRect.y + clippedRect.h; ++y) { uint32_t* row = pixels + y * pitch; std::fill(row + clippedRect.x, row + clippedRect.x + clippedRect.w, mappedColor); } } else if (color.a > 0) { float alpha = color.a / 255.0f; float invAlpha = 1.0f - alpha; for (int y = clippedRect.y; y < clippedRect.y + clippedRect.h; ++y) { uint32_t* row = pixels + y * pitch; for (int x = clippedRect.x; x < clippedRect.x + clippedRect.w; ++x) { uint32_t& dstPixel = row[x]; Uint8 dstR, dstG, dstB, dstA; SDL_GetRGBA(dstPixel, surface->format, &dstR, &dstG, &dstB, &dstA); Uint8 outR = static_cast(color.r * alpha + dstR * invAlpha); Uint8 outG = static_cast(color.g * alpha + dstG * invAlpha); Uint8 outB = static_cast(color.b * alpha + dstB * invAlpha); Uint8 outA = static_cast(color.a + dstA * invAlpha); dstPixel = SDL_MapRGBA(surface->format, outR, outG, outB, outA); } } } } // Helper function to draw a rectangle border with optional alpha blending on an RGBA32 surface static void drawRectSoftware(SDL_Surface* surface, const SDL_Rect* rect, SDL_Color color, bool blend) { // Top border SDL_Rect top = {rect->x, rect->y, rect->w, 1}; fillRectSoftware(surface, &top, color, blend); // Bottom border SDL_Rect bottom = {rect->x, rect->y + rect->h - 1, rect->w, 1}; fillRectSoftware(surface, &bottom, color, blend); // Left border SDL_Rect left = {rect->x, rect->y, 1, rect->h}; fillRectSoftware(surface, &left, color, blend); // Right border SDL_Rect right = {rect->x + rect->w - 1, rect->y, 1, rect->h}; fillRectSoftware(surface, &right, color, blend); } // Helper function to draw a line with optional alpha blending on an RGBA32 surface using Bresenham's algorithm static void drawLineSoftware(SDL_Surface* surface, int x1, int y1, int x2, int y2, SDL_Color color, bool blend) { // Handle trivial horizontal/vertical cases with fast fillRectSoftware if (x1 == x2) { int minY = std::min(y1, y2); int maxY = std::max(y1, y2); SDL_Rect rect = {x1, minY, 1, maxY - minY + 1}; fillRectSoftware(surface, &rect, color, blend); return; } if (y1 == y2) { int minX = std::min(x1, x2); int maxX = std::max(x1, x2); SDL_Rect rect = {minX, y1, maxX - minX + 1, 1}; fillRectSoftware(surface, &rect, color, blend); return; } int dx = std::abs(x2 - x1); int dy = std::abs(y2 - y1); int sx = (x1 < x2) ? 1 : -1; int sy = (y1 < y2) ? 1 : -1; int err = dx - dy; while (true) { drawPixelSoftware(surface, x1, y1, color, blend); if (x1 == x2 && y1 == y2) break; int e2 = 2 * err; if (e2 > -dy) { err -= dy; x1 += sx; } if (e2 < dx) { err += dx; y1 += sy; } } } Renderer::Renderer() { initStars(); } Renderer::~Renderer() { if (mFontTexture != nullptr) { SDL_DestroyTexture(mFontTexture); } if (mFontSurface != nullptr) { SDL_FreeSurface(mFontSurface); } if (mBackbufferSurface != nullptr) { SDL_FreeSurface(mBackbufferSurface); } if (mScaledSurface != nullptr) { SDL_FreeSurface(mScaledSurface); } if (mBackbufferTexture != nullptr) { SDL_DestroyTexture(mBackbufferTexture); } } void Renderer::initStars() { mStars.clear(); for (int i = 0; i < 60; ++i) { Star s; s.x = static_cast(std::rand() % 800); s.y = static_cast(std::rand() % 600); s.speed = 10.0f + static_cast(std::rand() % 30); s.size = 1.0f + static_cast(std::rand() % 3); s.alpha = 50.0f + static_cast(std::rand() % 150); mStars.push_back(s); } } bool Renderer::init(SDL_Renderer* renderer, int targetW, int targetH) { mRenderer = renderer; mTargetW = targetW; mTargetH = targetH; buildFontTexture(); if (mFontSurface == nullptr) { return false; } // Create software backbuffer surface of 800x600 in RGBA32 format mBackbufferSurface = SDL_CreateRGBSurfaceWithFormat(0, 800, 600, 32, SDL_PIXELFORMAT_RGBA32); if (mBackbufferSurface == nullptr) { return false; } #ifdef MIYOO_BUILD // On Miyoo, the maximum streaming texture size supported by the hardware/driver is 640x480. // So we always scale the 800x600 backbuffer to a 640x480 surface/texture in RAM, // and let the GPU/hardware scale the 640x480 texture to the actual window resolution (e.g., 752x560 on V4). mScaledSurface = SDL_CreateRGBSurfaceWithFormat(0, 640, 480, 32, SDL_PIXELFORMAT_RGBA32); if (mScaledSurface == nullptr) { return false; } mBackbufferTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, 640, 480); #else mBackbufferTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, mTargetW, mTargetH); #endif if (mBackbufferTexture == nullptr) { return false; } return true; } void Renderer::buildFontTexture() { // Lay out the 95-char font atlas in two rows to stay within the // 640px texture width limit imposed by the Miyoo MI_GFX driver. // Row 0: chars 0..47 (ASCII 32..79), Row 1: chars 48..94 (ASCII 80..126) static constexpr int CHARS_PER_ROW = 48; int atlasW = CHARS_PER_ROW * 8; // 384 pixels — fits in 640 int atlasH = 16; // 2 rows of 8 pixels each SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat( 0, atlasW, atlasH, 32, SDL_PIXELFORMAT_RGBA32); if (surface == nullptr) return; SDL_LockSurface(surface); uint32_t* pixels = (uint32_t*)surface->pixels; int pitch = atlasW; // pixels per row (stride = atlasW * 4 bytes, but we use uint32*) for (int c = 0; c < 95; ++c) { int row = c / CHARS_PER_ROW; int col = c % CHARS_PER_ROW; for (int r = 0; r < 8; ++r) { uint8_t byte = Font::BITMAP[c][r]; for (int bit = 0; bit < 8; ++bit) { bool on = (byte >> (7 - bit)) & 1; int px = col * 8 + bit; int py = row * 8 + r; pixels[py * pitch + px] = on ? 0xFFFFFFFF : 0x00000000; } } } SDL_UnlockSurface(surface); mFontSurface = surface; mFontTexture = SDL_CreateTextureFromSurface(mRenderer, mFontSurface); if (mFontTexture != nullptr) { SDL_SetTextureBlendMode(mFontTexture, SDL_BLENDMODE_BLEND); } } void Renderer::update(float dt) { mTime += dt; // 1. Update Parallax Background Stars for (auto& s : mStars) { s.y += s.speed * dt; if (s.y > 600.0f) { s.y = 0.0f; s.x = static_cast(std::rand() % 800); } } // 2. Update Physics Particles (gravitational debris) for (size_t i = 0; i < mParticles.size();) { Particle& p = mParticles[i]; p.life -= p.decay * dt; if (p.life <= 0.0f) { // Remove dead particles mParticles[i] = mParticles.back(); mParticles.pop_back(); } else { // Kinematic updates with gravity pulling down p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 320.0f * dt; // gravity i++; } } } SDL_Color Renderer::getPieceColor(PieceType type) { switch (type) { case PieceType::I: return {0, 240, 240, 255}; // Vibrant Cyan case PieceType::O: return {240, 240, 0, 255}; // Vibrant Yellow case PieceType::T: return {170, 0, 245, 255}; // Neon Purple case PieceType::S: return {0, 240, 0, 255}; // Neon Green case PieceType::Z: return {245, 0, 0, 255}; // Neon Red case PieceType::J: return {0, 100, 245, 255}; // Deep Blue case PieceType::L: return {245, 130, 0, 255}; // Bright Orange default: return {0, 0, 0, 0}; } } void Renderer::drawBlock(int gridX, int gridY, PieceType type, bool isGhost, float alphaOverride) { if (type == PieceType::NONE) return; int rx = BOARD_X + gridX * CELL_SIZE; int ry = BOARD_Y + gridY * CELL_SIZE; SDL_Color color = getPieceColor(type); if (isGhost) { // Draw pulsing outline for ghost pieces float pulse = 0.5f + 0.3f * std::sin(mTime * 10.0f); SDL_Color pulseCol = {color.r, color.g, color.b, static_cast(pulse * 255.0f * alphaOverride)}; SDL_Rect rect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; drawRectSoftware(mBackbufferSurface, &rect, pulseCol, true); return; } // Neon beveled crystal shading style // 1. Draw solid crystal core SDL_Color coreCol = {color.r, color.g, color.b, static_cast(255 * alphaOverride)}; SDL_Rect coreRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; fillRectSoftware(mBackbufferSurface, &coreRect, coreCol, true); // 2. Draw glossy 3D inner reflection core SDL_Color lighterColor = { static_cast(std::min(color.r + 60, 255)), static_cast(std::min(color.g + 60, 255)), static_cast(std::min(color.b + 60, 255)), 255 }; SDL_Color reflectionCol = {lighterColor.r, lighterColor.g, lighterColor.b, static_cast(200 * alphaOverride)}; SDL_Rect innerRect = {rx + 3, ry + 3, CELL_SIZE - 6, CELL_SIZE - 6}; fillRectSoftware(mBackbufferSurface, &innerRect, reflectionCol, true); // 3. Highlight top-left border (specular light sheen) SDL_Color highlightCol = {255, 255, 255, static_cast(180 * alphaOverride)}; drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); // 4. Shade bottom-right border (3D shadow depth) SDL_Color shadowCol = {0, 0, 0, static_cast(130 * alphaOverride)}; drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); } void Renderer::drawGrid(const Game& game) { // Subtle dark blue/gray playboard background SDL_Color boardCol = {15, 17, 24, 255}; SDL_Rect boardBG = {BOARD_X, BOARD_Y, BOARD_W, BOARD_H}; fillRectSoftware(mBackbufferSurface, &boardBG, boardCol, true); // Draw grid lines SDL_Color gridLineCol = {30, 34, 46, 120}; for (int x = 1; x < 10; ++x) { drawLineSoftware(mBackbufferSurface, BOARD_X + x * CELL_SIZE, BOARD_Y, BOARD_X + x * CELL_SIZE, BOARD_Y + BOARD_H, gridLineCol, true); } for (int y = 1; y < 20; ++y) { drawLineSoftware(mBackbufferSurface, BOARD_X, BOARD_Y + y * CELL_SIZE, BOARD_X + BOARD_W, BOARD_Y + y * CELL_SIZE, gridLineCol, true); } // Draw locked blocks on the board for (int y = 0; y < 20; ++y) { for (int x = 0; x < 10; ++x) { PieceType cell = game.getCell(x, y); if (cell != PieceType::NONE) { // If a line is cleared, skip drawing it here so it flashes white under the particles if (std::find(game.mClearedLinesThisTick.begin(), game.mClearedLinesThisTick.end(), y) != game.mClearedLinesThisTick.end()) { continue; } drawBlock(x, y, cell); } } } // Draw dynamic glowing borders around the play board (cyberpunk look) float pulse = 0.8f + 0.2f * std::sin(mTime * 4.0f); SDL_Color borderCol = {0, 150, 255, static_cast(255 * pulse)}; // Outer border outline layers for (int i = 0; i < 3; ++i) { SDL_Rect border = {BOARD_X - i, BOARD_Y - i, BOARD_W + i * 2, BOARD_H + i * 2}; drawRectSoftware(mBackbufferSurface, &border, borderCol, true); } } void Renderer::drawText(const std::string& text, int x, int y, int scale, SDL_Color color, bool center, bool glow) { if (mFontSurface == nullptr) return; // Font atlas layout: 48 chars per row, 2 rows of 8px each. static constexpr int CHARS_PER_ROW = 48; int totalWidth = text.length() * 8 * scale; int startX = center ? x - totalWidth / 2 : x; SDL_SetSurfaceBlendMode(mFontSurface, SDL_BLENDMODE_BLEND); auto charSrc = [](char c) -> SDL_Rect { int idx = (c >= 32 && c <= 126) ? (c - 32) : 0; int row = idx / CHARS_PER_ROW; int col = idx % CHARS_PER_ROW; return SDL_Rect{ col * 8, row * 8, 8, 8 }; }; // Optional black text outline to simulate glow/shadow if (glow) { SDL_SetSurfaceColorMod(mFontSurface, 0, 0, 0); SDL_SetSurfaceAlphaMod(mFontSurface, 180); for (int dx = -1; dx <= 1; ++dx) { for (int dy = -1; dy <= 1; ++dy) { if (dx == 0 && dy == 0) continue; int curX = startX; for (char c : text) { if (c >= 32 && c <= 126) { SDL_Rect src = charSrc(c); SDL_Rect dest = { curX + dx * scale, y + dy * scale, 8 * scale, 8 * scale }; SDL_BlitScaled(mFontSurface, &src, mBackbufferSurface, &dest); } curX += 8 * scale; } } } } // Main text render SDL_SetSurfaceColorMod(mFontSurface, color.r, color.g, color.b); SDL_SetSurfaceAlphaMod(mFontSurface, color.a); int curX = startX; for (char c : text) { if (c >= 32 && c <= 126) { SDL_Rect src = charSrc(c); SDL_Rect dest = { curX, y, 8 * scale, 8 * scale }; SDL_BlitScaled(mFontSurface, &src, mBackbufferSurface, &dest); } curX += 8 * scale; } } void Renderer::drawUI(const Game& game) { // 1. Title float titlePulse = std::sin(mTime * 3.0f); SDL_Color dynamicColor = (titlePulse >= 0.0f) ? COLOR_CYAN : COLOR_PINK; drawText("CYBERMATRIS", 400, 15, 3, dynamicColor, true, true); // 2. Hold Box Panel (Left Side) drawText("HOLD", 140, 50, 2, COLOR_CYAN, true, true); SDL_Color panelBorderCol = {0, 150, 255, 120}; SDL_Rect holdRect = {40, 70, 200, 110}; drawRectSoftware(mBackbufferSurface, &holdRect, panelBorderCol, true); PieceType holdPiece = game.getHoldPieceType(); if (holdPiece != PieceType::NONE) { int typeIdx = static_cast(holdPiece); SDL_Color pColor = getPieceColor(holdPiece); // Compute offsets to center pieces perfectly int offX = -12; int offY = -12; for (int i = 0; i < 4; ++i) { Point p = TETROMINO_CELLS[typeIdx][0][i]; int rx = 40 + 100 + offX + p.x * CELL_SIZE; int ry = 70 + 55 + offY + p.y * CELL_SIZE; // Draw beveled block SDL_Color blockCol = {pColor.r, pColor.g, pColor.b, 255}; SDL_Rect bRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; fillRectSoftware(mBackbufferSurface, &bRect, blockCol, true); SDL_Color highlightCol = {255, 255, 255, 160}; drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); SDL_Color shadowCol = {0, 0, 0, 110}; drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); } } // 3. Stats Dashboard Panel (Left Side, below Hold) drawText("STATS", 140, 205, 2, COLOR_CYAN, true, true); SDL_Rect statsRect = {40, 225, 200, 325}; drawRectSoftware(mBackbufferSurface, &statsRect, panelBorderCol, true); // Render text metrics inside dashboard drawText("SCORE", 140, 245, 1, COLOR_CYAN, true, false); std::stringstream ssScore; ssScore << std::setw(6) << std::setfill('0') << game.getScore(); drawText(ssScore.str(), 140, 260, 2, COLOR_ORANGE, true, true); drawText("LEVEL", 140, 305, 1, COLOR_CYAN, true, false); drawText(std::to_string(game.getLevel()), 140, 320, 2, COLOR_WHITE, true, true); drawText("LINES", 140, 365, 1, COLOR_CYAN, true, false); drawText(std::to_string(game.getLinesCleared()), 140, 380, 2, COLOR_WHITE, true, true); // Show Combo notifier if positive if (game.getCombo() > 0) { float comboPulse = std::sin(mTime * 15.0f) * 0.5f + 0.5f; SDL_Color cColor = {255, static_cast(100 + 155 * comboPulse), 0, 255}; std::string comboStr = "COMBO X" + std::to_string(game.getCombo() + 1); drawText(comboStr, 140, 440, 2, cColor, true, true); drawText("Pulsing!", 140, 470, 1, COLOR_PINK, true, false); } else { drawText("CHIPTUNE", 140, 445, 1, COLOR_PINK, true, false); drawText("SYNTH ACTIVE", 140, 465, 1, COLOR_CYAN, true, false); drawText("4-CH MONSTER", 140, 485, 1, COLOR_WHITE, true, false); } // 4. Next Pieces Queue Box (Right Side) drawText("NEXT", 660, 50, 2, COLOR_CYAN, true, true); SDL_Rect nextRect = {560, 70, 200, 305}; drawRectSoftware(mBackbufferSurface, &nextRect, panelBorderCol, true); // Draw next 3 pieces auto nextQueue = game.getNextQueue(); for (int n = 0; n < 3; ++n) { if (n >= (int)nextQueue.size()) break; PieceType nPiece = nextQueue[n]; int typeIdx = static_cast(nPiece); SDL_Color pColor = getPieceColor(nPiece); int offX = -12; int offY = -12; // Render mini blocks for each next queue element in slots for (int i = 0; i < 4; ++i) { Point p = TETROMINO_CELLS[typeIdx][0][i]; int rx = 560 + 100 + offX + p.x * CELL_SIZE; int ry = 70 + 60 + n * 95 + offY + p.y * CELL_SIZE; SDL_Color blockCol = {pColor.r, pColor.g, pColor.b, 255}; SDL_Rect bRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; fillRectSoftware(mBackbufferSurface, &bRect, blockCol, true); SDL_Color highlightCol = {255, 255, 255, 160}; drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); SDL_Color shadowCol = {0, 0, 0, 110}; drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); } } } void Renderer::drawVisualizer(Synth& synth, int x, int y, int w, int h) { // 1. Draw outer pane container SDL_Color containerCol = {0, 150, 255, 120}; SDL_Rect container = {x, y, w, h}; drawRectSoftware(mBackbufferSurface, &container, containerCol, true); // Subtle Visualizer label drawText("AUDIO OSCILLOSCOPE", x + w / 2, y + 10, 1, {0, 240, 255, 255}, true, false); // Fetch the live stereo buffer copies auto buffer = synth.getVisualizerBuffer(); if (buffer.empty()) return; int halfH = h / 2; int centerY = y + halfH + 10; int dataW = w - 20; int dataX = x + 10; // 2. Draw 12 spectrum equalizer bar segments in background int numBars = 12; int barW = (dataW - (numBars - 1) * 3) / numBars; int samplesPerBar = buffer.size() / numBars; for (int b = 0; b < numBars; ++b) { // Calculate dynamic energy (Root Mean Square average amplitude) of segment float rmsSum = 0.0f; for (int s = 0; s < samplesPerBar; ++s) { float val = buffer[b * samplesPerBar + s]; rmsSum += val * val; } float rms = std::sqrt(rmsSum / samplesPerBar); // Smooth scaling factor float barHeight = rms * (h - 60) * 2.8f; barHeight = std::clamp(barHeight, 2.0f, static_cast(h - 50)); // Pulsing color cycle: fades from hot purple to high green SDL_Color barCol = { static_cast(std::min(rms * 900.0f, 255.0f)), static_cast(180 - std::min(rms * 300.0f, 180.0f)), 245, 70 }; SDL_Rect barRect = { dataX + b * (barW + 3), y + h - 15 - static_cast(barHeight), barW, static_cast(barHeight) }; fillRectSoftware(mBackbufferSurface, &barRect, barCol, true); } // 3. Draw neon green oscilloscope line connecting points SDL_Color waveCol = {0, 255, 120, 255}; int prevX = dataX; int prevY = centerY; int step = buffer.size() / dataW; if (step <= 0) step = 1; for (int i = 0; i < dataW; ++i) { int idx = i * step; if (idx >= (int)buffer.size()) break; float sample = buffer[idx]; int px = dataX + i; // Amplify wave height int py = centerY + static_cast(sample * halfH * 1.5f); py = std::clamp(py, y + 25, y + h - 10); if (i > 0) { drawLineSoftware(mBackbufferSurface, prevX, prevY, px, py, waveCol, true); } prevX = px; prevY = py; } } void Renderer::spawnLineClearParticles(const Game& game) { // When lines clear, shatter blocks into glowing spark bursts! for (int y : game.mClearedLinesThisTick) { // For every block across the line for (int x = 0; x < 10; ++x) { PieceType pType = game.getCell(x, y); if (pType == PieceType::NONE) pType = PieceType::I; // fallback SDL_Color color = getPieceColor(pType); int rx = BOARD_X + x * CELL_SIZE + CELL_SIZE / 2; int ry = BOARD_Y + y * CELL_SIZE + CELL_SIZE / 2; // Spawn 5 explosive sparks per block for (int p = 0; p < 5; ++p) { Particle part; part.x = static_cast(rx); part.y = static_cast(ry); // Random explosive velocity vectors float angle = static_cast(std::rand() % 360) * (3.14159f / 180.0f); float speed = 50.0f + static_cast(std::rand() % 220); part.vx = std::cos(angle) * speed; part.vy = std::sin(angle) * speed - 50.0f; // slight upwards blast bias part.life = 1.0f; part.decay = 1.2f + static_cast(std::rand() % 10) * 0.1f; // decay ~0.8s part.size = 2.0f + static_cast(std::rand() % 4); part.color = color; mParticles.push_back(part); } } } } void Renderer::spawnLandDustParticles(int gridX, int gridY, PieceType type) { if (type == PieceType::NONE) return; SDL_Color color = {230, 235, 255, 200}; // light grayish white puff int rx = BOARD_X + gridX * CELL_SIZE + CELL_SIZE / 2; int ry = BOARD_Y + (gridY + 1) * CELL_SIZE; // spawn right at contact base // Spawn 8 tiny dust puff particles drifting left and right for (int i = 0; i < 8; ++i) { Particle p; p.x = static_cast(rx + (std::rand() % CELL_SIZE) - CELL_SIZE / 2); p.y = static_cast(ry - 2); p.vx = static_cast((std::rand() % 100) - 50); // drift sideways p.vy = -10.0f - static_cast(std::rand() % 30); // slight upwards float p.life = 1.0f; p.decay = 2.0f + static_cast(std::rand() % 10) * 0.2f; // quick decay ~0.4s p.size = 2.0f + static_cast(std::rand() % 3); p.color = color; mParticles.push_back(p); } } void Renderer::render(const Game& game, Synth& synth) { if (mBackbufferSurface == nullptr) return; // 1. Clear Screen with deep cosmic space black SDL_Color clearCol = {10, 11, 16, 255}; SDL_Rect bgRect = {0, 0, 800, 600}; fillRectSoftware(mBackbufferSurface, &bgRect, clearCol, false); // 2. Render drifting parallax background stars for (const auto& s : mStars) { SDL_Color starCol = {255, 255, 255, static_cast(s.alpha)}; SDL_Rect starRect = { static_cast(s.x), static_cast(s.y), static_cast(s.size), static_cast(s.size) }; fillRectSoftware(mBackbufferSurface, &starRect, starCol, true); } // Apply decayed screenshake camera offsets if any int camDX = 0; int camDY = 0; mShakeIntensity = game.getScreenShakeIntensity(); if (mShakeIntensity > 0.01f) { camDX = static_cast(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f); camDY = static_cast(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f); } // 3. Render the dynamic cyberpunk grid and locked board blocks drawGrid(game); // 4. Render Active Tetromino (glowing crystal style) PieceType activeType = game.getActivePieceType(); if (activeType != PieceType::NONE) { // A. Draw Ghost projection piece first (pulsing neon wireframe) auto ghostCells = game.getGhostCells(); for (const auto& p : ghostCells) { drawBlock(p.x, p.y, activeType, true); } // B. Draw Active Piece blocks auto activeCells = game.getActiveCells(); for (const auto& p : activeCells) { drawBlock(p.x, p.y, activeType); } } // 5. Render sidebars, score gauge, next and hold compartments drawUI(game); // 6. Render real-time audio visualizer oscilloscope panel drawVisualizer(synth, 560, 390, 200, 160); // 7. Render physics particles (debris sparks) for (const auto& p : mParticles) { SDL_Color pCol = {p.color.r, p.color.g, p.color.b, static_cast(p.life * 255.0f)}; SDL_Rect pRect = { static_cast(p.x - p.size / 2), static_cast(p.y - p.size / 2), static_cast(p.size), static_cast(p.size) }; fillRectSoftware(mBackbufferSurface, &pRect, pCol, true); } // 8. Render Game Over / Start Banner overlay if (game.getState() == GameState::GAME_OVER) { // Translucent overlay shroud SDL_Color overlayCol = {10, 10, 15, 200}; SDL_Rect overlay = {0, 0, 800, 600}; fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); float flash = std::sin(mTime * 6.0f) * 0.5f + 0.5f; SDL_Color flashRed = {255, static_cast(flash * 100), static_cast(flash * 50), 255}; drawText("GAME OVER", 400, 220, 4, flashRed, true, true); std::stringstream ssFinalScore; ssFinalScore << "FINAL SCORE " << game.getScore(); drawText(ssFinalScore.str(), 400, 280, 2, COLOR_CYAN, true, false); drawText("PRESS R TO RESTART", 400, 350, 2, COLOR_WHITE, true, true); drawText("OR ESC TO QUIT", 400, 390, 1, {180, 180, 200, 255}, true, false); } else if (game.getState() == GameState::START) { SDL_Color overlayCol = {8, 9, 13, 230}; SDL_Rect overlay = {0, 0, 800, 600}; fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); // Blinking start cue float blink = std::sin(mTime * 5.0f) * 0.5f + 0.5f; SDL_Color cueColor = {static_cast(180 + 75 * blink), static_cast(200 + 55 * blink), 255, 255}; drawText("CYBERMATRIS", 400, 180, 5, COLOR_PINK, true, true); drawText("RETROWAVE PROCEDURAL TETRIS", 400, 240, 1, COLOR_CYAN, true, false); // Game features overview drawText("PROCEDURAL CHIPTUNE MUSIC", 400, 300, 1, COLOR_WHITE, true, false); drawText("NEON PARTICLE SHATTER EFFECTS", 400, 320, 1, COLOR_WHITE, true, false); drawText("REAL-TIME AUDIO OSCILLOSCOPE", 400, 340, 1, COLOR_WHITE, true, false); drawText("PRESS ENTER TO START", 400, 420, 2, cueColor, true, true); // Controls list drawText("CONTROLS", 400, 475, 1, COLOR_CYAN, true, false); drawText("LEFT RIGHT MOVEMENT DOWN SOFT DROP SPACE HARD DROP", 400, 495, 1, {170, 180, 200, 255}, true, false); drawText("UP X ROTATE CLOCKWISE Z ROTATE COUNTER C HOLD PIECE", 400, 515, 1, {170, 180, 200, 255}, true, false); drawText("P PAUSE GAME R RESTART GAME", 400, 535, 1, {170, 180, 200, 255}, true, false); drawText("CREATED BY MATTEO BENEDETTO (ENNE2)", 400, 575, 1, {255, 200, 0, 255}, true, false); } else if (game.getState() == GameState::PAUSED) { SDL_Color overlayCol = {10, 11, 16, 150}; SDL_Rect overlay = {0, 0, 800, 600}; fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); float flash = std::sin(mTime * 4.0f) * 0.5f + 0.5f; SDL_Color flashCyan = {static_cast(flash * 100), 220, 255, 255}; drawText("PAUSED", 400, 260, 4, flashCyan, true, true); drawText("PRESS P TO RESUME", 400, 310, 2, COLOR_WHITE, true, false); } // 9. Upload backbuffer surface to streaming texture on GPU #ifdef MIYOO_BUILD // Scale the 800x600 software backbuffer to target screen size in software RAM SDL_BlitScaled(mBackbufferSurface, nullptr, mScaledSurface, nullptr); SDL_UpdateTexture(mBackbufferTexture, nullptr, mScaledSurface->pixels, mScaledSurface->pitch); #else SDL_UpdateTexture(mBackbufferTexture, nullptr, mBackbufferSurface->pixels, mBackbufferSurface->pitch); #endif // Clear the hardware renderer (Miyoo requires this before rendering copy) SDL_RenderClear(mRenderer); // Draw the texture containing our fully rendered software scene // If screenshake is active, apply the shake offset directly to the destination rectangle! #ifdef MIYOO_BUILD SDL_Rect destRect = { static_cast(camDX * static_cast(mTargetW) / 800.0f), static_cast(camDY * static_cast(mTargetH) / 600.0f), mTargetW, mTargetH }; #else SDL_Rect destRect = { camDX, camDY, mTargetW, mTargetH }; #endif SDL_RenderCopy(mRenderer, mBackbufferTexture, nullptr, &destRect); // Present the frame! SDL_RenderPresent(mRenderer); } void Renderer::takeScreenshot(const std::string& filename) { if (mBackbufferSurface == nullptr) return; SDL_SaveBMP(mBackbufferSurface, filename.c_str()); }