Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save EncodeTheCode/9b82fd7d3f887aaa203777513bf332ec to your computer and use it in GitHub Desktop.
Save EncodeTheCode/9b82fd7d3f887aaa203777513bf332ec to your computer and use it in GitHub Desktop.
import time
import png
from apng import APNG
from OpenGL.GL import *
import glfw
class APNGSprite:
def __init__(self, path, loop=True, layer_order=0, frame_interval_ms=100):
apng = APNG.open(path)
self.count = len(apng.frames)
if self.count == 0:
raise RuntimeError(f"No frames in '{path}'")
first_ctrl = apng.frames[0][1]
if hasattr(first_ctrl, 'delay_num') and hasattr(first_ctrl, 'delay_den') and first_ctrl.delay_den != 0:
detected_interval = first_ctrl.delay_num / first_ctrl.delay_den
self.interval = max(detected_interval, 0.001)
else:
self.interval = max(frame_interval_ms, 1) / 1000.0
self.loop = loop
self.start_time = time.perf_counter()
self.active = True
self.layer_order = layer_order
self.cached_frames = []
for frame, ctrl in apng.frames:
data = frame.to_bytes()
w, h, rows, _ = png.Reader(bytes=data).asRGBA8()
frame_buf = b"".join(rows)
self.cached_frames.append((frame_buf, ctrl, w, h))
self.orig_w, self.orig_h = self.cached_frames[0][2], self.cached_frames[0][3]
self.canvas = bytearray(self.orig_w * self.orig_h * 4)
self.prev_canvas = None
self.tex_id = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, self.tex_id)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
for p in (GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER):
glTexParameteri(GL_TEXTURE_2D, p, GL_LINEAR)
for p in (GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T):
glTexParameteri(GL_TEXTURE_2D, p, GL_CLAMP_TO_EDGE)
def _compose_frame(self, idx):
frame_buf, ctrl, w, h = self.cached_frames[idx]
x0, y0 = ctrl.x_offset, ctrl.y_offset
if getattr(ctrl, 'dispose_op', 0) == 1: # dispose to background
for yy in range(h):
off = ((y0+yy)*self.orig_w + x0) * 4
self.canvas[off:off + w*4] = b"\x00" * (w*4)
for yy in range(h):
base = ((y0 + yy) * self.orig_w + x0) * 4
row = frame_buf[yy*w*4:(yy+1)*w*4]
for px in range(w):
si, di = px*4, base + px*4
sr, sg, sb, sa = row[si:si+4]
dr, dg, db, da = self.canvas[di:di+4]
a = sa / 255.0
nr = int(sr*a + dr*(1 - a))
ng = int(sg*a + dg*(1 - a))
nb = int(sb*a + db*(1 - a))
na = int(255 * (a + da/255.0*(1 - a)))
self.canvas[di:di+4] = bytes((nr, ng, nb, na))
glBindTexture(GL_TEXTURE_2D, self.tex_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.orig_w, self.orig_h,
0, GL_RGBA, GL_UNSIGNED_BYTE, self.canvas)
if getattr(ctrl, 'dispose_op', 0) == 2 and self.prev_canvas is not None:
self.canvas[:] = self.prev_canvas
else:
self.prev_canvas = bytes(self.canvas)
def draw_fixed(self, x, y):
if not self.active:
return
elapsed = time.perf_counter() - self.start_time
frame_idx = int(elapsed / self.interval)
if self.loop:
frame_idx %= self.count
else:
frame_idx = min(frame_idx, self.count - 1)
self._compose_frame(frame_idx)
glPushMatrix()
glTranslatef(0, 0, self.layer_order)
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.tex_id)
glPushMatrix()
glLoadIdentity()
glTranslatef(x, y, 0)
# Flip texture coords to correct upside-down issue
glBegin(GL_QUADS)
glTexCoord2f(0, 0); glVertex2f(0, 0)
glTexCoord2f(1, 0); glVertex2f(self.orig_w, 0)
glTexCoord2f(1, 1); glVertex2f(self.orig_w, self.orig_h)
glTexCoord2f(0, 1); glVertex2f(0, self.orig_h)
glEnd()
glPopMatrix()
glPopMatrix()
glBindTexture(GL_TEXTURE_2D, 0)
def delete(self):
if self.active:
glDeleteTextures([self.tex_id])
self.active = False
def __del__(self):
try:
self.delete()
except:
pass
def main():
if not glfw.init():
raise RuntimeError("Failed to init GLFW")
window = glfw.create_window(800, 600, "APNG HUD Example", None, None)
if not window:
glfw.terminate()
raise RuntimeError("Failed to create window")
glfw.make_context_current(window)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glDisable(GL_DEPTH_TEST)
sprite = APNGSprite("nyan_cat.png", loop=True, layer_order=2)
while not glfw.window_should_close(window):
glfw.poll_events()
glClearColor(0.1, 0.1, 0.1, 1)
glClear(GL_COLOR_BUFFER_BIT)
w, h = glfw.get_framebuffer_size(window)
glViewport(0, 0, w, h)
glMatrixMode(GL_PROJECTION); glLoadIdentity()
glOrtho(0, w, 0, h, -1, 1)
glMatrixMode(GL_MODELVIEW); glLoadIdentity()
sprite.draw_fixed(10, h - 10 - sprite.orig_h)
glfw.swap_buffers(window)
sprite.delete()
glfw.terminate()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment