本文不允許任何形式的轉載!

閱讀提示

本系列文章不適合以下人群閱讀,如果你無意點開此文,請對號入座,避免浪費你寶貴的時間。

  • 想要學習利用遊戲引擎開發遊戲的朋友。本文不會涉及任何第三方遊戲引擎。
  • 不具備面向對象編程經驗的朋友。本文的語言主要是Javascript(ECMA 2016),如果你不具備JS編程經驗倒也無妨,但沒有面向對象程序語言經驗就不好辦了。
  • 想要直接下載實例代碼的朋友。抱歉,我都用嘴說,基本上沒有示例代碼。

上期作業

上期留了個作業,用Canvas 2d context的狀態讓一個方塊繞中心縮放。這個很簡單,首先移動到該方塊的中心並縮放以改變當前坐標系:

// 假設有一個矩形,變數名為rect,具有left,top,width,height屬性,

let scale = 1.5;

let transformX = rect.left + rect.width/2;

let transformY = rect.top + rect.height/2;

ctx.translate(transformX , transformY );

ctx.scale(scale,scale); // x和y軸同時縮放為以前的1.5倍

這時候如果開始繪製,就會發現這個矩形的左上角被移動到了其中心附近(並不是它之前的中心位置,參考上一期文章)並且發生了縮放。所以我們想要讓這個矩形是按照它之前中心縮放的,只需要把坐標系進行反向移動,讓此時的矩形中心和未改變之前的矩形中心重合。

由於整個坐標系被縮放,就不能直接translate(- transformX , -transformY )返回之前坐標系,在調用scale的時候已經和以前不一樣了,被放大(或者縮小)了scale倍,所以移動回去的坐標計算如下:

let newx = transformX / scale - transformX;
let newy = transformY / scale - transformY;

ctx.translate(newx , newy);

ctx.rect(rect.left,rect.top,rect.width,rect.height);

ctx.fillStyle = 某顏色;

ctx.fill();

繪製封裝

為了方便演示,我將在微信小遊戲上進行代碼測試,有興趣的朋友可以去看看什麼是微信小遊戲,反正都是js代碼,雖然跟瀏覽器端有本質上的區別吧。幾點需要注意,微信小遊戲不具備DOM,所以canvas是不能用document創建的,需要用它提供的介面wx.createCanvas , 同樣Image也不能用dom方式創建,也要用wx.createImage來獲取。

文中除了生成的類,其餘代碼都是在微信小遊戲的game.js中編寫的

我們一般在canvas上進行2d繪製一個圖形,都會有以下幾個步驟:

  1. 獲得canvas的2d context,也就是我說的畫筆
  2. 確定要繪製圖形的大小以及左上角坐標
  3. 開始具體繪製圖形

例如我想要畫一個紅色矩形,並且有個綠色的邊框,大致代碼如下:

let canvas = wx.createCanvas(); // 微信小遊戲獲得canvas的方法
let ctx = canvas.getContext(2d); // 獲得2d ctx

// 確定矩形的坐標位置以及大小
let left = 100;
let top = 100;
let width = 100;
let height = 100;

ctx.beginPath();
ctx.rect(left, top, width, height);
ctx.closePath();

ctx.fillStyle = red; // 設置填充色
ctx.fill();
ctx.strokeStyle = green; // 設置邊框色
ctx.stroke();

輸出結果:

如果我想要畫100個矩形呢,初學者都懂,這就需要封裝剛才的代碼改成一個可調用的方法,參數就是坐標,大小,顏色等:

let canvas = wx.createCanvas();
let ctx = canvas.getContext(2d);

for (let i = 0; i < 100; i++) {
let left = Math.floor(Math.random() * canvas.width);
let top = Math.floor(Math.random() * canvas.height);
let width = Math.floor(Math.random() * 100);
let height = Math.floor(Math.random() * 100);
let color = randomColor();
drawRectangle(ctx, left, top, width, height, randomColor(),randomColor());
}

function randomColor() {
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
return RGB( + r + , + g + , + b + );
}

function drawRectangle(ctx, left, top, width, height, color, borderColor) {
ctx.save();
ctx.beginPath();
ctx.rect(left, top, width, height);
ctx.closePath();

if (color) {
ctx.fillStyle = color;
ctx.fill();
}
if (borderColor) {
ctx.strokeStyle = borderColor;
ctx.stroke();
}
}

輸出結果:

經過封裝後,我們就可以調用drawRectangle方法在canvas上繪製方塊了。

Figure類的誕生

那麼如果我要想要繪製更多的圖形呢,比如我想要繪製一個圓,那我們也可以設計出這麼一個方法,叫做drawCircle,同樣,這個方法也可以給出一個左上角坐標,以及該圓形需要在canvas上顯示的大小,比如:

let canvas = wx.createCanvas();
let ctx = canvas.getContext(2d);

for (let i = 0; i < 100; i++) {
let left = Math.floor(Math.random() * canvas.width);
let top = Math.floor(Math.random() * canvas.height);
let width = Math.floor(Math.random() * 100);
let height = width;//Math.floor(Math.random() * 100);
let color = randomColor();
let borderColor = randomColor();
drawCircle(ctx, left, top, width, height, color, borderColor);
}

function randomColor() {
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
return RGB( + r + , + g + , + b + );
}

function drawCircle(ctx, left, top, width, height, color, borderColor) {
if (width != height) return;
let radius = width / 2;
ctx.save();
ctx.beginPath();
ctx.arc(left + radius, top + radius, radius, 0, 2 * Math.PI);
ctx.closePath();
if (color) {
ctx.fillStyle = color;
ctx.fill();
}
if (borderColor) {
ctx.strokeStyle = borderColor;
ctx.stroke();
}
}

輸出結果:

你肯定會注意到,不管我們繪製什麼圖形,實際上在2d畫布上都可以看成在一個以(left,top)為左上角坐標,(width,height)大小的一個矩形區域內的繪製結果,不管是圓,多邊形還是一個Image都是這樣的。

我們可以這樣抽象認為:在2d畫布上的繪製的任何一個圖形,都是在一個我們所給出的特定的矩形區域內的繪製結果,而這個矩形區域內繪製動作是可以我們需要進行替換修改的。這樣一來我們就可以定義一個類,這個類專門負責描述我們所要繪製圖形的屬性以及一個可以調用的繪製方法介面,Figure.js:

let _lt = Symbol(左上角坐標Float32數組,0是x,1是y);
let _wh = Symbol(圖形大小Float32數組,0是width,1是height);
export default class Figure{
constructor(p){
p = p || {};
this[_lt] = new Float32Array(2);
this[_wh] = new Float32Array(2);
this.left = p[left] || 0;
this.top = p[top] || 0;
this.width = p[width] || 0;
this.height = p[height] || 0;
}

get left(){
return this[_lt][0];
}
set left(value){
this[_lt][0] = value;
}
get top(){
return this[_lt][1];
}
set top(value){
this[_lt][1] = value;
}
get width(){
return this[_wh][0];
}
set width(value){
this[_wh][0] = value;
}
get height(){
return this[_wh][1];
}
set height(value){
this[_wh][1] = value;
}

/**
* 具體繪製方法
* @param ctx
*/
draw(ctx){

}
}

這是ECMA2016代碼,可以定義類以及一個叫做symbol的變數,還不會ECMA2016的朋友可以去看看。注意,因為js是不能聲明私有變數的,這裡我用symbol模擬私有變數。有人會問為什麼要用Float32Array去定義坐標以及長寬而不是直接定義成一個變數呢,個人認為定義成強類型這可以提高計算速度,至少省去了js類型判斷和轉換的操作,在日後的文章中,會涉及到很多線性代數的計算,都會用到強類型數組。

這個Figure類目前來看還什麼都做不了,僅僅只是描述了圖形的所在區域,不過要知道,這種是一個頂層類(js沒有抽象類,我就叫它頂層類吧),我們是可以再定義一些有具體繪製方法的類繼承之它,比如下面這個類Shape.js:

import Figure from "./Figure";

export default class Shape extends Figure {
constructor(p) {
p = p || {};
super(p);
this.color = p[color] || #000000;
this.borderColor = p[borderColor] || #000000;
}

draw(ctx) {
ctx.save();
ctx.beginPath();
this.drawShap(ctx);
ctx.closePath();
if (this.color) {
ctx.fillStyle = this.color;
ctx.fill();
}
if (this.borderColor) {
ctx.strokeStyle = this.borderColor;
ctx.stroke();
}
}

drawShape(ctx) {

}
}

Shape類繼承自Figure,並且增加了color和borderColor屬性,在draw方法里限制了path,以及根據color和borderColor決定是否要填充以及描邊。

同樣,Shape和Figure一樣也算是一個頂層類,因為真正實施具體繪製的類在後面:

import Shape from "./Shape";

export default class Rectangle extends Shape{
constructor(p){
super(p);
}

drawShape(ctx){
ctx.rect(0,0,this.width,this.height);
}
}

Rectangle就是一個真正要進行繪製的類了,它實現了Shape的drawShape方法。

看上去很繁瑣是吧,簡單繪製一個矩形定義了尼瑪3個類,沒耐心的人肯定跳腳罵娘。我們慢慢往下看,你就知道就這麼簡單的繪製矩形還有很多變化,如果不靠抽象出頂層類去封裝根本就不可能完成後續的工作。

我把剛才提到的繪製圓形的類也給出來:

import Shape from "./Shape";

export default class Circle extends Shape {
constructor(p) {
super(p);
}

get center() {
// 始終是由left和top以及區域大小來決定的
return {x: this.left + this.radius, y: this.top + this.radius};
}

set center(center) {
this.left = center.x - this.radius;
this.top = center.y - this.radius;
}

get radius() {
return this.width / 2;
}

set radius(r) {
this.width = r * 2;
}

set width(value) {
// 保證這個圓形所在區域是個正方形
super.width = value;
super.height = value;
}

set height(value) {
super.width = value;
super.height = value;
}

get width() {
return super.width;
}

get height() {
return super.height;
}

drawShape(ctx) {
ctx.arc(this.left+this.radius, this.top+this.radius, this.radius, 0, 2 * Math.PI);
}
}

比起矩形類要複雜點,是因為要約束width和height,以及要具有自己的center和radius屬性。

既然定義好了類,那一開始繪製100個隨機矩形的代碼就可以這樣寫了:

import Rectangle from "./example/Rectangle";

let canvas = wx.createCanvas();
let ctx = canvas.getContext(2d);

for (let i = 0; i < 100; i++) {
let left = Math.floor(Math.random() * canvas.width);
let top = Math.floor(Math.random() * canvas.height);
let width = Math.floor(Math.random() * 100);
let height = width;
let color = randomColor();
let borderColor = randomColor();
let rect = new Rectangle({
left: left,
top: top,
width: width,
height: height,
color: color,
borderColor: borderColor
});
rect.draw(ctx);
}

function randomColor() {
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
return RGB( + r + , + g + , + b + );
}

實際上我們發現,工作量並沒有變少,反而變多了。

引入新的屬性以及context狀態變化

現在我想畫兩個矩形,一個繞其中心旋轉了45度的矩形,而另一個是半透明的。

那我們需要利用context狀態變化配合繪製才能做到了,不明白的可以看我的上一期文章:

老臉叔叔:【老臉教你做遊戲】Context的狀態?

zhuanlan.zhihu.com
圖標

而這些旋轉啊,透明度啊等等,如果體現在我們剛才設計的Figure類中,實際上就是用來描述這個圖形一個屬性而已,而且我們可以在不改變Rectangle類的情況下來增加這些屬性(實際上Rectangle的父類Shape要改一個地方)。

注意,我們在改變一個Figure的狀態時,是不能影響到其他figure的,看下Figure的draw方法,這是要被子類複寫才能實現繪製的,但我們又要在這個方法里進行狀態變化設置,所以我們要加入一個方法,叫做drawSelf,專門讓子類複寫實現繪製,而我們只需要在draw方法里進行狀態的保存變化以及恢復即可:

