Skip to content

Instantly share code, notes, and snippets.

@jac18281828
Last active January 16, 2026 22:59
Show Gist options
  • Select an option

  • Save jac18281828/6d71ddd61022357ea3b718cce8c5159a to your computer and use it in GitHub Desktop.

Select an option

Save jac18281828/6d71ddd61022357ea3b718cce8c5159a to your computer and use it in GitHub Desktop.
python rendering sample codes
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}")
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}")
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()
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()
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