整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

原生 JS 手写一个优雅的图片预览功能,带你吃透背后

原生 JS 手写一个优雅的图片预览功能,带你吃透背后原理

本文将用一个极简的例子详细讲解如何用原生JS一步步实现完整的图片预览和查看功能,无任何第三方依赖,兼容PC与H5,实现了触屏双指缩放等,干货满满。

为提升阅读体验,正文中代码展示均有部分省略处理,完整代码可以在文末查看。

实现原理

实现图片预览/查看的关键在于 CSS3 中的 transform 变换,该属性应用于元素在2D或3D上的旋转,缩放,移动,倾斜等等变换,通过设置 translate(x,y) 即可偏移元素位置,设置scale即可缩放元素,当然你也可以只设置 matrix 来完成上述所有操作,这涉及到矩阵变换的知识,本文使用的均是CSS提供的语法糖进行变换操作。

PC上的点击、移动,H5的手势操作,都离不开DOM事件监听。例如鼠标移动事件对应 mousemove,移动端因为没有鼠标则对应 touchmove,而本文将介绍如何仅通过指针事件来进行多端统一的事件监听。在监听事件中我们可以通过 event 对象获取各种属性,例如常用的 offsetXoffsetY 相对偏移量,clientXclientY 距离窗口的横坐标和纵坐标等。

除此之外可能还需要具备一点数学基础,如果像我这样数学知识几乎都还给了高中老师的话可以复习下向量的加减计算。

打开蒙层

在开始前我们先准备一个图片列表,并绑定好点击事件,当点击图片时,通过 document.createElement 创建元素,然后把图片节点克隆进蒙层中,这对你来说并不难,简单实现如下。

<div id="list">
    <img class="item" src="...." />
    ............
</div>
/* 图片预览样式 */
.modal {
  touch-action: none;
  user-select: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0, 0, 0, 0.75);
}
.modal > img {
  position: absolute;
  padding: 0;
  margin: 0;
  transform: translateZ(0);
}
let cloneEl=null
let originalEl=null
document.getElementById('list').addEventListener('click', function (e) {
  e.preventDefault()
  if (e.target.classList.contains('item')) {
    originalEl=e.target // 缓存原始图DOM节点
    cloneEl=originalEl.cloneNode(true) // 克隆图片
    originalEl.style.opacity=0
    openPreview() // 打开预览
  }
})

function openPreview() {
  // 创建蒙层
  const mask=document.createElement('div')
  mask.classList.add('modal')
  // 添加在body下
  document.body.appendChild(mask)
  // 注册蒙层的点击事件,关闭弹窗
  const clickFunc=function () {
    document.body.removeChild(this)
    originalEl.style.opacity=1
    mask.removeEventListener('click', clickFunc)
  }
  mask.addEventListener("click", clickFunc)
  mask.appendChild(cloneEl) // 添加图片
}

// 用于修改样式的工具类,并且可以减少回流重绘,后面代码中会频繁用到
function changeStyle(el, arr) {
  const original=el.style.cssText.split(';')
  original.pop()
  el.style.cssText=original.concat(arr).join(';') + ';'
}

这时候我们成功添加一个打开预览的蒙层效果了,但克隆出来的图片位置是没有指定的,此时需要用 getBoundingClientRect() 方法获取一下元素相对于可视窗口的距离,设置为图片的起始位置,覆盖在原图片的位置之上,以取代文档流中的图片。

// ......
// 添加图片
const { top, left }=originalEl.getBoundingClientRect()
changeStyle(cloneEl, [`left: ${left}px`, `top: ${top}px`])
mask.appendChild(cloneEl)

效果如下,看起来像点击高亮图片的感觉:

接下来我们需要实现焦点放大的效果,简单来说就是计算两点之间的位移距离作为 translate 偏移量,将图片偏移到屏幕中心点位置,然后缩放一定的比例来达到查看效果,通过 transition 实现过渡动画。

