JAVASCRIPT GAMES

use the arrow keys to navigate; press space for slide overview

JAVASCRIPT GAMES

Intro

What is a game?

board game
  • it's FUN
  • it's INTERACTIVE
  • it's POINTLESS
  • It may or may not have a start and/or an ending
  • It may or may not have a competition element
  • It may be based on skills, luck or both

So you want to create games

war game
  • beware of scope: what can you create?
  • unless you work in a team of hundreds for several years, you'll never create something like Final Fantasy or God of War
  • don't focus on gameplay and graphics too soon; your first games are a learning experience, not your master work
  • start with a lousy version of tic-tac-toe, and be really proud of it
  • eventually you'll build games that you and others find enjoyable

Where to start

The first step should be to experiment and familiarize yourself with the terminology and basic principles:

wordcloud
  • shapes: geometry, mapping (texture, bump, displacement etc.), mesh, polygon, vertex...
  • light: illumination models (Phong, Lambert,...), color, shading, light sources (ambient, spot)...
  • audio: sound effects, context, sythesis...
  • rendering: context, scene, camera...
  • animation: frame, tween, kinematics...
  • gameplay: input, events, AI, collision detection...
  • math: origin, coordinates, vector, transformations...
  • ...

What is the best tool?

Which tool has a large scope, is flexible, easy to learn, preferably near to free, and good to begin with? Unity? Unreal? GameMaker? Godot?

unity

🦄 it doesn't exist

  • you'll dive in, get frustrated, disappointed, and quit before you even finish your first game
  • you'll learn a tool, not game programming; the tool gets in the way of learning about games

Comes in: basic web technologies

In my opinion, the best start is just a browser and a text editor. HTML, CSS and Javascript have all a beginner needs to start building great games.

oxo

Let's see some examples of what HTML, CSS and Javascript have to offer...

JAVASCRIPT GAMES

Javascript Goodies

LocalStorage

  • Persist data locally on the user's browser. Simple todo list example:
  • <!DOCTYPE html>
    <html lang="en">
    <body>
      <h1>My simple todo list</h1>
      <ul id="list1" contenteditable>
        <li>do groceries</li>
      </ul>
      <script>
        var list1 = document.getElementById("list1");
        list1.addEventListener('blur', function() {
          localStorage.setItem("todoData", this.innerHTML);
        });
        if (localStorage.getItem("todoData")) {
          list1.innerHTML = localStorage.getItem("todoData");
        }
      </script>
    </body>
    </html>

Drag and drop

  • Drag/drop made easy with ondrop event and dataTransfer.getData() method:
  •   ...
      <script>
        window.addEventListener('load', function() {
          var dropZone = document.querySelector('#dropzone');
          dropZone.addEventListener('dragenter', function(e) {
            e.stopPropagation();
            e.preventDefault();
          });
          dropZone.addEventListener('dragover', function(e) {
            e.stopPropagation();
            e.preventDefault();
          });
          dropZone.addEventListener('drop', function(e) {
            txtPassed = e.dataTransfer.getData('text/plain');
            this.innerHTML = txtPassed;
            e.stopPropagation();
            e.preventDefault();
          });
        });
      </script>
      <style>
        #dropzone {
          height: 200px;
          width: 400px;
          border: 2px ridge;
        }
      </style>
    </head>
    <body>
      <p>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
        dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
        ex ea commodo consequat.
      </p>
      <p>
        Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
        Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim
      </p>
      <div id="dropzone">
        ...select a text above, and drag it here...
      </div>
    </body>
    </html>
    

Drag and drop images

  • Example with images and draggable attribute:
  •   ...
      <script>
        window.addEventListener('load', function() {
          var dropZone = document.querySelector('#dropzone');
          dropZone.addEventListener('dragenter', function(e) {
            e.stopPropagation();
            e.preventDefault();
          });
          dropZone.addEventListener('dragover', function(e) {
            e.stopPropagation();
            e.preventDefault();
          });
          dropZone.addEventListener('drop', function(e) {
            imgPassed = e.dataTransfer.getData('text/uri-list');
            this.innerHTML = "<img src='" + imgPassed + "'/>";
            e.stopPropagation();
            e.preventDefault();
          });
        });
      </script>
      <style>
        #dropzone {
          height: 200px;
          width: 400px;
          border: 2px ridge;
        }
        ol {
          list-style: none;
          padding: 0;
        }
        ol li {
          display: inline-block;
        }
      </style>
    </head>
    <body>
      <div id="dragzone">
        <img src="img/product1.jpg" alt="ball" />
        <img src="img/product2.jpg" alt="animal" draggable="false" />
        <img src="img/product3.jpg" alt="rope" />
      </div>
      <div id="dropzone">
        ...drop item here...
      </div>
    </body>
    </html>
    

Images desktop drag in

  • Example images dragging from your desktop, Javascript:
  • window.addEventListener('load', function() {
      // drop area
      var dropzone = document.getElementById('dropzone');
    
      // thumbnails area
      var thumbnails = document.getElementById('thumbnails');
    
      // dropzone events
      dropzone.addEventListener('dragenter', function(e) {
        e.stopPropagation();
        e.preventDefault();
      });
      dropzone.addEventListener('dragover', function(e) {
        e.stopPropagation();
        e.preventDefault();
      });
      dropzone.addEventListener('dragleave', function(e) {
        e.stopPropagation();
        e.preventDefault();
      });
      dropzone.addEventListener('drop', function(e) {
        // fetch and iterate dropped files
        var files = e.dataTransfer.files;
        for (var i = 0; i < files.length; i++) {
          // get next file
          file = files[i];
    
          // images only
          if (!file.type.match(/image.*/)) continue;
    
          // create reader for dropped file
          var reader = new FileReader();
          reader.addEventListener('load', function(e) {
            thumbnails.innerHTML = thumbnails.innerHTML
              + ('<img src="' + e.target.result + '" alt="' + file.name + '" />');
          });
          reader.addEventListener('error', function(e) {
            alert('an error occurred');
          });
    
          // feed dropped file to reader
          reader.readAsDataURL(file);
        }
        e.stopPropagation();
        e.preventDefault();
      });
    
    });
    

