使用canvas的過程中,使用 drawImage 可以繪製圖片。但有時候,需要將一張本來方正的圖片,繪製到一個平行四邊形上面去。如下圖所示。

繪製一個變形後的圖片

大體的思路是,先對canvas坐標系使用一定的坐標變換,然後再使用 drawImage 繪製圖片,即可獲得以上圖片。

常用坐標變換

常規的變換有3種,平移,縮放,旋轉,原生的canvas已經包含了相應的一些方法。

為了方便觀察,先繪製上一個網格(其中, WIDTH 和 HEIGHT 分別為canvas的寬度與高度)

function drawGrids() {
drawGrid(x);
drawGrid(y);
}

function drawGrid(direction) {
var i, maxLength, step = 100;
ctx.strokeStyle = rgba(0,0,0,0.3);
if (direction == x) {
maxLength = HEIGHT;
} else if (direction == y) {
maxLength = WIDTH;
}
ctx.beginPath();
for (i = 0; i < maxLength; i += step) {
if (direction == x) {
ctx.moveTo(0, i);
ctx.lineTo(WIDTH, i);
} else if (direction == y) {
ctx.moveTo(i, 0);
ctx.lineTo(i, HEIGHT);
}
}
ctx.stroke();
ctx.closePath();
}

然後繪製一個圖片,

ctx.save();
ctx.lineWidth = 2;
ctx.strokeStyle = blue;
ctx.drawImage(image, 200, 200, width, height);
ctx.strokeRect(200, 200, width, height);
ctx.restore();

原圖

平移

context.translate(x, y);

各個參數含義和作用如下:

  • x 坐標系水平位移的距離。
  • y 坐標系垂直位移的距離。

平移坐標系,繪製一個圖片(為了便於對比觀察,將原圖也畫上,原圖沒有邊框)

ctx.save();
ctx.drawImage(image, 200, 200, width, height);
ctx.translate(100, 100);
ctx.lineWidth = 2;
ctx.strokeStyle = blue;
ctx.drawImage(image, 200, 200, width, height);
ctx.strokeRect(200, 200, width, height);
ctx.restore();

縮放

context.scale(x, y);

各個參數含義和作用如下:

  • x 坐標系水平縮放的比例。支持小數,如果值是-1,表示水平翻轉。
  • y 坐標系垂直縮放的比例。支持小數,如果值是-1,表示垂直翻轉。

ctx.save();
ctx.drawImage(image, 200, 200, width, height);
ctx.scale(2, 2);
ctx.lineWidth = 2;
ctx.strokeStyle = blue;
ctx.drawImage(image, 200, 200, width, height);
ctx.strokeRect(200, 200, width, height);
ctx.restore();

縮放

值得注意的是,原圖中,圖片的左上角在(200,200)這個位置,但是放大之後,圖片的左上角在(400,400)處。這是因為,縮放操作是都是相對於原點來計算的。

旋轉

context.rotate(angle);

各個參數含義和作用如下:

  • angle 坐標系旋轉的角度,單位是弧度。

ctx.save();
ctx.drawImage(image, 200, 200, width, height);
ctx.translate(100, 100);
ctx.lineWidth = 2;
ctx.strokeStyle = blue;
ctx.drawImage(image, 200, 200, width, height);
ctx.strokeRect(200, 200, width, height);
ctx.restore();

旋轉

與上方的縮放類似,旋轉操作後,不以原點為起點的圖片在繪製的過程中,起點位置都發生了改變。

鏡像

前面在將縮放的時候有提到,如果值是-1,則可以起到翻轉的效果。(為了便於觀察,在繪製前先進行了平移操作,藍框為水平翻轉,紅框為垂直翻轉)

ctx.save();
ctx.translate(400, 300);
ctx.drawImage(image, 100, 100, width, height);
ctx.lineWidth = 2;
ctx.strokeStyle = blue;

ctx.save();
ctx.scale(-1, 1);
ctx.drawImage(image, 100, 100, width, height);
ctx.strokeRect(100, 100, width, height);
ctx.restore();

ctx.save();
ctx.scale(1, -1);
ctx.strokeStyle = red;
ctx.drawImage(image, 100, 100, width, height);
ctx.strokeRect(100, 100, width, height);
ctx.restore();

