| import pygame |
| import numpy as np |
| from flask import Flask, Response, render_template_string, request |
| import time |
| import os |
|
|
| |
| os.environ['SDL_VIDEODRIVER'] = 'dummy' |
| pygame.init() |
| pygame.mixer.init(frequency=44100, size=-16, channels=2) |
|
|
| app = Flask(__name__) |
|
|
| class ShaderRenderer: |
| def __init__(self, width=640, height=480): |
| self.width = width |
| self.height = height |
| self.mouse_x = width // 2 |
| self.mouse_y = height // 2 |
| self.start_time = time.time() |
| self.surface = pygame.Surface((width, height)) |
| |
| |
| self.sound_source = 'none' |
| self.pygame_sound = None |
| self.pygame_playing = False |
| self.sound_amp = 0.0 |
| |
| |
| if os.path.exists('sound.mp3'): |
| try: |
| self.pygame_sound = pygame.mixer.Sound('sound.mp3') |
| print("✅ Pygame sound loaded") |
| except: |
| print("⚠️ Could not load sound.mp3") |
| |
| def set_mouse(self, x, y): |
| self.mouse_x = max(0, min(self.width, x)) |
| self.mouse_y = max(0, min(self.height, y)) |
| |
| def set_sound_source(self, source): |
| """Change sound source: none, pygame, browser""" |
| self.sound_source = source |
| |
| |
| if source == 'pygame': |
| if self.pygame_sound and not self.pygame_playing: |
| self.pygame_sound.play(loops=-1) |
| self.pygame_playing = True |
| self.sound_amp = 0.5 |
| else: |
| if self.pygame_playing: |
| pygame.mixer.stop() |
| self.pygame_playing = False |
| self.sound_amp = 0.0 |
| |
| def render_frame(self): |
| """Render a simple pattern that shader will transform""" |
| t = time.time() - self.start_time |
| |
| |
| self.surface.fill((20, 20, 30)) |
| |
| |
| pygame.draw.rect(self.surface, (255, 100, 100), (10, 10, 100, 30)) |
| font = pygame.font.Font(None, 24) |
| text = font.render("TOP", True, (255, 255, 255)) |
| self.surface.blit(text, (20, 15)) |
| |
| |
| pygame.draw.rect(self.surface, (100, 255, 100), (10, self.height-40, 100, 30)) |
| text = font.render("BOTTOM", True, (0, 0, 0)) |
| self.surface.blit(text, (20, self.height-35)) |
| |
| |
| circle_size = 30 + int(20 * np.sin(t * 2)) |
| |
| |
| if self.sound_source == 'pygame': |
| color = (100, 255, 100) |
| elif self.sound_source == 'browser': |
| color = (100, 100, 255) |
| else: |
| color = (255, 100, 100) |
| |
| pygame.draw.circle(self.surface, color, |
| (self.mouse_x, self.mouse_y), circle_size) |
| |
| |
| for x in range(0, self.width, 50): |
| alpha = int(40 + 20 * np.sin(x * 0.1 + t)) |
| pygame.draw.line(self.surface, (alpha, alpha, 50), |
| (x, 0), (x, self.height)) |
| for y in range(0, self.height, 50): |
| alpha = int(40 + 20 * np.cos(y * 0.1 + t)) |
| pygame.draw.line(self.surface, (alpha, alpha, 50), |
| (0, y), (self.width, y)) |
| |
| |
| meter_width = int(200 * self.sound_amp) |
| pygame.draw.rect(self.surface, (60, 60, 60), (self.width-220, 10, 200, 20)) |
| pygame.draw.rect(self.surface, (100, 255, 100), |
| (self.width-220, 10, meter_width, 20)) |
| |
| |
| shader_text = font.render("SHADER ON", True, (255, 255, 0)) |
| self.surface.blit(shader_text, (self.width-150, self.height-30)) |
| |
| return pygame.image.tostring(self.surface, 'RGB') |
| |
| def get_frame(self): |
| return self.render_frame() |
|
|
| renderer = ShaderRenderer() |
|
|
| @app.route('/') |
| def index(): |
| return render_template_string(''' |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>Pygame + WebGL Shader + Sound</title> |
| <style> |
| body { |
| margin: 0; |
| background: #0a0a0a; |
| color: white; |
| font-family: Arial, sans-serif; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| min-height: 100vh; |
| } |
| |
| .container { |
| text-align: center; |
| } |
| |
| canvas { |
| width: 640px; |
| height: 480px; |
| border: 3px solid #4CAF50; |
| border-radius: 8px; |
| cursor: crosshair; |
| display: block; |
| margin: 20px 0; |
| } |
| |
| .controls { |
| background: #1a1a1a; |
| padding: 20px; |
| border-radius: 8px; |
| margin-top: 20px; |
| } |
| |
| .button-group { |
| display: flex; |
| gap: 10px; |
| justify-content: center; |
| margin: 15px 0; |
| flex-wrap: wrap; |
| } |
| |
| button { |
| background: #333; |
| color: white; |
| border: none; |
| padding: 12px 24px; |
| font-size: 16px; |
| border-radius: 5px; |
| cursor: pointer; |
| transition: all 0.3s; |
| min-width: 120px; |
| } |
| |
| button:hover { |
| transform: scale(1.05); |
| } |
| |
| button.active { |
| background: #4CAF50; |
| box-shadow: 0 0 20px #4CAF50; |
| } |
| |
| button.shader.active { |
| background: #ffaa00; |
| color: black; |
| } |
| |
| button.sound.active { |
| background: #4CAF50; |
| } |
| |
| button.browser.active { |
| background: #2196F3; |
| } |
| |
| .status-bar { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-top: 15px; |
| padding: 10px; |
| background: #222; |
| border-radius: 5px; |
| } |
| |
| .indicator { |
| padding: 5px 10px; |
| border-radius: 4px; |
| font-size: 14px; |
| } |
| |
| .indicator.none { background: #444; } |
| .indicator.pygame { background: #4CAF50; } |
| .indicator.browser { background: #2196F3; } |
| .indicator.shader-on { background: #ffaa00; color: black; } |
| .indicator.shader-off { background: #666; } |
| |
| .meter { |
| width: 200px; |
| height: 20px; |
| background: #333; |
| border-radius: 10px; |
| overflow: hidden; |
| } |
| |
| .meter-fill { |
| height: 100%; |
| width: 0%; |
| background: linear-gradient(90deg, #4CAF50, #2196F3); |
| transition: width 0.05s; |
| } |
| |
| .badge { |
| display: inline-block; |
| padding: 5px 10px; |
| border-radius: 4px; |
| margin-left: 10px; |
| font-size: 12px; |
| } |
| |
| .badge.on { |
| background: #4CAF50; |
| color: white; |
| } |
| |
| .badge.off { |
| background: #ff4444; |
| color: white; |
| } |
| |
| .color-sample { |
| display: inline-block; |
| width: 12px; |
| height: 12px; |
| border-radius: 3px; |
| margin: 0 5px; |
| } |
| |
| .info-text { |
| font-size: 12px; |
| color: #666; |
| margin-top: 15px; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>🎮 Pygame + WebGL Shader + Sound</h1> |
| |
| <canvas id="canvas" width="640" height="480"></canvas> |
| |
| <div class="controls"> |
| <div class="button-group"> |
| <button id="shaderBtn" class="shader active" onclick="toggleShader()"> |
| 🔮 SHADER ON |
| </button> |
| <span id="shaderBadge" class="badge on">EFFECTS ACTIVE</span> |
| </div> |
| |
| <h3>🔊 Sound Source</h3> |
| <div class="button-group"> |
| <button id="btnNone" class="sound active" onclick="setSoundSource('none')">🔇 None</button> |
| <button id="btnPygame" class="sound" onclick="setSoundSource('pygame')">🎮 Pygame</button> |
| <button id="btnBrowser" class="sound" onclick="setSoundSource('browser')">🌐 Browser</button> |
| </div> |
| |
| <div class="status-bar"> |
| <div> |
| <span id="sourceIndicator" class="indicator none">Source: None</span> |
| <span id="shaderIndicator" class="indicator shader-on" style="margin-left: 10px;">Shader: ON</span> |
| </div> |
| <div class="meter"> |
| <div id="soundMeter" class="meter-fill"></div> |
| </div> |
| </div> |
| |
| <div class="info-text"> |
| <span class="color-sample" style="background: #ff6464;"></span> No sound |
| <span class="color-sample" style="background: #64ff64;"></span> Pygame sound |
| <span class="color-sample" style="background: #6464ff;"></span> Browser sound |
| | <span style="color: #ff6464;">🔴 TOP</span> marker should be at top |
| | <span style="color: #64ff64;">🟢 BOTTOM</span> at bottom |
| </div> |
| </div> |
| </div> |
| |
| <!-- Audio element for browser sound --> |
| <audio id="browserAudio" loop style="display:none;"> |
| <source src="/static/sound.mp3" type="audio/mpeg"> |
| </audio> |
| |
| <!-- WebGL Shader --> |
| <script id="vertex-shader" type="x-shader/x-vertex"> |
| attribute vec2 position; |
| varying vec2 vUv; |
| void main() { |
| // Flip Y coordinate to fix Pygame orientation |
| vUv = vec2(position.x * 0.5 + 0.5, 1.0 - (position.y * 0.5 + 0.5)); |
| gl_Position = vec4(position, 0.0, 1.0); |
| } |
| </script> |
| |
| <script id="fragment-shader" type="x-shader/x-fragment"> |
| precision highp float; |
| |
| uniform sampler2D uTexture; |
| uniform float uTime; |
| uniform vec2 uMouse; |
| uniform vec2 uResolution; |
| uniform bool uShaderEnabled; |
| |
| varying vec2 vUv; |
| |
| void main() { |
| // Get pixel from pygame texture (now correctly oriented) |
| vec4 color = texture2D(uTexture, vUv); |
| |
| if (uShaderEnabled) { |
| // SHADER EFFECTS ON |
| |
| // 1. Time-based color shift |
| color.r += sin(uTime + vUv.x * 10.0) * 0.2; |
| color.g += cos(uTime + vUv.y * 10.0) * 0.2; |
| |
| // 2. Mouse ripple effect |
| float dist = distance(vUv, uMouse); |
| if (dist < 0.2) { |
| float ripple = sin(dist * 50.0 - uTime * 5.0) * 0.5 + 0.5; |
| color.rgb += vec3(0.5, 0.2, 0.8) * ripple * 0.5; |
| } |
| |
| // 3. Scanlines |
| float scanline = sin(vUv.y * uResolution.y * 2.0 + uTime * 10.0) * 0.1 + 0.9; |
| color.rgb *= scanline; |
| |
| // 4. Edge glow |
| float edge = 1.0 - abs(vUv.x - 0.5) * 2.0; |
| edge *= 1.0 - abs(vUv.y - 0.5) * 2.0; |
| color.rgb += vec3(0.2, 0.1, 0.5) * edge * sin(uTime) * 0.3; |
| } |
| // else: SHADER EFFECTS OFF - pure Pygame pixels |
| |
| gl_FragColor = color; |
| } |
| </script> |
| |
| <script> |
| const canvas = document.getElementById('canvas'); |
| const gl = canvas.getContext('webgl'); |
| const browserAudio = document.getElementById('browserAudio'); |
| |
| if (!gl) { |
| alert('WebGL not supported!'); |
| } |
| |
| let texture = gl.createTexture(); |
| let startTime = Date.now() / 1000; |
| let shaderEnabled = true; |
| let mouse = [0.5, 0.5]; |
| let currentSource = 'none'; |
| |
| // UI Elements |
| const shaderBtn = document.getElementById('shaderBtn'); |
| const shaderBadge = document.getElementById('shaderBadge'); |
| const shaderIndicator = document.getElementById('shaderIndicator'); |
| |
| function toggleShader() { |
| shaderEnabled = !shaderEnabled; |
| |
| // Update UI |
| if (shaderEnabled) { |
| shaderBtn.className = 'shader active'; |
| shaderBtn.innerHTML = '🔮 SHADER ON'; |
| shaderBadge.className = 'badge on'; |
| shaderBadge.innerHTML = 'EFFECTS ACTIVE'; |
| shaderIndicator.className = 'indicator shader-on'; |
| shaderIndicator.innerHTML = 'Shader: ON'; |
| } else { |
| shaderBtn.className = ''; |
| shaderBtn.innerHTML = '🎮 SHADER OFF'; |
| shaderBadge.className = 'badge off'; |
| shaderBadge.innerHTML = 'PURE PYGAME'; |
| shaderIndicator.className = 'indicator shader-off'; |
| shaderIndicator.innerHTML = 'Shader: OFF'; |
| } |
| |
| // Update shader uniform |
| if (program) { |
| gl.useProgram(program); |
| const uShaderEnabled = gl.getUniformLocation(program, 'uShaderEnabled'); |
| gl.uniform1i(uShaderEnabled, shaderEnabled); |
| } |
| } |
| |
| // Sound meter animation |
| let soundAmp = 0; |
| function updateSoundMeter() { |
| if (currentSource === 'browser' && !browserAudio.paused) { |
| // Simulate amplitude from browser audio |
| soundAmp = 0.3 + 0.2 * Math.sin(Date.now() * 0.01); |
| document.getElementById('soundMeter').style.width = (soundAmp * 100) + '%'; |
| } else if (currentSource === 'pygame') { |
| // Get amplitude from server |
| fetch('/sound/amp') |
| .then(res => res.json()) |
| .then(data => { |
| soundAmp = data.amp; |
| document.getElementById('soundMeter').style.width = (soundAmp * 100) + '%'; |
| }); |
| } else { |
| soundAmp = 0; |
| document.getElementById('soundMeter').style.width = '0%'; |
| } |
| requestAnimationFrame(updateSoundMeter); |
| } |
| updateSoundMeter(); |
| |
| function setSoundSource(source) { |
| currentSource = source; |
| |
| // Update UI |
| document.getElementById('btnNone').className = source === 'none' ? 'sound active' : 'sound'; |
| document.getElementById('btnPygame').className = source === 'pygame' ? 'sound active' : 'sound'; |
| document.getElementById('btnBrowser').className = source === 'browser' ? 'sound active' : 'sound'; |
| |
| const indicator = document.getElementById('sourceIndicator'); |
| indicator.className = `indicator ${source}`; |
| indicator.innerHTML = `Source: ${source.charAt(0).toUpperCase() + source.slice(1)}`; |
| |
| // Handle audio |
| if (source === 'browser') { |
| browserAudio.play().catch(e => console.log('Audio play failed:', e)); |
| } else { |
| browserAudio.pause(); |
| browserAudio.currentTime = 0; |
| } |
| |
| // Tell server about source change |
| fetch('/sound/source', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({source: source}) |
| }); |
| } |
| |
| // Mouse tracking |
| canvas.addEventListener('mousemove', (e) => { |
| const rect = canvas.getBoundingClientRect(); |
| mouse[0] = (e.clientX - rect.left) / rect.width; |
| mouse[1] = 1.0 - (e.clientY - rect.top) / rect.height; |
| |
| const x = Math.round((e.clientX - rect.left) * 640 / rect.width); |
| const y = Math.round((e.clientY - rect.top) * 480 / rect.height); |
| |
| fetch('/mouse', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({x, y}) |
| }); |
| }); |
| |
| // Setup WebGL |
| function createShader(type, source) { |
| const shader = gl.createShader(type); |
| gl.shaderSource(shader, source); |
| gl.compileShader(shader); |
| return shader; |
| } |
| |
| // Compile shaders |
| const vertexShader = createShader(gl.VERTEX_SHADER, |
| document.getElementById('vertex-shader').textContent); |
| const fragmentShader = createShader(gl.FRAGMENT_SHADER, |
| document.getElementById('fragment-shader').textContent); |
| |
| // Create program |
| const program = gl.createProgram(); |
| gl.attachShader(program, vertexShader); |
| gl.attachShader(program, fragmentShader); |
| gl.linkProgram(program); |
| gl.useProgram(program); |
| |
| // Set up fullscreen quad |
| const vertices = new Float32Array([ |
| -1, -1, 1, -1, -1, 1, |
| -1, 1, 1, -1, 1, 1 |
| ]); |
| |
| const buffer = gl.createBuffer(); |
| gl.bindBuffer(gl.ARRAY_BUFFER, buffer); |
| gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); |
| |
| const position = gl.getAttribLocation(program, 'position'); |
| gl.enableVertexAttribArray(position); |
| gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); |
| |
| // Get uniform locations |
| const uTexture = gl.getUniformLocation(program, 'uTexture'); |
| const uTime = gl.getUniformLocation(program, 'uTime'); |
| const uMouse = gl.getUniformLocation(program, 'uMouse'); |
| const uResolution = gl.getUniformLocation(program, 'uResolution'); |
| const uShaderEnabled = gl.getUniformLocation(program, 'uShaderEnabled'); |
| |
| gl.uniform1i(uTexture, 0); |
| gl.uniform2f(uResolution, canvas.width, canvas.height); |
| gl.uniform1i(uShaderEnabled, shaderEnabled); |
| |
| // Texture setup |
| gl.bindTexture(gl.TEXTURE_2D, texture); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); |
| |
| // Main loop |
| function update() { |
| fetch('/frame') |
| .then(res => res.arrayBuffer()) |
| .then(buffer => { |
| // Update texture with new frame |
| gl.bindTexture(gl.TEXTURE_2D, texture); |
| gl.texImage2D( |
| gl.TEXTURE_2D, 0, gl.RGB, 640, 480, 0, |
| gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array(buffer) |
| ); |
| |
| // Update uniforms |
| const time = (Date.now() / 1000) - startTime; |
| gl.uniform1f(uTime, time); |
| gl.uniform2f(uMouse, mouse[0], mouse[1]); |
| |
| // Draw |
| gl.drawArrays(gl.TRIANGLES, 0, 6); |
| |
| requestAnimationFrame(update); |
| }); |
| } |
| |
| update(); |
| </script> |
| </body> |
| </html> |
| ''') |
|
|
| @app.route('/frame') |
| def frame(): |
| """Return raw RGB bytes""" |
| return Response(renderer.get_frame(), mimetype='application/octet-stream') |
|
|
| @app.route('/mouse', methods=['POST']) |
| def mouse(): |
| data = request.json |
| renderer.set_mouse(data['x'], data['y']) |
| return 'OK' |
|
|
| @app.route('/sound/source', methods=['POST']) |
| def sound_source(): |
| data = request.json |
| renderer.set_sound_source(data['source']) |
| return 'OK' |
|
|
| @app.route('/sound/amp') |
| def sound_amp(): |
| return {'amp': renderer.sound_amp} |
|
|
| |
| @app.route('/static/sound.mp3') |
| def serve_sound(): |
| if os.path.exists('sound.mp3'): |
| with open('sound.mp3', 'rb') as f: |
| return Response(f.read(), mimetype='audio/mpeg') |
| return 'Sound not found', 404 |
|
|
| if __name__ == '__main__': |
| print("\n" + "="*70) |
| print("🎮 Pygame + WebGL Shader + Sound") |
| print("="*70) |
| print("🌐 http://localhost:5000") |
| print("\n🔮 Shader Toggle: See pure Pygame vs. effects") |
| print("🔊 Sound Sources:") |
| print(" • None - No sound") |
| print(" • Pygame - sound.mp3 plays in Pygame (streamed)") |
| print(" • Browser - sound.mp3 plays in browser") |
| print("\n🎯 Orientation fixed: TOP at top, BOTTOM at bottom") |
| print(" Circle color indicates sound source") |
| print(" Sound meter shows activity") |
| print("="*70 + "\n") |
| app.run(host='0.0.0.0', port=5000, debug=False) |