Files desktop drag out (1)

  • Example dragging files to your desktop, HTML and CSS:
  • <style>
      .dragout {
        background: #999;
        color: #fff;
        text-decoration: none;
        padding: 2px 5px;
        margin: 10px;
        border-radius: 5px;
        font-size: 12px;
      }
    </style>
    ...
      Drag each of these files onto your desktop:
      <a class="dragout" href="http://static....es.pdf" data-downloadurl
      ="application/pdf:Chrome3DGlasses.pdf:http://static...es.pdf">PDF</a>
      <a class="dragout" href="media/star.mp3" data-downloadurl
      ="audio/mpeg:star.mp3:http://sli...star.mp3">MP3</a>
    

Files desktop drag out (2)

  • Example dragging files to your desktop, Javascript:
  • window.addEventListener('load', function() {
      // files ready for dragging
      var files = document.querySelectorAll('.dragout');
    
      // create drag events
      for (var i = 0; i < files.length; i++) {
        files[i].addEventListener('dragstart', function(e) {
          e.dataTransfer.setData('DownloadURL', this.getAttribute('data-downloadurl'));
        });
      }
    });
    

Webcam (1)

  • Access the webcam:
  • <!DOCTYPE html>
    <html lang="en">
    <head>
      <title>Webcam demo</title>
      <meta charset="UTF-8" />
      <style>
        #webcamstream {
          width: 640px;
          height: 480px;
        }
      </style>
    </head>
    <body>
      <video id="webcamstream" autoplay></video>
      <script>
        // get video element
        let video = document.getElementById('webcamstream');
    
        // open webcam and stream to video element
        navigator.mediaDevices.getUserMedia({
          video: true
        }).then(function(stream) {
          video.srcObject = stream;
        }).catch(function(err) {
          console.log(err);
        });
      </script>
    </body>
    </html>
    

Webcam (2)

  • Photobooth fun with webcam and CSS filters:
  • <!DOCTYPE html>
    <html lang="en">
    <head>
      <title>Photobooth demo</title>
      <meta charset="UTF-8" />
      <style>
        /* wrapper */
        #wrapper {
          position: relative;
          width: 640px;
          margin: 20px auto
        }
    
        /* video */
        #webcamstream {
          width: 640px;
          height: 480px;
        }
    
        /* canvases */
        canvas {
          width: 133px;
          height: 100px;
          margin: 10px 0;
          border: 1px solid #ccc;
        }
        canvas + canvas {
          margin-left: 29px;
        }
    
        /* buttons */
        button {
          border-radius: 4px;
          border: 1px solid #2a6496;
          background-color: #428bca;
          text-transform: uppercase;
          padding: 3px 6px;
          color: white;
          cursor: pointer;
        }
        #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(180deg);
        }
      </style>
    </head>
    <body>
      <div id="wrapper">
        <video id="webcamstream" autoplay></video>
        <div id="filters">
          <button data-filter="natural">natural</button>
          <button data-filter="sepia">sepia</button>
          <button data-filter="blur">blur</button>
          <button data-filter="grayscale">grayscale</button>
          <button data-filter="invert">invert</button>
          <button data-filter="hue-rotate">hue-rotate</button>
        </div>
        <button id="dosnap">take snapshot</button>
        <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>
    
        // shorthands
        let video = document.getElementById('webcamstream');
        let canvases = document.querySelectorAll('canvas');
        let nr = 0;
    
        // capture webcam
        navigator.mediaDevices.getUserMedia({
          video: true
        }).then(function(stream) {
          video.srcObject = stream;
        }).catch(function(err) {
          console.log(err);
        });
    
        // filters
        document.getElementById('filters').addEventListener('click', function(e) {
          video.classList.remove('natural', 'sepia', 'blur', 'grayscale', 'invert', 'hue-rotate');
          video.classList.add(e.target.getAttribute('data-filter'));
        });
    
        // take snapshot
        document.getElementById('dosnap').addEventListener('click', function() {
          let context = canvases[nr].getContext('2d');
          context.filter = getComputedStyle(video).getPropertyValue('filter');
          context.drawImage(video, 0, 0, 133, 100);
          nr = (nr + 1) % 4;
        });
    
      </script>
    </body>
    </html>
    

Geolocation API

  • Demo:
  • <!DOCTYPE html>
    <html lang="en">
    <head>
      <title>Geolocation demo</title>
      <meta charset="utf-8" />
      <script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>
      <style>
        #content {
          height: 300px;
          width: 500px;
        }
      </style>
    </head>
    <body>
      <button id="btn1">Show Position</button>
      <div id="content" style="height: 280px"></div>
      <script>
    
        // function to create map with position shown
        let createMap = function(position) {
          let myLatlng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
          let mapOptions = {
            zoom: 13,
            center: myLatlng,
            mapTypeId: google.maps.MapTypeId.ROADMAP
          };
          let map = new google.maps.Map(document.getElementById('content'), mapOptions);
          let marker = new google.maps.Marker({
              position: myLatlng,
              map: map,
              title: 'You are here'
          });
        }
    
        // if button is clicked, get position and create map
        document.getElementById('btn1').addEventListener('click', function() {
          if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(createMap);
          }
        });
    
      </script>
    </body>
    </html>

Motion Control API

  • Use device sensors like gyroscope, accelerometer, compass etc...; device orientation example:
  • <!DOCTYPE html>
    <html>
    <head>
      <title>Device Orientation Demo</title>
      <meta charset="utf-8" />
      <style>
        .layer {
          position: absolute;
        }
      </style>
    </head>
    <body>
      <img src="img/06_games/chrome-layer-01.png" alt="" class="layer">
      <img src="img/06_games/chrome-layer-02.png" alt="" class="layer">
      <img src="img/06_games/chrome-layer-03.png" alt="" class="layer">
      <img src="img/06_games/chrome-layer-04.png" alt="" class="layer">
      <img src="img/06_games/chrome-layer-05.png" alt="" class="layer">
      <script>
    
        // listen for orientation changes
        window.addEventListener('deviceorientation', function(evt) {
          // shift layers to simulate 3D
          let layers = document.querySelectorAll('.layer');
          for (let i = 0; i < layers.length; i++) {
            let layer = layers[i];
            layer.style.left = Math.round(evt.gamma * i).toString() + 'px';
            layer.style.top = Math.round(evt.beta * i).toString() + 'px';
          }
        });
    
      </script>
    </body>
    </html>
    

Notifications API

  • Little taskbar notification popups (you need to request permission once per site first):
  • <!DOCTYPE html>
    <html lang="en">
    <head>
      <title> Notification demo </title>
      <meta charset="utf-8" />
    </head>
    <body>
      <input type="button" id="lnkNotifyMe" value="Notify Me" />
      <script>
    
        document.getElementById('lnkNotifyMe').addEventListener('click', function() {
          if (Notification.permission == 'denied') return;
          if (Notification.permission == 'granted') {
            let notification = new Notification('Some notification', {body: 'Hi there!', icon: 'img/avatar.jpg' } );
            return;
          }
          Notification.requestPermission(function (permission) {
            if (permission === 'granted') {
              let notification = new Notification('Some notification', {body: 'Hi there!', icon: 'img/avatar.jpg' } );
            }
          });
        });
    
      </script>
    </body>
    </html>

Speech API

  • Quite simple to use, considering the complex technology:
  • <!DOCTYPE html>
    <html lang="en">
    <head>
      <title> Speech demo </title>
      <meta charset="utf-8" />
    </head>
    <body>
      <input type="button" id="lnkStart" value="Start listening" />
      <p><strong>You said:</strong> <em id="echo">nothing yet</em>.</p>
      <script>
    
        document.getElementById('lnkStart').addEventListener('click', function() {
          let recognition = new (webkitSpeechRecognition || SpeechRecognition)();
          recognition.lang = 'en-US';
          recognition.interimResults = false;
          recognition.maxAlternatives = 1;
          recognition.start();
          recognition.onresult = function(event) {
              document.getElementById('echo').innerHTML = event.results[0][0].transcript;
          };
        });
    
      </script>
    </body>
    </html>

Sound API (1)

  • Another very promising emerging technology is the sound API. Features:
    • sound scheduling
    • filters (low pass, notch, peak...)
    • volume, gain, cross-fadings, mixing
    • read and write raw data (which basically means anything is possible)
  • Partial implementations already on Chrome and Firefox
  • Some great online demos:
  • Check out this article on using live audio input from line-in or the microphone (be sure to try the pitch detector)

Sound API (2)

  • A basic demo (note how devices are chained together):
    <!DOCTYPE html>
    <html lang="nl">
    <head>
      <title> Webforms demo </title>
      <meta charset="utf-8" />
      <style>
        input {
          width: 40em;
        }
        label {
          font-family: Calibri;
          width: 5em;
          display: inline-block;
        }
      </style>
    </head>
    <body>
      <p><label for="rngFrequency">Frequency:</label> <input type="range" min="200" value="1100" max="2000" id="rngFrequency" /></p>
      <p><label for="rngVolume">Volume:</label> <input type="range" min="0" value="0.5" max="1" id="rngVolume" step="0.01" /></p>
      <script>
        // create audio context
        let audioContext = new AudioContext();
    
        // create oscillator
        let oscillator = audioContext.createOscillator();
        oscillator.frequency.value = 1000;
    
        // create amp
        let 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 sliders
        document.getElementById('rngFrequency').addEventListener('input', function() {
          oscillator.frequency.value = this.value;
        });
        document.getElementById('rngVolume').addEventListener('input', function() {
          amp.gain.value = this.value;
        });
    
      </script>
    </body>
    </html>
    

Sound API (3)

  • You can also simply load and play audio files:
    let song = new Audio('song.mp3');
    song.play(); // other methods: stop(), pause() etc...
    Example:
    <!DOCTYPE html>
    <html lang="nl">
    <body>
      <label>notes: <input type="text" value="A Cs D F A" id="inpSong"></label>
      <input type="button" id="btnPlay" value="play">
      <script>
    
        // play note i
        let playNote = function(src, delay) {
          // create audio
          let audio = new Audio(src);
          setTimeout(function() {
            audio.play();
          }, delay);
        }
    
        // add event listener to button
        document.getElementById('btnPlay').addEventListener('click', function() {
          // get input values
          let notes = document.getElementById('inpSong').value.trim().split(' ');
    
          // loop notes
          for (let i = 0; i < notes.length; i++) {
            playNote('sounds/violin-' + notes[i] + '.mp3', 1000 + 1000 * i);
          }
        });
    
      </script>
    </body>
    </html>
    
    

Canvas (1)

  • Enables bitmapped-style drawing
  • A basic demo:
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <title> HTML5 testpage </title>
      <meta charset="utf-8" />
      <script>
        window.addEventListener('load', function() {
          let canvasContext = document.getElementById("canvas").getContext("2d");
          canvasContext.fillRect(250, 25, 150, 100);
          canvasContext.beginPath();
          canvasContext.arc(450, 110, 100, Math.PI * 1/8, Math.PI * 3/2);
          canvasContext.lineWidth = 15;
          canvasContext.lineCap = 'round';
          canvasContext.strokeStyle = 'rgba(255, 127, 0, 0.5)';
          canvasContext.stroke();
        });
      </script>
    </head>
    <body>
      <canvas id="canvas" width="838" height="220"></canvas>
    </body>
    </html>
    

Canvas (2)

WebGL

More API's

  • Expect these API's to appear soon:
    • media capture API
    • battery status API
    • vibration API
    • contacts API
    • application cache API
    • web intents API
    • ...
  • → time to dust off your RSS reader!

JAVASCRIPT GAMES

CSS Goodies

Transforms

  • Possible transforms:
    • scaling
    • rotation
    • translation
    • skew/perspective
    • full 4x4 matrix transformation
  • transform: rotate(-5deg) scaleY(0.5);
    
    TRANSFORMS EXAMPLE

Transitions

  • Ease any CSS property change:
  • .container .box {
      transition: all 2s ease-in-out;
      margin-left: 0;
      border-radius: 0px;
      background-color: #ccc;
    }
    .container:hover .box {
      margin-left: 500px;
      border-radius: 30px;
      background-color: #c00;
    }
    
    hover me!

Animations

  • Define keyframes, properties and easing:
  • <!DOCTYPE html>
    <head>
      <meta charset="utf-8" />
      <title>Animation</title>
      <style>
        #scene {
          width: 800px;
          border: 2px solid #999;
          margin: 20px auto;
          height: 200px;
          overflow: hidden;
        }
        #busBig {
          background-image: url(img/busBig.png);
          width: 247px;
          height: 106px;
          margin-left: -300px;
          margin-top: 50px;
          animation: moveBus 3s 1 ease-in-out;
        }
        @keyframes moveBus {
          0% { margin-left: -300px; }
          50% { margin-left: 70px; }
          100% { margin-left: 2000px; }
        }
      </style>
    </head>
    <body>
      <div id="scene">
        <div id="busBig" class="layer"></div>
      </div>
    </body>
    </html>

Transforms and animations

  • Transforms and animations combined:
  • <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Morphing Power Cubes</title>
      <style>
        body {
          background-color: black;
          background-image: -webkit-gradient(radial, 50% 500, 1, 50% 500, 400,
            from(rgba(255, 255, 255, 0.3)),
            to(rgba(255, 255, 255, 0)));
        }
        #container {
          width: 100%;
          height: 700px;
          -webkit-perspective: 800; /* For compatibility with iPhone 3.0, we leave off the units here */
          -webkit-perspective-origin: 50% 0;
        }
        #stage {
          width: 100%;
          height: 100%;
          transition: transform 2s;
          transform-style: preserve-3d;
        }
        #cube {
          position: relative;
          top: 160px;
          margin: 0 auto;
          height: 200px;
          width: 200px;
          transform-style: preserve-3d;
        }
        .plane {
          position: absolute;
          height: 200px;
          width: 200px;
          border: 1px solid white;
          border-radius: 12px;
          text-align: center;
          font-size: 124pt;
          background-color: rgba(255, 255, 255, 0.6);
          backface-visibility: visible;
          opacity: 0.5;
        }
        #cube.animate {
          animation: spin 8s infinite linear;
        }
        @keyframes spin {
          from { transform: rotateY(0); }
          to   { transform: rotateY(-360deg); }
        }
        .one {
          transform: scale3d(1.2, 1.2, 1.2) rotateX(90deg) translateZ(100px);
        }
        .two {
          transform: scale3d(1.2, 1.2, 1.2) translateZ(100px);
        }
        .three {
          transform: scale3d(1.2, 1.2, 1.2) rotateY(90deg) translateZ(100px);
        }
        .four {
          transform: scale3d(1.2, 1.2, 1.2) rotateY(180deg) translateZ(100px);
        }
        .five {
          transform: scale3d(1.2, 1.2, 1.2) rotateY(-90deg) translateZ(100px);
        }
        .six {
          transform: scale3d(1.2, 1.2, 1.2) rotateX(-90deg) translateZ(100px) rotate(180deg);
        }
      </style>
    </head>
    <body>
      <div id="container">
        <div id="stage">
          <div id="cube" class="animate">
            <div class="plane one">1</div>
            <div class="plane two">2</div>
            <div class="plane three">3</div>
            <div class="plane four">4</div>
            <div class="plane five">5</div>
            <div class="plane six">6</div>
          </div>
        </div>
        <div id="controls">
          <p><input type="range" id="slider" min="0.1" max="20" step="0.1"></p>
          <p><button id="btnStartStop">without animation</button></p>
        </div>
      </div>
      <script type="text/javascript">
        document.getElementById('slider').addEventListener('input', function() {
          document.getElementById('cube').style.animationDuration = this.value + 's';
        });
        document.getElementById('btnStartStop').addEventListener('click', function() {
          if (this.innerHTML == 'without animation') {
            document.getElementById('cube').classList.remove('animate');
            this.innerHTML = 'with animation';
          } else {
            document.getElementById('cube').classList.add('animate');
            this.innerHTML = 'without animation';
          }
        });
      </script>
    </body>
    </html>
    
  • also check this pure HTML/CSS stopwatch demo; read about interactive email with CSS; more CSS experiments by Mark Robbins

Webfonts

  • Google has a great collection of free fonts you can just link and use. Just select your font, and include a single line in your <head>:
    <html>
    <head>
      <!-- copy-paste the Google Font link here -->
      <link href="https://fonts.googleapis.com/css?family=Spicy+Rice" rel="stylesheet">
      <style>
        p {
          font-family: 'Spicy Rice', sans-serif; /* use your new font here */
          font-size: 30px;
          line-height: 1.8;
        }
      </style>
    </head>
    <body>
      <p>Wie werkten meestal men menigte bersawa. Om monopolies ad nu mislukking interesten verscholen smeltovens. Brandhout mee snelleren geschiedt bezorgden aandeelen den are. Dat treffen gomboom zekeren tot fortuin gelaten stellen. Het ziet niet lage deze het per zes ipoh.</p>
    </body>
    </html>
    

