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

Beispiel:JS-WAAPI-3.html

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Karten Flip Animation mit Timeline</title>
  
<style>
:root {
  --kartenbreite: 15vw;
}

html,
body {
  height: 100vh;
  margin: 0;
}

body {
  display: grid;
  justify-content: center;
  align-items: end;
  perspective: 100vw;
}

.spielfeld {
  position: relative;
  width: calc(4 * var(--kartenbreite));
  box-sizing: content-box;
  aspect-ratio: 16/9;
  transform: rotateX(50deg);
  border: 1em solid #888;
  border-radius: 5vw;
  padding: 1em 3em;
  background: oklch(45% 0.13 125);
  transform-style: preserve-3d;
}

.karte {
  position: absolute;
  width: var(--kartenbreite);
  aspect-ratio: 0.665;
  outline: none;
  border: none;
  cursor: pointer;
  padding: 0;
  background-color: transparent;
  transform-style: preserve-3d;
  left: calc(var(--kartenbreite) / 3);
  top: calc(var(--kartenbreite) / 2);
}

.karte.animating {
  cursor: default;
  z-index: 100 !important;
  pointer-events: none;
}

.vorderseite,
.rueckseite {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
  border-radius: calc(var(--kartenbreite) / 11);
  left: 0;
  top: 0;
  pointer-events: none;
}

.vorderseite {
  transform: rotateY(180deg);
  border: thin solid black;
  background-size: cover;
  background-position: center;
}

.karte:nth-child(1) .vorderseite { 
  background-image: url("https://upload.wikimedia.org/wikipedia/commons/5/5f/English_pattern_ace_of_clubs.svg");
}
.karte:nth-child(2) .vorderseite { 
  background-image: url("https://upload.wikimedia.org/wikipedia/commons/2/26/English_pattern_2_of_hearts.svg");
}
.karte:nth-child(3) .vorderseite { 
  background-image: url("https://upload.wikimedia.org/wikipedia/commons/2/2c/English_pattern_3_of_diamonds.svg");
}

.rueckseite {
  background-color: white;
  background-image: 
    repeating-linear-gradient(
      45deg,
      steelblue 0px,
      steelblue 5px,
      transparent 5px,
      transparent 10px
    ),
    repeating-linear-gradient(
      -45deg,
      steelblue 0px,
      steelblue 5px,
      transparent 5px,
      transparent 10px
    );
  background-size: 28px 28px;
  border: thin solid black;
}

.steuerung,
#timelineInfo {
  text-align: center;
}

.steuerung button {
  cursor: pointer;
  padding: 1em;
}

.steuerung button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

</style>
</head>

<body>
  
<article class="spielfeld">
  
  <!-- Karte 1: links unten (Z=3px), rechts oben (Z=9px) -->
  <button class="karte" data-seite="links" data-karte="1">
    <span class="rueckseite"></span>
    <span class="vorderseite"></span>
  </button>
  
  <!-- Karte 2: links mitte (Z=6px), rechts mitte (Z=6px) -->
  <button class="karte" data-seite="links" data-karte="2">
    <span class="rueckseite"></span>
    <span class="vorderseite"></span>
  </button>
  
  <!-- Karte 3: links oben (Z=9px), rechts unten (Z=3px) -->
  <button class="karte" data-seite="links" data-karte="3">
    <span class="rueckseite"></span>
    <span class="vorderseite"></span>
  </button>
  
</article>

<p id="timelineInfo"></p>

<p class="steuerung">
  <button id="losButton">Los</button>
  <button id="pauseButton" disabled>Pause</button>
  <button id="zurueckButton">Zurück</button>
</p>

