图片局部放大的实现

上次在sf看到一个问题,用vue实现类似淘宝商品图片局部放大的效果,想想自己还没实现过,就尝试了一下,在搜索结果下,看到张鑫旭的一篇博文讲解,大致明白了其原理,于是写个对应的组件尝试一下。

需求

先描述一下大致需求:有几张小图,横向排列,当选中其中一张时,会显示小图对应的大图,并且在鼠标移动到大图的局部位置时,悬浮显示其局部位置的放大效果。

此需求分为两点:

  1. 小图排列,选中时显示对应的大图 (这里大小图应该有固定的显示尺寸了)
  2. 鼠标移动到大图时,显示局部位置放大效果(本文主题)

实现描述

对于需求一,在vue数据状态驱动机制下,其实就是定义两个数据:imgs(图片数据数组,含大图小图地址等细节)、activeIndex(当前选中索引)

只要控制好当小图点击时响应函数修改activeIndex即可,根据数据驱动,view也会跟着变化。

对于需求二,从上面提到的文章已经知道怎么实现了,主要逻辑是,鼠标移动时计算鼠标在图片里的相对位置,其次则是悬浮元素设置background-image,通过鼠标的相对位置来设置background-position.

当如果悬浮元素取得图片跟大图图片尺寸一致的话,其实就并没有什么放大效果了,纯粹局部裁剪的效果。

所以在描述需求时提到了一点,大小图应该有固定的显示尺寸,大图显示的尺寸一般并不会是原图尺寸而是一个固定比原图尺寸小的图,而放大效果,恰恰就是用的原图尺寸,这样就有了所谓的放大效果。

关于计算

逻辑弄清楚了,写代码前,需要确定一些计算公式,比如鼠标相对图片的相对位置、获取局部的背景图定位

鼠标的相对位置

这块很好做,鼠标的移动事件对象event带有pageX和pageY两个值,分别对应x、y轴上相对文档的坐标。

而要相对图片,则还需要图片的相对文档坐标信息,这个可以用getBoundingClientRect()获得topleft.

这样相对位置 就是 x = pageX - left; y = pageY - top;

局部背景图定位

显示局部的浮层有一定的宽高,假设为detailWidth \ detailHeight,我们获取的鼠标相对位置坐标实际上是相对于固定尺寸的图片的坐标,如果在实际原图大小上,其对应的坐标需要一定比例的转换,假如原图是显示图的两倍,那么相对位置a\b,在原图的位置应该是 2a\2b,这样我们就可以通过background-position: -2apx -2bpx来定位了。

但是如果这样子的话,局部显示的位置还有点问题,那就是我们想要的是鼠标指向的点应该显示在局部浮层的中心,所以定位还要考虑浮层本身的宽高尺寸。

因此定位公式如下:

x = - (posX × 比例 - detailWidth/2)
y = - (posY × 比例 - detailHeight/2)

代码

组件代码如下:

<template>
  <div class="img-show">
    <div class="big-img"><img @mousemove="move" @mouseout='detailShow=false' 
    :src="imgs[activeIndex].bigsrc" 
    :title="imgs[activeIndex].title">
        <div class="detail" :style="{width: detailWidth+'px',
         height: detailHeight+'px',
         backgroundImage:'url('+imgs[activeIndex].bigsrc+')',
         backgroundRepeat: 'no-repeat', 
         top: detailTop+'px', 
         left: detailLeft+'px',
         backgroundPosition: detailPosition}" v-show="detailShow">
         </div>
    </div>
    <ul class="small-img">
      <li v-for='(img, index) in imgs' @click="showBig(index)" :class='{ active: activeIndex==index}'>
      <img :src="img.src" :title="img.title" alt="">
      </li>
    </ul>
    <button @click="messSort">乱排</button>
    <button @click="reverseSort">倒排</button>
  </div>
</template>

