// // Created by Bobby Lucero on 4/20/24. // #include "Graphics.h" #include "PycronImage.h" #include "Font.h" #include "../Utilities.h" #include "../StateManager.h" #include #include Graphics::Graphics(int screenWidth, int screenHeight, int startupScale) : m_screenWidth(screenWidth), m_screenHeight(screenHeight){ m_NormalFont = new PixelFont(5, 7, "resources/fonts/main.font"); m_currentFont = m_NormalFont; m_startupScreenWidth = screenWidth * startupScale; m_startupScreenHeight = screenHeight * startupScale; m_windowWidth = m_startupScreenWidth; m_windowHeight = m_startupScreenHeight; SetConfigFlags(FLAG_WINDOW_RESIZABLE); InitWindow(m_startupScreenWidth, m_startupScreenHeight, "test"); SetTargetFPS(60); m_virtualScreen = LoadRenderTexture(screenWidth, screenHeight); m_origin = {0,0}; m_virtualScreenLocalBounds = {0.0f, 0.0f, (float)m_virtualScreen.texture.width, (float)m_virtualScreen.texture.height }; m_virtualScreenWindowBounds = {0.0f, 0.0f, (float)m_windowWidth, (float)m_windowHeight}; m_virtualScreenImageBuffer = GenImageColor(m_screenWidth, m_screenHeight, BLACK); m_virtualScreenColorBuffer = {}; m_virtualScreenColorBuffer.resize(screenWidth * screenHeight); calculateScreenPositionInWindow(); } Graphics::~Graphics() { delete m_NormalFont; m_NormalFont = nullptr; m_currentFont = nullptr; } void Graphics::draw(StateManager* stateManager) { m_windowShouldClose = WindowShouldClose(); if (IsWindowResized()) { m_windowWidth = GetScreenWidth(); m_windowHeight = GetScreenHeight(); calculateScreenPositionInWindow(); } stateManager->Draw(); copyBufferToGPU(); renderVirtualScreen(); } void Graphics::copyBufferToGPU() { Color* pixel_data = LoadImageColors(m_virtualScreenImageBuffer); for (int i = 0; i < m_screenWidth * m_screenHeight; ++i) { pixel_data[i] = GetColor(m_paletteByID[m_virtualScreenColorBuffer[i]]); } UpdateTexture(m_virtualScreen.texture, pixel_data); UnloadImageColors(pixel_data); } void Graphics::renderVirtualScreen() { BeginDrawing(); ClearBackground(BLACK); DrawTexturePro(m_virtualScreen.texture, m_virtualScreenLocalBounds, m_virtualScreenWindowBounds, m_origin, 0.0f, WHITE); DrawText(std::to_string(GetFPS()).c_str(), 10, 10, 30, YELLOW); EndDrawing(); } void Graphics::loadPalette(std::string path) { std::ifstream paletteFile(path); std::string line; if(paletteFile.is_open()){ int idx = 0; while(getline(paletteFile, line)){ int color = stoi(line, nullptr, 16); //Palette.push_back(); color = color << 8 | 0xFF; m_paletteByID.insert({idx, color}); idx++; } paletteFile.close(); } } void Graphics::calculateScreenPositionInWindow() { float virtualAspectRatio = (float)m_screenWidth / (float)m_screenHeight; float windowAspectRatio = (float)m_windowWidth / (float)m_windowHeight; if(windowAspectRatio > virtualAspectRatio) { m_virtualScreenWindowBounds.height = (float)m_windowHeight; m_virtualScreenWindowBounds.width = m_virtualScreenWindowBounds.height * virtualAspectRatio; m_origin.x = -(m_windowWidth / 2.0f - (m_virtualScreenWindowBounds.width / 2.0f)); m_origin.y = 0; }else { m_virtualScreenWindowBounds.width = (float)m_windowWidth; m_virtualScreenWindowBounds.height = m_virtualScreenWindowBounds.width / virtualAspectRatio; m_origin.x = 0; m_origin.y = -(m_windowHeight / 2.0f - (m_virtualScreenWindowBounds.height / 2.0f)); } } int Graphics::rgbToID(int r, int g, int b) { r = Clamp(r, 0, 255); g = Clamp(g, 0, 255); b = Clamp(b, 0, 255); uint8_t idx = 0; Color c = GetColor(m_paletteByID[idx]); double minDist = std::pow(r - c.r, 2) + std::pow(g - c.g, 2) + std::pow(b - c.b, 2); for(const auto& paletteColor : m_paletteByID){ c = GetColor(paletteColor.second); double dist = std::pow(r - c.r, 2) + std::pow(g - c.g, 2) + std::pow(b - c.b, 2); if(dist < minDist){ minDist = dist; idx = paletteColor.first; } } return idx; } int Graphics::mouseX() { float x = GetMouseX(); float adjX = x + m_origin.x; return (int)(adjX / m_virtualScreenWindowBounds.width * m_screenWidth); } int Graphics::mouseY() { float y = GetMouseY(); float adjY = y + m_origin.y; return (int)(adjY / m_virtualScreenWindowBounds.height * m_screenHeight); } void Graphics::toggleFullScreen() { if (IsWindowFullscreen()) { ToggleFullscreen(); SetWindowSize(m_startupScreenWidth, m_startupScreenHeight); m_windowWidth = m_startupScreenWidth; m_windowHeight = m_startupScreenHeight; } else { int monitor = GetCurrentMonitor(); m_windowWidth = GetMonitorWidth(monitor); m_windowHeight = GetMonitorHeight(monitor); SetWindowSize(m_windowWidth, m_windowHeight); ToggleFullscreen(); } calculateScreenPositionInWindow(); } void Graphics::bindMethods(pkpy::VM *vm) { vm->bind(vm->builtins, "clear(color: int)", [this](pkpy::VM* vm, pkpy::ArgsView args){ int index = pkpy::py_cast(vm, args[0]); Clear(index); return vm->None; }); vm->bind(vm->builtins, "pixel(x: int, y: int, color: int)", [this](pkpy::VM* vm, pkpy::ArgsView args){ float x = pkpy::py_cast(vm, args[0]); float y = pkpy::py_cast(vm, args[1]); float paletteIndex = pkpy::py_cast(vm, args[2]); this->Pixel(x, y, paletteIndex); return vm->None; }); vm->bind(vm->builtins, "get_pixel(x: int, y: int) -> int", [this](pkpy::VM* vm, pkpy::ArgsView args){ float x = pkpy::py_cast(vm, args[0]); float y = pkpy::py_cast(vm, args[1]); return pkpy::py_var(vm, this->GetPixel(x, y)); }); vm->bind(vm->builtins, "circle(x: int, y: int, radius: float, color: int)", [this](pkpy::VM* vm, pkpy::ArgsView args){ float x = pkpy::py_cast(vm, args[0]); float y = pkpy::py_cast(vm, args[1]); float radius = pkpy::py_cast(vm, args[2]); float paletteIndex = pkpy::py_cast(vm, args[3]); this->Circle(x, y, radius, paletteIndex); return vm->None; }); vm->bind(vm->builtins, "rectangle(x: int, y: int, width: int, height: int, color: int)", [this](pkpy::VM* vm, pkpy::ArgsView args){ float x = pkpy::py_cast(vm, args[0]); float y = pkpy::py_cast(vm, args[1]); float width = pkpy::py_cast(vm, args[2]); float height = pkpy::py_cast(vm, args[3]); float paletteIndex = pkpy::py_cast(vm, args[4]); this->Rect(x, y, width, height, paletteIndex); return vm->None; }); vm->bind(vm->builtins, "triangle(x1: int, y1: int, x2: int, y2: int, x3: int, y3: int, color: int)", [this](pkpy::VM* vm, pkpy::ArgsView args){ float x1 = pkpy::py_cast(vm, args[0]); float y1 = pkpy::py_cast(vm, args[1]); float x2 = pkpy::py_cast(vm, args[2]); float y2 = pkpy::py_cast(vm, args[3]); float x3 = pkpy::py_cast(vm, args[4]); float y3 = pkpy::py_cast(vm, args[5]); float paletteIndex = pkpy::py_cast(vm, args[6]); this->Triangle(x1, y1, x2, y2, x3, y3, paletteIndex); return vm->None; }); vm->bind(vm->builtins, "text(t: string, x: int, y: int, color: int)", [this](pkpy::VM* vm, pkpy::ArgsView args){ pkpy::PyObject* func_str = vm->builtins->attr("str"); pkpy::PyObject* result = vm->call(func_str, args[0]); std::string s = pkpy::py_cast(vm, result).str(); float x = pkpy::py_cast(vm, args[1]); float y = pkpy::py_cast(vm, args[2]); float paletteIndex = pkpy::py_cast(vm, args[3]); this->Text(s, x, y, paletteIndex); return vm->None; }); } void Graphics::Clear(int paletteIndex) { for (int y = 0; y < m_screenHeight; ++y) { for (int x = 0; x < m_screenWidth; ++x) { Pixel(x, y, paletteIndex); } } } void Graphics::Pixel(int x, int y, int paletteIndex) { paletteIndex = Clamp(paletteIndex, 0, m_paletteByID.size() - 1); if(x < 0 || y < 0 || x >= m_screenWidth || y >= m_screenHeight) return; m_virtualScreenColorBuffer[y * m_screenWidth + x] = paletteIndex; } void Graphics::Circle(int x, int y, int radius, int paletteIndex) { Ellipse(x, y, radius, radius, paletteIndex); } void Graphics::Ellipse(int x, int y, int w, int h, int paletteIndex){ int x0 = x - w; int y0 = y - h; int x1 = x + w; int y1 = y + h; if(x0 > x1 || y0 > y1) return; int a = abs(x1 - x0), b = abs(y1 - y0), b1 = b & 1; int dx = 4 * (1 - a) * b * b, dy = 4 * (b1 + 1) * a * a; int err = dx + dy + b1 * a * a, e2; if (x0 > x1) { x0 = x1; x1 += a; } if (y0 > y1) y0 = y1; y0 += (b + 1) / 2; y1 = y0 - b1; a *= 8 * a; b1 = 8 * b * b; int prevY = y0; do { Pixel( x1, y0, paletteIndex); Pixel( x0, y0, paletteIndex); Pixel( x0, y1, paletteIndex); Pixel( x1, y1, paletteIndex); if(y0 != prevY && (y0 - y) != h){ h_line(x1, y0, x - (x1 - x) + 1, paletteIndex); h_line(x1, y - (y0 - y), x - (x1 - x) + 1, paletteIndex); } prevY = y0; e2 = 2 * err; if (e2 <= dy) { y0++; y1--; err += dy += a; } /* y step */ if (e2 >= dx || 2 * err > dy) { x0++; x1--; err += dx += b1; } /* x step */ } while (x0 <= x1); while (y0-y1 < b) { /* too early stop of flat ellipses a=1 */ Pixel( x0 - 1, y0, paletteIndex); /* -> finish tip of ellipse */ Pixel( x1 + 1, y0++, paletteIndex); Pixel( x0 - 1, y1, paletteIndex); Pixel( x1 + 1, y1--, paletteIndex); } h_line(x - w, y, x + w, paletteIndex); } void Graphics::EllipseBorder(int x, int y, int w, int h, int paletteIndex){ int x0 = x - w; int y0 = y - h; int x1 = x + w; int y1 = y + h; if(x0 > x1 || y0 > y1) return; int a = abs(x1 - x0), b = abs(y1 - y0), b1 = b & 1; int dx = 4 * (1 - a) * b * b, dy = 4 * (b1 + 1) * a * a; int err = dx + dy + b1 * a * a, e2; if (x0 > x1) { x0 = x1; x1 += a; } if (y0 > y1) y0 = y1; y0 += (b + 1) / 2; y1 = y0 - b1; a *= 8 * a; b1 = 8 * b * b; do { Pixel( x1, y0, paletteIndex); Pixel( x0, y0, paletteIndex); Pixel( x0, y1, paletteIndex); Pixel( x1, y1, paletteIndex); e2 = 2 * err; if (e2 <= dy) { y0++; y1--; err += dy += a; } /* y step */ if (e2 >= dx || 2 * err > dy) { x0++; x1--; err += dx += b1; } /* x step */ } while (x0 <= x1); while (y0-y1 < b) { /* too early stop of flat ellipses a=1 */ Pixel( x0 - 1, y0, paletteIndex); /* -> finish tip of ellipse */ Pixel( x1 + 1, y0++, paletteIndex); Pixel( x0 - 1, y1, paletteIndex); Pixel( x1 + 1, y1--, paletteIndex); } } void Graphics::Rect(int x, int y, int width, int height, int paletteIndex) { for (int i = 0; i < height; ++i) { h_line(x, y + i, x + width - 1, paletteIndex); } } void Graphics::RectBorder(int x, int y, int width, int height, int paletteIndex) { h_line(x, y, x + width - 1, paletteIndex); h_line(x, y + height - 1, x + width - 1, paletteIndex); v_line(x, y + 1, y + height - 2, paletteIndex); v_line(x + width - 1, y + 1, y + height - 2, paletteIndex); } void Graphics::Text(const std::string& s, int x, int y, int paletteIndex) { for (int i = 0; i < s.size(); ++i) { char c = s[i]; std::string bitData = m_currentFont->GetCharData((int)c); //std::cout << c << ": " << (int)c << " = " << bitData << std::endl; for (int j = 0; j < bitData.size(); ++j) { if(bitData[j] == '1') Pixel(x + (j % m_currentFont->GetWidth()) + ((m_currentFont->GetWidth() + 1) * i), y + (j / m_currentFont->GetWidth()), paletteIndex); } } } void Graphics::updateVMVars(pkpy::VM* vm) { vm->builtins->attr().set("mouseX", pkpy::py_var(vm, mouseX())); vm->builtins->attr().set("mouseY", pkpy::py_var(vm, mouseY())); vm->builtins->attr().set("width", pkpy::py_var(vm, m_screenWidth)); vm->builtins->attr().set("height", pkpy::py_var(vm, m_screenHeight)); } void Graphics::h_line(int x1, int y, int x2, int paletteIndex) { if(y < 0 || y >= m_screenHeight) return; int startX = x1; int endX = x2; if(x1 > x2){ startX = x2; endX = x1; } for (int i = startX; i <= endX; ++i) { Pixel(i, y, paletteIndex); } } void Graphics::v_line(int x, int y1, int y2, int paletteIndex) { if(x < 0 || x >= m_screenWidth) return; int startY = y1; int endY = y2; if(y1 > y2){ startY = y2; endY = y1; } for (int i = startY; i <= endY; ++i) { Pixel(x, i, paletteIndex); } } int Graphics::GetPixel(int x, int y) { if(x < 0 || y < 0 || x >= m_screenWidth || y >= m_screenHeight) return 0; return m_virtualScreenColorBuffer[y * m_screenWidth + x]; } // https://github.com/OneLoneCoder/olcPixelGameEngine/blob/master/olcPixelGameEngine.h#L2456 void Graphics::Triangle(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, int paletteIndex) { auto drawline = [&](int sx, int ex, int ny) { for (int i = sx; i <= ex; i++) Pixel(i, ny, paletteIndex); }; int t1x, t2x, y, minx, maxx, t1xp, t2xp; bool changed1 = false; bool changed2 = false; int signx1, signx2, dx1, dy1, dx2, dy2; int e1, e2; // Sort vertices if (y1 > y2) { std::swap(y1, y2); std::swap(x1, x2); } if (y1 > y3) { std::swap(y1, y3); std::swap(x1, x3); } if (y2 > y3) { std::swap(y2, y3); std::swap(x2, x3); } t1x = t2x = x1; y = y1; // Starting points dx1 = (int)(x2 - x1); if (dx1 < 0) { dx1 = -dx1; signx1 = -1; } else signx1 = 1; dy1 = (int)(y2 - y1); dx2 = (int)(x3 - x1); if (dx2 < 0) { dx2 = -dx2; signx2 = -1; } else signx2 = 1; dy2 = (int)(y3 - y1); if (dy1 > dx1) { std::swap(dx1, dy1); changed1 = true; } if (dy2 > dx2) { std::swap(dy2, dx2); changed2 = true; } e2 = (int)(dx2 >> 1); // Flat top, just process the second half if (y1 == y2) goto next; e1 = (int)(dx1 >> 1); for (int i = 0; i < dx1;) { t1xp = 0; t2xp = 0; if (t1x < t2x) { minx = t1x; maxx = t2x; } else { minx = t2x; maxx = t1x; } // process first line until y value is about to change while (i < dx1) { i++; e1 += dy1; while (e1 >= dx1) { e1 -= dx1; if (changed1) t1xp = signx1;//t1x += signx1; else goto next1; } if (changed1) break; else t1x += signx1; } // Move line next1: // process second line until y value is about to change while (1) { e2 += dy2; while (e2 >= dx2) { e2 -= dx2; if (changed2) t2xp = signx2;//t2x += signx2; else goto next2; } if (changed2) break; else t2x += signx2; } next2: if (minx > t1x) minx = t1x; if (minx > t2x) minx = t2x; if (maxx < t1x) maxx = t1x; if (maxx < t2x) maxx = t2x; drawline(minx, maxx, y); // Draw line from min to max points found on the y // Now increase y if (!changed1) t1x += signx1; t1x += t1xp; if (!changed2) t2x += signx2; t2x += t2xp; y += 1; if (y == y2) break; } next: // Second half dx1 = (int)(x3 - x2); if (dx1 < 0) { dx1 = -dx1; signx1 = -1; } else signx1 = 1; dy1 = (int)(y3 - y2); t1x = x2; if (dy1 > dx1) { // swap values std::swap(dy1, dx1); changed1 = true; } else changed1 = false; e1 = (int)(dx1 >> 1); for (int i = 0; i <= dx1; i++) { t1xp = 0; t2xp = 0; if (t1x < t2x) { minx = t1x; maxx = t2x; } else { minx = t2x; maxx = t1x; } // process first line until y value is about to change while (i < dx1) { e1 += dy1; while (e1 >= dx1) { e1 -= dx1; if (changed1) { t1xp = signx1; break; }//t1x += signx1; else goto next3; } if (changed1) break; else t1x += signx1; if (i < dx1) i++; } next3: // process second line until y value is about to change while (t2x != x3) { e2 += dy2; while (e2 >= dx2) { e2 -= dx2; if (changed2) t2xp = signx2; else goto next4; } if (changed2) break; else t2x += signx2; } next4: if (minx > t1x) minx = t1x; if (minx > t2x) minx = t2x; if (maxx < t1x) maxx = t1x; if (maxx < t2x) maxx = t2x; drawline(minx, maxx, y); if (!changed1) t1x += signx1; t1x += t1xp; if (!changed2) t2x += signx2; t2x += t2xp; y += 1; if (y > y3) return; } } PycronImage* Graphics::loadImage(std::string path) { Image img = LoadImage(path.c_str()); ImageRotateCW(&img); ImageFlipHorizontal(&img); if(img.width == 0 && img.height == 0) std::cerr << "Image at path " << path << " is empty\n"; Color* colors = LoadImageColors(img); auto* idImg = new PycronImage(img.width, img.height); for (int i = 0; i < idImg->data.size(); ++i) { if(colors[i].a == 0){ idImg->data[i] = -1; }else{ idImg->data[i] = (int8_t)rgbToID(colors[i].r, colors[i].g, colors[i].b); } } return idImg; } void Graphics::Img(PycronImage* img, int x, int y) { for (int i = 0; i < img->data.size(); ++i) { if(img->data[i] == -1) continue; Pixel(x + i / img->width, y + i % img->width, img->data[i]); } }