n00b pro

Games

Javascript API's

Een Javascript api is heel iets anders dan een web API: het gaat eveneens om een set methodes die de programmeur kan gebruiken in zijn Javascript toepassing, maar dan op het gebied van webcam, bluetooth, fullscreen, speech recognition, 3D enz... Een uitgebreide lijst vind je op https://developer.mozilla.org/en-US/docs/Web/API

Dit hoofdstuk is “Games” gedoopt omdat het een verzameling bevat van allerlei leuke en eenvoudige technieken (waaronder een paar in CSS) die van pas kunnen komen in game-achtige toepassingen.

Local storage API

Zoals cookies, maar dan beter: localStorage bewaart teksten lokaal in de browser van de gebruiker met de eenvoudige methodes localStorage.getItem(key) en localStorage.setItem(key, value).

tekst bewaren

Eenvoudig voorbeeld waar de gekozen waarde uit een lijst persisteert:

<!-- HTML -->
<p>selecteer een waarde uit de lijst, en refresh:</p>
<select id="selTest">
  <option selected>keuze 1</option>
  <option>keuze 2</option>
  <option>keuze 3</option>
  <option>keuze 4</option>
</select>
<p><em>→ merk op dat de keuze persisteert</em></p>

<!-- SCRIPT -->
<script>
  const selTest = document.querySelector('#selTest');
  const savedValue = localStorage.getItem('selectValue');
  if (savedValue != undefined) selTest.value = savedValue;
  selTest.addEventListener('change', function() {
    console.log(this.value)
    localStorage.setItem('selectValue', this.value);
  });
</script>
geselecteerde waarde blijft bewaard

objecten bewaren

Met localStorage kan je enkel strings bewaren. Je kan objecten wel omzetten naar strings en terug met JSON.parse() en JSON.stringify():

<!-- HTML -->
<p>selecteer een waarde uit de lijst, en refresh:</p>
<p><label>Naam: <input id="inpName" type="text" value="Rogier"></label></p>
<p><label>Leeftijd: <input id="inpAge" type="number" value="54"></label></p>
<p>
  <button type="button" id="btnSave">opslaan</button>
  <button type="button" id="btnHerstel">herstellen</button>
</p>

<!-- SCRIPT -->
<script>
  const inpName = document.querySelector('#inpName');
  const inpAge = document.querySelector('#inpAge');
  document.querySelector('#btnSave').addEventListener('click', function() {
    const person = {
      name: inpName.value,
      age: inpAge.value,
    };
    localStorage.setItem('person', JSON.stringify(person));
  });
  document.querySelector('#btnHerstel').addEventListener('click', function() {
    if (!localStorage.getItem('person')) return;
    const person = JSON.parse(localStorage.getItem('person'));
    inpName.value = person.name;
    inpAge.value = person.age;
  });
</script>
herstellen naar laatst opgeslagen object

opgeslagen items bekijken

De localStorage items worden bewaard per site. Je kan ze altijd terugvinden in de inspector onder de Application tab:

opgeslagen localStorage items vind je onder de Application tab

Drag drop API

teksten verslepen

Drag drop van teksten is eenvoudig met het ondrop event en dataTransfer.getData():

<!-- CSS -->
<style>
  body {
    width: 500px;
  }
  textarea {
    border: 2px ridge;
    height: 200px;
    width: 100%;
  }
</style>

<!-- HTML -->
<p>
  In alteration insipidity impression by travelling reasonable up motionless. Of
  regard warmth by unable sudden garden ladies. No kept hung am size spot no. Likewise
  led and dissuade rejoiced welcomed husbands boy. Do listening on he suspected
  resembled. Water would still if to. Position boy required law moderate was may.
</p>
<textarea placeholder="...selecteer een stuk tekst hierboven, en sleep het hierin..."></textarea>

<!-- SCRIPT -->
<script>
  const dropZone = document.querySelector('textarea');
  dropZone.addEventListener('dragover', (e)  => e.preventDefault() );
  dropZone.addEventListener('dragenter', (e)  => e.preventDefault() );
  dropZone.addEventListener('drop', function(e) {
    e.preventDefault();
    txtPassed = e.dataTransfer.getData('text/plain');
    this.innerHTML = txtPassed;
  });
</script>
drag drop van teksten

afbeeldingen verslepen

