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 functionstart(){ "use strict"; // make the HTML element 'canvas' available to JS constcanvas=document.getElementById("canvas"); // make the 'context' of the canvas available to JS. // It offers many functions like 'fillRect', 'lineTo', // 'ellipse', ... constcontext=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 functionstart(){ "use strict"; // make the HTML element 'canvas' available to JS constcanvas=document.getElementById("canvas"); // make the 'context' of the canvas available to JS. // It offers many functions like 'fillRect', 'lineTo', // 'ellipse', ... constcontext=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) functionRect(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) classCircle{ 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(); } } functionstart(){ // provide canvas and context constcanvas=document.getElementById("canvas"); constcontext=canvas.getContext("2d"); // create a rectangle at a certain position constrect_1=newRect(context,100,100); rect_1.render(); // create a circle at a certain position constcircle_1=newCircle(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 // ---------------------------------------------------------------- classRect{ 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 classSmiley{ 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 // ---------------------------------------------- letcanvas; letcontext; letobstacles=[]; lethe,she; // -------------------------------------------------- // functions // -------------------------------------------------- // inititalize all objects, variables, ... of the game functionstart(){ // provide canvas and context canvas=document.getElementById("canvas"); context=canvas.getContext("2d"); // create some obstacles constobst1=newRect(context,200,80,10,210,"red"); constobst2=newRect(context,350,20,10,150,"red"); constobst3=newRect(context,500,100,10,210,"red"); obstacles.push(obst1); obstacles.push(obst2); obstacles.push(obst3); he=newSmiley(context,'\u{1F60E}',20,260); she=newSmiley(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 functionrenderAll(){ // remove every old drawings from the canvas context.clearRect(0,0,canvas.width,canvas.height); // show the rectangles for(leti=0;i<obstacles.length;i++){ obstacles[i].render(); } // show two smilies he.render(); she.render(); } // event handler to steer smiley's movement functionleftEvent(){ he.move(-10,0); renderAll(); } functionrightEvent(){ he.move(10,0); renderAll(); } functionupEvent(){ he.move(0,-10); renderAll(); } functiondownEvent(){ 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' // -------------------------------------------------------------- classCircle{ 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 // ---------------------------------------------- letball; letcanvas; letcontext; // -------------------------------------------------- // functions // -------------------------------------------------- // inititalize all objects, variables, .. of the game functionstart(){ // provide canvas and context canvas=document.getElementById("canvas"); context=canvas.getContext("2d"); // create a ball (class circle) at a certain position ball=newCircle(context,400,100,50); renderAll(); } // rendering consists of: // - clear the complete scene // - re-paint the complete scene functionrenderAll(){ // remove every old drawings from the canvas context.clearRect(0,0,canvas.width,canvas.height); ball.render(); } // event handling functioncanvasClicked(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.
<!DOCTYPEhtml> <html> <head> <!-- 'requestAnimationFrame()'versionofmovingasmileyacross thecanvaswithafixedspeed --> <title>Moveasmiley</title> <script> "use strict"; // --------------------------------------------------------------- // class Smiley // ---------------------------------------------------------------- classSmiley{ 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 // ---------------------------------------------- letcanvas; letcontext; letsmiley; letstop; letframeId; // use different smileys letsmileyText=['\u{1F60E}','\u{1F9B8}', '\u{1F9DA}','\u{1F9DF}','\u{1F47E}']; letsmileyTextCnt=0; // -------------------------------------------------- // functions // -------------------------------------------------- // inititalize all objects, variables, ... of the game functionstart(){ // provide canvas and context canvas=document.getElementById("canvas"); context=canvas.getContext("2d"); smiley=newSmiley(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() functionrenderAll(){ // 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 functionplayTheGame(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 functionstopEvent(){ stop=true; } </script> </head> <bodystyle="padding:1em"onload="start()"> <h1style="text-align: center">Continuousmovement</h1> <canvasid="canvas"width="700"height="300" style="margin-top:1em; background-color:yellow"> </canvas> <divstyle="margin-top: 1em"> <buttononClick="start()">Start</button> <buttononClick="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 // ---------------------------------------------------------------- classSmiley{ 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 // ---------------------------------------------- letcanvas; letcontext; letsmiley; letstop; letrefreshId; // use different smileys letsmileyText=['\u{1F60E}','\u{1F9B8}', '\u{1F9DA}','\u{1F9DF}','\u{1F47E}']; letsmileyTextCnt=0; // -------------------------------------------------- // functions // -------------------------------------------------- // inititalize all objects, variables, ... of the game functionstart(){ // provide canvas and context canvas=document.getElementById("canvas"); context=canvas.getContext("2d"); smiley=newSmiley(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 functionrenderAll(){ // 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 functionplayTheGame(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 functionstopEvent(){ 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>