<script>
'use strict';
document.addEventListener('DOMContentLoaded', function () {

const losButton = document.getElementById('losButton');
const pauseButton = document.getElementById('pauseButton');
const zurueckButton = document.getElementById('zurueckButton');
const karten = document.querySelectorAll('.karte');

// Prüfe ob bereits initialisiert
if (losButton.dataset.initialisiert === 'true') {
  console.warn('Buttons bereits initialisiert - überspringe Doppel-Initialisierung');
  return;
}
losButton.dataset.initialisiert = 'true';

const KARTENBREITE_VW = 15;
const MAX_FLUGHOEHE_FAKTOR = 2; // Flughöhe als Vielfaches der Kartenbreite
const MAX_FLUGHOEHE = (KARTENBREITE_VW * MAX_FLUGHOEHE_FAKTOR * window.innerWidth) / 100;
const ANIMATION_DAUER = 1500; // Dauer der Animation in Millisekunden
const timelineInfo = document.getElementById('timelineInfo');

let laufendeAnimationen = [];
let animationsTracker = null;

// Gemeinsame Filter-Funktion
function findeKarten(seite, ohneAnimierende = true) {
  return Array.from(karten).filter(k => 
    k.dataset.seite === seite && 
    (!ohneAnimierende || !k.classList.contains('animating'))
  );
}

// Aktualisiere Timeline-Info Anzeige
function aktualisiereTimelineInfo() {
  if (!animationsTracker) {
    timelineInfo.textContent = 'Keine Animation aktiv';
    timelineInfo.classList.remove('active');
    return;
  }
  
  const vergangeneZeit = Date.now() - animationsTracker.startZeit;
  const fortschritt = Math.min(100, (vergangeneZeit / ANIMATION_DAUER) * 100);
  const vergangeneMs = Math.min(ANIMATION_DAUER, vergangeneZeit);
  
  timelineInfo.textContent = `Karte ${animationsTracker.karteNr}: ${Math.round(vergangeneMs)}ms / ${ANIMATION_DAUER}ms (${fortschritt.toFixed(1)}%)`;
  timelineInfo.classList.add('active');
  
  if (vergangeneZeit < ANIMATION_DAUER && animationsTracker.animation.playState === 'running') {
    requestAnimationFrame(aktualisiereTimelineInfo);
  }
}

// Finde die oberste Karte auf dem linken Stapel
function findeObersteKarte() {
  const linkeKarten = findeKarten('links');
  if (linkeKarten.length === 0) return null;
  
  return linkeKarten.sort((a, b) => 
    parseInt(b.dataset.karte) - parseInt(a.dataset.karte)
  )[0];
}

// Finde die oberste Karte auf dem rechten Stapel
function findeObersteRechteKarte() {
  const rechteKarten = findeKarten('rechts');
  if (rechteKarten.length === 0) return null;
  
  return rechteKarten.sort((a, b) => 
    parseInt(b.dataset.rechtsZ) - parseInt(a.dataset.rechtsZ)
  )[0];
}

function aktualisiereButtons() {
  const hatPausierteAnimationen = laufendeAnimationen.length > 0 && 
    laufendeAnimationen.some(a => a.animation.playState === 'paused');
  
  const linkeKarten = findeKarten('links');
  const rechteKarten = findeKarten('rechts');
  
  losButton.disabled = linkeKarten.length === 0 || hatPausierteAnimationen;
  zurueckButton.disabled = rechteKarten.length === 0 || hatPausierteAnimationen;
  pauseButton.disabled = laufendeAnimationen.length === 0;
  
  if (laufendeAnimationen.length > 0) {
    const allesPausiert = laufendeAnimationen.every(a => a.animation.playState === 'paused');
    pauseButton.textContent = allesPausiert ? 'Weiter' : 'Pause';
  }
}

// Erstelle Animation
function erstelleAnimationen(karte, nachRechts) {
  const deltaXinVw = (KARTENBREITE_VW * 3) - (KARTENBREITE_VW / 3) + 1;
  const deltaX = (deltaXinVw * window.innerWidth) / 100;
  
  const startZ = nachRechts ? karte.dataset.linksZ : karte.dataset.rechtsZ;
  const endZ = nachRechts ? karte.dataset.rechtsZ : karte.dataset.linksZ;
  
  const startX = nachRechts ? 0 : deltaX;
  const endX = nachRechts ? deltaX : 0;
  
  // Animation mit allen Transformationen (translateX + translateZ + rotateY + rotateX)
  const startRotateY = nachRechts ? 0 : 180;
  const endRotateY = nachRechts ? 180 : 0;
  
  const animation = karte.animate([
    { 
      transform: `translateX(${startX}px) translateZ(${startZ}px) rotateY(${startRotateY}deg) rotateX(0deg)`,
      offset: 0
    },
    { 
      transform: `translateX(${startX + (endX - startX) * 0.3}px) translateZ(${Math.max(startZ, endZ) + MAX_FLUGHOEHE * 0.8}px) rotateY(${startRotateY + (endRotateY - startRotateY) * 0.3}deg) rotateX(30deg)`,
      offset: 0.3
    },
    { 
      transform: `translateX(${startX + (endX - startX) * 0.5}px) translateZ(${Math.max(startZ, endZ) + MAX_FLUGHOEHE}px) rotateY(${(startRotateY + endRotateY) / 2}deg) rotateX(40deg)`,
      offset: 0.5
    },
    { 
      transform: `translateX(${startX + (endX - startX) * 0.7}px) translateZ(${Math.max(startZ, endZ) + MAX_FLUGHOEHE * 0.8}px) rotateY(${startRotateY + (endRotateY - startRotateY) * 0.7}deg) rotateX(20deg)`,
      offset: 0.7
    },
    { 
      transform: `translateX(${endX}px) translateZ(${endZ}px) rotateY(${endRotateY}deg) rotateX(0deg)`,
      offset: 1
    }
  ], {
    duration: ANIMATION_DAUER,
    easing: 'ease-in-out',
    fill: 'forwards'
  });
  
  return { animation };
}

// Animation beenden und aufräumen
function beendeAnimation(karte, neueSeite, animObj) {
  karte.dataset.seite = neueSeite;
  
  // Setze finale Transformationen
  const deltaXinVw = (KARTENBREITE_VW * 3) - (KARTENBREITE_VW / 3) + 1;
  const deltaX = (deltaXinVw * window.innerWidth) / 100;
  const finalX = neueSeite === 'rechts' ? deltaX : 0;
  const finalZ = neueSeite === 'rechts' ? karte.dataset.rechtsZ : karte.dataset.linksZ;
  const finalRotateY = neueSeite === 'rechts' ? 180 : 0;
  
  karte.style.transform = `translateX(${finalX}px) translateZ(${finalZ}px) rotateY(${finalRotateY}deg) rotateX(0deg)`;
  
  karte.classList.remove('animating');
  laufendeAnimationen = laufendeAnimationen.filter(a => a.karte !== karte);
  
  // Timeline-Tracker zurücksetzen wenn Animation beendet
  if (animationsTracker && animationsTracker.karte === karte) {
    animationsTracker = null;
    aktualisiereTimelineInfo();
  }
  
  aktualisiereButtons();
}

// Animation starten
function starteAnimation(karte, nachRechts) {
  if (!karte) return;
  
  // Zusätzliche Sicherheit: Prüfe ob bereits animiert
  if (karte.classList.contains('animating')) {
    return;
  }
  
  karte.classList.add('animating');
  const animObj = erstelleAnimationen(karte, nachRechts);
  const neueSeite = nachRechts ? 'rechts' : 'links';
  
  laufendeAnimationen.push({ 
    karte, 
    animation: animObj.animation
  });
  
  // Starte Timeline-Tracking für diese Animation
  animationsTracker = {
    karte: karte,
    karteNr: parseInt(karte.dataset.karte),
    startZeit: Date.now(),
    animation: animObj.animation
  };
  aktualisiereTimelineInfo();
  
  aktualisiereButtons();
  
  animObj.animation.onfinish = () => {
    beendeAnimation(karte, neueSeite, animObj);
  };
}

// Event-Handler-Funktionen
function losClickHandler() {
  starteAnimation(findeObersteKarte(), true);
}

function pauseClickHandler() {
  if (laufendeAnimationen.length === 0) return;
  
  const allesPausiert = laufendeAnimationen.every(a => a.animation.playState === 'paused');
  
  laufendeAnimationen.forEach(({ animation }) => {
    allesPausiert ? animation.play() : animation.pause();
  });
  
  // Bei Fortsetzung der Animation: Timeline-Info neu starten
  if (allesPausiert && animationsTracker) {
    // Korrigiere die Startzeit basierend auf der aktuellen Animation-Zeit
    const animation = animationsTracker.animation;
    if (animation.currentTime) {
      animationsTracker.startZeit = Date.now() - animation.currentTime;
    }
    aktualisiereTimelineInfo();
  }
  
  aktualisiereButtons();
}

function zurueckClickHandler() {
  starteAnimation(findeObersteRechteKarte(), false);
}

// Event-Listener registrieren (mit { once: false } explizit, ist aber default)
losButton.addEventListener('click', losClickHandler);
pauseButton.addEventListener('click', pauseClickHandler);
zurueckButton.addEventListener('click', zurueckClickHandler);

// Initiale Z-Index Positionen für gestapelte Karten
karten.forEach((karte) => {
  const kartenNr = parseInt(karte.dataset.karte);
  
  // Prüfe ob diese Karte bereits initialisiert wurde
  if (karte.dataset.karteInitialisiert === 'true') {
    return; // Überspringe bereits initialisierte Karten
  }
  karte.dataset.karteInitialisiert = 'true';
  
  karte.style.zIndex = kartenNr;
  
  const linksZ = kartenNr * 3;
  const rechtsZ = (4 - kartenNr) * 3;
  
  karte.dataset.linksZ = linksZ;
  karte.dataset.rechtsZ = rechtsZ;
  karte.style.transform = `translateX(0px) translateZ(${linksZ}px) rotateY(0deg) rotateX(0deg)`;
  
  // Click-Handler für jede Karte
  karte.addEventListener('click', (event) => {
    const hatPausierteAnimationen = laufendeAnimationen.length > 0 && 
      laufendeAnimationen.some(a => a.animation.playState === 'paused');
    
    if (karte.classList.contains('animating') || hatPausierteAnimationen) return;
    
    const seite = karte.dataset.seite;
    const obersteKarte = seite === 'links' ? findeObersteKarte() : findeObersteRechteKarte();
    
    if (karte === obersteKarte) {
      // Verhindere Event-Bubbling
      event.stopPropagation();
      event.preventDefault();
      (seite === 'links' ? losButton : zurueckButton).click();
    }
  });
});

// Initiale Button-Zustände setzen
aktualisiereButtons();

// Initiale Timeline-Anzeige setzen
aktualisiereTimelineInfo();

});
</script>
</body>
</html>