Voorbeeld met afbeeldingen (tweede afbeelding is niet draggable wegens draggable="false"):

<!-- CSS -->
<style>
  #dropzone {
    border: 2px ridge;
    height: 200px;
    width: 400px;
  }
  ol {
    list-style: none;
    padding: 0;
  }
  ol li {
    display: inline-block;
  }
</style>

<!-- HTML -->
<div id="dragzone">
  <img src="img/product1.jpg" alt="">
  <img src="img/product2.jpg" alt="" draggable="false">
  <img src="img/product3.jpg" alt="">
</div>
<div id="dropzone">
  ...drop item here...
</div>

<!-- SCRIPT -->
<script>
  const dropZone = document.querySelector('#dropzone');
  dropZone.addEventListener('dragover', (e) => e.preventDefault());
  dropZone.addEventListener('dragenter', (e) => e.preventDefault());
  dropZone.addEventListener('drop', function (e) {
    imgPassed = e.dataTransfer.getData('text/uri-list');
    this.innerHTML = "<img src='" + imgPassed + "'/>";
    e.stopPropagation();
    e.preventDefault();
  });
</script>
drag drop van afbeeldingen

afbeeldingen van desktop naar pagina slepen

Voorbeeld met afbeeldingen vanuit verkenner naar een webpagina slepen:

<!-- CSS -->
<style>
  #dropzone {
    height: 150px;
    border: 2px dashed #0687FF;
  }
  #thumbnails {
    height: 125px;
    margin-top: 10px;
  }
  #thumbnails img {
    height: 100px;
    margin: 10px
  }
</style>

<!-- HTML -->
<div id="dropzone">Drop in images from your desktop</div>
<div id="thumbnails"></div>

<!-- SCRIPT -->
<script>

  // drop area
  const dropzone = document.querySelector('#dropzone');
  const thumbnails = document.querySelector('#thumbnails');

  // dropzone events
  dropzone.addEventListener('dragenter', (e) => { e.preventDefault(); });
  dropzone.addEventListener('dragover', (e) => { e.preventDefault(); });
  dropzone.addEventListener('dragleave', (e) => { e.preventDefault(); });
  dropzone.addEventListener('drop', function (e) {
    e.preventDefault();

    // fetch dropped files
    for (const file of e.dataTransfer.files) {
      // images only
      if (!file.type.match(/image.*/)) continue;

      // create reader for dropped file
      const reader = new FileReader();
      reader.addEventListener('load', function (e) {
        thumbnails.innerHTML = `${thumbnails.innerHTML}<img src="${e.target.result}" alt="${file.name}">`;
      });
      reader.addEventListener('error', () => { alert('an error occurred'); });

      // feed dropped file to reader
      reader.readAsDataURL(file);
    }
  });

</script>
sleep afbeeldingen in een pagina

Webcam API

De webcam gebruiken is heel eenvoudig: vraag een webcamstream op via navigator.mediaDevices.getUserMedia(), en stel het in als bron van een <video> element.

Voorbeeld 1: weergave webcam in een video element

<!-- CSS -->
<style>
  #webcam {
    border: 1px dotted #999;
    height: 240px;
    width: 320px;
  }
</style>

<!-- HTML -->
<video id="webcam"></video>
<p><button type="button" id="start">start webcam</button></p>
<p id="message"></p>

<!-- SCRIPT -->
<script>
  const vidWebcam = document.querySelector('#webcam');
  const btnStart = document.querySelector('#start');
  const parMessage = document.querySelector('#message');
  btnStart.addEventListener('click', async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
      vidWebcam.srcObject = stream;
      vidWebcam.autoplay = true;
    } catch {
      parMessage.innerHTML = 'weergave webcam mislukt';
    }
  });
</script>
webcam voorbeeld

Voorbeeld 2: photobooth

Een iets uitgebreider voorbeeld, met filters en de mogelijkheid snapshots te nemen:

