Skip to content

Instantly share code, notes, and snippets.

@MurageKibicho
Last active June 21, 2025 05:38
Show Gist options
  • Save MurageKibicho/c3f30f47b3b2e9eeb11064c5f5be86b7 to your computer and use it in GitHub Desktop.
Save MurageKibicho/c3f30f47b3b2e9eeb11064c5f5be86b7 to your computer and use it in GitHub Desktop.
SDL Bare minimum to get mouse scroll zoom working with OpenGL and Emscripten
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <SDL.h>
#include <SDL_opengles2.h>
#else
#include <SLD2/SDL.h>
#include <SDL2/SDL_opengles2.h>
#endif
#include <stdlib.h>
#include "cglm/cglm.h"
#include "cglm/call.h"
//Run : emcc HelloTriangle.c -s USE_SDL=2 -s FULL_ES2=1 -s WASM=1 -Icglm/include -DCGLM_HEADER_ONLY -o HelloTriangle.html
//Preview : emrun HelloTriangle.html
typedef struct event_handler_struct *EventHandler;
typedef struct camera_struct *Camera;
struct camera_struct
{
bool cameraUpdated;
bool windowResized;
int windowWidth;
int windowHeight;
GLfloat viewportX;
GLfloat viewportY;
GLfloat basePanX;
GLfloat basePanY;
GLfloat regPanX;
GLfloat regPanY;
GLfloat zoom;
GLfloat aspect;
GLfloat zoomMin;
GLfloat zoomMax;
};
struct event_handler_struct
{
//Window
SDL_Window *window;
Uint32 windowID;
//Mouse input
bool mouseButtonDown;
float mouseWheelDelta;
int mouseButtonDownX;
int mouseButtonDownY;
int mousePositionX;
int mousePositionY;
//Finger input
bool fingerDown;
int fingerDownX;
int fingerDownY;
int fingerDownID;
//Pinch input
bool pinchDown;
float pinchZoomDelta;
float pinchScale;
//Global Camera
Camera camera;
GLint shaderPan;
GLint shaderZoom;
GLint shaderAspect;
};
float clamp(float val, float lo, float hi) {
return (val < lo) ? lo : ((val > hi) ? hi : val);
}
Camera CreateCamera()
{
Camera camera = malloc(sizeof(struct camera_struct));
camera->cameraUpdated = false;
camera->windowResized = false;
camera->windowWidth = 0;
camera->windowHeight = 0;
camera->viewportX = 0.0f;
camera->viewportY = 0.0f;
camera->basePanX = 0.0f;
camera->basePanY = 0.0f;
camera->regPanX = 0.0f;
camera->regPanY = 0.0f;
camera->zoom = 1.0f;
camera->aspect = 1.0f;
camera->zoomMin = 0.1f;
camera->zoomMax = 10.0f;
return camera;
}
void Camera_SetWindowSize(Camera camera, int windowWidth, int windowHeight)
{
if(camera->windowWidth != windowWidth || camera->windowHeight != windowHeight)
{
camera->windowResized = true;
camera->cameraUpdated = true;
camera->windowWidth = windowWidth;
camera->windowHeight = windowHeight;
camera->viewportX = (float)windowWidth;
camera->viewportY = (float)windowHeight;
camera->aspect = (float)windowWidth / (float) windowHeight;
}
}
void Camera_DeviceToWorldCoords(Camera camera, float deviceX, float deviceY, float *worldX, float *worldY)
{
*worldX = deviceX / camera->zoom - camera->regPanX;
*worldY = deviceY / camera->aspect / camera->zoom - camera->regPanY;
}
void Camera_WindowToDeviceCoords(Camera camera, int windowX, int windowY, float *deviceX, float *deviceY)
{
float normalizedWindowX = windowX / (float)camera->windowWidth;
float normalizedWindowY = windowY / (float)camera->windowHeight;
*deviceX = (normalizedWindowX - 0.5f) * 2.0f;
*deviceY = (1.0f - normalizedWindowY - 0.5f) * 2.0f;
}
void Camera_WindowToWorldCoords(Camera camera, int windowX, int windowY, float *worldX, float *worldY)
{
float deviceX = 0.0f;float deviceY = 0.0f;
Camera_WindowToDeviceCoords(camera, windowX, windowY, &deviceX, &deviceY);
Camera_DeviceToWorldCoords(camera, windowX, windowY, &deviceX, &deviceY);
}
void Camera_SetZoomDelta(Camera camera, GLfloat zoomDelta)
{
camera->zoom = clamp(camera->zoom + zoomDelta, camera->zoomMin, camera->zoomMax);
camera->cameraUpdated = true;
}
void Camera_SetPanDelta(Camera camera, GLfloat deltaWorldX, GLfloat deltaWorldY)
{
camera->regPanX += deltaWorldX;
camera->regPanY += deltaWorldY;
camera->cameraUpdated = true;
}
void EventHandler_ResizeWindow(EventHandler eventHandler, int windowWidth, int windowHeight)
{
//Set OpenGL viewport
glViewport(0, 0, windowWidth, windowHeight);
Camera_SetWindowSize(eventHandler->camera, windowWidth, windowHeight);
}
EventHandler CreateEventHandler(char *windowName, int windowWidth, int windowHeight)
{
EventHandler eventHandler = malloc(sizeof(struct event_handler_struct));
//Initialize SDL window
eventHandler->window = SDL_CreateWindow(windowName, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,windowWidth, windowHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN);
eventHandler->windowID = SDL_GetWindowID(eventHandler->window);
//Initialize mouse handlers
eventHandler->mouseButtonDown = false;
eventHandler->mouseWheelDelta = 0.05f;
eventHandler->mouseButtonDownX = 0;
eventHandler->mouseButtonDownY = 0;
eventHandler->mousePositionX = 0;
eventHandler->mousePositionY = 0;
//Initialize finger handlers
eventHandler->fingerDown = false;
eventHandler->fingerDownX = 0.0f;
eventHandler->fingerDownY = 0.0f;
eventHandler->fingerDownID = 0;
//Initialize pinch handlers
eventHandler->pinchDown = false;
eventHandler->pinchZoomDelta = 0.001f;
eventHandler->pinchScale = 8.0f;
//Create camera
eventHandler->camera = CreateCamera();
eventHandler->shaderPan = 0;
eventHandler->shaderZoom = 0;
eventHandler->shaderAspect = 0;
//Create OpenGLES2 context inside SDL Window
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetSwapInterval(1);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
//Create OpenGL Context
SDL_GLContext openGLContext = SDL_GL_CreateContext(eventHandler->window);
if(openGLContext == NULL){SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create OpenGL Context: %s\n", SDL_GetError());exit(1);}
printf("INFO: GL version: %s\n", glGetString(GL_VERSION));
//OpenGL Set Background Color to white
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
//Call GL Clear
glClear(GL_COLOR_BUFFER_BIT);
//Change windowWidth and windowHeight to OpenGL window's actual size in pixels
int newWidth = 0;int newHeight=0;
SDL_GL_GetDrawableSize(eventHandler->window, &newWidth, &newHeight);
windowWidth = newWidth;windowHeight = newHeight;
printf("INFO: GL window size = %dx%d\n", windowWidth, windowHeight);
EventHandler_ResizeWindow(eventHandler, windowWidth, windowHeight);
return eventHandler;
}
void EventHandler_ZoomMouse(EventHandler eventHandler, bool mouseWheelDown)
{
float beforeZoomX = 0.0f;float beforeZoomY = 0.0f;
float afterZoomX = 0.0f;float afterZoomY = 0.0f;
Camera_WindowToWorldCoords(eventHandler->camera, eventHandler->mousePositionX, eventHandler->mousePositionY, &beforeZoomX, &beforeZoomY);
float zoomDelta = mouseWheelDown ? -eventHandler->mouseWheelDelta : eventHandler->mouseWheelDelta;
Camera_SetZoomDelta(eventHandler->camera, zoomDelta);
float deltaWorldX = afterZoomX - beforeZoomX;
float deltaWorldY = afterZoomY - beforeZoomY;
Camera_SetPanDelta(eventHandler->camera, deltaWorldX, deltaWorldY);
}
const GLchar* vertexSource =
"uniform vec2 pan; \n"
"uniform float zoom; \n"
"uniform float aspect; \n"
"attribute vec4 position; \n"
"varying vec3 color; \n"
"void main() \n"
"{ \n"
" gl_Position = vec4(position.xyz, 1.0); \n"
" gl_Position.xy += pan; \n"
" gl_Position.xy *= zoom; \n"
" gl_Position.y *= aspect; \n"
" color = gl_Position.xyz + vec3(0.5); \n"
"} \n";
// Fragment/pixel shader
const GLchar* fragmentSource =
"precision mediump float; \n"
"varying vec3 color; \n"
"void main() \n"
"{ \n"
" gl_FragColor = vec4 ( color, 1.0 ); \n"
"} \n";
void UpdateShader(EventHandler eventHandler)
{
// Get the current shader program
GLuint shaderProgram;
glGetIntegerv(GL_CURRENT_PROGRAM, (GLint*)&shaderProgram);
// Get uniform locations (could cache these for better performance)
eventHandler->shaderPan = glGetUniformLocation(shaderProgram, "pan");
eventHandler->shaderZoom = glGetUniformLocation(shaderProgram, "zoom");
eventHandler->shaderAspect = glGetUniformLocation(shaderProgram, "aspect");
// Create a vec2 for pan (since your camera stores pan components separately)
GLfloat pan[2] = {eventHandler->camera->regPanX, eventHandler->camera->regPanY};
// Set the uniform values
glUniform2fv(eventHandler->shaderPan, 1, pan);
glUniform1f(eventHandler->shaderZoom , eventHandler->camera->zoom);
glUniform1f(eventHandler->shaderAspect, eventHandler->camera->aspect);
}
GLuint InitializeShader()
{
// Create and compile vertex shader
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexSource, NULL);
glCompileShader(vertexShader);
//Check for compilation issues in vertex shader
GLint success;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success)
{
char compilationErrorString[512];
glGetShaderInfoLog(vertexShader, 512, NULL, compilationErrorString);
printf("Vertex shader compilation failed: %s\n", compilationErrorString);
}
// Create and compile fragment shader
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
glCompileShader(fragmentShader);
//Check for compilation issues in fragmentShader
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if(!success)
{
char compilationErrorString[512];
glGetShaderInfoLog(fragmentShader, 512, NULL, compilationErrorString);
printf("Fragment shader compilation failed: %s\n", compilationErrorString);
}
// Link vertex and fragment shader into shader program and use it
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
// Cleanup: Detach and delete shaders (they're now part of the program)
glDetachShader(shaderProgram, vertexShader);
glDetachShader(shaderProgram, fragmentShader);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return shaderProgram;
}
void InitializeTriangleGeometry(GLuint shaderProgram)
{
// Create vertex buffer object and copy vertex data into it
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//Triangle Vertices
GLfloat vertices[] =
{
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Specify the layout of the shader vertex data (positions only, 3 floats)
GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, 0);
}
void Redraw(SDL_Window *sdlWindow)
{
// Clear screen
glClear(GL_COLOR_BUFFER_BIT);
// Draw the vertex buffer
glDrawArrays(GL_TRIANGLES, 0, 3);
// Swap front/back framebuffers
SDL_GL_SwapWindow(sdlWindow);
}
void ProcessEventsWithSDL(EventHandler eventHandler)
{
SDL_Event event;
//Infinite loop to process Events
while(SDL_PollEvent(&event))
{
//Handle different event types using a switch
switch(event.type)
{
//Handle quit
case SDL_QUIT:
exit(0);
break;
//Handle Resizing
case SDL_WINDOWEVENT:
if(event.window.windowID == eventHandler->windowID && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
{
EventHandler_ResizeWindow(eventHandler, event.window.data1, event.window.data2);
}
break;
//Keyboard Events
case SDL_KEYDOWN:
printf("Key pressed: %s (SDL_Keycode: %d)\n", SDL_GetKeyName(event.key.keysym.sym),event.key.keysym.sym);
break;
case SDL_KEYUP:
printf("Key released: %s (SDL_Keycode: %d)\n", SDL_GetKeyName(event.key.keysym.sym),event.key.keysym.sym);
break;
//Mouse Events
case SDL_MOUSEMOTION:
printf("Mouse moved to (%d, %d)\n", event.motion.x, event.motion.y);
eventHandler->mousePositionX = event.motion.x;
eventHandler->mousePositionY = event.motion.y;
break;
case SDL_MOUSEBUTTONUP:
printf("Mouse button %d released\n", event.button.button);
break;
case SDL_MOUSEWHEEL:
printf("Mouse wheel scrolled: x=%d, y=%d\n", event.wheel.x, event.wheel.y);
printf("Zoom: %f, Pan: (%f, %f)\n", eventHandler->camera->zoom,eventHandler->camera->regPanX,eventHandler->camera->regPanY);
bool mouseWheelDown = (event.wheel.y < 0.0);
EventHandler_ZoomMouse(eventHandler, mouseWheelDown);
break;
//Touch Events
case SDL_FINGERDOWN:
printf("Finger %lld touched at (%.2f, %.2f)\n", event.tfinger.fingerId, event.tfinger.x, event.tfinger.y);
break;
case SDL_FINGERUP:
printf("Finger %lld lifted\n", event.tfinger.fingerId);
break;
case SDL_FINGERMOTION:
printf("Finger %lld moved to (%.2f, %.2f)\n", event.tfinger.fingerId, event.tfinger.x, event.tfinger.y);
break;
default:
break;
}
}
}
void MainLoop(void *mainLoopArgument)
{
EventHandler eventHandler = *(EventHandler*)mainLoopArgument;
ProcessEventsWithSDL(eventHandler);
UpdateShader(eventHandler);
Redraw(eventHandler->window);
}
int main()
{
if(SDL_Init(SDL_INIT_VIDEO) != 0){SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not Initialize Video: %s\n", SDL_GetError());exit(1);}
int windowWidth = 512;
int windowHeight= 512;
char *windowName = "HelloTriangle";
EventHandler eventHandler = CreateEventHandler(windowName, windowWidth, windowHeight);
if(eventHandler == NULL){printf("Error: Could not create eventHandler\n");}
//Initialize shader and geometry
GLuint shaderProgram = InitializeShader();
InitializeTriangleGeometry(shaderProgram);
//Start Emscripten main loop
void *mainLoopArgument = eventHandler;
#ifdef __EMSCRIPTEN__
int fps = 0;
emscripten_set_main_loop_arg(MainLoop, &mainLoopArgument, fps, true);
#else
while(true)
{
MainLoop(&mainLoopArgument);
}
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment