1. 程式人生 > >canvas-座標系、圓角矩形、紋理、剪裁

canvas-座標系、圓角矩形、紋理、剪裁

座標系

畫布大小:由canvas標籤上設定的width,height決定,預設是 300 x 150,這也是畫布座標系x軸和y軸的最大值,超過這兩個值,則意味著超過了畫布大小,超過的部分自然不會生效。
canvas標籤大小:由css樣式的width,height決定,預設是畫布的大小。
特殊情況: 畫布大小和canvas標籤大小不相等的時候,畫布會被縮放到跟標籤大小一樣。縮放不是等比例的,並且縮放完成後,畫布的座標系不變。因此最好把canvas標籤的css大小和canvas畫布大小設定為一致。

預設畫布(300 * 150)

<canvas id="canvas1" style={{ width:
'100px', height: '100px' }}>
</canvas> 複製程式碼
ctx.moveTo(0, 0);
ctx.lineTo(300, 150);
ctx.stroke();
複製程式碼


可以看到從(0,0)到(300, 150)這條線是從左上角到右下角的,可見畫布被不等比例縮放到跟標籤一樣大,同時座標系還是畫布的大小:300 * 150。
當畫布較小,而canvas標籤比較大的時候,圖形就會被放大,變形變模糊:

<canvas id="canvas2" width={10} height={20} style={{ width: '100px', height: '100px
' }}>
</canvas> 複製程式碼
ctx.moveTo(0, 0);
ctx.lineTo(10, 20);
ctx.stroke();
複製程式碼

在2倍屏和3倍屏上,可把畫布大小設定成標籤大小的2倍和3倍,這樣可以實現1px細線的效果,同時使線條更細膩

<canvas id="canvas3" width={200} height={200} style={{ width: '100px', height: '100px' }}></canvas>
複製程式碼
ctx.moveTo(0, 0);
ctx.lineTo(200, 200);
ctx.stroke();
複製程式碼

圓角矩形

要實現圓角矩形,先來了解一下畫圓的api。

arc(x, y, r, sAngle, eAngle, counterclockwise)

引數 描述
x 圓的中心的 x 座標。
y 圓的中心的 y 座標。
x 圓的半徑。
sAngle 起始角,以弧度計。(弧的圓形的三點鐘位置是 0 度)。
eAngle 結束角,以弧度計。
counterclockwise 可選。規定應該逆時針還是順時針繪圖,預設值false。false = 順時針,true = 逆時針。

畫圓

<canvas id="canvas11" width={100} height={100}></canvas>
複製程式碼
ctx.arc(50, 50, 20, 0, 2 * Math.PI);
複製程式碼

圓弧

<canvas id="canvas10" width={100} height={100}></canvas>
複製程式碼
ctx.arc(20, 20, 20, Math.PI, 1.5 * Math.PI);    // 左上角
ctx.arc(80, 20, 20, 1.5 * Math.PI, 2 * Math.PI);    // 右上角
ctx.arc(20, 80, 20, 0.5 * Math.PI, Math.PI);    // 左下角
ctx.arc(80, 80, 20, 0, 0.5 * Math.PI);    // 右下角
複製程式碼

有了這四段圓弧,再利用 closePath方法會連線路徑的特點,即可畫出圓角矩形了。來封裝一個畫圓角矩形的函式:

const drawRoundedRect = (ctx, x, y, width, height, radius, type) => {
  ctx.moveTo(x, y + radius);
  ctx.beginPath();
  ctx.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI);
  ctx.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI);
  ctx.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI);
  ctx.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI);
  ctx.closePath();
  const method = type || 'stroke';  // 預設描邊,傳入fill即可填充矩形
  ctx[method]();
};

drawRoundedRect(ctx, 20, 20, 50, 50, 10);
複製程式碼

紋理

createPattern(img, "repeat|repeat-x|repeat-y|no-repeat")

