mitmario.dev

Zufall und Bewegung

JavaScript Im Browser 4 Min Lesezeit 3 BeispieleLektion 5 von 6

Zwei Themen, die wenig miteinander zu tun haben und beide nirgends sonst hineinpassen.

Zufall

Zwei Rezepte für Zufall
// Math.random liefert eine Zahl von 0 bis knapp unter 1.
console.log("Roh:", Math.random());

// Rezept 1: eine ganze Zahl von min bis einschließlich max.
function zufallsZahl(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// Rezept 2: ein zufälliger Eintrag aus einer Liste.
function zufallsEintrag(liste) {
  return liste[Math.floor(Math.random() * liste.length)];
}

const wuerfe = [];
for (let i = 0; i < 12; i++) {
  wuerfe.push(zufallsZahl(1, 6));
}
console.log("Zwölf Würfe:", wuerfe.join(" "));
console.log("Kleinster:", Math.min(...wuerfe), "Größter:", Math.max(...wuerfe));

console.log("Zufällig gewählt:", zufallsEintrag(["HTML", "CSS", "JavaScript"]));

// Für Kennungen ist Math.random die falsche Wahl.
console.log("Kennung:", crypto.randomUUID());
console.log("Noch eine:", crypto.randomUUID());

Math.random() gibt dir eine Kommazahl von 0 bis knapp unter 1. Die 1 selbst kommt nie heraus, und das ist wichtig für die Rezepte darunter.

Zwei Formeln brauchst du im Alltag:

Eine ganze Zahl von min bis einschließlich max: mal die Spanne, abrunden, plus min. Das + 1 in der Spanne ist der Grund, warum max mitzählt, und es ist die Stelle, an der sich alle vertun.

Ein zufälliger Eintrag aus einer Liste: mal die Länge, abrunden, als Index benutzen.

Mehr sind es nicht. Wenn du beide einmal in eine kleine Hilfsdatei geschrieben hast, schreibst du sie nie wieder.

Für Kennungen nimmst du etwas anderes

Math.random() ist nicht dafür gedacht, eindeutige oder unvorhersehbare Werte zu erzeugen. Die Zahlen sehen zufällig aus, sind es aber nur oberflächlich: Sie stammen aus einer Berechnung, die sich fortsetzen lässt.

Für Kennungen gibt es crypto.randomUUID(). Das liefert eine Zeichenkette in einem festen Format, mit Bindestrichen an festen Stellen, und die Wahrscheinlichkeit einer Wiederholung ist so klein, dass sie in der Praxis keine Rolle spielt.

Faustregel: Sobald ein Wert eindeutig sein oder nicht erratbar sein muss, ist Math.random die falsche Wahl.

requestAnimationFrame

Eine Bewegung mit requestAnimationFrame
const balken = document.querySelector(".balken");
const stand = document.getElementById("stand");

const DAUER = 2000;
const ZIEL = 300;

let frameId = null;
let beginn = 0;

function schritt(jetzt) {
  // Die Zeitmarke kommt vom Browser. Damit rechnest du,
  // nicht mit einem festen Schritt je Bild.
  const anteil = Math.min(1, (jetzt - beginn) / DAUER);
  balken.style.width = `${Math.round(ZIEL * anteil)}px`;
  stand.textContent = `${Math.round(anteil * 100)} Prozent`;

  if (anteil < 1) {
    frameId = requestAnimationFrame(schritt);
  } else {
    frameId = null;
  }
}

document.getElementById("start").addEventListener("click", () => {
  if (frameId !== null) {
    cancelAnimationFrame(frameId);
  }
  beginn = performance.now();
  frameId = requestAnimationFrame(schritt);
});

document.getElementById("stopp").addEventListener("click", () => {
  if (frameId !== null) {
    cancelAnimationFrame(frameId);
    frameId = null;
    stand.textContent = stand.textContent + " (angehalten)";
  }
});

Für Bewegung gibt es requestAnimationFrame. Du übergibst eine Funktion, und der Browser ruft sie vor dem nächsten Bild auf.

Der Unterschied zu setInterval aus Lektion 15.1 hat zwei Teile:

Es läuft im Takt des Bildschirms. Ein setInterval mit 16 Millisekunden trifft den Takt manchmal und manchmal nicht, und dann fällt ein Bild aus oder wird doppelt gezeichnet. Das sieht man.

Es pausiert in einem versteckten Tab. Ein setInterval läuft dort weiter und verbraucht Rechenzeit und Akku für etwas, das niemand sieht.

Ein versteckter Tab ist dabei wirklich ein Tab des Browsers. Wechselst du hier auf den Reiter Console, läuft die Animation weiter: Start, umschalten, kurz warten, zurückschalten, und der Balken steht am Ende. Die Seite ist nur verdeckt, nicht weg.

Wie bei den Timern bekommst du eine Kennung zurück, und mit cancelAnimationFrame hältst du an. Und wie bei den Timern gehört das Anhalten dazu, sobald die Sache in einer Komponente steckt, die wieder abgebaut wird (Lektion 18.6).

Sieh der Bewegung beim Rechnen zu. Starte das zweite Beispiel und schalt danach das Fadenkreuz in der Browser-Leiste ein, also den Inspector. Zeig auf den Balken, und im Auskunftsstreifen darunter zählt es mit: 93 × 24, 135 × 24, 178 × 24, 263 × 24. Das ist dieselbe Zahl, die deine Funktion gerade in style.width schreibt, nur aus der anderen Richtung gelesen.

Die Reihenfolge ist wichtig: erst starten, dann das Fadenkreuz. Ein eingeschalteter Inspector fängt den Klick auf „Start” ab und friert seine Auswahl auf dem Knopf ein, und dann steht dort zwei Sekunden lang button#start statt des Balkens.

Rechne mit der Zeitmarke

Warum nicht mit einem festen Schritt
const DAUER = 1000;
const ZIEL = 300;

// Fassung A: fester Schritt je Bild, ausgelegt auf 60 Hz.
function mitFestemSchritt(bilderProSekunde) {
  let x = 0;
  const bilder = bilderProSekunde;
  for (let i = 0; i < bilder; i++) {
    x = x + ZIEL / 60;
  }
  return Math.round(x);
}

// Fassung B: gerechnet aus der vergangenen Zeit.
function mitZeitmarke(bilderProSekunde) {
  let x = 0;
  const bilder = bilderProSekunde;
  for (let i = 0; i < bilder; i++) {
    const vergangen = ((i + 1) / bilderProSekunde) * 1000;
    x = ZIEL * Math.min(1, vergangen / DAUER);
  }
  return Math.round(x);
}

console.log("Nach einer Sekunde auf 60 Hz:");
console.log("  fester Schritt:", mitFestemSchritt(60));
console.log("  Zeitmarke:", mitZeitmarke(60));

console.log("Nach einer Sekunde auf 120 Hz:");
console.log("  fester Schritt:", mitFestemSchritt(120));
console.log("  Zeitmarke:", mitZeitmarke(120));

console.log("Das Ziel waren", ZIEL, "Pixel.");

Der wichtigste Satz dieser Lektion, und er kostet Anfänger regelmäßig einen Nachmittag.

Deine Funktion bekommt einen Wert übergeben: die Zeit, zu der dieses Bild gezeichnet wird. Damit rechnest du.

Das ist keine Uhrzeit, sondern eine Zahl in Millisekunden seit dem Laden der Seite, mit Nachkommastellen. Dieselbe Zahl liefert dir performance.now(), wenn du den Startzeitpunkt selbst festhalten willst. Für Zeitpunkte im Kalender bleibt Date.now() zuständig, für Dauern ist diese Uhr die genauere.

Verlockend wäre, bei jedem Aufruf einen festen Betrag dazuzuzählen, ausgelegt auf 60 Bilder pro Sekunde. Auf einem 60-Hertz-Bildschirm stimmt das. Auf einem 120-Hertz-Bildschirm läuft die Animation doppelt so schnell, und auf einem überlasteten Gerät zu langsam.

Im dritten Beispiel stehen beide Rechnungen nebeneinander, einmal mit 60 und einmal mit 120 Bildern. Die Fassung mit der Zeitmarke landet in beiden Fällen bei 300 Pixeln, die andere einmal bei 300 und einmal bei 600.

Das Muster dazu ist immer dasselbe: Beim Start merkst du dir den Anfangszeitpunkt. In jedem Bild rechnest du aus der Differenz einen Anteil zwischen 0 und 1 und leitest daraus die Position ab. Bei 1 ist es vorbei.

Und so sieht das von innen aus. Öffne das zweite Beispiel, geh in den Reiter Debug, drück Aufzeichnen und starte danach die Animation. Danach steht dort, was in jedem einzelnen Bild passiert ist: bei mir 616 Schritte, davon 121 Durchläufe der Zeilen 13 bis 20. Blätter durch, und neben der Zeile mit dem Anteil steht jedes Mal sein neuer Wert, anteil=0.0003…, anteil=0.0086…, anteil=0.0169…, bis am Ende anteil=1 kommt und frameId auf null geht.

Zwei Sachen kannst du daran ablesen. 121 Bilder in zwei Sekunden, das sind rund 60 pro Sekunde, und genau das meint „im Takt des Bildschirms”. Und der Abstand zwischen zwei Anteilen ist immer ungefähr derselbe, 0.0083. Mal die zwei Sekunden Dauer sind das 16,6 Millisekunden, also die Zeit zwischen zwei Bildern. Deine Funktion hat diese Zahl nirgends hingeschrieben. Sie steht da, weil sie aus der Uhr kommt.

Und der Verweis auf CSS

Zum Schluss der Satz, der die meiste Arbeit spart: Das meiste davon brauchst du nicht.

Ein Element, das an eine andere Stelle gleiten, ein- oder ausblenden soll, ist in CSS eine Zeile transition. Der Browser macht das dann selbst, oft ganz ohne den Hauptfaden zu belasten, und deine Animation ruckelt nicht, während JavaScript gerade etwas anderes rechnet.

requestAnimationFrame nimmst du für das, was CSS nicht kann: wenn der Verlauf von Daten abhängt, wenn du auf einen Zwischenstand reagieren musst, oder wenn du auf eine Zeichenfläche malst.

Zum Mitnehmen

Rechne in einer Animation immer mit der Zeitmarke, nie mit einem festen Schritt je Bild. Sonst läuft sie auf einem schnellen Bildschirm doppelt so schnell.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.