← index
libmx2/libmx/gl_matrix_psyche/matrix_psyche.cpp
Source: libmx2/libmx/gl_matrix_psyche/matrix_psyche.cpp
#include"mx.hpp"
#include"argz.hpp"

#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#include <GLES3/gl3.h>
#endif
#include<random>
#include<deque>
#include"gl.hpp"
#include"loadpng.hpp"
#include"model.hpp"
#define CHECK_GL_ERROR() \
{ GLenum err = glGetError(); \
if (err != GL_NO_ERROR) \
printf("OpenGL Error: %d at %s:%d\n", err, __FILE__, __LINE__); }

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#ifdef __EMSCRIPTEN__
const char *vSource = R"(#version 300 es
precision highp float;

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;

void main() {
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;
    TexCoord = aTexCoord;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
)";

const char *fSource = R"(#version 300 es
precision highp float;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform float time;
uniform vec3 xorColor;
uniform vec3 viewPos;

out vec4 FragColor;

vec4 matrixEffect(vec4 texColor, vec2 texCoord, float time) {
    if (texColor.r < 0.03 && texColor.g < 0.03 && texColor.b < 0.03) {
        discard;
    }
    
    float scanLine = sin(texCoord.y * 100.0) * 0.04 + 0.96;
    texColor.rgb *= scanLine;
    
    float flicker = sin(time * 2.1) * 0.01 + 0.99;
    texColor.rgb *= flicker;
    
    float distortion = sin(texCoord.y * 10.0 + time * 0.5) * 0.001;
    texColor.rgb += distortion;
    
    texColor.g = min(1.0, texColor.g * 1.15);
    
    float noise = fract(sin(dot(texCoord, vec2(12.9898, 78.233) * time * 0.001)) * 43758.5453);
    texColor.rgb += (noise * 0.03 - 0.015);
    
    float aberration = 0.002;
    texColor.r = texture(texture1, vec2(texCoord.x + aberration, texCoord.y)).r;
    texColor.g = texture(texture1, texCoord).g;
    texColor.b = texture(texture1, vec2(texCoord.x - aberration, texCoord.y)).b;
    
    float brightness = dot(texColor.rgb, vec3(0.2126, 0.7152, 0.0722));
    float glow = smoothstep(0.3, 1.0, brightness) * 0.5;
    texColor.rgb += vec3(0.0, glow, 0.0) * 2.0;
    
    if (mod(time * 10.0, 20.0) < 0.2) {
        float glitchLine = step(0.8, fract(texCoord.y * 20.0 + time));
        texColor.rgb = mix(texColor.rgb, vec3(1.0, 1.0, 1.0), glitchLine * 0.2);
    }
    
    return texColor;
}

void main() {
    vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
    vec3 norm = normalize(Normal);
    float diff = max(dot(norm, lightDir), 0.0);
    
    vec2 animatedTexCoord = TexCoord;
    animatedTexCoord.y += sin(time * 0.5 + TexCoord.x * 10.0) * 0.02;
    
    animatedTexCoord.x += cos(time * 0.3 + TexCoord.y * 15.0) * 0.01;
    
    vec4 texColor = texture(texture1, animatedTexCoord);
    
    texColor = matrixEffect(texColor, animatedTexCoord, time);
    
    vec3 ambient = 0.3 * texColor.rgb;
    vec3 diffuse = diff * 0.7 * texColor.rgb;
    
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    vec3 specular = 0.3 * spec * vec3(0.0, 1.0, 0.0);
    
    float ambientPulse = 1.0 + sin(time * 2.0) * 0.1;
    vec3 result = (ambient * ambientPulse) + diffuse + specular;
    
    result.g = min(1.0, result.g * 1.2);
    if(texColor.r < 0.1 && texColor.g < 0.1 && texColor.b < 0.1) {
        discard;
    }

    FragColor = vec4(sin(result * xorColor * time), texColor.a);
}
)";
#else
const char *vSource = R"(#version 330 core

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;