let _lt = Symbol(左上角坐標Float32數組,0是x,1是y);
let _wh = Symbol(圖形大小Float32數組,0是width,1是height);
export default class Figure {
constructor(p) {
p = p || {};
this[_lt] = new Float32Array(2);
this[_wh] = new Float32Array(2);
this.left = p[left] || 0;
this.top = p[top] || 0;
this.width = p[width] || 0;
this.height = p[height] || 0;
this.opacity = p[opacity] || 1; // 新加入的透明度屬性,默認為不透明
this.rotate = p[rotate] || 0 // 新加入的旋轉度數,默認為0
}

get left() {
return this[_lt][0];
}

set left(value) {
this[_lt][0] = value;
}

get top() {
return this[_lt][1];
}

set top(value) {
this[_lt][1] = value;
}

get width() {
return this[_wh][0];
}

set width(value) {
this[_wh][0] = value;
}

get height() {
return this[_wh][1];
}

set height(value) {
this[_wh][1] = value;
}

/**
* 對外調用的介面
* @param ctx
*/
draw(ctx) {
// 保存之前狀態,然後就可以任意修改了,
// 最後恢復一下就可以保證在這個figure之外的其他地方狀態不變
ctx.save();
ctx.globalAlpha = this.opacity; // 設置透明度
// 我們默認以中心旋轉
let center = {x: this.left + this.width / 2, y: this.top + this.height / 2};
ctx.translate(center.x, center.y);
ctx.rotate(this.rotate * Math.PI / 180); // 把角度轉成弧度
ctx.translate(-center.x, -center.y);
this.drawSelf(ctx);
ctx.restore(); // 恢復到之前狀態
}

/**
* 具體繪製自己方法
* @param ctx
*/
drawSelf(ctx) {

}
}

新的Figure類在draw方法中實現了旋轉以及透明度的設置,再次友情提示,如果不明白繞中心旋轉怎麼寫的,請退回去看我的

老臉叔叔:【老臉教你做遊戲】Context的狀態?

zhuanlan.zhihu.com
圖標

實際上我們還需要把伸縮也加進去,我就不寫了,文章一開始我就已經寫好了。

這個draw方法子類就不要隨便複寫了,只要複寫drawSelf方法就可以,所以我們還要把Shape類複寫的draw方法改成drawSelf才算最終完成。

根據一開始我給出的case,我們的代碼如下:

let canvas = wx.createCanvas();
let ctx = canvas.getContext(2d);
let rect = new Rectangle({
left: 100,
top: 100,
width: 100,
height: 200,
color: white,
rotate: 45
});

// rect1的左上角是rect的中心點

let rect1 = new Rectangle({
left: 150,
top: 200,
width: 200,
height: 200,
color: red,
opacity:0.5
});
rect.draw(ctx);
rect1.draw(ctx);

輸出結果如下:

根據代碼我們可以看出,圖形被定義為了類後,每次繪製一個圖形我們只需要描述出這個圖形的基本屬性即可,這大大增加了代碼可讀性,而且比起簡單的方法封裝,重構性和擴展性強太多太多,這也是為什麼面向對象比起面向方法「高級」。「萬物皆可對象」,這句話雖然是當年調侃java程序員的,但是仔細想想並不無道理。

子Figure是什麼?

這一節很重要,請認真看

現在讓我們重新思考一下,Figure類到底是什麼。

我們定義Figure的初衷是希望能將繪製圖形對象化,目前來看好像初步做到了。那好,我再給出一個case,我想要在畫布上繪製兩個圖形,兩個正方形,左上角坐標要重合,並且其中一個正方形的width是另外一個的一半。

按照目前我們的設計來看,那就定義兩個Rectangle,然後兩個Figure的左上角坐標要重合,並且設置好對應的width即可。看似並且有什麼難度嘛,但我現在希望這兩個圖形要保證同時基於最大正方形的中心進行縮放和旋轉呢?

如果你做過UI編程,應該不難發現,我們設計的Figure類有個很大的弊端,就是它只有一層結構,即所有Figure都是在canvas上的,大家都是同一level,沒有層次結構。

而我們熟悉的html也好,xml也好,都是有層次結構的,所以Figure類也需要進行這樣的修改,也就是我們常說的嵌套。

我們可以認為,一個Figure只是我們要繪製圖形的一部分而已,整個圖形還有其他部分需要繪製,而這些其他部分是可以嵌套到Figure中的。也就是說我們可以在Figure上增加子Figure,這樣一來Figure就可以設計成為一個自包含的類,也就是設計模式中的composite模式,其實就是一顆樹形的數據結構。每當我們調用Figure的draw方法的時候,除了要drawSelf外,還要逐個調用子FIgure的draw方法,形成一個遞歸繪製的過程。

Figure還需要具備一個parent屬性,用來指向它的父節點。在Figure增加子節點的時候需要改變parent的值,如果該子節點已經有了父節點,那就要讓它原來的父節點把它剔除掉;刪除節點的時候需要把parent置為null或者undefiend。

讓我們改造一下Figure類:

let _lt = Symbol(左上角坐標Float32數組,0是x,1是y);
let _wh = Symbol(圖形大小Float32數組,0是width,1是height);
export default class Figure {
constructor(p) {
p = p || {};
this[_lt] = new Float32Array(2);
this[_wh] = new Float32Array(2);
this.left = p[left] || 0;
this.top = p[top] || 0;
this.width = p[width] || 0;
this.height = p[height] || 0;
this.opacity = p[opacity] || 1; // 新加入的透明度屬性,默認為不透明
this.rotate = p[rotate] || 0 // 新加入的旋轉度數,默認為0
this.children = []; // 增加子figure數組
this.parent = null;
}

indexOf(figure) {
for (let i = 0; i < this.children.length; i++) {
if (figure == this.children[i]) return i;
}
return -1;
}

addChild(figure) {
if (figure.parent) {
figure.parent.removeChild(figure);
}
this.children.push(figure);
figure.parent = this;
}

removeChild(figure) {
let index = this.indexOf(figure);
if (index != -1) {
this.children.splice(index, 1);
figure.parent = null;
}
}

getGraph() {
if (this.parent == null) {
return this;
} else {
return this.parent.getGraph();
}
}

get left() {
return this[_lt][0];
}

set left(value) {
this[_lt][0] = value;
}

get top() {
return this[_lt][1];
}

set top(value) {
this[_lt][1] = value;
}

get width() {
return this[_wh][0];
}

set width(value) {
this[_wh][0] = value;
}

get height() {
return this[_wh][1];
}

set height(value) {
this[_wh][1] = value;
}

/**
* 對外調用的介面
* @param ctx
*/
draw(ctx) {
ctx.save(); // 保存之前狀態,然後就可以任意修改了,最後恢復一下就可以保證在這個figure之外的其他地方狀態不變
ctx.globalAlpha = this.opacity; // 設置透明度
// 我們默認以中心旋轉
let center = {x: this.left + this.width / 2, y: this.top + this.height / 2};
ctx.translate(center.x, center.y);
ctx.rotate(this.rotate * Math.PI / 180); // 把角度轉成弧度
ctx.translate(-center.x, -center.y);
this.drawSelf(ctx);
this.drawChildren(ctx); // 繪製完自己後再繪製子Figure
ctx.restore(); // 恢復到之前狀態
}

/**
* 子Figure的繪製
* @param ctx
*/
drawChildren(ctx) {
for (let i = 0; i < this.children.length; i++) {
let childFigure = this.children[i];
childFigure.draw(ctx);
}
}

/**
* 具體繪製自己方法
* @param ctx
*/
drawSelf(ctx) {

}
}

注意,我們在改變了Figure的狀態後(旋轉拉伸等)並沒有恢復之前狀態,而是直接調用drawChildren,所以Figure的狀態是基於其父Figure狀態的,這也正是我們想要的。

那剛才我給的case就可以這樣做了: 先定義一個較大Rectangle,然後再定義另一個較小的Rectangle,將這個較小的Rectangle作為figure加入到較大的Rectangle中,一旦較大的Rectangle(父figure)發生移動、旋轉、拉伸,其子節點也會發生相應的變化且保持其在父節點的相對位置不變,比如以下代碼:

import Rectangle from "./example/Rectangle";

let canvas = wx.createCanvas();
let ctx = canvas.getContext(2d);
let rect = new Rectangle({
left: 100,
top: 100,
width: 400,
height: 400,
color: white,
rotate: 45
});
// rect里嵌套了另一個rect
let childRect = new Rectangle({
left: 10,
top: 10,
width: 200,
height: 200,
color: red,
opacity:0.5
});
rect.addChild(childRect);
rect.draw(ctx);

rect具有一個子節點childRect,並且rect發生了旋轉,childRect也跟著rect(它的父節點)一起進行了變換:

現在回過頭再抽象地看下canvas,也就是我們的頂層畫布,其實也是一個Figure啊,只是這個Figure的大小和左上角跟最頂層畫布重合併且和canvas一樣大,而我們剛才在canvas上畫的那些矩形、圓形只是這個頂層Figure的一些子Figure而已。

那我們為什麼不設計一個類來代表這個最頂層的畫布呢:

import Figure from "./Figure";

export default class Graph extends Figure {
constructor(canvas) {
super();
this.canvas = canvas;
this.left = 0;
this.top = 0;
this.width = canvas.width;
this.height = canvas.height;
this.ctx = canvas.getContext(2d);
}

refresh() {
this.ctx.clearRect(0,0,this.width,this.height);
this.draw(this.ctx);
}

get parent(){
return null;
}

set parent(p){}
}