引數 描述
img 規定要使用的圖片、畫布或視訊元素。
repeat 預設。該模式在水平和垂直方向重複。
repeat-x 該模式只在水平方向重複。
repeat-y 該模式只在垂直方向重複。
no-repeat 該模式只顯示一次(不重複)。
原圖

嘗試如下程式碼:

<canvas id="canvas13" width={400} height={300}></canvas>
複製程式碼
const img = new Image();
img.onload = () => {
console.log('catImg', img.width, img.height);
const pat = ctx.createPattern(img, 'no-repeat');
ctx.fillStyle = pat;
drawRoundedRect(ctx, 130, 30, 250, 260, 10, 'fill');
};
img.src = catImgSrc;
複製程式碼

從結果裡發現兩個問題:

  1. 圖片的左上角和矩形的左上角不在同一點上;
  2. 矩形貌似少了右邊和下邊的部分。

接下來嘗試修改程式碼裡的 no-repeatrepeat:

結合這兩個結果和上面的問題,總結出紋理的如下特點:

  1. 紋理是從畫布的(0,0)點開始無縮放渲染的;
  2. 設定no-repeat會導致那些沒有紋理覆蓋的區域是空白。

這意味著我們不能自由的使用紋理來實現圓角圖片的效果。 接下來了解一下canvas提供的專門用於圖片剪裁的api。

剪裁

剪裁分為畫布的剪裁clip和圖片的剪裁drawImage,先來介紹一下圖片的剪裁。

drawImage(img, sx, sy, swidth, sheight, x, y, width, height)

引數 描述
img 規定要使用的影象、畫布或視訊。
sx 可選。開始剪下的 x 座標位置。
sy 可選。開始剪下的 y 座標位置。
swidth 可選。被剪下影象的寬度。
sheight 可選。被剪下影象的高度。
x 在畫布上放置影象的 x 座標位置。
y 在畫布上放置影象的 y 座標位置。
width 可選。要使用的影象的寬度。(伸展或縮小影象)
height 可選。要使用的影象的高度。(伸展或縮小影象)

drawImage可接受3、5、9個引數。 接下來介紹一下分別傳這幾個引數的表現,先看原圖:
大小:1080 * 720

三個引數

會被當做:img、x、y。圖片不會縮放和剪裁,直接渲染到畫布上,超過畫布的區域被隱藏,也可理解成超過畫布的區域被剪掉了。

    const img = new Image();
    img.onload = () => {
      console.log('img:', img.width, img.height);
      ctx.drawImage(img, 10, 20);
    };
    img.src = imgSrc;
複製程式碼

九個引數

按照上面表格的定義進行剪裁和縮放。

const img = new Image();
img.onload = () => {
  ctx.drawImage(img, 0, 0, 500, 500, 30, 30, 150, 150);
};
img.src = imgSrc;
複製程式碼

五個引數

img、x、y、width、height,此時圖片不會被剪裁,而是直接縮放到目標寬高,且是不等比例的。

const img = new Image();
img.onload = () => {
  ctx.drawImage(img, 0, 0, 150, 150);
};
img.src = imgSrc;
複製程式碼

接下來傳入9個引數,剪裁圖片的寬高等於圖片的寬高,來驗證和5個引數是一樣的效果:

const img = new Image();
img.onload = () => {
  ctx.drawImage(img, 0, 0, 1080, 720, 0, 0, 150, 150);
};
img.src = imgSrc;
複製程式碼

另外,從原圖上剪裁的時候,不要超過圖片的寬高,否則會出現空白。
drawImage這個api能實現把圖片剪裁成直角矩形,卻不能實現圓角的效果。而上面的紋理方法,能實現圓角圖片,但效果不是十分理想,畢竟它是用來做紋理的,而不是圖片剪裁。接下來再瞭解一個api: clip,通過它配合drawImage,能實現把圖片剪裁成圓角矩形、圓形、甚至任意形狀。

clip()

