教你用canvas打造一個炫酷的碎片切圖效果

2022-10-24 12:01:32

前言

今天分享一個炫酷的碎片式切圖效果,這個其實在自己的之前的部落格上有實現過,本人覺得這個效果還是挺炫酷的,這次還是用我們的canvas來實現,程式碼量不多,但有些地方還是需要花點時間去理解的,需要點數學幾何理解能力,老規矩,我們還是先看效果再來看實現步驟。

如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新文章~

需求分析

從上面我們看到圖片在切換的時候其實是一個一個的小碎片慢慢從點選位置往外擴散開來,這一個個小碎片,在頁面中其實就是一個個的小方塊。這裡的難點在於如何將一張完整的圖片切割成一個一個的小方塊分別進行渲染,還有就是這個稜形圖案的位置確定。

  • 切割:這裡我們可以以座標系的形式來進行切割,每一個方塊都對應著它們自己在座標系中的位置(x, y)
  • 繪製:這裡的重點在於drawImage方法
  • 稜形擴散:這裡需要點數學幾何理解能力,後面作圖理解

實現過程

座標系

在實現之前,我們先來理解一個概念:座標系

注意:這裡所說的座標系不是我們數學中的座標系,但兩者又有些類似,不同點在於兩者的原點位置以及y軸的方向不同。

切割

這一步主要是為了確定每一個單元格的大小,單元格的長寬最好不要是最大公約數或最小公約數,因為過大效果不夠炫,過小效能會有壓力。

我這裡畫板長寬為 800 * 530 ,選取 16 * 15 為單元尺寸,即整個畫布由 50 * 35 共 1750 個單元格組成。切割分完單元格之後我們需要先計算一些基本的引數備用。

this.imgW = 800; // 圖片原始寬
this.imgH = 530; // 圖片原始高

this.conW = 800; // 畫布寬
this.conH = 530; //  畫布高

this.dw = 16; // 單元格寬
this.dh = 15; // 單元格高

this.I = this.conH / this.dh; //單元行數
this.J = this.conW / this.dw; // 單元列數

this.DW = this.imgW / this.J; // 原圖單元寬
this.DH = this.imgH / this.I; // 原圖單元高

行數 = 畫布高度 / 單元格高度;列數 = 畫面寬度 / 單元格寬度

繪製

本次繪製的重點在於drawImage這個方法,我們可以先來了解一下這個方法的引數及功能

drawImage

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

這個方法一共有9個引數,作用是在畫布上繪製影象。看到這麼多引數是不是已經被勸退了,哈哈

  • image:繪製到上下文的元素。允許任何的畫布影象源,例如:HTMLImageElementSVGImageElementHTMLVideoElementHTMLCanvasElementImageBitmapOffscreenCanvasVideoFrame
  • sx:(可選)需要繪製到目標上下文中的,image 的矩形(裁剪)選擇框的左上角 X 軸座標。可以使用 3 引數或 5 引數語法來省略這個引數。
  • s y:(可選)需要繪製到目標上下文中的,image 的矩形(裁剪)選擇框的左上角 Y 軸座標。可以使用 3 引數或 5 引數語法來省略這個引數。
  • sWidth:(可選)需要繪製到目標上下文中的,image 的矩形(裁剪)選擇框的寬度。如果不說明,整個矩形(裁剪)從座標的 sxsy 開始,到 image 的右下角結束。可以使用 3 引數或 5 引數語法來省略這個引數。使用負值將翻轉這個影象。
  • sHeight:(可選)需要繪製到目標上下文中的,image的矩形(裁剪)選擇框的高度。使用負值將翻轉這個影象。
  • dximage 的左上角在目標畫布上 X 軸座標。
  • dyimage 的左上角在目標畫布上 Y 軸座標。
  • dWidthimage 在目標畫布上繪製的寬度。允許對繪製的 image 進行縮放。如果不說明,在繪製時 image 寬度不會縮放。注意,這個引數不包含在 3 引數語法中。
  • dHeightimage 在目標畫布上繪製的高度。允許對繪製的 image 進行縮放。如果不說明,在繪製時 image 高度不會縮放。注意,這個引數不包含在 3 引數語法中。

