HTML/JS Plasma Demoscene
An interactive Web Audio-enabled demoscene featuring plasma effects, particle systems, and dynamic audio visualization. Includes fullscreen toggle, audio synthesis, and real-time animation.
demoscene.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Demoscene — HTML + JS</title>
<style>
:root{--bg:#0b0510;--accent:#8ef;--muted:rgba(255,255,255,0.06)}
html,body{height:100%;margin:0;background:var(--bg);color:#fff;font-family:system-ui,sans-serif}
canvas{display:block;width:100vw;height:100vh}
.ui{position:fixed;left:12px;top:12px;z-index:50;backdrop-filter:blur(6px);background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);padding:10px;border-radius:10px;font-size:13px}
.ui button{background:transparent;border:1px solid rgba(255,255,255,0.06);color:inherit;padding:6px 8px;border-radius:6px;cursor:pointer}
.ui .hint{opacity:.7;font-size:12px;margin-top:6px}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="ui">
<div style="display:flex;gap:8px;align-items:center">
<button id="toggle">Pause</button>
<button id="audioBtn">Play Audio</button>
<button id="fs">Fullscreen</button>
</div>
<div class="hint">F = fullscreen, S = toggle animation, A = toggle audio</div>
</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W = canvas.width = innerWidth;
let H = canvas.height = innerHeight;
window.addEventListener('resize', ()=>{W = canvas.width = innerWidth;H = canvas.height = innerHeight;});
let running = true;
let t0 = performance.now();
let time = 0;
const PARTICLE_COUNT = 180;
const particles = [];
for(let i=0;i<PARTICLE_COUNT;i++){
particles.push({x: Math.random()*W,y: Math.random()*H,vx: (Math.random()-0.5)*0.6,vy: (Math.random()-0.5)*0.6,r: Math.random()*2.6+0.6,life: Math.random()*200+100,hueOffset: Math.random()*360});
}
let audioCtx = null;
let analyser = null;
let dataArray = null;
let audioOn = false;
let source = null;
function initAudio(){
if(audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 512;
const bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
const osc1 = audioCtx.createOscillator(); osc1.type = 'sine'; osc1.frequency.value = 110;
const osc2 = audioCtx.createOscillator(); osc2.type = 'sawtooth'; osc2.frequency.value = 220;
const gain = audioCtx.createGain(); gain.gain.value = 0.08;
const gain2 = audioCtx.createGain(); gain2.gain.value = 0.03;
const noise = audioCtx.createBufferSource();
const buffer = audioCtx.createBuffer(1, audioCtx.sampleRate * 2, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for(let i=0;i<data.length;i++) data[i] = (Math.random()*2-1) * 0.25;
noise.buffer = buffer; noise.loop = true;
const noiseGain = audioCtx.createGain(); noiseGain.gain.value = 0.02;
osc1.connect(gain);
osc2.connect(gain2);
noise.connect(noiseGain);
const out = audioCtx.createGain(); out.gain.value = 0.9;
gain.connect(out); gain2.connect(out); noiseGain.connect(out);
out.connect(analyser);
analyser.connect(audioCtx.destination);
osc1.start(); osc2.start(); noise.start();
source = {osc1,osc2,noise,out,gain,gain2,noiseGain};
}
function toggleAudio(){
if(!audioCtx) initAudio();
if(!audioOn){
if(audioCtx.state === 'suspended') audioCtx.resume();
audioOn = true; document.getElementById('audioBtn').textContent = 'Stop Audio';
} else {
audioOn = false; document.getElementById('audioBtn').textContent = 'Play Audio';
if(audioCtx) audioCtx.suspend();
}
}
document.getElementById('audioBtn').addEventListener('click', toggleAudio);
document.getElementById('toggle').addEventListener('click', ()=>{running = !running;document.getElementById('toggle').textContent = running? 'Pause' : 'Resume';if(running) requestAnimationFrame(loop);});
document.getElementById('fs').addEventListener('click', ()=>{if(document.fullscreenElement) document.exitFullscreen(); else document.documentElement.requestFullscreen();});
window.addEventListener('keydown',(e)=>{
if(e.key.toLowerCase()==='f'){if(document.fullscreenElement) document.exitFullscreen(); else document.documentElement.requestFullscreen();}
if(e.key.toLowerCase()==='s'){running = !running;document.getElementById('toggle').textContent = running? 'Pause' : 'Resume';if(running) requestAnimationFrame(loop);}
if(e.key.toLowerCase()==='a') toggleAudio();
});
function palette(a){
const r = Math.sin(0.3*a + 0) * 0.5 + 0.5;
const g = Math.sin(0.3*a + 2) * 0.5 + 0.5;
const b = Math.sin(0.3*a + 4) * 0.5 + 0.5;
return [Math.floor(r*255),Math.floor(g*255),Math.floor(b*255)];
}
function drawPlasma(ctx, w, h, t, intensity){
const img = ctx.createImageData(w, h);
const data = img.data;
const cx = w/2, cy = h/2;
const scale = 0.0045 * (1 + 0.3*Math.sin(t*0.6));
for(let y=0;y<h;y++){
for(let x=0;x<w;x++){
const i = (y*w + x) * 4;
const dx = x - cx;
const dy = y - cy;
const d = Math.sqrt(dx*dx + dy*dy);
const v = Math.sin((dx*scale*80) + t*1.2) + Math.cos((dy*scale*80) - t*1.3) + Math.sin(d*scale*40 - t*0.7);
const col = palette((v + t*0.8) * 20);
data[i] = Math.min(255, col[0] * (0.6 + intensity));
data[i+1] = Math.min(255, col[1] * (0.6 + intensity*0.9));
data[i+2] = Math.min(255, col[2] * (0.6 + intensity*0.7));
data[i+3] = 220;
}
}
ctx.putImageData(img, 0, 0);
}
function drawParticles(ctx, t, intensity){
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for(const p of particles){
p.x += p.vx * (1 + intensity*3);
p.y += p.vy * (1 + intensity*3);
p.life -= 1 + intensity*2;
if(p.x < -20) p.x = W+20; if(p.x > W+20) p.x = -20;
if(p.y < -20) p.y = H+20; if(p.y > H+20) p.y = -20;
if(p.life <= 0){
p.x = Math.random()*W; p.y = Math.random()*H; p.life = Math.random()*200+100; p.vx = (Math.random()-0.5)*0.6; p.vy = (Math.random()-0.5)*0.6;
}
const hue = (t*40 + p.hueOffset) % 360;
ctx.beginPath();
const rad = p.r * (1 + intensity*2);
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, rad*6);
const c1 = `hsla(${hue},90%,60%,0.9)`;
const c2 = `hsla(${(hue+50)%360},80%,45%,0.02)`;
grad.addColorStop(0,c1);
grad.addColorStop(1,c2);
ctx.fillStyle = grad;
ctx.arc(p.x,p.y,rad*6,0,Math.PI*2);
ctx.fill();
}
ctx.restore();
}
function drawTunnel(ctx, w, h, t, intensity){
ctx.save();
ctx.translate(w/2,h/2);
ctx.rotate(t*0.1);
const layers = 40;
for(let i=layers;i>0;i--){
const frac = i/layers;
const r = Math.pow(frac,0.6) * Math.min(w,h) * 0.9;
ctx.beginPath();
ctx.lineWidth = Math.max(1, (1-frac)*12);
ctx.strokeStyle = `hsla(${(t*40 + i*8) % 360},80%,55%,${0.06 + (1-frac)*0.18 * (0.6+intensity)})`;
ctx.arc(0,0,r,0,Math.PI*2);
ctx.stroke();
}
ctx.restore();
}
function loop(now){
if(!running) return;
const dt = (now - t0) / 1000;
t0 = now;
time += dt;
let intensity = 0;
if(analyser && audioOn){
analyser.getByteFrequencyData(dataArray);
let sum = 0; let count = 0;
for(let i=2;i<60;i++){ sum += dataArray[i]; count++; }
const avg = (sum/count) / 255;
intensity = Math.pow(avg,1.6);
}
ctx.fillStyle = 'rgba(6,5,8,0.18)';
ctx.fillRect(0,0,W,H);
const offW = Math.max(160, Math.floor(W/2));
const offH = Math.max(120, Math.floor(H/2));
const off = document.createElement('canvas'); off.width = offW; off.height = offH;
const offCtx = off.getContext('2d');
drawPlasma(offCtx, offW, offH, time*0.7, intensity);
ctx.save();
const sx = 1 + Math.sin(time*0.6)*0.015 * (1+intensity*2);
const sy = 1 + Math.cos(time*0.7)*0.015 * (1+intensity*2);
ctx.globalAlpha = 0.95;
ctx.drawImage(off, 0, 0, offW, offH, (1-sx)/2*W, (1-sy)/2*H, W*sx, H*sy);
ctx.restore();
drawTunnel(ctx,W,H,time,intensity);
drawParticles(ctx,time,intensity);
const g = ctx.createRadialGradient(W/2,H/2,Math.min(W,H)*0.15,W/2,H/2,Math.max(W,H));
g.addColorStop(0,'rgba(0,0,0,0)');
g.addColorStop(1,'rgba(0,0,0,0.5)');
ctx.fillStyle = g; ctx.fillRect(0,0,W,H);
ctx.fillStyle = 'rgba(255,255,255,0.06)';
ctx.fillRect(12,H-60,220,44);
ctx.fillStyle = 'rgba(255,255,255,0.65)';
ctx.font = '12px system-ui,Segoe UI,Roboto';
ctx.fillText('Demoscene — HTML + JS', 22, H-36);
ctx.fillStyle = 'rgba(255,255,255,0.45)';
ctx.fillText('Press F fullscreen • A toggle audio • S pause', 22, H-20);
requestAnimationFrame(loop);
}
requestAnimationFrame((ts)=>{ t0 = ts; loop(ts); });
window._demoscene = {toggleAudio, start: ()=>{running=true; requestAnimationFrame(loop)}, stop: ()=>{running=false}};
</script>
</body>
</html>
Usage Instructions
- Copy the entire code above
- Save as an HTML file (e.g.,
demoscene.html) - Open in any modern browser (Chrome, Firefox, Edge recommended)
- Controls:
- F - Toggle fullscreen
- A - Toggle audio synthesis
- S - Pause/resume animation
- Mouse - UI buttons for controls
Tkinter Sinusoids Demoscene
A Python/Tkinter demoscene featuring animated sinusoidal waves with dynamic colors, particle effects, and interactive controls for speed adjustment and fullscreen toggling.
demoscene_tk.py
import tkinter as tk
import time
import math
import random
def hsv_to_hex(h,s,v):
h_i = int(h*6)
f = h*6 - h_i
p = v*(1-s)
q = v*(1-f*s)
t = v*(1-(1-f)*s)
r,g,b = [(v,t,p),(q,v,p),(p,v,t),(p,q,v),(t,p,v),(v,p,q)][h_i%6]
return '#%02x%02x%02x' % (int(r*255),int(g*255),int(b*255))
class Demoscene:
def __init__(self):
self.root = tk.Tk()
self.root.title("Tkinter Demoscene — Sinusoids")
self.fullscreen = False
self.w = 1200
self.h = 700
self.canvas = tk.Canvas(self.root, width=self.w, height=self.h, bg='#07030a', highlightthickness=0)
self.canvas.pack(fill=tk.BOTH, expand=True)
self.root.bind('<Configure>', self.on_resize)
self.root.bind('<space>', self.toggle)
self.root.bind('f', self.toggle_full)
self.root.bind('F', self.toggle_full)
self.root.bind('<Up>', self.speed_up)
self.root.bind('<Down>', self.speed_down)
self.paused = False
self.start = time.perf_counter()
self.speed = 1.0
self.layers = []
self.layer_count = 8
for i in range(self.layer_count):
hue = random.random()
color = hsv_to_hex(hue,0.8,0.9)
amplitude = (8 + i*6)
freq = 0.8 + i*0.15
phase = random.random()*math.pi*2
thickness = 1 + (i%3)
self.layers.append({'hue':hue,'color':color,'amp':amplitude,'freq':freq,'phase':phase,'thick':thickness})
self.trail = []
self.max_trail = 12
self.animate()
def on_resize(self,event):
if event.width>10 and event.height>10:
self.w = event.width
self.h = event.height
self.canvas.config(width=self.w, height=self.h)
def toggle(self,event=None):
self.paused = not self.paused
if not self.paused:
self.start = time.perf_counter() - self.t_offset
def toggle_full(self,event=None):
self.fullscreen = not self.fullscreen
self.root.attributes('-fullscreen', self.fullscreen)
def speed_up(self,event=None):
self.speed *= 1.15
def speed_down(self,event=None):
self.speed /= 1.15
def draw_wave(self,layer,t):
pts = []
amp = layer['amp'] * (1 + 0.35*math.sin(t*0.9 + layer['phase']))
freq = layer['freq']
phase = t*freq*1.6 + layer['phase']
mid = self.h/2
step = max(2, int(self.w/600))
for x in range(0,self.w+1,step):
nx = x/self.w*math.pi*2
y = mid + math.sin(nx* (1.6+freq) + phase + x*0.003)*amp + math.sin((x*0.01)+t*1.2+layer['phase'])* (amp*0.35)
pts.append(x); pts.append(y)
id = self.canvas.create_line(*pts, fill=layer['color'], width=layer['thick'], smooth=True, splinesteps=8)
return id
def fade_background(self):
self.canvas.create_rectangle(0,0,self.w,self.h, fill='#07030a', outline='')
def animate(self):
now = time.perf_counter()
if self.paused:
if not hasattr(self,'t_offset'):
self.t_offset = now - self.start
self.root.after(30, self.animate)
return
self.t_offset = 0
t = (now - self.start) * self.speed
self.canvas.delete('all')
glow_count = 6
for i in range(glow_count):
alpha = int(8 + i*6)
shade = '#%02x%02x%02x' % (alpha, int(alpha*0.7), int(alpha*0.4))
self.canvas.create_oval(-self.w*0.6+i*40, -self.h*0.6+i*30, self.w*1.6-i*40, self.h*1.6-i*30, fill=shade, outline='')
for idx,layer in enumerate(self.layers):
layer_id = self.draw_wave(layer, t + idx*0.13)
self.canvas.itemconfigure(layer_id, stipple='')
particle_count = 90
for i in range(particle_count):
x = (i/particle_count)*self.w + math.sin(t*0.9 + i)*12
y = self.h*0.5 + math.cos((i*0.2)+t*1.6)* (30 + math.sin(i*0.4+t*0.7)*40)
r = 1 + (math.sin(t*2 + i)*0.6 + 0.6)*2
hue = (0.6 + math.sin(i*0.1 + t*0.4)*0.2) % 1.0
color = hsv_to_hex(hue,0.7,0.95)
self.canvas.create_oval(x-r,y-r,x+r,y+r, fill=color, outline='')
self.canvas.create_rectangle(12,self.h-62,260,self.h-12, fill='black', outline='', stipple='')
self.canvas.create_text(22,self.h-46, anchor='w', text='Tkinter Demoscene — Sinusoids', fill='#e7eefc', font=('Helvetica',12,'bold'))
self.canvas.create_text(22,self.h-28, anchor='w', text='Space pause/resume F fullscreen Up/Down speed', fill='#cfe0ff', font=('Helvetica',10))
self.root.after(16, self.animate)
if __name__ == '__main__':
Demoscene()
tk.mainloop()
Usage Instructions
- Save the code as
demoscene_tk.py - Run with Python 3:
python demoscene_tk.py - Requirements: Python 3 with Tkinter (usually included)
- Controls:
- Space - Pause/resume animation
- F - Toggle fullscreen
- Up Arrow - Increase animation speed
- Down Arrow - Decrease animation speed
Particle Attractor Demoscene
Interactive particle system where particles are attracted to mouse cursor. Features dynamic colors, smooth particle physics, and real-time mouse interaction.
demoscene_particles.py
import tkinter as tk, time, math, random
def hsv(h,s,v):
i=int(h*6);f=h*6-i;p=v*(1-s);q=v*(1-f*s);t=v*(1-(1-f)*s)
r,g,b=[(v,t,p),(q,v,p),(p,v,t),(p,q,v),(t,p,v),(v,p,q)][i%6]
return '#%02x%02x%02x'%(int(r*255),int(g*255),int(b*255))
class P:
def __init__(self):
self.root=tk.Tk();self.root.title('Particle Attractor');self.w=1100;self.h=700
self.c=tk.Canvas(self.root,width=self.w,height=self.h,bg='#030113',highlightthickness=0);self.c.pack(fill=tk.BOTH,expand=True)
self.root.bind('<Configure>',self.resize);self.root.bind('<space>',self.toggle);self.root.bind('f',self.full)
self.particles=[{'x':random.random()*self.w,'y':random.random()*self.h,'vx':0,'vy':0,'h':random.random()} for _ in range(220)]
self.mx=self.w/2;self.my=self.h/2;self.root.bind('<Motion>',lambda e: [setattr(self,'mx',e.x),setattr(self,'my',e.y)])
self.start=time.perf_counter();self.paused=False;self.loop()
def resize(self,e):self.w=e.width;self.h=e.height;self.c.config(width=self.w,height=self.h)
def toggle(self,e=None):self.paused=not self.paused
def full(self,e=None):self.root.attributes('-fullscreen', not self.root.attributes('-fullscreen'))
def loop(self):
now=time.perf_counter()
if not self.paused:
self.c.delete('all')
for p in self.particles:
dx=self.mx-p['x'];dy=self.my-p['y'];d=math.hypot(dx,dy)+1
fx=dx/d**1.3;fy=dy/d**1.3
p['vx']=p['vx']*0.92+fx*2; p['vy']=p['vy']*0.92+fy*2
p['x']+=p['vx'];p['y']+=p['vy']
if p['x']<0: p['x']=self.w
if p['x']>self.w: p['x']=0
if p['y']<0: p['y']=self.h
if p['y']>self.h: p['y']=0
r=1.2+abs(p['vx'])*1.3
col=hsv((p['h']+0.2*math.sin(now+p['x']*0.01))%1,0.8,0.95)
self.c.create_oval(p['x']-r,p['y']-r,p['x']+r,p['y']+r,fill=col,outline='')
self.c.create_text(18,self.h-40,anchor='w',text='Particle Attractor — move mouse to attract • Space pause • F fullscreen',fill='#dfe',font=('Helvetica',12))
self.root.after(16,self.loop)
if __name__=='__main__':
P();tk.mainloop()
Usage Instructions
- Save as
demoscene_particles.py - Run with:
python demoscene_particles.py - Move your mouse to attract particles
- Controls:
- Mouse movement - Attract particles to cursor
- Space - Pause/resume animation
- F - Toggle fullscreen
Cartesian Demo
Simple Cartesian coordinate system demo featuring animated waves, rotating points, and line animations with colorful visual effects.
CartesianDemo.py
import tkinter as tk
import math
import random
root = tk.Tk()
root.title("Cartesian Demoscene Show")
canvas = tk.Canvas(root, width=800, height=500, bg="black")
canvas.pack()
width, height = 800, 500
center_x, center_y = width//2, height//2
wave_points = []
wave_x = 0
wave_amplitude = 80
wave_frequency = 0.03
angle = 0
rot_radius = 120
line_x = 0
colors = ["cyan", "magenta", "lime", "yellow", "orange"]
canvas.create_line(0, center_y, width, center_y, fill="white")
canvas.create_line(center_x, 0, center_x, height, fill="white")
def animate():
global wave_x, wave_points, angle, line_x
canvas.delete("wave")
canvas.delete("point")
canvas.delete("line")
y_wave = center_y + wave_amplitude * math.sin(wave_frequency * wave_x)
wave_points.append((wave_x, y_wave))
for i in range(1, len(wave_points)):
canvas.create_line(wave_points[i-1], wave_points[i],
fill=random.choice(colors), width=2, tags="wave")
wave_x += 3
if wave_x > width:
wave_x = 0
wave_points = []
x_rot = center_x + rot_radius * math.cos(angle)
y_rot = center_y + rot_radius * math.sin(angle)
canvas.create_oval(x_rot-5, y_rot-5, x_rot+5, y_rot+5, fill="cyan", tags="point")
angle += 0.06
y_line = center_y + 100 * math.sin(line_x/40)
canvas.create_line(center_x, center_y, center_x + line_x, y_line, fill="magenta", width=3, tags="line")
line_x += 3
if line_x > width//2:
line_x = 0
root.after(30, animate)
animate()
root.mainloop()
Usage Instructions
- Save as
CartesianDemo.py - Run with:
python CartesianDemo.py - Features:
- Animated sine wave across Cartesian plane
- Rotating point in circular motion
- Dynamic line animation from center
- Random color selection for wave segments
Lissajous Aurora Demoscene
Beautiful Lissajous curve visualization with aurora-like effects, animated particles, and smooth color transitions.
demoscene_lissajous.py
import tkinter as tk, time, math, random
def hsv(h,s,v):
i=int(h*6);f=h*6-i;p=v*(1-s);q=v*(1-f*s);t=v*(1-(1-f)*s)
r,g,b=[(v,t,p),(q,v,p),(p,v,t),(p,q,v),(t,p,v),(v,p,q)][i%6]
return '#%02x%02x%02x'%(int(r*255),int(g*255),int(b*255))
class D:
def __init__(self):
self.root=tk.Tk();self.root.title('Lissajous Aurora');self.w=1100;self.h=700
self.c=tk.Canvas(self.root,width=self.w,height=self.h,bg='#020113',highlightthickness=0);self.c.pack(fill=tk.BOTH,expand=True)
self.root.bind('<Configure>',self.resize);self.root.bind('<space>',self.toggle);self.root.bind('f',self.full)
self.start=time.perf_counter();self.paused=False;self.speed=1.0;self.a=3;self.b=2;self.ang=0
self.layers=[(random.random(),20+ i*10,1+0.2*i) for i in range(9)]
self.loop()
def resize(self,e):self.w=e.width;self.h=e.height;self.c.config(width=self.w,height=self.h)
def toggle(self,e=None):self.paused=not self.paused
def full(self,e=None):self.root.attributes('-fullscreen', not self.root.attributes('-fullscreen'))
def loop(self):
now=time.perf_counter()
if not self.paused:
t=(now-self.start)*self.speed;self.ang=t*0.8
self.c.delete('all')
for i,(hue,amp,thick) in enumerate(self.layers):
pts=[]
A=amp*(1+0.3*math.sin(t*0.6+i));B=amp*(1+0.2*math.cos(t*0.9+i))
for x in range(0,self.w,8):
nx=x/self.w*math.pi*2
y=self.h/2+math.sin(self.a*nx+self.ang*(0.6+i*0.02))*A+math.cos(self.b*nx+self.ang*(0.9+i*0.03))*B
pts.append(x);pts.append(y)
col=hsv((hue+0.1*math.sin(t*0.3))%1,0.8,0.95)
self.c.create_line(*pts,fill=col,width=thick,smooth=True,splinesteps=8)
for p in range(120):
x=(p/self.w)*self.w+math.sin(t*0.9+p)*12
y=self.h*0.5+math.cos(p*0.2+t*1.6)*(30+math.sin(p*0.4+t*0.7)*40)
r=1+ (math.sin(t*2+p)*0.6+0.6)*2
col=hsv((0.6+math.sin(p*0.12+t*0.3)*0.2)%1,0.7,0.95)
self.c.create_oval(x-r,y-r,x+r,y+r,fill=col,outline='')
self.c.create_text(18,self.h-40,anchor='w',text='Lissajous Aurora Space pause F fullscreen',fill='#dfe',font=('Helvetica',12))
self.root.after(16,self.loop)
if __name__=='__main__':
D();tk.mainloop()
Usage Instructions
- Save as
demoscene_lissajous.py - Run with:
python demoscene_lissajous.py - Features:
- Lissajous curves with multiple layers
- Dynamic particle effects
- HSV color transitions
- Smooth animation with adjustable speed
- Controls:
- Space - Pause/resume animation
- F - Toggle fullscreen
Conway's Game of Life
Interactive Conway's Game of Life simulation with full controls. Features pattern presets, adjustable speed, and real-time statistics.
game_of_life.py
import tkinter as tk
import random
import time
class GameOfLife:
def __init__(self, root):
self.root = root
self.root.title("Conway's Game of Life")
self.root.configure(bg='#0f172a')
# Game configuration
self.rows = 40
self.cols = 60
self.cell_size = 15
self.speed = 10 # generations per second
self.is_running = False
self.generation = 0
# Initialize grid
self.grid = [[0 for _ in range(self.cols)] for _ in range(self.rows)]
# Colors
self.colors = {
'bg': '#0f172a',
'grid': '#1e293b',
'cell': '#10b981',
'text': '#e2e8f0',
'button': '#3b82f6',
'button_hover': '#2563eb'
}
self.setup_ui()
self.setup_bindings()
def setup_ui(self):
# Control frame
control_frame = tk.Frame(self.root, bg=self.colors['bg'])
control_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)
# Buttons
buttons = [
("Start", self.start_game, "#10b981"),
("Pause", self.pause_game, "#f59e0b"),
("Reset", self.reset_game, "#ef4444"),
("Clear", self.clear_grid, "#6b7280"),
("Random", self.random_grid, "#8b5cf6"),
("Step", self.step, "#3b82f6"),
]
for text, command, color in buttons:
btn = tk.Button(
control_frame,
text=text,
command=command,
bg=color,
fg='white',
font=('Arial', 10, 'bold'),
relief=tk.FLAT,
padx=15,
pady=5,
cursor='hand2'
)
btn.pack(side=tk.LEFT, padx=5)
# Speed control
speed_frame = tk.Frame(control_frame, bg=self.colors['bg'])
speed_frame.pack(side=tk.LEFT, padx=20)
tk.Label(speed_frame, text="Speed:", bg=self.colors['bg'], fg=self.colors['text'],
font=('Arial', 10)).pack(side=tk.LEFT, padx=(0, 5))
self.speed_slider = tk.Scale(
speed_frame,
from_=1,
to=20,
orient=tk.HORIZONTAL,
length=100,
bg=self.colors['bg'],
fg=self.colors['text'],
highlightthickness=0,
command=self.change_speed
)
self.speed_slider.set(self.speed)
self.speed_slider.pack(side=tk.LEFT)
# Stats frame
stats_frame = tk.Frame(self.root, bg=self.colors['bg'])
stats_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=(0, 10))
self.stats_labels = {}
stats = [
("Generation:", "generation", "0"),
("Live Cells:", "live_cells", "0"),
("Status:", "status", "Paused")
]
for label, key, value in stats:
frame = tk.Frame(stats_frame, bg=self.colors['bg'])
frame.pack(side=tk.LEFT, padx=20)
tk.Label(frame, text=label, bg=self.colors['bg'], fg=self.colors['text'],
font=('Arial', 10)).pack(side=tk.LEFT)
self.stats_labels[key] = tk.Label(
frame,
text=value,
bg=self.colors['bg'],
fg=self.colors['cell'],
font=('Arial', 10, 'bold'),
width=10
)
self.stats_labels[key].pack(side=tk.LEFT, padx=(5, 0))
# Canvas for grid
self.canvas = tk.Canvas(
self.root,
width=self.cols * self.cell_size,
height=self.rows * self.cell_size,
bg=self.colors['grid'],
highlightthickness=0
)
self.canvas.pack(padx=10, pady=(0, 10))
self.draw_grid()
# Pattern buttons
pattern_frame = tk.Frame(self.root, bg=self.colors['bg'])
pattern_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10)
patterns = [
("Glider", self.add_glider),
("Blinker", self.add_blinker),
("Glider Gun", self.add_glider_gun),
("Random Fill", self.random_grid)
]
for text, command in patterns:
btn = tk.Button(
pattern_frame,
text=text,
command=command,
bg='#475569',
fg='white',
font=('Arial', 9),
relief=tk.FLAT,
padx=10,
pady=3,
cursor='hand2'
)
btn.pack(side=tk.LEFT, padx=5)
def setup_bindings(self):
self.canvas.bind('', self.toggle_cell)
self.root.bind('', lambda e: self.toggle_running())
self.root.bind('r', lambda e: self.reset_game())
self.root.bind('c', lambda e: self.clear_grid())
self.root.bind('s', lambda e: self.step())
def draw_grid(self):
self.canvas.delete('all')
# Draw cells
for row in range(self.rows):
for col in range(self.cols):
x1 = col * self.cell_size
y1 = row * self.cell_size
x2 = x1 + self.cell_size
y2 = y1 + self.cell_size
if self.grid[row][col]:
self.canvas.create_rectangle(
x1, y1, x2, y2,
fill=self.colors['cell'],
outline=self.colors['grid'],
width=1
)
else:
self.canvas.create_rectangle(
x1, y1, x2, y2,
fill=self.colors['bg'],
outline=self.colors['grid'],
width=1
)
def toggle_cell(self, event):
if self.is_running:
return
col = event.x // self.cell_size
row = event.y // self.cell_size
if 0 <= row < self.rows and 0 <= col < self.cols:
self.grid[row][col] = 1 - self.grid[row][col]
self.draw_grid()
self.update_stats()
def count_neighbors(self, row, col):
count = 0
for i in range(-1, 2):
for j in range(-1, 2):
if i == 0 and j == 0:
continue
r = (row + i) % self.rows
c = (col + j) % self.cols
count += self.grid[r][c]
return count
def next_generation(self):
new_grid = [[0 for _ in range(self.cols)] for _ in range(self.rows)]
for row in range(self.rows):
for col in range(self.cols):
neighbors = self.count_neighbors(row, col)
if self.grid[row][col]:
# Cell is alive
if neighbors in [2, 3]:
new_grid[row][col] = 1
else:
# Cell is dead
if neighbors == 3:
new_grid[row][col] = 1
self.grid = new_grid
self.generation += 1
self.draw_grid()
self.update_stats()
# Stop if no live cells
if self.count_live_cells() == 0 and self.is_running:
self.pause_game()
def count_live_cells(self):
return sum(sum(row) for row in self.grid)
def update_stats(self):
self.stats_labels['generation'].config(text=str(self.generation))
self.stats_labels['live_cells'].config(text=str(self.count_live_cells()))
self.stats_labels['status'].config(
text="Running" if self.is_running else "Paused",
fg="#10b981" if self.is_running else "#f59e0b"
)
def start_game(self):
if not self.is_running:
self.is_running = True
self.update_stats()
self.run_game()
def pause_game(self):
self.is_running = False
self.update_stats()
def toggle_running(self):
if self.is_running:
self.pause_game()
else:
self.start_game()
def run_game(self):
if self.is_running:
self.next_generation()
delay = 1000 // self.speed
self.root.after(delay, self.run_game)
def reset_game(self):
self.pause_game()
self.grid = [[0 for _ in range(self.cols)] for _ in range(self.rows)]
self.generation = 0
self.draw_grid()
self.update_stats()
def clear_grid(self):
self.reset_game()
def random_grid(self):
self.pause_game()
for row in range(self.rows):
for col in range(self.cols):
self.grid[row][col] = 1 if random.random() < 0.2 else 0
self.generation = 0
self.draw_grid()
self.update_stats()
def step(self):
self.pause_game()
self.next_generation()
def change_speed(self, value):
self.speed = int(value)
def add_glider(self):
self.pause_game()
center_row = self.rows // 2
center_col = self.cols // 2
# Glider pattern
pattern = [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
for dr, dc in pattern:
row = (center_row + dr) % self.rows
col = (center_col + dc) % self.cols
self.grid[row][col] = 1
self.draw_grid()
self.update_stats()
def add_blinker(self):
self.pause_game()
center_row = self.rows // 2
center_col = self.cols // 2
# Blinker pattern
for dc in range(-1, 2):
row = center_row
col = (center_col + dc) % self.cols
self.grid[row][col] = 1
self.draw_grid()
self.update_stats()
def add_glider_gun(self):
self.pause_game()
center_row = self.rows // 2
center_col = self.cols // 2
# Gosper Glider Gun pattern (simplified)
gun_pattern = [
(5, 1), (5, 2), (6, 1), (6, 2),
(5, 11), (6, 11), (7, 11),
(4, 12), (8, 12),
(3, 13), (9, 13),
(3, 14), (9, 14),
(6, 15),
(4, 16), (8, 16),
(5, 17), (6, 17), (7, 17),
(6, 18)
]
for dr, dc in gun_pattern:
row = (center_row + dr) % self.rows
col = (center_col + dc) % self.cols
if 0 <= row < self.rows and 0 <= col < self.cols:
self.grid[row][col] = 1
self.draw_grid()
self.update_stats()
if __name__ == "__main__":
root = tk.Tk()
game = GameOfLife(root)
root.mainloop()
Usage Instructions
- Save as
game_of_life.py - Run with:
python game_of_life.py - Requirements: Python 3 with Tkinter
- Controls:
- Click cells - Toggle alive/dead (when paused)
- Space - Start/pause simulation
- R - Reset grid
- C - Clear grid
- S - Step forward one generation
- Buttons - Start, Pause, Reset, Clear, Random, Step
- Slider - Adjust simulation speed
- Pattern buttons - Add predefined patterns
- Features:
- Interactive grid with click-to-toggle cells
- Real-time generation and cell count statistics
- Adjustable simulation speed
- Pattern presets (Glider, Blinker, Glider Gun)
- Random grid generation
- Automatic pause when no live cells remain