ctx.restore();

鏡像

自定義的坐標變換

自定義坐標變換包括兩個方法:transform 和 setTransform。這兩者大體相同, transform 方法和 setTransform 方法的區別在於,後者一旦執行會完全重置已有的變換, transform 方法則是累加。

context.transform(a, b, c, d, e, f);

各個參數含義和作用如下:

  • a 水平縮放
  • b 水平斜切
  • c 垂直斜切
  • d 垂直縮放
  • e 水平位移
  • f 垂直位移

a ~ f 這些參數對應的變換矩陣描述為: egin{bmatrix} a & c & e\b & d & f\ 0 & 0&1 end{bmatrix}quad

transform 和 setTransform 的優點是能實現可能的任何效果,缺點則是不夠直觀。上述的translate(),scale(),rotate(),這三個方法,都可以使用的 transform 方法來實現。

ctx.translate(x, y);
ctx.transform(1, 0, 0, 1, x, y);

ctx.scale(x, y);
ctx.transform(x, 0, 0, y, 0, 0);

ctx.rotate(angle);
sin = Math.sin(angle);
cos = Math.cos(angle);
ctx.transform(cos, sin, -sin, cos, 0, 0);

接下來,利用 transform 來畫出最上面的效果。

先找出需要變形成的平行四邊形的幾個頂點,然後畫出邊框

var points = [{
x: 200,
y: 400,
},
{
x: 500,
y: 300,
},
{
x: 600,
y: 400,
},
{
x: 300,
y: 500,
},
];

function drawPath(points) {
var p0 = points[0],
p1 = points[1],
p2 = points[2],
p3 = points[3];
ctx.save();
ctx.beginPath();
ctx.strokeStyle = #e00;
ctx.moveTo(p0.x, p0.y);
ctx.lineTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.lineTo(p3.x, p3.y);
ctx.lineTo(p0.x, p0.y);
ctx.stroke();
ctx.closePath();
ctx.restore();
}

邊框

以左上角為起點,先畫一個寬高均為100的圖

寬高均為100

然後水平放大3倍

水平放大3倍

然後再水平斜切

水平斜切

最後再垂直斜切

垂直斜切

即可完成需求。

代碼實現如下(注釋中包含了分析)

function drawImageInPoints(ctx, image, points) {
var xSize, ySize, xTan, yTan,
width = 100,
height = 100,
transform = {},
p0 = points[0],
p1 = points[1],
p3 = points[3];

// 根據圖片左上角的坐標 設置e和f
transform.e = p0.x;
transform.f = p0.y;

// 根據進行斜切之前的矩形和圖片尺寸的大小關係 設置a和d
xSize = p1.x - p0.x;
transform.a = xSize / width;
ySize = p3.y - p0.y;
transform.d = ySize / height;

// 根據圖片是否需要斜切,來設置 設置b和d
// 水平斜切,水平線旋轉到目標線的角度的正切值,角度以順時針旋轉為正
// 垂直斜切,垂直線旋轉到目標線的角度的正切值,角度以逆時針旋轉為正
if (p1.x == p0.x) {
transform.b = 0;
console.log(不應該相等);
} else {
xTan = (p1.y - p0.y) / (p1.x - p0.x);
transform.b = xTan * transform.a;
}

if (p3.y == p0.y) {
transform.c = 0;
console.log(不應該相等);
} else {
yTan = (p3.x - p0.x) / (p3.y - p0.y);
transform.c = yTan * transform.d;
}

ctx.save();
ctx.transform(transform.a, transform.b, transform.c, transform.d, transform.e, transform.f);
ctx.strokeStyle = #6cf;
ctx.drawImage(image, 0, 0, width, height);
ctx.restore();

}

這裡需要注意的一點為:points為一個數組,長度為4,每個數組元素表示一個(x,y)坐標,數組的順序為特定的順序,第一個點為左上角的點,剩下的點以順時針方向添加。若4個點不能構成一個平行四邊形,則會認作是第一個點,第三個點,第四個點組成的平行四邊形,第三個傳入的點坐標可以為null。


推薦閱讀:
相關文章