Variable fonts (1)

  • Classic fonts can be customized by size and also have a few weights:
    <!DOCTYPE html>
    <!-- see https://www.axis-praxis.org/specimens/decovar -->
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Lato</title>
      <link href="https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900" rel="stylesheet">
      <style>
        #container {
          display: flex;
          justify-content: flex-start;
        }
        #controls {
          width: 250px;
        }
        #text {
          font-family: Lato;
          font-size: 30px;
          font-weight: 300;
          margin: 0;
          margin-left: 20px;
          width: calc(100% - 270px);
        }
      </style>
    </head>
    <body>
      <div id="container">
        <div id="controls">
          <p>Color: <input type="color" id="color" value="#000000"></p>
          <p>Size: <input type="range" id="size" min="10" max="180" value="30" step="1"></p>
          <p>Weight: <input type="range" id="weight" min="100" max="900" step="100" value="300"></p>
        </div>
        <p id="text">In no impression assistance contrasted. Manners she wishing justice hastily new anxious. At discovery discourse departure objection we.</p>
      </div>
      <script>
        // aliases
        let color = document.getElementById('color');
        let size = document.getElementById('size');
        let weight = document.getElementById('weight');
    
        // apply all settings
        let applyAll = function() {
          text.style.color = color.value;
          text.style.fontSize = size.value + 'px';
          text.style.fontWeight = weight.value;
        }
    
        // attach events
        color.addEventListener('change', applyAll);
        size.addEventListener('input', applyAll);
        weight.addEventListener('input', applyAll);
      </script>
    </body>
    </html>
    

Variable fonts (2)

  • Variable fonts have much more tweaking possibilities, e.g. the beautiful Decovar font:
    <!DOCTYPE html>
    <!-- see https://www.axis-praxis.org/specimens/decovar -->
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Variable fonts</title>
      <style>
        @font-face {
          font-family: Decovar;
          src: url(fonts/DecovarAlpha-VF.ttf) format("truetype");
        }
        #container {
          display: flex;
          justify-content: flex-start;
        }
        #controls {
          width: 250px;
        }
        #text {
          font-family: Decovar;
          font-size: 30px;
          margin: 0;
          margin-left: 20px;
          width: calc(100% - 270px);
        }
      </style>
    </head>
    <body>
      <div id="container">
        <div id="controls">
          <p>Color: <input type="color" id="color" value="#000000"></p>
          <p>Size: <input type="range" id="size" min="10" max="180" value="30" step="1"></p>
          <p>Setting 1: <input type="range" id="setting1" min="0" max="1000" step="1" value="0"></p>
          <p>Setting 2: <input type="range" id="setting2" min="0" max="1000" step="1" value="0"></p>
          <p>Setting 3: <input type="range" id="setting3" min="0" max="1000" step="1" value="0"></p>
          <p>more settings see <a href="https://www.axis-praxis.org/specimens/decovar">this page</a></p>
        </div>
        <p id="text">In no impression assistance contrasted. Manners she wishing justice hastily new anxious. At discovery discourse departure objection we.</p>
      </div>
      <script>
        // aliases
        let color = document.getElementById('color');
        let size = document.getElementById('size');
        let setting1 = document.getElementById('setting1');
        let setting2 = document.getElementById('setting2');
        let setting3 = document.getElementById('setting3');
        let text = document.getElementById('text');
    
        // apply all settings
        let applyAll = function() {
          text.style.color = color.value;
          text.style.fontSize = size.value + 'px';
          text.style.fontVariationSettings = "'WMX2' " + setting1.value + ", 'TRMG' " + setting2.value + ", 'BLDA' " + setting3.value;
        }
    
        // attach events
        color.addEventListener('change', applyAll);
        size.addEventListener('input', applyAll);
        setting1.addEventListener('input', applyAll);
        setting2.addEventListener('input', applyAll);
        setting3.addEventListener('input', applyAll);
      </script>
    </body>
    </html>

Variable fonts (3)

  • Don't think of fonts as text only; it can contain any glyph, also icons like Zycon:
    <!DOCTYPE html>
    <!-- see https://v-fonts.com/fonts/zycon -->
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Variable fonts &mdahs; Zycon</title>
      <style>
        @font-face {
          font-family: Zycon;
          src: url(fonts/Zycon.woff2);
        }
        #container {
          display: flex;
          justify-content: flex-start;
        }
        #controls {
          width: 250px;
        }
        #text {
          font-family: Zycon;
          font-size: 80px;
          margin: 0;
          margin-left: 20px;
        }
      </style>
    </head>
    <body>
      <div id="container">
        <div id="controls">
          <p>Color: <input type="color" id="color" value="#000000"></p>
          <p>Size: <input type="range" id="size" min="10" max="180" value="80" step="1"></p>
          <p>T1: <input type="range" id="setting1" min="0" max="1" value="0" step="0.01" value="0"></p>
          <p>T2: <input type="range" id="setting2" min="0" max="1" value="0" step="0.01" value="0"></p>
          <p>T3: <input type="range" id="setting3" min="0" max="1" value="0" step="0.01" value="0"></p>
          <p>T4: <input type="range" id="setting4" min="0" max="1" value="0" step="0.01" value="0"></p>
          <p>M1: <input type="range" id="setting5" min="0" max="1" value="0" step="0.01" value="0"></p>
          <p>M2: <input type="range" id="setting6" min="0" max="1" value="0" step="0.01" value="0"></p>
        </div>
        <p id="text">☀✯➟⬤🌝🏵🐈🐕🐢💡🔒🕛🖐🚴🦉🦎</p>
      </div>
      <script>
        // aliases
        let color = document.getElementById('color');
        let size = document.getElementById('size');
        let setting1 = document.getElementById('setting1');
        let setting2 = document.getElementById('setting2');
        let setting3 = document.getElementById('setting3');
        let setting4 = document.getElementById('setting4');
        let setting5 = document.getElementById('setting5');
        let setting6 = document.getElementById('setting6');
        let text = document.getElementById('text');
    
        // apply all settings
        let applyAll = function() {
          text.style.color = color.value;
          text.style.fontSize = size.value + 'px';
              text.style.fontVariationSettings = "'T1  ' " + setting1.value + ",'T2  ' " + setting2.value + ",'T3  ' " + setting3.value + ",'T4  ' " + setting4.value + ",'M1  ' " + setting5.value + ",'M2  ' " + setting6.value + "";
        }
    
        // attach events
        color.addEventListener('change', applyAll);
        size.addEventListener('input', applyAll);
        setting1.addEventListener('input', applyAll);
        setting2.addEventListener('input', applyAll);
        setting3.addEventListener('input', applyAll);
        setting4.addEventListener('input', applyAll);
        setting5.addEventListener('input', applyAll);
        setting6.addEventListener('input', applyAll);
      </script>
    </body>
    </html>
    