<script>
export default {
  name: 'imgshow',
  data () {
    var imgs = [
        {
          src:'http://localhost:10087/1.jpg',
          bigsrc:'http://localhost:10087/b1.jpg',
          title:'我是图1'
        },
        {
          src:'http://localhost:10087/2.jpg',
          bigsrc:'http://localhost:10087/b2.jpg',
          title:'我是图2'
        },
        {
          src:'http://localhost:10087/3.jpg',
          bigsrc:'http://localhost:10087/b3.jpg',
          title:'我是图3'
        },
        {
          src:'http://localhost:10087/4.jpg',
          bigsrc:'http://localhost:10087/b4.jpg',
          title:'我是图4'
        }
      ];
    return {
      imgs,
      activeIndex:0,
      detailShow:false,
      detailTop:0,
      detailLeft:0,
      detailWidth: 200,
      detailHeight: 200,
      detailPosition: '0 0'
    }
  },
  computed:{
    offset(){
      return this.$el.getBoundingClientRect()
    }
  },
  methods:{
    showBig (index){
      this.activeIndex = index
    },
    messSort (){
      this.imgs = this.imgs.sort((a,b)=>Math.random()>0.5)
    },
    reverseSort(){
      this.imgs = this.imgs.reverse()
    },
    move(e){
      this.detailShow = true
      var x = e.pageX - this.offset.left,
          y = e.pageY - this.offset.top;
          
          if(x>this.offset.width||x<0||y<0||y>this.offset.height){
            this.detailShow = false
            return;
          }
          this.detailTop = y-this.detailHeight
          this.detailLeft = x+this.detailWidth/2

          this.detailPosition = '-'+Math.round(x*(430/300)-this.detailWidth/2)+'px -'+Math.round(y*(430/300)-this.detailHeight/2)+'px'
    }
  }
}
</script>
<style scoped>
.img-show{
  width: 300px;
  margin:50px auto;
}
.big-img{
  width: 100%;
  position: relative;
}
.detail{
  position: absolute;
  border: 3px solid #fff;
}
.big-img img{
  width: 100%;
}
.small-img{
  width: 100%;
}
.small-img li{
  float: left;
  box-sizing: border-box;
  border:1px solid #fff;
  width: 25%;
  height: 25%;
  list-style: none;
  cursor: pointer;
}
.small-img li.active{
  border-color:#333;
}
.small-img img{
  width: 100%;
}
</style>

效果如下:

image

update!!!

后面又做了功能的优化处理,加入了鼠标移动面板和边缘检测处理。

1.增加鼠标移动时在图片上层增加半透明的浮层块来表示放大部分范围

实现上只需要增加浮层块的一个宽高和根据移动坐标调整自身偏移即可,但为了防止浮层导致鼠标事件切换而出现闪烁问题,可以在浮层设置pointer-events:none来回避浮层影响事件。

2.边缘检测,像淘宝那种,当鼠标移动到某个局部范围已经无法填充放大浮层的内容时,在边缘处移动,放大图并不会做改变,除了一些位置偏移,具体看效果图,这个不好描述。

实现上,我们需要清楚,局部放大图所展示的局部范围,在大图处对应的位置,由于原始图跟大图之间存在比例α = srcWidth/bigWidth, 那么局部放大图实际上是原始图的一个局部,所以局部放大图的宽高跟大图局部的范围比例也是α。于是我们可以知道当局部浮层左边缘和大图左边缘重叠时,局部浮层的中心点位置为局部浮层宽度的一半,这样我们就知道鼠标可移动的实际边缘点的left值,以此类推,也可以求出上下右的值。

大概代码如下:

<template>
  <div class="img-show2">
    <div class="big-img" ref='test'><img ref="big" @mousemove="move" @mouseout='detailShow=false' :src="imgs[activeIndex].bigsrc" :title="imgs[activeIndex].title" alt="">
     <span class="cursor" :style="{width: cursorWidth+'px',
       height: cursorHeight+'px',
       top: detailShow?cursorTop+'px':0, 
       left: detailShow?cursorLeft+'px':0}" v-show="detailShow"></span>
    <div class="detail" :style="{width: detailWidth+'px',
     height: detailHeight+'px',
     backgroundImage:'url('+imgs[activeIndex].bigsrc+')',
     backgroundRepeat: 'no-repeat', 
     top: detailTop+'px', 
     left: detailLeft+'px',
     backgroundPosition: detailShow?detailPosition:'0 0'}" v-show="detailShow"></div></div>
    <ul class="small-img">
      <li v-for='(img, index) in imgs' @click="showBig(index)" :class='{ active: activeIndex==index}'><img :src="img.src" :title="img.title" alt=""></li>
    </ul>
   
  </div>
