JavaScript/Exercises/IntroGraphic

From Wikibooks, open books for an open world
Jump to navigation Jump to search



We offer a lot of examples. The structure of the source code, in general, follows a particular pattern: global definitions / start function / render function / program logic (playTheGame + event handler). We believe that this separation offers an easy understanding of the code, especially the distinction between logic and rendering. But this architecture is just a suggestion; others may also, or even better, suit your needs.

Canvas[edit | edit source]

To develop apps that contain graphical elements, you need to include an area into your HTML where you can 'paint'. HTML elements like button, div, or others contain primarily text, colors, or a (static) image or video.

The HTML element canvas is designed to fulfill this purpose. It is like a board where dots, lines, circles, ... can be drawn.

Click to see solution
<!DOCTYPE html>
<html>
<head>
  <title>Canvas 1</title>
  <script>
  // ..
  </script>
</head>

<body style="padding:1em">

  <h1 style="text-align: center">An HTML &lt;canvas> is an area for drawing</h1>

  <canvas id="canvas" width="700" height="300"
          style="margin-top:1em; background-color:yellow" >
  </canvas>

  <p></p>
  <button id="start" onClick="start()">Start</button>

</body>
</html>

During this introduction, the HTML part is mainly identical to the above one. Sometimes there are some more buttons or explanations. To beautify the page, you may want to add some more CSS definitions.

Draw into a canvas[edit | edit source]

The main work is done in JavaScript. The first example draws two rectangles, a 'path' (a line), and a text.

Click to see solution
  <script>
  // We show only the JavaScript part

  function start() {
    "use strict";

    // make the HTML element 'canvas' available to JS
    const canvas = document.getElementById("canvas");

    // make the 'context' of the canvas available to JS.
    // It offers many functions like 'fillRect', 'lineTo',
    // 'ellipse', ...
    const context = canvas.getContext("2d");

    // demonstrate some functions

    // an empty rectangle
    context.lineWidth = 2;
    context.strokeRect(20, 20, 250, 150);

    // a filled rectangle
    context.fillStyle = "lime";
    context.fillRect(100, 150, 250, 100);

    // a line
    context.beginPath();
    context.moveTo(500, 100);  // no drawing
    context.lineTo(520, 40);
    context.lineTo(550, 150);
    context.stroke();          // drawing

    // some text
    context.fillStyle = "blue";
    context.font = "20px Arial";
    context.fillText("A short line of some text", 400, 250); 
  
  }
  </script>

Exercise: Draw a line like the uppercase character "M" and surround it with a rectangle.

Click to see solution
  <script>
  // We show only the JavaScript part

  function start() {
    "use strict";

    // make the HTML element 'canvas' available to JS
    const canvas = document.getElementById("canvas");

    // make the 'context' of the canvas available to JS.
    // It offers many functions like 'fillRect', 'lineTo',
    // 'ellipse', ...
    const context = canvas.getContext("2d");

    // draw a "M"
    context.lineWidth = 2;
    context.beginPath();
    context.moveTo(190, 180);
    context.lineTo(200, 100);
    context.lineTo(230, 130);
    context.lineTo(260, 100);
    context.lineTo(270, 180);
    context.stroke();

    // an empty rectangle surrounding the "M"
    context.strokeRect(150, 70, 150, 150);
  }
  <script>

Work in an object-oriented way[edit | edit source]

Please work in a structured, object-oriented way and use the classical prototype or the class syntax to define often-used objects. In our examples, we use the prototype syntax for Rect (rectangle) and the class syntax for Circle to give you examples for both. It's a good idea to create separate JS-files for each such object resp. class to separate them from other JS-files, which shall handle other aspects of the games.

We define Rect and Circle with some attributes and functions:

Click to see solution
"use strict";

// use 'classical' prototype-syntax for 'Rect' (as an example)
function Rect(context, x = 0, y = 0, width = 100, height = 100, color = 'green') {
  this.context = context;
  this.x = x;
  this.y = y;
  this.width  = width;
  this.height = height;
  this.color  = color;

  // function to render the rectangle
  this.render = function () {
    context.fillStyle = this.color;
    context.fillRect(this.x, this.y, this.width, this.height);
  }
}

// use class-syntax for 'Circle' (as an example)
class Circle {
  constructor(context, x = 10, y = 10, radius = 10, color = 'blue') {
    this.context = context;
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
  }

  // function to render the circle
  render() {
    this.context.beginPath(); // restart colors and lines
    this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
    this.context.fillStyle = this.color;
    this.context.fill();
  }
}