中心点位置我们可以通过 window 下的 innerWidthinnerHeight 来获取浏览器可视区域宽高,然后除以2即可得到中心点坐标。

const { innerWidth: winWidth, innerHeight: winHeight }=window

// 计算自适应屏幕的缩放值
function adaptScale() {
  const { offsetWidth: w, offsetHeight: h }=originalEl // 获取文档中图片的宽高
  let scale=0
  scale=winWidth / w
  if (h * scale > winHeight - 80) {
    scale=(winHeight - 80) / h
  }
  return scale
}

// 移动图片到屏幕中心位置
  const originalCenterPoint={ x: offsetWidth / 2 + left, y: offsetHeight / 2 + top }
  const winCenterPoint={ x: winWidth / 2, y: winHeight / 2 }
  const offsetDistance={ left: winCenterPoint.x - originalCenterPoint.x + left, top: winCenterPoint.y - originalCenterPoint.y + top }
  const diffs={ left: ((adaptScale() - 1) * offsetWidth) / 2, top: ((adaptScale() - 1) * offsetHeight) / 2 }
  changeStyle(cloneEl, ['transition: all 0.3s', `width: ${offsetWidth * adaptScale() + 'px'}`, `transform: translate(${offsetDistance.left - left - diffs.left}px, ${offsetDistance.top - top - diffs.top}px)`])
  // 动画结束后消除定位重置的偏差
  setTimeout(()=> {
    changeStyle(cloneEl, ['transition: all 0s', `left: 0`, `top: 0`, `transform: translate(${offsetDistance.left - diffs.left}px, ${offsetDistance.top - diffs.top}px)`])
    offset={ left: offsetDistance.left - diffs.left, top: offsetDistance.top - diffs.top } // 记录值
  }, 300)

这里先利用绝对定位 left top 来设置克隆元素的初始位置,再通过 translate 偏移位置,是为了更自然地实现动画效果,动画结束后再将绝对定位的数值归零并把偏移量加进 translate 中,并且这里我并没有直接使用 scale 放大元素,而是将比例转化为宽高的变化。最终效果如下:

图片缩放(PC)

在PC实现图片缩放相对是比较简单的,我们利用滚轮事件监听并改变 scale 值即可。重点是利用 deltaY 值的正负来判断滚轮是朝上还是朝下:

let origin='center'
let scale=1
// 注册事件
mask.addEventListener('mousewheel', zoom, { passive: false })

// 滚轮缩放
const zoom=(event)=> {
  if (!event.deltaY) {
    return
  }
  event.preventDefault()
  origin=`${event.offsetX}px ${event.offsetY}px`
  
  if (event.deltaY < 0) {
    scale +=0.1 // 放大
  } else if (event.deltaY > 0) {
    scale >=0.2 && (scale -=0.1) // 缩小
  }
  changeStyle(cloneEl, ['transition: all .15s', `transform-origin: ${origin}`, `transform: translate(${offset.left + 'px'}, ${offset.top + 'px'}) scale(${scale})`])
}

因为缩放始终都以图片中心为原点进行缩放,这显然不符合我们的操作习惯,所以在上面的代码中,我们通过鼠标当前的偏移量offsetX、offsetY 的值改变 transform-origin 来动态设置缩放的原点,效果如下:

乍一看好像没什么问题,事实上如果鼠标不断移动且幅度很大时会出现抖动,需要消除原点位置突然改变带来的影响才能完全解决这个问题(起初我并未发现,后面在做移动端缩放时简直是灾难级体验)而由于PC上问题并不明显,这里先按下不表,后面会详细提到。

移动查看

由于缩放导致图像发生变化,我们自然地想到要靠移动来观察图片,此时体现在PC端的是按住鼠标拖拽,移动端则是手指点击滑动,而两者各自的事件监听显然并不共通,如以移动事件为例,PC端对应的是 mousemove 事件而移动端则是 touchmove 事件,这就意味着如果我们要做到兼容多端,就必须注册多个事件监听。