</template>

<script>
export default {
  name: 'imgshow2',
  props:[
    'detailWidth',
    'detailHeight',
    'detailTop',
    'detailLeft',
    'bigWidth',
    'srcWidth'],
  data () {
    var imgs = [
        {
          src:'http://localhost:10087/1.jpg',
          bigsrc:'http://localhost:10087/b1.jpg',
          title:'我是图1'
        },
        {
          src:'http://localhost:10087/2.jpg',
          bigsrc:'http://localhost:10087/b2.jpg',
          title:'我是图2'
        },
        {
          src:'http://localhost:10087/3.jpg',
          bigsrc:'http://localhost:10087/b3.jpg',
          title:'我是图3'
        },
        {
          src:'http://localhost:10087/4.jpg',
          bigsrc:'http://localhost:10087/b4.jpg',
          title:'我是图4'
        }
      ];
    return {
      imgs,
      activeIndex:0,
      detailShow:false,
      px:0,//鼠标移动后x轴的有效值,超过边缘按边缘值取值
      py:0,
      //鼠标浮层宽度,高度
      cursorWidth: this.detailWidth*this.bigWidth/this.srcWidth,
      cursorHeight: this.detailHeight*this.bigWidth/this.srcWidth 
    }
  },
  computed:{
    offset(){
      return this.$refs.big.getBoundingClientRect()
    },
    //局部放大图有效图的边缘范围:上下左右
    moveArea(){
      var left,right,top,bottom;
      left = Math.round(this.detailWidth/2/this.srcWidth*this.bigWidth)
      right = Math.round(this.offset.width - left)
      top = Math.round(this.detailHeight/2/this.srcWidth*this.bigWidth)
      bottom = Math.round(this.offset.height - top)
      return {
        left,
        right,
        top,
        bottom
      }
    },
    //放大图背景图的偏移位置
    detailPosition(){
      var posX = Math.round(this.px*(this.srcWidth/this.bigWidth)-this.detailWidth/2),
          posY = Math.round(this.py*(this.srcWidth/this.bigWidth)-this.detailHeight/2)
      return '-'+posX+'px -'+posY+'px'
    },
    //鼠标浮层的偏移位置
    cursorTop(){
      return this.py - this.moveArea.top
    },
    cursorLeft(){
      return this.px - this.moveArea.left
    }
  },
  methods:{
    showBig (index){
      this.activeIndex = index
    },
    
    move(e){
      this.detailShow = true
      var x = e.pageX - this.offset.left,
          y = e.pageY - this.offset.top;
          this.px = x,
          this.py =y;
          
          if(x>this.offset.width||x<0||y<0||y>this.offset.height){
            this.detailShow = false
            return;
          }
          if(x<this.moveArea.left){
            this.px = this.moveArea.left
          }
          if(x>this.moveArea.right){
            this.px = this.moveArea.right
          }
          if(y<this.moveArea.top){
            this.py = this.moveArea.top
          }
          if(y>this.moveArea.bottom){
            this.py = this.moveArea.bottom
          }

         
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.cursor{
  position: absolute;
  display: block;
  background: rgba(0,0,0,.3);
  pointer-events: none;
}
.img-show2{
  width: 300px;
  margin:50px auto;
}
.big-img{
  width: 100%;
  position: relative;
  cursor: crosshair;
}
.detail{
  position: absolute;
  border: 3px solid #fff;
}
.big-img img{
  width: 100%;
}
.small-img{
  width: 100%;
}
.small-img li{
  float: left;
  box-sizing: border-box;
  border:1px solid #fff;
  width: 25%;
  height: 25%;
  list-style: none;
  cursor: pointer;
}
.small-img li.active{
  border-color:#333;
}
.small-img img{
  width: 100%;
}
</style>

较之之前的代码,我根据数据变动,将一些数据调整到props由父组件来传递,一些data数据改为computed避免重复计算。

遇到的问题: 就是refs的获取,需要组件挂载后才能获得,而一些computed因为template需要用到,并且引用到了refs导致,refs获取不到,后来发现这些computed初始化并不一定需要 ,所以用了变量三元运算符来规避计算获取。

效果如下:

image