Herzlich willkommen zum SELF-Treffen 2026
vom 24.04. – 26.04.2026
in Halle (Saale)
Beispiel:Breakout.html
Aus SELFHTML-Wiki
<!DOCTYPE html> <html lang="de"> <head> <meta charset="UTF-8"> <title>Breakout</title> <style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background: #111;
color: #fff;
font-family: monospace;
}
canvas {
background: #000;
border: medium solid #555;
display: block;
width: 100%;
max-width: 800px;
margin: 0 0.5rem;
cursor: none;
}
p {
margin: 0.5rem;
font-size: clamp(0.7rem, 2vw, 1.2rem);
}
#tonBtn {
background: none;
border: none;
font-size: 2em;
font-family: inherit;
cursor: pointer;
margin-top: -1rem;
}
</style> </head>
<body>
PUNKTE: 0 | LEBEN: 3 | TON: <button id="tonBtn">🔊</button>
<canvas id="c"></canvas>
← → Pfeiltasten / Maus · Leertaste / Klick für Start
<script> document.addEventListener('DOMContentLoaded', function () {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
// --- Ton ---
let audioCtx = null;
let soundOn = true;
document.getElementById('tonBtn').addEventListener('click', () => {
soundOn = !soundOn;
document.getElementById('tonBtn').textContent = soundOn ? '🔊' : '🔇';
});
function getAudioCtx() {
if (!audioCtx) audioCtx = new AudioContext();
return audioCtx;
}
function beep(freq = 440, dur = 0.05) {
if (!soundOn) return;
const ac = getAudioCtx();
const osc = ac.createOscillator();
const gain = ac.createGain();
osc.connect(gain);
gain.connect(ac.destination);
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.2, ac.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
osc.start();
osc.stop(ac.currentTime + dur);
}
// --- Canvas-Grösse responsiv bestimmen, nur beim Laden --- const SEITENVERHAELTNIS = 4 / 3; const dpr = window.devicePixelRatio || 1;
function canvasGroesseSetzen() {
const cssBreite = Math.min(canvas.parentElement.clientWidth - 16, 800);
const cssHoehe = Math.round(cssBreite / SEITENVERHAELTNIS);
canvas.style.width = cssBreite + 'px';
canvas.style.height = cssHoehe + 'px';
canvas.width = Math.round(cssBreite * dpr);
canvas.height = Math.round(cssHoehe * dpr);
ctx.scale(dpr, dpr);
}
canvasGroesseSetzen();
// Logische Spielgrösse (CSS-Pixel, unabhängig von dpr) const W = canvas.width / dpr; const H = canvas.height / dpr;
// --- Konfiguration ---
const KONFIG = {
paddle: {
breite: Math.round(W * 0.15),
hoehe: Math.round(H * 0.018),
y: H - H * 0.08,
geschwindigkeit: W * 0.012,
},
ball: {
radius: Math.round(W * 0.012),
geschwindigkeit: Math.hypot(W * 0.006, H * 0.011),
},
steine: {
spalten: 10,
reihen: 6,
abstand: Math.round(W * 0.007),
get breite() { return Math.round((W - this.abstand) / this.spalten - this.abstand); },
hoehe: Math.round(H * 0.038),
get offsetX() { return (W - this.spalten * (this.breite + this.abstand) + this.abstand) / 2; },
offsetY: Math.round(H * 0.1),
farben: ['#e74c3c','#e67e22','#f1c40f','#2ecc71','#3498db','#9b59b6'],
},
schrift: {
gross: Math.min(Math.round(W * 0.15), 80),
klein: Math.round(W * 0.03),
},
};
// --- Zustand ---
const state = { score: 0, lives: 3, paused: false, pauseMsg: };
let bricks = [], ball = {}, paddle = {};
let rafId = null;
const keys = {};
function pause(msg) {
state.paused = true;
state.pauseMsg = msg;
}
// --- Init ---
function init() {
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }
Object.assign(state, { score: 0, lives: 3, paused: false, pauseMsg: });
const { paddle: P, steine: S } = KONFIG;
paddle = { x: W/2 - P.breite/2, y: P.y };
bricks = [];
for (let r = 0; r < S.reihen; r++)
for (let c = 0; c < S.spalten; c++)
bricks.push({ x: S.offsetX + c*(S.breite+S.abstand), y: S.offsetY + r*(S.hoehe+S.abstand), alive: true, color: S.farben[r] });
resetBall();
updateInfo();
rafId = requestAnimationFrame(loop);
}
function resetBall() {
const { paddle: P, ball: B } = KONFIG;
ball = { x: W/2, y: P.y - B.radius - 2, vx: B.geschwindigkeit * 0.5, vy: -B.geschwindigkeit, launched: false };
}
// --- Game Loop ---
function loop() {
update();
draw();
if (state.paused) {
showMessage(state.pauseMsg, 'Punkte: ' + state.score);
rafId = null;
} else {
rafId = requestAnimationFrame(loop);
}
}
// --- Steuerung ---
window.addEventListener('keydown', e => {
keys[e.key] = true;
if (e.key === ' ') {
getAudioCtx();
if (state.paused) init();
else ball.launched = true;
}
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
canvas.addEventListener('mousemove', e => {
const r = canvas.getBoundingClientRect();
const scale = W / r.width;
paddle.x = (e.clientX - r.left) * scale - KONFIG.paddle.breite / 2;
}, { passive: true });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
const r = canvas.getBoundingClientRect();
const scale = W / r.width;
paddle.x = (e.touches[0].clientX - r.left) * scale - KONFIG.paddle.breite / 2;
}, { passive: false });
canvas.addEventListener('click', () => {
getAudioCtx();
if (state.paused) init();
else ball.launched = true;
});
// --- Update ---
function update() {
const { paddle: P, ball: B, steine: S } = KONFIG;
if (keys['ArrowLeft']) paddle.x -= P.geschwindigkeit; if (keys['ArrowRight']) paddle.x += P.geschwindigkeit; paddle.x = Math.max(0, Math.min(W - P.breite, paddle.x));
if (!ball.launched) { ball.x = paddle.x + P.breite/2; return; }
ball.x += ball.vx; ball.y += ball.vy;
// Wände (Richtung mit Math.abs garantieren, dann Position korrigieren)
if (ball.x - B.radius < 0) {
ball.vx = Math.abs(ball.vx);
ball.x = B.radius;
beep(300);
} else if (ball.x + B.radius > W) {
ball.vx = -Math.abs(ball.vx);
ball.x = W - B.radius;
beep(300);
}
if (ball.y - B.radius < 0) {
ball.vy = Math.abs(ball.vy);
ball.y = B.radius;
beep(300);
}
// Paddle-Kollision
if (ball.vy > 0
&& ball.y + B.radius >= paddle.y
&& ball.y + B.radius <= paddle.y + P.hoehe
&& ball.x >= paddle.x
&& ball.x <= paddle.x + P.breite) {
const angle = (ball.x - (paddle.x + P.breite/2)) / (P.breite/2) * (Math.PI / 3);
ball.vx = B.geschwindigkeit * Math.sin(angle);
ball.vy = -Math.abs(B.geschwindigkeit * Math.cos(angle));
ball.y = paddle.y - B.radius; // rauskorrigieren
beep(480);
}
// Ball verloren
if (ball.y > H) {
state.lives--;
updateInfo();
if (state.lives <= 0) { pause('GAME OVER'); return; }
resetBall();
}
// Block-Kollision
for (const b of bricks) {
if (!b.alive) continue;
if (ball.x + B.radius > b.x && ball.x - B.radius < b.x + S.breite &&
ball.y + B.radius > b.y && ball.y - B.radius < b.y + S.hoehe) {
b.alive = false;
state.score++;
updateInfo();
ball.vy *= -1;
beep(660);
break;
}
}
if (bricks.every(b => !b.alive)) pause('GEWONNEN!');
}
function updateInfo() {
document.getElementById('infoText').textContent = `PUNKTE: ${state.score} | LEBEN: ${state.lives} | TON: `;
}
// --- Zeichnen ---
function draw() {
const { paddle: P, ball: B, steine: S, schrift: F } = KONFIG;
ctx.clearRect(0, 0, W, H);
ctx.save();
ctx.globalAlpha = 0.15;
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.round(W * 0.18)}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('BREAKOUT', W/2, H * 0.83);
ctx.restore();
for (const b of bricks) {
if (!b.alive) continue;
ctx.fillStyle = b.color;
ctx.fillRect(b.x, b.y, S.breite, S.hoehe);
}
ctx.fillStyle = '#fff'; ctx.fillRect(paddle.x, paddle.y, P.breite, P.hoehe);
ctx.beginPath(); ctx.arc(ball.x, ball.y, B.radius, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); }
// --- Meldung im Canvas ---
function showMessage(msg, sub) {
const { schrift: F } = KONFIG;
ctx.save();
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.fillStyle = '#fff';
ctx.font = `bold ${F.gross}px monospace`;
ctx.fillText(msg, W/2, H/2 - F.gross * 0.5);
ctx.font = `${F.klein}px monospace`;
ctx.fillStyle = '#aaa';
ctx.fillText(sub, W/2, H/2 + F.klein * 1.2);
ctx.restore();
}
init();
}); </script>
</body> </html>