JavaScript/練習/IntroGraphic
我們提供了許多示例。一般來說,原始碼的結構遵循特定的模式:全域性定義/啟動函式/渲染函式/程式邏輯(playTheGame + 事件處理程式)。我們認為這種分離可以輕鬆理解程式碼,特別是邏輯和渲染之間的區別。但是,此架構只是一個建議;其他架構也可能適合您的需求,甚至更好。
為了開發包含圖形元素的應用程式,您需要在您的 HTML 中包含一個區域,以便您可以在其中“繪畫”。HTML 元素(如button、div 或其他元素)主要包含文字、顏色或(靜態)影像或影片。
HTML 元素canvas旨在實現此目的。它就像一塊板,可以在上面繪製點、線、圓等。
<!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>
在本介紹中,HTML 部分主要與上面的部分相同。有時會有一些額外的按鈕或說明。為了美化頁面,您可能需要新增一些額外的 CSS 定義。
主要工作是在 JavaScript 中完成的。第一個示例繪製了兩個矩形,一條“路徑”(一條線)和一個文字。
<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>
練習:繪製一條像大寫字母“M”的線,並用一個矩形包圍它。
<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>
請以結構化的、面向物件 的方式工作,並使用經典的 prototype 或 class 語法來定義常用的物件。在我們的示例中,我們使用 prototype 語法來定義 Rect(矩形),並使用 class 語法來定義 Circle,以提供兩種語法的示例。最好為每個這樣的物件 resp. 類建立單獨的 JS 檔案,以將其與處理遊戲的其他方面相關的其他 JS 檔案分離。
我們使用一些屬性和函式定義 Rect 和 Circle
"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();
}
上面的示例建立了一個沒有其物件發生任何變化或運動的單一圖形。這種圖形只需要繪製一次。但是,如果任何物件改變了位置,圖形就必須重新繪製。這可以透過不同的方式完成。
- 單一運動:由事件觸發的重新繪製
- 持續運動:呼叫函式
windows.requestAnimationFrame - 持續運動:呼叫函式
windows.setIntervall
情況 1:如果 - 在一個事件之後 - 一個物件的新的位置不再改變(它的“速度”為 0),則該事件可以直接觸發重新繪製。不需要其他操作。
如果某些物件旨在無任何使用者互動地持續地在螢幕上移動,那麼情況就會發生重大變化。這意味著它們有自己的“速度”。為了實現這種自動運動,重新繪製必須以某種方式由系統完成。兩個函式 requestAnimationFrame 和 setIntervall 被設計用來處理這部分。
情況 2:requestAnimationFrame(() => func()) 儘可能地觸發一次渲染。它接受一個引數,即應該渲染整個圖形的函式。func 函式的執行必須再次導致 requestAnimationFrame(() => func()) 的呼叫,只要動畫繼續進行。
情況 3:setIntervall(func, 25) 在一個迴圈中以一定的毫秒數(第二個引數)重複呼叫一個函式(第一個引數)。被呼叫的函式應該透過首先刪除所有舊內容,然後重新繪製所有內容來渲染圖形 - 就像 requestAnimationFrame 一樣。如今,requestAnimationFrame 比 setIntervall 更受歡迎,因為它的計時更準確,效果更好(例如,沒有閃爍,運動更平滑),並且效能更好。
可以透過點選“左”、“右”、“上”、“下”按鈕或按箭頭鍵來移動一個圖形。每次點選都會建立一個事件,呼叫事件處理程式,它透過一個固定值更改圖形的位置,最後,整個場景被重新繪製。重新繪製包括兩個步驟。首先,整個畫布被清除。其次,場景中的所有物件都被繪製,無論它們是否改變了位置。
<!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>
以下示例為畫布添加了一個事件處理程式 canvasClicked。其中,該事件包含滑鼠位置。
如果要將一個物件移動到滑鼠指向的位置,則需要知道點選事件發生的位置。您可以透過評估事件處理程式的引數“event”的詳細資訊來訪問此資訊。event.offsetX/Y 屬性顯示這些座標。它們與 circle 的附加 jumpTo 方法一起使用來移動圓形。
<!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>
此類應用程式顯示了一個常見的程式碼結構。
- 類和原型函式被宣告為包含它們的方法。
- 變數被宣告。
- 一個“啟動”或“初始化”函式建立所有必要的物件。作為它的最後一步,它呼叫渲染整個場景的函式。
- 渲染函式清除整個場景並再次繪製所有可視物件。
- 對按鈕或其他事件做出反應的事件處理程式更改可視物件的位置。它們不渲染它們。作為它們的最後一步,它們呼叫渲染整個場景的函式。
- 事件處理程式實現某種“業務邏輯”。因此,它們在不同的程式中會有很大的差異。其他部分具有更標準化和靜態的行為。
處理持續移動的物體類似於上面展示的逐步移動。區別在於,在使用者操作後,物體不僅僅移動到不同的位置。而是持續移動。物體現在有了“速度”。速度的實現必須由軟體以某種方式完成。requestAnimationFrame 和 setIntervall 這兩個函式被設計來處理這部分。
由於 requestAnimationFrame 在瀏覽器中廣泛可用,因此它比傳統的 setIntervall 更受歡迎。它的計時更準確,結果更好(例如,無閃爍,更平滑的移動),並且效能更好。
在下面的程式中,一個笑臉以恆定的速度在螢幕上移動。程式的整體結構類似於上面展示的解決方案。一旦啟動,運動就會持續進行,無需進一步的使用者互動。
- 類和原型函式被宣告為包含它們的方法。
- 變數被宣告。
- 一個“啟動”或“初始化”函式建立所有必要的物件。作為它的最後一步,它呼叫渲染整個場景的函式。
- 渲染函式 - 在我們的例子中是
renderAll- 清除整個場景並(重新)渲染所有視覺物件。 - 在渲染函式的末尾,實現了與上述程式的關鍵區別:它呼叫
requestAnimationFrame(() => func())。這儘可能地啟動了從 RAM 到物理螢幕的單個渲染物件的傳輸。
- 它接受一個引數,即執行遊戲邏輯(結合事件)的函式,在我們的例子中是
playTheGame。在動畫繼續進行的情況下,被呼叫函式的執行必須再次導致requestAnimationFrame(() => func())的呼叫。
- 對按鈕或其他事件做出反應的事件處理程式更改視覺物件的 位置或速度。它們不會渲染它們。此外,它們沒有必要呼叫負責渲染的函式;由於前面的兩個步驟,渲染一直在進行。
- 其中一個事件是 stopEvent。它設定一個布林變數,指示遊戲應該停止。此變數在
renderAll中進行評估。如果它被設定為true,則呼叫系統函式cancelAnimationFrame而不是requestAnimationFrame來終止動畫迴圈 - 以及笑臉的運動。
<!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>
下一個程式實現了相同的笑臉運動。它使用傳統的 setInterval 函式而不是 requestAnimationFrame。兩個解決方案的原始碼略有不同。
- 在
start函式的末尾,有一個對setInterval的呼叫,帶有兩個引數。在playTheGame中實現的程式邏輯作為第一個引數給出。第二個引數是此函式再次呼叫後經過的毫秒數。 - 在程式的其餘部分中,
setInterval不會再次呼叫 - 與上面的requestAnimationFrame相反。它已經啟動了(無限)迴圈,不需要任何進一步的參與。 - 與上面的
requestAnimationFrame類似,呼叫clearInterval來終止迴圈。
<!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>