最近工做中须要制做一个图片预览的插件,在参考了不少产品(掘金、知乎、简书、石墨等)的图片预览以后,最终仍是以为石墨的比较符合咱们的产品需求。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
的一些用法有了大体的了解。
实不相瞒,想要个赞!