JavaScript/Exercises/IntroGraphic
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.
<!DOCTYPE html>
<html>
<head>
<title>Canvas 1</title>
<script>
// ..
</script>
</head>
<body style="padding:1em">
<h1 style="text-align: center">An HTML <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.
<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.
<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:
"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.
- Single movement: Re-painting initiated by an event
- Continous movement: Call to function
windows.requestAnimationFrame
- 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.
<!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.
<!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 ofrequestAnimationFrame(() => 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 totrue
, the system functioncancelAnimationFrame
is called instead ofrequestAnimationFrame
to terminate the loop of animations - and the smiley's movement.
<!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 tosetInterval
with two parameters. The program's logic, implemented inplayTheGame
, 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 aboverequestAnimationFrame
. It has initiated its (infinite) looping and doesn't need any further attendance. - Similar to the above
requestAnimationFrame
, aclearInterval
is called to terminate the looping.
<!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>