最近工做中須要製做一個圖片預覽的插件,在參考了不少產品(掘金、知乎、簡書、石墨等)的圖片預覽以後,最終仍是以爲石墨的比較符合咱們的產品需求。javascript
原本覺得能在社區中找到相關插件,但想法美好,現實卻很骨感,因而便決定本身手擼一個,順便也學習一下組件的開發流程。html
項目最終的實現效果以下圖,基本上跟石墨的圖片預覽是一毛同樣的。支持 放大圖片、縮小圖片、原尺寸大小顯示、圖片適應屏幕、下載圖片(這個還在開發中),也就是底部欄的五個操做按鈕。java
組件是基於 React Hooks
和 TypeScript
實現的,打包工具使用的是 webpack
。node
本篇文章對
webpack
的配置不會作相應的介紹,若是你們對webpack
感興趣的話,能夠參考筆者整理的 淺談 Webpack 性能優化(內附巨詳細 Webpack 學習筆記)。react
.
├── node_modules // 第三方的依賴
├── config // webpack 配置文件夾
├── webpack.base.js // webpack 公共配置文件
├── webpack.dev.config.js // 開發環境配置文件
└── webpack.prod.config.js // 生產環境配置文件
├── example // 開發時預覽代碼
├── src // 示例代碼目錄
├── app.js // 測試項目 入口 js 文件
└── index.less // 測試項目 入口 樣式文件 文件
├── src // 組件源代碼目錄
├── components // 輪子的目錄
├── photoGallery // photoGallery 組件文件夾
├── types // typescripe 的接口定義
├── utils // 工具函數目錄
├── images // 圖片文件目錄
├── index.html // 項目入口模版文件
├── index.tsx // 項目入口文件
└── index.less // 項目入口樣式文件
├── lib // 組件打包結果目錄
├── .babelrc // babel 配置文件
├── .gitignore // git上傳時忽略的文件
├── .npmignore // npm 上傳忽略文件
├── README.md
├── tslint.json // tslint 的配置文件
├── tsconfig.json // ts 的配置文件
├── package-lock.json // yarn lock 文件
└── package.json // 當前整一個項目的依賴
複製代碼
倉庫地址在此:仿石墨的圖片預覽插件。webpack
此插件的核心在於圖片的展現,以及圍繞對預覽圖片進行的操做,如 放大、縮小、適應屏幕,而這幾個操做又都是跟圖片的尺寸有關的,其實咱們只要知道在點擊相應操做的按鈕的時候,圖片應該顯示多大的尺寸,整個問題就解決了。git
因而筆者就研究了一波其背後預覽邏輯,發現了幾個對編碼比較有用的點:github
首先圖片不能一直放大和縮小,它一定有一個最大值和最小值,操做了一波發現石墨中 預覽圖片的最大值是原圖的 4 倍、最小值是原圖的 10 倍,與此同時還須要規定從原圖開始點擊幾回到最大值或者最小值,在插件中我規定的次數是 6
次。web
這樣在圖片加載完成以後,咱們能很方便的算出這張預覽圖片的全部尺寸,能夠將這些尺寸維護在一個數組中,這樣在每個放大縮小的點擊背後,都會有一個圖片尺寸與其對應。算法
接着咱們須要知道的是當前預覽圖片的顯示尺寸位於 尺寸數組 中的哪個 index
,有了這個 index
以後,咱們就只須要取出這個 index
對應的圖片寬度進行繪製便可。
這裏就涉及到圖片首次在容器中的顯示狀況了,咱們拿長圖舉例:長圖預覽,插件會在圖片上下兩側留出必定的間距,這個間距實際上是固定的,在石墨中我算了一下,上下兩側留出的間隙各是容器高度的 5%
,具體能夠看下圖(原諒圖片很魔性),圖中 A
的距離是 B
的 5%
。
這樣咱們能夠計算出當前圖片的尺寸,拿這個尺寸去 尺寸數組 中找到與這個值最爲接近的值,這個最接近的值的索引就是當前預覽圖片的 index
值。
還有一個石墨的預覽圖片是經過 canvas
畫上去的,咱們這裏也會使用 canvas
的 drawImage
這個 api
來進行圖片的繪製,固然在不支持 canvas
的瀏覽器上,咱們就直接使用 <img />
標籤。
在本文就主要分析
canvas
畫圖這一塊內容,<img />
標籤其實也是相似的。
到這裏基本上此插件的難點都已經解決了,接下來咱們就開始分析相應的代碼。
首先咱們來看一下插件的所須要的參數,大體能夠歸爲下面幾個:
visible
:控制預覽插件的顯示隱藏imgData
:須要預覽的圖片數組currentImg
:再打開預覽插件的時候,默認顯示第幾張圖hideModal
:預覽插件的關閉方法筆者能想到的暫時就這四個,基本上其實也已經夠用了,使用以下:
<PhotoGallery
visible={visible}
imgData={ImgData}
currentImg = {9}
hideModal={
() => {
setVisible(false);
}
}
/>
複製代碼
插件的結構其實很簡單,其實就三塊:圖片顯示塊、圖片列表選擇側邊欄、底部操做塊,定義爲三個子組件塊:分別爲 <Canvas />
、<Sidebar />
、<Footer />
,統一由一個父組件管理。
由於咱們主要講解
canvas
畫圖片,因此圖片顯示塊就設置爲<Canvas />
,不支持的canvas
的瀏覽器,在源碼中會使用<Image />
組件來進行圖片展現,這裏就不作具體介紹了,你們能夠參考源碼。
父組件代碼以下:
// src/components/photoGallery/index.tsx
import React, { useState } from 'react';
import classNames from 'classnames';
import { Footer, Sidebar, Canvas } from './components';
const photoGallery = (props: Props): JSX.Element => {
const { imgData, currentImg, visible } = props;
// 當前顯示第幾張圖片
const [currentImgIndex, setCurrentImgIndex] = useState(currentImg);
return (
<div
className={
classNames(
styles.modalWrapper,
{
[styles.showImgGallery]: visible, // 根據 visible 渲染插件
}
)
}
>
<div className={styles.contentWrapper}>
<Canvas
// 要加載的圖片 url
imgUrl={imgUrl}
/>
</div>
<Sidebar
// 圖片數組
imgData={imgData}
/>
<Footer
// 圖片數量
imgsLens={imgData.length}
// 當前第幾張
currentImgIndex={currentImgIndex}
/>
</div>
);
}
複製代碼
如上圖所示,這樣插件的大體的結構就算完成了,接下來就是最核心的圖片顯示模塊的邏輯。
咱們先建立一個類 canvas.ts
,對於圖片的預覽操做,咱們都在這個類中進行操做。
這個類接受兩個參數,一個是渲染的容器 dom
,另一個就是實例化所須要用到的參數 options
,下面是 options
的接口實現:
interface CanvasOptions {
imgUrl: string; // 圖片地址
winWidth: number; // 屏幕寬度
winHeight: number; // 屏幕高度
canUseCanvas: boolean; // 瀏覽器是否可使用 canUseCanvas
loadingComplete?(instance: any): void; // 製做圖片 loading 效果
}
複製代碼
還有咱們會講一系列跟預覽圖片有關的屬性都掛在其實例屬性上,如:
el
:渲染的容器canUseCanvas
:是否支持 canvas
,決定以什麼方式畫圖context
:canvas
的畫布 getContext('2d')
image
:預覽圖片對象imgUrl
:預覽圖片 url
imgTop
:圖片右上角目標 canvas
中 y
軸的高度imgLeft
:圖片右上角目標 canvas
中 x
軸的高度LongImgTop
:圖片距離容器頂部的距離,用於圖片滾動和拖動LongImgLeft
:圖片距離容器左側的距離,用於圖片滾動和拖動sidebarWidth
:側邊欄的寬度footerHeight
:底部欄的高度cImgWidth
:畫布中圖片的寬度cImgHeight
:畫布中圖片的高度winWidth
:屏幕的寬度winHeight
:屏幕的高度curPos
:鼠標拖動圖片是須要用的的 x/y
值curScaleIndex
:當前顯示圖片,位於尺寸數組中的哪個 index
fixScreenSize
:使用屏幕大小的尺寸數組中的 index
值EachSizeWidthArray
:圖片的尺寸數組,包含了放大縮小全部尺寸的寬度值isDoCallback
:圖片是否加載完成插件中使用的屬性值基本上都在上面了。
首先咱們先來看一下這個 canvas
畫圖的這個 api
,它能幫助咱們在畫布上繪製圖像、畫布或視頻。
咱們能夠經過下面的方法放大來幫咱們畫出一張圖片:
var c = document.getElementById("myCanvas");
// 建立畫布
var ctx = c.getContext("2d");
// 開始繪製
ctx.drawImage(image, dx, dy, dWidth, dHeight);
複製代碼
其中參數的意思分別爲:
image
:規定要使用的圖像、畫布或視頻。dx
:image
的左上角在目標 canvas
上 X
軸座標dy
:image
的左上角在目標 canvas
上 y
軸座標dWidth
:image
在目標 canvas
上繪製的寬度。dHeight
:image
在目標 canvas
上繪製的高度。具體能夠看下圖:
關於此方法更多用法你們能夠參考:drawImage 的 MDN 文檔。
有了這個 api
以後,咱們其實只要計算出這個 api
對應的 5 個參數便可,舉個簡單的例子,下面這張圖咱們改怎麼獲得 5
個參數:
image
對象咱們可使用 new Image()
來實例化一個 image
對象,並指定他的 src
屬性爲相應的圖片 url
地址,這樣就能夠獲得一個 image
對象,當圖片加載完成以後,咱們就能夠經過 imgDom.naturalWidth
和 imgDom.naturalHeight
圖片的原始寬高:
// src/components/photoGallery/canvas.ts
loadimg(imgurl) {
const imgDom = new Image();
imgDom.src = imgUrl;
imgDom.onload = function() {
// 圖片加載完成以後
// 作你想要作的事情
}
}
複製代碼
dx
與 dy
、dwidth
與 dHeight
屬性咱們以長圖舉例:咱們在講解思路的時候分析過,上下兩邊留空的部分是 圖片顯示容器高度 的 5%
,在這裏咱們定義了底部塊的高度(footerHeight
)爲 50px
,側邊欄的寬度(sidebarWidth
)爲 120px
,這就變成了一道小學應用題,咱們能夠經過 window.innerWidth
和 window.innerHeight
來獲得屏幕的寬(winWidth
)和高(winHeight
),通過計算咱們即可以獲得咱們所需的四個屬性:
/** * winWidth:屏幕寬度 * winHeight:屏幕高度 * footerHeight:底部高度 * sidebarWidth:側邊欄寬度 * wrapperWidth:圖片顯示區域寬度 * wrapperHeight:圖片顯示區域高度 * naturalWidth: 圖片原始寬度 * naturalHeight: 圖片原始高度 */
wrapperHeight = winHeight - footerHeight;
wrapperWidth = winWidth - sidebarWidth;
dy = wrapperHeight * 0.05;
dHeight = wrapperHeight - 2 * dy;
// 與原始寬高有個等比例的關係
dWidth = naturalWidth * dHeight / naturalHeight;
dx = (wrapperWidth - dWidth) / 2
複製代碼
上面就是計算咱們所需五個屬性的過程,總的來講仍是比較方便的。
因此在咱們每次要繪製圖片的時候,只要計算出這 5
個值就 ok
了。
咱們在 utils
下的 img.ts
中定義一個方法 getBoundingClientRect
,用來獲得 圖片的顯示寬高和他距離容器頂部的 imgTop
、以及距離左側的 imgLeft
。
// src/utils/img.ts
/** * 返回第一次加載圖片的寬高,和 imgTop/imgLeft * 經過返回的參數 直接 經過 drawImage 畫圖了 **/
export const getBoundingClientRect = (options: RectWidth): BoundingClientRect => {
const {
naturalWidth, // 圖片原始寬
naturalHeight, // 圖片原始高
wrapperWidth, // 顯示容器寬
wrapperHeight, // 顯示容器高
winWidth, // 屏幕寬度
} = options;
// 圖片寬高比
const imageRadio = naturalWidth / naturalHeight;
// 顯示容器寬高比
const wrapperRadio = wrapperWidth / wrapperHeight;
// 長圖的邏輯
if (imageRadio <= 1) {
// 具體畫布上方默認是 容器高度的 0.05
imgTop = wrapperHeight * 0.05;
// 圖片的高度
ImgHeight = wrapperHeight - wrapperHeight * 0.05 * 2;
// 根據原始寬高,等比例獲得圖片寬度
ImgWidth = ImgHeight * naturalWidth / naturalHeight;
// 若是圖片的寬高比顯示容器的寬高比大
// 說明圖片左右兩側的寬度須要固定爲容器的寬度的 0.05 倍了
if (wrapperRadio <= imageRadio) {
ImgWidth = wrapperWidth - wrapperWidth * 0.05 * 2;
ImgHeight = ImgWidth * naturalHeight / naturalWidth;
imgTop = (wrapperHeight - ImgHeight) / 2
}
// ...
imgLeft = newWinWidth - ImgWidth / 2;
}
// 處理寬圖的邏輯
// ...
// 返回
return {
imgLeft,
imgTop,
ImgWidth,
ImgHeight,
}
}
複製代碼
更詳細的代碼你們能夠參考源碼。
咱們在以前提到,咱們能夠把圖片放大縮小過程當中全部的尺寸都放到一個數組中去,方便以後經過索引去獲得相應的圖片尺寸,那麼怎麼進行操做呢?
其實只要在圖片加載完成以後,獲得圖片的原始寬高,經過原始寬高,經過相應的計算公式,計算獲得相應的尺寸數組,塞入數組便可。
在類中定義一個 setEachSizeArr
實例方法:
// src/components/photoGallery/canvas.ts
/** * 計算圖片放大、縮小各尺寸的大小數組, */
private setEachSizeArr () {
const image = this.image;
// 獲得尺寸數組
const EachSizeWidthArray: number[] = getEachSizeWidthArray({
naturalWidth: image.width,
naturalHeight: image.height,
})
// 掛到實例屬性上去
this.EachSizeWidthArray = EachSizeWidthArray;
// 獲得適應屏幕的 index
// 也就是操做按鈕中的 第四個按鈕
const fixScreenSize = getFixScreenIndex({
naturalWidth: image.width,
naturalHeight: image.height,
wrapperWidth: this.cWidth,
wrapperHeight: this.cHeight,
}, EachSizeWidthArray);
// 將適應屏幕的 index 掛到實例屬性
this.fixScreenSize = fixScreenSize;
}
複製代碼
getEachSizeWidthArray
咱們經過此方法獲得尺寸數組,由於最大的圖片是原圖的 4 倍,最小的圖片是原圖的 1/10
,從最小到原圖 和 從原圖到最大 都須要通過 6
次,咱們能夠根據比例得出每個尺寸的大小,具體的代碼筆者就不貼了。
getFixScreenIndex
咱們經過此方法獲得適應屏幕的尺寸數組的 index
,原理就是在尺寸數組中第一個寬高小於顯示容器寬高的 index
。
這兩個方法的具體代碼筆者就不貼了,你們有興趣能夠去源碼查看。
咱們要計算出首次圖片渲染出來時候,位於尺寸數組的那一個 index
,由於咱們獲得首次渲染圖片的寬度,能夠拿這個寬度去與尺寸數組中數組進行比對,最接近的這個值的索引 index
,就是當前圖片的 index
值:
// src/components/photoGallery/canvas.ts
/** * 設置當前 EachSizeWidthArray 的索引,用於 放大縮小 */
private setCurScaleIndex() {
const cImgWidth = this.cImgWidth || this.image.width;
const EachSizeWidthArray = this.EachSizeWidthArray;
const curScaleIndex = getCurImgIndex(EachSizeWidthArray, cImgWidth);
this.curScaleIndex = curScaleIndex;
}
複製代碼
getCurImgIndex
咱們經過此方法來獲得當前圖片款的索引值,他是根據當前渲染的圖片寬度,去 尺寸數組 取出最接近預覽圖片寬度,從而獲得當前圖片的 index
,具體實現你們能夠參考源碼。
放大預覽的邏輯實際上就是根據放大以後的尺寸,計算出當前圖片的距離 canvas
頂部的高度 imgTop
、以及距離左側 canvas
的 imgLeft
。
前面咱們已經獲得首次圖片展現索引了,當咱們點擊放大的時候,無非就是將當前索引值加一,縮小就是減一。
咱們能夠根據新的索引值去 尺寸數組 中取出對應索引的寬度,經過圖片原始寬高,能夠等比例獲得當前應該顯示的寬高,最後咱們只須要計算出,放大後的圖片的 imgTop
和 imgLeft
的值,其實就能實現功能了:
/** * 修改當前 圖片大小數組中的 索引 * @param curSizeIndex : */
public changeCurSizeIndex(curSizeIndex: number) {
let curScaleIndex = curSizeIndex;
if (curScaleIndex > 12) curScaleIndex = 12;
if (curScaleIndex < 0) curScaleIndex = 0;
// 畫布寬高,即顯示容器寬高
const cWidth = this.cWidth;
const cHeight = this.cHeight;
// 上一次的索引
const prevScaleTimes = this.curScaleIndex;
// 尺寸數組
const EachSizeWidthArray = this.EachSizeWidthArray;
let scaleRadio = 1;
// 這一次寬度與上一次的比值
// 經過這個值能更方便的獲得圖片寬高
scaleRadio = EachSizeWidthArray[curScaleIndex] / EachSizeWidthArray[prevScaleTimes];
// 當前圖片寬高
this.cImgHeight = this.cImgHeight * scaleRadio;
this.cImgWidth = this.cImgWidth * scaleRadio;
// 獲得最新的 imgTop
// imgTop 值正負值是根據畫布左上角的點,向下爲正
this.imgTop = cHeight / 2 - (cHeight / 2 - this.imgTop) * scaleRadio;
// 設置當前 索引值
this.curScaleIndex = curScaleIndex;
// 若是圖片沒有超過畫布的寬和高
if (this.cImgHeight < cHeight && this.cImgWidth < cWidth) {
this.imgTop = (cHeight - this.cImgHeight) / 2;
}
// imgLeft 的計算
this.imgLeft = cWidth / 2 - this.cImgWidth / 2;
// 在圖片滑動的時候或者拖動的時候須要用到
this.LongImgTop = this.imgTop;
this.LongImgLeft = this.imgLeft;
// 繪製圖片
// ...
}
複製代碼
在 canvas
中進行圖片滾動,其實就是從新計算圖片的 imgTop
和 imgLeft
,而後對其進行從新繪製。
這裏咱們使用滾輪事件 onWheel
來計算滾動的距離 ,經過事件對象 event
上的 deltaX
和 deltaY
獲得的在 x/y
軸上的滾動距離。
這裏須要注意的一個點是對邊界值的處理,
imgTop
不能無止境的大和小,其最大不能超過咱們以前規定的LONG_IMG_TOP
這個值,咱們設置的是10px
,最小能夠參照下面的計算方式(寬度的邊界值計算相似,就不作介紹了)/** * minImgTop:最小的 imgTop 值 * maxImgTop:最大的 imgTop 值 * imgHeight:圖片高度 * winHeight:屏幕高度 * footerHeight:底部操做欄高度 * LONG_IMG_TOP:咱們設置的一個上下常量 padding */ // 最小確定是負數 minImgTop = -(imgHeight - (winHeight - footerHeight - LONG_IMG_TOP)) // 最大 maxImgTop = LONG_IMG_TOP 複製代碼
接下來咱們在 canvas
類中定義一個 WheelUpdate
事例方法,暴露出去給外部調用,
// src/components/photoGallery/canvas.ts
/** * 滾輪事件 * @param e wheel 的事件參數 */
public WheelUpdate(e: any) {
// ...
// 圖片顯示容器的寬高
const cWidth = this.cWidth;
const cHeight = this.cHeight;
// 若是圖片的寬高都小於圖片顯示容器的寬高就直接返回
if (this.cImgHeight < cHeight && this.cImgWidth < cWidth) {
return;
}
// 若是圖片的高度 大於 顯示容器的 高度
// 則容許 在 Y 方向上 滑動
if (this.cImgHeight > cHeight) {
// 此值保存當前圖片距離容器 imgTop
this.LongImgTop = this.LongImgTop - e.deltaY;
// e.deltaY 向下
if (e.deltaY > 0) {
// 這裏作一個極限值的判斷
// 具體是咱們的算法
if ((-this.LongImgTop) > this.cImgHeight + LONG_IMG_TOP - window.innerHeight + this.footerHeight) {
this.LongImgTop = -(this.cImgHeight + LONG_IMG_TOP - window.innerHeight + this.footerHeight);
}
} else {
// 往上滑的時候,最大值是兼容值 LONG_IMG_TOP
if (this.LongImgTop > LONG_IMG_TOP) {
this.LongImgTop = LONG_IMG_TOP;
}
}
}
// 處理 x 軸上的滾動
// ...
// 賦值 imgTop,imgLeft
this.imgTop = this.LongImgTop;
this.imgLeft = this.LongImgLeft;
// 繪製圖片
// ...
}
複製代碼
圖片拖動的咱們須要藉助 onMouseDown
、onMouseMove
、onMouseUp
三個事件函數。其實操做方式可圖片滾動相似,咱們須要計算出新的 imgTop
和 imgLeft
去從新繪製圖片,可是咱們不能經過 event
下面直接獲得拖動的值了,須要經過後一次與前一次的差值,來得出拖動的距離,進而計算出 imgTop
和 imgLeft
值,
首先咱們把圖片拖動過程當中的實時座標掛在實例屬性 curPos
上,在 onMouseDown
的時候進行初始座標賦值,這樣在 onMouseMove
函數中咱們就能獲得鼠標按下的初始座標了。
// src/components/photoGallery/index.tsx
/** * 鼠標按下事件 * @param e * @param instance : 圖片預覽的實例 */
const MouseDown = (e: any, instance: any) => {
// 全局 moveFlag 表示拖動是否開始
moveFlag = true;
const { clientX, clientY } = e;
// 給當前預覽實例設置初始 x、y 座標
instance.curPos.x = clientX;
instance.curPos.y = clientY;
// ...
};
/** * 鼠標擡起事件 */
const MouseUp = (e: any) => {
moveFlag = false;
};
/** * 鼠標移動事件 */
const MouseMove = useCallback((e: any, instance: any) => {
// 直接調用實例下的 MoveCanvas 方法
instance.MoveCanvas(moveFlag, e);
}, [])
複製代碼
接下來咱們看一下最主要的拖動方法 MoveCanvas
,咱們經過實時的座標值減去上一次的座標值(curPos
保存的值)作比較,得出滑動的距離,這樣咱們便能得出最新的 imgTop
和 imgLeft
值了,固然這裏也不要忘記對邊界值的計算。
// src/components/photoGallery/canvas.ts
/** * 鼠標拖動的事件 * @param moveFlag : 是否能移動的標誌位 * @param e */
public MoveCanvas(moveFlag: boolean, e: any) {
// 在拖動狀況下才執行拖動邏輯
if (moveFlag) {
// 圖片顯示容器的寬高
const cWidth = this.cWidth;
const cHeight = this.cHeight;
if (this.cImgHeight < cHeight && this.cImgWidth < cWidth) {
return;
}
// 當前滑動的座標
const { clientX, clientY } = e;
// 上一次座標
const curX = this.curPos.x;
const curY = this.curPos.y;
// 處理 Y 軸上的滾動
if (this.cImgHeight > this.cHeight) {
// 此值保存當前圖片距離容器 imgTop
this.LongImgTop = this.LongImgTop + (clientY - this.curPos.y);
// 與滾動相似的邊界值計算
}
// 處理 x 軸上的滾動
// ...
// 更新實例屬性上的 x、y 值
this.curPos.x = clientX;
this.curPos.y = clientY;
// 賦值 imgTop,imgLeft
this.imgTop = this.LongImgTop;
this.imgLeft = this.LongImgLeft;
// 繪製圖片
// ...
}
}
複製代碼
咱們在點擊圖片的時候去關閉圖片預覽插件,不過這裏須要考慮的是,咱們可以拖動圖片,當用戶是拖動圖片的時候,咱們就不須要關閉插件,因此咱們就須要判斷用戶鼠標按下以前和以後, x/y
座標值有沒有發生過改變,若是發生過改變了,那咱們就不執行關閉操做,不然直接將預覽插件直接關閉。
由於 mosueDown
和 mouseUp
事件是要早於 click
事件的,咱們設置一個標誌位 DoClick
,若是鼠標按下先後位置沒變的話,此標誌位就爲 true
,那麼當圖片點擊的時候,就直接進行關閉,反之就不處理。
// src/components/photoGallery/index.tsx
const MouseDown = (e: any, instance: any) => {
// ...
StartPos.x = clientX;
StartPos.y = clientY;
}
const MouseUp = (e: any) => {
if (e.clientX === StartPos.x && e.clientY === StartPos.y) {
DoClick = true;
} else {
DoClick = false;
}
}
const Click = () => {
if (!DoClick) return;
const { hideModal } = props;
if (hideModal) {
hideModal();
}
}
複製代碼
咱們以前建立了一個預覽圖片的類,那麼具體須要在何時去實例化呢?
只須要監聽在傳入的 imgUrl
變化的時候,就去把以前的實例清空,同時新實例化一個插件就 ok 了。
// src/components/photoGallery/components/Canvas.tsx
const Canvas = (props: Props): JSX.Element => {
// ...
// canvas 的 dom 元素
let canvasRef: any = useRef();
// 存放預覽圖片實例的變量
let canvasInstance: any = useRef(null);
useEffect((): void => {
if (canvasInstance.current) canvasInstance.current = null;
const canvasNode = canvasRef.current;
canvasInstance.current = new ImgToCanvas(canvasNode, {
imgUrl,
winWidth,
winHeight,
canUseCanvas,
// 圖片加載完成鉤子
loadingComplete: function(instance) {
props.setImgLoading(false);
props.setCurSize(instance.curScaleIndex);
props.setFixScreenSize(instance.fixScreenSize);
},
});
}, [imgUrl]);
// ...
}
複製代碼
有了這個圖片實例 canvasInstance
,對於這張預覽圖的各類操做,好比 放大、縮小 咱們均可以調用其擁有的方法就能夠簡單實現了。
當咱們在屏幕尺寸變化的時候,須要根據最新的尺寸去實時繪製圖片,這裏咱們寫了一個自定義 Hooks
,監聽屏幕 size
的變化。
// src/components/photoGallery/index.tsx
function useWinSize(){
const [ size , setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const onResize = useCallback(()=>{
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
})
}, []);
useEffect(()=>{
window.addEventListener('resize', onResize, false);
return ()=>{
window.removeEventListener('resize', onResize, false);
}
}, [])
return size;
}
複製代碼
canvas
繪製閃爍還有一個問題就在 canvas
繪製過程當中,當屏幕 resize
的過程當中會出現閃爍的問題,以下圖:
這是由於重繪畫布的時候,咱們須要使用 clearRect
來清空畫布,此時的畫布是空的,開始重繪須要相應的時間,所以在視覺會出現閃屏的狀況。解決閃屏,其實就是怎麼解決繪製時間較長的問題。
咱們能夠參考 雙緩存 的概念來解決這個問題,將繪製過程交給了 緩存 canvas,這樣頁面中的 canvas
就省去了繪製過程,而 緩存 canvas 並無添加到頁面,因此咱們就看不到繪製過程,在 緩存 canvas
繪製好以後,直接將其賦給頁面原來的 canvas
這樣就解決了閃屏的問題。
// src/components/photoGallery/canvas.ts
class ImgToCanvas {
// ...
private cacheCanvas : any;
private context : any;
// ...
private drawImg (type?: string) {
// 頁面中 canvas
const context = this.context;
// ...
// 建立一個 緩存 canvas,並掛到實例屬性 cacheCanvas 下
if (!this.cacheCanvas) {
this.cacheCanvas = document.createElement("canvas");
}
// 設置 緩存 canvas 的寬高
this.cacheCanvas.width = this.cWidth;
this.cacheCanvas.height = this.cHeight;
// 建立畫布
const tempCtx = this.cacheCanvas.getContext('2d')!;
// 使用 緩存 canvas 畫圖
tempCtx.drawImage(image, this.imgLeft, this.imgTop, this.cImgWidth, this.cImgHeight);
// 清除畫布,並將緩存 canvas 賦給 頁面 canvas
requestAnimationFrame(() => {
this.clearLastCanvas(context);
context.drawImage(this.cacheCanvas, 0, 0);
})
// ...
}
}
複製代碼
這篇文章整理了一個仿水墨圖片預覽插件從零到一的實現過程,從 思路分析、代碼結構劃分、主要邏輯的實現 這幾個方面闡述了一波。
經過這個插件的編寫,筆者對於 canvas
的畫圖 api
、如何處理 canvas
繪圖過程當中出現的圖片閃爍的問題,以及對於 React Hooks
的一些用法有了大體的瞭解。
實不相瞞,想要個贊!