Herzlich willkommen zum SELF-Treffen 2026
vom 24.04. – 26.04.2026
in Halle (Saale)
Beispiel:Pong.html
<!DOCTYPE html> <html lang="de"> <head>
<meta charset="UTF-8">
<title>Pong – WAAPI Edition</title>
<style>
:root {
--akzent: #00ffd2;
--dunkel: #111;
}
* {
box-sizing: border-box;
letter-spacing: 0.3em;
}
body {
background: #222;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: monospace;
overflow: hidden;
}
h1,
h2 {
color: var(--akzent);
text-shadow: 0 0 1rem var(--akzent);
}
p { color: #aaa; }
.einblendung {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #00211e;
color: #ddd;
gap: 1rem;
}
.einblendung.verborgen { display: none; }
#siegeseinblendung { cursor: pointer; }
#spielfeld {
position: relative;
width: 800px;
height: 500px;
background: var(--dunkel);
border: thin solid #444;
overflow: hidden;
}
#mittellinie {
position: absolute;
left: 50%;
top: 0;
width: 0;
height: 100%;
border-left: medium dashed #333;
}
.schlaeger {
position: absolute;
width: 12px;
height: 80px;
background: #fff;
top: 0;
}
#schlaeger-spieler { left: .8rem; }
#schlaeger-computer { right: .8rem; }
#ball {
position: absolute;
width: .8rem;
height: .8rem;
border-radius: 50%;
background: var(--akzent);
transform: translate(-50%, -50%);
}
.punkte {
position: absolute;
top: 1rem;
font-size: 3rem;
color: #444;
pointer-events: none;
}
#punkte-spieler { left: 25%; transform: translateX(-50%); }
#punkte-computer { left: 75%; transform: translateX(-50%); }
#werkzeugleiste {
display: flex;
align-items: center;
justify-content: space-between;
width: 800px;
gap: 1em;
}
#titel { margin: 0; }
#werkzeuge-rechts {
display: flex;
align-items: center;
gap: 1em;
}
#werkzeugleiste label { color: var(--akzent); }
#zoom-auswahl,
#tonschalter,
#abbrechen-schalter {
color: var(--akzent);
border: thin solid #444;
border-radius: .3em;
padding: .2em .5em;
font-family: monospace;
cursor: pointer;
}
#zoom-auswahl {
background: var(--dunkel);
outline: none;
}
#tonschalter,
#abbrechen-schalter { background: none; }
#tonschalter.stumm {
color: #555;
border-color: #333;
}
#abbrechen-schalter:hover {
color: #ff4455;
border-color: #ff4455;
}
</style>
</head> <body>
Inhaltsverzeichnis
WAAPI-PONG
<label for="zoom-auswahl">ZOOM</label>
<select id="zoom-auswahl">
<option value="50">50%</option>
<option value="80">80%</option>
<option value="100" selected>100%</option>
<option value="150">150%</option>
<option value="200">200%</option>
<option value="250">250%</option>
<option value="300">300%</option>
</select>
<button id="tonschalter" title="Ton ein/aus">TON AN</button>
<button id="abbrechen-schalter" title="Spiel abbrechen">STOP</button>
PONG
Maus · Tasten QA oder ↑↓ Pfeiltasten · Touch
Klick, Enter oder Tap zum Starten
Klick, Enter oder Tap zum Neustart
<script> document.addEventListener('DOMContentLoaded', function () {
// DOM-Referenzen const dom = {
spielfeld: document.getElementById('spielfeld'),
ball: document.getElementById('ball'),
schlaegerSpieler: document.getElementById('schlaeger-spieler'),
schlaegerComputer: document.getElementById('schlaeger-computer'),
punkteSpieler: document.getElementById('punkte-spieler'),
punkteComputer: document.getElementById('punkte-computer'),
starteinblendung: document.getElementById('starteinblendung'),
siegeseinblendung: document.getElementById('siegeseinblendung'),
siegerTitel: document.getElementById('sieger-titel'),
endstand: document.getElementById('endstand'),
tonschalter: document.getElementById('tonschalter'),
abbrechenSchalter: document.getElementById('abbrechen-schalter'),
zoomAuswahl: document.getElementById('zoom-auswahl'),
};
// Konfiguration const KONFIG = {
feld: { breite: 800, hoehe: 500 },
schlaeger: { breite: 12, hoehe: 80, geschwindigkeit: 7 },
ball: {
radius: 7, startGeschwindigkeit: 5,
startVx: 4, startVy: 3,
maxGeschwindigkeit: 18, beschleunigung: 0.4,
maxWinkel: Math.PI / 4,
},
ki: { verfolgung: 0.08 },
siegPunkte: 5,
schleife: { basisFps: 60, maxDeltaMs: 50 },
};
// Hilfsfunktionen const begrenzen = (v, min, max) => Math.max(min, Math.min(max, v)); const mitteY = (s) => s.y + KONFIG.schlaeger.hoehe / 2;
function ballTrifftSchlaeger(ball, schlaeger) {
const r = KONFIG.ball.radius;
return ball.x + r > schlaeger.x
&& ball.x - r < schlaeger.x + KONFIG.schlaeger.breite
&& ball.y + r > schlaeger.y
&& ball.y - r < schlaeger.y + KONFIG.schlaeger.hoehe;
}
function ballGeschwindigkeitSetzen(ball, richtung, tempo, winkel) {
ball.geschwindigkeit = tempo; ball.vx = richtung * tempo * Math.cos(winkel); ball.vy = tempo * Math.sin(winkel);
}
// Eingabe const TASTENBELEGUNG = {
ArrowUp: 'hoch', q: 'hoch', Q: 'hoch', ArrowDown: 'runter', a: 'runter', A: 'runter',
};
const eingabe = {
hoch: false, runter: false, modus: 'maus', mausY: KONFIG.feld.hoehe / 2,
};
// Spielzustand const zustand = {
laeuft: false,
letzterZeitstempel: null,
spieler: { x: 12, y: KONFIG.feld.hoehe / 2 - KONFIG.schlaeger.hoehe / 2, punkte: 0 },
computer: { x: KONFIG.feld.breite - 12 - KONFIG.schlaeger.breite, y: KONFIG.feld.hoehe / 2 - KONFIG.schlaeger.hoehe / 2, punkte: 0 },
ball: { x: KONFIG.feld.breite / 2, y: KONFIG.feld.hoehe / 2, vx: 4, vy: 3, geschwindigkeit: 5 },
};
// WAAPI-Effekte const blitzAnimation = () =>
dom.spielfeld.animate(
[{ borderColor: 'var(--akzent)' }, { borderColor: '#444' }],
{ duration: 300, easing: 'ease-out', fill: 'forwards' }
);
const punkteAnimation = (element) =>
element.animate(
[
{ transform: 'translateX(-50%) scale(1.5)', color: '#00ffd2', textShadow: '0 0 20px #00ffd2' },
{ transform: 'translateX(-50%) scale(1)', color: '#aaa', textShadow: '0 0 8px #ffffff22' },
],
{ duration: 600, easing: 'ease-out', fill: 'forwards' }
);
// Ton per Web Audio API function klangeErstellen() {
const ctx = new (window.AudioContext || window.webkitAudioContext)(); let aktiv = true;
function ton(typ, freqStart, freqEnde, lautstaerke, dauer, versatz = 0) {
const t = ctx.currentTime + versatz;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = typ;
osc.frequency.setValueAtTime(freqStart, t);
osc.frequency.exponentialRampToValueAtTime(freqEnde, t + dauer);
gain.gain.setValueAtTime(lautstaerke, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + dauer);
osc.start(t);
osc.stop(t + dauer);
}
return {
istAktiv: () => aktiv,
setAktiv: (v) => { aktiv = Boolean(v); },
aufwecken: () => ctx.resume(),
schlaeger: () => aktiv && ton('square', 400, 120, 0.18, 0.01),
wand: () => aktiv && ton('sine', 400, 1000, 0.10, 0.05),
punkt: () => aktiv && ton('triangle', 330, 165, 0.15, 0.35),
sieg: () => aktiv && [261, 330, 392, 523].forEach((f, i) =>
ton('triangle', f, f, 0.18, 0.25, 0.5 + i * 0.13)),
};
} const klang = klangeErstellen();
// Spiel-Übergänge function ballZuruecksetzen(richtung) {
Object.assign(zustand.ball, {
x: KONFIG.feld.breite / 2,
y: KONFIG.feld.hoehe / 2,
geschwindigkeit: KONFIG.ball.startGeschwindigkeit,
vx: richtung * KONFIG.ball.startVx,
vy: (Math.random() > 0.5 ? 1 : -1) * KONFIG.ball.startVy,
});
blitzAnimation();
}
function siegAnzeigen(gewinner) {
zustand.laeuft = false;
dom.siegerTitel.textContent = gewinner === 'spieler' ? '🏆 DU GEWINNST!' : '💻 COMPUTER GEWINNT';
dom.endstand.textContent = `${zustand.spieler.punkte} : ${zustand.computer.punkte}`;
dom.siegeseinblendung.classList.remove('verborgen');
klang.sieg();
}
function spielStarten() {
klang.aufwecken();
dom.starteinblendung.classList.add('verborgen');
zustand.laeuft = true;
zustand.letzterZeitstempel = null;
requestAnimationFrame(spielschleife);
}
function spielNeustarten() {
zustand.spieler.punkte = 0;
zustand.computer.punkte = 0;
dom.punkteSpieler.textContent = dom.punkteComputer.textContent = '0';
zustand.spieler.y = zustand.computer.y = KONFIG.feld.hoehe / 2 - KONFIG.schlaeger.hoehe / 2;
dom.siegeseinblendung.classList.add('verborgen');
ballZuruecksetzen(1);
zustand.laeuft = true;
zustand.letzterZeitstempel = null;
requestAnimationFrame(spielschleife);
}
// Eingabe-Listener dom.spielfeld.addEventListener('mousemove', (e) => {
eingabe.mausY = e.offsetY / zoomFaktor; eingabe.modus = 'maus';
});
dom.spielfeld.addEventListener('touchmove', (e) => {
e.preventDefault(); const r = dom.spielfeld.getBoundingClientRect(); eingabe.mausY = (e.touches[0].clientY - r.top) / zoomFaktor; eingabe.modus = 'maus';
}, { passive: false });
document.addEventListener('keydown', (e) => {
const aktion = TASTENBELEGUNG[e.key];
if (aktion) { e.preventDefault(); eingabe[aktion] = true; eingabe.modus = 'tastatur'; return; }
if (e.key === 'Enter') {
if (!dom.starteinblendung.classList.contains('verborgen')) dom.starteinblendung.click();
else if (!dom.siegeseinblendung.classList.contains('verborgen')) dom.siegeseinblendung.click();
}
});
document.addEventListener('keyup', (e) => {
const aktion = TASTENBELEGUNG[e.key]; if (aktion) eingabe[aktion] = false;
});
dom.starteinblendung.addEventListener('click', spielStarten); dom.siegeseinblendung.addEventListener('click', spielNeustarten);
// Spielschritt (Logik) function aktualisieren(deltaMs) {
const dt = deltaMs / (1000 / KONFIG.schleife.basisFps);
const { ball, spieler, computer } = zustand;
// Spieler-Schläger
if (eingabe.modus === 'tastatur') {
if (eingabe.hoch) spieler.y -= KONFIG.schlaeger.geschwindigkeit * dt;
if (eingabe.runter) spieler.y += KONFIG.schlaeger.geschwindigkeit * dt;
spieler.y = begrenzen(spieler.y, 0, KONFIG.feld.hoehe - KONFIG.schlaeger.hoehe);
} else {
spieler.y = begrenzen(eingabe.mausY - KONFIG.schlaeger.hoehe / 2, 0, KONFIG.feld.hoehe - KONFIG.schlaeger.hoehe);
}
// Computer-KI computer.y += (ball.y - mitteY(computer)) * KONFIG.ki.verfolgung * dt; computer.y = begrenzen(computer.y, 0, KONFIG.feld.hoehe - KONFIG.schlaeger.hoehe);
// Ball bewegen ball.x += ball.vx * dt; ball.y += ball.vy * dt;
// Wandabprall (oben/unten)
const r = KONFIG.ball.radius;
if (ball.y - r < 0) {
ball.y = r; ball.vy = Math.abs(ball.vy); klang.wand();
} else if (ball.y + r > KONFIG.feld.hoehe) {
ball.y = KONFIG.feld.hoehe - r; ball.vy = -Math.abs(ball.vy); klang.wand();
}
// Schlägertreffer
const aktiverSchlaeger = ball.x < KONFIG.feld.breite / 2 ? spieler : computer;
if (ballTrifftSchlaeger(ball, aktiverSchlaeger)) {
const treffer = (ball.y - mitteY(aktiverSchlaeger)) / (KONFIG.schlaeger.hoehe / 2);
const winkel = begrenzen(treffer, -1, 1) * KONFIG.ball.maxWinkel;
const richtung = ball.x < KONFIG.feld.breite / 2 ? 1 : -1;
const neueGeschwindigkeit = Math.min(ball.geschwindigkeit + KONFIG.ball.beschleunigung, KONFIG.ball.maxGeschwindigkeit);
ballGeschwindigkeitSetzen(ball, richtung, neueGeschwindigkeit, winkel);
ball.x = richtung === 1
? aktiverSchlaeger.x + KONFIG.schlaeger.breite + r
: aktiverSchlaeger.x - r;
klang.schlaeger();
}
// Tor & Punkte
const torLinks = ball.x - r < 0;
const torRechts = ball.x + r > KONFIG.feld.breite;
if (torLinks || torRechts) {
const torschuetze = torLinks ? computer : spieler;
const punkteElement = torLinks ? dom.punkteComputer : dom.punkteSpieler;
const naechsteRichtung = torLinks ? 1 : -1;
torschuetze.punkte++;
punkteElement.textContent = torschuetze.punkte;
punkteAnimation(punkteElement);
klang.punkt();
if (torschuetze.punkte >= KONFIG.siegPunkte) { siegAnzeigen(torLinks ? 'computer' : 'spieler'); return; }
ballZuruecksetzen(naechsteRichtung);
}
}
// Darstellung const darstellungsCache = { spielerY: NaN, computerY: NaN, ballX: NaN, ballY: NaN };
function zeichnen() {
const { ball, spieler, computer } = zustand;
if (darstellungsCache.spielerY !== spieler.y) {
dom.schlaegerSpieler.style.transform = `translate3d(0,${spieler.y}px,0)`;
darstellungsCache.spielerY = spieler.y;
}
if (darstellungsCache.computerY !== computer.y) {
dom.schlaegerComputer.style.transform = `translate3d(0,${computer.y}px,0)`;
darstellungsCache.computerY = computer.y;
}
if (darstellungsCache.ballX !== ball.x || darstellungsCache.ballY !== ball.y) {
dom.ball.style.transform = `translate3d(${ball.x}px,${ball.y}px,0) translate(-50%,-50%)`;
darstellungsCache.ballX = ball.x;
darstellungsCache.ballY = ball.y;
}
}
// Hauptschleife function spielschleife(zeitstempel) {
if (!zustand.laeuft) return; if (zustand.letzterZeitstempel === null) zustand.letzterZeitstempel = zeitstempel; const deltaMs = Math.min(zeitstempel - zustand.letzterZeitstempel, KONFIG.schleife.maxDeltaMs); zustand.letzterZeitstempel = zeitstempel; aktualisieren(deltaMs); zeichnen(); requestAnimationFrame(spielschleife);
}
// Steuerelemente dom.tonschalter.addEventListener('click', () => {
klang.setAktiv(!klang.istAktiv());
dom.tonschalter.textContent = klang.istAktiv() ? 'TON AN' : 'TON AUS';
dom.tonschalter.classList.toggle('stumm', !klang.istAktiv());
});
let zoomFaktor = 1;
dom.zoomAuswahl.addEventListener('change', () => {
zoomFaktor = dom.zoomAuswahl.value / 100; document.documentElement.style.zoom = zoomFaktor;
});
dom.abbrechenSchalter.addEventListener('click', () => {
if (!zustand.laeuft) return;
zustand.laeuft = false;
zustand.spieler.punkte = 0;
zustand.computer.punkte = 0;
dom.punkteSpieler.textContent = dom.punkteComputer.textContent = '0';
zustand.spieler.y = zustand.computer.y = KONFIG.feld.hoehe / 2 - KONFIG.schlaeger.hoehe / 2;
dom.siegeseinblendung.classList.add('verborgen');
dom.starteinblendung.classList.remove('verborgen');
ballZuruecksetzen(1);
}); }); </script> </body> </html>