<!-- CSS -->
<style>
  /* general */
  body {
    margin: 20px auto;
    width: 640px;
  }
  /* webcam */
  #webcamstream {
    height: 480px;
    width: 640px;
  }
  /* snapshots */
  canvas {
    border: 1px solid #ccc;
    margin: 10px 0;
  }
  canvas + canvas {
    margin-left: 29px;
  }
  /* buttons */
  button {
    background-color: #428bca;
    border: 1px solid #2a6496;
    border-radius: 4px;
    color: white;
    cursor: pointer;
    padding: 3px 6px;
    text-transform: uppercase;
  }
  #dosnap {
    position: absolute;
    top: 10px;
    right: 10px;
  }
  /* filters */
  .sepia {
    filter: sepia(1);
  }
  .blur {
    filter: blur(5px);
  }
  .grayscale {
    filter: grayscale(1);
  }
  .invert {
    filter: invert(1);
  }
  .hue-rotate {
    filter: hue-rotate(90deg);
  }
</style>

<!-- HTML -->
<video id="webcamstream"></video>
<div id="filters">
  <button type="button" data-filter="natural">natural</button>
  <button type="button" data-filter="sepia">sepia</button>
  <button type="button" data-filter="blur">blur</button>
  <button type="button" data-filter="grayscale">grayscale</button>
  <button type="button" data-filter="invert">invert</button>
  <button type="button" data-filter="hue-rotate">hue-rotate</button>
</div>
<p><button type="button" id="btnsnap">take snapshot</button></p>
<div id="canvases">
  <canvas width="133" height="100"></canvas>
  <canvas width="133" height="100"></canvas>
  <canvas width="133" height="100"></canvas>
  <canvas width="133" height="100"></canvas>
</div>

<!-- SCRIPT -->
<script>
  // inits
  const vidWebcam = document.querySelector('#webcamstream');
  const btnSnap = document.querySelector('#btnsnap');
  const snapshots = document.querySelectorAll('canvas');
  const btnsFilter = document.querySelector('#filters');
  let nr = 0;

  // take snapshot
  btnSnap.addEventListener('click', function () {
    const context = snapshots[nr].getContext('2d');
    context.filter = getComputedStyle(vidWebcam).getPropertyValue('filter');
    context.drawImage(vidWebcam, 0, 0, 133, 100);
    nr = (nr + 1) % 4;
  });

  // apply filter
  btnsFilter.addEventListener('click', function (e) {
    btnsFilter.querySelectorAll('button').forEach(btn => {
      vidWebcam.classList.remove(btn.dataset.filter);
    });
    vidWebcam.classList.add(e.target.getAttribute('data-filter'));
  });

  // capture webcam
  async function startWebcam() {
    vidWebcam.autoplay = true;
    const stream = await navigator.mediaDevices.getUserMedia({ video: true });
    vidWebcam.srcObject = stream;
  };
  startWebcam();

</script>
photobooth voorbeeld

Nog meer online experimenten:

Geolocation API

De positie van de gebruiker is eenvoudig te bepalen via navigator.geolocation:

if (navigator.geolocation) {
  navigator.geolocation.getCurrentPosition(function(pos) {
    console.log(`je positie: ${pos.coords.longitude}, ${pos.coords.latitude}`);
  });
}

Voorbeeld: positie met custom marker tonen op kaart (OpenLayers)

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Geolocation demo</title>
  <meta charset="utf-8">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v9.0.0/ol.css">
  <style>
    #map {
      height: 300px;
      width: 500px;
    }
  </style>
</head>

<body>
  <p><button type="button" id="btn1">Show Position</button></p>
  <div id="map"></div>
  <script src="https://cdn.jsdelivr.net/npm/ol@v9.0.0/dist/ol.js"></script>
  <script>
    // show map with custom marker on current position
    function showPosition(position) {
      const longitude = position.coords.longitude
      const latitude = position.coords.latitude

      // create marker
      const marker = new ol.Feature({
        geometry: new ol.geom.Point(ol.proj.fromLonLat([longitude, latitude]))
      });

      // custom marker icon
      marker.setStyle(new ol.style.Style({
        image: new ol.style.Icon({
          src: 'img/marker.png'
        })
      }));

      // create layer with marker
      const vectorLayer = new ol.layer.Vector({
        source: new ol.source.Vector({
          features: [marker]
        })
      });

      // create map with layer
      const map = new ol.Map({
        target: 'map',
        layers: [
          new ol.layer.Tile({
            source: new ol.source.OSM()
          }),
          vectorLayer
        ],
        view: new ol.View({
          center: ol.proj.fromLonLat([longitude, latitude]),
          zoom: 12 // Zoom level
        })
      });
    }

    // button click event
    document.querySelector('#btn1').addEventListener('click', function () {
      if (!navigator.geolocation) return;
      navigator.geolocation.getCurrentPosition(showPosition);
    });
  </script>
</body>

</html>
tonen van huidige positie op een OpenLayers kaart

Device orientation API

Indien aanwezig op het toestel, kan je de gyroscoop, accelerometer, kompas enz... gebruiken in Javascript. Voorbeeld voor de kanteling van het toestel (werkt enkel op smartphones en sommige laptops als bepaalde Macbooks), dat gebruik maakt van het deviceorientation event van window:

<!-- CSS -->
<style>
  .layer {
    position: absolute; /* stapel layers bovenop elkaar */
  }
</style>

<!-- HTML -->
<img src="img/chrome-layer-01.png" alt="" class="layer">
<img src="img/chrome-layer-02.png" alt="" class="layer">
<img src="img/chrome-layer-03.png" alt="" class="layer">
<img src="img/chrome-layer-04.png" alt="" class="layer">
<img src="img/chrome-layer-05.png" alt="" class="layer">

<!-- SCRIPT -->
<script>
  window.addEventListener('deviceorientation', function(e) {
    const layers = document.querySelectorAll('.layer');
    const offset = 50;
    for (let i = 0; i < layers.length; i++) {
      const layer = layers[i];
      layer.style.left = `${Math.round(e.gamma * i) + offset}px`;
      layer.style.top = `${Math.round(e.beta * i) + offset}px`;
    }
  });
</script>
detectie kantelhoek device

Notification API

Toon taskbar notificaties rechtsonder:

<!-- HTML -->
<input type="button" id="lnkNotifyMe" value="Notify Me">

<!-- SCRIPT -->
<script>
  function showNotification(title, msg, icon) {
    const notification = new Notification(title, { body: msg, icon: icon } );
  }
  document.getElementById('lnkNotifyMe').addEventListener('click', function() {
    if (Notification.permission == 'denied') return;
    if (Notification.permission == 'granted') {
      showNotification('Some notification', 'Hi there!', 'img/avatar.jpg');
      return;
    }
    Notification.requestPermission(function (permission) {
      if (permission === 'granted') {
        showNotification('Some notification', 'Hi there!', 'img/avatar.jpg');
      }
    });
  });
</script>
taskbar notificaties met Javascript

SpeechRecognition API

Basisvoorbeeld:

<!-- HTML -->
<p><button id="btnStart">Start listening</button></p>
<p><strong>You said:</strong> <em id="echo">nothing yet</em></p>

<!-- SCRIPT -->
<script>
  const btnStart = document.querySelector('#btnStart');
  const emEcho = document.querySelector('#echo');

  btnStart.addEventListener('click', function() {
    const recognition = new (webkitSpeechRecognition || SpeechRecognition)();
    recognition.lang = 'en-US';
    recognition.start();
    recognition.addEventListener('result', function(e) {
        emEcho.innerHTML = e.results[0][0].transcript;
    });
  });
</script>
stemherkenning met Javascript

Audio en video

Je kan eenvoudig audio en video laden, afspelen, volume instellen enz... Meest voorkomende properties en methodes:

property omschrijving
currentTime huidige speelpositie in seconden
duration lengte van het bestand in seconden
isPaused is het gepauzeerd of niet (true of false)
loop herhalen of niet (true of false)
src bron van het geluidsbestand (URL)
volume volume (getal tussen 0 en 1)
methode omschrijving
play() speel af
pause() pauzeer

Voorbeeld 1: geluidseffecten

<!-- HTML -->
<p><button id="btnRight"><img src="img/code_right.png" height="20" alt=""> RIGHT</button></p>
<p><button id="btnWrong"><img src="img/code_wrong.png" height="20" alt=""> WRONG</button></p>

<!-- SCRIPT -->
<script>
  const sfxRight = new Audio('media/right.mp3');
  const sfxWrong = new Audio('media/wrong.mp3');
  document.querySelector('#btnRight').addEventListener('click', function() {
    sfxRight.play();
  });
  document.querySelector('#btnWrong').addEventListener('click', function() {
    sfxWrong.play();
  });
</script>
twee buttons die geluidsfragment afspelen (zie demo)

Voorbeeld 2: muziekdoos

Tik een reeks van namen van noten in, en laat het muziekje afspelen:

<!-- HTML -->
<p>
  <label>notes: <input type="text" value="A Cs D F A" id="inpSong"></label>
  <input type="button" id="btnPlay" value="play">
</p>
<p id="msg"></p>

<!-- SCRIPT -->
<script>
  // inits
  const msg = document.querySelector('#msg');
  let notes;

  // play note i
  function playNextNote() {
    if (!notes.length) {
        msg.innerHTML = 'finished';
        return;
    }
    note = notes.shift();
    msg.innerHTML = `playing ${note}`;
    const audio = new Audio(`media/notes/violin-${note}.mp3`);
    audio.addEventListener('ended', playNextNote);
    audio.play();
  }

  // add event listener to button
  document.querySelector('#btnPlay').addEventListener('click', function() {
    notes = document.querySelector('#inpSong').value.trim().split(' ');
    playNextNote();
  });
</script>
eenvoudige muziekspeler waar ingegeven noten in volgorde afgespeeld worden

Voorbeeld 3: video kiezen en afspelen met weergave tijd

<!-- HTML -->
<p>
   <select id="selVideo">
     <option value="">selecteer video...</option>
     <option>dinosaur.mp4</option>
     <option>fish.mp4</option>
     <option>toys.mp4</option>
   </select>
 </p>
 <p><video id="theVideo" width="640" height="360"></video></p>
 <p>tijd: <span id="elapsed">-</span></p>

 <!-- CSS -->
 <style>
   video {
     background-color: #ccc;
   }
 </style>

<!-- SCRIPT -->
<script>
  const video = document.querySelector('#theVideo');
  const elapsed = document.querySelector('#elapsed');
  document.querySelector('#selVideo').addEventListener('change', function() {
    if (this.value == '') return;
    video.src = `media/${this.value}`
    video.play();
  });
  setInterval(function() {
    if (!video.src) return;
    elapsed.innerHTML = `${video.currentTime.toFixed(1)}/${video.duration.toFixed(1)}s`;
  }, 50);
</script>
video afspelen uit lisjtje met weergave verstreken tijd

Sound API

Dit is een zeer uitgebreide API waarmee je zowat alles kan wat met geluid te maken heeft:

Enkele mooie online voorbeelden:

Voorbeeld: eenvoudige oscillator met versterker

Merk op hoe in de code als het ware toestellen aan elkaar gekoppeld worden:

<!-- HTML -->
<p><label>Frequency: <input type="range" min="200" value="1100" max="2000" id="rngFrequency"></label> </p>
<p><label>Volume: <input type="range" min="0" value="0.5" max="1" id="rngVolume" step="0.01"></label></p>
<p><button id="btnStop">stop</button></p>

<!-- SCRIPT -->
<script>
  // create audio context
  const audioContext = new AudioContext();

  // create oscillator
  const oscillator = audioContext.createOscillator();
  oscillator.frequency.value = 1000;

  // create amp
  const amp = audioContext.createGain();
  amp.gain.value = 0.5;

  // connect oscillator to amp, and amp to output
  oscillator.connect(amp);
  amp.connect(audioContext.destination);

  // start oscillator
  oscillator.start();

  // bind events
  document.querySelector('#rngFrequency').addEventListener('input', function() {
    oscillator.frequency.value = this.value;
  });
  document.querySelector('#rngVolume').addEventListener('input', function() {
    amp.gain.value = this.value;
  });
  document.querySelector('#btnStop').addEventListener('click', function() {
    oscillator.stop();
    this.disabled = true;
  });
</script>
afspelen sinusgolf en regelen volume en frequentie

Nog meer API's: battery API, gamepad API, bluetooth API...

Er zijn nog veel meer API's te ontdekken — velen gevestigd, sommigen nog experimenteel. Een uitgebreide lijst vind je op https://developer.mozilla.org/en-US/docs/Web/API

Animaties

Wat is een animatie?

Een animatie is in essentie een snelle opeenvolging van stilstaande afbeeldingen of frames, die een illusie van een vloeiende beweging creëren:

stop motion – bekijk online
zoetrope – bekijk hier of hier
flip book – bekijk online

Sprites

De meeste computerspellen bevatten bewegende objecten of sprites, die geanimeerd worden. Enkele definities:

Spritesheets zijn afbeeldingen die alle mogelijke versies van één object bevatten. Een voorbeeld uit de klassieker Doom I:

spritesheet van een karakter uit Doom I

Animaties met setInterval()

De methode setInterval(callback, ms) voert een methode callback uit met vaste tussenposen van ms milliseconden. De timer kan gestopt worden met clearInterval(). Eenvoudig voorbeeld:

Voorbeeld 1: klok

<!-- HTML -->
<p>de tijd is <span id="clock"></span></p>
<p><button id="btnStop">stop de tijd</button></p>

<script>
  function padZeros(n) {
    return ('0' + n).slice(-2);
  }
  function displayCurrentTime() {
    const now = new Date();
    var hours = padZeros(now.getHours());
    var minutes = padZeros(now.getMinutes());
    var seconds = padZeros(now.getSeconds());
    document.querySelector('#clock').innerHTML = `${hours}:${minutes}:${seconds}`;
  }
  const timer = setInterval(displayCurrentTime, 100);
  document.querySelector('#btnStop').addEventListener('click', () => clearInterval(timer));
</script>
eenvoudige klok

Voorbeeld 2: stuiterende bal

Je zou setInterval() ook kunnen gebruiken om animaties in je game te timen. Eenvoudig voorbeeld met een stuitende bal:

<!-- HTML -->
<div id="app"><img src="img/ball.png" id="ball" alt=""></div>

<!-- CSS -->
<style>
  #app {
    background-color: #eee;
    height: 200px;
    position: relative;
    width: 400px;
  }

  #ball {
    height: 50px;
    left: 10px;
    position: absolute;
    top: 100px;
    width: 50px;
  }
</style>

<!-- SCRIPT -->
<script>
  let speedX = 10; let speedY = 5; let speedR = 10;
  let x = 10; let y = 100; let r = 0;
  const ball = document.querySelector('#ball');
  function doLoop() {
    x += speedX;
    y += speedY;
    r += speedR;
    ball.style.left = `${x}px`;
    ball.style.top = `${y}px`;
    ball.style.transform = `rotate(${r}deg)`;
    if (x < 0 || x > 350) { speedX *= -1; speedR *= -1; }
    if (y < 0 || y > 150) { speedY *= -1; speedR *= -1; }
  }
  setInterval(doLoop, 20); // herhaal elke 20ms
</script>
stuiterende bal

De animatieframes worden exact om de 20ms berekend en getoond. Dit is ok bij heel eenvoudige games en animaties, maar bij complexere games kan het zijn dan de GPU niet meer kan volgen, en dat animaties schokkerig of onvolledig worden. Daarom gebruiken we voor games requestAnimationFrame() (zie volgend onderdeel).

Animaties met requestAnimationFrame()

De requestAnimationFrame() is speciaal bedoeld voor games: het wordt opgeroepen standaard 60 keer per seconde, maar niet eerder dan de GPU klaar is voor het volgende frame. Vergelijk volgende fragmenten:

function doLoop() {
  // animatie logica hier
  // ...
}
setInterval(doLoop, 20); // voer doLoop() elke 20ms uit


function doLoop() {
  // animatie logica hier
  // ...
  // voer doLoop() pas weer uit als de GPU klaar is
  requestAnimationFrame(doLoop);
}
doLoop(); // start the loop

Voorbeeld 1: stuiterende bal

Het stuiterende bal voorbeeld herschreven met requestAnimationFrame():

<!-- HTML -->
<div id="app"><img src="img/ball.png" id="ball" alt=""></div>

<!-- CSS -->
<style>
  #app {
    background-color: #eee;
    height: 200px;
    position: relative;
    width: 400px;
  }

  #ball {
    height: 50px;
    left: 10px;
    position: absolute;
    top: 100px;
    width: 50px;
  }
</style>

<!-- SCRIPT -->
<script>
  let speedX = 10; let speedY = 5; let speedR = 10;
  let x = 10; let y = 100; let r = 0;
  const ball = document.querySelector('#ball');
  function doLoop() {
    x += speedX;
    y += speedY;
    r += speedR;
    ball.style.left = `${x}px`;
    ball.style.top = `${y}px`;
    ball.style.transform = `rotate(${r}deg)`;
    if (x < 0 || x > 350) { speedX *= -1; speedR *= -1; }
    if (y < 0 || y > 150) { speedY *= -1; speedR *= -1; }
    requestAnimationFrame(doLoop); // call next frame whenever the CPU is ready
  }
  doLoop(); // start the loop
