为什么会有这个组件
因为某天我在用电脑逛p站(pixiv)时,发现看图的效果不是那么令人满意,我希望看图时能全屏浏览,并且支持图片的放大缩小和拖拽,不知道是不是搜索关键字不对,逛了一圈发现莫得喜欢的轮子,刚好又有些思路而且闲着无聊,于是决定自己封装一个组件。最后的效果是这样的。好耶,2K曲面屏看纸片人老婆
截图半分钟,压图半小时
你也可以点这里在线查看效果(要用电脑端哦)
源码在这里,感谢这个项目让我想起了我的github账号(求一波star)
使用方法
注册
全局注册
1. npm install easy-preview 2. 在main.js中加入下面两句 import EasyPreview from "easy-preview"; Vue.use(EasyPreview); 3. 使用<EasyPreview>标签即可
局部注册
1. 使用npm install easy-preview安装插件 2. 在组件中注册EasyPreview 3. 使用<EasyPreview>标签即可
import EasyPreview from "easy-preview"; export default { name: 'app', components: {EasyPreview}, data () { return {} } }
使用
打开和隐藏的控制权交给组件内部
比较简单,只要传入一个img标签给插槽使用并传入img-src属性的值即可
<EasyPreview :img-src="imgSrc"> <img :src="imgSrc" width="500" style="border-radius: 10px" alt=""> </EasyPreview>
控制权不交给组件的使用
这个时候要传入一个属性options,并将options.controlByUsers置为true,此时插槽会失效,需要传入一个额外的属性:show-preview控制显示和隐藏,此时点击右上角自带的关闭按钮改为触发自定义的clickCloseButton和click-close-button 事件(两个都会触发),你可以选择监听事件并修改传入的show-preview的值。
<img :src="imgSrc" alt="" width="500" style="border-radius: 10px" @click="onclick"> <EasyPreview :img-src="imgSrc" :options="options" :show-preview="showPreview" @clickCloseButton="onClickCloseButton"></EasyPreview> { methods : { onclick() { this.showPreview = true } onClickCloseButton() { this.showPreview = false; } } }
参数说明
提供的全部可传入参数
属性名 | 含义 | 默认值 | 备注 |
---|---|---|---|
imgSrc | 浏览时的图片链接 | "" | |
options | 自定义选项 | null | 具体参数看下面 |
showPreview | 是否展示预览图 | false | 仅控制权不是组件内部时生效 |
clickCloseButton | 点击关闭按钮时会触发的自定义事件 | 仅控制权不是组件内部时生效 ,需要绑定回调函数 | |
click-close-button | 点击关闭按钮时会触发的自定义事件 | 仅控制权不是组件内部时生效 ,需要绑定回调函数 |
PS:clickCloseButton绑定的事件执行时会被传入一个函数,执行这个函数可以把图片恢复初始状态,调用时可以传入一个延迟执行的时间,这个时间默认是500ms(如果你没有修改transition的时间的话,最好不要修改它)
onClickCloseButton(reset) { this.showPreview = false; reset(500); },
options的几个可选项
属性名 | 含义 | 默认值 | 备注 |
---|---|---|---|
controlByUsers | 控制权是否交给组件外部 | false | |
showCloseButton | 是否显示右上角的关闭按钮 | true | |
showStatusExtraStyle | 展示状态时额外的样式 | "" | 可以传入对象或者字符串,样式优先级为内联级 |
hideStatusExtraStyle | 隐藏状态时额外的样式 | "" | 可以传入对象或者字符串,样式优先级为内联级 |
buttonExtraStyle | 右上的按钮没有hover时的额外样式 | "" | 可以传入对象或者字符串,样式优先级为内联级 |
buttonHoverExtraStyle | 右上的按钮hover时的额外样式 | "" | 可以传入对象或者字符串,样式优先级为内联级 |
实现思路
鼠标滚动缩放
鼠标滚动时先判断是上还是下,上是放大,下就是缩小,直接通过transform来放大缩小就行
放大时的处理
放大两次鼠标指着的位置(缩放中心transform-origin)不同,就要移动图片来保持放大后鼠标扔指着同个位置,就需要进行移动,移动的代码如下
this.magnification是现在的缩放倍数,this.prevOrigin.x 和 this.prevOrigin.y是上次的缩放中心
this.e 和 this.f 是图像水平和垂直方向的偏移量,对应transform: matrix(a, b, c, d, e, f) 的e和f
// 计算需要偏移的量 let moveX = (1 - this.magnification) * (originX - this.prevOrigin.x); let moveY = (1 - this.magnification) * (originY - this.prevOrigin.y); // 进行移动 this.e -= moveX; this.f -= moveY;
当然也不是每次放大都会保证鼠标指着的位置不变,在图像视觉大小小于屏幕大小(准确来说是父容器大小,但是全屏时大小一致)时,要做到放大是两边同时等距放大,所以在代码里有下面的处理
// 如果图片的视觉大小小于wrapper的大小,就把transform-origin取到图片中央,强制等距放大 if (imgVisualHeight < wrapperHeight) { originY = imgHeight / 2; } if (imgVisualWidth < wrapperWidth) { originX = imgWidth / 2; }
缩小时的处理
缩放倍率大于1.5
如果缩放倍率大于1.5,就尽可能保证缩小后鼠标扔指向相同的位置,但这并不是100%的,在缩放前要计算下次缩放后图片会不会出现图片比屏幕大,但是又有部分图片没有填充到图片的情况,代码如下
this.rate是放大或缩小一次的倍数,这里是1.05
// 如果现在的缩放倍率已经大于1.5 if (this.magnification > 1.5) { // 计算让鼠标能指向同个位置的修正X和Y let moveX = (1 - this.magnification) * (originX - this.prevOrigin.x); let moveY = (1 - this.magnification) * (originY - this.prevOrigin.y); // 计算下次图片放缩后的位置 const imgOffsetLeft = this.$refs.img.offsetLeft; const imgOffsetTop = this.$refs.img.offsetTop; const magnification = this.magnification / this.rate; const x = this.originX; const y = this.originY; const e = this.e - moveX; const f = this.f - moveY; const nextImgVisualWidth = imgWidth * magnification; const nextImgVisualHeight = imgHeight * magnification; // 图片的视觉左边 / 顶边 / 右边 / 底边离wrapper左边 / 顶边 / 右边 / 底边的距离 const left = (magnification - 1) * x - e - imgOffsetLeft; const top = (magnification - 1) * y - imgOffsetTop - f; const right = nextImgVisualWidth - left - wrapperWidth; const bottom = nextImgVisualHeight - top - wrapperHeight;
计算出图片四边离容器四边的距离后,就开始处理有空白的情况,限于篇幅仅展示x轴方向的处理
// 如果缩放后的图片比wrapper的宽,同时图片左边没有顶到wrapper的左边 if (nextImgVisualWidth > wrapperWidth && left < 0) { // 让图片的左边能顶到wrapper的左边 moveX -= left; } else if (nextImgVisualWidth > wrapperWidth && right < 0) { // 让图片的左边能顶到wrapper的右边 moveX += right; }
还有另外一种情况,就是尽管缩放倍数大于1.5,但图片的宽或高的某一边仍然不能占据完屏幕,这个时候也要保证缩小后两边的间隙相同,处理在下面
// 如果图片的大小已经小于wrapper的大小 // 要让图片两边的空白相同 if (nextImgVisualWidth < wrapperWidth) { // 计算要移动多少才能让两边的空白相同 let average = (left + right) / 2; let diff = left - average; // 移动过去 moveX -= diff; } if (nextImgVisualHeight < wrapperHeight) { let average = (top + bottom) / 2; let diff = top - average; moveY -= diff; }
计算完要偏移的量就可以偏移了
// 修正位置 this.e -= moveX; this.f -= moveY;
缩放倍率小于1.5
这时开始逐渐恢复图片,即将e和f归0,但是不能一下子归0,所以我们用下面的公式计算这次移动的多少
// 本次移动距离 = 还需移动的长度 ÷ 还能移动的次数 let moveX = -this.e / this.optionCount; let moveY = -this.f / this.optionCount;
这样会显得比较"循序渐进",当然移动后可能出现上面说的,图片宽高大于屏幕宽高但该方向没占满屏幕,或者图片宽高小于屏幕宽高,移动后两边的间隙不同的情况,所以还是需要进行相同的处理,因为上面已经放过代码了,这里就不再说一次了。
鼠标拖动移动图片
估计不少人都做过拖拽移动吧,这里用的也是类似的原理,在mousedown中记录坐标,在mousemove中计算偏移,然后修改x和y方向的偏移,这里要说的就是一些边缘判断
在进行真正的移动前,要计算这么移动后会不会出现图片大于屏幕,但是又出现空白的情况,即,移动距离 = Math.min(图片的边缘屏幕边缘的距离, 鼠标离上一次位置的偏移量),这样就可以保证不会越界,如果图片大小已经小于屏幕大小,就不能在对应方向上移动了。
代码如下
// 水平移动的方向 let hd = translateX > 0 ? "right" : "left"; // 垂直移动方向 let vd = translateY > 0 ? "down" : "up"; .... // 省略了计算这四个值的代码 const left = (magnification - 1) * x - e - imgOffsetLeft; const top = (magnification - 1) * y - imgOffsetTop - f; const right = nextImgVisualWidth - left - wrapperWidth; const bottom = nextImgVisualHeight - top - wrapperHeight; // 判断图片能否向左 / 右 / 上 / 下 移动 // 判断的依据是往该方向移动后是否出现空隙 let leftAble = right > 0; let rightAble = left > 0; let upAble = bottom > 0; let downAble = top > 0; // 如果水平移动方向是左 if (hd === "left") { if (leftAble) { // 计算最多能偏移的大小, 超过这个大小会出现间隙 translateX = -Math.min(Math.abs(translateX), right); } else { // 如果不能偏移就把偏移量置为0 translateX = 0; } } // 其他几个方向的处理同理 ......
完整代码(带注释)点 这里