为什么会有这个组件
因为某天我在用电脑逛p站(pixiv)时,发现看图的效果不是那么令人满意,点开一个图片后居然不能放大,上移下移要通过鼠标滚轮,感觉有点反人类,我希望在网页看图时能全屏浏览图片,并且支持图片的放大缩小和拖拽,不知道是不是搜索关键字不对,逛了一圈发现莫得喜欢的轮子,刚好又有些思路而且闲着无聊,于是决定自己封装一个组件。最后的效果是这样的。好耶,2K曲面屏看纸片人老婆

截图半分钟,压图半小时
你也可以点这里在线查看效果(要用电脑端哦)
源码在这里,感谢这个项目让我想起了我的github账号(求一波star)
使用方法
注册
全局注册
1. npm install easy-preview -D
2. 在main.js中加入下面两句
import EasyPreview from "easy-preview";
Vue.use(EasyPreview);
3. 使用<EasyPreview>标签即可
局部注册
1. 使用npm install easy-preview -D安装插件
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;
}
}
// 其他几个方向的处理同理
......
总体来说思路还是简单的,就是写的时候有点坑233
完整代码(带详细注释)点 这里
后记
为什么有这个后记呢,因为不写点啥就会显得结束地很突兀,所以就写这个后记来缓冲一下。 嗯嗯,那么下次见。