Herzlich willkommen zum SELF-Treffen 2026
vom 24.04. – 26.04.2026 in Halle (Saale)

Beispiel:Pong.html

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

<!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>
0
0

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>