├── .gitignore ├── src ├── images │ ├── demo.gif │ ├── liya.jpg │ ├── pinch.png │ ├── rain.jpg │ ├── swipe.png │ ├── favicon.ico │ ├── kalaqiu.jpg │ ├── qrcode.png │ ├── spread.png │ ├── vertical.png │ ├── baiyuekui.jpg │ ├── cyberpunk.jpg │ ├── trajectory.png │ └── jinglingwangzuo.jpg ├── css │ └── noname-gallery.css └── js │ └── noname-gallery.js ├── .editorconfig ├── guide.txt ├── README.md ├── index.html └── dist ├── noname-gallery.min.js └── noname-gallery.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | .idea/ -------------------------------------------------------------------------------- /src/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/demo.gif -------------------------------------------------------------------------------- /src/images/liya.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/liya.jpg -------------------------------------------------------------------------------- /src/images/pinch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/pinch.png -------------------------------------------------------------------------------- /src/images/rain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/rain.jpg -------------------------------------------------------------------------------- /src/images/swipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/swipe.png -------------------------------------------------------------------------------- /src/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/favicon.ico -------------------------------------------------------------------------------- /src/images/kalaqiu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/kalaqiu.jpg -------------------------------------------------------------------------------- /src/images/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/qrcode.png -------------------------------------------------------------------------------- /src/images/spread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/spread.png -------------------------------------------------------------------------------- /src/images/vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/vertical.png -------------------------------------------------------------------------------- /src/images/baiyuekui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/baiyuekui.jpg -------------------------------------------------------------------------------- /src/images/cyberpunk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/cyberpunk.jpg -------------------------------------------------------------------------------- /src/images/trajectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/trajectory.png -------------------------------------------------------------------------------- /src/images/jinglingwangzuo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18223781723/noname-gallery/HEAD/src/images/jinglingwangzuo.jpg -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/css/noname-gallery.css: -------------------------------------------------------------------------------- 1 | /* 容器 */ 2 | .noname-gallery-container { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | z-index: 1000; 9 | overflow: hidden; 10 | user-select: none; 11 | touch-action: none; 12 | } 13 | 14 | /* 背景 */ 15 | .noname-gallery-bg { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | z-index: -1; 22 | background: #000; 23 | } 24 | 25 | /* 计数器 */ 26 | .noname-gallery-counter { 27 | position: absolute; 28 | top: 15px; 29 | left: 15px; 30 | z-index: 1; 31 | font-size: 14px; 32 | color: #F9F9F9; 33 | pointer-events: none; 34 | } 35 | 36 | .noname-gallery-wrap { 37 | display: flex; 38 | margin: 0; 39 | padding: 0; 40 | list-style: none; 41 | } 42 | 43 | .noname-gallery-wrap li { 44 | width: 100%; 45 | height: 100vh; 46 | overflow: hidden; 47 | } 48 | 49 | .noname-gallery-img { 50 | display: block; 51 | transform-origin: left top; 52 | object-fit: cover; 53 | /* 使用-webkit-user-drag: none;会导致华为浏览器长按不显示拖动缩略图 */ 54 | } -------------------------------------------------------------------------------- /guide.txt: -------------------------------------------------------------------------------- 1 | 单击(移动距离小于10) 2 | PC端单击图片区域进行缩放 3 | PC端单击非图片区域关闭画廊 4 | 移动端单击关闭画廊 5 | 6 | 双击(两次距离小于30) 7 | PC端不响应双击事件 8 | 移动端双击缩放图片 9 | 10 | 区分拖拽目标(dragTarget) 11 | 如果图片未进行缩放操作,且拖拽为水平方向,dragTarget = wrap 12 | 如果图片已进行缩放操作,但宽高未超过屏幕宽高,且拖拽为水平方向,dragTarget = wrap 13 | 如果图片已进行缩放操作,且宽高超过屏幕宽高,且拖拽为水平方向(向左拖拽),且图片处在右边界,dragTarget = wrap 14 | 如果图片已进行缩放操作,且宽高超过屏幕宽高,且拖拽为水平方向(向右拖拽),且图片处在左边界,dragTarget = wrap 15 | 如果图片未进行缩放操作,且拖拽未垂直方向,dragTarget = img 16 | 如果图片已放大,且宽高超过屏幕宽高,且图片不处在左右边界,dragTarget = img 17 | 18 | 拖拽(移动距离大于10) 19 | 当拖拽目标为wrap时,即dragTarget = wrap,水平方向拖动距离大于屏幕宽度10%,即可切换下一张图片 20 | 当拖拽目标为img时,即dragTarget = img,垂直方向拖动距离大于屏幕高度10%,且图片未进行缩放操作,即可关闭画廊 21 | 22 | 双指 23 | 当拖拽目标为wrap时,即dragTarget = wrap,可双指交替滑动,滑动边界为上一张左边界,当前,下一张右边界 24 | 当拖拽目标为img时,即dragTarget = img,双指可以缩放图片 25 | 当拖拽目标为img时,即dragTarget = img,且拖拽为垂直方向,双指可交替拖拽 26 | 27 | 动画 28 | 实现方式分为requestAnimationFrame和transition 29 | 由于实现方式的不同,动画分为可打断的动画和不可打断的动画。可打断的动画即动画过程中,可以响应用户的操作。反之则为不可打断的动画。(transition实现的动画也是可以打断的,只不过需要写个函数计算当前动画的值) 30 | 其实完全可以使用requestAnimationFrame实现动画,从而让所有动画都可以打断,但是由于transition在移动端具有更好的兼容性,动画更流畅。所以为了统一交互,只能做出取舍。 31 | 32 | 可打断的动画如下 33 | wrap滑动切换图片动画(用户可快速滑动) 34 | 图片缩放后,宽高有一个超过屏幕宽高,用户滑动执行惯性滚动动画,用户点击屏幕或键盘切换图片时会停止惯性滚动 35 | 36 | 不可打断的动画如下 37 | 开始动画 38 | 单击、双击缩放动画 39 | 双指缩小、垂直滑动未关闭的恢复动画 40 | wrap滑动切换图片时,上一张图片如果放大过的恢复动画 41 | 关闭动画 42 | 43 | 遇到的问题 44 | 使用transition同时缩放宽高和transform: translate3d()时,移动端微信浏览器的动画存在抖动和轨迹偏移的问题。如果用top,left代替,则不会出现此问题。但性能不如transform: translate。demo:http://jsdemo.codeman.top/html/transition.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # noname-gallery 2 | JavaScript image gallery, easy to use, no dependencies. 3 | 4 | ![demo](https://github.com/18223781723/noname-gallery/blob/main/src/images/demo.gif) 5 | 6 | # Demo 7 | Demo: http://nonamegallery.codeman.top 8 | 9 | ![二维码](https://github.com/18223781723/noname-gallery/blob/main/src/images/qrcode.png) 10 | 11 | # Getting Started 12 | In a browser: 13 | ```javascript 14 | 15 | 16 | 17 | ``` 18 | 19 | # Example 20 | ```javascript 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 47 | ``` 48 | 49 | # Options 50 | | Params | Type | Defaults | Description | 51 | | :---- | :---- | :---- | :---- | 52 | | options | object | | 配置项 | 53 | | options.list | array | HTMLImageElement[] | 图片列表,必填参数 | 54 | | options.index | number | 0 | 索引 | 55 | | options.fadeInOut | boolean | true | 动画淡入淡出,当缩略图和预览尺寸不匹配时,建议开启 | 56 | | options.useTransform | boolean | true | 使用transform或宽高缩放 | 57 | | options.verticalZoom | boolean | true | 垂直滑动时缩小图片 | 58 | | options.openKeyboard | boolean | false | 开启键盘控制,esc关闭画廊,方向键切换图片 | 59 | | options.zoomToScreenCenter | boolean | false | 将放大区域移动至屏幕中心显示 | 60 | | options.duration | number | 300 | 动画持续时间,单位ms | 61 | | options.minScale | number | 1.5 | 最小放大倍数 | 62 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | NonameGallery 10 | 11 | 12 | 83 | 84 | 85 | 86 |
87 |

NonameGallery v1.0.0

88 |

基于JavaScript开发的图片预览插件,支持PC端和移动端,兼容主流浏览器,简单易用,零依赖。

89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 | 99 | 121 | 122 |

手势支持

123 |

支持所有基本手势,包括单击关闭画廊,双击缩放图片,双指缩放图片,左右滑动切换图片。

124 |
125 | 126 |
127 |

双指放大图片

128 |

当用户两根手指分别向外扩展时,图片则会相应放大。最大放大尺寸会在图片实际宽高和图片预览宽高*1.5(用户可配置)两者中取较大值。 129 |

130 |
131 |
132 |
133 | 134 |
135 |

双指缩小图片

136 |

当用户两根手指分别向内收缩时,图片则会相应缩小。最小尺寸为图片预览宽高*0.7(系统默认值)。 137 |

138 |
139 |
140 |
141 | 142 |
143 |

水平滑动切换图片

144 |

当用户单个手指水平滑动距离超过屏幕宽度*0.1时,则会切换图片。如果图片为放大状态且宽度大于屏幕宽度,则需要先滑动到图片边界。 145 |

146 |
147 |
148 |
149 | 150 |
151 |

垂直滑动关闭画廊

152 |

当用户单个手指垂直滑动距离超过屏幕高度*0.1时,会关闭画廊,如果图片为放大状态,则会响应拖动查看图片事件。 153 |

154 |
155 |
156 |

PC端如何操作

157 |

PC端操作基本类似,支持单击关闭画廊,点击图片缩放,增加键盘控制,ESC关闭画廊,方向键切换图片。

158 |
159 | 160 | 161 | 162 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /dist/noname-gallery.min.js: -------------------------------------------------------------------------------- 1 | !function(){function t(t){if(0===t.list.length)throw new Error("options.list can not be empty array");this.options=Object.assign({},this.defaults,t)}t.prototype.defaults={list:[],index:0,fadeInOut:!0,useTransform:!0,verticalZoom:!0,openKeyboard:!1,zoomToScreenCenter:!1,duration:300,minScale:1.5},t.prototype.init=function(){this.setProperties(),this.setWindowSize(),this.setPreviewList(),this.setWrap(),this.render(),this.getElement(),this.bindEventListener(),this.open()},t.prototype.setProperties=function(){this.container=null,this.bg=null,this.counter=null,this.wrap=null,this.imgList=null,this.bgOpacity=1,this.windowWidth=0,this.windowHeight=0,this.index=this.options.index,this.wrapWidth=0,this.wrapX=0,this.previewList=[],this.currentImg={x:0,y:0,width:0,height:0,scale:1,opacity:1,status:""},this.pointers=[],this.point1={x:0,y:0},this.point2={x:0,y:0},this.diff={x:0,y:0},this.distance={x:0,y:0},this.lastDistance={x:0,y:0},this.lastPoint1={x:0,y:0},this.lastPoint2={x:0,y:0},this.lastMove={x:0,y:0},this.lastCenter={x:0,y:0},this.tapCount=0,this.dragDirection="",this.dragTarget="",this.status="",this.isPointerdown=!1,this.isAnimating=!0,this.isWrapAnimating=!1,this.tapTimeout=null,this.pointerdownTime=null,this.pointermoveTime=null,this.pinchTime=null,this.inertiaRafId=null,this.wrapRafId=null},t.prototype.setWindowSize=function(){this.windowWidth=window.innerWidth,this.windowHeight=window.innerHeight},t.prototype.setPreviewList=function(){for(const t of this.options.list){const i=t.getBoundingClientRect(),e=this.getImgSize(t.naturalWidth,t.naturalHeight,this.windowWidth,this.windowHeight),s=Math.max(this.decimal(t.naturalWidth/e.width,5),this.options.minScale);this.previewList.push({x:this.decimal((this.windowWidth-e.width)/2,2),y:this.decimal((this.windowHeight-e.height)/2,2),width:this.decimal(e.width,2),height:this.decimal(e.height,2),maxWidth:this.decimal(e.width*s,2),maxHeight:this.decimal(e.height*s,2),maxScale:s,thumbnail:{x:this.decimal(i.left,2),y:this.decimal(i.top,2),width:this.decimal(i.width,2),height:this.decimal(i.height,2),scaleX:this.decimal(i.width/e.width,5),scaleY:this.decimal(i.height/e.height,5)}})}},t.prototype.setCurrentImg=function(t,i,e,s,n,h,r){this.currentImg={x:t,y:i,width:e,height:s,scale:n,opacity:h,status:r}},t.prototype.setWrap=function(){this.wrapWidth=this.previewList.length*this.windowWidth,this.wrapX=this.index*this.windowWidth*-1},t.prototype.render=function(){let t="opacity: 0;";this.options.useTransform&&(t+="transition: opacity "+this.options.duration+"ms ease-out;");let i='",document.body.insertAdjacentHTML("beforeend",i)},t.prototype.getElement=function(){this.container=document.querySelector(".noname-gallery-container"),this.bg=document.querySelector(".noname-gallery-bg"),this.counter=document.querySelector(".noname-gallery-counter"),this.wrap=document.querySelector(".noname-gallery-wrap"),this.imgList=document.querySelectorAll(".noname-gallery-img");for(let t=0,i=this.imgList.length;t1&&(Math.abs(this.point1.x-this.lastPoint1.x)>30||Math.abs(this.point1.y-this.lastPoint1.y)>30)&&(this.tapCount=1),clearTimeout(this.tapTimeout),window.cancelAnimationFrame(this.inertiaRafId),window.cancelAnimationFrame(this.wrapRafId)):2===this.pointers.length&&(this.tapCount=0,this.point2={x:this.pointers[1].clientX,y:this.pointers[1].clientY},this.lastCenter=this.getCenter(this.point1,this.point2),this.lastDistance={x:this.distance.x,y:this.distance.y},this.lastPoint2={x:this.pointers[1].clientX,y:this.pointers[1].clientY},""===this.dragTarget&&(this.dragTarget="img")),this.lastPoint1={x:this.pointers[0].clientX,y:this.pointers[0].clientY})},t.prototype.handlePointermove=function(t){if(!this.isPointerdown)return;this.handlePointers(t,"update");const i={x:this.pointers[0].clientX,y:this.pointers[0].clientY};if(1===this.pointers.length)this.diff={x:i.x-this.lastMove.x,y:i.y-this.lastMove.y},this.distance={x:i.x-this.point1.x+this.lastDistance.x,y:i.y-this.point1.y+this.lastDistance.y},this.lastMove={x:i.x,y:i.y},this.pointermoveTime=Date.now(),(Math.abs(this.distance.x)>10||Math.abs(this.distance.y)>10)&&(this.tapCount=0,""===this.dragDirection&&""===this.dragTarget&&(this.getDragDirection(),this.getDragTarget())),"wrap"===this.dragTarget?this.handleWrapPointermove():"img"===this.dragTarget&&this.handleImgPointermove();else if(2===this.pointers.length){const t={x:this.pointers[1].clientX,y:this.pointers[1].clientY};"img"===this.dragTarget&&"verticalToClose"!==this.currentImg.status&&this.handlePinch(i,t),this.lastPoint1={x:i.x,y:i.y},this.lastPoint2={x:t.x,y:t.y}}t.preventDefault()},t.prototype.handlePointerup=function(t){this.isPointerdown&&(this.handlePointers(t,"delete"),0===this.pointers.length?(this.isPointerdown=!1,0===this.tapCount?"wrap"===this.dragTarget?this.handleWrapPointerup():"img"===this.dragTarget&&this.handleImgPointerup():1===this.tapCount?"mouse"===t.pointerType?t.clientX>=this.currentImg.x&&t.clientX<=this.currentImg.x+this.currentImg.width&&t.clientY>=this.currentImg.y&&t.clientY<=this.currentImg.y+this.currentImg.height?this.handleZoom({x:t.clientX,y:t.clientY}):this.close():Date.now()-this.pointerdownTime<500?this.tapTimeout=setTimeout((()=>{this.close()}),250):this.tapCount=0:this.tapCount>1&&this.handleZoom({x:t.clientX,y:t.clientY})):1===this.pointers.length&&(this.point1={x:this.pointers[0].clientX,y:this.pointers[0].clientY},this.lastMove={x:this.pointers[0].clientX,y:this.pointers[0].clientY}))},t.prototype.handlePointercancel=function(t){this.tapCount=0,this.isPointerdown=!1,this.pointers.length=0,this.isWrapAnimating&&this.handleWrapPointerup()},t.prototype.handlePointers=function(t,i){for(let e=0;e0?this.index--:["ArrowRight","ArrowDown"].includes(t.key)&&i1){if(this.options.useTransform)i.element.style.transition="transform "+this.options.duration+"ms ease-out, opacity "+this.options.duration+"ms ease-out",i.element.style.transform="translate3d("+i.x+"px,"+i.y+"px, 0) scale(1)";else{const t={img:{width:{from:this.currentImg.width,to:i.width},height:{from:this.currentImg.height,to:i.height},x:{from:this.currentImg.x,to:i.x},y:{from:this.currentImg.y,to:i.y},index:this.index}};this.raf(t)}i.element.style.cursor="zoom-in",this.setCurrentImg(i.x,i.y,i.width,i.height,1,1,"")}else{const e=this.windowWidth/2,s=this.windowHeight/2,n=this.decimal((t.x-i.x)*i.maxScale,2),h=this.decimal((t.y-i.y)*i.maxScale,2);let r,o;if(i.maxWidth>this.windowWidth?(r=this.options.zoomToScreenCenter?e-n:t.x-n,r>0?r=0:rthis.windowHeight?(o=this.options.zoomToScreenCenter?s-h:t.y-h,o>0?o=0:oh.maxScale?(s=h.maxScale/this.currentImg.scale,this.currentImg.scale=h.maxScale,this.currentImg.width=h.maxWidth,this.currentImg.height=h.maxHeight):nthis.windowWidth?this.currentImg.x>0?this.currentImg.x=0:this.currentImg.xthis.windowHeight?this.currentImg.y>0?this.currentImg.y=0:this.currentImg.y=e/s?t>e?(n=e,h=e/t*i):(n=t,h=i):i>s?(n=s/i*t,h=s):(n=t,h=i),{width:n,height:h}},t.prototype.getDragDirection=function(){Math.abs(this.distance.x)>Math.abs(this.distance.y)?this.dragDirection="h":this.dragDirection="v"},t.prototype.getDragTarget=function(){let t=!1,i=!1;this.currentImg.width>this.windowWidth?(this.diff.x>0&&0===this.currentImg.x||this.diff.x<0&&this.currentImg.x===this.windowWidth-this.currentImg.width)&&(t=!0):this.currentImg.width>=this.previewList[this.index].width&&(i=!0),"h"===this.dragDirection&&(t||i)?this.dragTarget="wrap":this.dragTarget="img"},t.prototype.getDistance=function(t,i){const e=t.x-i.x,s=t.y-i.y;return Math.hypot(e,s)},t.prototype.handleWrapPointermove=function(){if(this.wrapX>0||this.wrapX<(this.previewList.length-1)*this.windowWidth*-1)this.wrapX+=.3*this.diff.x;else{this.wrapX+=this.diff.x;const t=(this.index-1)*this.windowWidth*-1,i=(this.index+1)*this.windowWidth*-1;this.wrapX>t?this.wrapX=t:this.wrapXthis.windowWidth||this.currentImg.height>this.windowHeight?(this.currentImg.x+=this.diff.x,this.currentImg.y+=this.diff.y,this.handleBoundary(),this.currentImg.status="inertia",this.options.useTransform?(t.element.style.transition="none",t.element.style.transform="translate3d("+this.currentImg.x+"px, "+this.currentImg.y+"px, 0) scale("+this.currentImg.scale+")"):t.element.style.transform="translate3d("+this.currentImg.x+"px, "+this.currentImg.y+"px, 0)"):"v"===this.dragDirection&&this.currentImg.width<=t.width&&this.currentImg.height<=t.height&&(this.currentImg.status="verticalToClose",this.bgOpacity=this.decimal(1-Math.abs(this.distance.y)/(this.windowHeight/1.2),5),this.bgOpacity<0&&(this.bgOpacity=0),this.options.verticalZoom?(this.currentImg.scale=this.bgOpacity,this.currentImg.width=this.decimal(t.width*this.currentImg.scale,2),this.currentImg.height=this.decimal(t.height*this.currentImg.scale,2),this.currentImg.x=t.x+this.distance.x+(t.width-this.currentImg.width)/2,this.currentImg.y=t.y+this.distance.y+(t.height-this.currentImg.height)/2):(this.currentImg.x=t.x,this.currentImg.y=t.y+this.distance.y,this.currentImg.scale=1),this.bg.style.opacity=this.bgOpacity,this.options.useTransform?(this.bg.style.transition="none",t.element.style.transition="none",t.element.style.transform="translate3d("+this.currentImg.x+"px, "+this.currentImg.y+"px , 0) scale("+this.currentImg.scale+")"):(t.element.style.width=this.currentImg.width+"px",t.element.style.height=this.currentImg.height+"px",t.element.style.transform="translate3d("+this.currentImg.x+"px, "+this.currentImg.y+"px , 0)"))},t.prototype.handleWrapPointerup=function(){const t=Math.round(.1*this.windowWidth),i=this.index;Math.abs(this.distance.x)>t&&(this.distance.x>0&&i>0?this.index--:this.distance.x<0&&i1e3)this.handleInertia();else if("verticalToClose"===this.currentImg.status&&Math.abs(this.distance.y)>=t)this.close();else if("shrink"===this.currentImg.status||"verticalToClose"==this.currentImg.status&&Math.abs(this.distance.y)1||Math.abs(i.y)>1)&&(e.inertiaRafId=window.requestAnimationFrame(s))}))},t.prototype.handleWrapSwipe=function(){this.isWrapAnimating=!0;const t={wrap:{x:{from:this.wrapX,to:this.windowWidth*this.index*-1}}};this.wrapRaf(t),this.counter.innerHTML=this.index+1+" / "+this.previewList.length},t.prototype.handleLastImg=function(t){if(this.index!==t){if(this.currentImg.scale>1){const i=this.previewList[t];if(this.options.useTransform)i.element.style.transition="transform "+this.options.duration+"ms ease-out, opacity "+this.options.duration+"ms ease-out",i.element.style.transform="translate3d("+i.x+"px, "+i.y+"px, 0) scale(1)";else{const e={img:{width:{from:this.currentImg.width,to:i.width},height:{from:this.currentImg.height,to:i.height},x:{from:this.currentImg.x,to:i.x},y:{from:this.currentImg.y,to:i.y},index:t}};this.raf(e)}i.element.style.cursor="zoom-in"}const i=this.previewList[this.index];this.setCurrentImg(i.x,i.y,i.width,i.height,1,1,"")}},t.prototype.handleTransitionEnd=function(t){"IMG"===t.target.tagName&&(t.target===this.previewList[this.index].element&&(this.isAnimating=!1,this.dragTarget="",this.dragDirection=""),"close"===this.status&&(this.unbindEventListener(),this.container.remove()))},t.prototype.decimal=function(t,i){const e=Math.pow(10,i);return Math.round(t*e)/e},t.prototype.getCenter=function(t,i){return{x:(t.x+i.x)/2,y:(t.y+i.y)/2}},t.prototype.easeOut=function(t,i,e,s){const n=e/s;return-(i-t)*n*(n-2)+t},t.prototype.raf=function(t){const i=this;let e,s=0;const n=this.options.duration,h=this.previewList[t.img.index];window.requestAnimationFrame((function r(o){void 0===e&&(e=o);let a=o-e;if(a>n&&(a=n,s++),t.bg){const e=i.decimal(i.easeOut(t.bg.opacity.from,t.bg.opacity.to,a,n),5);i.bg.style.opacity=e}if(t.img.opacity){const e=i.decimal(i.easeOut(t.img.opacity.from,t.img.opacity.to,a,n),5);h.element.style.opacity=e}const d=i.decimal(i.easeOut(t.img.width.from,t.img.width.to,a,n),2),l=i.decimal(i.easeOut(t.img.height.from,t.img.height.to,a,n),2),m=i.decimal(i.easeOut(t.img.x.from,t.img.x.to,a,n),2),c=i.decimal(i.easeOut(t.img.y.from,t.img.y.to,a,n),2);h.element.style.width=d+"px",h.element.style.height=l+"px",h.element.style.transform="translate3d("+m+"px, "+c+"px, 0)",s<=1?window.requestAnimationFrame(r):(t.img.index===i.index&&(i.isAnimating=!1,i.dragTarget="",i.dragDirection=""),"close"===i.status&&(i.unbindEventListener(),i.container.remove()))}))},t.prototype.wrapRaf=function(t){const i=this;let e,s=0;const n=this.options.duration;this.wrapRafId=window.requestAnimationFrame((function h(r){void 0===e&&(e=r);let o=r-e;o>n&&(o=n,s++),i.wrapX=i.decimal(i.easeOut(t.wrap.x.from,t.wrap.x.to,o,n),2),i.wrap.style.transform="translate3d("+i.wrapX+"px, 0, 0)",s<=1?i.wrapRafId=window.requestAnimationFrame(h):(i.isWrapAnimating=!1,i.dragTarget="",i.dragDirection="")}))},"function"==typeof define&&define.amd?define((function(){return t})):"object"==typeof module&&"object"==typeof exports?module.exports=t:window.NonameGallery=t}(); -------------------------------------------------------------------------------- /src/js/noname-gallery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 使用PointerEvent实现的图片预览插件 3 | * @param {object} options 配置项 4 | */ 5 | function NonameGallery(options) { 6 | // 抛出错误 7 | if (options.list.length === 0) { 8 | throw new Error('options.list can not be empty array'); 9 | } 10 | this.options = Object.assign({}, this.defaults, options); 11 | } 12 | /** 13 | * 默认配置项 14 | */ 15 | NonameGallery.prototype.defaults = { 16 | list: [], // HTMLImageElement[] 17 | index: 0, // 索引 18 | fadeInOut: true, // 淡入淡出 19 | useTransform: true, // 宽高缩放只能使用requestAnimationFrame,transition同时过渡宽高和transform时会发生抖动以及动画轨迹偏移的问题,手机端微信浏览器尤其严重 20 | verticalZoom: true, // 垂直滑动缩放图片 21 | openKeyboard: false, // 开启键盘 esc关闭,方向键切换图片 22 | zoomToScreenCenter: false, // 将放大区域移动到屏幕中心显示 23 | duration: 300, // 动画持续时间 24 | minScale: 1.5 // 最小放大倍数 25 | } 26 | /** 27 | * 初始化 28 | */ 29 | NonameGallery.prototype.init = function () { 30 | // 设置属性值 31 | this.setProperties(); 32 | // 设置视口大小 33 | this.setWindowSize(); 34 | // 设置previewList 35 | this.setPreviewList(); 36 | // 设置wrap宽度和x轴偏移量 37 | this.setWrap(); 38 | // 渲染 39 | this.render(); 40 | // 获取元素 41 | this.getElement(); 42 | // 绑定事件 43 | this.bindEventListener(); 44 | // 打开画廊 45 | this.open(); 46 | } 47 | /** 48 | * 设置属性值(防止全局实例,多次调用出现问题) 49 | */ 50 | NonameGallery.prototype.setProperties = function () { 51 | this.container = null; // .noname-gallery-container 52 | this.bg = null; // .noname-gallery-bg 53 | this.counter = null; // .noname-gallery-counter 54 | this.wrap = null; // .noname-gallery-wrap 55 | this.imgList = null; // .noname-gallery-img 56 | this.bgOpacity = 1; // .noname-gallery-bg 透明度 57 | this.windowWidth = 0; // 视口宽度 58 | this.windowHeight = 0; // 视口高度 59 | this.index = this.options.index; // 预览图片索引 60 | this.wrapWidth = 0; // .noname-gallery-wrap 宽度 61 | this.wrapX = 0; // .noname-gallery-wrap x轴偏移量 62 | this.previewList = []; // 预览图片列表 63 | this.currentImg = { 64 | x: 0, // 当前图片旋转中心(已设置为左上角)相对屏幕左上角偏移值 65 | y: 0, // 当前图片旋转中心(已设置为左上角)相对屏幕左上角偏移值 66 | width: 0, // 当前图片宽度 67 | height: 0, // 当前图片高度 68 | scale: 1, // 当前图片缩放倍数 69 | opacity: 1, // 当前图片透明度 开启淡入淡出时会使用到 70 | status: '' // 当前图片状态 shrink(scale < 1) verticalToClose inertia 71 | }; 72 | this.pointers = []; // 指针数组用于保存多个触摸点 73 | this.point1 = { x: 0, y: 0 }; // 第一个触摸点 74 | this.point2 = { x: 0, y: 0 }; // 第二个触摸点 75 | this.diff = { x: 0, y: 0 }; // 相对于上一次移动差值 76 | this.distance = { x: 0, y: 0 }; // 移动距离 77 | this.lastDistance = { x: 0, y: 0 }; // 双指滑动时记录上一次移动距离 78 | this.lastPoint1 = { x: 0, y: 0 }; // 上一次第一个触摸点位置,用于判断双击距离是否大于30 79 | this.lastPoint2 = { x: 0, y: 0 }; // 上一次第二个触摸点位置 80 | this.lastMove = { x: 0, y: 0 }; // 上一次移动坐标 81 | this.lastCenter = { x: 0, y: 0 }; // 上一次双指中心位置 82 | this.tapCount = 0; // 点击次数 1 = 单击 大于1 = 双击 83 | this.dragDirection = ''; // 拖拽方向 v(vertical) h(horizontal) 84 | this.dragTarget = ''; // 拖拽目标 wrap img 85 | this.status = ''; // close 时移除dom 86 | this.isPointerdown = false; // 按下标识 87 | this.isAnimating = true; // 是否正在执行动画 88 | this.isWrapAnimating = false; // 是否正在执行wrap切换动画,不响应键盘事件 89 | this.tapTimeout = null; // 单击延时器 250ms 判断双击 90 | this.pointerdownTime = null; // pointerdown time 91 | this.pointermoveTime = null; // 鼠标松开距离最后一次移动小于200ms执行惯性滑动 92 | this.pinchTime = null; // 距离上一次双指缩放的时间 93 | this.inertiaRafId = null; // requestAnimationFrame id 用于停止惯性滑动动画 94 | this.wrapRafId = null; // requestAnimationFrame id 用于停止wrap滑动动画 95 | } 96 | /** 97 | * 设置视口大小 98 | */ 99 | NonameGallery.prototype.setWindowSize = function () { 100 | this.windowWidth = window.innerWidth; 101 | this.windowHeight = window.innerHeight; 102 | } 103 | /** 104 | * 设置previewList 105 | */ 106 | NonameGallery.prototype.setPreviewList = function () { 107 | for (const img of this.options.list) { 108 | const rect = img.getBoundingClientRect(); 109 | const result = this.getImgSize(img.naturalWidth, img.naturalHeight, this.windowWidth, this.windowHeight); 110 | const maxScale = Math.max(this.decimal(img.naturalWidth / result.width, 5), this.options.minScale); 111 | this.previewList.push({ 112 | x: this.decimal((this.windowWidth - result.width) / 2, 2), // 预览图片左上角相对于视口的横坐标 113 | y: this.decimal((this.windowHeight - result.height) / 2, 2), // 预览图片左上角相对于视口的纵坐标 114 | width: this.decimal(result.width, 2), // 预览图片显示宽度 115 | height: this.decimal(result.height, 2), // 预览图片显示高度 116 | maxWidth: this.decimal(result.width * maxScale, 2), // 预览图片最大宽度 117 | maxHeight: this.decimal(result.height * maxScale, 2), // 预览图片最大高度 118 | maxScale: maxScale, // 预览图片最大缩放比例 119 | thumbnail: { 120 | x: this.decimal(rect.left, 2), // 缩略图左上角相对于视口的横坐标 121 | y: this.decimal(rect.top, 2), // 缩略图左上角相对于视口的纵坐标 122 | width: this.decimal(rect.width, 2), // 缩略图显示宽度 123 | height: this.decimal(rect.height, 2), // 缩略图显示高度 124 | scaleX: this.decimal(rect.width / result.width, 5), // 缩略图x轴比例 125 | scaleY: this.decimal(rect.height / result.height, 5) // 缩略图y轴比例 126 | } 127 | }); 128 | } 129 | } 130 | /** 131 | * 设置当前图片数据 132 | * @param {number} x 横坐标 133 | * @param {number} y 纵坐标 134 | * @param {number} width 宽度 135 | * @param {number} height 高度 136 | * @param {number} scale 缩放值 137 | * @param {number} opacity 透明度 138 | * @param {string} status 状态 shrink verticalToClose inertia 139 | */ 140 | NonameGallery.prototype.setCurrentImg = function (x, y, width, height, scale, opacity, status) { 141 | this.currentImg = { 142 | x: x, 143 | y: y, 144 | width: width, 145 | height: height, 146 | scale: scale, 147 | opacity: opacity, 148 | status: status 149 | }; 150 | } 151 | /** 152 | * 设置wrap宽度和x轴偏移量 153 | */ 154 | NonameGallery.prototype.setWrap = function () { 155 | this.wrapWidth = this.previewList.length * this.windowWidth; 156 | this.wrapX = this.index * this.windowWidth * -1; 157 | } 158 | /** 159 | * 渲染 160 | */ 161 | NonameGallery.prototype.render = function () { 162 | // bg背景透明度过渡效果 163 | let cssText = 'opacity: 0;'; 164 | if (this.options.useTransform) { 165 | cssText += 'transition: opacity ' + this.options.duration + 'ms ease-out;'; 166 | } 167 | let html = ''; 199 | document.body.insertAdjacentHTML('beforeend', html); 200 | } 201 | /** 202 | * 获取元素 203 | */ 204 | NonameGallery.prototype.getElement = function () { 205 | this.container = document.querySelector('.noname-gallery-container'); 206 | this.bg = document.querySelector('.noname-gallery-bg'); 207 | this.counter = document.querySelector('.noname-gallery-counter'); 208 | this.wrap = document.querySelector('.noname-gallery-wrap'); 209 | this.imgList = document.querySelectorAll('.noname-gallery-img'); 210 | for (let i = 0, length = this.imgList.length; i < length; i++) { 211 | this.previewList[i].element = this.imgList[i]; // HTMLImageElement 212 | } 213 | } 214 | /** 215 | * 打开画廊 216 | */ 217 | NonameGallery.prototype.open = function () { 218 | const item = this.previewList[this.index]; 219 | if (this.options.useTransform) { 220 | // 强制重绘,否则合并计算样式,导致无法触发过渡效果,或使用setTimeout,个人猜测最短时长等于,1000 / 60 = 16.66666 ≈ 17 221 | window.getComputedStyle(item.element).opacity; 222 | this.bg.style.opacity = '1'; 223 | if (this.options.fadeInOut) { 224 | item.element.style.opacity = '1'; 225 | } 226 | item.element.style.transform = 'translate3d(' + item.x + 'px,' + item.y + 'px, 0) scale(1)'; 227 | } else { 228 | const obj = { 229 | bg: { 230 | opacity: { from: 0, to: 1 } 231 | }, 232 | img: { 233 | width: { from: item.thumbnail.width, to: item.width }, 234 | height: { from: item.thumbnail.height, to: item.height }, 235 | x: { from: item.thumbnail.x, to: item.x }, 236 | y: { from: item.thumbnail.y, to: item.y }, 237 | index: this.index 238 | } 239 | } 240 | if (this.options.fadeInOut) { 241 | obj.img.opacity = { from: 0, to: 1 }; 242 | } 243 | this.raf(obj); 244 | } 245 | // 设置当前图片数据 246 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 247 | } 248 | /** 249 | * 关闭画廊 250 | */ 251 | NonameGallery.prototype.close = function () { 252 | this.isAnimating = true; 253 | this.status = 'close'; 254 | const item = this.previewList[this.index]; 255 | if (this.options.useTransform) { 256 | if (this.options.fadeInOut) { 257 | item.element.style.opacity = '0'; 258 | } 259 | item.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 260 | item.element.style.transform = 'translate3d(' + item.thumbnail.x + 'px, ' + item.thumbnail.y + 'px, 0) scale(' + item.thumbnail.scaleX + ', ' + item.thumbnail.scaleY + ')'; 261 | this.bg.style.transition = 'opacity ' + this.options.duration + 'ms ease-out'; 262 | this.bg.style.opacity = '0'; 263 | } else { 264 | const obj = { 265 | bg: { 266 | opacity: { from: this.bgOpacity, to: 0 } 267 | }, 268 | img: { 269 | width: { from: this.currentImg.width, to: item.thumbnail.width }, 270 | height: { from: this.currentImg.height, to: item.thumbnail.height }, 271 | x: { from: this.currentImg.x, to: item.thumbnail.x }, 272 | y: { from: this.currentImg.y, to: item.thumbnail.y }, 273 | index: this.index 274 | } 275 | } 276 | if (this.options.fadeInOut) { 277 | obj.img.opacity = { from: 1, to: 0 }; 278 | } 279 | this.raf(obj); 280 | } 281 | } 282 | /** 283 | * 绑定事件 284 | */ 285 | NonameGallery.prototype.bindEventListener = function () { 286 | this.handlePointerdown = this.handlePointerdown.bind(this); 287 | this.handlePointermove = this.handlePointermove.bind(this); 288 | this.handlePointerup = this.handlePointerup.bind(this); 289 | this.handlePointercancel = this.handlePointercancel.bind(this); 290 | this.handleResize = this.handleResize.bind(this); 291 | this.handleTransitionEnd = this.handleTransitionEnd.bind(this); 292 | this.container.addEventListener('pointerdown', this.handlePointerdown); 293 | this.container.addEventListener('pointermove', this.handlePointermove); 294 | this.container.addEventListener('pointerup', this.handlePointerup); 295 | this.container.addEventListener('pointercancel', this.handlePointercancel); 296 | this.container.addEventListener('transitionend', this.handleTransitionEnd); 297 | window.addEventListener('resize', this.handleResize); 298 | window.addEventListener('orientationchange', this.handleResize); 299 | if (this.options.openKeyboard) { 300 | this.handleKeydown = this.handleKeydown.bind(this); 301 | window.addEventListener('keydown', this.handleKeydown); 302 | } 303 | } 304 | /** 305 | * 解绑事件 306 | */ 307 | NonameGallery.prototype.unbindEventListener = function () { 308 | this.container.removeEventListener('pointerdown', this.handlePointerdown); 309 | this.container.removeEventListener('pointermove', this.handlePointermove); 310 | this.container.removeEventListener('pointerup', this.handlePointerup); 311 | this.container.removeEventListener('pointercancel', this.handlePointercancel); 312 | this.container.removeEventListener('transitionend', this.handleTransitionEnd); 313 | window.removeEventListener('resize', this.handleResize); 314 | window.removeEventListener('orientationchange', this.handleResize); 315 | if (this.options.openKeyboard) { 316 | window.removeEventListener('keydown', this.handleKeydown); 317 | } 318 | } 319 | /** 320 | * 处理pointerdown 321 | * @param {PointerEvent} e 322 | */ 323 | NonameGallery.prototype.handlePointerdown = function (e) { 324 | // 非鼠标左键点击或正在执行开始动画,缩放动画,垂直滑动、双指缩小恢复动画,结束动画 325 | if (e.pointerType === 'mouse' && e.button !== 0 || this.isAnimating) { 326 | return; 327 | } 328 | this.pointers.push(e); 329 | this.point1 = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 330 | if (this.pointers.length === 1) { 331 | this.container.setPointerCapture(e.pointerId); 332 | this.isPointerdown = true; 333 | this.distance = { x: 0, y: 0 }; 334 | this.lastDistance = { x: 0, y: 0 }; 335 | this.pointerdownTime = Date.now(); 336 | this.pinchTime = null; 337 | this.lastMove = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 338 | if (this.isWrapAnimating === false) { 339 | this.tapCount++; 340 | } 341 | // 双击两点距离不超过30 342 | if (this.tapCount > 1 && (Math.abs(this.point1.x - this.lastPoint1.x) > 30 || Math.abs(this.point1.y - this.lastPoint1.y) > 30)) { 343 | this.tapCount = 1; 344 | } 345 | clearTimeout(this.tapTimeout); 346 | window.cancelAnimationFrame(this.inertiaRafId); 347 | window.cancelAnimationFrame(this.wrapRafId); 348 | } else if (this.pointers.length === 2) { 349 | this.tapCount = 0; 350 | this.point2 = { x: this.pointers[1].clientX, y: this.pointers[1].clientY }; 351 | this.lastCenter = this.getCenter(this.point1, this.point2); 352 | this.lastDistance = { x: this.distance.x, y: this.distance.y }; 353 | this.lastPoint2 = { x: this.pointers[1].clientX, y: this.pointers[1].clientY }; 354 | if (this.dragTarget === '') { 355 | this.dragTarget = 'img'; 356 | } 357 | } 358 | this.lastPoint1 = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 359 | } 360 | /** 361 | * 处理pointermove 362 | * @param {PointerEvent} e 363 | */ 364 | NonameGallery.prototype.handlePointermove = function (e) { 365 | if (!this.isPointerdown) { 366 | return; 367 | } 368 | this.handlePointers(e, 'update'); 369 | const current1 = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 370 | if (this.pointers.length === 1) { 371 | this.diff = { x: current1.x - this.lastMove.x, y: current1.y - this.lastMove.y }; 372 | this.distance = { x: current1.x - this.point1.x + this.lastDistance.x, y: current1.y - this.point1.y + this.lastDistance.y }; 373 | this.lastMove = { x: current1.x, y: current1.y }; 374 | this.pointermoveTime = Date.now(); 375 | if (Math.abs(this.distance.x) > 10 || Math.abs(this.distance.y) > 10) { 376 | this.tapCount = 0; 377 | // 偏移量大于10才判断dragDirection和dragTarget 378 | if (this.dragDirection === '' && this.dragTarget === '') { 379 | this.getDragDirection(); 380 | this.getDragTarget(); 381 | } 382 | } 383 | if (this.dragTarget === 'wrap') { 384 | this.handleWrapPointermove(); 385 | } else if (this.dragTarget === 'img') { 386 | this.handleImgPointermove(); 387 | } 388 | } else if (this.pointers.length === 2) { 389 | const current2 = { x: this.pointers[1].clientX, y: this.pointers[1].clientY }; 390 | if (this.dragTarget === 'img' && this.currentImg.status !== 'verticalToClose') { 391 | this.handlePinch(current1, current2); 392 | } 393 | this.lastPoint1 = { x: current1.x, y: current1.y }; 394 | this.lastPoint2 = { x: current2.x, y: current2.y }; 395 | } 396 | // 阻止默认事件,例如拖拽图片 397 | e.preventDefault(); 398 | } 399 | /** 400 | * 处理pointerup 401 | * @param {PointerEvent} e 402 | */ 403 | NonameGallery.prototype.handlePointerup = function (e) { 404 | if (!this.isPointerdown) { 405 | return; 406 | } 407 | this.handlePointers(e, 'delete'); 408 | if (this.pointers.length === 0) { 409 | this.isPointerdown = false; 410 | if (this.tapCount === 0) { 411 | if (this.dragTarget === 'wrap') { 412 | this.handleWrapPointerup(); 413 | } else if (this.dragTarget === 'img') { 414 | this.handleImgPointerup(); 415 | } 416 | } else if (this.tapCount === 1) { 417 | if (e.pointerType === 'mouse') { 418 | // 由于调用过setPointerCapture方法,导致无法使用e.target来判断触发事件的元素,所以只能根据点击位置来判断 419 | if (e.clientX >= this.currentImg.x && e.clientX <= this.currentImg.x + this.currentImg.width && 420 | e.clientY >= this.currentImg.y && e.clientY <= this.currentImg.y + this.currentImg.height) { 421 | this.handleZoom({ x: e.clientX, y: e.clientY }); 422 | } else { 423 | this.close(); 424 | } 425 | } else { 426 | // 触发移动端长按保存图片后不关闭画廊 427 | if (Date.now() - this.pointerdownTime < 500) { 428 | this.tapTimeout = setTimeout(() => { 429 | this.close(); 430 | }, 250); 431 | } else { 432 | this.tapCount = 0; 433 | } 434 | } 435 | } else if (this.tapCount > 1) { 436 | this.handleZoom({ x: e.clientX, y: e.clientY }); 437 | } 438 | } else if (this.pointers.length === 1) { 439 | this.point1 = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 440 | this.lastMove = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 441 | } 442 | } 443 | /** 444 | * 处理pointercancel 445 | * @param {PointerEvent} e 446 | */ 447 | NonameGallery.prototype.handlePointercancel = function (e) { 448 | this.tapCount = 0; 449 | this.isPointerdown = false; 450 | this.pointers.length = 0; 451 | if (this.isWrapAnimating) { 452 | // 长按图片呼出菜单后继续执行wrap动画 453 | this.handleWrapPointerup(); 454 | } 455 | } 456 | /** 457 | * 更新或删除指针 458 | * @param {PointerEvent} e 459 | * @param {string} type update delete 460 | */ 461 | NonameGallery.prototype.handlePointers = function (e, type) { 462 | for (let i = 0; i < this.pointers.length; i++) { 463 | if (this.pointers[i].pointerId === e.pointerId) { 464 | if (type === 'update') { 465 | this.pointers[i] = e; 466 | } else if (type === 'delete') { 467 | this.pointers.splice(i, 1); 468 | } 469 | } 470 | } 471 | } 472 | /** 473 | * 处理视口宽高 474 | */ 475 | NonameGallery.prototype.handleResize = function () { 476 | // 设置视口大小 477 | this.setWindowSize(); 478 | // 设置previewList 479 | this.previewList.length = 0; 480 | this.setPreviewList(); 481 | // 设置当前图片数据 482 | const item = this.previewList[this.index]; 483 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 484 | // 设置wrap宽度和x轴偏移量 485 | this.setWrap(); 486 | this.wrap.style.width = this.wrapWidth + 'px'; 487 | this.wrap.style.transform = 'translate3d(' + this.wrapX + 'px, 0, 0)'; 488 | // 设置图片数据 489 | for (let i = 0, length = this.imgList.length; i < length; i++) { 490 | const item = this.previewList[i]; 491 | item.element = this.imgList[i]; 492 | item.element.style.width = item.width + 'px'; 493 | item.element.style.height = item.height + 'px'; 494 | if (this.options.useTransform) { 495 | item.element.style.transition = 'none'; 496 | item.element.style.transform = 'translate3d(' + item.x + 'px, ' + item.y + 'px, 0) scale(1)'; 497 | } else { 498 | item.element.style.transform = 'translate3d(' + item.x + 'px, ' + item.y + 'px, 0)'; 499 | } 500 | item.element.style.cursor = 'zoom-in'; 501 | } 502 | if (this.options.useTransform) { 503 | this.bg.style.transition = 'none'; 504 | } 505 | this.bgOpacity = 1; 506 | this.bg.style.opacity = this.bgOpacity; 507 | this.tapCount = 0; 508 | } 509 | /** 510 | * 处理keydown 511 | * @param {KeyboardEvent} e 512 | */ 513 | NonameGallery.prototype.handleKeydown = function (e) { 514 | if (this.isAnimating || this.isWrapAnimating) { 515 | return; 516 | } 517 | const lastIndex = this.index; 518 | if (e.key === 'Escape') { 519 | this.close(); 520 | } else if (['ArrowLeft', 'ArrowUp'].includes(e.key) && lastIndex > 0) { 521 | this.index--; 522 | } else if (['ArrowRight', 'ArrowDown'].includes(e.key) && lastIndex < this.previewList.length - 1) { 523 | this.index++; 524 | } 525 | window.cancelAnimationFrame(this.inertiaRafId); 526 | this.handleWrapSwipe(); 527 | this.handleLastImg(lastIndex); 528 | } 529 | /** 530 | * 处理缩放 531 | */ 532 | NonameGallery.prototype.handleZoom = function (point) { 533 | this.isAnimating = true; 534 | // 缩放时重置点击计数器 535 | this.tapCount = 0; 536 | const item = this.previewList[this.index]; 537 | if (this.currentImg.scale > 1) { 538 | if (this.options.useTransform) { 539 | item.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 540 | item.element.style.transform = 'translate3d(' + item.x + 'px,' + item.y + 'px, 0) scale(1)'; 541 | } else { 542 | const obj = { 543 | img: { 544 | width: { from: this.currentImg.width, to: item.width }, 545 | height: { from: this.currentImg.height, to: item.height }, 546 | x: { from: this.currentImg.x, to: item.x }, 547 | y: { from: this.currentImg.y, to: item.y }, 548 | index: this.index 549 | } 550 | } 551 | this.raf(obj); 552 | } 553 | item.element.style.cursor = 'zoom-in'; 554 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 555 | } else { 556 | const halfWindowWidth = this.windowWidth / 2; 557 | const halfWindowHeight = this.windowHeight / 2; 558 | const left = this.decimal((point.x - item.x) * item.maxScale, 2); 559 | const top = this.decimal((point.y - item.y) * item.maxScale, 2); 560 | let x, y; 561 | if (item.maxWidth > this.windowWidth) { 562 | if (this.options.zoomToScreenCenter) { 563 | x = halfWindowWidth - left; 564 | } else { 565 | x = point.x - left; 566 | } 567 | if (x > 0) { 568 | x = 0; 569 | } else if (x < this.windowWidth - item.maxWidth) { 570 | x = this.windowWidth - item.maxWidth; 571 | } 572 | } else { 573 | x = (this.windowWidth - item.maxWidth) / 2; 574 | } 575 | x = this.decimal(x, 2); 576 | if (item.maxHeight > this.windowHeight) { 577 | if (this.options.zoomToScreenCenter) { 578 | y = halfWindowHeight - top; 579 | } else { 580 | y = point.y - top; 581 | } 582 | if (y > 0) { 583 | y = 0; 584 | } else if (y < this.windowHeight - item.maxHeight) { 585 | y = this.windowHeight - item.maxHeight; 586 | } 587 | } else { 588 | y = (this.windowHeight - item.maxHeight) / 2; 589 | } 590 | y = this.decimal(y, 2); 591 | if (this.options.useTransform) { 592 | item.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 593 | item.element.style.transform = 'translate3d(' + x + 'px,' + y + 'px, 0) scale(' + item.maxScale + ')'; 594 | } else { 595 | const obj = { 596 | img: { 597 | width: { from: item.width, to: item.maxWidth }, 598 | height: { from: item.height, to: item.maxHeight }, 599 | x: { from: item.x, to: x }, 600 | y: { from: item.y, to: y }, 601 | index: this.index 602 | } 603 | } 604 | this.raf(obj); 605 | } 606 | item.element.style.cursor = 'zoom-out'; 607 | this.setCurrentImg(x, y, item.maxWidth, item.maxHeight, item.maxScale, 1, ''); 608 | } 609 | } 610 | /** 611 | * 处理双指缩放 612 | * @param {object} a 第一个点的位置 613 | * @param {object} b 第二个点的位置 614 | */ 615 | NonameGallery.prototype.handlePinch = function (a, b) { 616 | const MIN_SCALE = 0.7; 617 | this.pinchTime = Date.now(); 618 | let ratio = this.getDistance(a, b) / this.getDistance(this.lastPoint1, this.lastPoint2); 619 | let scale = this.decimal(this.currentImg.scale * ratio, 5); 620 | const item = this.previewList[this.index]; 621 | if (scale > item.maxScale) { 622 | ratio = item.maxScale / this.currentImg.scale; 623 | this.currentImg.scale = item.maxScale; 624 | this.currentImg.width = item.maxWidth; 625 | this.currentImg.height = item.maxHeight; 626 | } else if (scale < MIN_SCALE) { 627 | ratio = MIN_SCALE / this.currentImg.scale; 628 | this.currentImg.scale = MIN_SCALE; 629 | this.currentImg.width = this.decimal(item.width * MIN_SCALE, 2); 630 | this.currentImg.height = this.decimal(item.height * MIN_SCALE, 2); 631 | } else { 632 | this.currentImg.scale = scale; 633 | this.currentImg.width = this.decimal(this.currentImg.width * ratio, 2); 634 | this.currentImg.height = this.decimal(this.currentImg.height * ratio, 2); 635 | } 636 | this.currentImg.status = this.currentImg.scale < 1 ? 'shrink' : ''; 637 | const center = this.getCenter(a, b); 638 | // 计算偏移量 639 | this.currentImg.x -= (ratio - 1) * (center.x - this.currentImg.x) - center.x + this.lastCenter.x; 640 | this.currentImg.y -= (ratio - 1) * (center.y - this.currentImg.y) - center.y + this.lastCenter.y; 641 | this.lastCenter = { x: center.x, y: center.y }; 642 | this.handleBoundary(); 643 | if (this.options.useTransform) { 644 | item.element.style.transition = 'none'; 645 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px, 0) scale(' + this.currentImg.scale + ')'; 646 | } else { 647 | item.element.style.width = this.currentImg.width + 'px'; 648 | item.element.style.height = this.currentImg.height + 'px'; 649 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px, 0)'; 650 | } 651 | } 652 | /** 653 | * 处理边界 654 | */ 655 | NonameGallery.prototype.handleBoundary = function () { 656 | if (this.currentImg.width > this.windowWidth) { 657 | if (this.currentImg.x > 0) { 658 | this.currentImg.x = 0 659 | } else if (this.currentImg.x < this.windowWidth - this.currentImg.width) { 660 | this.currentImg.x = this.windowWidth - this.currentImg.width; 661 | } 662 | } else { 663 | this.currentImg.x = (this.windowWidth - this.currentImg.width) / 2; 664 | } 665 | if (this.currentImg.height > this.windowHeight) { 666 | if (this.currentImg.y > 0) { 667 | this.currentImg.y = 0 668 | } else if (this.currentImg.y < this.windowHeight - this.currentImg.height) { 669 | this.currentImg.y = this.windowHeight - this.currentImg.height; 670 | } 671 | } else { 672 | this.currentImg.y = (this.windowHeight - this.currentImg.height) / 2; 673 | } 674 | } 675 | /** 676 | * 获取图片缩放尺寸 677 | * @param {number} naturalWidth 图片自然宽度 678 | * @param {number} naturalHeight 图片自然高度 679 | * @param {number} maxWidth 图片显示最大宽度 680 | * @param {number} maxHeight 图片显示最大高度 681 | * @returns 682 | */ 683 | NonameGallery.prototype.getImgSize = function (naturalWidth, naturalHeight, maxWidth, maxHeight) { 684 | const imgRatio = naturalWidth / naturalHeight; 685 | const maxRatio = maxWidth / maxHeight; 686 | let width, height; 687 | // 如果图片自然宽高比例 >= 显示宽高比例 688 | if (imgRatio >= maxRatio) { 689 | if (naturalWidth > maxWidth) { 690 | width = maxWidth; 691 | height = maxWidth / naturalWidth * naturalHeight; 692 | } else { 693 | width = naturalWidth; 694 | height = naturalHeight; 695 | } 696 | } else { 697 | if (naturalHeight > maxHeight) { 698 | width = maxHeight / naturalHeight * naturalWidth; 699 | height = maxHeight; 700 | } else { 701 | width = naturalWidth; 702 | height = naturalHeight; 703 | } 704 | } 705 | return { width: width, height: height } 706 | } 707 | /** 708 | * 获取拖拽方向 709 | */ 710 | NonameGallery.prototype.getDragDirection = function () { 711 | if (Math.abs(this.distance.x) > Math.abs(this.distance.y)) { 712 | this.dragDirection = 'h'; 713 | } else { 714 | this.dragDirection = 'v'; 715 | } 716 | } 717 | /** 718 | * 获取拖拽目标 719 | */ 720 | NonameGallery.prototype.getDragTarget = function () { 721 | let flag1 = false, flag2 = false; 722 | if (this.currentImg.width > this.windowWidth) { 723 | if ( 724 | (this.diff.x > 0 && this.currentImg.x === 0) || 725 | (this.diff.x < 0 && this.currentImg.x === this.windowWidth - this.currentImg.width) 726 | ) { 727 | flag1 = true; 728 | } 729 | } else { 730 | if (this.currentImg.width >= this.previewList[this.index].width) { 731 | flag2 = true; 732 | } 733 | } 734 | if (this.dragDirection === 'h' && (flag1 || flag2)) { 735 | this.dragTarget = 'wrap'; 736 | } else { 737 | this.dragTarget = 'img'; 738 | } 739 | } 740 | /** 741 | * 获取两点距离 742 | * @param {object} a 第一个点的位置 743 | * @param {object} b 第二个点的位置 744 | * @returns 745 | */ 746 | NonameGallery.prototype.getDistance = function (a, b) { 747 | const x = a.x - b.x; 748 | const y = a.y - b.y; 749 | return Math.hypot(x, y); // Math.sqrt(x * x + y * y); 750 | } 751 | /** 752 | * 处理wrap移动 753 | */ 754 | NonameGallery.prototype.handleWrapPointermove = function () { 755 | if (this.wrapX > 0 || this.wrapX < (this.previewList.length - 1) * this.windowWidth * - 1) { 756 | this.wrapX += this.diff.x * 0.3; 757 | } else { 758 | this.wrapX += this.diff.x; 759 | const LEFT_X = (this.index - 1) * this.windowWidth * -1; 760 | const RIGHT_X = (this.index + 1) * this.windowWidth * -1; 761 | if (this.wrapX > LEFT_X) { 762 | this.wrapX = LEFT_X; 763 | } else if (this.wrapX < RIGHT_X) { 764 | this.wrapX = RIGHT_X; 765 | } 766 | } 767 | this.wrap.style.transform = 'translate3d(' + this.wrapX + 'px, 0, 0)'; 768 | } 769 | /** 770 | * 处理img移动 771 | */ 772 | NonameGallery.prototype.handleImgPointermove = function () { 773 | const item = this.previewList[this.index]; 774 | // 如果图片当前宽高大于视口宽高,拖拽查看图片,可惯性滚动 775 | if (this.currentImg.width > this.windowWidth || this.currentImg.height > this.windowHeight) { 776 | this.currentImg.x += this.diff.x; 777 | this.currentImg.y += this.diff.y; 778 | this.handleBoundary(); 779 | this.currentImg.status = 'inertia'; 780 | if (this.options.useTransform) { 781 | item.element.style.transition = 'none'; 782 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px, 0) scale(' + this.currentImg.scale + ')'; 783 | } else { 784 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px, 0)'; 785 | } 786 | } else { 787 | // 如果垂直拖拽图片且图片未被放大(某些图片尺寸放到最大也没有超出视口宽高) 788 | if (this.dragDirection === 'v' && this.currentImg.width <= item.width && this.currentImg.height <= item.height) { 789 | this.currentImg.status = 'verticalToClose'; 790 | this.bgOpacity = this.decimal(1 - Math.abs(this.distance.y) / (this.windowHeight / 1.2), 5); 791 | if (this.bgOpacity < 0) { 792 | this.bgOpacity = 0; 793 | } 794 | if (this.options.verticalZoom) { 795 | this.currentImg.scale = this.bgOpacity; 796 | this.currentImg.width = this.decimal(item.width * this.currentImg.scale, 2); 797 | this.currentImg.height = this.decimal(item.height * this.currentImg.scale, 2); 798 | this.currentImg.x = item.x + this.distance.x + (item.width - this.currentImg.width) / 2; 799 | this.currentImg.y = item.y + this.distance.y + (item.height - this.currentImg.height) / 2; 800 | } else { 801 | this.currentImg.x = item.x; 802 | this.currentImg.y = item.y + this.distance.y; 803 | this.currentImg.scale = 1; 804 | } 805 | this.bg.style.opacity = this.bgOpacity; 806 | if (this.options.useTransform) { 807 | this.bg.style.transition = 'none'; 808 | item.element.style.transition = 'none'; 809 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px , 0) scale(' + this.currentImg.scale + ')'; 810 | } else { 811 | item.element.style.width = this.currentImg.width + 'px'; 812 | item.element.style.height = this.currentImg.height + 'px'; 813 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px , 0)'; 814 | } 815 | } 816 | } 817 | } 818 | /** 819 | * 处理wrap移动结束 820 | */ 821 | NonameGallery.prototype.handleWrapPointerup = function () { 822 | // 拖拽距离超过屏幕宽度10%即可切换下一张图片 823 | const MIN_SWIPE_DISTANCE = Math.round(this.windowWidth * 0.1); 824 | const lastIndex = this.index; 825 | if (Math.abs(this.distance.x) > MIN_SWIPE_DISTANCE) { 826 | if (this.distance.x > 0 && lastIndex > 0) { 827 | this.index--; 828 | } else if (this.distance.x < 0 && lastIndex < this.previewList.length - 1) { 829 | this.index++; 830 | } 831 | } 832 | this.handleWrapSwipe(); 833 | this.handleLastImg(lastIndex); 834 | } 835 | /** 836 | * 处理img移动结束 837 | */ 838 | NonameGallery.prototype.handleImgPointerup = function () { 839 | // 垂直滑动距离超过屏幕高度10%即可关闭画廊 840 | const MIN_CLOSE_DISTANCE = Math.round(this.windowHeight * 0.1); 841 | const item = this.previewList[this.index]; 842 | const now = Date.now(); 843 | if (this.currentImg.status === 'inertia' && now - this.pointermoveTime < 200 && now - this.pinchTime > 1000) { 844 | this.handleInertia(); 845 | } else if (this.currentImg.status === 'verticalToClose' && Math.abs(this.distance.y) >= MIN_CLOSE_DISTANCE) { 846 | this.close(); 847 | } else if (this.currentImg.status === 'shrink' || (this.currentImg.status == 'verticalToClose' && Math.abs(this.distance.y) < MIN_CLOSE_DISTANCE)) { 848 | this.isAnimating = true; 849 | if (this.options.useTransform) { 850 | this.bg.style.opacity = '1'; 851 | item.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 852 | item.element.style.transform = 'translate3d(' + item.x + 'px, ' + item.y + 'px, 0) scale(1)'; 853 | } else { 854 | const obj = { 855 | bg: { 856 | opacity: { from: this.bgOpacity, to: 1 } 857 | }, 858 | img: { 859 | width: { from: this.currentImg.width, to: item.width }, 860 | height: { from: this.currentImg.height, to: item.height }, 861 | x: { from: this.currentImg.x, to: item.x }, 862 | y: { from: this.currentImg.y, to: item.y }, 863 | index: this.index 864 | } 865 | } 866 | this.raf(obj); 867 | } 868 | this.bgOpacity = 1; 869 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 870 | } 871 | if (this.isAnimating === false) { 872 | this.dragTarget = ''; 873 | this.dragDirection = ''; 874 | } 875 | } 876 | /** 877 | * 处理惯性滚动 878 | */ 879 | NonameGallery.prototype.handleInertia = function () { 880 | const item = this.previewList[this.index]; 881 | const speed = { x: this.diff.x, y: this.diff.y }; 882 | const self = this; 883 | function step(timestamp) { 884 | speed.x *= 0.95; 885 | speed.y *= 0.95; 886 | self.currentImg.x = self.decimal(self.currentImg.x + speed.x, 2); 887 | self.currentImg.y = self.decimal(self.currentImg.y + speed.y, 2); 888 | self.handleBoundary(); 889 | if (self.options.useTransform) { 890 | item.element.style.transform = 'translate3d(' + self.currentImg.x + 'px, ' + self.currentImg.y + 'px, 0) scale(' + self.currentImg.scale + ')'; 891 | } else { 892 | item.element.style.transform = 'translate3d(' + self.currentImg.x + 'px, ' + self.currentImg.y + 'px, 0)'; 893 | } 894 | if (Math.abs(speed.x) > 1 || Math.abs(speed.y) > 1) { 895 | self.inertiaRafId = window.requestAnimationFrame(step); 896 | } 897 | } 898 | this.inertiaRafId = window.requestAnimationFrame(step); 899 | } 900 | /** 901 | * 处理wrap滑动 902 | */ 903 | NonameGallery.prototype.handleWrapSwipe = function () { 904 | this.isWrapAnimating = true; 905 | const obj = { 906 | wrap: { 907 | x: { from: this.wrapX, to: this.windowWidth * this.index * -1 } 908 | } 909 | } 910 | this.wrapRaf(obj); 911 | this.counter.innerHTML = (this.index + 1) + ' / ' + this.previewList.length; 912 | } 913 | /** 914 | * 处理上一张图片 915 | * @param {number} lastIndex 916 | */ 917 | NonameGallery.prototype.handleLastImg = function (lastIndex) { 918 | // 根据索引判断是否切换图片 919 | if (this.index !== lastIndex) { 920 | // 如果上一张图片放大过,则恢复 921 | if (this.currentImg.scale > 1) { 922 | const lastItem = this.previewList[lastIndex]; 923 | if (this.options.useTransform) { 924 | lastItem.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 925 | lastItem.element.style.transform = 'translate3d(' + lastItem.x + 'px, ' + lastItem.y + 'px, 0) scale(1)'; 926 | } else { 927 | const obj = { 928 | img: { 929 | width: { from: this.currentImg.width, to: lastItem.width }, 930 | height: { from: this.currentImg.height, to: lastItem.height }, 931 | x: { from: this.currentImg.x, to: lastItem.x }, 932 | y: { from: this.currentImg.y, to: lastItem.y }, 933 | index: lastIndex 934 | } 935 | } 936 | this.raf(obj); 937 | } 938 | lastItem.element.style.cursor = 'zoom-in'; 939 | } 940 | // 设置当前图片数据 941 | const item = this.previewList[this.index]; 942 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 943 | } 944 | } 945 | /** 946 | * 过渡结束回调 947 | * @param {TransitionEvent } e 948 | */ 949 | NonameGallery.prototype.handleTransitionEnd = function (e) { 950 | // 过滤掉bg transitionend 951 | if (e.target.tagName === 'IMG') { 952 | // wrap滑动,上一张图片恢复动画完成后,不清除dragTarget,因为wrap动画可打断 953 | if (e.target === this.previewList[this.index].element) { 954 | this.isAnimating = false; 955 | this.dragTarget = ''; 956 | this.dragDirection = ''; 957 | } 958 | if (this.status === 'close') { 959 | // 解绑事件 960 | this.unbindEventListener(); 961 | this.container.remove(); 962 | } 963 | } 964 | } 965 | /** 966 | * 保留n位小数 967 | * @param {number} num 数字 968 | * @param {number} n n位小数 969 | * @returns 970 | */ 971 | NonameGallery.prototype.decimal = function (num, n) { 972 | const x = Math.pow(10, n); 973 | return Math.round(num * x) / x; 974 | } 975 | /** 976 | * 获取中心点 977 | * @param {object} a 第一个点的位置 978 | * @param {object} b 第二个点的位置 979 | * @returns 980 | */ 981 | NonameGallery.prototype.getCenter = function (a, b) { 982 | const x = (a.x + b.x) / 2; 983 | const y = (a.y + b.y) / 2; 984 | return { x: x, y: y }; 985 | } 986 | /** 987 | * 曲线函数 988 | * @param {number} from 开始位置 989 | * @param {number} to 结束位置 990 | * @param {number} time 动画已执行的时间 991 | * @param {number} duration 动画时长 992 | * @returns 993 | */ 994 | NonameGallery.prototype.easeOut = function (from, to, time, duration) { 995 | const change = to - from; 996 | const t = time / duration; 997 | return -change * t * (t - 2) + from; 998 | } 999 | /** 1000 | * 开始、结束、缩放、恢复(例如下滑关闭未达到临界值)动画函数 1001 | * @param {object} obj 属性 1002 | */ 1003 | NonameGallery.prototype.raf = function (obj) { 1004 | const self = this; 1005 | let start; 1006 | let count = 0; 1007 | const duration = this.options.duration; 1008 | const item = this.previewList[obj.img.index]; 1009 | function step(timestamp) { 1010 | if (start === undefined) { 1011 | start = timestamp; 1012 | } 1013 | let time = timestamp - start; 1014 | if (time > duration) { 1015 | time = duration; 1016 | count++; 1017 | } 1018 | if (obj.bg) { 1019 | const bgOpacity = self.decimal(self.easeOut(obj.bg.opacity.from, obj.bg.opacity.to, time, duration), 5); 1020 | self.bg.style.opacity = bgOpacity; 1021 | } 1022 | if (obj.img.opacity) { 1023 | const opacity = self.decimal(self.easeOut(obj.img.opacity.from, obj.img.opacity.to, time, duration), 5); 1024 | item.element.style.opacity = opacity; 1025 | } 1026 | const width = self.decimal(self.easeOut(obj.img.width.from, obj.img.width.to, time, duration), 2); 1027 | const height = self.decimal(self.easeOut(obj.img.height.from, obj.img.height.to, time, duration), 2); 1028 | const x = self.decimal(self.easeOut(obj.img.x.from, obj.img.x.to, time, duration), 2); 1029 | const y = self.decimal(self.easeOut(obj.img.y.from, obj.img.y.to, time, duration), 2); 1030 | item.element.style.width = width + 'px'; 1031 | item.element.style.height = height + 'px'; 1032 | item.element.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0)'; 1033 | if (count <= 1) { 1034 | window.requestAnimationFrame(step); 1035 | } else { 1036 | if (obj.img.index === self.index) { 1037 | self.isAnimating = false; 1038 | self.dragTarget = ''; 1039 | self.dragDirection = ''; 1040 | } 1041 | if (self.status === 'close') { 1042 | self.unbindEventListener(); 1043 | self.container.remove(); 1044 | } 1045 | } 1046 | } 1047 | window.requestAnimationFrame(step); 1048 | } 1049 | /** 1050 | * 动画函数 1051 | * @param {object} obj 1052 | */ 1053 | NonameGallery.prototype.wrapRaf = function (obj) { 1054 | const self = this; 1055 | let start; 1056 | let count = 0; 1057 | const duration = this.options.duration; 1058 | function step(timestamp) { 1059 | if (start === undefined) { 1060 | start = timestamp; 1061 | } 1062 | let time = timestamp - start; 1063 | if (time > duration) { 1064 | time = duration; 1065 | count++; 1066 | } 1067 | self.wrapX = self.decimal(self.easeOut(obj.wrap.x.from, obj.wrap.x.to, time, duration), 2); 1068 | self.wrap.style.transform = 'translate3d(' + self.wrapX + 'px, 0, 0)'; 1069 | if (count <= 1) { 1070 | self.wrapRafId = window.requestAnimationFrame(step); 1071 | } else { 1072 | self.isWrapAnimating = false; 1073 | self.dragTarget = ''; 1074 | self.dragDirection = ''; 1075 | } 1076 | } 1077 | this.wrapRafId = window.requestAnimationFrame(step); 1078 | } -------------------------------------------------------------------------------- /dist/noname-gallery.js: -------------------------------------------------------------------------------- 1 | ; (function () { 2 | 'use strict'; 3 | /** 4 | * 使用PointerEvent实现的图片预览插件 5 | * @param {object} options 配置项 6 | */ 7 | function NonameGallery(options) { 8 | // 抛出错误 9 | if (options.list.length === 0) { 10 | throw new Error('options.list can not be empty array'); 11 | } 12 | this.options = Object.assign({}, this.defaults, options); 13 | } 14 | /** 15 | * 默认配置项 16 | */ 17 | NonameGallery.prototype.defaults = { 18 | list: [], // HTMLImageElement[] 19 | index: 0, // 索引 20 | fadeInOut: true, // 淡入淡出 21 | useTransform: true, // 宽高缩放只能使用requestAnimationFrame,transition同时过渡宽高和transform时会发生抖动以及动画轨迹偏移的问题,手机端微信浏览器尤其严重 22 | verticalZoom: true, // 垂直滑动缩放图片 23 | openKeyboard: false, // 开启键盘 esc关闭,方向键切换图片 24 | zoomToScreenCenter: false, // 将放大区域移动到屏幕中心显示 25 | duration: 300, // 动画持续时间 26 | minScale: 1.5 // 最小放大倍数 27 | } 28 | /** 29 | * 初始化 30 | */ 31 | NonameGallery.prototype.init = function () { 32 | // 设置属性值 33 | this.setProperties(); 34 | // 设置视口大小 35 | this.setWindowSize(); 36 | // 设置previewList 37 | this.setPreviewList(); 38 | // 设置wrap宽度和x轴偏移量 39 | this.setWrap(); 40 | // 渲染 41 | this.render(); 42 | // 获取元素 43 | this.getElement(); 44 | // 绑定事件 45 | this.bindEventListener(); 46 | // 打开画廊 47 | this.open(); 48 | } 49 | /** 50 | * 设置属性值(防止全局实例,多次调用出现问题) 51 | */ 52 | NonameGallery.prototype.setProperties = function () { 53 | this.container = null; // .noname-gallery-container 54 | this.bg = null; // .noname-gallery-bg 55 | this.counter = null; // .noname-gallery-counter 56 | this.wrap = null; // .noname-gallery-wrap 57 | this.imgList = null; // .noname-gallery-img 58 | this.bgOpacity = 1; // .noname-gallery-bg 透明度 59 | this.windowWidth = 0; // 视口宽度 60 | this.windowHeight = 0; // 视口高度 61 | this.index = this.options.index; // 预览图片索引 62 | this.wrapWidth = 0; // .noname-gallery-wrap 宽度 63 | this.wrapX = 0; // .noname-gallery-wrap x轴偏移量 64 | this.previewList = []; // 预览图片列表 65 | this.currentImg = { 66 | x: 0, // 当前图片旋转中心(已设置为左上角)相对屏幕左上角偏移值 67 | y: 0, // 当前图片旋转中心(已设置为左上角)相对屏幕左上角偏移值 68 | width: 0, // 当前图片宽度 69 | height: 0, // 当前图片高度 70 | scale: 1, // 当前图片缩放倍数 71 | opacity: 1, // 当前图片透明度 开启淡入淡出时会使用到 72 | status: '' // 当前图片状态 shrink(scale < 1) verticalToClose inertia 73 | }; 74 | this.pointers = []; // 指针数组用于保存多个触摸点 75 | this.point1 = { x: 0, y: 0 }; // 第一个触摸点 76 | this.point2 = { x: 0, y: 0 }; // 第二个触摸点 77 | this.diff = { x: 0, y: 0 }; // 相对于上一次移动差值 78 | this.distance = { x: 0, y: 0 }; // 移动距离 79 | this.lastDistance = { x: 0, y: 0 }; // 双指滑动时记录上一次移动距离 80 | this.lastPoint1 = { x: 0, y: 0 }; // 上一次第一个触摸点位置,用于判断双击距离是否大于30 81 | this.lastPoint2 = { x: 0, y: 0 }; // 上一次第二个触摸点位置 82 | this.lastMove = { x: 0, y: 0 }; // 上一次移动坐标 83 | this.lastCenter = { x: 0, y: 0 }; // 上一次双指中心位置 84 | this.tapCount = 0; // 点击次数 1 = 单击 大于1 = 双击 85 | this.dragDirection = ''; // 拖拽方向 v(vertical) h(horizontal) 86 | this.dragTarget = ''; // 拖拽目标 wrap img 87 | this.status = ''; // close 时移除dom 88 | this.isPointerdown = false; // 按下标识 89 | this.isAnimating = true; // 是否正在执行动画 90 | this.isWrapAnimating = false; // 是否正在执行wrap切换动画,不响应键盘事件 91 | this.tapTimeout = null; // 单击延时器 250ms 判断双击 92 | this.pointerdownTime = null; // pointerdown time 93 | this.pointermoveTime = null; // 鼠标松开距离最后一次移动小于200ms执行惯性滑动 94 | this.pinchTime = null; // 距离上一次双指缩放的时间 95 | this.inertiaRafId = null; // requestAnimationFrame id 用于停止惯性滑动动画 96 | this.wrapRafId = null; // requestAnimationFrame id 用于停止wrap滑动动画 97 | } 98 | /** 99 | * 设置视口大小 100 | */ 101 | NonameGallery.prototype.setWindowSize = function () { 102 | this.windowWidth = window.innerWidth; 103 | this.windowHeight = window.innerHeight; 104 | } 105 | /** 106 | * 设置previewList 107 | */ 108 | NonameGallery.prototype.setPreviewList = function () { 109 | for (const img of this.options.list) { 110 | const rect = img.getBoundingClientRect(); 111 | const result = this.getImgSize(img.naturalWidth, img.naturalHeight, this.windowWidth, this.windowHeight); 112 | const maxScale = Math.max(this.decimal(img.naturalWidth / result.width, 5), this.options.minScale); 113 | this.previewList.push({ 114 | x: this.decimal((this.windowWidth - result.width) / 2, 2), // 预览图片左上角相对于视口的横坐标 115 | y: this.decimal((this.windowHeight - result.height) / 2, 2), // 预览图片左上角相对于视口的纵坐标 116 | width: this.decimal(result.width, 2), // 预览图片显示宽度 117 | height: this.decimal(result.height, 2), // 预览图片显示高度 118 | maxWidth: this.decimal(result.width * maxScale, 2), // 预览图片最大宽度 119 | maxHeight: this.decimal(result.height * maxScale, 2), // 预览图片最大高度 120 | maxScale: maxScale, // 预览图片最大缩放比例 121 | thumbnail: { 122 | x: this.decimal(rect.left, 2), // 缩略图左上角相对于视口的横坐标 123 | y: this.decimal(rect.top, 2), // 缩略图左上角相对于视口的纵坐标 124 | width: this.decimal(rect.width, 2), // 缩略图显示宽度 125 | height: this.decimal(rect.height, 2), // 缩略图显示高度 126 | scaleX: this.decimal(rect.width / result.width, 5), // 缩略图x轴比例 127 | scaleY: this.decimal(rect.height / result.height, 5) // 缩略图y轴比例 128 | } 129 | }); 130 | } 131 | } 132 | /** 133 | * 设置当前图片数据 134 | * @param {number} x 横坐标 135 | * @param {number} y 纵坐标 136 | * @param {number} width 宽度 137 | * @param {number} height 高度 138 | * @param {number} scale 缩放值 139 | * @param {number} opacity 透明度 140 | * @param {string} status 状态 shrink verticalToClose inertia 141 | */ 142 | NonameGallery.prototype.setCurrentImg = function (x, y, width, height, scale, opacity, status) { 143 | this.currentImg = { 144 | x: x, 145 | y: y, 146 | width: width, 147 | height: height, 148 | scale: scale, 149 | opacity: opacity, 150 | status: status 151 | }; 152 | } 153 | /** 154 | * 设置wrap宽度和x轴偏移量 155 | */ 156 | NonameGallery.prototype.setWrap = function () { 157 | this.wrapWidth = this.previewList.length * this.windowWidth; 158 | this.wrapX = this.index * this.windowWidth * -1; 159 | } 160 | /** 161 | * 渲染 162 | */ 163 | NonameGallery.prototype.render = function () { 164 | // bg背景透明度过渡效果 165 | let cssText = 'opacity: 0;'; 166 | if (this.options.useTransform) { 167 | cssText += 'transition: opacity ' + this.options.duration + 'ms ease-out;'; 168 | } 169 | let html = ''; 201 | document.body.insertAdjacentHTML('beforeend', html); 202 | } 203 | /** 204 | * 获取元素 205 | */ 206 | NonameGallery.prototype.getElement = function () { 207 | this.container = document.querySelector('.noname-gallery-container'); 208 | this.bg = document.querySelector('.noname-gallery-bg'); 209 | this.counter = document.querySelector('.noname-gallery-counter'); 210 | this.wrap = document.querySelector('.noname-gallery-wrap'); 211 | this.imgList = document.querySelectorAll('.noname-gallery-img'); 212 | for (let i = 0, length = this.imgList.length; i < length; i++) { 213 | this.previewList[i].element = this.imgList[i]; // HTMLImageElement 214 | } 215 | } 216 | /** 217 | * 打开画廊 218 | */ 219 | NonameGallery.prototype.open = function () { 220 | const item = this.previewList[this.index]; 221 | if (this.options.useTransform) { 222 | // 强制重绘,否则合并计算样式,导致无法触发过渡效果,或使用setTimeout,个人猜测最短时长等于,1000 / 60 = 16.66666 ≈ 17 223 | window.getComputedStyle(item.element).opacity; 224 | this.bg.style.opacity = '1'; 225 | if (this.options.fadeInOut) { 226 | item.element.style.opacity = '1'; 227 | } 228 | item.element.style.transform = 'translate3d(' + item.x + 'px,' + item.y + 'px, 0) scale(1)'; 229 | } else { 230 | const obj = { 231 | bg: { 232 | opacity: { from: 0, to: 1 } 233 | }, 234 | img: { 235 | width: { from: item.thumbnail.width, to: item.width }, 236 | height: { from: item.thumbnail.height, to: item.height }, 237 | x: { from: item.thumbnail.x, to: item.x }, 238 | y: { from: item.thumbnail.y, to: item.y }, 239 | index: this.index 240 | } 241 | } 242 | if (this.options.fadeInOut) { 243 | obj.img.opacity = { from: 0, to: 1 }; 244 | } 245 | this.raf(obj); 246 | } 247 | // 设置当前图片数据 248 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 249 | } 250 | /** 251 | * 关闭画廊 252 | */ 253 | NonameGallery.prototype.close = function () { 254 | this.isAnimating = true; 255 | this.status = 'close'; 256 | const item = this.previewList[this.index]; 257 | if (this.options.useTransform) { 258 | if (this.options.fadeInOut) { 259 | item.element.style.opacity = '0'; 260 | } 261 | item.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 262 | item.element.style.transform = 'translate3d(' + item.thumbnail.x + 'px, ' + item.thumbnail.y + 'px, 0) scale(' + item.thumbnail.scaleX + ', ' + item.thumbnail.scaleY + ')'; 263 | this.bg.style.transition = 'opacity ' + this.options.duration + 'ms ease-out'; 264 | this.bg.style.opacity = '0'; 265 | } else { 266 | const obj = { 267 | bg: { 268 | opacity: { from: this.bgOpacity, to: 0 } 269 | }, 270 | img: { 271 | width: { from: this.currentImg.width, to: item.thumbnail.width }, 272 | height: { from: this.currentImg.height, to: item.thumbnail.height }, 273 | x: { from: this.currentImg.x, to: item.thumbnail.x }, 274 | y: { from: this.currentImg.y, to: item.thumbnail.y }, 275 | index: this.index 276 | } 277 | } 278 | if (this.options.fadeInOut) { 279 | obj.img.opacity = { from: 1, to: 0 }; 280 | } 281 | this.raf(obj); 282 | } 283 | } 284 | /** 285 | * 绑定事件 286 | */ 287 | NonameGallery.prototype.bindEventListener = function () { 288 | this.handlePointerdown = this.handlePointerdown.bind(this); 289 | this.handlePointermove = this.handlePointermove.bind(this); 290 | this.handlePointerup = this.handlePointerup.bind(this); 291 | this.handlePointercancel = this.handlePointercancel.bind(this); 292 | this.handleResize = this.handleResize.bind(this); 293 | this.handleTransitionEnd = this.handleTransitionEnd.bind(this); 294 | this.container.addEventListener('pointerdown', this.handlePointerdown); 295 | this.container.addEventListener('pointermove', this.handlePointermove); 296 | this.container.addEventListener('pointerup', this.handlePointerup); 297 | this.container.addEventListener('pointercancel', this.handlePointercancel); 298 | this.container.addEventListener('transitionend', this.handleTransitionEnd); 299 | window.addEventListener('resize', this.handleResize); 300 | window.addEventListener('orientationchange', this.handleResize); 301 | if (this.options.openKeyboard) { 302 | this.handleKeydown = this.handleKeydown.bind(this); 303 | window.addEventListener('keydown', this.handleKeydown); 304 | } 305 | } 306 | /** 307 | * 解绑事件 308 | */ 309 | NonameGallery.prototype.unbindEventListener = function () { 310 | this.container.removeEventListener('pointerdown', this.handlePointerdown); 311 | this.container.removeEventListener('pointermove', this.handlePointermove); 312 | this.container.removeEventListener('pointerup', this.handlePointerup); 313 | this.container.removeEventListener('pointercancel', this.handlePointercancel); 314 | this.container.removeEventListener('transitionend', this.handleTransitionEnd); 315 | window.removeEventListener('resize', this.handleResize); 316 | window.removeEventListener('orientationchange', this.handleResize); 317 | if (this.options.openKeyboard) { 318 | window.removeEventListener('keydown', this.handleKeydown); 319 | } 320 | } 321 | /** 322 | * 处理pointerdown 323 | * @param {PointerEvent} e 324 | */ 325 | NonameGallery.prototype.handlePointerdown = function (e) { 326 | // 非鼠标左键点击或正在执行开始动画,缩放动画,垂直滑动、双指缩小恢复动画,结束动画 327 | if (e.pointerType === 'mouse' && e.button !== 0 || this.isAnimating) { 328 | return; 329 | } 330 | this.pointers.push(e); 331 | this.point1 = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 332 | if (this.pointers.length === 1) { 333 | this.container.setPointerCapture(e.pointerId); 334 | this.isPointerdown = true; 335 | this.distance = { x: 0, y: 0 }; 336 | this.lastDistance = { x: 0, y: 0 }; 337 | this.pointerdownTime = Date.now(); 338 | this.pinchTime = null; 339 | this.lastMove = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 340 | if (this.isWrapAnimating === false) { 341 | this.tapCount++; 342 | } 343 | // 双击两点距离不超过30 344 | if (this.tapCount > 1 && (Math.abs(this.point1.x - this.lastPoint1.x) > 30 || Math.abs(this.point1.y - this.lastPoint1.y) > 30)) { 345 | this.tapCount = 1; 346 | } 347 | clearTimeout(this.tapTimeout); 348 | window.cancelAnimationFrame(this.inertiaRafId); 349 | window.cancelAnimationFrame(this.wrapRafId); 350 | } else if (this.pointers.length === 2) { 351 | this.tapCount = 0; 352 | this.point2 = { x: this.pointers[1].clientX, y: this.pointers[1].clientY }; 353 | this.lastCenter = this.getCenter(this.point1, this.point2); 354 | this.lastDistance = { x: this.distance.x, y: this.distance.y }; 355 | this.lastPoint2 = { x: this.pointers[1].clientX, y: this.pointers[1].clientY }; 356 | if (this.dragTarget === '') { 357 | this.dragTarget = 'img'; 358 | } 359 | } 360 | this.lastPoint1 = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 361 | } 362 | /** 363 | * 处理pointermove 364 | * @param {PointerEvent} e 365 | */ 366 | NonameGallery.prototype.handlePointermove = function (e) { 367 | if (!this.isPointerdown) { 368 | return; 369 | } 370 | this.handlePointers(e, 'update'); 371 | const current1 = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 372 | if (this.pointers.length === 1) { 373 | this.diff = { x: current1.x - this.lastMove.x, y: current1.y - this.lastMove.y }; 374 | this.distance = { x: current1.x - this.point1.x + this.lastDistance.x, y: current1.y - this.point1.y + this.lastDistance.y }; 375 | this.lastMove = { x: current1.x, y: current1.y }; 376 | this.pointermoveTime = Date.now(); 377 | if (Math.abs(this.distance.x) > 10 || Math.abs(this.distance.y) > 10) { 378 | this.tapCount = 0; 379 | // 偏移量大于10才判断dragDirection和dragTarget 380 | if (this.dragDirection === '' && this.dragTarget === '') { 381 | this.getDragDirection(); 382 | this.getDragTarget(); 383 | } 384 | } 385 | if (this.dragTarget === 'wrap') { 386 | this.handleWrapPointermove(); 387 | } else if (this.dragTarget === 'img') { 388 | this.handleImgPointermove(); 389 | } 390 | } else if (this.pointers.length === 2) { 391 | const current2 = { x: this.pointers[1].clientX, y: this.pointers[1].clientY }; 392 | if (this.dragTarget === 'img' && this.currentImg.status !== 'verticalToClose') { 393 | this.handlePinch(current1, current2); 394 | } 395 | this.lastPoint1 = { x: current1.x, y: current1.y }; 396 | this.lastPoint2 = { x: current2.x, y: current2.y }; 397 | } 398 | // 阻止默认事件,例如拖拽图片 399 | e.preventDefault(); 400 | } 401 | /** 402 | * 处理pointerup 403 | * @param {PointerEvent} e 404 | */ 405 | NonameGallery.prototype.handlePointerup = function (e) { 406 | if (!this.isPointerdown) { 407 | return; 408 | } 409 | this.handlePointers(e, 'delete'); 410 | if (this.pointers.length === 0) { 411 | this.isPointerdown = false; 412 | if (this.tapCount === 0) { 413 | if (this.dragTarget === 'wrap') { 414 | this.handleWrapPointerup(); 415 | } else if (this.dragTarget === 'img') { 416 | this.handleImgPointerup(); 417 | } 418 | } else if (this.tapCount === 1) { 419 | if (e.pointerType === 'mouse') { 420 | // 由于调用过setPointerCapture方法,导致无法使用e.target来判断触发事件的元素,所以只能根据点击位置来判断 421 | if (e.clientX >= this.currentImg.x && e.clientX <= this.currentImg.x + this.currentImg.width && 422 | e.clientY >= this.currentImg.y && e.clientY <= this.currentImg.y + this.currentImg.height) { 423 | this.handleZoom({ x: e.clientX, y: e.clientY }); 424 | } else { 425 | this.close(); 426 | } 427 | } else { 428 | // 触发移动端长按保存图片后不关闭画廊 429 | if (Date.now() - this.pointerdownTime < 500) { 430 | this.tapTimeout = setTimeout(() => { 431 | this.close(); 432 | }, 250); 433 | } else { 434 | this.tapCount = 0; 435 | } 436 | } 437 | } else if (this.tapCount > 1) { 438 | this.handleZoom({ x: e.clientX, y: e.clientY }); 439 | } 440 | } else if (this.pointers.length === 1) { 441 | this.point1 = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 442 | this.lastMove = { x: this.pointers[0].clientX, y: this.pointers[0].clientY }; 443 | } 444 | } 445 | /** 446 | * 处理pointercancel 447 | * @param {PointerEvent} e 448 | */ 449 | NonameGallery.prototype.handlePointercancel = function (e) { 450 | this.tapCount = 0; 451 | this.isPointerdown = false; 452 | this.pointers.length = 0; 453 | if (this.isWrapAnimating) { 454 | // 长按图片呼出菜单后继续执行wrap动画 455 | this.handleWrapPointerup(); 456 | } 457 | } 458 | /** 459 | * 更新或删除指针 460 | * @param {PointerEvent} e 461 | * @param {string} type update delete 462 | */ 463 | NonameGallery.prototype.handlePointers = function (e, type) { 464 | for (let i = 0; i < this.pointers.length; i++) { 465 | if (this.pointers[i].pointerId === e.pointerId) { 466 | if (type === 'update') { 467 | this.pointers[i] = e; 468 | } else if (type === 'delete') { 469 | this.pointers.splice(i, 1); 470 | } 471 | } 472 | } 473 | } 474 | /** 475 | * 处理视口宽高 476 | */ 477 | NonameGallery.prototype.handleResize = function () { 478 | // 设置视口大小 479 | this.setWindowSize(); 480 | // 设置previewList 481 | this.previewList.length = 0; 482 | this.setPreviewList(); 483 | // 设置当前图片数据 484 | const item = this.previewList[this.index]; 485 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 486 | // 设置wrap宽度和x轴偏移量 487 | this.setWrap(); 488 | this.wrap.style.width = this.wrapWidth + 'px'; 489 | this.wrap.style.transform = 'translate3d(' + this.wrapX + 'px, 0, 0)'; 490 | // 设置图片数据 491 | for (let i = 0, length = this.imgList.length; i < length; i++) { 492 | const item = this.previewList[i]; 493 | item.element = this.imgList[i]; 494 | item.element.style.width = item.width + 'px'; 495 | item.element.style.height = item.height + 'px'; 496 | if (this.options.useTransform) { 497 | item.element.style.transition = 'none'; 498 | item.element.style.transform = 'translate3d(' + item.x + 'px, ' + item.y + 'px, 0) scale(1)'; 499 | } else { 500 | item.element.style.transform = 'translate3d(' + item.x + 'px, ' + item.y + 'px, 0)'; 501 | } 502 | item.element.style.cursor = 'zoom-in'; 503 | } 504 | if (this.options.useTransform) { 505 | this.bg.style.transition = 'none'; 506 | } 507 | this.bgOpacity = 1; 508 | this.bg.style.opacity = this.bgOpacity; 509 | this.tapCount = 0; 510 | } 511 | /** 512 | * 处理keydown 513 | * @param {KeyboardEvent} e 514 | */ 515 | NonameGallery.prototype.handleKeydown = function (e) { 516 | if (this.isAnimating || this.isWrapAnimating) { 517 | return; 518 | } 519 | const lastIndex = this.index; 520 | if (e.key === 'Escape') { 521 | this.close(); 522 | } else if (['ArrowLeft', 'ArrowUp'].includes(e.key) && lastIndex > 0) { 523 | this.index--; 524 | } else if (['ArrowRight', 'ArrowDown'].includes(e.key) && lastIndex < this.previewList.length - 1) { 525 | this.index++; 526 | } 527 | window.cancelAnimationFrame(this.inertiaRafId); 528 | this.handleWrapSwipe(); 529 | this.handleLastImg(lastIndex); 530 | } 531 | /** 532 | * 处理缩放 533 | */ 534 | NonameGallery.prototype.handleZoom = function (point) { 535 | this.isAnimating = true; 536 | // 缩放时重置点击计数器 537 | this.tapCount = 0; 538 | const item = this.previewList[this.index]; 539 | if (this.currentImg.scale > 1) { 540 | if (this.options.useTransform) { 541 | item.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 542 | item.element.style.transform = 'translate3d(' + item.x + 'px,' + item.y + 'px, 0) scale(1)'; 543 | } else { 544 | const obj = { 545 | img: { 546 | width: { from: this.currentImg.width, to: item.width }, 547 | height: { from: this.currentImg.height, to: item.height }, 548 | x: { from: this.currentImg.x, to: item.x }, 549 | y: { from: this.currentImg.y, to: item.y }, 550 | index: this.index 551 | } 552 | } 553 | this.raf(obj); 554 | } 555 | item.element.style.cursor = 'zoom-in'; 556 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 557 | } else { 558 | const halfWindowWidth = this.windowWidth / 2; 559 | const halfWindowHeight = this.windowHeight / 2; 560 | const left = this.decimal((point.x - item.x) * item.maxScale, 2); 561 | const top = this.decimal((point.y - item.y) * item.maxScale, 2); 562 | let x, y; 563 | if (item.maxWidth > this.windowWidth) { 564 | if (this.options.zoomToScreenCenter) { 565 | x = halfWindowWidth - left; 566 | } else { 567 | x = point.x - left; 568 | } 569 | if (x > 0) { 570 | x = 0; 571 | } else if (x < this.windowWidth - item.maxWidth) { 572 | x = this.windowWidth - item.maxWidth; 573 | } 574 | } else { 575 | x = (this.windowWidth - item.maxWidth) / 2; 576 | } 577 | x = this.decimal(x, 2); 578 | if (item.maxHeight > this.windowHeight) { 579 | if (this.options.zoomToScreenCenter) { 580 | y = halfWindowHeight - top; 581 | } else { 582 | y = point.y - top; 583 | } 584 | if (y > 0) { 585 | y = 0; 586 | } else if (y < this.windowHeight - item.maxHeight) { 587 | y = this.windowHeight - item.maxHeight; 588 | } 589 | } else { 590 | y = (this.windowHeight - item.maxHeight) / 2; 591 | } 592 | y = this.decimal(y, 2); 593 | if (this.options.useTransform) { 594 | item.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 595 | item.element.style.transform = 'translate3d(' + x + 'px,' + y + 'px, 0) scale(' + item.maxScale + ')'; 596 | } else { 597 | const obj = { 598 | img: { 599 | width: { from: item.width, to: item.maxWidth }, 600 | height: { from: item.height, to: item.maxHeight }, 601 | x: { from: item.x, to: x }, 602 | y: { from: item.y, to: y }, 603 | index: this.index 604 | } 605 | } 606 | this.raf(obj); 607 | } 608 | item.element.style.cursor = 'zoom-out'; 609 | this.setCurrentImg(x, y, item.maxWidth, item.maxHeight, item.maxScale, 1, ''); 610 | } 611 | } 612 | /** 613 | * 处理双指缩放 614 | * @param {object} a 第一个点的位置 615 | * @param {object} b 第二个点的位置 616 | */ 617 | NonameGallery.prototype.handlePinch = function (a, b) { 618 | const MIN_SCALE = 0.7; 619 | this.pinchTime = Date.now(); 620 | let ratio = this.getDistance(a, b) / this.getDistance(this.lastPoint1, this.lastPoint2); 621 | let scale = this.decimal(this.currentImg.scale * ratio, 5); 622 | const item = this.previewList[this.index]; 623 | if (scale > item.maxScale) { 624 | ratio = item.maxScale / this.currentImg.scale; 625 | this.currentImg.scale = item.maxScale; 626 | this.currentImg.width = item.maxWidth; 627 | this.currentImg.height = item.maxHeight; 628 | } else if (scale < MIN_SCALE) { 629 | ratio = MIN_SCALE / this.currentImg.scale; 630 | this.currentImg.scale = MIN_SCALE; 631 | this.currentImg.width = this.decimal(item.width * MIN_SCALE, 2); 632 | this.currentImg.height = this.decimal(item.height * MIN_SCALE, 2); 633 | } else { 634 | this.currentImg.scale = scale; 635 | this.currentImg.width = this.decimal(this.currentImg.width * ratio, 2); 636 | this.currentImg.height = this.decimal(this.currentImg.height * ratio, 2); 637 | } 638 | this.currentImg.status = this.currentImg.scale < 1 ? 'shrink' : ''; 639 | const center = this.getCenter(a, b); 640 | // 计算偏移量 641 | this.currentImg.x -= (ratio - 1) * (center.x - this.currentImg.x) - center.x + this.lastCenter.x; 642 | this.currentImg.y -= (ratio - 1) * (center.y - this.currentImg.y) - center.y + this.lastCenter.y; 643 | this.lastCenter = { x: center.x, y: center.y }; 644 | this.handleBoundary(); 645 | if (this.options.useTransform) { 646 | item.element.style.transition = 'none'; 647 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px, 0) scale(' + this.currentImg.scale + ')'; 648 | } else { 649 | item.element.style.width = this.currentImg.width + 'px'; 650 | item.element.style.height = this.currentImg.height + 'px'; 651 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px, 0)'; 652 | } 653 | } 654 | /** 655 | * 处理边界 656 | */ 657 | NonameGallery.prototype.handleBoundary = function () { 658 | if (this.currentImg.width > this.windowWidth) { 659 | if (this.currentImg.x > 0) { 660 | this.currentImg.x = 0 661 | } else if (this.currentImg.x < this.windowWidth - this.currentImg.width) { 662 | this.currentImg.x = this.windowWidth - this.currentImg.width; 663 | } 664 | } else { 665 | this.currentImg.x = (this.windowWidth - this.currentImg.width) / 2; 666 | } 667 | if (this.currentImg.height > this.windowHeight) { 668 | if (this.currentImg.y > 0) { 669 | this.currentImg.y = 0 670 | } else if (this.currentImg.y < this.windowHeight - this.currentImg.height) { 671 | this.currentImg.y = this.windowHeight - this.currentImg.height; 672 | } 673 | } else { 674 | this.currentImg.y = (this.windowHeight - this.currentImg.height) / 2; 675 | } 676 | } 677 | /** 678 | * 获取图片缩放尺寸 679 | * @param {number} naturalWidth 图片自然宽度 680 | * @param {number} naturalHeight 图片自然高度 681 | * @param {number} maxWidth 图片显示最大宽度 682 | * @param {number} maxHeight 图片显示最大高度 683 | * @returns 684 | */ 685 | NonameGallery.prototype.getImgSize = function (naturalWidth, naturalHeight, maxWidth, maxHeight) { 686 | const imgRatio = naturalWidth / naturalHeight; 687 | const maxRatio = maxWidth / maxHeight; 688 | let width, height; 689 | // 如果图片自然宽高比例 >= 显示宽高比例 690 | if (imgRatio >= maxRatio) { 691 | if (naturalWidth > maxWidth) { 692 | width = maxWidth; 693 | height = maxWidth / naturalWidth * naturalHeight; 694 | } else { 695 | width = naturalWidth; 696 | height = naturalHeight; 697 | } 698 | } else { 699 | if (naturalHeight > maxHeight) { 700 | width = maxHeight / naturalHeight * naturalWidth; 701 | height = maxHeight; 702 | } else { 703 | width = naturalWidth; 704 | height = naturalHeight; 705 | } 706 | } 707 | return { width: width, height: height } 708 | } 709 | /** 710 | * 获取拖拽方向 711 | */ 712 | NonameGallery.prototype.getDragDirection = function () { 713 | if (Math.abs(this.distance.x) > Math.abs(this.distance.y)) { 714 | this.dragDirection = 'h'; 715 | } else { 716 | this.dragDirection = 'v'; 717 | } 718 | } 719 | /** 720 | * 获取拖拽目标 721 | */ 722 | NonameGallery.prototype.getDragTarget = function () { 723 | let flag1 = false, flag2 = false; 724 | if (this.currentImg.width > this.windowWidth) { 725 | if ( 726 | (this.diff.x > 0 && this.currentImg.x === 0) || 727 | (this.diff.x < 0 && this.currentImg.x === this.windowWidth - this.currentImg.width) 728 | ) { 729 | flag1 = true; 730 | } 731 | } else { 732 | if (this.currentImg.width >= this.previewList[this.index].width) { 733 | flag2 = true; 734 | } 735 | } 736 | if (this.dragDirection === 'h' && (flag1 || flag2)) { 737 | this.dragTarget = 'wrap'; 738 | } else { 739 | this.dragTarget = 'img'; 740 | } 741 | } 742 | /** 743 | * 获取两点距离 744 | * @param {object} a 第一个点的位置 745 | * @param {object} b 第二个点的位置 746 | * @returns 747 | */ 748 | NonameGallery.prototype.getDistance = function (a, b) { 749 | const x = a.x - b.x; 750 | const y = a.y - b.y; 751 | return Math.hypot(x, y); // Math.sqrt(x * x + y * y); 752 | } 753 | /** 754 | * 处理wrap移动 755 | */ 756 | NonameGallery.prototype.handleWrapPointermove = function () { 757 | if (this.wrapX > 0 || this.wrapX < (this.previewList.length - 1) * this.windowWidth * - 1) { 758 | this.wrapX += this.diff.x * 0.3; 759 | } else { 760 | this.wrapX += this.diff.x; 761 | const LEFT_X = (this.index - 1) * this.windowWidth * -1; 762 | const RIGHT_X = (this.index + 1) * this.windowWidth * -1; 763 | if (this.wrapX > LEFT_X) { 764 | this.wrapX = LEFT_X; 765 | } else if (this.wrapX < RIGHT_X) { 766 | this.wrapX = RIGHT_X; 767 | } 768 | } 769 | this.wrap.style.transform = 'translate3d(' + this.wrapX + 'px, 0, 0)'; 770 | } 771 | /** 772 | * 处理img移动 773 | */ 774 | NonameGallery.prototype.handleImgPointermove = function () { 775 | const item = this.previewList[this.index]; 776 | // 如果图片当前宽高大于视口宽高,拖拽查看图片,可惯性滚动 777 | if (this.currentImg.width > this.windowWidth || this.currentImg.height > this.windowHeight) { 778 | this.currentImg.x += this.diff.x; 779 | this.currentImg.y += this.diff.y; 780 | this.handleBoundary(); 781 | this.currentImg.status = 'inertia'; 782 | if (this.options.useTransform) { 783 | item.element.style.transition = 'none'; 784 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px, 0) scale(' + this.currentImg.scale + ')'; 785 | } else { 786 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px, 0)'; 787 | } 788 | } else { 789 | // 如果垂直拖拽图片且图片未被放大(某些图片尺寸放到最大也没有超出视口宽高) 790 | if (this.dragDirection === 'v' && this.currentImg.width <= item.width && this.currentImg.height <= item.height) { 791 | this.currentImg.status = 'verticalToClose'; 792 | this.bgOpacity = this.decimal(1 - Math.abs(this.distance.y) / (this.windowHeight / 1.2), 5); 793 | if (this.bgOpacity < 0) { 794 | this.bgOpacity = 0; 795 | } 796 | if (this.options.verticalZoom) { 797 | this.currentImg.scale = this.bgOpacity; 798 | this.currentImg.width = this.decimal(item.width * this.currentImg.scale, 2); 799 | this.currentImg.height = this.decimal(item.height * this.currentImg.scale, 2); 800 | this.currentImg.x = item.x + this.distance.x + (item.width - this.currentImg.width) / 2; 801 | this.currentImg.y = item.y + this.distance.y + (item.height - this.currentImg.height) / 2; 802 | } else { 803 | this.currentImg.x = item.x; 804 | this.currentImg.y = item.y + this.distance.y; 805 | this.currentImg.scale = 1; 806 | } 807 | this.bg.style.opacity = this.bgOpacity; 808 | if (this.options.useTransform) { 809 | this.bg.style.transition = 'none'; 810 | item.element.style.transition = 'none'; 811 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px , 0) scale(' + this.currentImg.scale + ')'; 812 | } else { 813 | item.element.style.width = this.currentImg.width + 'px'; 814 | item.element.style.height = this.currentImg.height + 'px'; 815 | item.element.style.transform = 'translate3d(' + this.currentImg.x + 'px, ' + this.currentImg.y + 'px , 0)'; 816 | } 817 | } 818 | } 819 | } 820 | /** 821 | * 处理wrap移动结束 822 | */ 823 | NonameGallery.prototype.handleWrapPointerup = function () { 824 | // 拖拽距离超过屏幕宽度10%即可切换下一张图片 825 | const MIN_SWIPE_DISTANCE = Math.round(this.windowWidth * 0.1); 826 | const lastIndex = this.index; 827 | if (Math.abs(this.distance.x) > MIN_SWIPE_DISTANCE) { 828 | if (this.distance.x > 0 && lastIndex > 0) { 829 | this.index--; 830 | } else if (this.distance.x < 0 && lastIndex < this.previewList.length - 1) { 831 | this.index++; 832 | } 833 | } 834 | this.handleWrapSwipe(); 835 | this.handleLastImg(lastIndex); 836 | } 837 | /** 838 | * 处理img移动结束 839 | */ 840 | NonameGallery.prototype.handleImgPointerup = function () { 841 | // 垂直滑动距离超过屏幕高度10%即可关闭画廊 842 | const MIN_CLOSE_DISTANCE = Math.round(this.windowHeight * 0.1); 843 | const item = this.previewList[this.index]; 844 | const now = Date.now(); 845 | if (this.currentImg.status === 'inertia' && now - this.pointermoveTime < 200 && now - this.pinchTime > 1000) { 846 | this.handleInertia(); 847 | } else if (this.currentImg.status === 'verticalToClose' && Math.abs(this.distance.y) >= MIN_CLOSE_DISTANCE) { 848 | this.close(); 849 | } else if (this.currentImg.status === 'shrink' || (this.currentImg.status == 'verticalToClose' && Math.abs(this.distance.y) < MIN_CLOSE_DISTANCE)) { 850 | this.isAnimating = true; 851 | if (this.options.useTransform) { 852 | this.bg.style.opacity = '1'; 853 | item.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 854 | item.element.style.transform = 'translate3d(' + item.x + 'px, ' + item.y + 'px, 0) scale(1)'; 855 | } else { 856 | const obj = { 857 | bg: { 858 | opacity: { from: this.bgOpacity, to: 1 } 859 | }, 860 | img: { 861 | width: { from: this.currentImg.width, to: item.width }, 862 | height: { from: this.currentImg.height, to: item.height }, 863 | x: { from: this.currentImg.x, to: item.x }, 864 | y: { from: this.currentImg.y, to: item.y }, 865 | index: this.index 866 | } 867 | } 868 | this.raf(obj); 869 | } 870 | this.bgOpacity = 1; 871 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 872 | } 873 | if (this.isAnimating === false) { 874 | this.dragTarget = ''; 875 | this.dragDirection = ''; 876 | } 877 | } 878 | /** 879 | * 处理惯性滚动 880 | */ 881 | NonameGallery.prototype.handleInertia = function () { 882 | const item = this.previewList[this.index]; 883 | const speed = { x: this.diff.x, y: this.diff.y }; 884 | const self = this; 885 | function step(timestamp) { 886 | speed.x *= 0.95; 887 | speed.y *= 0.95; 888 | self.currentImg.x = self.decimal(self.currentImg.x + speed.x, 2); 889 | self.currentImg.y = self.decimal(self.currentImg.y + speed.y, 2); 890 | self.handleBoundary(); 891 | if (self.options.useTransform) { 892 | item.element.style.transform = 'translate3d(' + self.currentImg.x + 'px, ' + self.currentImg.y + 'px, 0) scale(' + self.currentImg.scale + ')'; 893 | } else { 894 | item.element.style.transform = 'translate3d(' + self.currentImg.x + 'px, ' + self.currentImg.y + 'px, 0)'; 895 | } 896 | if (Math.abs(speed.x) > 1 || Math.abs(speed.y) > 1) { 897 | self.inertiaRafId = window.requestAnimationFrame(step); 898 | } 899 | } 900 | this.inertiaRafId = window.requestAnimationFrame(step); 901 | } 902 | /** 903 | * 处理wrap滑动 904 | */ 905 | NonameGallery.prototype.handleWrapSwipe = function () { 906 | this.isWrapAnimating = true; 907 | const obj = { 908 | wrap: { 909 | x: { from: this.wrapX, to: this.windowWidth * this.index * -1 } 910 | } 911 | } 912 | this.wrapRaf(obj); 913 | this.counter.innerHTML = (this.index + 1) + ' / ' + this.previewList.length; 914 | } 915 | /** 916 | * 处理上一张图片 917 | * @param {number} lastIndex 918 | */ 919 | NonameGallery.prototype.handleLastImg = function (lastIndex) { 920 | // 根据索引判断是否切换图片 921 | if (this.index !== lastIndex) { 922 | // 如果上一张图片放大过,则恢复 923 | if (this.currentImg.scale > 1) { 924 | const lastItem = this.previewList[lastIndex]; 925 | if (this.options.useTransform) { 926 | lastItem.element.style.transition = 'transform ' + this.options.duration + 'ms ease-out, opacity ' + this.options.duration + 'ms ease-out'; 927 | lastItem.element.style.transform = 'translate3d(' + lastItem.x + 'px, ' + lastItem.y + 'px, 0) scale(1)'; 928 | } else { 929 | const obj = { 930 | img: { 931 | width: { from: this.currentImg.width, to: lastItem.width }, 932 | height: { from: this.currentImg.height, to: lastItem.height }, 933 | x: { from: this.currentImg.x, to: lastItem.x }, 934 | y: { from: this.currentImg.y, to: lastItem.y }, 935 | index: lastIndex 936 | } 937 | } 938 | this.raf(obj); 939 | } 940 | lastItem.element.style.cursor = 'zoom-in'; 941 | } 942 | // 设置当前图片数据 943 | const item = this.previewList[this.index]; 944 | this.setCurrentImg(item.x, item.y, item.width, item.height, 1, 1, ''); 945 | } 946 | } 947 | /** 948 | * 过渡结束回调 949 | * @param {TransitionEvent } e 950 | */ 951 | NonameGallery.prototype.handleTransitionEnd = function (e) { 952 | // 过滤掉bg transitionend 953 | if (e.target.tagName === 'IMG') { 954 | // wrap滑动,上一张图片恢复动画完成后,不清除dragTarget,因为wrap动画可打断 955 | if (e.target === this.previewList[this.index].element) { 956 | this.isAnimating = false; 957 | this.dragTarget = ''; 958 | this.dragDirection = ''; 959 | } 960 | if (this.status === 'close') { 961 | // 解绑事件 962 | this.unbindEventListener(); 963 | this.container.remove(); 964 | } 965 | } 966 | } 967 | /** 968 | * 保留n位小数 969 | * @param {number} num 数字 970 | * @param {number} n n位小数 971 | * @returns 972 | */ 973 | NonameGallery.prototype.decimal = function (num, n) { 974 | const x = Math.pow(10, n); 975 | return Math.round(num * x) / x; 976 | } 977 | /** 978 | * 获取中心点 979 | * @param {object} a 第一个点的位置 980 | * @param {object} b 第二个点的位置 981 | * @returns 982 | */ 983 | NonameGallery.prototype.getCenter = function (a, b) { 984 | const x = (a.x + b.x) / 2; 985 | const y = (a.y + b.y) / 2; 986 | return { x: x, y: y }; 987 | } 988 | /** 989 | * 曲线函数 990 | * @param {number} from 开始位置 991 | * @param {number} to 结束位置 992 | * @param {number} time 动画已执行的时间 993 | * @param {number} duration 动画时长 994 | * @returns 995 | */ 996 | NonameGallery.prototype.easeOut = function (from, to, time, duration) { 997 | const change = to - from; 998 | const t = time / duration; 999 | return -change * t * (t - 2) + from; 1000 | } 1001 | /** 1002 | * 开始、结束、缩放、恢复(例如下滑关闭未达到临界值)动画函数 1003 | * @param {object} obj 属性 1004 | */ 1005 | NonameGallery.prototype.raf = function (obj) { 1006 | const self = this; 1007 | let start; 1008 | let count = 0; 1009 | const duration = this.options.duration; 1010 | const item = this.previewList[obj.img.index]; 1011 | function step(timestamp) { 1012 | if (start === undefined) { 1013 | start = timestamp; 1014 | } 1015 | let time = timestamp - start; 1016 | if (time > duration) { 1017 | time = duration; 1018 | count++; 1019 | } 1020 | if (obj.bg) { 1021 | const bgOpacity = self.decimal(self.easeOut(obj.bg.opacity.from, obj.bg.opacity.to, time, duration), 5); 1022 | self.bg.style.opacity = bgOpacity; 1023 | } 1024 | if (obj.img.opacity) { 1025 | const opacity = self.decimal(self.easeOut(obj.img.opacity.from, obj.img.opacity.to, time, duration), 5); 1026 | item.element.style.opacity = opacity; 1027 | } 1028 | const width = self.decimal(self.easeOut(obj.img.width.from, obj.img.width.to, time, duration), 2); 1029 | const height = self.decimal(self.easeOut(obj.img.height.from, obj.img.height.to, time, duration), 2); 1030 | const x = self.decimal(self.easeOut(obj.img.x.from, obj.img.x.to, time, duration), 2); 1031 | const y = self.decimal(self.easeOut(obj.img.y.from, obj.img.y.to, time, duration), 2); 1032 | item.element.style.width = width + 'px'; 1033 | item.element.style.height = height + 'px'; 1034 | item.element.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0)'; 1035 | if (count <= 1) { 1036 | window.requestAnimationFrame(step); 1037 | } else { 1038 | if (obj.img.index === self.index) { 1039 | self.isAnimating = false; 1040 | self.dragTarget = ''; 1041 | self.dragDirection = ''; 1042 | } 1043 | if (self.status === 'close') { 1044 | self.unbindEventListener(); 1045 | self.container.remove(); 1046 | } 1047 | } 1048 | } 1049 | window.requestAnimationFrame(step); 1050 | } 1051 | /** 1052 | * 动画函数 1053 | * @param {object} obj 1054 | */ 1055 | NonameGallery.prototype.wrapRaf = function (obj) { 1056 | const self = this; 1057 | let start; 1058 | let count = 0; 1059 | const duration = this.options.duration; 1060 | function step(timestamp) { 1061 | if (start === undefined) { 1062 | start = timestamp; 1063 | } 1064 | let time = timestamp - start; 1065 | if (time > duration) { 1066 | time = duration; 1067 | count++; 1068 | } 1069 | self.wrapX = self.decimal(self.easeOut(obj.wrap.x.from, obj.wrap.x.to, time, duration), 2); 1070 | self.wrap.style.transform = 'translate3d(' + self.wrapX + 'px, 0, 0)'; 1071 | if (count <= 1) { 1072 | self.wrapRafId = window.requestAnimationFrame(step); 1073 | } else { 1074 | self.isWrapAnimating = false; 1075 | self.dragTarget = ''; 1076 | self.dragDirection = ''; 1077 | } 1078 | } 1079 | this.wrapRafId = window.requestAnimationFrame(step); 1080 | } 1081 | if (typeof define === 'function' && define.amd) { 1082 | define(function () { return NonameGallery; }); 1083 | } else if (typeof module === 'object' && typeof exports === 'object') { 1084 | module.exports = NonameGallery; 1085 | } else { 1086 | window.NonameGallery = NonameGallery; 1087 | } 1088 | })(); --------------------------------------------------------------------------------