void main() {
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;
    TexCoord = aTexCoord;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
)";

const char *fSource = R"(#version 330 core

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform float time;
uniform vec3 xorColor;
uniform vec3 viewPos;

out vec4 FragColor;

vec4 matrixEffect(vec4 texColor, vec2 texCoord, float time) {
    if (texColor.r < 0.03 && texColor.g < 0.03 && texColor.b < 0.03) {
        discard;
    }
    
    float scanLine = sin(texCoord.y * 100.0) * 0.04 + 0.96;
    texColor.rgb *= scanLine;
    
    float flicker = sin(time * 2.1) * 0.01 + 0.99;
    texColor.rgb *= flicker;
    
    float distortion = sin(texCoord.y * 10.0 + time * 0.5) * 0.001;
    texColor.rgb += distortion;
    
    texColor.g = min(1.0, texColor.g * 1.15);
    
    float noise = fract(sin(dot(texCoord, vec2(12.9898, 78.233) * time * 0.001)) * 43758.5453);
    texColor.rgb += (noise * 0.03 - 0.015);
    
    float aberration = 0.002;
    texColor.r = texture(texture1, vec2(texCoord.x + aberration, texCoord.y)).r;
    texColor.g = texture(texture1, texCoord).g;
    texColor.b = texture(texture1, vec2(texCoord.x - aberration, texCoord.y)).b;
    
    float brightness = dot(texColor.rgb, vec3(0.2126, 0.7152, 0.0722));
    float glow = smoothstep(0.3, 1.0, brightness) * 0.5;
    texColor.rgb += vec3(0.0, glow, 0.0) * 2.0;
    
    if (mod(time * 10.0, 20.0) < 0.2) {
        float glitchLine = step(0.8, fract(texCoord.y * 20.0 + time));
        texColor.rgb = mix(texColor.rgb, vec3(1.0, 1.0, 1.0), glitchLine * 0.2);
    }
    
    return texColor;
}

void main() {
    vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
    vec3 norm = normalize(Normal);
    float diff = max(dot(norm, lightDir), 0.0);
    
    vec2 animatedTexCoord = TexCoord;
    animatedTexCoord.y += sin(time * 0.5 + TexCoord.x * 10.0) * 0.02;
    
    animatedTexCoord.x += cos(time * 0.3 + TexCoord.y * 15.0) * 0.01;
    
    vec4 texColor = texture(texture1, animatedTexCoord);
    
    texColor = matrixEffect(texColor, animatedTexCoord, time);
    
    vec3 ambient = 0.3 * texColor.rgb;
    vec3 diffuse = diff * 0.7 * texColor.rgb;
    
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    vec3 specular = 0.3 * spec * vec3(0.0, 1.0, 0.0);
    
    float ambientPulse = 1.0 + sin(time * 2.0) * 0.1;
    vec3 result = (ambient * ambientPulse) + diffuse + specular;
    
    result.g = min(1.0, result.g * 1.2);

    if(texColor.r < 0.1 && texColor.g < 0.1 && texColor.b < 0.1) {
        discard;
    }

    FragColor = vec4(sin(result * xorColor * time), texColor.a);
}
)";
#endif


float generateRandomFloat(float min, float max) {
    static std::random_device rd; 
    static std::default_random_engine eng(rd()); 
    std::uniform_real_distribution<float> dist(min, max);
    return dist(eng);
}

class Matrix {
public:
    mx::Font the_font;
    std::unordered_map<std::string, SDL_Surface*> char_textures;
    const size_t MAX_CACHE_SIZE = 1000; 
    std::deque<std::string> cache_access_order; 
    Matrix() = default;
    ~Matrix() {
        for(auto &it : char_textures) {
            SDL_FreeSurface(it.second);
        }
    }       

    void load(gl::GLWindow *win) {
        the_font.loadFont(win->util.getFilePath("data/keifont.ttf"), 36);
    }

    std::string unicodeToUTF8(int codepoint) {
        std::string utf8;
        if (codepoint <= 0x7F) {
            utf8 += static_cast<char>(codepoint);
        } else if (codepoint <= 0x7FF) {
            utf8 += static_cast<char>((codepoint >> 6) | 0xC0);
            utf8 += static_cast<char>((codepoint & 0x3F) | 0x80);
        } else if (codepoint <= 0xFFFF) {
            utf8 += static_cast<char>((codepoint >> 12) | 0xE0);
            utf8 += static_cast<char>(((codepoint >> 6) & 0x3F) | 0x80);
            utf8 += static_cast<char>((codepoint & 0x3F) | 0x80);
        } else if (codepoint <= 0x10FFFF) {
            utf8 += static_cast<char>((codepoint >> 18) | 0xF0);
            utf8 += static_cast<char>(((codepoint >> 12) & 0x3F) | 0x80);
            utf8 += static_cast<char>(((codepoint >> 6) & 0x3F) | 0x80);
            utf8 += static_cast<char>((codepoint & 0x3F) | 0x80);
        }
        return utf8;
    }

    std::vector<std::pair<int, int>> codepoint_ranges = {
        {0x3041, 0x3096},   
        {0x30A0, 0x30FF}
    };

    int getRandomCodepoint() {
        int range_index = rand() % codepoint_ranges.size();
        int start = codepoint_ranges[range_index].first;
        int end = codepoint_ranges[range_index].second;
        return start + rand() % (end - start + 1);
    }

    SDL_Color computeTrailColor(int trail_offset, int trail_length) {
        float intensity = 1.0f - (float)trail_offset / (float)trail_length;
        if (intensity < 0.0f) intensity = 0.0f;

        Uint8 alpha = static_cast<Uint8>(255 * intensity);
        if (alpha < 50) alpha = 50;

        Uint8 green = static_cast<Uint8>(255 * intensity);
        if (green < 100) green = 100; 
        SDL_Color color = {0, green, 0, alpha};
        return color;
    }

