**

<table><tbody><tr><td bgcolor="#FDFFE7"><font size="4">原创不易,希望能关注下我们,再顺手点个赞~~<font></font></font></td></tr></tbody></table> **

> 本文首发于政采云前端团队博客: 基于 Vue 的商品主图放大镜方案open in new window

一玲.png

前言

在做电商类应用时,难免会遇到商品主图实现放大镜效果的场景,现有的基于Vue的第三方包不多并且无法直接复用,今天,我来分享一种高稳定性的基于 Vue 的图片放大镜方法。

实现原理

放大镜的原理用一句话概括,就是根据小图上的鼠标位置去定位大图。

> 图1 原理图(以2倍放大为例)

image-20190915202151028

相信原理图已经画的很明白了, 图中,左侧框是小图框,其蓝色区域为图片遮罩层(需放大区域),右侧框是整个大图目前所在区域,其蓝色区域是放大区域,设置超出隐藏,就实现了放大遮罩区域的效果。

显然,两块蓝色区域存在着某种对应关系,即遮罩的左上角位置(相对于小图,以下称 X 坐标)和放大区域(相对于大图)的左上角位置是成比例的,即放大倍数。计算出 X 坐标后,适当调整背景图的位置,使大图向反方向移动 scale 倍的 X 坐标即可。

X 坐标为(maskX,maskY),以计算 maskX 为例:

鼠标移动中会产生 e.clientX ,标识鼠标与浏览器左侧的距离,小图与浏览器左侧的距离是 left ,由于遮罩始终是一个以鼠标为中心的正方形,所以:

maskX = e.clientX - left - mask/2

同理,

maskY = e.clientY - top - mask/2

大图的对应样式设置为:

{
  left: - maskX * scale + &#39;px&#39;;
  top: - maskY * scale + &#39;px&#39;;
}

效果演示

> 图2 长图展示

大

> 图3 宽图展示

> 图4 两倍放大效果图

大

> 图5 四倍放大效果图

小

核心代码

HTML

一般放大镜实现的是 1:1 等宽等高的正方形图片,这里兼容了其他比例的图片,设置图片为垂直居中对齐,包括小图,大图。如果小图不够充满整个小图框,余留下的空白部分也可以有放大效果,只不过放大结果依然是空白。 这样只需计算背景图的移动距离,不用过多的关注图片定位问题。

&lt;template&gt;
 &lt;div class=&quot;magnifier&quot;&gt;
    &lt;!-- 小图 --&gt;
    &lt;div class=&quot;small-box&quot; @mouseover=&quot;handOver&quot;  @mousemove=&quot;handMove&quot; @mouseout=&quot;handOut&quot;&gt;
      &lt;img class=&quot;smallPic&quot; :src=&quot;`${src}?x-oss-process=image/resize,l_836`&quot; /&gt;
      &lt;div class=&quot;magnifier-zoom&quot; 
        v-show=&quot;showMask&quot;
        :style=&quot;{
          background: configs.maskColor,
          height: configs.maskWidth + &#39;px&#39;,
          width: configs.maskHeight + &#39;px&#39;, 
          opacity: configs.maskOpacity, 
          transform: transformMask
        }&quot;
      &gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;!-- 大图, 注意误差 --&gt;
    &lt;div class=&quot;magnifier-layer&quot; 
      v-show=&quot;showMagnifier&quot;
      :style=&quot;{ 
        width: configs.width + &#39;px&#39;, 
        height: configs.height + &#39;px&#39;, 
        left: configs.width + 20 + &#39;px&#39; 
      }&quot;
    &gt;
      &lt;div class=&quot;big-box&quot;
        :style=&quot;{ 
          width: bigWidth + &#39;px&#39;,
          height: bigHeight + &#39;px&#39;,
          left: moveLeft,
          top: moveTop
        }&quot;
      &gt;
        &lt;div class=&quot;big-box-img&quot;
          :style=&quot;{ 
            width: bigWidth - 2  + &#39;px&#39;, 
            height: bigHeight - 2 + &#39;px&#39; 
          }&quot;
        &gt;
          &lt;img
            :src=&quot;bigSrc&quot;
            :style=&quot;{ 
              maxWidth: bigWidth - 2 + &#39;px&#39;, 
              maxHeight: bigHeight -2 + &#39;px&#39; 
            }&quot;
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
JS

这里主要有三个事件函数。

  • handOver:鼠标进入到小图框上的事件,此时显示遮罩和放大区域,并计算小图框的位置信息。
handOver() {
  // 计算小图框在浏览器中的位置
  this.imgObj = this.$el.getElementsByClassName(&#39;small-box&#39;)[0];
  this.imgRectNow = this.imgObj.getBoundingClientRect();
  this.showMagnifier = true;
  this.showMask = true;
}
  
  • handMove:鼠标在小图上的移动事件,此事件发生在 handOver 之后,计算数据,移动遮罩以及背景图;
handMove(e) {
  // 计算初始的遮罩左上角的坐标
  let objX = e.clientX - this.imgRectNow.left;
  let objY = e.clientY - this.imgRectNow.top;

  // 计算初始的遮罩左上角的坐标
  let maskX = objX - this.configs.maskWidth / 2;
  let maskY = objY - this.configs.maskHeight / 2;

  // 判断是否超出界限,并纠正
  maskY = maskY &lt; 0 ? 0 : maskY; 
  maskX = maskX &lt; 0 ? 0 : maskX; 
  if(maskY + this.configs.maskHeight &gt;= this.imgRectNow.height) {
    maskY = this.imgRectNow.height - this.configs.maskHeight;
  }
  if(maskX + this.configs.maskWidth &gt;= this.imgRectNow.width) {
    maskX = this.imgRectNow.width - this.configs.maskWidth;
  }

  // 遮罩移动
  this.transformMask = `translate(${maskX}px, ${maskY}px)`;

  // 背景图移动
  this.moveLeft = - maskX * this.configs.scale + &quot;px&quot;;
  this.moveTop = - maskY * this.configs.scale + &quot;px&quot;;
}
  • handOut:鼠标离开小图事件,此时无放大镜效果,隐藏遮罩和放大区域。
handOut() {
  this.showMagnifier = false;
  this.showMask = false;
}

以上三个事件基本上就实现了图片的放大镜功能。

但仔细看,你会发现每次移入小图框都会触发一次 handOver 事件,并且计算一次小图框 DOM (imgObj) 。

为了优化此问题,可以用 init 标识是否是页面加载后首次触发 handOver 事件,如果是初始化就计算imgObj 信息,否则不计算。

handOver() {
  if (!this.init) {
    this.init = true;
    // 原 handOver 事件
    ...
  } 
  this.showMagnifier = true;
  this.showMask = true;
},
  

在测试的过程中,发现页面滚动后,会出现遮罩定位错误的情况,原来是因为初始化时,我们固定死了小图框的位置信息(存放在 this.imgRectNow ),导致 handMove 事件中的移动数据计算错误。

解决这个问题有两种方案:

  • 监听 scroll 事件,更新 this.imgRectNow;
  • 在 handMove 事件中更新 this.imgRectNow。

这里选择了第二种。

handMove(e) {
  // 动态获取小图的位置(或者监听 scroll )
  let imgRectNow = this.imgObj.getBoundingClientRect();
  let objX = e.clientX - imgRectNow.left;
  let objY = e.clientY - imgRectNow.top;
  // 原 handMove 事件剩余内容
  ...
},

综合以上,我们已经实现了一个完美的图片放大镜功能。最终的 js 如下所示:

data() {
  return {
    imgObj: {},
    moveLeft: 0,
    moveTop: 0,
    transformMask:`translate(0px, 0px)`,
    showMagnifier:false,
    showMask:false,
    init: false,
  };
},
computed: {
  bigWidth(){
    return this.configs.scale * this.configs.width;
  },
  bigHeight(){
    return this.configs.scale * this.configs.height;
  }
},
methods: {
  handMove(e) {
    // 动态获取小图的位置(或者监听 scroll )
    let imgRectNow = this.imgObj.getBoundingClientRect();
    let objX = e.clientX - imgRectNow.left;
    let objY = e.clientY - imgRectNow.top;

    // 计算初始的遮罩左上角的坐标
    let maskX = objX - this.configs.maskWidth / 2;
    let maskY = objY - this.configs.maskHeight / 2;

    // 判断是否超出界限,并纠正
    maskY = maskY &lt; 0 ? 0 : maskY; 
    maskX = maskX &lt; 0 ? 0 : maskX; 
    if(maskY + this.configs.maskHeight &gt;= imgRectNow.height) {
      maskY = imgRectNow.height - this.configs.maskHeight;
    }
    if(maskX + this.configs.maskWidth &gt;= imgRectNow.width) {
      maskX = imgRectNow.width - this.configs.maskWidth;
    }

    // 遮罩移动
    this.transformMask = `translate(${maskX}px, ${maskY}px)`;

    // 背景图移动
    this.moveLeft = - maskX * this.configs.scale + &quot;px&quot;;
    this.moveTop = - maskY * this.configs.scale + &quot;px&quot;;
  },
  handOut() {
    this.showMagnifier = false;
    this.showMask = false;
  },
  handOver() {
    if (!this.init) {
      this.init = true;
      this.imgObj = this.$el.getElementsByClassName(&#39;small-box&#39;)[0];
    }
    this.showMagnifier = true;
    this.showMask = true;
  }
}

使用方法

本示例中的固定参数:小图框:420 * 420 。

程序可接受参数:

// 小图地址
src: {
  type: String,
},
// 大图地址
bigSrc: {
  type: String,
},
// 配置项
configs: {
  type: Object,
    default() {
    return {
      width:420,//放大区域
      height:420,//放大区域
      maskWidth:210,//遮罩
      maskHeight:210,//遮罩
      maskColor:&#39;rgba(25,122,255,0.5)&#39;,//遮罩样式
      maskOpacity:0.6,
      scale:2,//放大比例
    };
  }
}

文中图 2 是一张长图,小图的最大边不超过 836px(二倍图) ,大图为了视觉效果,分辨率尽量高点,程序会根据配置项自动设置对应的 height , width ,长图与宽图的效果对比可参考图3。

配置项可根据应用场景自行设置,本文示例的配置项是 2 倍放大,效果可参考图 4,四倍放大效果可参考图 5。

总结

其实图片放大镜的实现思路没有那么复杂,核心点有两点:

  • 小图、大图的定位,遮罩和放大区域的创建方法
  • 放大镜的原理理解,并用代码实现 DOM 的移动等。

本文顺着这个思路,做了一个简单的实现,还有一些优化的空间,欢迎各位大佬在评论区讨论。虽然代码看起来不是非常优雅,但是足够明了,感兴趣的同学可以自己尝试一下。

招贤纳士

招人,前端,隶属政采云前端大团队(ZooTeam),50 余个小伙伴正等你加入一起浪。如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5年工作时间3年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手参与一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

推荐阅读

Vue 组件数据通信方案总结open in new window

自动化 Web 性能优化分析方案open in new window

CSS 层叠上下文(Stacking Context)open in new window