iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
1
Modern Web

網頁阿尼尛,到底是在幹尛?系列 第 21

第二十章、你好啊鐵人們,接下來就由我鎖鏈 Canvas 對付你!(肆)

簡介

在昨天我們講到了物件導向的基礎觀念,今天我們就來加深理解並且稍微談談資料驅動這件事情吧!

我們在寫程式的時候會很常用到 事件驅動,也就是我們會直接透過 Don Tree 去選取元素並且綁定事件操作元素。通常這樣的寫法會應用在資料與畫面混雜的程式碼內。

但是,由於我們在撰寫 Canvas 的時候很常會需要先建立 Canvas 畫布的設定、圖形元素的設定,在針對這些設定把元素填入、把元素動畫的時間填入。因此若是沒有透過 資料驅動 來管理程式碼,整坨的 Canvas 程式碼會變得非常難以閱讀。

沒有做 class 封裝管理的程式碼

用 MDN 上的範例來說:

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var raf;

var ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 2,
  radius: 25,
  color: 'blue',
  draw: function() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
};

function draw() {
  ctx.clearRect(0,0, canvas.width, canvas.height);
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;
  raf = window.requestAnimationFrame(draw);
}

canvas.addEventListener('mouseover', function(e) {
  raf = window.requestAnimationFrame(draw);
});

canvas.addEventListener('mouseout', function(e) {
  window.cancelAnimationFrame(raf);
});

ball.draw();

我們可以看到範例在 ball 這個變數內寫了有關於 圖形元素「球」的設定,並且在內部建立的一個 function 來執行「繪製圖形」這件事。

但是像這樣的寫法我們很難去管理這個元素,且若我們要使用 「球」這個類別的設定,就只能畫出 寬 100、高 100、藍色 的圓,若是想要畫一個紅色的圓,就必須要在建立一個全新的變數去設定。

到這邊大家應該就會發現,像工程師這種懶到不行的生物,怎麼可能重複撰寫一大包程式只為了讓藍色的球變成紅色的球呢~

透過 class 封裝程式碼

下面我們來做個小範例:

  • 建立圖形元素的類別(元素在畫布上的定位、動畫時的幀數該如何運算等):
class Arrow {
  constructor(options) {
    this.imagePath = options.imagePath;
    this.image = this.createImage();

    this.position = options.position || { x: 0, y: 0 };
    this.drawArea = options.drawArea;

    this.ticksPerFrame = 40;
    this.tickCount = 0;
  }

  createImage() {
    const image = new Image();
    image.src = this.imagePath;
    return image;
  }
  updateFrame() {
    this.tickCount++;
    if (this.tickCount > this.ticksPerFrame) {
      this.tickCount = 0;
    } else {
      this.position.x = this.tickCount * 10;
      console.log(this.position, this.tickCount);
    }
  }
}
  • 建立 Canvas 畫布的類別(Canvas 的環境設定、元素在畫布上的寬高、呼叫播放動畫的 function):
class CanvasAnimation {
  constructor(options) {
    this.width = options.width;
    this.height = options.height;

    this.canvas = document.querySelector(options.selector);
    this.initCanvas();
    this.context = this.getCanvasContext();

    this.elements = options.elements || [];
  }

  initCanvas() {
    this.canvas.width = this.width;
    this.canvas.height = this.height;
  }
  getCanvasContext() {
    return this.canvas.getContext('2d');
  }
  
  update() {
    this.elements.forEach(element => element.updateFrame());
  }
  
  render() {
    for (const element of this.elements) {
      this.context.clearRect(0, 0, 512, 512);
      this.context.drawImage(
        element.image,
        0, 0,
        this.width, this.height,
        element.position.x, 0,
        element.drawArea.width, element.drawArea.height
      );
    }
  }

  play() {
    window.requestAnimationFrame(this.play.bind(this));
    this.update();
    this.render();
  }
}
  • 建立圖形元素的物件實例
const arrow = new Arrow({
  imagePath: 'https://i.imgur.com/6PhDMp8.png',
  position: { x: 0, y: 0 },
  drawArea: { width: 64, height: 64 }
});
  • 建立動畫模式的物件實例
const canvasAnimation = new CanvasAnimation({
  selector: '.canvas',
  width: 512,
  height: 512,
  elements: [arrow]
});
  • 播放動畫
// 為了確保圖形元素 100% 的載入後才跑動畫的寫法
arrow.image.addEventListener('load', () => {
  canvasAnimation.play();
});

像這樣的寫法,我們可以將每一個設定動作都封裝在 class 裡面,讓閱讀程式的人(比如 N 個月後的你)能更更聚焦在每一段程式碼執行的動作。

並且我們透過資料驅動,讓我們可以更彈性的去設定每一個元素,如此一來程式碼就會更加簡潔。


今天我們透過 Canvas 更深入理解物件導向、ES6 class、跟資料驅動程式碼的差異,不知道大家有沒有理解呢?

也許我們在程式碼上很少會使用到這些觀念,但是學會了這些語法,就會讓人有種特別厲害、特別有成長的感覺呢!

那麼今天就到這邊,大家明天見!


參考資料


上一篇
第十九章、你好啊鐵人們,接下來就由我鎖鏈 Canvas 對付你!(參)
下一篇
第二十一章、你好啊鐵人們,接下來就由我鎖鏈 Canvas 對付你!(伍)
系列文
網頁阿尼尛,到底是在幹尛?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言