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>