這9個引數我們可以這樣來記憶,第一個引數是影象源,接下來的四個引數指的是原圖,最後四個引數指的是畫布

切割&渲染

這裡我們主要是將一張圖片切割成一個個的小碎片,是這些碎片拼起來就是一張完整的圖片。

class ChipBanner {
  constructor() {
    this.cvs = document.querySelector("#chip");
    this.ctx = this.cvs.getContext("2d");
    this.imgList = document.querySelectorAll(".bg");
    this.imgIndex = 0;
    this.isAnimating = false;

    this.imgW = 800; //圖片原始寬/高
    this.imgH = 530;

    this.conW = 800; //畫布寬/高
    this.conH = 530;

    this.dw = 16; //畫布單元寬/高
    this.dh = 15;

    this.I = this.conH / this.dh; //單元行/列數
    this.J = this.conW / this.dw;

    this.DW = this.imgW / this.J; //原圖單元寬/高
    this.DH = this.imgH / this.I;
  }

  init() {
    this.ctx.beginPath();

    for (let i = 0; i < this.I; i++) {
      for (let j = 0; j < this.J; j++) {
        this.chipDraw(this.imgList[this.imgIndex], i, j);
      }
    }

    this.ctx.closePath();
    this.ctx.stroke();
  }
  drawText() {
    this.ctx.font = "150px serif";
    this.ctx.strokeStyle = "white";
    this.ctx.strokeText("1024", 500, 500);
  }

  chipDraw(img, i, j) {
    this.drawText();
    //負責繪製,i: 單元行號;j: 單元列號
    this.ctx.drawImage(
      img,
      this.DW * j,
      this.DH * i,
      this.DW,
      this.DH,
      this.dw * j,
      this.dh * i,
      this.dw,
      this.dh
    );
  }
}

這裡正確拼出來看到的和正常圖片沒有任何區別

再來看一張拼錯的圖

剛開始幾何座標那裡沒寫對,拼出來就成這樣了,哈哈,看著就像動畫幀卡住的樣子。

動畫

這裡主要是要找出某個點周圍稜形範圍內的所有點的座標,然後在清除這些座標圖案的同時,開始繪製下一張圖片。

菱形線上的點與座標的 行號差值的絕對值 + 列號差值的絕對值 = 距離

找出座標稜形範圍內所有的點

countAround(i, j, dst) {
    let arr = [];
    for (let m = i - dst; m <= i + dst; m++) {
      for (let n = j - dst; n <= j + dst; n++) {
        if (
          Math.abs(m - i) + Math.abs(n - j) == dst &&
          m >= 0 &&
          n >= 0 &&
          m <= this.I - 1 &&
          n <= this.J - 1
        ) {
          arr.push({ x: m, y: n });
        }
      }
    }
    return arr;
  }

清除單元格畫布

chipClear(i, j) {
    this.ctx.clearRect(this.dw * j, this.dh * i, this.dw, this.dh);
}

合併&動畫

start(i, j) {
    if (this.isAnimating) return;

    this.isAnimating = true;

    this.imgIndex++;

    if (this.imgIndex > this.imgList.length - 1) this.imgIndex = 0;

    let _this = this,
      dst = 0,
      timer = setInterval(() => {
        let resArr = _this.countAround(i, j, dst);

        resArr.forEach((item) => {
          _this.chipClear(item.x, item.y);  // 清除單元格
          _this.chipDraw(_this.imgList[_this.imgIndex], item.x, item.y); // 繪製下一張圖片
        });

        if (!resArr.length) {
          clearInterval(timer);
          _this.isAnimating = false;
        }
        dst++;
      }, 30);
  }

大功告成,這樣就實現了一個炫酷的碎片式切圖效果了~

最後

喜歡的同學歡迎點個贊呀,想要檢視原始碼的同學快來公眾號回覆碎片吧~

原文首發地址點這裡,歡迎大家關注公眾號 「前端南玖」,如果你想進前端交流群一起學習,請點這裡

我是南玖,我們下期見!!!