Last active
January 16, 2026 22:59
-
-
Save jac18281828/6d71ddd61022357ea3b718cce8c5159a to your computer and use it in GitHub Desktop.
python rendering sample codes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import math | |
| import random | |
| import time | |
| from collections import deque | |
| import pyglet | |
| from pyglet import gl | |
| # -------------------- CONFIG -------------------- | |
| PARTICLES = 20_000 | |
| VSYNC = False | |
| RUN_SECONDS = 30.0 | |
| # Update chunking - spread physics across N frames | |
| UPDATE_CHUNKS = 4 | |
| # ------------------------------------------------ | |
| # Create fullscreen window | |
| config = pyglet.gl.Config(double_buffer=True) | |
| try: | |
| window = pyglet.window.Window(fullscreen=True, vsync=VSYNC, config=config) | |
| except: | |
| window = pyglet.window.Window(fullscreen=True, vsync=VSYNC) | |
| WIDTH, HEIGHT = window.width, window.height | |
| print(f"Window size: {window.width}x{window.height}") | |
| print(f"Framebuffer size: {window.get_framebuffer_size()}") | |
| print(f"Pixel ratio: {window.get_framebuffer_size()[0] / window.width}") | |
| print(f"Renderer: {pyglet.gl.gl_info.get_renderer()}") | |
| batch = pyglet.graphics.Batch() | |
| def clamp(x, a, b): | |
| return a if x < a else b if x > b else x | |
| # Neon palette - bright saturated colors | |
| NEON_COLORS = [ | |
| (255, 80, 220), # Pink | |
| (210, 80, 255), # Magenta | |
| (120, 70, 255), # Purple | |
| ( 60, 150, 255), # Blue | |
| ( 80, 255, 255), # Cyan | |
| (100, 255, 150), # Green | |
| (255, 160, 60), # Orange | |
| ] | |
| # ---------- Particle class using native pyglet shapes ---------- | |
| class Particle: | |
| __slots__ = ("x", "y", "vx", "vy", "phase", "circle") | |
| def __init__(self): | |
| self.x = random.uniform(0, WIDTH) | |
| self.y = random.uniform(0, HEIGHT) | |
| speed = random.uniform(80, 200) | |
| angle = random.uniform(0, math.tau) | |
| self.vx = math.cos(angle) * speed | |
| self.vy = math.sin(angle) * speed | |
| color = random.choice(NEON_COLORS) | |
| self.phase = random.uniform(0, math.tau) | |
| # Simple circle - no opacity changes for max performance | |
| self.circle = pyglet.shapes.Circle( | |
| self.x, self.y, random.uniform(2.0, 4.0), | |
| segments=8, | |
| color=color, | |
| batch=batch | |
| ) | |
| def step(self, dt): | |
| dt_scaled = dt * UPDATE_CHUNKS | |
| self.x += self.vx * dt_scaled | |
| self.y += self.vy * dt_scaled | |
| if self.x < 0: | |
| self.x = 0 | |
| self.vx *= -1 | |
| elif self.x > WIDTH: | |
| self.x = WIDTH | |
| self.vx *= -1 | |
| if self.y < 0: | |
| self.y = 0 | |
| self.vy *= -1 | |
| elif self.y > HEIGHT: | |
| self.y = HEIGHT | |
| self.vy *= -1 | |
| self.circle.x = self.x | |
| self.circle.y = self.y | |
| print(f"Creating {PARTICLES} smooth particles...") | |
| particles = [Particle() for _ in range(PARTICLES)] | |
| print("Particles created!") | |
| # Pre-slice particles into chunks for round-robin updates | |
| chunk_size = (PARTICLES + UPDATE_CHUNKS - 1) // UPDATE_CHUNKS | |
| particle_chunks = [particles[i:i + chunk_size] for i in range(0, PARTICLES, chunk_size)] | |
| current_chunk = 0 | |
| # ---------- Stats display ---------- | |
| label = pyglet.text.Label("", x=14, y=14, font_size=14) | |
| label.color = (255, 255, 255, 220) | |
| draw_times = deque(maxlen=3000) | |
| t_start = time.perf_counter() | |
| t_last_draw = None | |
| draw_frames = 0 | |
| closing = False | |
| def compute_stats(): | |
| dts = list(draw_times) | |
| dts.sort() | |
| avg = sum(dts) / len(dts) | |
| p95 = dts[int(0.95 * len(dts)) - 1] | |
| worst = dts[-1] | |
| fps = 1.0 / avg if avg > 0 else 0.0 | |
| return fps, avg, p95, worst | |
| @window.event | |
| def on_draw(): | |
| global t_last_draw, draw_frames, closing | |
| if closing: | |
| return | |
| now = time.perf_counter() | |
| if t_last_draw is not None: | |
| draw_times.append(now - t_last_draw) | |
| t_last_draw = now | |
| draw_frames += 1 | |
| # Set a very dark blue background instead of pure black | |
| # Pure black can trigger HDR display dithering/local dimming artifacts | |
| gl.glClearColor(0.01, 0.01, 0.02, 1.0) | |
| window.clear() | |
| batch.draw() | |
| if len(draw_times) > 60 and draw_frames % 10 == 0: | |
| fps, avg, p95, worst = compute_stats() | |
| label.text = f"draw_fps={fps:6.1f} avg_ms={avg*1000:6.2f} p95_ms={p95*1000:6.2f} worst_ms={worst*1000:6.2f} particles={PARTICLES}" | |
| label.draw() | |
| def update(dt): | |
| global current_chunk | |
| dt = clamp(dt, 0.0, 1/30) | |
| # Update only one chunk of particles this frame | |
| for p in particle_chunks[current_chunk]: | |
| p.step(dt) | |
| current_chunk = (current_chunk + 1) % len(particle_chunks) | |
| window.dispatch_event("on_draw") | |
| window.flip() | |
| def stop_later(dt): | |
| global closing | |
| closing = True | |
| pyglet.app.exit() | |
| pyglet.clock.schedule(update) | |
| pyglet.clock.schedule_once(stop_later, RUN_SECONDS) | |
| try: | |
| pyglet.app.run() | |
| finally: | |
| try: | |
| window.close() | |
| except Exception: | |
| pass | |
| if draw_times: | |
| fps, avg, p95, worst = compute_stats() | |
| print(f"\nSummary over ~{RUN_SECONDS:.1f}s (draw-based):") | |
| print(f"draw_frames={draw_frames}") | |
| print(f"draw_fps={fps:.1f}, avg_ms={avg*1000:.2f}, p95_ms={p95*1000:.2f}, worst_ms={worst*1000:.2f}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import math | |
| import random | |
| import time | |
| from collections import deque | |
| from io import BytesIO | |
| import pyglet | |
| from PIL import Image, ImageDraw, ImageFilter | |
| # -------------------- CONFIG -------------------- | |
| PARTICLES = 20_000 # More particles now that we chunk updates | |
| VSYNC = False | |
| RUN_SECONDS = 30.0 | |
| # Eye candy settings | |
| ENABLE_STARS = True # Disable stars (can cause noise on some displays) | |
| STAR_COUNT = 200 | |
| ENABLE_TRAILS = False # Disable trails for performance | |
| # Update chunking - spread physics across N frames | |
| UPDATE_CHUNKS = 4 # Update 1/4 of particles per frame | |
| # ------------------------------------------------ | |
| # Create fullscreen window - automatically uses native resolution | |
| config = pyglet.gl.Config( | |
| double_buffer=True, | |
| sample_buffers=1, # Enable multisampling (antialiasing) | |
| samples=4, # 4x MSAA for smooth edges | |
| ) | |
| try: | |
| window = pyglet.window.Window(fullscreen=True, vsync=VSYNC, config=config) | |
| except pyglet.window.NoSuchConfigException: | |
| # Fallback if MSAA not supported | |
| print("MSAA not available, using default config") | |
| window = pyglet.window.Window(fullscreen=True, vsync=VSYNC) | |
| # Get actual dimensions from window | |
| WIDTH, HEIGHT = window.width, window.height | |
| # Print diagnostic info for debugging cross-platform differences | |
| print(f"Window size: {window.width}x{window.height}") | |
| print(f"Framebuffer size: {window.get_framebuffer_size()}") | |
| print(f"Pixel ratio: {window.get_framebuffer_size()[0] / window.width}") | |
| print(f"OpenGL version: {pyglet.gl.gl_info.get_version()}") | |
| print(f"Renderer: {pyglet.gl.gl_info.get_renderer()}") | |
| batch_bg = pyglet.graphics.Batch() | |
| batch_particles = pyglet.graphics.Batch() | |
| batch_fg = pyglet.graphics.Batch() | |
| def clamp(x, a, b): | |
| return a if x < a else b if x > b else x | |
| def lerp(a, b, t): | |
| return a + (b - a) * t | |
| def color_lerp(c1, c2, t): | |
| return ( | |
| int(lerp(c1[0], c2[0], t)), | |
| int(lerp(c1[1], c2[1], t)), | |
| int(lerp(c1[2], c2[2], t)), | |
| ) | |
| # Neon palette | |
| PINK = (255, 80, 220) | |
| MAG = (210, 80, 255) | |
| PURP = (120, 70, 255) | |
| BLUE = ( 60, 150, 255) | |
| CYAN = ( 80, 255, 255) | |
| GREEN = (100, 255, 150) | |
| ORANGE = (255, 160, 60) | |
| WHITE = (255, 255, 255) | |
| NEON_COLORS = [PINK, MAG, PURP, BLUE, CYAN, GREEN, ORANGE] | |
| # ---------- Create soft glow texture for particles ---------- | |
| def create_soft_glow_texture(size, color, blur_radius=None): | |
| """Create a soft, gaussian-blurred circular glow texture.""" | |
| if blur_radius is None: | |
| blur_radius = size // 3 | |
| padding = blur_radius * 2 | |
| img_size = size + padding * 2 | |
| img = Image.new('RGBA', (img_size, img_size), (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| center = img_size // 2 | |
| radius = size // 2 | |
| # Draw a bright core with gradient falloff | |
| for i in range(3): | |
| r = radius - i * (radius // 4) | |
| alpha = 255 - i * 60 | |
| draw.ellipse( | |
| [center - r, center - r, center + r, center + r], | |
| fill=(*color, alpha) | |
| ) | |
| img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius)) | |
| img_bytes = BytesIO() | |
| img.save(img_bytes, format='PNG') | |
| img_bytes.seek(0) | |
| return pyglet.image.load('glow.png', file=img_bytes) | |
| print("Generating particle glow textures...") | |
| GLOW_SIZES = [16, 24, 32] | |
| glow_textures = {} | |
| for size in GLOW_SIZES: | |
| for i, color in enumerate(NEON_COLORS): | |
| key = (size, i) | |
| glow_textures[key] = create_soft_glow_texture(size, color, blur_radius=size//2) | |
| print(f"Generated {len(glow_textures)} glow textures") | |
| # ---------- Background: twinkling stars (optional) ---------- | |
| stars = [] | |
| if ENABLE_STARS: | |
| pixel_ratio = window.get_framebuffer_size()[0] / window.width | |
| min_star_radius = 1.5 * pixel_ratio # Larger for HiDPI to avoid noise | |
| for _ in range(STAR_COUNT): | |
| x = random.uniform(0, WIDTH) | |
| y = random.uniform(0, HEIGHT) | |
| r = random.uniform(max(1.2, min_star_radius), 2.0) | |
| ph = random.uniform(0, math.tau) | |
| star = pyglet.shapes.Circle(x, y, r, segments=16, color=(180, 180, 220), batch=batch_bg) | |
| star.opacity = random.randint(100, 180) | |
| stars.append((star, ph)) | |
| # ---------- Particle class with smooth rendering ---------- | |
| class Particle: | |
| __slots__ = ("x", "y", "vx", "vy", "color_idx", "phase", "sprite", "size_idx", "base_speed", "base_opacity") | |
| def __init__(self): | |
| self.x = random.uniform(0, WIDTH) | |
| self.y = random.uniform(0, HEIGHT) | |
| # Speed with some variation | |
| speed = random.uniform(80, 200) | |
| angle = random.uniform(0, math.tau) | |
| self.vx = math.cos(angle) * speed | |
| self.vy = math.sin(angle) * speed | |
| self.base_speed = speed | |
| # Color and visual properties | |
| self.color_idx = random.randint(0, len(NEON_COLORS) - 1) | |
| self.phase = random.uniform(0, math.tau) | |
| self.size_idx = random.choice(GLOW_SIZES) | |
| # Create main sprite with glow texture | |
| texture = glow_textures[(self.size_idx, self.color_idx)] | |
| self.sprite = pyglet.sprite.Sprite(texture, x=self.x, y=self.y, batch=batch_particles) | |
| self.sprite.scale = random.uniform(0.25, 0.55) | |
| self.base_opacity = random.randint(140, 240) | |
| self.sprite.opacity = self.base_opacity | |
| def step(self, dt, t): | |
| # Scale dt by chunk count since we update less frequently | |
| dt_scaled = dt * UPDATE_CHUNKS | |
| # Move particle | |
| self.x += self.vx * dt_scaled | |
| self.y += self.vy * dt_scaled | |
| # Bounce off walls | |
| if self.x < 0: | |
| self.x = 0 | |
| self.vx *= -1 | |
| elif self.x > WIDTH: | |
| self.x = WIDTH | |
| self.vx *= -1 | |
| if self.y < 0: | |
| self.y = 0 | |
| self.vy *= -1 | |
| elif self.y > HEIGHT: | |
| self.y = HEIGHT | |
| self.vy *= -1 | |
| # Update sprite position | |
| self.sprite.x = self.x | |
| self.sprite.y = self.y | |
| # Smooth pulsing/glinting effect | |
| pulse = 0.75 + 0.25 * math.sin(t * 3.5 + self.phase) | |
| glint = max(0, math.sin(t * 8.0 + self.phase * 2.5)) ** 12 | |
| brightness = pulse + glint * 0.6 | |
| self.sprite.opacity = int(clamp(self.base_opacity * brightness, 80, 255)) | |
| print(f"Creating {PARTICLES} smooth particles...") | |
| particles = [Particle() for _ in range(PARTICLES)] | |
| print("Particles created!") | |
| # Pre-slice particles into chunks for round-robin updates | |
| chunk_size = (PARTICLES + UPDATE_CHUNKS - 1) // UPDATE_CHUNKS | |
| particle_chunks = [particles[i:i + chunk_size] for i in range(0, PARTICLES, chunk_size)] | |
| current_chunk = 0 | |
| # ---------- Stats display ---------- | |
| label = pyglet.text.Label("", x=14, y=14, font_size=14) | |
| label.color = (255, 255, 255, 220) | |
| draw_times = deque(maxlen=3000) | |
| t_start = time.perf_counter() | |
| t_last_draw = None | |
| draw_frames = 0 | |
| closing = False | |
| def compute_stats(): | |
| dts = list(draw_times) | |
| dts.sort() | |
| avg = sum(dts) / len(dts) | |
| p95 = dts[int(0.95 * len(dts)) - 1] | |
| worst = dts[-1] | |
| fps = 1.0 / avg if avg > 0 else 0.0 | |
| return fps, avg, p95, worst | |
| @window.event | |
| def on_draw(): | |
| global t_last_draw, draw_frames, closing | |
| if closing: | |
| return | |
| now = time.perf_counter() | |
| if t_last_draw is not None: | |
| draw_times.append(now - t_last_draw) | |
| t_last_draw = now | |
| draw_frames += 1 | |
| window.clear() | |
| batch_bg.draw() | |
| batch_particles.draw() | |
| batch_fg.draw() | |
| if len(draw_times) > 60 and draw_frames % 10 == 0: | |
| fps, avg, p95, worst = compute_stats() | |
| label.text = f"draw_fps={fps:6.1f} avg_ms={avg*1000:6.2f} p95_ms={p95*1000:6.2f} worst_ms={worst*1000:6.2f} particles={PARTICLES}" | |
| label.draw() | |
| def update(dt): | |
| global current_chunk | |
| dt = clamp(dt, 0.0, 1/30) | |
| now = time.perf_counter() | |
| t = now - t_start | |
| # Twinkle stars (only a subset each frame for performance) | |
| if ENABLE_STARS: | |
| for s, ph in stars[::10]: | |
| tw = 0.5 + 0.5 * math.sin(t * 2.5 + ph) | |
| s.opacity = int(80 + 120 * tw) | |
| # Update only one chunk of particles this frame (round-robin) | |
| for p in particle_chunks[current_chunk]: | |
| p.step(dt, t) | |
| # Advance to next chunk | |
| current_chunk = (current_chunk + 1) % len(particle_chunks) | |
| window.dispatch_event("on_draw") | |
| window.flip() | |
| def stop_later(dt): | |
| global closing | |
| closing = True | |
| pyglet.app.exit() | |
| pyglet.clock.schedule(update) | |
| pyglet.clock.schedule_once(stop_later, RUN_SECONDS) | |
| try: | |
| pyglet.app.run() | |
| finally: | |
| try: | |
| window.close() | |
| except Exception: | |
| pass | |
| if draw_times: | |
| fps, avg, p95, worst = compute_stats() | |
| print(f"\nSummary over ~{RUN_SECONDS:.1f}s (draw-based):") | |
| print(f"draw_frames={draw_frames}") | |
| print(f"draw_fps={fps:.1f}, avg_ms={avg*1000:.2f}, p95_ms={p95*1000:.2f}, worst_ms={worst*1000:.2f}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import pyglet | |
| import time | |
| from collections import deque | |
| w = pyglet.window.Window(2560, 1440, vsync=True) | |
| label = pyglet.text.Label('', x=10, y=10) | |
| times = deque(maxlen=300) | |
| t0 = time.perf_counter() | |
| x = 0.0 | |
| @w.event | |
| def on_draw(): | |
| global x | |
| w.clear() | |
| # Simple CPU-side animation + draw (lightweight) | |
| x = (x + 4.0) % w.width | |
| pyglet.shapes.Circle(x, w.height//2, 40).draw() | |
| # Frame time stats | |
| now = time.perf_counter() | |
| dt = now - on_draw.last | |
| on_draw.last = now | |
| times.append(dt) | |
| if len(times) > 10: | |
| avg = sum(times) / len(times) | |
| p95 = sorted(times)[int(0.95*len(times))-1] | |
| label.text = f"fps~{1/avg:5.1f} avg_ms={avg*1000:5.2f} p95_ms={p95*1000:5.2f}" | |
| label.draw() | |
| on_draw.last = t0 | |
| pyglet.app.run() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import gc | |
| import math | |
| import random | |
| import time | |
| from collections import deque | |
| from io import BytesIO | |
| import pyglet | |
| from PIL import Image, ImageDraw, ImageFilter | |
| # -------------------- CONFIG -------------------- | |
| WIDTH, HEIGHT = 2560, 1440 | |
| VSYNC = False | |
| RUN_SECONDS = 30.0 | |
| # Eye candy knobs | |
| CLOUD_BLOBS = 275 | |
| CLOUD_LAYERS = 2 | |
| STAR_COUNT = 400 | |
| TRAIL_POINTS = 120 | |
| TRAIL_HALF_LIFE = 0.12 | |
| STAR_STRIDE = 14 | |
| CLOUD_STRIDE = 6 | |
| CLOUD_CHUNK = 30 | |
| # Orb speed multiplier (1.0 = default, 0.5 = half speed, 0.25 = quarter speed) | |
| ORB_SPEED = 0.3 | |
| # ------------------------------------------------ | |
| window = pyglet.window.Window(WIDTH, HEIGHT, vsync=VSYNC, resizable=False) | |
| # Capture sizes BEFORE run() so we can print after safely. | |
| try: | |
| WIN_SIZE = (window.width, window.height) | |
| FB_SIZE = window.get_framebuffer_size() | |
| except Exception: | |
| WIN_SIZE = (WIDTH, HEIGHT) | |
| FB_SIZE = None | |
| batch_bg = pyglet.graphics.Batch() | |
| batch_fg = pyglet.graphics.Batch() | |
| def clamp(x, a, b): | |
| return a if x < a else b if x > b else x | |
| def lerp(a, b, t): | |
| return a + (b - a) * t | |
| def color_lerp(c1, c2, t): | |
| return ( | |
| int(lerp(c1[0], c2[0], t)), | |
| int(lerp(c1[1], c2[1], t)), | |
| int(lerp(c1[2], c2[2], t)), | |
| ) | |
| # Neon palette | |
| PINK = (255, 80, 220) | |
| MAG = (210, 80, 255) | |
| PURP = (120, 70, 255) | |
| BLUE = ( 60, 150, 255) | |
| WHITE = (255, 255, 255) | |
| # ---------- Background: stars ---------- | |
| stars = [] | |
| for _ in range(STAR_COUNT): | |
| x = random.uniform(0, WIDTH) | |
| y = random.uniform(0, HEIGHT) | |
| r = random.uniform(0.6, 1.4) | |
| ph = random.uniform(0, math.tau) | |
| star = pyglet.shapes.Circle(x, y, r, color=(200, 200, 255), batch=batch_bg) | |
| star.opacity = random.randint(80, 200) | |
| stars.append((star, ph)) | |
| # ---------- Background: cloud blobs (soft gaussian sprites) ---------- | |
| # Create a few pre-blurred cloud textures at different sizes for reuse | |
| def create_soft_cloud_texture(size, color, blur_radius=None): | |
| """Create a soft, gaussian-blurred circular cloud texture.""" | |
| if blur_radius is None: | |
| blur_radius = size // 4 | |
| # Create larger image for blur padding | |
| padding = blur_radius * 2 | |
| img_size = size + padding * 2 | |
| # Create RGBA image | |
| img = Image.new('RGBA', (img_size, img_size), (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| # Draw filled circle in center | |
| center = img_size // 2 | |
| radius = size // 2 | |
| draw.ellipse( | |
| [center - radius, center - radius, center + radius, center + radius], | |
| fill=(*color, 255) | |
| ) | |
| # Apply gaussian blur for soft edges | |
| img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius)) | |
| # Convert to pyglet texture | |
| img_bytes = BytesIO() | |
| img.save(img_bytes, format='PNG') | |
| img_bytes.seek(0) | |
| return pyglet.image.load('cloud.png', file=img_bytes) | |
| # Pre-generate cloud textures at a few sizes for variety | |
| print("Generating soft cloud textures...") | |
| CLOUD_TEXTURE_SIZES = [128, 192, 256, 320, 384] | |
| cloud_textures = {} | |
| for size in CLOUD_TEXTURE_SIZES: | |
| # Create textures in different colors | |
| for name, color in [('purp', PURP), ('pink', PINK), ('blue', BLUE), ('mag', MAG)]: | |
| key = (size, name) | |
| cloud_textures[key] = create_soft_cloud_texture(size, color, blur_radius=size//3) | |
| print(f"Generated {len(cloud_textures)} cloud textures") | |
| # Create cloud sprites using the soft textures | |
| clouds = [] | |
| for layer in range(CLOUD_LAYERS): | |
| for _ in range(CLOUD_BLOBS): | |
| x = random.uniform(-200, WIDTH + 200) | |
| y = random.uniform(-200, HEIGHT + 200) | |
| # Pick a random texture size and color | |
| size = random.choice(CLOUD_TEXTURE_SIZES) | |
| color_name = random.choice(['purp', 'pink', 'blue', 'mag']) | |
| texture = cloud_textures[(size, color_name)] | |
| # Scale for variety (0.5x to 2x) | |
| scale = random.uniform(0.5, 2.0) * (1.0 + 0.25 * layer) | |
| # Create sprite | |
| sprite = pyglet.sprite.Sprite(texture, x=x, y=y, batch=batch_bg) | |
| sprite.scale = scale | |
| sprite.anchor_x = texture.width // 2 | |
| sprite.anchor_y = texture.height // 2 | |
| base_op = random.randint(15, 50) # Slightly higher opacity since it's blurred | |
| sprite.opacity = base_op | |
| vx = random.uniform(-10, 10) * (0.4 + 0.25 * layer) | |
| vy = random.uniform(-10, 10) * (0.4 + 0.25 * layer) | |
| ph = random.uniform(0, math.tau) | |
| clouds.append((sprite, vx, vy, ph, base_op)) | |
| # ---------- The bouncing orb (lens effect) ---------- | |
| orb_r = 54.0 | |
| orb_x, orb_y = WIDTH * 0.5, HEIGHT * 0.5 | |
| # Medium outer glow (soft halo) - with more segments for smoothness | |
| glow_outer = pyglet.shapes.Circle(orb_x, orb_y, orb_r * 1.6, segments=48, color=PURP, batch=batch_fg) | |
| glow_outer.opacity = 35 | |
| # The lens ring - using Arc for just the outline (not filled) | |
| RING_THICKNESS = 6 | |
| # Outer ring edge (bright) | |
| ring_arc1 = pyglet.shapes.Arc(orb_x, orb_y, orb_r, segments=48, color=PINK, batch=batch_fg) | |
| ring_arc1.opacity = 200 | |
| # Inner ring edge | |
| ring_arc2 = pyglet.shapes.Arc(orb_x, orb_y, orb_r - 3, segments=48, color=MAG, batch=batch_fg) | |
| ring_arc2.opacity = 140 | |
| # Innermost ring edge | |
| ring_arc3 = pyglet.shapes.Arc(orb_x, orb_y, orb_r - 5, segments=48, color=PURP, batch=batch_fg) | |
| ring_arc3.opacity = 80 | |
| # Very subtle inner lens tint - almost invisible | |
| lens_inner = pyglet.shapes.Circle(orb_x, orb_y, orb_r - RING_THICKNESS, segments=48, color=BLUE, batch=batch_fg) | |
| lens_inner.opacity = 8 # Extremely transparent | |
| # Lens highlight (specular reflection) - small bright spot | |
| highlight_offset = orb_r * 0.35 | |
| lens_highlight = pyglet.shapes.Circle(orb_x - highlight_offset, orb_y + highlight_offset, orb_r * 0.18, segments=16, color=WHITE, batch=batch_fg) | |
| lens_highlight.opacity = 70 | |
| # Secondary smaller highlight | |
| lens_highlight2 = pyglet.shapes.Circle(orb_x + highlight_offset * 0.4, orb_y - highlight_offset * 0.6, orb_r * 0.08, segments=12, color=WHITE, batch=batch_fg) | |
| lens_highlight2.opacity = 90 | |
| # ---------- Lens distortion effect ---------- | |
| # Pool of circles to show "warped" versions of background elements inside the lens | |
| # These get repositioned each frame to create barrel/magnification distortion | |
| LENS_MAGNIFICATION = 1.25 # How much to magnify (1.0 = no magnification) | |
| LENS_DISTORT_SAMPLES = 24 # Number of distortion sample points | |
| # Create pool of distortion circles for stars | |
| distort_stars = [] | |
| for i in range(LENS_DISTORT_SAMPLES): | |
| dc = pyglet.shapes.Circle(0, 0, 2.0, segments=8, color=(220, 220, 255), batch=batch_fg) | |
| dc.opacity = 0 # Start invisible | |
| distort_stars.append(dc) | |
| # Create pool of distortion circles for clouds (fewer, larger, smooth) | |
| distort_clouds = [] | |
| for i in range(8): | |
| dc = pyglet.shapes.Circle(0, 0, 40, segments=32, color=PURP, batch=batch_fg) | |
| dc.opacity = 0 | |
| distort_clouds.append(dc) | |
| # For position tracking (used by update) | |
| class OrbState: | |
| x = orb_x | |
| y = orb_y | |
| orb = OrbState() | |
| # Trail as a set of reusable arcs (rings), so they don't fill in the lens center | |
| trail_render = [] | |
| for i in range(TRAIL_POINTS): | |
| t = i / max(1, (TRAIL_POINTS - 1)) | |
| c = color_lerp(PINK, PURP, t) | |
| # Use Arc instead of Circle - creates ring outlines that don't block the lens | |
| # Use fewer segments for trail (performance) since they're smaller | |
| tr = pyglet.shapes.Arc(orb.x, orb.y, orb_r*(1.15 - 0.55*t), segments=24, color=c, batch=batch_fg) | |
| tr.opacity = 0 | |
| trail_render.append(tr) | |
| # Store recent positions + timestamps (ring-buffer via deque) | |
| trail = deque(maxlen=TRAIL_POINTS) # elements: (x, y, timestamp) | |
| # Motion (scaled by ORB_SPEED) | |
| vx = 980.0 * ORB_SPEED | |
| vy = 720.0 * ORB_SPEED | |
| impact = 0.0 | |
| t0 = time.perf_counter() | |
| # Stats (draw based) | |
| draw_times = deque(maxlen=4000) | |
| t_last_draw = None | |
| draw_frames = 0 | |
| closing = False | |
| label = pyglet.text.Label("", x=14, y=14, font_size=14) | |
| label.color = (255, 255, 255, 220) | |
| cloud_accum = 0.0 | |
| cloud_i = 0 | |
| def compute_stats(): | |
| dts = list(draw_times) | |
| dts.sort() | |
| avg = sum(dts) / len(dts) | |
| p95 = dts[int(0.95 * len(dts)) - 1] | |
| worst = dts[-1] | |
| fps = 1.0 / avg if avg > 0 else 0.0 | |
| return fps, avg, p95, worst | |
| @window.event | |
| def on_draw(): | |
| global t_last_draw, draw_frames, closing | |
| if closing: | |
| return | |
| now = time.perf_counter() | |
| if t_last_draw is not None: | |
| draw_times.append(now - t_last_draw) | |
| t_last_draw = now | |
| draw_frames += 1 | |
| window.clear() | |
| batch_bg.draw() | |
| batch_fg.draw() | |
| if len(draw_times) > 120 and draw_frames % 12 == 0: | |
| fps, avg, p95, worst = compute_stats() | |
| label.text = f"draw_fps={fps:7.1f} avg_ms={avg*1000:6.2f} p95_ms={p95*1000:6.2f} worst_ms={worst*1000:6.2f}" | |
| label.draw() | |
| def update(dt): | |
| global vx, vy, impact, cloud_i | |
| now = time.perf_counter() | |
| t = now - t0 | |
| # Keep dt sane | |
| dt = clamp(dt, 0.0, 1/30) | |
| # Twinkle a subset of stars (cheap) | |
| for s, ph in stars[::STAR_STRIDE]: | |
| tw = 0.55 + 0.45 * math.sin(t*2.2 + ph) | |
| s.opacity = int(70 + 150 * tw) | |
| # Drift some clouds in a small chunk every frame | |
| n = len(clouds) | |
| end = min(cloud_i + CLOUD_CHUNK, n) | |
| for j in range(cloud_i, end): | |
| blob, cvx, cvy, ph, base_op = clouds[j] | |
| blob.x += cvx * dt | |
| blob.y += cvy * dt | |
| if blob.x < -300: blob.x = WIDTH + 300 | |
| if blob.x > WIDTH + 300: blob.x = -300 | |
| if blob.y < -300: blob.y = HEIGHT + 300 | |
| if blob.y > HEIGHT + 300: blob.y = -300 | |
| blob.opacity = int(base_op + 6 * math.sin(t*0.7 + ph)) | |
| cloud_i = 0 if end >= n else end | |
| # Move orb | |
| orb.x += vx * dt | |
| orb.y += vy * dt | |
| hit = False | |
| if orb.x - orb_r < 0: | |
| orb.x = orb_r | |
| vx = abs(vx); hit = True | |
| elif orb.x + orb_r > WIDTH: | |
| orb.x = WIDTH - orb_r | |
| vx = -abs(vx); hit = True | |
| if orb.y - orb_r < 0: | |
| orb.y = orb_r | |
| vy = abs(vy); hit = True | |
| elif orb.y + orb_r > HEIGHT: | |
| orb.y = HEIGHT - orb_r | |
| vy = -abs(vy); hit = True | |
| speed = math.hypot(vx, vy) | |
| if hit: | |
| impact = min(1.0, 0.15 + speed / 2600.0) | |
| impact *= (0.90 ** (dt * 60)) | |
| # Color shift with speed for ring edges | |
| v = clamp(speed / 1900.0, 0.0, 1.0) | |
| ring_color = color_lerp(PINK, PURP, v) if v < 0.6 else color_lerp(PURP, BLUE, (v - 0.6) / 0.4) | |
| ring_arc1.color = ring_color | |
| ring_arc2.color = color_lerp(MAG, BLUE, v) | |
| ring_arc3.color = color_lerp(PURP, PINK, v) | |
| # Glow pulses | |
| pulse = 0.5 + 0.5 * math.sin(t * 2.0) | |
| glow_outer.opacity = int(25 + 20 * pulse) | |
| # Lens inner subtle pulse - keep very transparent | |
| lens_inner.opacity = int(6 + 4 * pulse) | |
| # Deformation look: fake squash/stretch by offsetting glow radii | |
| stretch = 1.0 + 0.75 * impact | |
| squash = 1.0 - 0.55 * impact | |
| glow_outer.radius = orb_r * (1.6 * stretch) | |
| # Ring deformation on impact | |
| ring_arc1.radius = orb_r * lerp(1.0, stretch * 0.95, impact * 0.3) | |
| ring_arc2.radius = (orb_r - 3) * lerp(1.0, squash * 1.05, impact * 0.3) | |
| ring_arc3.radius = (orb_r - 5) * lerp(1.0, squash * 1.05, impact * 0.3) | |
| lens_inner.radius = (orb_r - RING_THICKNESS) * lerp(1.0, squash, impact * 0.2) | |
| # Align all lens components with orb position | |
| glow_outer.x = orb.x | |
| glow_outer.y = orb.y | |
| ring_arc1.x = orb.x | |
| ring_arc1.y = orb.y | |
| ring_arc2.x = orb.x | |
| ring_arc2.y = orb.y | |
| ring_arc3.x = orb.x | |
| ring_arc3.y = orb.y | |
| lens_inner.x = orb.x | |
| lens_inner.y = orb.y | |
| # Highlight follows orb with slight lag for realism | |
| hl_offset = orb_r * 0.35 | |
| lens_highlight.x = orb.x - hl_offset + vx * 0.008 | |
| lens_highlight.y = orb.y + hl_offset - vy * 0.008 | |
| lens_highlight2.x = orb.x + hl_offset * 0.4 + vx * 0.005 | |
| lens_highlight2.y = orb.y - hl_offset * 0.6 - vy * 0.005 | |
| # ---------- Lens distortion: warp stars inside the lens ---------- | |
| # Optimized: use bounding box pre-check and stride through stars | |
| lens_r = orb_r - RING_THICKNESS # Inner lens radius | |
| lens_r_sq = lens_r * lens_r | |
| lens_bbox = lens_r + 5 # Slightly larger for bounding box check | |
| # Find stars inside/near the lens and create distorted copies | |
| star_idx = 0 | |
| # Stride through stars for performance (check every 2nd star) | |
| for star, ph in stars[::2]: | |
| if star_idx >= LENS_DISTORT_SAMPLES: | |
| break | |
| # Quick bounding box check first (cheaper than distance) | |
| dx = star.x - orb.x | |
| dy = star.y - orb.y | |
| if abs(dx) > lens_bbox or abs(dy) > lens_bbox: | |
| continue | |
| dist_sq = dx * dx + dy * dy | |
| if dist_sq < lens_r_sq: | |
| # Star is inside lens - apply barrel distortion (push outward from center) | |
| dist = math.sqrt(dist_sq) if dist_sq > 0 else 0.001 | |
| # Normalized distance from center (0 at center, 1 at edge) | |
| norm_dist = dist / lens_r | |
| # Barrel distortion: magnify more at center, less at edges | |
| magnify = LENS_MAGNIFICATION * (1.0 + 0.3 * (1.0 - norm_dist)) | |
| # Calculate distorted position (pushed outward from lens center) | |
| new_x = orb.x + dx * magnify | |
| new_y = orb.y + dy * magnify | |
| # Only show if still inside lens area | |
| new_dx = new_x - orb.x | |
| new_dy = new_y - orb.y | |
| if new_dx * new_dx + new_dy * new_dy < lens_r_sq: | |
| dc = distort_stars[star_idx] | |
| dc.x = new_x | |
| dc.y = new_y | |
| dc.radius = star.radius * magnify * 1.2 # Slightly larger | |
| dc.color = star.color | |
| dc.opacity = min(255, int(star.opacity * 1.3)) # Brighter | |
| star_idx += 1 | |
| # Hide unused distortion circles | |
| for i in range(star_idx, LENS_DISTORT_SAMPLES): | |
| distort_stars[i].opacity = 0 | |
| # ---------- Lens distortion: warp clouds inside the lens ---------- | |
| # Note: With blurred cloud sprites, the lens distortion for clouds is subtle | |
| # We'll use the sprite's approximate size for calculations | |
| cloud_idx = 0 | |
| cloud_check_r = lens_r + 150 # Clouds are big, use larger check radius | |
| # Stride through clouds for performance (check every 4th cloud) | |
| for sprite, cvx, cvy, ph, base_op in clouds[::4]: | |
| if cloud_idx >= 8: | |
| break | |
| dx = sprite.x - orb.x | |
| dy = sprite.y - orb.y | |
| # Quick bounding box check | |
| if abs(dx) > cloud_check_r or abs(dy) > cloud_check_r: | |
| continue | |
| dist_sq = dx * dx + dy * dy | |
| # Approximate cloud radius from sprite size | |
| cloud_radius = (sprite.width * sprite.scale) / 2 | |
| # Check if cloud overlaps with lens | |
| check_r = lens_r + cloud_radius * 0.3 | |
| if dist_sq < check_r * check_r: | |
| dist = math.sqrt(dist_sq) if dist_sq > 0 else 0.001 | |
| norm_dist = min(1.0, dist / lens_r) | |
| magnify = LENS_MAGNIFICATION * (1.0 + 0.2 * (1.0 - norm_dist)) | |
| dc = distort_clouds[cloud_idx] | |
| dc.x = orb.x + dx * magnify | |
| dc.y = orb.y + dy * magnify | |
| dc.radius = min(lens_r * 0.8, cloud_radius * magnify * 0.3) | |
| dc.color = blob.color | |
| dc.opacity = min(40, int(blob.opacity * 1.5)) | |
| cloud_idx += 1 | |
| # Hide unused cloud distortion circles | |
| for i in range(cloud_idx, 8): | |
| distort_clouds[i].opacity = 0 | |
| # Record trail sample (position + time) | |
| trail.appendleft((orb.x, orb.y, now)) | |
| # Render trail by age with exponential decay | |
| # alpha = exp(-age / tau) where tau derived from half-life | |
| tau = TRAIL_HALF_LIFE / math.log(2.0) # half-life -> time constant | |
| for i, tr in enumerate(trail_render): | |
| if i < len(trail): | |
| x, y, ts = trail[i] | |
| age = now - ts | |
| a = math.exp(-age / tau) # 1 -> 0 | |
| tr.x = x | |
| tr.y = y | |
| tr.opacity = int(140 * (a ** 1.4)) | |
| else: | |
| tr.opacity = 0 | |
| # Aggressive uncapped draw attempt | |
| window.dispatch_event("on_draw") | |
| window.flip() | |
| def stop_later(dt): | |
| global closing | |
| closing = True | |
| pyglet.app.exit() | |
| def main(): | |
| pyglet.clock.schedule(update) | |
| pyglet.clock.schedule_once(stop_later, RUN_SECONDS) | |
| gc_was_enabled = True | |
| try: | |
| gc_was_enabled = gc.isenabled() | |
| gc.disable() | |
| pyglet.app.run() | |
| finally: | |
| if gc_was_enabled: | |
| gc.enable() | |
| try: | |
| window.close() | |
| except Exception: | |
| pass | |
| # Summary | |
| if draw_times: | |
| fps, avg, p95, worst = compute_stats() | |
| print(f"\nSummary over ~{RUN_SECONDS:.1f}s (draw-based):") | |
| print(f"draw_frames={draw_frames}") | |
| print(f"draw_fps={fps:.1f}, avg_ms={avg*1000:.2f}, p95_ms={p95*1000:.2f}, worst_ms={worst*1000:.2f}") | |
| print("window size:", WIN_SIZE[0], WIN_SIZE[1]) | |
| if FB_SIZE is not None: | |
| print("framebuffer size:", FB_SIZE) | |
| if __name__ == "__main__": | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| pyglet |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import math | |
| import random | |
| import time | |
| from collections import deque | |
| import pyglet | |
| from pyglet import gl | |
| # -------------------- CONFIG -------------------- | |
| PARTICLES = 15_000 # More stars since they're smaller now | |
| VSYNC = False | |
| RUN_SECONDS = 30.0 | |
| # Update chunking - spread physics across N frames | |
| UPDATE_CHUNKS = 4 | |
| # ------------------------------------------------ | |
| # Create fullscreen window | |
| config = pyglet.gl.Config(double_buffer=True) | |
| try: | |
| window = pyglet.window.Window(fullscreen=True, vsync=VSYNC, config=config) | |
| except: | |
| window = pyglet.window.Window(fullscreen=True, vsync=VSYNC) | |
| WIDTH, HEIGHT = window.width, window.height | |
| print(f"Window size: {window.width}x{window.height}") | |
| print(f"Framebuffer size: {window.get_framebuffer_size()}") | |
| print(f"Pixel ratio: {window.get_framebuffer_size()[0] / window.width}") | |
| print(f"Renderer: {pyglet.gl.gl_info.get_renderer()}") | |
| batch = pyglet.graphics.Batch() | |
| def clamp(x, a, b): | |
| return a if x < a else b if x > b else x | |
| # Neon palette - bright saturated colors | |
| NEON_COLORS = [ | |
| (255, 80, 220), # Pink | |
| (210, 80, 255), # Magenta | |
| (120, 70, 255), # Purple | |
| ( 60, 150, 255), # Blue | |
| ( 80, 255, 255), # Cyan | |
| (100, 255, 150), # Green | |
| (255, 160, 60), # Orange | |
| ] | |
| # ---------- Particle class - tight glowing stars ---------- | |
| class Particle: | |
| __slots__ = ("x", "y", "vx", "vy", "phase", "glow", "core", "base_opacity") | |
| def __init__(self): | |
| self.x = random.uniform(0, WIDTH) | |
| self.y = random.uniform(0, HEIGHT) | |
| speed = random.uniform(40, 120) # Slower, more graceful | |
| angle = random.uniform(0, math.tau) | |
| self.vx = math.cos(angle) * speed | |
| self.vy = math.sin(angle) * speed | |
| color = random.choice(NEON_COLORS) | |
| self.phase = random.uniform(0, math.tau) | |
| # Tight star sizes - small core with subtle glow | |
| core_radius = random.uniform(1.0, 2.0) | |
| glow_radius = core_radius * 1.8 # Tighter glow ratio | |
| # Subtle outer glow | |
| self.glow = pyglet.shapes.Circle( | |
| self.x, self.y, glow_radius, | |
| segments=10, | |
| color=color, | |
| batch=batch | |
| ) | |
| self.glow.opacity = random.randint(30, 60) | |
| # Bright pinpoint core | |
| self.core = pyglet.shapes.Circle( | |
| self.x, self.y, core_radius, | |
| segments=8, | |
| color=color, | |
| batch=batch | |
| ) | |
| self.core.opacity = random.randint(200, 255) | |
| self.base_opacity = self.core.opacity | |
| def step(self, dt, t): | |
| dt_scaled = dt * UPDATE_CHUNKS | |
| self.x += self.vx * dt_scaled | |
| self.y += self.vy * dt_scaled | |
| if self.x < 0: | |
| self.x = 0 | |
| self.vx *= -1 | |
| elif self.x > WIDTH: | |
| self.x = WIDTH | |
| self.vx *= -1 | |
| if self.y < 0: | |
| self.y = 0 | |
| self.vy *= -1 | |
| elif self.y > HEIGHT: | |
| self.y = HEIGHT | |
| self.vy *= -1 | |
| self.glow.x = self.x | |
| self.glow.y = self.y | |
| self.core.x = self.x | |
| self.core.y = self.y | |
| # Gentle twinkling | |
| twinkle = 0.85 + 0.15 * math.sin(t * 4.0 + self.phase) | |
| self.core.opacity = int(self.base_opacity * twinkle) | |
| print(f"Creating {PARTICLES} smooth particles...") | |
| particles = [Particle() for _ in range(PARTICLES)] | |
| print("Particles created!") | |
| # Pre-slice particles into chunks for round-robin updates | |
| chunk_size = (PARTICLES + UPDATE_CHUNKS - 1) // UPDATE_CHUNKS | |
| particle_chunks = [particles[i:i + chunk_size] for i in range(0, PARTICLES, chunk_size)] | |
| current_chunk = 0 | |
| # ---------- Stats display ---------- | |
| label = pyglet.text.Label("", x=14, y=14, font_size=14) | |
| label.color = (255, 255, 255, 220) | |
| draw_times = deque(maxlen=3000) | |
| t_start = time.perf_counter() | |
| t_last_draw = None | |
| draw_frames = 0 | |
| closing = False | |
| def compute_stats(): | |
| dts = list(draw_times) | |
| dts.sort() | |
| avg = sum(dts) / len(dts) | |
| p95 = dts[int(0.95 * len(dts)) - 1] | |
| worst = dts[-1] | |
| fps = 1.0 / avg if avg > 0 else 0.0 | |
| return fps, avg, p95, worst | |
| @window.event | |
| def on_draw(): | |
| global t_last_draw, draw_frames, closing | |
| if closing: | |
| return | |
| now = time.perf_counter() | |
| if t_last_draw is not None: | |
| draw_times.append(now - t_last_draw) | |
| t_last_draw = now | |
| draw_frames += 1 | |
| # Set a very dark blue background instead of pure black | |
| # Pure black can trigger HDR display dithering/local dimming artifacts | |
| gl.glClearColor(0.01, 0.01, 0.02, 1.0) | |
| window.clear() | |
| batch.draw() | |
| if len(draw_times) > 60 and draw_frames % 10 == 0: | |
| fps, avg, p95, worst = compute_stats() | |
| label.text = f"draw_fps={fps:6.1f} avg_ms={avg*1000:6.2f} p95_ms={p95*1000:6.2f} worst_ms={worst*1000:6.2f} particles={PARTICLES}" | |
| label.draw() | |
| def update(dt): | |
| global current_chunk | |
| dt = clamp(dt, 0.0, 1/30) | |
| now = time.perf_counter() | |
| t = now - t_start | |
| # Update only one chunk of particles this frame | |
| for p in particle_chunks[current_chunk]: | |
| p.step(dt, t) | |
| current_chunk = (current_chunk + 1) % len(particle_chunks) | |
| window.dispatch_event("on_draw") | |
| window.flip() | |
| def stop_later(dt): | |
| global closing | |
| closing = True | |
| pyglet.app.exit() | |
| pyglet.clock.schedule(update) | |
| pyglet.clock.schedule_once(stop_later, RUN_SECONDS) | |
| try: | |
| pyglet.app.run() | |
| finally: | |
| try: | |
| window.close() | |
| except Exception: | |
| pass | |
| if draw_times: | |
| fps, avg, p95, worst = compute_stats() | |
| print(f"\nSummary over ~{RUN_SECONDS:.1f}s (draw-based):") | |
| print(f"draw_frames={draw_frames}") | |
| print(f"draw_fps={fps:.1f}, avg_ms={avg*1000:.2f}, p95_ms={p95*1000:.2f}, worst_ms={worst*1000:.2f}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment