n00b pro

04. Games

Dit hoofdstuk is onderdeel van de cursus Javascript. Andere cursussen in dezelfde reeks: HTML, CSS, C#, Ontwikkelomgeving.

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:

<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>
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);
});
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():

<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>
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;
});
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():

<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, en sleep het hierin..."></textarea>
body {
   width: 500px;
}
textarea {
   border: 2px ridge;
   height: 200px;
   width: 100%;
}
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;
});
drag drop van teksten

afbeeldingen verslepen

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

<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>
#dropzone {
   border: 2px ridge;
   height: 200px;
   width: 400px;
}
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();
});
drag drop van afbeeldingen

afbeeldingen van desktop naar pagina slepen

Voorbeeld met afbeeldingen vanuit verkenner naar een webpagina slepen:

<div id="dropzone">Drop in images from your desktop</div>
<div id="thumbnails"></div>
#dropzone {
   height: 150px;
   border: 2px dashed #0687FF;
}
#thumbnails {
   height: 125px;
   margin-top: 10px;
}
#thumbnails img {
   height: 100px;
   margin: 10px
}
// drop area
const dropzone = document.querySelector('#dropzone');
const thumbs = 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) {
         thumbs.innerHTML += `<img src="${e.target.result}" alt="${file.name}">`;
      });
      reader.addEventListener('error', () => { alert('an error occurred'); });

      // feed dropped file to reader
      reader.readAsDataURL(file);
   }
});
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

<video id="webcam"></video>
<p><button type="button" id="start">start webcam</button></p>
<p id="message"></p>
#webcam {
   border: 1px dotted #999;
   height: 240px;
   width: 320px;
}
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';
   }
});
webcam voorbeeld

Voorbeeld 2: photobooth

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

<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>
/* 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);
}
// 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();
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:

<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">
.layer {
   position: absolute; /* stapel layers bovenop elkaar */
}
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`;
   }
});
detectie kantelhoek device

Notification API

Toon taskbar notificaties rechtsonder:

<input type="button" id="lnkNotifyMe" value="Notify Me">
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');
      }
   });
});
taskbar notificaties met Javascript

SpeechRecognition API

Basisvoorbeeld:

<p><button id="btnStart">Start listening</button></p>
<p><strong>You said:</strong> <em id="echo">nothing yet</em></p>
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;
   });
});
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

<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>
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();
});
twee buttons die geluidsfragment afspelen (zie demo)

Voorbeeld 2: muziekdoos

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

<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>
// 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();
});
eenvoudige muziekspeler waar ingegeven noten in volgorde afgespeeld worden

Voorbeeld 3: video kiezen en afspelen met weergave tijd

<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>
video {
   background-color: #ccc;
}
const vid = document.querySelector('#theVideo');
const elapsed = document.querySelector('#elapsed');
document.querySelector('#selVideo').addEventListener('change', function() {
   if (this.value == '') return;
   vid.src = `media/${this.value}`
   vid.play();
});
setInterval(function() {
   if (!video.src) return;
   elapsed.innerHTML = `${vid.currentTime.toFixed(1)}/${vid.duration.toFixed(1)}s`;
}, 50);
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:

<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>
// 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;
});
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

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

const btnStop = document.querySelector('#btnStop');
const clock = document.querySelector('#clock');

function padZeros(n) {
   return ('0' + n).slice(-2);
}
function displayCurrentTime() {
   const now = new Date();
   const hours = padZeros(now.getHours());
   const minutes = padZeros(now.getMinutes());
   const seconds = padZeros(now.getSeconds());
   clock.innerHTML = `${hours}:${minutes}:${seconds}`;
}
const timer = setInterval(displayCurrentTime, 100);
btnStop.addEventListener('click', () => clearInterval(timer));
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:

<div id="app"><img src="img/ball.png" id="ball" alt=""></div>
#app {
   background-color: #eee;
   height: 200px;
   position: relative;
   width: 400px;
}

#ball {
   height: 50px;
   left: 10px;
   position: absolute;
   top: 100px;
   width: 50px;
}
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
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 de loop

Voorbeeld 1: stuiterende bal

Het stuiterende bal voorbeeld herschreven met requestAnimationFrame():

<div id="app"><img src="img/ball.png" id="ball" alt=""></div>
#app {
   background-color: #eee;
   height: 200px;
   position: relative;
   width: 400px;
}

#ball {
   height: 50px;
   left: 10px;
   position: absolute;
   top: 100px;
   width: 50px;
}
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); // toon volgende frame wanneer CPU er klaar voor is
}
doLoop(); // start de loop
stuiterende bal

Voorbeeld 2: gunman game

Een compleet voorbeeld met animaties, besturing en geluidseffecten:

<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>
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;
}
// 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();
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="common/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):