    SDL_Surface* createMatrixRain(TTF_Font* font, int screen_width, int screen_height) {
        SDL_Surface* matrix_surface = SDL_CreateRGBSurfaceWithFormat(
            0, screen_width, screen_height, 32, SDL_PIXELFORMAT_RGBA32);
        
        if (!matrix_surface) {
            printf("Failed to create surface: %s\n", SDL_GetError());
            return nullptr;
        }
        
        SDL_FillRect(matrix_surface, NULL, SDL_MapRGBA(matrix_surface->format, 0, 0, 0, 0));
        
        int char_width = 0;
        int char_height = 0;
        TTF_SizeUTF8(font, "A", &char_width, &char_height);
        
        int num_columns = (screen_width / char_width) * 1.5;
        int num_rows = screen_height / char_height + 1;
        
        static std::vector<float> fall_positions(num_columns, 0.0f);
        static std::vector<float> fall_speeds(num_columns, 0.0f);
        static std::vector<int> trail_lengths(num_columns, 0);
        static std::vector<float> column_brightness(num_columns, 1.0f);
        static std::vector<bool> is_highlight_column(num_columns, false);
        static std::vector<int> mutation_counters(num_columns, 0);
        static Uint32 last_time = 0;
        
        if (fall_positions[0] == 0.0f) {
            for (int col = 0; col < num_columns; ++col) {
                fall_positions[col] = static_cast<float>(rand() % num_rows);
                fall_speeds[col] = generateRandomFloat(2.0f, 8.0f);
                trail_lengths[col] = rand() % 25 + 10;
                is_highlight_column[col] = (rand() % 20 == 0);
                column_brightness[col] = generateRandomFloat(0.8f, 1.2f);
            }
            last_time = SDL_GetTicks();
        }
        
        Uint32 current_time = SDL_GetTicks();
        float delta_time = (current_time - last_time) / 1000.0f;
        last_time = current_time;
        
        for (int col = 0; col < num_columns; ++col) {
            if (rand() % 100 == 0) {
                fall_speeds[col] = generateRandomFloat(2.0f, 8.0f);
            }
            
            fall_positions[col] += fall_speeds[col] * delta_time;
            
            if (fall_positions[col] >= num_rows) {
                fall_positions[col] -= num_rows;
                trail_lengths[col] = rand() % 25 + 10;
                is_highlight_column[col] = (rand() % 20 == 0);
                column_brightness[col] = generateRandomFloat(0.8f, 1.2f);
            }
            
            for (int trail_offset = 0; trail_offset < trail_lengths[col]; ++trail_offset) {
                int row = static_cast<int>(fall_positions[col] - trail_offset + num_rows) % num_rows;
                
                int random_char_code;
                if (trail_offset == 0 || (rand() % 50 == 0)) {
                    random_char_code = getRandomCodepoint();
                    mutation_counters[col] = (mutation_counters[col] + 1) % 5;
                } else {
                    if (rand() % 30 == 0) {
                        random_char_code = getRandomCodepoint();
                    } else {
                        random_char_code = getRandomCodepoint();
                    }
                }
                
                std::string random_char = unicodeToUTF8(random_char_code);
                
                SDL_Color color;
                if (trail_offset == 0) {
                    if (is_highlight_column[col]) {
                        color = {255, 255, 255, 255};
                    } else {
                        color = {180, 255, 180, 255};
                    }
                } else {
                    color = computeFilmLikeTrailColor(trail_offset, trail_lengths[col], column_brightness[col], is_highlight_column[col]);
                }
                
                SDL_Surface* char_surface = nullptr;
                std::string cache_key = random_char + "_" + 
                                      std::to_string(color.r) + "_" + 
                                      std::to_string(color.g) + "_" + 
                                      std::to_string(color.b) + "_" + 
                                      std::to_string(color.a);

                if (char_textures.find(cache_key) == char_textures.end()) {
                    if (char_textures.size() >= MAX_CACHE_SIZE && !cache_access_order.empty()) {
                        std::string oldest_key = cache_access_order.front();
                        cache_access_order.pop_front();
                        SDL_FreeSurface(char_textures[oldest_key]);
                        char_textures.erase(oldest_key);
                    }
                    
                    char_surface = TTF_RenderUTF8_Blended(font, random_char.c_str(), color);
                    if (char_surface) {
                        char_textures[cache_key] = char_surface;
                        cache_access_order.push_back(cache_key);
                    } else {
                        continue; 
                    }
                } else {
                    auto it = std::find(cache_access_order.begin(), cache_access_order.end(), cache_key);
                    if (it != cache_access_order.end()) {
                        cache_access_order.erase(it);
                    }
                    cache_access_order.push_back(cache_key);
                    char_surface = char_textures[cache_key];
                }

                SDL_Rect dst_rect = {col * char_width, row * char_height, char_width, char_height};
                SDL_BlitSurface(char_surface, nullptr, matrix_surface, &dst_rect);
            }
        }
        
        return matrix_surface;
    }

    SDL_Color computeFilmLikeTrailColor(int trail_offset, int trail_length, float brightness_multiplier, bool is_highlight) {
        float intensity = 1.0f - (float)trail_offset / (float)trail_length;
        intensity = powf(intensity, 1.7f);
        if (intensity < 0.0f) intensity = 0.0f;
        
        intensity *= brightness_multiplier;
        
        Uint8 alpha = static_cast<Uint8>(255 * intensity);
        if (alpha < 30) alpha = 30;
        
        Uint8 green = static_cast<Uint8>(255 * intensity);
        Uint8 red = static_cast<Uint8>(120 * intensity * 0.4f);
        Uint8 blue = static_cast<Uint8>(120 * intensity * 0.2f);
        
        if (is_highlight && trail_offset < trail_length / 3) {
            red = static_cast<Uint8>(200 * intensity);
            blue = static_cast<Uint8>(180 * intensity * 0.7f);
        }
        
        if (rand() % 10 == 0) {
            float flicker = generateRandomFloat(0.8f, 1.2f);
            red = static_cast<Uint8>(std::min(255.0f, red * flicker));
            green = static_cast<Uint8>(std::min(255.0f, green * flicker));
            blue = static_cast<Uint8>(std::min(255.0f, blue * flicker));
        }
        
        SDL_Color color = {red, green, blue, alpha};
        return color;
    }
};
    
