前言

最近工作中需求制作一个图片预览的插件,在参阅了许多产品(掘金、知乎、简书、石墨等)的图片预览之后,终究仍是觉得石墨的比较契合咱们的产品需求。

本来认为能在社区中找到相关插件,但想法美好,实际却很骨感,于是k V } = x便决议自己手H N ! [ h撸一个,趁便也学习一下组件的开发流程。

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

项目介绍

项目预览图

项目终究的完结作用如下图,基本上跟石墨的图片预览是一毛一样的。支持 扩大图片@ S ` l 5缩小图片原尺度巨细显现图片习惯屏幕下载图片(这个还在开发中),也便是底部栏的五个操作按钮。

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

技术栈

组件是依据 React HooksTypeScript 完结的,打包东西运用的是 webpack

本篇文章对 webpack 的装备不会做相应的介绍,假如咱们对 webpacI N + dk 感爱好的话,能够参阅笔者收拾的 webpack 学习文档。

项目目录

.
├── node_modules // 第三方的依赖
├── config    	// webpack 装备文件夹
├── webpack.base.js 	// webpack 公共装= 6 T e备文件
├── webpack.dev.config.js  // 开发环境装备文件H  Z q O
└── webpack.prod.confiR 1 H e : xg.js  // 出产环境装备文件
├── example    // 开发时预览代码
├── src    // 示例代码目录
├── app.js     // 测验项目 进口 jsR U 1 a 文件
└── index.less // 测验项目 进口 款式文件 文件
├── src    	// 组件源代码目录
├── components 	// 轮子的目录
├── photoa p 8Gallery // photoGallery 组件文件夹
├── types  // typescripe 的接口界说C X E── utils  // 东西函数目录
├── im} G Cages  // 图片文件目录
├── index.html  // 项目进口模版文件
├── index.tsx  	// 项目进口文件
└── index.less   // 项目进1 C ? ^ * w口款式文件
├── libF 2 V $ 3  // 组件打包结果目录
├── .babelrc // babel 装备文件
├── .gitignore // git上传时疏忽的文件
├── .npmignore // npm 上传疏忽文件
├── README.md
├── tslint.json // tslint 的装备文件
├── tsconfig.json // ts 的装备文件
├── package-lock.json    // yarn lock 文件
└── package.json // 当时整一个项目的依赖

库房地址

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

思路剖析

此插件的中心在于图片的展现,以及围绕对预览图片进行的操作,如 扩大缩小习惯屏幕,而这几个操作又都是跟图片的尺度有关的,其实咱们只b w 1要知道在点击相应操作的按钮的时分,图片应该显现多大的尺度,整个问题就处理了。

于是笔者就研究了一波其z M C & c d B背面预览逻辑,发现o z X Q ^ W ^ = .了几个对编码比较有用的点:

首要图片不能一向扩大和缩小,它必定有一个最大值和最小值,操作了一波发现石墨中 预览图片的最大值是i ) g *原图的 4 倍最小值是原图的 10 倍,与此同时还需求规则从原) t / o S N图开端点击几回到最大值或许最小值,在插件中我规则的次数是 6 次。

这样在图片加载完结之后,咱们能很便利的算出这张预览图片的一切尺度,S I (能够将这些尺度维护在一个数组中,这样在每一个扩大缩小的点击背面,都会有c D l U J ;一个图片尺度与其对应。

接着咱们需求知道的是当时预览图片的显现尺度位于 尺度数组 中的哪一个 index,有了这个E w { R @ u . j ] index 之后,咱们就只需求取出这个 index 对应的图片宽度进行制作即可。

这儿就J . . j j涉及到图片初次在容器中的显现情况了,咱们拿长图举例:长图预览,插件会在图l ( u , c片上下两头留出一定的间隔,这个间隔其实是固定的,在石墨中o I S q 7 S 6 l我算了一下,上下两头留出的空隙各是容器高度的 5%,详细能够看下图(原谅( 8 + i t j图片很魔性),图中 A 的间隔是 B5%

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

3 / r样咱们能够核算出当时图片的尺度,拿这个尺度去 尺度数组 中找到与这个值最为挨u N n g 5 ` 8近的值,这个最挨近的值的索引便是当时预览图片的 index 值。

还有一个石墨的预览图片是经过 canvas 画上去的,咱们这儿也会运用 canvasdrawImaq x *ge 这个 ap4 = n N 0 ri 来进行图片的制作,当然在不支持 canvas 的浏览器上,咱们就直接运用 <s T b Q K s fimg /> 标签。

在本文就首要剖W o 7 ocanvas 画图这一块内容,<img /> 标签其实也是相似的。

到这儿h & B D基本上此插件的难点都现已处理了,接下来咱们就开端剖析相应的代码。

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

代码剖析

插件接收参数

首要咱们来看一下插件的所需求的参数,大致能够归为下面几个:

  • visible:操控预览插件的显现隐藏
  • imgData:需求预览的图片数组
  • currentImg:再打开预览插m ! n R N J件的时分,默许显现第几张图
  • hideModal:预览插件的封闭办法

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

<PhotoGallery
visij % ! }ble={visible}
imgData=k _ + ~ r{ImgDatad : n [ E K @}
currentImg =Z N 1 Y {9}
hideModal={
() => {
setVisible(false);
}
}
/>

插件结构

插件的结构其实很简略,其实就三块:图片显现块图片列表选择侧边栏S 1 .底部操作块,界说为三个子组件块:. % = B $ + C分别为 <Canvas /><Sidebar /><Footer /> ,一致由一个父组件办理。

由于咱们首要解说 canvas 画图片,所以图片显[ { u现块就设置为 <Canvas />,不支持的 canvasM G s : 的浏7 W # M d J % w览器,在源码中会运用 <Image /> 组件来进行图片展现,这儿i + O 5 # f就不做详细介绍了,咱们能够参阅U p , n 9源码。

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

父组件代码如下:

// src/components/photoh 7 H / T fGallery/index.tsx
import React, { useState }  from 'react';
imz a O , { V rport classNames from 'classnam5 H O i s A . b ?es';
import { Footer, Sidebar, Canvas } from './components';
const photoGallery = (props: Props): JSX.El? @ h @ ) v H =ement => {
const { imgData, currentImg, visible } = props;
// 当时显现第几张图片
const [currd 1 C y b V ] 5entImgIndex, setCurrentImgIndex] = useState(currentImg);
return (
<div
className={
classNames(
styles.modalWrapper,
{
[styles.showImgGallery]: visible, // 依据 visible 烘托插件
}
)
}
>
&! s Z o A Y G Rlt;div className={styles.contentWrapper}>
<Canve W ^as
// 要加载的图片 url
imgUrl={imgUrl}
/>
</div>
<Sidebar
// 图片数组
imgData={imgData}
/>
<H & k I;Footer
// 图片数量
imgsLens={imgData.length}
// 当时第n F ` Z i a *几张
currentImgIndex={currentImgIndex}
/>
</div>
);
}

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

图片预览中心逻辑

咱们先创C P * ]立一个类 canvas.ts,对于图片的预览操作,咱们都在这个类中进行操作。

这个类接受两个~ r , Q W . . k参数,一个是烘托的容器 dom,别的一个便是实例化所R + ,需求用到的F y U ; V ; 1参数 options,下面是 optionsm k J W g T接口T u ! j完结:

interface CanvasOptions {
imgUrl: string; // 图片地址
winWidth: number; // 屏幕宽度
winHeight: numb= n ~er; // 屏幕高度
canUseCanvas: boolean; // 浏览器是否能 F * ~ 9够运用 canUseCanvas
lo^ L 6 o i W a kadingComplete?(insta? $ X o pnce: any): void; // 制作图片 loading 作用
}

还有咱们会讲一系列跟预览图片有关的特点都挂在其实例特点l q 7 5 S上,如:

  • el:烘托的容器
  • canUseCanvasz H |:是否支持 canvas,决议以什么办法画图
  • contextcanvas 的画布 getContext('2d')
  • image:预览图片目标
  • imgUrl:预览图片 url
  • imgTop:图z B A C u f K ! j片右上角方针 canvasy 轴的高度
  • imgLeff * At:图片右s l a ,上角方针 canvasx 轴的高度
  • LongImgTop:图片间隔容器顶部的间隔,用于图片翻滚和拖动
  • Longt $ P ^ z ? @ImgLeft:图片间隔容器左侧的间隔,用于图片翻滚和拖动
  • sidebarWidth:侧边栏的宽度
  • footerHeighk X B jt:底k p d e B L R G .部栏的高度
  • cImgWidthI H u:画布中图片的宽度
  • cImgHeight:画布中图片的高度
  • winWidth:屏幕的宽度
  • winHeight:屏幕的高度
  • curPos:鼠标拖动图片是需求用的的 x/y
  • curScaleIndex:当时显现图片,位于尺度数组中的哪一个 index
  • fixScreenSize:运用屏幕巨细的尺度数组中的 index
  • EachSizeWidthArray:图片的尺度数组,包含了扩大缩小一切尺度的宽度值
  • isDoCallback:图片是否加载完结

插件中运用的特点值基本上都在上面了。

先画一张简略的图

首要咱们先来看一下这个 canvas 画图的这个 api,它能协助咱们在画布上制作图画、画布或视频。

咱们能够经过下面的办法扩大来帮咱们画出一张图片:

var c = document.getEle` | M } a I = q 2mentById("myCanvas, o U A 2 O @ ~"U e B);
// 创立画布
var ctx = c.getContext("2d");
// 开端制作
ctx.f 6 5 h udrawImage(h t ( ~ y q Gimage, dx, dy, dWidth, dHp E : Leight);

其中参数的意思分别为( = 4 L | A S

  • image:规则要运用的图画、画布或视频。
  • dximaZ ] R a C U Ige 的左上角在方针 canvasX 轴坐标
  • dyimage的左上角在方针 canvasy 轴坐标
  • dWidthimage 在方针 canvas 上制作的宽度。
  • dHeightimage在方针 canvas 上制作y H $ 2 F % `的高度。

详细能够看下图:

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

关于此办法更多用法咱们能够参阅:drawImage 的 MDN 文档。

有了这个 api 之后,咱们其实只要核算出这个 api 对应的 5 个参数即$ z i ~ & +可,举个简略的比如,下面这张图咱们改怎样f X ~ z k 3 1 s得到 5 个参数:

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)
  • imag9 F y Me 目标