</script>
stuiterende bal

Voorbeeld 2: gunman game

Een compleet voorbeeld met animaties, besturing en geluidseffecten:

<!-- HTML -->
<div id="app">
  <img id="gunman" alt="">
  <div class="buttons">
    <button id="btnStop">stop</button>
    <button id="btnWalk">walk</button>
    <button id="btnRun">run</button>
    <button id="btnShoot">shoot</button>
    <button id="btnDie">die</button>
    <button id="btnTurn">turn</button>
  </div>
</div>

<!-- CSS -->
<style>
  body {
    background-color: #333;
  }

  #app {
    background-color: rgb(112, 61, 61);
    background-image: url(img/panorama.jpg);
    background-position: -750px 0;
    width: 780px;
    height: 500px;
    margin: 0 auto;
    position:  relative;
  }

  .buttons {
    display: flex;
    left: 20px;
    position: absolute;
    top: 20px;
    z-index: 10;
  }

  .buttons button {
    margin: 0 5px;
  }

  #gunman {
    background-image: url(img/sprite.png);
    bottom: 100px;
    display: block;
    height: 150px;
    left: 400px;
    overflow: hidden;
    position: absolute;
    width: 150px;
  }
</style>

<!-- SCRIPT -->
<script>
  // sprites list
  const sprites = [];

  // sound fx
  const sndSplash = new Audio('media/splash.mp3');
  const sndGun = new Audio('media/gun.mp3');
  sndGun.loop = true;

  // DOM elements
  const app = document.querySelector('#app');
  const btnStop = document.querySelector('#btnStop');
  const btnWalk = document.querySelector('#btnWalk');
  const btnRun = document.querySelector('#btnRun');
  const btnShoot = document.querySelector('#btnShoot');
  const btnDie = document.querySelector('#btnDie');
  const btnTurn = document.querySelector('#btnTurn');

  // background offset
  let bgOffsetX = -750;

  // gunman
  const gunman = {
    el: document.querySelector('#gunman'), // DOM element
    w: 150, // sprite width
    h: 150, // sprite height
    fps: 5, // frames per second
    isDead: false,
    startTime: new Date(), // in milliseconds
    direction: 1, // 1 = left, -1 = right
    state: 'walk', // stop, walk, die, shoot
    setState: function(state) { // sets sprite state
      sndGun.pause();
      this.startTime = new Date();
      this.state = state;
      this.fps = 15;
      if (state == 'run') this.fps = 15;
      if (state == 'walk') this.fps = 5;
      if (state == 'stop') this.fps = 3;
      if (state == 'die') sndSplash.play();
      if (state == 'shoot') sndGun.play();
    },
    frames: { // series of images from sprite sheet
      stop: [
        [0, 0]
      ],
      walk: [
        [2, 0],
        [3, 0],
        [4, 0]
      ],
      run: [
        [2, 0],
        [3, 0],
        [4, 0]
      ],
      die: [
        [0, 1],
        [1, 1],
        [2, 1],
        [3, 1],
        [4, 1],
        [5, 1],
        [6, 1],
        [7, 1],
        [8, 1],
        [9, 1]
      ],
      shoot: [
        [0, 0],
        [0, 0],
        [1, 0],
        [1, 0]
      ]
    }
  };

  // add gunman to sprites list
  sprites.push(gunman);

  // render a sprite
  function renderSprite(sprite) {
    // ignore dead sprites
    if (sprite.isDead) return;

    // detect death
    const timePassed = new Date() - sprite.startTime;
    const currFrames = sprite.frames[sprite.state];
    let frameNr = parseInt(timePassed * sprite.fps / 1000);
    if (sprite.state == 'die' && frameNr >= currFrames.length) {
      sprite.isDead = true;
      return;
    }

    // adjust frame
    frameNr = frameNr % currFrames.length;
    sprite.el.style.backgroundPositionX = `-${sprite.w * currFrames[frameNr][0]}px`;
    sprite.el.style.backgroundPositionY = `-${sprite.h * currFrames[frameNr][1]}px`;
    sprite.el.style.transform = `scaleX(${sprite.direction})`;

    // adjust background
    if (sprite.state == 'walk' || sprite.state == 'run') bgOffsetX += sprite.direction * sprite.fps / 5;
    app.style.backgroundPositionX = `${bgOffsetX}px`;
  }

  function doLoop() {
    for (const sprite of sprites) renderSprite(sprite);
    requestAnimationFrame(doLoop);
  }

  btnStop.addEventListener('click', function() {
    gunman.setState('stop');
  });
  btnWalk.addEventListener('click', function() {
    gunman.setState('walk');
  });
  btnRun.addEventListener('click', function() {
    gunman.setState('run');
  });
  btnShoot.addEventListener('click', function() {
    sndGun.play();
    gunman.setState('shoot');
  });
  btnDie.addEventListener('click', function() {
    gunman.setState('die');
  });
  btnTurn.addEventListener('click', function() {
    gunman.direction = gunman.direction * -1;
  });

  // start the game
  doLoop();</script>