那么有没有一种事件可以做到同时监听鼠标操作手指操作呢?答案是有的!那就是 指针事件(Pointer events),它被设计出来就是为了便于提供更加一致与良好的体验,无需关心不同用户和场景在输入硬件上的差异。接下来我们就以此事件为基础来完成各项操作功能。

指针 是输入设备的硬件层抽象(比如鼠标,触摸笔,或触摸屏上的一个触摸点),它能指向一个具体表面(如屏幕)上的一个(或一组)坐标,可以表示包括接触点的位置,引发事件的设备类型,接触表面受到的压力等。

PointerEvent 接口继承了所有 MouseEvent 中的属性,以保障原有为鼠标事件所开发的内容能更加有效的迁移到指针事件。

移动图片的实现是比较简单的,在每次指针按下时我们记录 clientXclientY 为初始值,移动时计算当前的值与初始点位的差值加到 translate 偏移量中即可。需要注意的是每次移动事件结束时都必须更新初始点位,否则膨胀的偏移距离会使图片加速逃逸可视区域。另外当抬起动作结束时,会触发 click 事件,所以注意加入全局变量标记以及定时器进行一些判断处理。

let startPoint={ x: 0, y: 0 } // 记录初始触摸点位
let isTouching=false // 标记是否正在移动
let isMove=false // 正在移动中,与点击做区别

// 鼠标/手指按下
window.addEventListener('pointerdown', function (e) {
  e.preventDefault()
  isTouching=true
  startPoint={ x: e.clientX, y: e.clientY }
})
// 鼠标/手指抬起
window.addEventListener('pointerup', function (e) {
  isTouching=false
  setTimeout(()=> {
    isMove=false
  }, 300);
})
// 鼠标/手指移动
window.addEventListener('pointermove', (e)=> {
  if (isTouching) {
    isMove=true
    // 单指滑动/鼠标移动
    offset={
      left: offset.left + (e.clientX - startPoint.x),
      top: offset.top + (e.clientY - startPoint.y),
    }
    changeStyle(cloneEl, [`transform: translate(${offset.left + 'px'}, ${offset.top + 'px'}) scale(${scale})`, `transform-origin: ${origin}`])
    // 注意移动完也要更新初始点位,否则图片会加速逃逸可视区域
    startPoint={ x: e.clientX, y: e.clientY }
  }
})

双指缩放(移动端)

TouchEvent 的事件对象中,我们可以找到 touches 这个数组,在移动端通常都是利用这个数组来判断触点个数的,例如 touches.length > 1 即是多点操作,这是我们实现双指缩放的基础。但在 指针事件 中却找不到类似的对象(MDN对其描述只是扩展了 MouseEvent 的接口),想来 Touch 对象只服务于 TouchEvent 这点其实也可以理解,所以要自己实现对触摸点数的记录。

这里我们使用 Map 数组对触摸点进行记录(通过这个实例你可以看到 Map 数组纯 api 操作增删改有多么优雅)。其中我们利用 pointerId 标识触摸点,移动事件中根据事件对象的 pointerId 来更新对应触点(指针)的数据,当触点抬起时则从Map中删除点位:

let touches=new Map() // 触摸点数组

window.addEventListener('pointerdown', function (e) {
  e.preventDefault()
  touches.set(e.pointerId, e) // TODO: 点击存入触摸点
  isTouching=true
  startPoint={ x: e.clientX, y: e.clientY }
  if (touches.size===2) { 
        // TODO: 判断双指触摸,并立即记录初始数据
  }
})

window.addEventListener('pointerup', function (e) {
  touches.delete(e.pointerId) // TODO: 抬起时移除触摸点
  // .....
})

window.addEventListener('pointermove', (e)=> {
  if (isTouching) {
    isMove=true
    if (touches.size < 2) {
      // TODO: 单指滑动,或鼠标拖拽
    } else {
      // TODO: 双指缩放
      touches.set(e.pointerId, e) // 更新点位数据
      // .......
    }
  }
})

Map 是二维数组,可以利用 Array.from 转成普通数组即可通过 index 下标取值。

简单在手机浏览器上测试后发现,这个数组偶尔会不停增加(例如在滑动页面时),也就是 pointerup 会出现不能正确删除对应点位的情况,或者说被意外中断了,此时会触发 pointercancel 事件监听(对应 touchcancel 事件),我们必须在这里清空数组,这是容易被忽略的一点,原本 TouchEvent 中的 touches 并不需要处理。

window.addEventListener('pointercancel', function (e) {
  touches.clear() // 可能存在特定事件导致中断,所以需要清空
})

现在我们有了对触摸点判断的基础,就可以开始实现缩放了,当双指接触屏幕时,记录两点间距离作为初始值,当双指在屏幕上捏合,两点间距不停发生变化,此时存在一个变化比例=当前距离 / 初始距离,该比例作为改变 scale 的系数就能得到新的缩放值。

在上一篇文章手写拖拽效果中我也讲到了如何在JS中使用数学方法计算两点间距离,下面介绍另一种常见的简洁写法,Math.hypot() 函数返回其参数的平方和的平方根:

// 坐标点1: start,坐标点2: end,求两点距离:
Math.hypot(end.x - start.x, end.y - start.y)
// 所以为什么上面代码可以计算两点距离,因为等价于:
Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2))

回到代码中,直接取出 touches 的前两个点位,于两点间获取距离:

// 获取距离
function getDistance() {
  const touchArr=Array.from(touches)
  if (touchArr.length < 2) {
    return 0
  }
  const start=touchArr[0][1]
  const end=touchArr[1][1]
  return Math.hypot(end.x - start.x, end.y - start.y)
}

继续完善上面的事件监听代码:

let touches=new Map() // 触摸点数组
let lastDistance=0 // 记录最后的双指初始距离
let lastScale=1 // 记录下最后的缩放值

window.addEventListener('pointerdown', function (e) {
  // .........
  if (touches.size===2) { // TODO: 判断双指触摸,并立即记录初始数据
    lastDistance=getDistance()
    lastScale=scale // 把当前的缩放值存起来
  }
})

window.addEventListener('pointerup', function (e) {
// .........
  if (touches.size <=0) {
    // .........
  } else {
    const touchArr=Array.from(touches)
    // 双指如果抬起了一个,可能还有单指停留在触屏上继续滑动,所以更新点位
    startPoint={ x: touchArr[0][1].clientX, y: touchArr[0][1].clientY }
  }
  // .......
})

window.addEventListener('pointermove', (e)=> {
  e.preventDefault()
  if (isTouching) {
    isMove=true
    if (touches.size < 2) { // 单指滑动
      // .......
    } else {
      // 双指缩放
      touches.set(e.pointerId, e)
      const ratio=getDistance() / lastDistance // 比较距离得出比例
      scale=ratio * lastScale // 修改新的缩放值
      changeStyle(cloneEl, ['transition: all 0s', `transform: translate(${offset.left + 'px'}, ${offset.top + 'px'}) scale(${scale})`, `transform-origin: ${origin}`])
    }
  }
})

以上仅是实现了缩放的处理,而缩放原点还在默认的图片中心,就和PC端一样我们还要改变原点才显得自然,对于双指缩放来说,改变的只是两点间距离,无论双指间距如何改变,两点连成的线段中心点是不会变的,所以我们只要通过两点求出中心点坐标然后设置为缩放原点坐标即可:

window.addEventListener('pointermove', (e)=> {
  // .......
      // 双指缩放
      const ratio=getDistance() / lastDistance // 比较距离得出比例
      scale=ratio * lastScale // 修改新的缩放值    
      const touchArr=Array.from(touches)
      const start=touchArr[0][1]
      const end=touchArr[1][1]
      x=(start.offsetX + end.offsetX) / 2
      y=(start.offsetY + end.offsetY) / 2
      origin=`${x}px ${y}px`
      changeStyle(cloneEl, ['transition: all 0s', `transform: translate(${offset.left + 'px'}, ${offset.top + 'px'}) scale(${scale})`, `transform-origin: ${origin}`])
  // ........
})

这时缩放感觉是没有问题了,但是每当往屏幕中的不同位置再多进行几次操作时,图片会突然间闪动一下位置,到最后几乎不受控制。

这就回到前面提到的,原点位置突然改变带来的偏移量引起了图片位置的闪动,这段偏移是如何产生的呢?我们画两张图看下,在原点变化的前后图像的坐标点发生了哪些变化:

如上图,原点为 O 时,我们取右下角的点设为点 A,图像放大2倍时 A 点变换到 B 点。

而当原点突然变为 O’ 时,点 A 在图像放大2倍时则变换到了 B' 点。

我们可以把图像的偏移抽象为图像某个点位的偏移,这样问题就变成了计算 BB' 的距离:

设原点 **O=(Ox , Oy)**,点 A=(x, y),缩放值为 s,OA 向量乘缩放倍数得出 OB 的向量:

B 坐标就等于 OB 向量加上原点 O 的坐标:

同理得出点 B' 的坐标:

BB' 的距离就是两点相减后的结果,两点已在上面得出,代入计算过程这里就不多写了,最终化简的结果如下:

在进行缩放时我们主动改变 scale 的值,那么 s 是已知的,双指落下时是我们主动改变了缩放原点,(Ox , Oy)(O'x , O'y) 这两个点也是已知的,那么根据上面的式子就可以得出 BB' 的实际距离了,也就是图像的偏移量。这么说有点抽象,我们还是回到代码中,在双指缩放时将这个偏移量减掉,同样的在PC端的缩放中,我们也加入对偏移量的修正:

let scaleOrigin={ x: 0, y: 0, }
// 获取中心改变的偏差
function getOffsetCorrection(x=0, y=0) {
  const touchArr=Array.from(touches)
  if (touchArr.length===2) {
    const start=touchArr[0][1]
    const end=touchArr[1][1]
    x=(start.offsetX + end.offsetX) / 2
    y=(start.offsetY + end.offsetY) / 2
  }
  origin=`${x}px ${y}px`
  const offsetLeft=(scale - 1) * (x - scaleOrigin.x) + offset.left
  const offsetTop=(scale - 1) * (y - scaleOrigin.y) + offset.top
  scaleOrigin={ x, y }
  return { left: offsetLeft, top: offsetTop }
}

window.addEventListener('pointermove', (e)=> {
  // .......
      // 双指缩放
      touches.set(e.pointerId, e)
      const ratio=getDistance() / lastDistance
      scale=ratio * lastScale
      offset=getOffsetCorrection()
      changeStyle(cloneEl, ['transition: all 0s', `transform: translate(${offset.left + 'px'}, ${offset.top + 'px'}) scale(${scale})`, `transform-origin: ${origin}`])
  // ........
})

// 滚轮缩放
const zoom=(event)=> {
  // ........
  offset=getOffsetCorrection(event.offsetX, event.offsetY)
  changeStyle(cloneEl, ['transition: all .15s', `transform-origin: ${origin}`, `transform: translate(${offset.left + 'px'}, ${offset.top + 'px'}) scale(${scale})`])
}

最终双指缩放效果如下,啊~ 如此丝滑,不由得流下两行热泪

一些细节

有些在正文中未提及,但同样重要的小细节。

是什么阻止了滚动?

虽然浏览器滚动对应的其实是 scroll 事件,但我们在PC上滚动通常都是用利用滚轮(笔记本触控板也被视作滚轮),所以在滚轮事件阻止系统默认事件也就阻止了滚动,但不是完全阻止,因为滚动条没隐藏的话还是可以拖动来滚动页面的,在本文例子中并没有针对滚动做什么处理,如果需要完全禁止滚动,应该在打开弹窗时为 body 设置 overflow'hidden'

注意滚轮事件(wheel)是可以触发冒泡捕获的,而滚动事件(scroll)却无法触发冒泡,了解更多可以看我在掘金的一篇文章:面试官:说说哪些浏览器事件不会冒泡 - 掘金。

至于移动端又是为什么阻止了滚动呢?得益于一个强大的CSS属性,可能在开头布局部分你就发现了这个属性,没错,这里为弹层遮罩设置了 touch-action: none; 从而阻止了所有手势效果,自然也就不会发生页面滚动。该属性在平时的业务代码中也可用于优化移动端性能、解决 touchmovepassive 报错等,这个我在之前另一篇文章中有提到,感兴趣可以看看:学会一行CSS即可提升页面滚动性能 - 掘金。

translateZ(0) 有什么用?

在本例的代码中这个CSS本身是没有意义的,为的只是触发css3硬件加速来提升性能,那为什么不直接使用 translate3d() 呢?又或者使用 will-change: transform; 来告诉浏览器提升渲染性能呢?



答案是后两者都会使移动端的图片变模糊!(Android似乎不会)起初我发现图片在手机上模糊的问题时,调试很久都没定位到源头,一筹莫展之际想起以前做H5网页常使用 vant 框架,就想要不看看它源码中的图片预览组件吧,很快我找到相关代码位置,代码截图:

从代码片段中我看到vant并没有使用任何translate3dscale3d属性,看来是真的有坑了。其实我们使用translate3d提升性能也是把第三个参数一直设置为0(2d平面没有Z轴)来实现的,这和translateZ(0)是等价的。 但奇怪的是单独设置translateZ却没有引发问题。。

will-change 这个属性,我也是最近无意中发现的,根据 MDN 文档的描述,该属性是用于提升性能的最后手段,怎么理解这句话呢?根据上面实践的结论来看,应该可以认为是浏览器尝试牺牲掉一些画面质量来换取性能提升的一种手段。

完整代码

相关链接:JS图片预览/查看效果(兼容PC与H5) - 码上掘金

结束

以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注,我会更新更多实用的前端知识与技巧。我是茶无味的一天,期待与你共同成长~

一、项目背景】

生活中经常会见到很多gif图,那么gif图到底是什么?GIF是一种位图。简单来说就是通过每一张张静图,通过控制它的关键帧,从而达到静态图动起来的效果。

这种GIF图的效果,也可以用html+CSS3结合来做。

【二、项目目标】

完成GIF图的制作。

【三、项目分析】

1、分析图片。打开其中一张图。

2、可以看到这张图有45张不一样动作的静态图合成。有点击属性。如图所示:

看到这张照片是7020*156,一共有45帧。高度不变,宽度7020/45帧,就可以把每一帧的内容显示出来。

【四、项目准备】

1、图片:准备自己的喜欢的GIF静态长图,保存在文件夹。

2、软件:Dreamweaver。

【五、项目实现】

1、创建div 存放图片和文件,添加class属性。

<body>
  <div class="box">
  <div class="box2">
  </div>  
  </div>
</body>

2、添加CSS样式

1) 设置box的宽、高、位置、背景颜色。

.box{
      width: 300px;
      height: 300px;
      background: #ccc;
      position: absolute;
      left: 0px;
      top: 0;
    }

2)加载图片,设置宽、高,-webkit-animation动画效果。

.box2{
        width: 156px;
        height: 156px;
        background: url("fox45.png");
         -webkit-animation:aa 3s steps(45) infinite ;
      }
   @-webkit-keyframes aa{
  
  
      100%{
     background-position: -7020px 0;
      }
     }

CSS3 animation属性中的steps实现GIF动图(逐帧动画)

steps(45)表示让整个动画在45个关键帧之间切换。这个松鼠的图片中

包含了45帧,所以这里设置了45。而且我们的动画时长是3s,也就是说每一帧

停留1s,这就和普通的GIF动图达到了一样的效果。

【六、效果展示】

1、点击F12运行到浏览器。

2、点击图片,效果如下。

【七、总结】

1、本项目,就gif图遇到的一些难点进行了分析及提供解决方案。

2、html+css也可以做出网站页面的效果,在上面显示图片标题的地方不能用绝对定位,于是用的relative定位,这个地方是布局的核心部分。

3、按照操作步骤,自己尝试去做。自己实现的时候,总会有各种各样的问题,切勿眼高手低,勤动手,才可以理解的更加深刻。

4、需要本文源码的小伙伴,后台回复“GIF图”四个字,即可获取。

****看完本文有收获?请转发分享给更多的人****

IT共享之家

入群请在微信后台回复【入群】


想学习更多Python网络爬虫与数据挖掘知识,可前往专业网站:http://pdcfighting.com/

绍一个比较常见的动画效果。

在日常开发中,为了强调凸显某些文本或者元素,会加一些扫光动效,起到吸引眼球的效果,比如文本的

或者是一个卡片容器,里面可能是图片或者文本或者任意元素

除此之外,还有那种不规则的图片,比如一张奖品图片,只会在图片本身出现扫光,透明度地方则不会

这些是如何实现的呢?一起看看吧

一、CSS 扫光的原理

CSS扫光动画的原理很简单,就是一个普通的、从左到右的、无限循环的位移动画

位移动画可以选择transform或者改变background-position都行。

至于扫光,我们只需要绘制一条斜向上45deg的线性渐变就可以了,示意如下

用CSS实现就是

background: linear-gradient(45deg, rgba(255,255,255,0) 40%, rgba(255, 255, 255, 0.7), rgba(255,255,255,0) 60%);

准备工作做好了,下面看 3 种不同场景的实现

二、文本扫光

首先来看文本扫光。

由于扫光在文本内部,所以需要将这个渐变作为文本的颜色。文本渐变色,可以用backgrond-clip:text来实现,假设HTML是这样的

<h1 class="shark-txt">前端侦探</h1>

为了让效果看起来更加明显,我们用一个比较粗的字体

h1{
  font-size: 60px;
  font-family: "RZGFDHDHJ";
  font-weight: normal;
  color: #9747FF;
}

效果如下

现在我们通过background-clip来添加扫光,由于是裁剪背景,所以需要将当前文本颜色设置透明,建议通过-webkit-text-fill-color: transparent来设置,这样可以保留文本原有颜色,好处是其他地方,比如background-color可以直接使用原有文本颜色currentColor,具体实现如下

.shark-txt{
  -webkit-text-fill-color: transparent;
  background: linear-gradient(45deg, rgba(255,255,255,0) 40%, rgba(255, 255, 255, 0.7), rgba(255,255,255,0) 60%) -100%/50% no-repeat currentColor;
  -webkit-background-clip: text;
}

效果如下

最后就是让这个扫光动起来了。

由于是在文本内部,所以这里可以通过改变background-position来实现扫光动画了,动画很简单,如下

@keyframes shark-txt {
  form{
    background-position: -100%;
  }
  to {
    background-position: 200%;
  }
}

但是这样做没有动画效果,完全不会动。

这是因为背景默认尺寸是100%,根据背景偏移百分比的计算规则,当背景尺寸等于容器尺寸时,百分比完全失效,具体规则如下

给定背景图像位置的百分比偏移量是相对于容器的。值 0% 表示背景图像的左(或上)边界与容器的相应左(或上)边界对齐,或者说图像的 0% 标记将位于容器的 0% 标记上。值为 100% 表示背景图像的 (或 )边界与容器的 (或 )边界对齐,或者说图像的 100% 标记将位于容器的 100% 标记上。因此 50% 的值表示水平或垂直居中背景图像,因为图像的 50% 将位于容器的 50% 标记处。类似的,background-position: 25% 75% 表示图像上的左侧 25% 和顶部 75% 的位置将放置在距容器左侧 25% 和距容器顶部 75% 的容器位置。

developer.mozilla.org/zh-CN/docs/…

(container width - image width) * (position x%)=(x offset value)
(container height - image height) * (position y%)=(y offset value)

所以这种情况下,我们可以手动改小一点背景尺寸,比如50%

.shark-txt {
    -webkit-text-fill-color: transparent;
    background: linear-gradient(45deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0) 60%) -100% / 50% no-repeat currentColor;
    -webkit-background-clip: text;
    animation: shark-txt 2s infinite;
}

这样就能完美实现文本扫光效果了

三、卡片容器扫光

还有一种比较常见的是容器内的扫光动效,通常是在一个圆角矩形的容器里。

像这种情况下就不能直接用背景渐变了,因为会被容器内的其他元素覆盖。所以我们需要创建一个伪元素,然后通过改变伪元素的位移来实现扫光动画了。

假设有一个容器,容器内有一张图片,HTML如下

<div class="shark-wrap card">
    <img src="https://imgservices-1252317822.image.myqcloud.com/coco/b11272023/ececa9a5.7y0amw.jpg">
</div>

简单修饰一下

.card{
  width: 300px;
  border-radius: 8px;
  background-color: #FFE8A3;
}
.card img{
  display: block;
  width: 100%;
}

效果如下

下面通过伪元素来创建一个扫光层,设置位移动画

.shark-wrap::after{
  content: '';
  position: absolute;
  inset: -20%;
  background: linear-gradient(45deg, rgba(255,255,255,0) 40%, rgba(255, 255, 255, 0.7), rgba(255,255,255,0) 60%);
  animation: shark-wrap 2s infinite;
  transform: translateX(-100%);
}
@keyframes shark-wrap {
  to {
    transform: translateX(100%);
  }
}

效果如下

最后直接超出隐藏就行了

.shark-wrap{
  overflow: hidden;
}

最终效果如下

也适合那种圆形头像

四、不规则图片扫光

其实前面两种情况已经适合大部分场景了,其实还有一种情况,就是那种不规则的图片扫光。这种图片无法直接通过overflow:hidden去隐藏多余部分,比如这样

很明显在图片之外的地方也出现了扫光,无法做到扫光在图形的"内部"。

那么,有没有办法根据图片的外形去裁剪呢?当然也是有办法的,这里需要用到CSS mask遮罩。

简单来说,就是直接将该图片作为遮罩图片,这样只有形状内的部分可见,形状外的直接被裁剪了

在上一种场景的情况下,只需要在此基础之上,添加一个完全相同的 mask遮罩就行了

<div class="shark-wrap" style="-webkit-mask: url(https://imgservices-1252317822.image.myqcloud.com/coco/s09252023/3af9e8de.00uqxe.png) 0 0/100%">
  <img class="logo" src="https://imgservices-1252317822.image.myqcloud.com/coco/s09252023/3af9e8de.00uqxe.png">
</div>

这样就可以把扫光多余的部分裁剪掉了

换张图也能很好适配

以上所有 demo 可以查看以下链接

  • CSS shark animation (codepen.io)


五、总结一下

以上就本文的全部内容了,共介绍了3种不同的扫光场景,你学到了吗?下面总结一下重点

  1. 扫光样式本身可以直接用线性渐变绘制而成
  2. 扫光动画原理很简单,就是一个水平的位移动画
  3. 文本扫光动画需要通过改变background-postion实现
  4. 当背景尺寸等于容器尺寸时,设置background-postion百分比无效
  5. 普通容器的扫光效果需要借助伪元素实现,因为如果使用背景会被容器内的元素覆盖
  6. 普通容器的扫光动画可以直接用transfrom实现
  7. 使用overflow:hidden裁剪容器外的部分
  8. 不规则图片的扫光效果无法直接根据形状裁剪
  9. 借助CSS mask可以根据图片本身裁剪掉扫光多余部分