最近工做中須要製做一個圖片預覽的插件,在參考了不少產品(掘金、知乎、簡書、石墨等)的圖片預覽以後,最終仍是以爲石墨的比較符合咱們的產品需求。javascript
原本覺得能在社區中找到相關插件,但想法美好,現實卻很骨感,因而便決定本身手擼一個,順便也學習一下組件的開發流程。html
項目最終的實現效果以下圖,基本上跟石墨的圖片預覽是一毛同樣的。支持 放大圖片、縮小圖片、原尺寸大小顯示、圖片適應屏幕、下載圖片(這個還在開發中),也就是底部欄的五個操做按鈕。java
組件是基於 React Hooks
和 TypeScript
實現的,打包工具使用的是 webpack
。node
本篇文章對webpack
的配置不會作相應的介紹,若是你們對webpack
感興趣的話,能夠參考筆者整理的 淺談 Webpack 性能優化(內附巨詳細 Webpack 學習筆記)。
. ├── 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 // 當前整一個項目的依賴
倉庫地址在此:仿石墨的圖片預覽插件。react
此插件的核心在於圖片的展現,以及圍繞對預覽圖片進行的操做,如 放大、縮小、適應屏幕,而這幾個操做又都是跟圖片的尺寸有關的,其實咱們只要知道在點擊相應操做的按鈕的時候,圖片應該顯示多大的尺寸,整個問題就解決了。webpack
因而筆者就研究了一波其背後預覽邏輯,發現了幾個對編碼比較有用的點:git
首先圖片不能一直放大和縮小,它一定有一個最大值和最小值,操做了一波發現石墨中 預覽圖片的最大值是原圖的 4 倍、最小值是原圖的 10 倍,與此同時還須要規定從原圖開始點擊幾回到最大值或者最小值,在插件中我規定的次數是 6
次。github
這樣在圖片加載完成以後,咱們能很方便的算出這張預覽圖片的全部尺寸,能夠將這些尺寸維護在一個數組中,這樣在每個放大縮小的點擊背後,都會有一個圖片尺寸與其對應。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
的一些用法有了大體的瞭解。
實不相瞞,想要個贊!