screenshot van de gunman game

3D in Javascript

3D begrippen

Voor 3D graphics en animaties hebben we het volgende nodig:

Om die scene effectief weer te geven (renderen) moet je ook denken aan:

Tenslotte hebben we voor animaties ook één of andere game loop nodig die de frames weergeeft.

3D met Three.js library

Er zijn verschillende libraries die het werken met 3D in Javascript vereenvoudigen, maar een uitstekende is Three.js.

Voorbeeld 1: roterende donut eenvoudig

Codevoorbeeld van een roterende donut:


<!-- SCRIPTS -->
<script src="vendor/three.min.js"></script>
<script>
  // init scene
  const scene = new THREE.Scene();

  // add ground
  const groundGeometry = new THREE.PlaneGeometry(200, 300, 32);
  const  groundTexture = (new THREE.TextureLoader()).load('img/floor.jpg');
  groundTexture.wrapS = THREE.RepeatWrapping;
  groundTexture.wrapT = THREE.RepeatWrapping;
  groundTexture.repeat.set(4, 4);
  const groundMaterial = new THREE.MeshPhongMaterial({
    shininess: 15,
    specular: 0x888888,
    shading: THREE.SmoothShading,
    side: THREE.DoubleSide,
    map: groundTexture
  });
  const ground = new THREE.Mesh(groundGeometry, groundMaterial);
  ground.rotation.x = Math.PI / 360 * 110;
  ground.castShadow = false;
  ground.receiveShadow = true;
  scene.add(ground);

  // add shape
  const shapeGeometry = new THREE.TorusGeometry(30, 10, 12, 24);
  const shapeMaterial = new THREE.MeshPhongMaterial({
    color: 0x156289,
    side: THREE.DoubleSide,
    shading: THREE.FlatShading,
    shininess: 60,
    specular: 0x156289
  });
  const shape = new THREE.Mesh(shapeGeometry, shapeMaterial);
  shape.castShadow = true;
  shape.receiveShadow = false;
  shape.position.y = 70;
  shape.rotation.y = Math.PI / 360 * 120;
  scene.add(shape);

  // add ambient light
  const ambientlight = new THREE.AmbientLight(0x444444, 2.5);
  scene.add(ambientlight);

  // add spotlight
  const spotlight = new THREE.SpotLight(0xFFFFFF, 0.7);
  spotlight.position.set(150, 200, -75);
  spotlight.shadow.camera.visible = true;
  spotlight.castShadow = true;
  spotlight.penumbra = 0.1;
  spotlight.angle = 0.4;
  scene.add(spotlight);

  // add camera
  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 1000);
  camera.position.y = 250;
  camera.position.z = 250;
  camera.lookAt(new THREE.Vector3(0, 50, 0));

  // init renderer
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor(new THREE.Color(0x000000));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  document.body.appendChild(renderer.domElement);

  // render
  function render() {
    // keep looping
    requestAnimationFrame(render);
    shape.rotation.y += 0.03;

    // render the scene
    renderer.render(scene, camera);
  }
  render();
</script>
roterende donut in Three.js

Voorbeeld 2: roterende donut geavanceerd

Geavanceerde versie van de roterende donut (demo hier, zip met code hier):

Voorbeeld 3: vier op een rij in 3D

Vier op een rij in 3D (demo hier, zip met code hier):

Voorbeeld 4: 3D OXO

OXO in 3D (demo hier, zip met code hier):