頂層畫布是不具備parent的,所以這裡對parent屬性進行了修改。Graph維護了canvas2d的context,並具備一個獨立的refresh方法,用來重新繪製整個canvas。

既然有了這個頂層類,那我們剛才的代碼可以改成:

import Rectangle from "./example/Rectangle";
import Graph from "./example/Graph";

let canvas = wx.createCanvas();
let graph = new Graph(canvas);
let rect = new Rectangle({
left: 100,
top: 100,
width: 400,
height: 400,
color: white,
rotate: 45
});

let childRect = new Rectangle({
left: 10,
top: 10,
width: 200,
height: 200,
color: red,
opacity:0.5
});
rect.addChild(childRect);
graph.addChild(rect);
graph.refresh();

既然有了層級結構,那下面一部分就要好講了。

到底用哪個坐標系?

我們習慣於在繪製的時候都處在所在畫布的坐標系(或者說是在繪製圖形所在的父節點坐標系中),比如lineTo(x,y),這裡的x和y都是基於其所在畫布的,縱觀網上各種教程啊demo,都是這種習慣,很少提及繪製的時候使用的是自身的坐標系,可能是不太符合我們慣有的思維習慣。

我們剛才設計的Figure類,具有left和top兩個坐標屬性,我們習慣的認為當我們要去實現繪製方法的時候,left和top就是我們要繪製圖形的左上角坐標。實際上不是,我們要繪製的圖形的左上角坐標其實是(0,0),left和top只是這個圖形在其父Figure坐標系中的位置而已,可能說的有點繞。

我們一開始設計出Figure類的目的就是想讓繪製圖形對象化,那麼這個對象化的圖形在繪製自身的時候,我們就應該將它所在區域看成一個以(0,0)作為原點(左上角),(width,height)作為右下角的這麼一個區域,而我們的所有繪製操作都是在這個坐標系中完成的,比如之前Rectangle的繪製方法

drawShape(ctx){
ctx.rect(this.left,this.top,this.width,this.height);
}

就應該改成:

drawShape(ctx){
ctx.rect(0,0,this.width,this.height);
}

但這種修改的前提是要在繪製操作調用之前切換坐標系,而這個操作在Figure類里是沒有的,我們現在加上:

draw(ctx) {
ctx.save(); // 保存之前狀態,然後就可以任意修改了,最後恢復一下就可以保證在這個figure之外的其他地方狀態不變
ctx.translate(this.left, this.top); // 先切換到當前坐標系
ctx.globalAlpha = this.opacity; // 設置透明度
// 我們默認以中心旋轉,坐標系發生變化後,中心坐標要以(0,0)為左上角計算
let center = {x: this.width / 2, y: this.height / 2};
ctx.translate(center.x, center.y);
ctx.rotate(this.rotate * Math.PI / 180); // 把角度轉成弧度
ctx.translate(-center.x, -center.y);
this.drawSelf(ctx);
this.drawChildren(ctx); // 繪製完自己後再繪製子Figure
ctx.restore(); // 恢復到之前狀態
}

看上去好像使用哪個坐標系都差不多,實際上使用自身坐標系來繪製是最好的,這樣一來left和top並不干擾我們的實際繪製編碼,而且更符合Figure的定義,即left和top只是描述該Figure的位置而已,和怎麼去繪製這個Figure是沒有關係的,此外在後續的文章里會慢慢體現出它的好處。

同樣Figure的width和height其實也是只是描述該圖形的大小而已,實際上這兩個值在Figure子類實現繪製方法的時候並不能影響繪製結果,只能認為width和height這兩個屬性是一個約束值。這裡就要涉及到ctx.clip方法(區域剪切),以及bounds判斷等。這些以後再提

小結

這個Figure看上去很簡單,實際上是大多數2d的graph程序都是這樣設計的,而且是一個核心類。即使從canvas 2d遷移到webgl上,這個類也無需作為太多修改。

對此各位看官有什麼看法呢?歡迎到其他博主文章下留言。

作業

按照我給出的Figure類,設計一個專門繪製圖片的類,命名為FigureImage。(ctx有一個drawImage方法,根據這個方法來實現即可)

推薦閱讀:

查看原文 >>
相关文章