function start() {
  // provide canvas and context
  const canvas = document.getElementById("canvas");
  const context = canvas.getContext("2d");

  // create a rectangle at a certain position
  const rect_1 = new Rect(context, 100, 100);
  rect_1.render();

  // create a circle at a certain position
  const circle_1 = new Circle(context, 400, 100, 50);
  circle_1.render();
}

Introduction to movements[edit | edit source]

The above example creates a single graphic without any change or movement of its objects. Such graphics must be painted only once. But if any object changes its position, the graphic must be re-painted. This can be done in different ways.

  1. Single movement: Re-painting initiated by an event
  2. Continous movement: Call to function windows.requestAnimationFrame
  3. Continous movement: Call to function windows.setIntervall

Case 1: If - after an event - the new position of an object will not change anymore (its 'speed' is 0), the event can initiate the re-painting directly. Nothing else is necessary.

The situation changes significantly if it is intended that some objects shall move across the screen constantly without further user interaction. That means that they have their own 'speed'. To realize this automatic movement, the re-painting must be done in one way or another by the system. The two functions requestAnimationFrame and setIntervall are designed to handle this part.

Case 2: requestAnimationFrame(() => func()) initiates a single rendering as far as possible. It accepts one parameter, which is the function that should render the complete graphic. It is necessary that the execution of the called function func leads again to the call of requestAnimationFrame(() => func()) as long as the animation shall go on.

Case 3: setIntervall(func, 25) calls in a loop a function (its first parameter) repeatedly after certain milliseconds (its second parameter). The called function shall render the graphic by first deleting all old content and then re-painting everything - in the same way as requestAnimationFrame. Nowadays, requestAnimationFrame is preferred over setIntervall because its timing is more accurate, results are better (e.g., no flickering, smoother movements), and it shows better performance.

Single movements (jumping)[edit | edit source]

Step-by-step with fixed width[edit | edit source]

A figure can be moved across the canvas by clicking on 'left', 'right', 'up', 'down' buttons - or by pressing the arrow keys. Each click creates an event, the event handler is called, he changes the position of the figure by a fixed value, and lastly, the complete scene gets re-drawn. Re-drawing consists of two steps. First, the complete canvas is cleared. Second, all objects of the scene are drawn, regardless of whether they have changed their position or not.

Click to see solution
<!DOCTYPE html>
<html>
<head>

  <!--
  moving a smiley across the canvas; so far without 
  collison detection
  -->

  <title>Move a smiley</title>
  <script>
  "use strict";

  // ---------------------------------------------------------------
  //            class rectangle
  // ----------------------------------------------------------------
  class Rect {
    constructor(context, x = 0, y = 0, width = 10, height = 10, color = "lime") {
      this.context = context;
      this.x = x;
      this.y = y;
      this.width  = width
      this.height = height;
      this.color  = color;
    }

    // methods to move the rectangle
    right() {this.x++}
    left()  {this.x--}
    down()  {this.y++}
    up()    {this.y--}

    render() {
      context.fillStyle = this.color;
      context.fillRect(this.x, this.y, this.width, this.height);
    }
  } // end of class

  class Smiley {
    constructor(context, text, x = 0, y = 0) {
      this.context = context;
      this.text = text;
      this.x = x;
      this.y = y;
    }

    // method to move a smiley
    move(x, y) {
      this.x += x;
      this.y += y;
    }

    render() {
      this.context.font = "30px Arial";
      this.context.fillText(this.text, this.x, this.y);
    }
  }  // end of class

  // ----------------------------------------------
  // variables that are known in the complete file
  // ----------------------------------------------
  let canvas;
  let context;
  let obstacles = [];
  let he, she;

  // --------------------------------------------------
  // functions
  // --------------------------------------------------

  // inititalize all objects, variables, ... of the game
  function start() {
    // provide canvas and context
    canvas = document.getElementById("canvas");
    context = canvas.getContext("2d");

    // create some obstacles
    const obst1 = new Rect(context, 200,  80, 10, 210, "red");
    const obst2 = new Rect(context, 350,  20, 10, 150, "red");
    const obst3 = new Rect(context, 500, 100, 10, 210, "red");
    obstacles.push(obst1);
    obstacles.push(obst2);
    obstacles.push(obst3);

    he  = new Smiley(context, '\u{1F60E}',  20, 260);
    she = new Smiley(context, '\u{1F60D}', 650, 280);

    // show the scene at user's screen
    renderAll();
  }

  // rendering consists of:
  //   - clear the complete scene
  //   - re-paint the complete scene
  function renderAll() {

    // remove every old drawings from the canvas
    context.clearRect(0, 0, canvas.width, canvas.height);

    // show the rectangles
    for (let i = 0; i < obstacles.length; i++) {
      obstacles[i].render();
    }

    // show two smilies
    he.render();
    she.render();
  }

  // event handler to steer smiley's movement
  function leftEvent() {
    he.move(-10, 0);
    renderAll();
  }
  function rightEvent() {
    he.move(10, 0);
    renderAll();
  }
  function upEvent() {
    he.move(0, -10);
    renderAll();
  }
  function downEvent() {
    he.move(0, 10);
    renderAll();
  }
  </script>
</head>

<body style="padding:1em" onload="start()">

  <h1 style="text-align: center">Single movements: step-by-step jumping</h1>
  <h3 style="text-align: center">(without collision detection)</h3>

  <canvas id="canvas" width="700" height="300"
          style="margin-top:1em; background-color:yellow" >
  </canvas>

  <div style="margin-top: 1em">
    <button onClick="leftEvent()">Left</button>
    <button onClick="upEvent()">Up</button>
    <button onClick="downEvent()">Down</button>
    <button onClick="rightEvent()">Right</button>
    <button style="margin-left: 2em" onClick="start()">Reset</button>
  <div>

</body>
</html>

Jump to a particular position[edit | edit source]

The following example adds an event handler canvasClicked to the canvas. Among others, the event contains the mouse position.

If you want to move an object to the position to which the mouse is pointing, it's necessary to know where the click-event has occurred. You have access to this information by evaluating the details of the parameter 'event' of the event handler. The event.offsetX/Y properties show those coordinates. They are used in combination with the additional jumpTo method of circle to move the circle.

Click to see solution
<!DOCTYPE html>
<html>
<!-- A ball is jumping to positions where the user has clicked to  -->
<head>
  <title>Jumping Ball</title>
  <script>
  "use strict";

  // --------------------------------------------------------------
  // class 'Circle'
  // --------------------------------------------------------------
  class Circle {
    constructor(context, x = 10, y = 10, radius = 10, color = 'blue') {
      this.context = context;
      this.x = x;
      this.y = y;
      this.radius = radius;
      this.color = color;
    }

    // method to render the circle
    render() {
      this.context.beginPath(); // restart colors and lines
      this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
      this.context.fillStyle = this.color;
      this.context.fill();
    }

    // method to jump to a certain position
    jumpTo(x, y) {
      this.x = x;
      this.y = y;
    }

  } // end of class

  // ----------------------------------------------
  // variables that are known in the complete file
  // ----------------------------------------------
  let ball;
  let canvas;
  let context;


  // --------------------------------------------------
  // functions
  // --------------------------------------------------

  // inititalize all objects, variables, .. of the game
  function start() {
    // provide canvas and context
    canvas = document.getElementById("canvas");
    context = canvas.getContext("2d");

    // create a ball (class circle) at a certain position
    ball = new Circle(context, 400, 100, 50);
    renderAll();
  }

  // rendering consists of:
  //   - clear the complete scene
  //   - re-paint the complete scene
  function renderAll() {

    // remove every old drawings from the canvas
    context.clearRect(0, 0, canvas.width, canvas.height);
    ball.render();
  }

  // event handling
  function canvasClicked(event) {
    ball.jumpTo(event.offsetX, event.offsetY);
    renderAll();
  }
  </script>
</head>

<body style="padding:1em" onload="start()">

  <h1 style="text-align: center">Click to the colored area</h1>

  <canvas id="canvas" width="700" height="300" onclick="canvasClicked(event)"
          style="margin-top:1em; background-color:yellow" >
  </canvas>

</body>
</html>

Summary[edit | edit source]

Such applications show a common code structure.

  • Classes and prototype functions are declared inclusive of their methods.
  • Variables are declared.
  • A 'start' or 'init' function creates all necessary objects. As its last step, it calls the function that renders the complete scene.
  • The rendering function clears the complete scene and draws all visual objects (again).
  • Event handlers that react on buttons or other events change the position of visual objects. They do not render them. As their last step, they call the function that renders the complete scene.
The event handlers implement some kind of 'business logic'. Therefore they differ significantly from program to program. The other parts have a more standardized and static behavior.

Continous movements[edit | edit source]

Working with continuously moving objects is similar to the above-shown stepwise movements. The distinction is that after the user action, the object does not only move to a different position. Instead, the movement goes on. The object now has a 'speed'. The realization of the speed must be done in one way or another by the software. The two functions requestAnimationFrame and setIntervall are designed to handle this part.

Since requestAnimationFrame is widely available in browsers, it is favored over the traditional setIntervall. Its timing is more accurate, results are better (e.g., no flickering, smoother movements), and it shows better performance.

requestAnimationFrame[edit | edit source]

In the following program, a smiley is moved across the screen at a constant speed. The program's overall structure is similar to the above-shown solutions. Once initiated, the movement keeps going on without further user interaction.

  • Classes and prototype functions are declared inclusive of their methods.
  • Variables are declared.
  • A 'start' or 'init' function creates all necessary objects. As its last step, it calls the function that renders the complete scene.
  • The rendering function - renderAll in our case - clears the complete scene and (re)renders all visual objects.
  • At the end of the rendering function, the crucial distinction to the above programs is implemented: it calls requestAnimationFrame(() => func()). That initiates a single transfer of the rendered objects from RAM to the physical screen as far as possible.
And it accepts one parameter, which is the function that executes the game's logic (in combination with events), playTheGame in our case. It is necessary that the execution of the called function again leads to the call of requestAnimationFrame(() => func()) as long as the animation shall go on.
  • Event handlers that react on buttons or other events change the position or speed of visual objects. They do not render them. Also, it's not necessary that they call the function that is responsible for the rendering; the rendering is always going on due to the previous two steps.
  • One of those events is stopEvent. It sets a boolean variable that indicates that the game shall stop. This variable is evaluated in renderAll. If it is set to true, the system function cancelAnimationFrame is called instead of requestAnimationFrame to terminate the loop of animations - and the smiley's movement.
Click to see solution
<!DOCTYPE html>
<html>
<head>

  <!--
  'requestAnimationFrame()' version of moving a smiley across
  the canvas with a fixed speed
  -->

  <title>Move a smiley</title>
  <script>
  "use strict";

  // ---------------------------------------------------------------
  //            class Smiley
  // ----------------------------------------------------------------
  class Smiley {
    constructor(context, text, x = 0, y = 0) {
      this.context = context;
      this.text = text;
      this.x = x;
      this.y = y;
    }

    // change the text (smiley's look)
    setText(text) {
      this.text = text;
    }

    // methods to move a smiley
    move(x, y) {
      this.x += x;
      this.y += y;
    }
    moveTo(x, y) {
      this.x = x;
      this.y = y;
    }

    render() {
      this.context.font = "30px Arial";
      this.context.fillText(this.text, this.x, this.y);
    }
  }  // end of class

  // ----------------------------------------------
  // variables that are known in the complete file
  // ----------------------------------------------
  let canvas;
  let context;
  let smiley;
  let stop;
  let frameId;

  // use different smileys
  let smileyText = ['\u{1F60E}', '\u{1F9B8}',
                    '\u{1F9DA}', '\u{1F9DF}', '\u{1F47E}'];
  let smileyTextCnt = 0;

  // --------------------------------------------------
  // functions
  // --------------------------------------------------

  // inititalize all objects, variables, ... of the game
  function start() {
    // provide canvas and context
    canvas = document.getElementById("canvas");
    context = canvas.getContext("2d");

    smiley = new Smiley(context, smileyText[smileyTextCnt], 20, 100);
    smileyTextCnt++;
    stop = false;

    // show the scene on user's screen
    renderAll();
  }

  // rendering consists of:
  //   - clear the complete scene
  //   - re-paint the complete scene
  //   - call the game's logic again via requestAnimationFrame()
  function renderAll() {

    // remove every old drawings from the canvas
    context.clearRect(0, 0, canvas.width, canvas.height);

    // show the smiley
    smiley.render();

    // re-start the game's logic, which lastly leads to 
    // a rendering of the canvas
    if (stop) {
      // interrupt animation, if the flag is set
      window.cancelAnimationFrame(frameId);
    } else {
      // repeat animation
      frameId = window.requestAnimationFrame(() => playTheGame(canvas, context)); 
    }
  }

  // the game's logic
  function playTheGame(canvas, context) {

    // here, we use a very simple logic: move the smiley
    // across the canvas towards right
    if (smiley.x > canvas.width) {  // outside of right border
      smiley.moveTo(0, smiley.y);   // re-start at the left border
      smiley.text = smileyText[smileyTextCnt]; // with a different smiley

      // rotate through the array of smileys
      if (smileyTextCnt < smileyText.length - 1) {
        smileyTextCnt++;
      } else {
        smileyTextCnt = 0;
      }

    } else {
      smiley.move(3, 0);
    }

    // show the result
    renderAll(canvas, context);
  }

  // a flag for stopping the 'requestAnimationFrame' loop
  function stopEvent() {
    stop = true;
  }
  </script>
</head>

<body style="padding:1em" onload="start()">

  <h1 style="text-align: center">Continuous movement</h1>

  <canvas id="canvas" width="700" height="300"
          style="margin-top:1em; background-color:yellow" >
  </canvas>

  <div style="margin-top: 1em">
    <button onClick="start()">Start</button>
    <button onClick="stopEvent()">Stop</button>
  <div>

</body>
</html>

setIntervall[edit | edit source]

The next program implement the same smiley movement. It uses the traditional setInterval function instead of requestAnimationFrame. The source code of the two solutions differs only slightly.

  • At the end of the start function there is a call to setInterval with two parameters. The program's logic, implemented in playTheGame, is given as the first parameter. The second parameter is the time in milliseconds after that this function is called again and again.
  • Within the rest of the program setInterval is not invoked again - in opposite to the above requestAnimationFrame. It has initiated its (infinite) looping and doesn't need any further attendance.
  • Similar to the above requestAnimationFrame, a clearInterval is called to terminate the looping.
Click to see solution
<!DOCTYPE html>
<html>
<head>

  <!--
  'setInterval()' version of moving a smiley across the canvas with a fixed speed
  -->

  <title>Move a smiley</title>
  <script>
  "use strict";

  // ---------------------------------------------------------------
  //            class Smiley
  // ----------------------------------------------------------------
  class Smiley {
    constructor(context, text, x = 0, y = 0) {
      this.context = context;
      this.text = text;
      this.x = x;
      this.y = y;
    }

    // change the text (smiley's look)
    setText(text) {
      this.text = text;
    }

    // methods to move a smiley
    move(x, y) {
      this.x += x;
      this.y += y;
    }
    moveTo(x, y) {
      this.x = x;
      this.y = y;
    }

    render() {
      this.context.font = "30px Arial";
      this.context.fillText(this.text, this.x, this.y);
    }
  }  // end of class

  // ----------------------------------------------
  // variables that are known in the complete file
  // ----------------------------------------------
  let canvas;
  let context;
  let smiley;
  let stop;
  let refreshId;

  // use different smileys
  let smileyText = ['\u{1F60E}', '\u{1F9B8}',
                    '\u{1F9DA}', '\u{1F9DF}', '\u{1F47E}'];
  let smileyTextCnt = 0;

  // --------------------------------------------------
  // functions
  // --------------------------------------------------

  // inititalize all objects, variables, ... of the game
  function start() {
    // provide canvas and context
    canvas = document.getElementById("canvas");
    context = canvas.getContext("2d");

    smiley = new Smiley(context, smileyText[smileyTextCnt], 20, 100);
    smileyTextCnt++;
    stop = false;

    // show the scene on user's screen every 30 milliseconds
    // (the parameters for the function are given behind the milliseconds)
    refreshId = setInterval(playTheGame, 30, canvas, context);
  }

  // rendering consists of:
  //   - clear the complete scene
  //   - re-paint the complete scene
  function renderAll() {

    // remove every old drawings from the canvas
    context.clearRect(0, 0, canvas.width, canvas.height);

    // show the smiley
    smiley.render();

    // it's not necessary to re-start the game's logic or rendering
    // it's done automatically by 'setInterval'
    if (stop) {
      // interrupt animation, if the flag is set
      clearInterval(refreshId);
      // there is NO 'else' part. 'setInterval' initiates the
      // rendering automatically.
    }
  }

  // the game's logic
  function playTheGame(canvas, context) {

    // here, we use a very simple logic: move the smiley
    // across the canvas towards right
    if (smiley.x > canvas.width) {  // outside of right border
      smiley.moveTo(0, smiley.y);   // re-start at the left border
      smiley.text = smileyText[smileyTextCnt]; // with a different smiley

      // rotate through the array of smileys
      if (smileyTextCnt < smileyText.length - 1) {
        smileyTextCnt++;
      } else {
        smileyTextCnt = 0;
      }

    } else {
      smiley.move(3, 0);
    }

    // show the result
    renderAll(canvas, context);
  }

  // a flag for stopping the 'setInterval' loop
  function stopEvent() {
    stop = true;
  }
  </script>
</head>

<body style="padding:1em" onload="start()">

  <h1 style="text-align: center">Continuous movement</h1>

  <canvas id="canvas" width="700" height="300"
          style="margin-top:1em; background-color:yellow" >
  </canvas>

  <div style="margin-top: 1em">
    <button onClick="start()">Start</button>
    <button onClick="stopEvent()">Stop</button>
  <div>

</body>
</html>

See also[edit | edit source]