Demo实现:悬浮图片放大展示

2,116 阅读2分钟

实现效果

example.gif

期望功能

  • 当悬浮框触碰到图片边界时,不会继续向外移动
  • 悬浮放大展示的区域与悬浮框选的区域一致

思路

逻辑上分成三个部分

  • 一部分是容器负责展示图片
  • 还有一部分是悬浮框负责交互的悬浮效果
  • 还有就是最终展示出来的图片

主要的交互是设置悬浮框的位置,和展示图片的区域

展示图片的区域位置放在哪里其实影响不大,悬浮框跟img就需要在一个container容器下,一个是为了img设置宽高100%撑满容器,一个是为了悬浮框设置absolute定位用于定位

可以得到html的结构大致如下

<div
  ref="containerRef"
  class="container"
  :style="containerStyle"
>
  <!-- img-box -->
  <img :src="defaultImgSrc">
  <!-- hover-box -->
  <div class="hover-box" :style="hoverBoxStyle" />
</div>

<!-- preview-box -->
<div
  class="preview-box" :style="previewBoxStyle"
/>

悬浮框的样式

悬浮框的基本样式如下

{
    width: 100px,
    aspect-ratio: 1,
    position: absolute,
    left: 0,
    top: 0,
    background-color: rgba(24, 144, 255,.6),
    transform: `translateX(-50%)
                translateY(-50%),
}
  • 设置aspect-ratio而不是height是为了接下设置展示图片的大小时只需要设置一样的aspect-ratio就可以保证展示区域的比例同悬浮框的比例一致
  • 接下来就是在transform上做功夫,获取到鼠标的位置,并对悬浮框做相应的偏移

获取鼠标的偏移位置

借助vueuse的useMouse,可以很轻松获取到鼠标的位置

const { x, y } = useMouse({
target: containerRef,
})

设置target为containerRef,这样x,y的值只会在鼠标在container容器上移动时才会更新

但这个位置相对于窗口的偏移量,而我们需要的是相对于container容器上的偏移量,所以还需要获取contain容器距离窗口的偏移量

通过不断叠加父容器的offsetLeft和offsetTop来做到

/**
 * get offsetTop and offsetLeft to window
 */
function getPageLeftAndTop(element: HTMLElement) {
  let pageLeft = element.offsetLeft
  let pageTop = element.offsetTop
  let parent = element.offsetParent as HTMLElement

  while (parent) {
    pageLeft += parent.offsetLeft
    pageTop += parent.offsetTop
    parent = parent.offsetParent as HTMLElement
  }

  return {
    pageLeft,
    pageTop,
  }
}

const mousePosition = computed(() => {
let minusLeft = 0
let minusTop = 0
const containerElement = toValue(containerRef)

if (containerElement) {
  const { pageLeft, pageTop } = getPageLeftAndTop(containerElement)
  minusLeft = pageLeft
  minusTop = pageTop
}

return {
  mouseX: x.value - minusLeft,
  mouseY: y.value - minusTop,
}
})

这样就在mousePosition里面得到了鼠标相对于container的x和y值

添加悬浮框的样式

此时,完整的悬浮框样式就可以写出来了

const hoverBoxStyle = computed<CSSProperties>(() => {
    return {
      width: `${toValue(hoverWidth)}px`,
      aspectRatio: aspectRatio.value,
      position: 'absolute',
      left: 0,
      top: 0,
      backgroundColor: 'rgba(24, 144, 255,.6)',
      transform: `translateX(-50%)
                    translateY(-50%)
                    translateX(${mousePosition.value.mouseX}px)
                    translateY(${mousePosition.value.mouseY}px)`,
    }
})

这里把悬浮框的宽度(hoverWidth),和比例(aspectRatio)都抽离出来便于后续处理

处理鼠标移动到container边缘情况

当鼠标位置移动到container边缘时,悬浮框的位置也会跟随着移动到边缘,那显然我们是不希望悬浮框的边界超出容器的,这样就没有显示的图片了,所以需要再对mousePosition做一下处理

const mousePosition = computed(() => {
    let minusLeft = 0
    let minusTop = 0
    let containerWidth = Infinity
    let containerHeight = Infinity
    const containerElement = toValue(containerRef)

    if (containerElement) {
      const { pageLeft, pageTop } = getPageLeftAndTop(containerElement)
      minusLeft = pageLeft
      minusTop = pageTop
      containerWidth = containerElement.offsetWidth
      containerHeight = containerElement.offsetHeight
    }

    return {
      mouseX: Math.min(Math.max(toValue(hoverWidth as any) / 2, x.value - minusLeft), containerWidth - toValue(hoverWidth as any) / 2),
      mouseY: Math.min(Math.max(toValue(hoverHeight as any) / 2, y.value - minusTop), containerHeight - toValue(hoverHeight as any) / 2),
    }
})

使得mouseX最小为悬浮框宽度的一半,最大为容器的宽度减去一半的悬浮框的宽度,高度同理

这样就得到了一个悬浮框的样式,还有mousePosition用于后续的图片显示

图片展示框的样式

首先可以得到图片展示框的基本样式

{
  width: 300px,
  aspect-ratio: 1,
  backgroundImage: url('example'),
  overflow: 'hidden',
  background-repeat: 'no-repeat',
}

计算背景图片的大小

我们可以根据展示容器的大小除以悬浮框的大小得到一个放大比例,再与展示框的大小做乘法,就可以得到图片的大小了

const previewBoxStyle = computed<CSSProperties>(() => {
    const previewHeight = toValue(previewWidth as any) / aspectRatio.value

    /** @default 3 */
    const xRatio = toValue(previewWidth as any) / toValue(hoverWidth as any)
    /** @default 3 */
    const yRatio = previewHeight / toValue(hoverHeight as any)

    /** @default 600 */
    const xBgSize = toValue(containerWidth as any) * xRatio
    /** @default 600 */
    const yBgSize = toValue(containerHeight as any) * yRatio

    return {
      width: `${toValue(previewWidth)}px`,
      aspectRatio: aspectRatio.value,
      backgroundImage: `url(${imgSrc})`,
      overflow: 'hidden',
      backgroundRepeat: 'no-repeat',
      backgroundSize: `${xBgSize}px ${yBgSize}px`,
    }
  })

计算背景图片的偏移量

以宽度为例,前面计算得到的mousePosition,是悬浮框使用的偏移量,而悬浮框的transform是做了translateX(-50%)和translateY(-50%)的,所以它的偏移中心可以视为悬浮框中心的位置,而图片展示框的偏移中心还是默认的左上角,所以需要先将mousePosition的值修正到左上角为中心的偏移量,也就是

mousePosition.value.mouseX - toValue(hoverWidth as any) / 2

将要偏移的值与之前的放大比例相乘,就可以得到图片的偏移量,不过图片的偏移是负的

就可以得到完整的展示图片框的样式

const previewBoxStyle = computed<CSSProperties>(() => {
    const previewHeight = toValue(previewWidth as any) / aspectRatio.value

    /** @default 3 */
    const xRatio = toValue(previewWidth as any) / toValue(hoverWidth as any)
    /** @default 3 */
    const yRatio = previewHeight / toValue(hoverHeight as any)

    /** @default 600 */
    const xBgSize = toValue(containerWidth as any) * xRatio
    /** @default 600 */
    const yBgSize = toValue(containerHeight as any) * yRatio

    const xPosition = (mousePosition.value.mouseX - toValue(hoverWidth as any) / 2) * -xRatio
    const yPosition = (mousePosition.value.mouseY - toValue(hoverHeight as any) / 2) * -yRatio

    return {
      width: `${toValue(previewWidth)}px`,
      aspectRatio: aspectRatio.value,
      backgroundImage: `url(${imgSrc})`,
      overflow: 'hidden',
      backgroundRepeat: 'no-repeat',
      backgroundSize: `${xBgSize}px ${yBgSize}px`,
      backgroundPosition: `left ${xPosition}px top ${yPosition}px`,
    }
})

其他样式

当鼠标不出现在container容器上时,我期望不展示悬浮框

.container{
  margin: auto;

  img{
    width: 100%;
    height: 100%;
    // object-fit: contain;
  }

  .hover-box{
    display: none;
  }

  &:hover .hover-box{
    display: initial;
  }
}

存在的问题

  • 需要提前知道图片的大小,以便让图片盛满容器
  • 无法给图片设置object-fit: cover;属性值来设置图片的显示效果,必须是撑满容器的

其他