点击上方“前端教程”,选择“星标”

每天前端开发干货第一时间送达!

作者:DARRELL

https://juejin.im/post/5e9bf299f265da47ee3f6c31

前言

最近工作中需要制作一个图片预览的插件,在参考了很多产品(掘金、知乎、简书、石墨等)的图片预览之后,最终还是觉得石墨的比较符合我们的产品需求。

本来以为能在社区中找到相关插件,但想法美好,现实却很骨感,于是便决定自己手撸一个,顺便也学习一下组件的开发流程。


项目介绍

项目预览图

项目最终的实现效果如下图,基本上跟石墨的图片预览是一毛一样的。支持 放大图片缩小图片原尺寸大小显示图片适应屏幕下载图片(这个还在开发中),也就是底部栏的五个操作按钮。


技术栈

组件是基于 React HooksTypeScript 实现的,打包工具使用的是 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 // 当前整一个项目的依赖

仓库地址

仓库地址在此:仿石墨的图片预览插件。

思路分析

此插件的核心在于图片的展示,以及围绕对预览图片进行的操作,如 放大缩小适应屏幕,而这几个操作又都是跟图片的尺寸有关的,其实我们只要知道在点击相应操作的按钮的时候,图片应该显示多大的尺寸,整个问题就解决了。

于是笔者就研究了一波其背后预览逻辑,发现了几个对编码比较有用的点:

首先图片不能一直放大和缩小,它必定有一个最大值和最小值,操作了一波发现石墨中 预览图片的最大值是原图的 4 倍最小值是原图的 10 倍,与此同时还需要规定从原图开始点击几次到最大值或者最小值,在插件中我规定的次数是 6 次。

这样在图片加载完成之后,我们能很方便的算出这张预览图片的所有尺寸,可以将这些尺寸维护在一个数组中,这样在每一个放大缩小的点击背后,都会有一个图片尺寸与其对应。

接着我们需要知道的是当前预览图片的显示尺寸位于 尺寸数组 中的哪一个 index,有了这个 index 之后,我们就只需要取出这个 index 对应的图片宽度进行绘制即可。

这里就涉及到图片首次在容器中的显示情况了,我们拿长图举例:长图预览,插件会在图片上下两侧留出一定的间距,这个间距其实是固定的,在石墨中我算了一下,上下两侧留出的间隙各是容器高度的 5%,具体可以看下图(原谅图片很魔性),图中 A 的距离是 B5%


这样我们可以计算出当前图片的尺寸,拿这个尺寸去 尺寸数组 中找到与这个值最为接近的值,这个最接近的值的索引就是当前预览图片的 index 值。

还有一个石墨的预览图片是通过 canvas 画上去的,我们这里也会使用 canvasdrawImage 这个 api 来进行图片的绘制,当然在不支持 canvas 的浏览器上,我们就直接使用 标签。

在本文就主要分析 canvas 画图这一块内容, 标签其实也是类似的。

到这里基本上此插件的难点都已经解决了,接下来我们就开始分析相应的代码。


代码分析

插件接收参数

首先我们来看一下插件的所需要的参数,大致可以归为下面几个:

  • visible:控制预览插件的显示隐藏

  • imgData:需要预览的图片数组

  • currentImg:再打开预览插件的时候,默认显示第几张图

  • hideModal:预览插件的关闭方法

笔者能想到的暂时就这四个,基本上其实也已经够用了,使用如下:

  visible={visible}  imgData={ImgData}  currentImg = {9}  hideModal={    () => {      setVisible(false);    }  }/>

插件结构

插件的结构其实很简单,其实就三块:图片显示块图片列表选择侧边栏底部操作块,定义为三个子组件块:分别为 、、 ,统一由一个父组件管理。

因为我们主要讲解 canvas 画图片,所以图片显示块就设置为 ,不支持的 canvas 的浏览器,在源码中会使用 组件来进行图片展示,这里就不做具体介绍了,大家可以参考源码。


父组件代码如下:

// src/components/photoGallery/index.tsximport 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 (    
className={ classNames( styles.modalWrapper, { [styles.showImgGallery]: visible, // 根据 visible 渲染插件 } ) } >

// 要加载的图片 url imgUrl={imgUrl} />

// 图片数组 imgData={imgData} /> // 图片数量 imgsLens={imgData.length} // 当前第几张 currentImgIndex={currentImgIndex} />

);}

如上图所示,这样插件的大致的结构就算完成了,接下来就是最核心的图片显示模块的逻辑。

图片预览核心逻辑

我们先创建一个类 canvas.ts,对于图片的预览操作,我们都在这个类中进行操作。

这个类接受两个参数,一个是渲染的容器 dom,另外一个就是实例化所需要用到的参数 options,下面是 options 的接口实现:

interface CanvasOptions {  imgUrl: string; // 图片地址  winWidth: number; // 屏幕宽度  winHeight: number; // 屏幕高度  canUseCanvas: boolean; // 浏览器是否可以使用 canUseCanvas  loadingComplete?(instance: any): void; // 制作图片 loading 效果}

还有我们会讲一系列跟预览图片有关的属性都挂在其实例属性上,如:

  • el:渲染的容器

  • canUseCanvas:是否支持 canvas,决定以什么方式画图

  • contextcanvas 的画布 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:规定要使用的图像、画布或视频。

  • dximage 的左上角在目标 canvasX 轴坐标

  • dyimage的左上角在目标 canvasy 轴坐标

  • dWidthimage 在目标 canvas 上绘制的宽度。

  • dHeightimage在目标 canvas 上绘制的高度。

具体可以看下图:


关于此方法更多用法大家可以参考:drawImage 的 MDN 文档。

有了这个 api 之后,我们其实只要计算出这个 api 对应的 5 个参数即可,举个简单的例子,下面这张图我们改怎么得到 5 个参数:


  • image 对象

我们可以使用 new Image() 来实例化一个 image 对象,并指定他的 src 属性为相应的图片 url 地址,这样就可以得到一个 image 对象,当图片加载完成之后,我们就可以通过 imgDom.naturalWidthimgDom.naturalHeight 图片的原始宽高:

// src/components/photoGallery/canvas.tsloadimg(imgurl) {  const imgDom = new Image();  imgDom.src = imgUrl;  imgDom.onload = function() {    // 图片加载完成之后    // 做你想要做的事情  }}
  • dxdydwidthdHeight 属性

我们以长图举例:我们在讲解思路的时候分析过,上下两边留空的部分是 图片显示容器高度5%,在这里我们定义了底部块的高度(footerHeight)为 50px,侧边栏的宽度(sidebarWidth)为 120px,这就变成了一道小学应用题,我们可以通过 window.innerWidthwindow.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、以及距离左侧 canvasimgLeft

前面我们已经得到首次图片展示索引了,当我们点击放大的时候,无非就是将当前索引值加一,缩小就是减一。

我们可以根据新的索引值去 尺寸数组 中取出对应索引的宽度,通过图片原始宽高,可以等比例得到当前应该显示的宽高,最后我们只需要计算出,放大后的图片的 imgTopimgLeft 的值,其实就能实现功能了:

/** * 修改当前 图片大小数组中的 索引 * @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 中进行图片滚动,其实就是重新计算图片的 imgTopimgLeft,然后对其进行重新绘制。

这里我们使用滚轮事件 onWheel 来计算滚动的距离 ,通过事件对象 event 上的 deltaXdeltaY 得到的在 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;  // 绘制图片  // ...}

拖动事件

图片拖动的我们需要借助 onMouseDownonMouseMoveonMouseUp 三个事件函数。其实操作方式可图片滚动类似,我们需要计算出新的 imgTopimgLeft 去重新绘制图片,但是我们不能通过 event 下面直接得到拖动的值了,需要通过后一次与前一次的差值,来得出拖动的距离,进而计算出 imgTopimgLeft 值,

首先我们把图片拖动过程中的实时坐标挂在实例属性 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 保存的值)做比较,得出滑动的距离,这样我们便能得出最新的 imgTopimgLeft 值了,当然这里也不要忘记对边界值的计算。

// 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 坐标值有没有发生过改变,如果发生过改变了,那我们就不执行关闭操作,否则直接将预览插件直接关闭。

因为 mosueDownmouseUp 事件是要早于 click 事件的,我们设置一个标志位 DoClick,如果鼠标按下前后位置没变的话,此标志位就为 true,那么当图片点击的时候,就直接进行关闭,反之就不处理。

// src/components/photoGallery/index.tsxconst 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.tsxconst 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.tsxfunction 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.tsclass 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 的一些用法有了大致的了解。

实不相瞒,想要个赞!


好文章,我在看❤️

dw按钮图片滚动js_使用 React Hooks 实现仿石墨的图片预览插件(巨详细)相关推荐

  1. 基于 React hooks + Typescript + Cesium 实现地下模式预览

    文章目录 功能介绍 实现思路 实现步骤 记录原始球体相关参数 开启地下透明模式 更新影像图层透明度 封装 UndergroundModeComponent 组件 UndergroundModeComp ...

  2. React 图片预览插件 rc-viewer @hanyk/rc-viewer

    最近一个需求是图片要实现预览并且可以上下切换,react接触不是很久,查了好多资料,最终对@hanyk/rc-viewer下手,jquery用多了伙伴都知道viewer.js,一个很强大的图片预览插件 ...

  3. 移动端图片预览插件-fly-zomm-img.min.js

    移动端图片预览插件,一个JQ的插件,支持手势放大缩小:有点小bug,不过感觉是可以接受的: 插件的地址:http://www.jq22.com/jquery-info15466 那里有具体的说明,但是 ...

  4. 图片预览小窗口html,jQuery和css3超酷图片预览插件

    这是一款使用css3和Velocity.js制作的jquery图片预览插件. 电子商务网站设计的宗旨是通过简化的设计提高用户体验,提高转化率也有一定的模式.另外一种模式是在需要的地方提供更多的信息来达 ...

  5. vue3 开发一个图片预览插件

    vue3 的插件开发和vue2思路类似但是写法却迥异.主要变化在vue3没有了extend构造器. 这次我们通过一个图片预览插件,贯通整个vue3插件开发的过程. 1 在src下新建lplugins文 ...

  6. pc 图片预览放大 端vue_安利一款简单好用的Vue图片预览插件

    在项目中因为要经常用到图片预览效果,自己写的话麻烦死啦(懒) vue-photo-preview 一个基于 photoswipe 的 vue 图片预览插件,支持移动端和PC端,支持各种手势操作,放大缩 ...

  7. VSCode图片预览插件 Image preview

    VSCode前端开发图片预览插件 Image preview(支持css预览 支持svg格式) 一款提高前端开发效率的插件 ,代码中hover直接预览图片 先上效果图 不仅html中可以预览 js代码 ...

  8. html页面点击图片名称查看图片------图片预览插件viewer.js使用

    前言 在做项目时,会遇到在前台展示附件详情,比如图片,word,pdf等.viewer.js 图片查看器,用来查看页面中的图片. 基本效果: Viewer.js的使用 这是一个github工程,功能很 ...

  9. html网页图片滚动代码用css和div,DIV+CSS网页图片滚动源代码

    DIV+CSS网页图片滚动源代码 style="overflow:hidden;width:500px;"> border="0"> id=&quo ...

最新文章

  1. python数学计算例子_Python OpenCV实例:直方图计算(数学公式简单实现)
  2. 使用自连接、for xml path('')和stuff合并显示多行数据到一行中(转)
  3. Windows无法更新的解决办法
  4. java 数组用字符做下表_JAVA字符串json数组使用Formatter格式化成表格形式
  5. centos6.9搭建lnmp环境
  6. 自建站如何通过Facebook广告引流?
  7. Java使用Redis(jedis)
  8. 斐波那契数列(剑指offer)
  9. loadGrid layui
  10. Why hash maps in Java 8 use binary tree instead of linked list?
  11. LNMP架构 源码安装nginx+mysql+php+memcache+论坛
  12. 点云配准1:配准基础及icp算法
  13. 信息安全技术标准合集
  14. 免root卸载MIUI中在通知栏中推送广告的应用——msa
  15. 简单提高MIDI音量的方法
  16. python主函数怎么写_python主方法怎么写
  17. 2023最新毕业设计选题 -python毕设选题推荐 - 如何选题 避免被坑
  18. 蓝狐SEO关键词按天计费系统_seo扣费系统源码
  19. C语言——三位数排序
  20. CSS实现鼠标经过网页图标弹出微信二维码

热门文章

  1. 合理利用延迟初始化优化 Spring Boot
  2. Spring Boot 2.x基础教程:使用JdbcTemplate访问MySQL数据库
  3. 【实用】面对枯燥的源码,如何才能看得下去?
  4. python添加时间戳_Python 给某个文件名添加时间戳的方法
  5. cmakelist 寻找opencv
  6. numpy 筛选面积最大
  7. labelimg颜色
  8. python调用dll依赖项
  9. sigmoid函数求导与自然指数
  10. parse() got an unexpected keyword argument 'transport_encoding'