UnnamedPythonMicroConsole/src/Graphics/Graphics.cpp
Xenthera e23be1cd58 StateManager and States now have their own reference to Graphics instead of being passed into Draw(), Added image class and test images
Image class is just array of palette indices converted from the rgb data of any image by using euclidian distance between palette rgb and pixel rgb. Works well.
2024-05-22 16:09:19 -04:00

579 lines
18 KiB
C++

//
// Created by Bobby Lucero on 4/20/24.
//
#include "Graphics.h"
#include "PycronImage.h"
#include "Font.h"
#include "../Utilities.h"
#include "../StateManager.h"
#include <fstream>
#include <raymath.h>
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<int>(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<float>(vm, args[0]);
float y = pkpy::py_cast<float>(vm, args[1]);
float paletteIndex = pkpy::py_cast<float>(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<float>(vm, args[0]);
float y = pkpy::py_cast<float>(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<float>(vm, args[0]);
float y = pkpy::py_cast<float>(vm, args[1]);
float radius = pkpy::py_cast<float>(vm, args[2]);
float paletteIndex = pkpy::py_cast<float>(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<float>(vm, args[0]);
float y = pkpy::py_cast<float>(vm, args[1]);
float width = pkpy::py_cast<float>(vm, args[2]);
float height = pkpy::py_cast<float>(vm, args[3]);
float paletteIndex = pkpy::py_cast<float>(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<float>(vm, args[0]);
float y1 = pkpy::py_cast<float>(vm, args[1]);
float x2 = pkpy::py_cast<float>(vm, args[2]);
float y2 = pkpy::py_cast<float>(vm, args[3]);
float x3 = pkpy::py_cast<float>(vm, args[4]);
float y3 = pkpy::py_cast<float>(vm, args[5]);
float paletteIndex = pkpy::py_cast<float>(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<pkpy::Str&>(vm, result).str();
float x = pkpy::py_cast<float>(vm, args[1]);
float y = pkpy::py_cast<float>(vm, args[2]);
float paletteIndex = pkpy::py_cast<float>(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]);
}
}