class Game : public gl::GLObject {
public:
    Matrix matrix;
    Game() = default;
    virtual ~Game() override {
        if(texture != 0) {
            glDeleteTextures(1, &texture);
        }
    }

    GLuint texture = 0;

    void load(gl::GLWindow *win) override {
        font.loadFont(win->util.getFilePath("data/font.ttf"), 24);
        if(!shader_program.loadProgramFromText(vSource, fSource)) {
            throw mx::Exception("Failed to load shader program");
        }
        if(!cube.openModel(win->util.getFilePath("data/cube.mxmod.z"))) {
            throw mx::Exception("Failed to load model");
        }
        cube.setShaderProgram(&shader_program, "texture1");
        shader_program.useProgram();
        matrix.load(win);
        SDL_Surface *surf = matrix.createMatrixRain(matrix.the_font.wrapper().unwrap(), 1440, 1080); 
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surf->w, surf->h, 0, 
                    GL_RGBA, GL_UNSIGNED_BYTE, surf->pixels);
        glGenerateMipmap(GL_TEXTURE_2D);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        SDL_FreeSurface(surf);
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
    }

    void draw(gl::GLWindow *win) override {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE);
        
        Uint32 currentTime = SDL_GetTicks();
        float deltaTime = (currentTime - lastUpdateTime) / 1000.0f; 
        lastUpdateTime = currentTime;
        
        update(deltaTime);
        
        SDL_Surface *matrix_surface = matrix.createMatrixRain(matrix.the_font.wrapper().unwrap(), 1440, 1080);
        mx::Texture::flipSurface(matrix_surface);
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, matrix_surface->w, matrix_surface->h, GL_RGBA, GL_UNSIGNED_BYTE, matrix_surface->pixels);
        glGenerateMipmap(GL_TEXTURE_2D);
        SDL_FreeSurface(matrix_surface);
        
        if (!insideCube) {
            rotation_x += deltaTime * 15.0f; 
            rotation_y += deltaTime * 20.0f;
        }
        
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
        model = glm::rotate(model, glm::radians(rotation_y), glm::vec3(0.0f, 1.0f, 0.0f));
        model = glm::rotate(model, glm::radians(rotation_x), glm::vec3(1.0f, 0.0f, 0.0f));
        
        glm::mat4 view;
        glm::vec3 viewPos;
        float nearPlane = 0.01f; 
        
        if (insideCube) {
            viewPos = glm::vec3(0.0f, 0.0f, 0.0f);
            nearPlane = 0.05f;
            glm::vec3 direction;
            direction.x = cos(glm::radians(cameraPitch)) * cos(glm::radians(cameraYaw));
            direction.y = sin(glm::radians(cameraPitch));
            direction.z = cos(glm::radians(cameraPitch)) * sin(glm::radians(cameraYaw));
            glCullFace(GL_FRONT);
            view = glm::lookAt(
                viewPos,
                viewPos + glm::normalize(direction),
                glm::vec3(0.0f, 1.0f, 0.0f)
            );
        } else {
            viewPos = glm::vec3(0.0f, 0.0f, 12.0f); 
            glCullFace(GL_BACK);
            view = glm::lookAt(
                viewPos,
                glm::vec3(0.0f, 0.0f, 0.0f),
                glm::vec3(0.0f, 1.0f, 0.0f)
            );
        }
        
        float aspectRatio = static_cast<float>(win->w) / static_cast<float>(win->h);
        glm::mat4 projection = glm::perspective(
            glm::radians(insideCube ? 90.0f : 45.0f), 
            aspectRatio, 
            nearPlane,  
            400.0f  
        );
        
        shader_program.useProgram();
        shader_program.setUniform("model", model);
        shader_program.setUniform("view", view);
        shader_program.setUniform("projection", projection);
        shader_program.setUniform("texture1", 0);
        shader_program.setUniform("time", static_cast<float>(currentTime) / 1000.0f);
        shader_program.setUniform("viewPos", viewPos);
        shader_program.setUniform("xorColor", glm::vec3(generateRandomFloat(0.4f, 1.5f), generateRandomFloat(0.4f, 1.5f) , generateRandomFloat(0.4f, 1.5f)));
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture);
        cube.drawArraysWithTexture(texture, "texture1");
    
        glDisable(GL_DEPTH_TEST);
        glDisable(GL_BLEND);
        glDisable(GL_CULL_FACE);

        win->text.setColor({255, 255, 255, 255});
        if(menu_shown && insideCube == false) {
            win->text.printText_Solid(font, 25.0f, 25.0f, "Matrix Cube - FPS: " + std::to_string(int(1.0f/deltaTime)));        
            std::string cameraMode = insideCube ? "Inside" : "Outside";
            win->text.printText_Solid(font, 25.0f, 60.0f, "Press ENTER to toggle " + cameraMode + " mode");
            win->text.printText_Solid(font, 25.0f, 95.0f, "Use Arrow Keys to look around");
        }
    }
    
    void event(gl::GLWindow *win, SDL_Event &e) override {
        if (e.type == SDL_KEYDOWN) {
            switch(e.key.keysym.sym) {
                case SDLK_RETURN: 
                    insideCube = !insideCube;
                    if (insideCube) {
                        cameraPitch = 0.0f;
                        cameraYaw = 0.0f;
                    }
                    break;
                case SDLK_SPACE:
                    menu_shown = !menu_shown;
                    break;
            }
        }  else if (e.type == SDL_FINGERDOWN) {
            touchActive = true;
            lastTouchX = static_cast<int>(e.tfinger.x * win->w);
            lastTouchY = static_cast<int>(e.tfinger.y * win->h);
            
            int tapX = static_cast<int>(e.tfinger.x * win->w);
            int tapY = static_cast<int>(e.tfinger.y * win->h);
            Uint32 currentTime = SDL_GetTicks();
            
            int dx = tapX - lastTapX;
            int dy = tapY - lastTapY;
            int distanceSquared = dx*dx + dy*dy;
            
            if (static_cast<int>(currentTime - lastTapTime) < DOUBLE_TAP_TIME && 
                distanceSquared < DOUBLE_TAP_DISTANCE * DOUBLE_TAP_DISTANCE) {
                
                insideCube = !insideCube;
                if (insideCube) {
                    cameraPitch = 0.0f;
                    cameraYaw = 0.0f;
                }
            }
            
            lastTapTime = currentTime;
            lastTapX = tapX;
            lastTapY = tapY;
        }
        else if (e.type == SDL_FINGERUP) {
            touchActive = false;
        }
        else if (e.type == SDL_FINGERMOTION && touchActive) {
            if (insideCube) {
                int touchX = static_cast<int>(e.tfinger.x * win->w);
                int touchY = static_cast<int>(e.tfinger.y * win->h);
                int dx = touchX - lastTouchX;
                int dy = touchY - lastTouchY;
                float touchSensitivity = 0.2f;
                cameraYaw += dx * touchSensitivity;
                cameraPitch -= dy * touchSensitivity; 
                if (cameraPitch > 89.0f) cameraPitch = 89.0f;
                if (cameraPitch < -89.0f) cameraPitch = -89.0f;       
                if (cameraYaw < 0.0f) cameraYaw += 360.0f;
                if (cameraYaw > 360.0f) cameraYaw -= 360.0f;
                lastTouchX = touchX;
                lastTouchY = touchY;
            }
        }
    }
    void update(float deltaTime) {
        const Uint8* keyState = SDL_GetKeyboardState(NULL);
        
        if (insideCube) {
            float rotationSpeed = 100.0f * deltaTime;
            
            if (keyState[SDL_SCANCODE_LEFT]) {
                cameraYaw -= rotationSpeed;
            }
            
            if (keyState[SDL_SCANCODE_RIGHT]) {
                cameraYaw += rotationSpeed;
            }
            
            if (keyState[SDL_SCANCODE_UP]) {
                cameraPitch += rotationSpeed;
            }
            
            if (keyState[SDL_SCANCODE_DOWN]) {
                cameraPitch -= rotationSpeed;
            }
            
            if (cameraPitch > 89.0f) cameraPitch = 89.0f;
            if (cameraPitch < -89.0f) cameraPitch = -89.0f;
            if (cameraYaw < 0.0f) cameraYaw += 360.0f;
            if (cameraYaw > 360.0f) cameraYaw -= 360.0f;
            
            static float fallSpeed = 0.5f;
            
            if (rand() % 100 == 0) {
                fallSpeed = generateRandomFloat(0.3f, 1.0f);
            }
            
            cameraPosition.y -= fallSpeed * deltaTime;
            
            if (cameraPosition.y < -5.0f) {
                cameraPosition.y = 5.0f;
            }
        } else {
            cameraPosition = glm::vec3(0.0f, 0.0f, 12.0f);
        }
    }