咱们能够运用 new Image() 来实例化一个 image 目标,并指定他的 src 特点为相应的图片 url 地址,这样就能够得到一个 image 目标,当图片加载完结之后,咱们就能够经过 imgDom.naturalWidthimgDom.naturalHeight 图片的原始宽高:

// src/components/photoS t ~Gallery/canvas.ts
loadimg(imgurl) {
const imgDom = new Imh o 9age();
imgDom.src = imgl 8 RUrl;
imgDom.onload = function() {
// 图片加载完结之后
// 做你想要做的工作
}
}
  • dxdydwidthdHeight 特点

咱们以长图– A ? n 3 & { z S举例:咱们在解说思路的时分剖析过,上下两头留空的部分是 图片显现容器高度5%F P #,在这儿咱们界说了底部块的高度(3 e ^ C . * ) efooterHeight)为 5T A 40px,侧边栏的宽度(sidebarWidth)为 120px,这就变成了一道小学应用题,咱们能够经过 window.innerWidthwindow.innerHeight 来得到屏幕的宽(winWidth)和高(winHeight),经过核算咱们便能够得到咱们所需的四个特点:

/**
* winWidth:屏幕宽度o * E
* winHeight:屏幕高度
* footerHeight:底部高度
* sidebarWidth:侧边栏宽度
* wrapperWidth:图片显现区域宽度
* wrapperHeight:图片显现区域高度
* naturalWidth: 图片原始宽度
* naturalHeight: 图片原始高度
*/
wrapp@ R 4erHeight = winHeight - footerHeight;
wrapperWidth = winWidth - sidebarl * ( ,Width;
dy = wrapperHeight * 0.05;
dHeight = wrapperHeigh ` n a &ht - 2 * dy;
// 与原始宽高有个等比例的关系
dWidth = naturalWidth * dHeiga g yht / naturalHeight1 w V F G U & j;
dx = (wrapperWidth - dWidth) / 2

上面便是核算咱? D 4 ; w m y们所需五个特点的进程,总的来说仍是比较便利的。

所以在咱们每非必须制作图片的时分,只要核算出这 5 个值就 ok 了。

初始图片宽高

咱们在 utils 下的 img.ts 中界说一个办法 getBoundingClient, . HRect,用来得到 图片的显现宽高和他间隔容器顶部的 imgTop、以及间隔左侧的 imgLeft

// src/utilsm . h f 0 k q } E/imgC ^ J.ts
/] {  ( p Z s**
* 回来第一次加载图片的宽高,和 imgTop/imgLefr S 0 @ ?t
* 经过回来的参数 直接 经过 drawImage 画图了
**/
export const getBoundingClientRect = (options: RectWidth): BoundingClientRect => {
const {
naturalWidth, // 图片原始宽
naturalHeight, // 图片原始高
wrappY m a p g g U 0erWidth, // 显现容器宽
wrapperHeigV ^ d jhY 7 ^ } D $ ht, // 显现容器高
winWidt e b U u 4th, // 屏幕宽度
} = options;
// 图片宽高比
const image| D 6 Y W #Radio = naturalWidth / naturalHeightd v /;
// 显现w F r 1 ` 3 } b 3容器宽高比
const wrappG j  c ierRadio = wrapperWidth / wrapperHeight;
// 长图的逻辑
if (imageRadio <= 1) {
// 详细画布上方默许是 容器高度的 0.05
imgTop = w5 o v H j H 9rapperHeight * 0.05;
// 图片的高度
ImgHeight = wrI W - 4apperHeight - wrapperHeight * 0.X g 8 _ k (05 * 2;
// 依据原始宽高,等比w 0 : } 0例得到图片宽度
ImgWidth = ImgHeight * natu2 L d p @ralWidth / naturalHeight;
// 假如图片的宽高比显现容器的宽高比大
// 阐明图片左右两头的宽度需求固定y s T * L l为容器的宽度的 0.05 倍了
if (wrapperRadio <= imageRadio) {
ImgWidth = wrapperWidth - w{ } Q 5 n K irapperWidth * 0.05 * 2;
ImgHeight =  ImgWidth * naturalHeight / naturalWidth;
imgTop = (wraps ~ ( h P G zp@ : A E .erHeight - ImgHeight) / 2
}
// ...
imgLeft = newWinWidth - ImgWidth / 2;
}
// 处理宽图的逻辑
// ...
// 回来
return {
imgLeft,
imgTop,j y k Y
ImgWidth,
ImgHeight,
}
}

更详细的代码咱M w l c G k B们能够参阅源码。

预览图片尺度数组

咱们在之前提到,咱们能够把图片扩大缩小进程中一N b d 1 + A y切的尺度/ V H都放到一个数@ l u R p G q组中去,便利之后经过索引去得到相应的图片尺度,那么怎样进行操作呢?

其实只要在图片加载完结之后,得到图片的原始宽高,经过原始宽高,经过相应的核算公式,核算得到相应的尺度数组,塞入数组即可。

在类中界说一个 setEachSizeArr 实例办法:6 K h + _ ~

//g p  ? n 2 v z src/components/photoGalY v { dlery/canvas.ts
/C + Y 3 ] : - c 9**
* 核算图片扩大、缩小各尺度的巨细数组,
*/
private setEachSizeArr () {
const image = thiy e J ms.image;
// 得到尺度A [ ] R I n J O数组
const EachSizeWidthArray: number[]U { V l / = getEachSizeWidthArray({
naturalf M ] h v t W T eWidth: is ) 8 8 ( * Amage.width,S D w g
nat6 $ a s % ~ M 3 ^uralHeight: 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 = fixScrs J U d : 2eenSize;
}
  • getEachSizeWidthArray

咱们经过此办法得到尺度数组,由于最大的图片是原图的 4 倍,最小的图片是原图的 1/10,从最r x [ U小到原图 和 从原图到最大 都需求经过 6 次,咱们能够依据J ~ @ i 1 / [ L比例得出每一个尺度的巨细,详细的代码o + + [ ] b ( H笔者就不贴了。

  • getFixScreenIndex

咱们经过此办法得到习惯屏幕的尺度数组的 iy # g Qndex,原理便是在尺度数组中第一个宽高小于显现容器宽高的w } e index

这两个办法的详细代码笔者就不贴了,咱们有爱好能够去源码检查。

初始预览图片索引

咱们要核算出初次L E Y 8 9图片烘托出来时分,位于尺度数组的那一个 i$ ` X c DnD ) Tdex,由于咱们得到初次烘托图片的宽度,能够拿H 9 n %这个宽度去与尺度数组中数组进行比对,最挨近的这个值的索引 indy k ~ Kex,便是当时图片的 index 值:

// src/components/photoGallery/4 3 H r k 0 ~ vcanvas.ts
/**
* 设置当时 EachSizeWidthArray 的索E i k K O引,用于 扩大缩小
*/
private setCurScg 5 N :aleIndex() {
const cImgWidth = this.cImgWidth ||% F m @ D . this.image.width;
cm . y W D ; { Konst EachSizeWidthArray = this.EachSizeWidthArray;
const curScaleIndex = getCurImgIndex(EachSizeWidthArray, cImgG = S ] A C v kWidth);
this.curScaleIndex = curScaleIndex;
}
  • getCurImgIndex

咱们经Q l q * {过此办法来得到当时图片款的索引值,他是依据当时烘托的图片宽度,去 尺度数组 取出最挨近预览图片宽度,从而得到当时图片的 index,详细完结咱们G V e ; Z V B / K能够参阅源码。

扩大缩小逻辑

扩大预览的逻辑实际上便是依据扩大之后的尺度,核算出当时图片的间隔 canvas 顶部的高度 imgTop、以及间隔左k L Q $canvasimgLeft

前面咱们现已得到初次图片展现索引了,当咱们点击扩大的时分,无非便是将当时索引值加一,缩小便是减一。

咱们能够依据新的索引值去 尺度数组 中取出对应索引的宽B p I V m d ~度,经过图片原始宽高,能够等比例得到当时应该显现的宽高,最终咱们只需求核算出,扩大后的图片的 imgTopimgLeft 的值,其实就能完结功能了:

/**
* 修正当时 图片巨细数组中的 索引
* @param curSizeIndex :
*/
public changeCurSizeIndex(curSizeIndex: numbers & l O , |) {
let curScaleIn/ K dex = curSizeIndex;
if (curScw ) . WaleInj ) 3 f . 6 Wdex > 12) curScaleIndex = 12;
if (curScaleIndex <9 j c R S 6; 0) curScaleIndex = 0;
// 画布宽高,即显现容器宽高
const cWidth = this.cWidth;
const cHeight = t/ C c / C 4his.cHeight;
// 上一次的索引
const prevScaleTimes = this.curScaU M P )leIndex;
// 尺度数组
const EachSizeWid@ . _thArray = this.EachSizeWidthArray;
let scaleRas s K ydio = 1;
// 这一次宽度与上一次的比值
// 经过这个值能更便利的得到图片宽高
scaleRadio = EachSizeWidthArray[curScaleIndex] / EachSizeWidthArray[prevScaleTimes];
// 当时图片宽高
this.cIT G 4 fmgHeight = this.cImgHet s W  ] Y K @ight * scalem - 3 [ x uRadio;
this.cImgWidth = this.cImgWidth * scaleRadio;# ! m W N * =  A
// 得到最新的 imgTop
// imgTop 值正负值是依据画布左上角的点- T 5 D m j A l Q,向下为正
this.imgTop = cHeight / 2 - (cHeight / 2 - this.imgTop) * scaleRadio;
// 设置当时 索引值
this.curScaleIndex = curScale%  G jIndex;
// 假如图片没有超越画布! 6 ^ ) o 7 k )的宽和高
if (this.cImgHeight < cHeight && this.cImgWidth < cWidth) {
this.imgTop = (cHeight - this.cImgHeiG  c $ H R r ight) / 2;
}
// imgLefO a ! n .t 的核算
this.imgLeft = cW4 Q l t Z & 5idth / 2 - this.cImgWidth / 2;
// 在图片滑动的时分或许拖动的时分需求用到
this.Lo| H / M i U ,ngImgTop = this.imgTop;
this.LongImgLeft = this.imgL; ? h x , % S aeft;
// 制作图片
// ...
}

事情

翻滚事情

canvas 中进行图片翻滚,其实便是从头核算图片的 imgTopimgLeft,然后对其进行从头制作。

这儿咱们运用滚轮事情 onWheel 来核k X O D Q 3 f算翻滚的间隔 ,经过事情目标 event 上的 deltaXdeltaY 得到的在 x/y 轴上的翻滚间隔。

这儿需求注意的一个点是对鸿沟值的处理,imgTop 不能无止境的大和小,其最大不能超越咱们之N Q前规则的 LONG_IMG_TOP 这个值,咱们设置的是 10px,最小能够参照下面的核算办法(宽度的鸿沟值核算相似,就不做介绍了)

/**
* minImgTop:最小的 imgTop 值
* maxImgTop:最大的 imgTop 值
*4 y _  imgHeight:图片高度
* winHeight:屏幕高度
* footerHeight:底部操作栏高度
* LONG_IMG_TOP:咱们G 7 : q U ) f w设置的一个上下常量 padding
*/
// 最小肯定是负数
minImgTop = -(imgHeight - (winHeight - footerHeight - LONG_IMG_TOP))
//Y | S O z Z ) 最大
maxImgTop = LONG_IMG_TOP

接下来咱们在 canvas 类中界说一个 WheelUpdate 案例办法,暴露出去给外部调用,

// src/components/photoGallery/canvas.ts
/**
* 滚轮事情
* @param e wheel 的事情参数
*/
public WheelUpdate(e: a K ` :ny) {
// ...
// 图片显现容器的宽高
const cWidth = this.cWidth;
const cHeight = this.cHeight;
// 假如图片的宽高都小于图片显现容器的宽高就直接回来
if (this.cImgHeight < cHeight && t{ v = ` O 5 ` ~his.cImgWidth < cWidth) {
return;
}
// 假如图片的高度 大于 显现容器的 高度
// 则允许 在 Y 方向上 滑] t u
if (this.cImgHeight > cz 4 %Height) {
// 此值保存当时图片间隔容器 imgTop
this.LongImgTop = this.LongImgTop - e.deltaY;
// e.del1 t b 6 @ *  m xtaY 向下
if (e.deltaY > 0) {
// 这儿做一个极限值的判别
// 详细是咱们的算法
i] z # 3 L p of ((-A l z O 7 lthis.Lon^ z 0 [ q $ k D QgImgTop) > thY 4 h  }is.cImgHeight + LONG_IMG_TOP - window.innerHeight + this.footerHeight) {
thim I e L B Qs.LongImgTop = -(ti y 2 _his.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$ E [;
this.imgLeftU / E A = this.LongImgLeft;
// 制作图片
// ...
}

拖动) r c x r事情

图片拖动的咱们需求凭借 onMouseDownonMouseMoveonMouseUp 三个事情函数。其实操作办法可图片翻滚相似,咱们需求核算出新的 imgTopimgLeft 去从头制作图片,可是咱们不能经J 6 o # I 2 5event 下面直接得到拖动的值了,需求经+ A v = j 9 I J过后一次与前一次的差值,来得出拖动的间隔G ? + l / {,从而核算| w – . | *imgTopimgLeF { K & 8 n ` dft 值,

首要咱们把图片拖动进程中的实时坐标挂在实例特点 curPos[ x s 上,在 onMouseDown 的时分进行初始坐标赋值,这样在K % K ` | ; : onMouseMove] q x A Z + 1 b i 函数中咱们就能得到鼠标D L C % % ,按下的初始坐标了。

// src/components/pho! D v wtoGallery+ S ?/i[ n / | a [ 0ndex.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;
// ...
};
/V . ?**
* 鼠标抬起事情
*/
const MouseUp = (e: any) => {
moveFlag = false;
};
/**
* 鼠标| q Q 1 `移动事情
*/
const MouseMove = useCallbackm U 4 2 A H K C ]((e: any, instance: any) =>X K . * O M S v; {
// 直接调用实例下的 MoveCanvas 办法
instance.MoveCanvas(moveFlag, e);
}, [])

$ k 3 ^ V ?下来咱们看一下最首要的拖动办法 MoveCanvo . a 8as,咱们经过实时的坐标M ? 5 U ` c v D 4值减去上一次的坐标值(curPos 保存的值)做比较,得出滑动! V ] e u k的间隔,这样咱们便能得出最新的 imgTopimgLeft 值了,当然这儿也不要忘掉对/ + + 1 @ h 1鸿沟值的核算。

// src/componentsW / Q M d/photoGallery/canvas.ts
/**
* 鼠标拖动的事情
* @param moveFlag : 是否能移动的标E f b志位
* @param e
*/
public MoveCanvas(moveFlag: boolean, e: any) {
// 在O * ? A R拖动情况下才履行拖动逻辑k z H Q 8 6 [ P 2
if (moveFlag) {
// 图片显现容器的宽高
const cWidth = this.cWidth;
const cHeight = this.cHeight;
if (this.cImgHeight < cHeight && thiD % ` G $ s xs.cImgWidth < cWidth) {
return;
}
//I # I E d + = 3 当时滑动的坐标
const { cl R f ` ) /ientX, clientY } = e;
// 上一次坐标
const cu= 5 , j ]rX = this.curPos.x;
const curY = this.curPoy K ; _s.y;
/Q q ? P a ] 0 = 2/ 处理 Y 轴上的翻滚 
if (this.cImgHeight > this.cHeight) {
// 此值保存当时图片间隔容器 imgTop
thia y Gs.LongImgTop = this.LG 0 ,ongImgTop + (clientY - this.cuz 1 w vrPos.y);
// 与翻滚相似的鸿沟值核算
}
// 处理 x 轴上的翻滚 
// ...
// 更新实例特点上的 x、y 值
this.curPos.x = clientX;
this.curPos.y = clientY;
// 赋值 im3  0 rgTop,imgLeft
this.imgTop; % / @ =S } 2 9 { g x + % this.LongImgTop;
this.imgLeft = this.LongImgLeft;
// 制作图片
// ...
}
}

预览插件封闭

咱们在点击图片的时分去封闭图片预览插件,不过这儿需求考虑的是,咱们能够拖动图片,当用户是拖动图片的时分,咱们就不需求封闭插件,所以咱们就需求判别用户鼠标按下之前和之后, x/y 坐标值有没有发生过改变,假如发生过改变了,那咱们就不履行封闭操作,不然直接将预览插件直接封闭。

由于 mosueDownmouseUp 事情是要早于 click 事情的,咱们设置一个标志位 DoClick,假如鼠标按下前后位置没变的话,此标志位就为 true,那么当图片点击的时分,就直接进行封d I 3 x 9 G闭,反之就不处理。

// src/components/photoGal) Y e v F g Hlery/index.tsx
const MouseDown = (e: any, instance: any) => {
// ...
StartPos.x = clientX;
Star[ H * 6 Q XtPos.y = client] G f f V O f `Y;
}
const MouseUpY { P j [ n A = (e: any) => {
if (e.clientX === Star. + F { wtPos.x && e.clientY === StartPos.y) {
DoClick = true;
} else {
DoClick = false;
}
}P K t X 3 w x
const Click = () =&gC ! Ut; {
if (!DoClick) return;
const { hideModal } = props;
if (hideModal) {
hideModal();
}u S U + B
}

其他知识点

图片类何时实例化

咱们之前创立了一个预览图片的类,那么详细) a Q A需求在什么时分去实例化呢?

只需求监听在传入的 imgs b y eUrl 变化的时分,就去把之前的实例清空,同时新实例化一个插件就 ok 了。

// src/components/photoGallery/components/Canvas.tsx
const Canvas =@ h u ( 1 s (props: Props): JSC 2 YX.Element => {Y F r
// ...
/# ~ 0 : Z o y 1/ canvas 的 dom 元素
let canvasRef: any = useRef();
// 寄存预览图片实例的变量
let canvasInstance: any = useRef(null);
useEffect((): void => {
if (canvasInstance.current) canvasInw l jstance.current = null;
const canvasNodeb g A % v = canvasRef.current;
canvasInstance.current = new ImgToCanvas(canvasNode, {
imgUrl,
winWidth,
winHeight,
canUseCanvas,
// 图片加载完结钩子
loadingComplete: function(instance) {a f ( t w 2 b
props.setImgLoa1 d Tding(far 9 S ]lse);l l ; S ~ 5 X 9
props.setCurSize(instance.curA 9 =ScaleIndex);
pro] * ;ps.setFixScreenSize(instance.fixScreenSize);
},
});
},g V % U  ! & [imgUrl]);
// ...
}

有了这个图片实例 canvasInstance,对于这张预览图的各种操作,比如J h j 扩大缩小 咱们都能够调用其具有的办法就能够简略完结了。

屏幕尺度

当咱们在屏幕尺度变化的时分,需求依据最新o e G t L ) f [ 9的尺度去实时制作图片,这儿咱们写了一个自界说 Hooks,监听屏幕 size 的变化。

// src/components/photoGallery/index.tsx
function useWinSize(){
const [ size , setSiz2 / S - Y O L / +e] = useState({
width:  document.documentElement.clientWidth,
height: document.documentEG o leG E ^ment.clientHeight
});
const on~ V 4 |Resize = useV y VCallback(()=>{
setSize({
width: document.documentElement.clientWidth,
height: do( J hcument.documentElement.clientHeight,
})
}, []);
useEffect(()=>{
windH H Q bow.addEventListener('resize', onResize, false);
return ()=>{
window.removeEventListener('resize', onResize, false)z g Y p ` 9 1 H Z;
}
}, [])
return size;
}

canvas 制作闪耀

还有一个问题就在 canvas 制作进程中,当屏幕 resize 的进程中会呈现闪耀的问题,如下图:

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

这是由于重绘画布的时分,咱们需求运用 clear^ w 2 ~Rect 来清空画布,此时的画布是空的,开端重绘需求相应的时刻,因此在视觉会呈现闪屏的情况。处理闪屏,其实便是怎样处理制作时刻较长的问题

咱们能够参阅 双缓存 的概念来处理这个问题,将制作进程交给了 缓存 canvas,这样页面中的 canvas 就省去了制作进程,) w S ) o S缓存 c% N anvas 并没有添加到页面,所以咱们就看不到制作进程,在 缓存 canvas 制作好之后,直接将其赋给页面原来的 c{ H M ) y ( Danvas 这样就处理了闪屏的问题。

// src/components/photoGallery/canvas.ts
class ImgToCanvas {
// ...
private cacheCanvas : any;
private context : any;
// ..i ^ d.
privatO a 7 0 R ~ u fe drawImg (type?: string) {
// 页面中 canvas
const context = this.context;
// ...
// 创立一个 缓存 canvas,并挂到实例特点 cacheCanvas 下
if (!this.cach8 g 9 (  5 +eCanvas) {
this.cacheCanvas = document.c, ; K d ureateElement("canvas");
}
// 设置 缓存 canvas 的宽高
this.cacheCanvas.r ^ k y ]width = this.cWidth;
this.cacheCanvas.heig( t a % - 2ht = this.cHeight;
// 创立画布
const tempCtx = this.cacheCanvas.getCoo Y y s & / 1ntext('2d')!;
// 运H r I g 4 3 + N +用 缓存 canvas 画图
tempCtw ? H Fx.drawImage(image, this.imgLeft, this.imgTopL d / u a u . q Q, this.cImgWidth, this.cImgHeight)o E Y c x [ #;
// 铲除画布,并将缓存 canvas 赋给 页面 canvas
requestAnimationFrame(() => {
this.clearLastCanvas(context);
context.drawImage(this.cacheCanvas, 0, 0);
})
// ...
}
}

小结

这篇文章收拾了一个仿水墨图片预览插件从零到一的完结w 6 { : d R进程,从 思路剖析代码结构划分首要逻辑的完结S 3 E ` 4 (几个方面阐述了一波。

经过这个插件的编写,笔者对于 canvas 的画图 ap) { E t ; } y n ri、如何处理 ca? j E j _ d Gnvas 绘图进程中呈现的图片闪耀的问题,以及对于 React Hooks 的一些用法有了大致的了解。

实不相瞒,想要个赞!

使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

参阅内容

  • webpack 学习文档
  • 运用双缓存处理 Canvas clearRect 引起的闪9 m 9 5 c C屏问题
  • 从零开端完结类 antd 分页器(三):发布npm