Variable fonts (4)

  • You can combine these with any CSS technique, e.g. animation:
    <!DOCTYPE html>
    <!-- see https://v-fonts.com/fonts/zycon -->
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Variable fonts &mdahs; Zycon</title>
      <style>
        @font-face {
          font-family: Zycon;
          src: url(demos/fonts/Zycon.woff2);
        }
        #text {
          font-family: Zycon;
          font-size: 180px;
          font-variation-settings: "T1  " 0, "T2  " 0, "T3  " 0, "T4  " 0, "M1  " 0, "M2  " 0;
          animation: variation 0.3s linear infinite;
        }
        @keyframes variation {
          0% {
            font-variation-settings: "T1  " 0, "T2  " 0, "T3  " 0, "T4  " 0, "M1  " 0, "M2  " 0;
          }
          100% {
            font-variation-settings: "T1  " 0, "T2  " 0, "T3  " 0, "T4  " 0, "M1  " 1, "M2  " 0;
          }
        }
    
      </style>
    </head>
    <body>
      <p id="text">🚴</p>
    </body>
    </html>

CSS Blend modes

  • Fairly new technology with variable browser support.
  • background-image: url(demos/06_games/img/kaynewest.jpg);
    background-color: #add8e6; /* lightblue */
    background-blend-mode: multiply; /* also darken, lighten, dodge etc... */
    background-size: cover;
    

SVG filters (1)

  • SVG can also used to create filters:
    <!DOCTYPE html>
    <!-- see https://yoksel.github.io/svg-filters/#/ for more filter fun -->
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>SVG filter</title>
      <style>
        #van {
          -webkit-filter: url(#filter1);
          filter: url(#filter1);
        }
      </style>
    </head>
    <body>
      <svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="filters">
        <defs>
          <filter id="filter1">
            <feGaussianBlur in="SourceGraphic" stdDeviation="20,0" />
          </filter>
        </defs>
      </svg>
      <img src="demos/06_games/img/van.jpg" alt="van" id="van">
    </body>
    </html>
    

SVG filters (2)

  • Another SVG filter example, conversion of any image to duotone:
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>filter with SVG</title>
      <style>
        html, body {
          height: 100%;
          margin: 0;
          overflow: hidden;
        }
        body {
          position: relative;
        }
        #controls {
          position: absolute;
          left: 10px;
          top: 10px;
          border: 2px ridge;
        }
        #duotone {
          width: 100%;
          height: 100%;
          background-position: center;
          background-repeat:no-repeat;
          background-size: cover;
        }
        svg {
            color-interpolation-filters:sRGB;
        }
      </style>
    </head>
    <body>
      <svg width="1024" height="640" viewBox="0 0 1024 640" id="duotone"
        preserveAspectRatio="xMidYMid slice">
        <defs>
          <filter id="duotone-filter">
            <feColorMatrix
              type="matrix"
              values="1  0  0  0  0
                      0  1  0  0  0
                      0  0  1  0  0
                      0  0  0  1  0"/>
          </filter>
        </defs>
        <image width="1024" height="640" filter="url(#duotone-filter)"
          xlink:href="img/kanye-west.jpg"/>
      </svg>
      <div id="controls">
        <input type="color" id="color1" value="#8a702d" />
        <input type="color" id="color2" value="#19264f" />
      </div>
      <script>
        // converts #hex to array of rgb values
        function hexToRgb(hex) {
          var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
          return result ? [
            parseInt(result[1], 16), // R
            parseInt(result[2], 16), // G
            parseInt(result[3], 16) // B
          ] : null;
        }
    
        // calculates filter matrix for two colors
        function convertToDueTone(color1, color2) {
          var matrix = document.querySelector('feColorMatrix');
          var value = [
            [color1[0]/256 - color2[0]/256, 0, 0, 0, color2[0]/256],
            [color1[1]/256 - color2[1]/256, 0, 0, 0, color2[1]/256],
            [color1[2]/256 - color2[2]/256, 0, 0, 0, color2[2]/256],
            [0, 0, 0, 1, 0]
          ];
          matrix.setAttribute('values', value.join(' '));
        }
    
        // init
        var inpColor1 = document.getElementById('color1');
        var inpColor2 = document.getElementById('color2');
        convertToDueTone(hexToRgb(inpColor1.value), hexToRgb(inpColor2.value));
    
        // bind color picker change events
        document.getElementById('color1').addEventListener('change', function() {
          convertToDueTone(hexToRgb(inpColor1.value), hexToRgb(inpColor2.value));
        })
        document.getElementById('color2').addEventListener('change', function() {
          convertToDueTone(hexToRgb(inpColor1.value), hexToRgb(inpColor2.value));
        })
    
      </script>
    </body>
    </html>
  • SVG's graphic possibilities are almost endless
  • small drawback: SVG support is variable

Inline SVG graphics

  • Crazy text demo:
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!-- Created with Inkscape (http://www.inkscape.org/)
    Created by ed@opera.com, 2007-07-05.
    -->
    <svg
       xmlns:svg="http://www.w3.org/2000/svg"
       xmlns="http://www.w3.org/2000/svg"
       xmlns:xlink="http://www.w3.org/1999/xlink"
       xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
       version="1.0"
       viewBox="0 0 500 500"
       id="svgroot">
      <defs>
        <linearGradient
           id="linearGradient3270">
          <stop
             style="stop-color:#4c0000;stop-opacity:1"
             offset="0"
             id="stop3272" />
          <stop
             style="stop-color:#9b0000;stop-opacity:0.77319586"
             offset="1"
             id="stop3278" />
        </linearGradient>
        <filter
           id="filter3246">
          <feGaussianBlur
             id="feGaussianBlur3248"
             stdDeviation="5.3786588"
             inkscape:collect="always" />
        </filter>
        <radialGradient
           cx="75.110962"
           cy="419.52441"
           r="286.5"
           fx="75.110962"
           fy="419.52441"
           id="radialGradient3276"
           xlink:href="#linearGradient3270"
           gradientUnits="userSpaceOnUse"
           gradientTransform="matrix(0.6032362,1.2076622,-1.5679002,0.7831777,700.96274,-211.40865)"
           spreadMethod="pad" />
      </defs>
      <g>
        <rect
           width="500"
           height="500"
           style="fill:url(#radialGradient3276);stroke-width:30;stroke-linecap:round;"
           id="bg" />
      <g transform="translate(-120 -100)">
        <path
           d="M 362.85715,349.50504 C 358.12981,356.81092 350.67666,346.73714 350.71429,341.64789 C 350.81627,327.85633 367.22572,322.12645 378.57144,325.21933 C 398.86627,330.75175 406.26373,354.82622 399.28571,373.07648 C 389.04518,399.85947 356.42337,409.13291 331.42856,398.07645 C 298.1144,383.33994 286.92857,341.90419 302.14288,310.2193 C 321.28853,270.3471 371.65046,257.23526 410.00003,276.64791 C 456.4465,300.15923 471.49146,359.50501 447.85713,404.50506 C 420.00489,457.53607 351.64169,474.51851 299.99997,446.64787 C 240.37757,414.47011 221.45475,337.06819 253.57146,278.79071 C 290.06451,212.57214 376.51952,191.70688 441.42862,228.07649 C 514.24681,268.8777 537.05603,364.39597 496.42854,435.93365 C 451.32433,515.35404 346.73537,540.10836 268.57138,495.21929"
           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:30;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3246)"
           id="path2160" />
        <path
           d="M 362.85715,349.50504 C 358.12981,356.81092 350.67666,346.73714 350.71429,341.64789 C 350.81627,327.85633 367.22572,322.12645 378.57144,325.21933 C 398.86627,330.75175 406.26373,354.82622 399.28571,373.07648 C 389.04518,399.85947 356.42337,409.13291 331.42856,398.07645 C 298.1144,383.33994 286.92857,341.90419 302.14288,310.2193 C 321.28853,270.3471 371.65046,257.23526 410.00003,276.64791 C 456.4465,300.15923 471.49146,359.50501 447.85713,404.50506 C 420.00489,457.53607 351.64169,474.51851 299.99997,446.64787 C 240.37757,414.47011 221.45475,337.06819 253.57146,278.79071 C 290.06451,212.57214 376.51952,191.70688 441.42862,228.07649 C 514.24681,268.8777 537.05603,364.39597 496.42854,435.93365 C 451.32433,515.35404 346.73537,540.10836 268.57138,495.21929"
           transform="translate(0.758,-1.533235)"
           style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
           id="path3287" />
        <text editable="simple" dy="5"
           style="font-size:12px;font-family:Bitstream Vera Sans,sans-serif">
         <textPath xlink:href="#path3287">
        Hello spiral text field Hello spiral text field
        Hello spiral text field Hello spiral text field
        Hello spiral text field Hello spiral text field
        Hello spiral text field Hello spiral text field
        </textPath>
        </text>
      </g>
      </g>
    </svg>
    

JAVASCRIPT GAMES

2D sprites and animations

What are animations

  • An animation is basically a sequence of still images, creating the illusion of a fluid motion:
    stop motion – watch online
    zoetrope – watch here or here
    flip book – watch online

Animation definitions

  • Most computergames contain moving objects or sprites, that need animation. Some definitions:
    • scene: the world the game is played in
    • sprite: a object in the scene that can be animated (Super Mario, coin, bullet, bomb...)
    • sprite sheet: an image containing all image versions of the sprite
    • game loop: a piece of code that executes iteratively x times per second
    • frame: a still image of an animation
    • FPS: frames per second)

Loop with setInterval()

  • You could use the vintage Javascript method setInterval() to time your animations. A basic bouncing ball example:
    <html lang="en">
    <head>
      <style>
        #app { width: 400px; height: 200px; position: relative; background-color: #ccc; }
        #ball { width: 50px; height: 50px; position: absolute; left: 10px; top: 100px; }
      </style>
    </head>
    <body>
      <div id="app"><img src="demos/06_games/setinterval/ball.png" id="ball" alt=""></div>
      <script>
        let speedX = 10; let speedY = 5; let speedR = 10;
        let x = 10; let y = 100; let r = 0;
        let 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); // repeat doLoop function every 20ms
      </script>
    </body>
    </html>

requestAnimationFrame()

  • Problem: the FPS in setInterval() is fixed, which could be too fast for your CPU and lead to jerky or incomplete animations.
    function doLoop() {
      // animation logic here
      // ...
    }
    setInterval(doLoop, 20); // repeat doLoop function every 20ms
    
  • Solution: use the new requestAnimationFrame() method, which uses a flexibel interval depending on the complexity of the scene:
    function doLoop() {
      // animation logic here
      // ...
      requestAnimationFrame(doLoop); // call next frame whenever the CPU is ready
    }
    doLoop(); // start the loop

Full example

  • A full example with animation, sound effects etc. (Javascript is listed; inspect the demo source code for HTML and CSS):
    // sprites list
    let sprites = [];
    
    // sound fx
    let sndSplash = new Audio('snd/splash.mp3');
    let sndGun = new Audio('snd/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 object
    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 = 10;
        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
    const renderSprite = function (sprite) {
      // ignore dead sprites
      if (sprite.isDead) return;
    
      //
      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';
    }
    
    // main game loop
    const doLoop = function () {
      for (let sprite of sprites) renderSprite(sprite);
      requestAnimationFrame(doLoop);
    }
    
    // window loaded
    window.addEventListener('load', function() {
      // add button event listeners
      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 game loop
      doLoop();
    
    }); // end window.onload
                    

JAVASCRIPT GAMES

3D ThreeJS

WebGL & threejs

  • WebGL (Web Graphics Library) is a Javascript API for rendering 3D directly in the browser without the need of extra plugins.
  • Browser support is actually excellent, also on mobile devices.
  • WebGL is too basic to work with directly, so we'll use the ThreeJS library for our code.

The term 'Library' in WebGL is a little bit unfortunate, as it suggests it is just an extension, while actually it is the core. Web Graphics API would have been a better name.

WebGL doesn't know basic shapes like balls or cubes, only polygons.

Creating 3D

  • For 3D graphics & animations, we need to define:
    • a scene containing all objects, lights etc...
    • some objects (color, texture, material, shape, position, rotation...)
    • some lights (color, aperture, position, direction...)
    • a camera (position, target...)
    • probably sound effects
  • To render the scene, we need to think of:
    • which objects cast and receive shadows and how
    • reflection properties of objects
  • For animations, we also need:
    • a loop generating successive frames
3D scene

setting up threejs

We basically just need an HTML document, and link our scripts

<!DOCTYPE html>
<html>
<head>
    <title>ThreeJS demo</title>
    <meta charset="utf-8" />
</head>
<body>
    <!-- link to threejs library -->
    <script src="vendor/three.min.js"></script>
    <!-- link to our script -->
    <script src="js/scripts.js"></script>
</body>
</html>

  • from here, we'll continue in scripts.js

scene

The scene is just an empty space which will contain all our objects, lights etc... Let's create one:

// init scene
scene = new THREE.Scene();

objects (1)

In 3D, shapes are defined as a collection of adjacent polygons, called a polygon mesh. Less polygons = faster rendering (games), more polygons = better detail (movies)

objects (2)

To define an object ('mesh'), you need at least a shape ('geometry') and a material. ThreeJS has a few predefined shapes like sphere, cone, box, plane... Let's add a ground and a shape:

// add ground
let groundGeometry = new THREE.PlaneGeometry( 200, 300, 32 );
let groundMaterial = new THREE.MeshPhongMaterial({
    color: 0x666666,
    side: THREE.DoubleSide
});
let ground = new THREE.Mesh( groundGeometry, groundMaterial );
ground.rotation.x = Math.PI / 360 * 110;
scene.add( ground );

// add torus
let shapeGeometry = new THREE.TorusGeometry(30, 10, 12, 24);
let shapeMaterial = new THREE.MeshPhongMaterial({
    color: 0x156289
});
let shape = new THREE.Mesh(shapeGeometry, shapeMaterial);
shape.position.y = 70;
shape.rotation.y = Math.PI / 360 * 120;
scene.add( shape );

camera & renderer

To actually create an image, we need a camera viewpoint, from where we render the scene:

// 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
renderer.render(scene, camera); 

lighting (1)

  • You need lighting to see anything. Most important techniques:
    • ambient: uniform lighting on all surfaces and objects; doesn't cast shadows
    • point: light from a single point, omnidirectional
    • directional: from a single direction, all rays are parallel
    • spot: light from a single point, spreads outwards in a cone, like a spotlight

lighting (2)

We add ambient light and a spotlight:

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

// add spotlight
let spotlight = new THREE.SpotLight(0xFFFFFF, 0.7);
spotlight.position.set(150, 200, -75);
spotlight.penumbra = 0.1;
spotlight.angle = 0.4;
scene.add(spotlight);

shadows & shading

We still need to create shadows, and specify how objects are shaded. Let's add relevant properties:

...
let groundMaterial = new THREE.MeshPhongMaterial({
    ...
    shininess: 15,
    specular: 0x888888,
    shading: THREE.SmoothShading
});
...
ground.castShadow = false;
ground.receiveShadow = true;
...
let shapeMaterial = new THREE.MeshPhongMaterial({
    ...
    shading: THREE.FlatShading,
    shininess: 60,
    specular: 0x156289,
});
...
shape.castShadow = true;
shape.receiveShadow = false;
...
spotlight.castShadow = true;
spotlight.penumbra = 0.1;
...
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
...

animations

To animate a scene, we need to change some properties and re-render x times per second. We'll use the native requestAnimationFrame function to create a loop:

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

    // render the scene
    renderer.render(scene, camera);
}
render();

The requestAnimationFrame function works a little bit like a variable delay: it executes within 1/60th of a second (60 FPS), or later if scenes are heavy to render. This prevents the dropping of frames or parts of frames, ensuring a smooth rendering.

texture

Adding texture to objects is easy, e.g. the ground floor:

let groundTexture = (new THREE.TextureLoader()).load('img/floor.jpg');
groundTexture.wrapS = THREE.RepeatWrapping;
groundTexture.wrapT = THREE.RepeatWrapping;
groundTexture.repeat.set( 4, 4 );
let groundMaterial = new THREE.MeshPhongMaterial({
  ...
  map: groundTexture
});

To add texture, create an animation first; it might take a few frames before the image is loaded.

complete code listing

Our complete code:

// init scene
let scene = new THREE.Scene();

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

// add shape
let shapeGeometry = new THREE.TorusGeometry(30, 10, 12, 24);
let shapeMaterial = new THREE.MeshPhongMaterial({
    color: 0x156289,
    side: THREE.DoubleSide,
    shading: THREE.FlatShading,
    shininess: 60,
    specular: 0x156289
});
let 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
let ambientlight = new THREE.AmbientLight(0x444444, 2.5);
scene.add(ambientlight);

// add spotlight
let 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
let render = function() {
    // keep looping
    requestAnimationFrame(render);
    shape.rotation.y += 0.03;

    // render the scene
    renderer.render(scene, camera);
}
render();

Odisee logo