private:
    mx::Font font;
    Uint32 lastUpdateTime = SDL_GetTicks();
    mx::Model cube;
    gl::ShaderProgram shader_program;
    float rotation_x = 0.0f;
    float rotation_y = 0.0f;
    bool insideCube = false;
    float cameraYaw = 0.0f;
    float cameraPitch = 0.0f;
    glm::vec3 cameraPosition = glm::vec3(0.0f, 0.0f, 3.0f);
    bool touchActive = false;
    int lastTouchX = 0;
    int lastTouchY = 0;
    Uint32 lastTapTime = 0;
    int lastTapX = 0;
    int lastTapY = 0;
    const int DOUBLE_TAP_TIME = 300; 
    const int DOUBLE_TAP_DISTANCE = 30; 
    bool menu_shown = true;
};

class MainWindow : public gl::GLWindow {
public:
    MainWindow(std::string path, int tw, int th) : gl::GLWindow("Enter the Matrix", tw, th) {
        setPath(path);
        setObject(new Game());
        object->load(this);
    }
    
    ~MainWindow() override {}
    
    virtual void event(SDL_Event &e) override {}
    
    virtual void draw() override {
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glEnable(GL_DEPTH_TEST);
        glViewport(0, 0, w, h);
        object->draw(this);
        swap();
        delay();
    }
};

MainWindow *main_w = nullptr;

void eventProc() {
    main_w->proc();
}

int main(int argc, char **argv) {
#ifdef __EMSCRIPTEN__
    try {
    MainWindow main_window("", 1920, 1080);
    main_w =&main_window;
    emscripten_set_main_loop(eventProc, 0, 1);
    } catch(const mx::Exception &e) {
        std::cerr << "Error: " << e.text() << "\n";
        exit(EXIT_FAILURE);
    }
#else
    Arguments args = proc_args(argc, argv);
    try {
        MainWindow main_window(args.path, args.width, args.height);
	if(args.fullscreen)
		main_window.setFullScreen(true);
        main_window.loop();
    } catch(const mx::Exception &e) {
        mx::system_err << "mx: Exception: " << e.text() << "\n";
        mx::system_err.flush();
        exit(EXIT_FAILURE);
    }
#endif
    return 0;
}