clip()方法就是把畫布中的某個區域臨時剪切出來,剪下之前要定義這個區域的路徑。剪下以後,所有的繪製只有落在這個區域裡才會生效,在這個區域外的不會生效。之所以說“臨時”,是因為如果在剪下之前呼叫了save()方法,則畫布狀態會被儲存下來,之後呼叫restore()方法即可恢復之前的狀態,即 clip 的那個區域的限制不再繼續生效,而之前落在區域外的繪製也不會因為 restore 而被繪製出來。 嘗試如下程式碼:

<canvas id="canvas15" width={150} height={150}></canvas>
複製程式碼
// 定義一個區域
drawRoundedRect(ctx, 20, 20, 100, 100, 10);
    
const img = new Image();
img.onload = () => {
  ctx.save();
  ctx.clip();
  ctx.drawImage(img, 0, 0, 100, 100);
};
img.src = imgSrc;
複製程式碼

效果:

看到這個效果很興奮,接下來只要把圖片的目標區域先從畫布上剪下下來,再呼叫drawImage去繪製圖片,則圖片就會變成想要的形狀。至於原始圖片,則可以通過 drawImage先剪裁一個想要的區域,再進行繪製。
從原圖上剪裁出一部分,再繪製成兩個圓角的圖片:

<canvas id="canvas16" width={300} height={200}></canvas>
複製程式碼
const img = new Image();
img.onload = () => {
  ctx.save();

  ctx.strokeStyle = '#fff';
  drawRoundedRect(ctx, 20, 20, 100, 100, 10);
  ctx.clip();
  ctx.drawImage(img, 500, 100, 500, 500, 20, 20, 100, 100);
  ctx.restore();

  ctx.strokeStyle = '#fff';
  drawRoundedRect(ctx, 150, 20, 100, 100, 5);
  ctx.clip();
  ctx.drawImage(img, 500, 100, 500, 500, 150, 20, 100, 100);
};
img.src = imgSrc;
複製程式碼

效果:

接下來封裝一個能實現圓角功能的 drawImage:

const drawRoundedImage = (ctx, radius, img, sx, sy, swidth, sheight, x, y, width, height) => {
  ctx.save();
  ctx.moveTo(x, y + radius);
  ctx.beginPath();
  if (width === height && radius >= width / 2) {
    ctx.arc(x + radius, y + radius, radius, 0, 2 * Math.PI);
  } else {
    ctx.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI);
    ctx.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI);
    ctx.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI);
    ctx.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI);
  }
  ctx.closePath();
  ctx.clip();
  ctx.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);
  ctx.restore();
};

const img = new Image();
img.onload = () => {
  drawRoundedImage(ctx, 10, img, (1080 - 720) / 2, 0, 720, 720, 10, 10, 180, 180);
};
img.src = imgSrc;
複製程式碼

效果:

如果把上面的繪製路徑部分提出來當成引數傳入,則可實現使用者自定義圖形然後將圖片剪裁成該形狀的功能:

const drawImageToWhatYouWant = (ctx, getPath, img, sx, sy, swidth, sheight, x, y, width, height) => {
  ctx.save();
  getPath(ctx);  // 自定義圖形的路徑
  ctx.clip();
  ctx.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);
  ctx.restore();
};
複製程式碼

儲存圖片

toDataURL([type, encoderOptions])

引數 描述
type 圖片格式,預設為image/png
encoderOptions 在指定圖片格式為 image/jpeg 或 image/webp的情況下,可以從 0 到 1 的區間內選擇圖片的質量。如果超出取值範圍,將會使用預設值 0.92。其他引數會被忽略。
呼叫:
const imgStr = canvas.toDataURL("image/jpeg", 1.0);
複製程式碼

注意:當畫布中包含圖片時,此圖片必須是允許跨域的,否則呼叫toDataURL 會報錯!

檢視完整程式碼示例:github.com/vivimee/lea…