diff options
| -rw-r--r-- | pix.py | 8 | ||||
| -rw-r--r-- | renderer.py | 152 | ||||
| -rw-r--r-- | requirements.txt | 5 | ||||
| -rw-r--r-- | transition.py | 67 | ||||
| -rw-r--r-- | window.py | 37 |
5 files changed, 182 insertions, 87 deletions
@@ -4,17 +4,9 @@ from threading import Thread from window import PixDisplay from immich import ImmichConnector -def load_textures(pd): - # Load two images for transition - tex = ImageTexture("image1.jpg") - pd.textures.append(tex) - tex = ImageTexture("image2.jpg") - pd.textures.append(tex) - if __name__ == "__main__": immichConnector = ImmichConnector("http://192.168.1.13", "m5nqOoBc4uhAba21gZdCP3z8D3JT4GPxDXL2psd52EA") pd = PixDisplay() - #t1 = Thread(target=load_textures, daemon=True, args=(pd,)) t1 = Thread(target=immichConnector.idle, daemon=True, args=(pd,)) t1.start() pd.main() diff --git a/renderer.py b/renderer.py new file mode 100644 index 0000000..92d0686 --- /dev/null +++ b/renderer.py @@ -0,0 +1,152 @@ +import numpy as np +from OpenGL.GL import * +from OpenGL.GLUT import * +from OpenGL.GLU import * +from typing import Protocol + +class ImageRenderer: + def __init__(self): + # Setup shader and quad + self.shader = self._init_shader() + self._init_quad() + + # Get uniform locations from the shader + self.uTransform = glGetUniformLocation(self.shader, "uTransform") + self.uAlpha = glGetUniformLocation(self.shader, "uAlpha") + + # Use the shader program and set default texture unit to 0 + glUseProgram(self.shader) + glUniform1i(glGetUniformLocation(self.shader, "uTexture"), 0) + + # Setup transition + self.transition = None + + def set_transition(self, transition): + self.transition = transition(self) + + @staticmethod + def compile_shader(source, shader_type): + shader = glCreateShader(shader_type) # Create a new shader object + glShaderSource(shader, source) # Attach the shader source code + glCompileShader(shader) # Compile the shader + if not glGetShaderiv(shader, GL_COMPILE_STATUS): # Check if compilation succeeded + raise RuntimeError(glGetShaderInfoLog(shader).decode()) # Raise error with log if failed + return shader + + def _init_shader(self): + vertex_src = """ + #version 330 core + layout (location = 0) in vec2 aPos; // Vertex position + layout (location = 1) in vec2 aTexCoord; // Texture coordinates + uniform mat4 uTransform; // Transformation matrix + out vec2 TexCoord; // Output texture coordinate + + void main() { + gl_Position = uTransform * vec4(aPos, 0.0, 1.0); // Apply transformation + TexCoord = aTexCoord; // Pass tex coord to fragment shader + } + """ + fragment_src = """ + #version 330 core + in vec2 TexCoord; // Interpolated texture coordinates + out vec4 FragColor; // Final fragment color + uniform sampler2D uTexture; // Texture sampler + uniform float uAlpha; // Global alpha for transparency + + void main() { + vec4 texColor = texture(uTexture, TexCoord); // Sample texture + FragColor = vec4(texColor.rgb, texColor.a * uAlpha); // Apply alpha blending + } + """ + + # Compile and link shaders into a program + vs = ImageRenderer.compile_shader(vertex_src, GL_VERTEX_SHADER) + fs = ImageRenderer.compile_shader(fragment_src, GL_FRAGMENT_SHADER) + prog = glCreateProgram() + glAttachShader(prog, vs) + glAttachShader(prog, fs) + glLinkProgram(prog) + return prog + + def _init_quad(self): + # Define a full-screen quad with positions and texture coordinates + quad = np.array([ + -1, -1, 0, 0, # Bottom-left + 1, -1, 1, 0, # Bottom-right + -1, 1, 0, 1, # Top-left + 1, 1, 1, 1, # Top-right + ], dtype=np.float32) + + # Create and bind a Vertex Array Object + self.vao = glGenVertexArrays(1) + vbo = glGenBuffers(1) + glBindVertexArray(self.vao) + glBindBuffer(GL_ARRAY_BUFFER, vbo) + glBufferData(GL_ARRAY_BUFFER, quad.nbytes, quad, GL_STATIC_DRAW) + + # Setup vertex attributes: position (location = 0) + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 16, ctypes.c_void_p(0)) + glEnableVertexAttribArray(0) + + # Setup vertex attributes: texture coordinates (location = 1) + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 16, ctypes.c_void_p(8)) + glEnableVertexAttribArray(1) + + def draw_static(self, tex, win_w, win_h, alpha): + glUseProgram(self.shader) + glBindVertexArray(self.vao) + glBindTexture(GL_TEXTURE_2D, tex.id) + + # Calculate aspect ratios + img_aspect = tex.width / tex.height + win_aspect = win_w / win_h + + # Calculate scaling factors to preserve image aspect ratio + if img_aspect > win_aspect: + sx = 1.0 + sy = win_aspect / img_aspect + else: + sx = img_aspect / win_aspect + sy = 1.0 + + # Create transformation matrix for aspect-ratio-correct rendering + transform = np.array([ + [sx, 0, 0, 0], + [0, sy, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ], dtype=np.float32) + + # Pass transformation and alpha to the shader + glUniformMatrix4fv(self.uTransform, 1, GL_FALSE, transform) + glUniform1f(self.uAlpha, alpha) + + # Draw the textured quad + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) + + def draw_transition(self, tex_start, tex_end, win_w, win_h, delta_time, transition_time, transition_duration): + assert self.transition, "No transition has been set" + return self.transition.draw(tex_start, tex_end, win_w, win_h, delta_time, transition_time, transition_duration) + + +class Transition(Protocol): + def __init__(self, renderer): + self.renderer = renderer + + def draw(self, tex_start, tex_end, win_w, win_h, delta_time, transition_time, transition_duration): + pass + + +class TransitionMix(Transition): + def draw(self, tex_start, tex_end, win_w, win_h, delta_time, transition_time, transition_duration): + # Update alpha value for fade effect + alpha = transition_time / transition_duration + if alpha > 1.0: + alpha = 1.0 + + # Draw the first image + self.renderer.draw_static(tex_start, win_w, win_h, 1 - alpha) # TODO instead of decreasing alpha, draw transparent letterboxes + # Draw the second image (with transparency) + self.renderer.draw_static(tex_end, win_w, win_h, alpha) + + return alpha >= 1.0 # Complete diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7d9001a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask +numpy +pillow +pyopengl +requests diff --git a/transition.py b/transition.py deleted file mode 100644 index a076889..0000000 --- a/transition.py +++ /dev/null @@ -1,67 +0,0 @@ -from OpenGL.GL import * -from OpenGL.GLUT import * -from OpenGL.GLU import * - -class Transition: - def draw(self, texture_prev, texture_next, window_width, window_height, delta_time, transition_time, transition_duration): - # Update alpha value for fade effect - alpha = transition_time / transition_duration - if alpha > 1.0: - alpha = 1.0 - - # Draw the first image - self.draw_image(texture_prev, window_width, window_height, 1 - alpha) # TODO instead of decreasing alpha, draw transparent letterboxes - # Draw the second image (with transparency) - self.draw_image(texture_next, window_width, window_height, alpha) - - return alpha >= 1.0 # Complete - - - - # Draw the image with blending enabled (to allow fade effect) - def draw_image(self, texture, window_width, window_height, alpha): - if not alpha: return - - glEnable(GL_BLEND) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glBindTexture(GL_TEXTURE_2D, texture.id) - glColor4f(1.0, 1.0, 1.0, alpha) # Set alpha to control transparency - - # Calculate aspect ratio - img_aspect = texture.width / float(texture.height) - win_aspect = window_width / float(window_height) - - scaled_width = window_width - scaled_height = window_height - - # Scale the image to fit inside the window while maintaining aspect ratio - if img_aspect > win_aspect: - # Image is wider than window, letterbox vertically - scaled_height = window_width / img_aspect - else: - # Image is taller than window, letterbox horizontally - scaled_width = window_height * img_aspect - - # Position the image so it is centered - offset_x = (window_width - scaled_width) / 2 - offset_y = (window_height - scaled_height) / 2 - - # Normalize coordinates to range from -1 to 1 - x1 = (offset_x / window_width ) * 2 - 1 - y1 = (offset_y / window_height) * 2 - 1 - x2 = ((offset_x + scaled_width ) / window_width ) * 2 - 1 - y2 = ((offset_y + scaled_height) / window_height) * 2 - 1 - - # Draw the image in the center with scaling - glBegin(GL_QUADS) - glTexCoord2f(0, 0) - glVertex2f(x1, y1) - glTexCoord2f(1, 0) - glVertex2f(x2, y1) - glTexCoord2f(1, 1) - glVertex2f(x2, y2) - glTexCoord2f(0, 1) - glVertex2f(x1, y2) - glEnd() - - glDisable(GL_BLEND) @@ -3,7 +3,7 @@ from OpenGL.GLUT import * from OpenGL.GLU import * from time import time -from transition import Transition +from renderer import ImageRenderer, TransitionMix from texture import ImageTexture @@ -14,9 +14,12 @@ class PixDisplay: self.image_time = 0 self.textures = [] self.current_texture_index = 0 - self.transition = Transition() + self.renderer = None self.window_width = 0 self.window_height = 0 + # TODO + self.max_framerate = 60 + self.frame_time = int(1000 / self.max_framerate) # In ms self.display_duration = 2.0 self.transition_duration = 0.5 @@ -39,9 +42,9 @@ class PixDisplay: self.last_time = current_time if not self.textures: - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - glLoadIdentity() glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + #glLoadIdentity() glutSwapBuffers() return @@ -60,25 +63,24 @@ class PixDisplay: if window_width != self.window_width or window_height != self.window_height: self.window_width, self.window_height = window_width, window_height + glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - glLoadIdentity() + #glLoadIdentity() - self.transition.draw_image(self.texture_current, self.window_width, self.window_height, 1) + self.renderer.draw_static(self.texture_current, self.window_width, self.window_height, 1) - glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black glutSwapBuffers() return self.window_width, self.window_height = window_width, window_height + glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - glLoadIdentity() + #glLoadIdentity() # DRAW transition_time = self.image_time - self.display_duration - complete = self.transition.draw(self.texture_current, self.texture_next, self.window_width, self.window_height, delta_time, transition_time, self.transition_duration) - - glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black + complete = self.renderer.draw_transition(self.texture_current, self.texture_next, self.window_width, self.window_height, delta_time, transition_time, self.transition_duration) glutSwapBuffers() @@ -86,6 +88,10 @@ class PixDisplay: self.image_time = 0 self.current_texture_index = self.next_texture_index + def timer(self, value): + glutPostRedisplay() + glutTimerFunc(self.frame_time, self.timer, 0) # Schedule next frame + # Initialization and main loop def main(self): # Initialize the window @@ -94,6 +100,8 @@ class PixDisplay: glutCreateWindow("Image Viewer with Fade Transition") glEnable(GL_TEXTURE_2D) + self.renderer = ImageRenderer() + self.renderer.set_transition(TransitionMix) self.image_time = 0 self.start_time = time() self.last_time = time() @@ -104,7 +112,12 @@ class PixDisplay: glOrtho(-1, 1, -1, 1, -1, 1) glMatrixMode(GL_MODELVIEW) + # Enable alpha blending + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + # Run display glutDisplayFunc(self.display) - glutIdleFunc(self.display) + glutTimerFunc(self.frame_time, self.timer, 0) glutMainLoop() |
