跳轉到內容

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>

另請參閱

[編輯 | 編輯原始碼]
華夏公益教科書