跳转到内容

JavaScript/练习/IntroGraphic

来自维基教科书,开放世界中的开放书籍



我们提供了许多示例。一般来说,源代码的结构遵循特定的模式:全局定义/启动函数/渲染函数/程序逻辑(playTheGame + 事件处理程序)。我们认为这种分离可以轻松理解代码,特别是逻辑和渲染之间的区别。但是,此架构只是一个建议;其他架构也可能适合您的需求,甚至更好。

为了开发包含图形元素的应用程序,您需要在您的 HTML 中包含一个区域,以便您可以在其中“绘画”。HTML 元素(如buttondiv 或其他元素)主要包含文本、颜色或(静态)图像或视频。

HTML 元素canvas旨在实现此目的。它就像一块板,可以在上面绘制点、线、圆等。

点击查看解决方案
<!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>

在本介绍中,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 文件分离。

我们使用一些属性和函数定义 RectCircle

点击查看解决方案
"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();
}

运动入门

[编辑 | 编辑源代码]

上面的示例创建了一个没有其对象发生任何变化或运动的单一图形。这种图形只需要绘制一次。但是,如果任何对象改变了位置,图形就必须重新绘制。这可以通过不同的方式完成。

  1. 单一运动:由事件触发的重新绘制
  2. 持续运动:调用函数 windows.requestAnimationFrame
  3. 持续运动:调用函数 windows.setIntervall

情况 1:如果 - 在一个事件之后 - 一个对象的新的位置不再改变(它的“速度”为 0),则该事件可以直接触发重新绘制。不需要其他操作。

如果某些对象旨在无任何用户交互地持续地在屏幕上移动,那么情况就会发生重大变化。这意味着它们有自己的“速度”。为了实现这种自动运动,重新绘制必须以某种方式由系统完成。两个函数 requestAnimationFramesetIntervall 被设计用来处理这部分。

情况 2requestAnimationFrame(() => func()) 尽可能地触发一次渲染。它接受一个参数,即应该渲染整个图形的函数。func 函数的执行必须再次导致 requestAnimationFrame(() => func()) 的调用,只要动画继续进行。

情况 3setIntervall(func, 25) 在一个循环中以一定的毫秒数(第二个参数)重复调用一个函数(第一个参数)。被调用的函数应该通过首先删除所有旧内容,然后重新绘制所有内容来渲染图形 - 就像 requestAnimationFrame 一样。如今,requestAnimationFramesetIntervall 更受欢迎,因为它的计时更准确,效果更好(例如,没有闪烁,运动更平滑),并且性能更好。

单一运动(跳跃)

[编辑 | 编辑源代码]

固定宽度逐步操作

[编辑 | 编辑源代码]

可以通过点击“左”、“右”、“上”、“下”按钮或按箭头键来移动一个图形。每次点击都会创建一个事件,调用事件处理程序,它通过一个固定值更改图形的位置,最后,整个场景被重新绘制。重新绘制包括两个步骤。首先,整个画布被清除。其次,场景中的所有对象都被绘制,无论它们是否改变了位置。

点击查看解决方案
<!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>

此类应用程序显示了一个常见的代码结构。

  • 类和原型函数被声明为包含它们的方法。
  • 变量被声明。
  • 一个“启动”或“初始化”函数创建所有必要的对象。作为它的最后一步,它调用渲染整个场景的函数。
  • 渲染函数清除整个场景并再次绘制所有可视对象。
  • 对按钮或其他事件做出反应的事件处理程序更改可视对象的位置。它们渲染它们。作为它们的最后一步,它们调用渲染整个场景的函数。
事件处理程序实现某种“业务逻辑”。因此,它们在不同的程序中会有很大的差异。其他部分具有更标准化和静态的行为。

持续运动

[编辑 | 编辑源代码]

处理持续移动的物体类似于上面展示的逐步移动。区别在于,在用户操作后,物体不仅仅移动到不同的位置。而是持续移动。物体现在有了“速度”。速度的实现必须由软件以某种方式完成。requestAnimationFramesetIntervall 这两个函数被设计来处理这部分。

由于 requestAnimationFrame 在浏览器中广泛可用,因此它比传统的 setIntervall 更受欢迎。它的计时更准确,结果更好(例如,无闪烁,更平滑的移动),并且性能更好。

requestAnimationFrame

[编辑 | 编辑源代码]

在下面的程序中,一个笑脸以恒定的速度在屏幕上移动。程序的整体结构类似于上面展示的解决方案。一旦启动,运动就会持续进行,无需进一步的用户交互。

  • 类和原型函数被声明为包含它们的方法。
  • 变量被声明。
  • 一个“启动”或“初始化”函数创建所有必要的对象。作为它的最后一步,它调用渲染整个场景的函数。
  • 渲染函数 - 在我们的例子中是 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>

setIntervall

[编辑 | 编辑源代码]

下一个程序实现了相同的笑脸运动。它使用传统的 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>

另请参阅

[编辑 | 编辑源代码]
华夏公益教科书