Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created June 23, 2025 06:19
Show Gist options
  • Save EncodeTheCode/6da348d6e068b8f461cfe2d0bcee977c to your computer and use it in GitHub Desktop.
Save EncodeTheCode/6da348d6e068b8f461cfe2d0bcee977c to your computer and use it in GitHub Desktop.
import heapq
import re
import numpy as np
from OpenGL.GL import *
from stb import image
# === Global registries & ID recycling heap ===
_images = {} # auto-named images
_named_images = {} # user-named images
_next_id = 1
_free_ids = []
class GLImage:
def __init__(self, filepath,
x_pos=0, y_pos=0,
x=-1, y=-1,
scale=1.0, name=None):
global _images, _named_images
self.filepath = filepath
self.x_pos = x_pos
self.y_pos = y_pos
self.scale = scale
self.visible = True
self.texture_id = None
self.layer = 0 # default layer (z-index)
# size overrides; -1 means “use original”
self._override_x = x
self._override_y = y
# load texture, compute sizes
self._load_texture()
self._compute_display_size()
# assign name & register
if name is None:
self._is_named = False
self.name = self._generate_unique_name()
_images[self.name] = self
else:
if name in _named_images:
raise ValueError(f"A named image '{name}' already exists.")
self._is_named = True
self.name = name
_named_images[self.name] = self
# ——— auto-naming helpers ———
@staticmethod
def _generate_unique_name():
global _images, _next_id, _free_ids
while _free_ids:
candidate = heapq.heappop(_free_ids)
candidate_name = f"image{candidate:03d}"
if candidate_name not in _images:
return candidate_name
name = f"image{_next_id:03d}"
_next_id += 1
return name
# ——— registry access ———
@staticmethod
def get_image(name):
return _images.get(name) or _named_images.get(name)
@staticmethod
def all_images(auto_named=True, user_named=True):
imgs = []
if auto_named:
imgs.extend(_images.values())
if user_named:
imgs.extend(_named_images.values())
return imgs
# ——— texture loading & sizing ———
def _load_texture(self):
img = image.load(self.filepath)
if img is None:
raise RuntimeError(f"Failed to load '{self.filepath}'")
self._orig_w, self._orig_h, channels = img.width, img.height, img.channels
if channels == 3:
arr = np.frombuffer(img.pixels, dtype=np.uint8)
arr = arr.reshape((self._orig_h, self._orig_w, 3))
alpha = 255 * np.ones((self._orig_h, self._orig_w, 1), np.uint8)
arr = np.concatenate((arr, alpha), axis=2)
pixels = arr.tobytes()
else:
pixels = img.pixels # channels == 4
self.texture_id = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, self.texture_id)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self._orig_w, self._orig_h, 0,
GL_RGBA, GL_UNSIGNED_BYTE, pixels)
glBindTexture(GL_TEXTURE_2D, 0)
image.free(img)
def _compute_display_size(self):
base_w = self._override_x if self._override_x != -1 else self._orig_w
base_h = self._override_y if self._override_y != -1 else self._orig_h
self._disp_w = base_w * self.scale
self._disp_h = base_h * self.scale
# ——— public setters ———
def set_position(self, x_pos, y_pos):
self.x_pos = x_pos
self.y_pos = y_pos
def set_scale(self, scale):
self.scale = scale
self._compute_display_size()
def set_size(self, x=-1, y=-1):
self._override_x = x
self._override_y = y
self._compute_display_size()
def show(self):
self.visible = True
def hide(self):
self.visible = False
# ——— deletion & ID recycling ———
def delete(self):
global _images, _named_images, _free_ids, _next_id
if self.texture_id:
glDeleteTextures([self.texture_id])
self.texture_id = None
if self._is_named:
_named_images.pop(self.name, None)
else:
_images.pop(self.name, None)
m = re.match(r'image(\d{3})$', self.name)
if m:
heapq.heappush(_free_ids, int(m.group(1)))
# shrink _next_id if highest freed ID matches
while _free_ids and max(_free_ids) == _next_id - 1:
top = max(_free_ids)
_free_ids.remove(top)
heapq.heapify(_free_ids)
_next_id -= 1
def draw(self):
if not self.visible or self.texture_id is None:
return
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.texture_id)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
x0, y0 = self.x_pos, self.y_pos
w, h = self._disp_w, self._disp_h
glBegin(GL_QUADS)
glTexCoord2f(0, 0); glVertex2f(x0, y0)
glTexCoord2f(1, 0); glVertex2f(x0 + w, y0)
glTexCoord2f(1, 1); glVertex2f(x0 + w, y0 + h)
glTexCoord2f(0, 1); glVertex2f(x0, y0 + h)
glEnd()
glBindTexture(GL_TEXTURE_2D, 0)
glDisable(GL_TEXTURE_2D)
glDisable(GL_BLEND)
# ——— helper functions ———
def GLImage_Delete(name: str) -> bool:
"""Delete image by name (auto or user)."""
img = GLImage.get_image(name)
if img:
img.delete()
return True
return False
def GLImage_SetLayerOrder(name: str, layer: int) -> bool:
"""
Set the drawing layer (z-index) of an image by its name.
Higher layers draw on top. Returns True if found and set.
"""
img = GLImage.get_image(name)
if not img:
return False
img.layer = layer
return True
def GLImage_DrawAll():
"""
Draw all registered images in ascending layer order.
Call this in your render loop instead of drawing individually.
"""
for img in sorted(GLImage.all_images(), key=lambda im: im.layer):
img.draw()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment