html2canvas 和 animation.css相遇引发的问题

349 阅读5分钟

前言

在前端开发领域,页面截图功能与页面进入动画都是极为常见的需求。页面截图能够满足用户留存页面信息、分享特定内容等需求,在产品展示、报告生成等场景中广泛应用;而页面进入动画则能有效提升用户体验,赋予页面灵动的开场,增强用户对页面的第一印象。html2canvas 作为实现页面截图的有力工具,animation.css 作为提供丰富动画效果的框架,在各自领域发挥着重要作用。但当两者结合使用时,却容易产生一系列棘手的问题,下面我们就来深入探讨。

html2canvas 和 animation.css 的基本原理

html2canvas

html2canvas 基于浏览器的渲染机制,利用 JavaScript 的解析能力,深度遍历 HTML 文档的 DOM 树。在遍历过程中,它会收集每个 DOM 元素的样式属性,包括但不限于 CSS 盒模型属性(width、height、margin、padding 等)、字体样式(font-family、font-size 等)、颜色信息以及定位属性(position、left、top 等)。收集完这些信息后,html2canvas 会在内存中创建一个与原页面布局一致的 canvas 元素,并将这些样式和元素内容精确绘制到 canvas 上,从而实现将网页内容转化为 canvas 图像的功能。

animation.css

animation.css 构建于 CSS3 强大的动画规范之上,通过 @keyname 规则定义动画的关键帧。每个关键帧都包含了动画在特定时间点的样式状态,比如一个淡入动画,起始关键帧设置 opacity 为 0,结束关键帧设置 opacity 为 1。通过 animation 属性,开发者可以控制动画的诸多参数,如 animation-duration 定义动画持续时间,animation-delay 设置动画延迟开始时间。

业务需求以及遇到的问题

笔者遇到的需求也是比较常见的,swiperanimition 相结合,每次切换swiperslide 会触发animition.css 的动画效果。

到了最后一页的时候需要通过html2canvas 去保存当前页内容。

第一个需求需要我们给swiper 初始化的时候增加一点对动画类的处理实现

//swiper.animate.js
//隐藏元素
export const swiperAnimateCache=()=> {
    const allBoxes: any = window.document.documentElement.querySelectorAll(".animation")
    for (var i = 0; i < allBoxes.length; i++) {
        allBoxes[i].attributes["style"]
            ? allBoxes[i].setAttribute("swiper-animate-style-cache", allBoxes[i].attributes["style"].value)
            : allBoxes[i].setAttribute("swiper-animate-style-cache", " ")
        allBoxes[i].style.visibility = "hidden"
    }
}
// 开始动画
export const swiperAnimate = (a: any) => {
//每次添加的时候先把样式清除一遍
    clearSwiperAnimate()
    var b = a.slides[a.activeIndex].querySelectorAll(".animation")
    for (var i = 0; i < b.length; i++) {
        b[i].style.visibility = "visible"
        const effect = b[i].attributes["swiper-animate-effect"]
            ? b[i].attributes["swiper-animate-effect"].value
            : ""
        b[i].className = b[i].className + " " + effect + " " + "animated"
        const duration = b[i].attributes["swiper-animate-duration"]
            ? b[i].attributes["swiper-animate-duration"].value
            : ""
        // duration && style
        const delay = b[i].attributes["swiper-animate-delay"]
            ? b[i].attributes["swiper-animate-delay"].value
            : ""
        const style = b[i].attributes["style"].value + "animation-duration:" + duration + ";-webkit-animation-duration:" + duration + ";" + "animation-delay:" + delay + ";-webkit-animation-delay:" + delay + ";"
        // delay && (style = style )
        b[i].setAttribute("style", style)
    }
}

// 清楚样式。 获取 .animation 类名,  注意这个所有的.animation 类名都会被获取,所以可以自己取
export const clearSwiperAnimate = () => {
    const allBoxes: any = window.document.documentElement.querySelectorAll(".animation")
    for (var i = 0; i < allBoxes.length; i++) {
        allBoxes[i].attributes["swiper-animate-style-cache"] && allBoxes[i].setAttribute("style", allBoxes[i].attributes["swiper-animate-style-cache"].value)
        allBoxes[i].style.visibility = "hidden"
        allBoxes[i].className = allBoxes[i].className.replace("animated", " ")
        const effectValue = allBoxes[i].attributes["swiper-animate-effect"].value
        /* eslint-disable-next-line */
        allBoxes[i].attributes['swiper-animate-effect'] && (allBoxes[i].className = allBoxes[i].className.replace(effectValue, ' '))
    }
}

我这里使用的swiperswiper/vue


 <Swiper @swiper="onSwiper" @init="init" @slide-change-transition-end="slideChangeTransitionEnd"
      @slideChange="onSlideChange" direction="vertical" class="mySwiper">
        <swiper-slide>
        <div swiper-animate-effect="animate__fadeInLeft" swiper-animate-duration="0.6s" class="animation">
          </div>
        </swiper-slide>
 </Swiper>
 <script setup lang="ts">
 import { swiperAnimate, swiperAnimateCache } from '@/utils/swiper.animate';
    const init = (e: any) => {
      swiperAnimateCache()
      swiperAnimate(e)
    }
</script>

动画状态捕捉异常

当使用 html2canvas 对正在执行 animation.css 动画的页面元素进行截图时,很可能会截取到非预期的动画状态。由于 html2canvas 在执行截图操作时,是基于某个瞬间的 DOM 状态进行绘制,而动画却在运行过程中。在实际效果中,出现过渲染第一帧和后续帧的情况,导致绘制的内容是进入动画没执行完毕的内容。

动画状态捕捉异常

当使用 html2canvas 对正在执行 animation.css 动画的页面元素进行截图时,很容易出现动画状态捕捉异常的问题。由于 html2canvas 在执行截图操作时,是基于某个瞬间的 DOM 状态进行绘制,而动画在运行过程中处于动态变化。在实际效果中,曾出现过渲染第一帧和后续帧的情况,导致绘制的内容是进入动画未执行完毕的内容。这使得截图结果与预期不符,无法准确呈现用户期望的页面状态。

为了解决这一问题,我们可以参考以下代码逻辑:

const handleSave = () => {
  console.log('进入 handleSave 函数');
  if (shareBox.value) {
    console.log('shareBox.value 是有效的 DOM 元素');
    const animatedElements = shareBox.value.querySelectorAll('.animation');
    requestAnimationFrame(() => {
      html2canvas(shareBox.value, {
        width: shareBox.value.offsetWidth,
        scale: 2,
        useCORS: true,
      })
       .then((canvas) => {
          console.log('html2canvas 执行成功,获取到 canvas');
          const url = canvas.toDataURL("image/png");
          if (url) {
            console.log('成功获取图片 dataURL,准备展示预览');
            imagePreviewRef.value.showPreview([url]);
            isHidden.value = false;
            nextTick(() => showToast('已为您生成图片,请长按保存!'));
          } else {
            nextTick(() => showToast('保存失败,请稍后重试!'));
          }
        })
       .catch((error) => {
          console.error('html2canvas 执行出错:', error);
          nextTick(() => showToast('保存失败,请稍后重试!'));
          // 恢复动画
          animatedElements.forEach((element) => {
            element.style.animation = ''; // 恢复原始动画
          });
        });
    });
  } else {
    console.log('shareBox.value 是无效的 DOM 元素');
  }
};

这段代码定义了handleSave函数用于截图操作。首先判断shareBox.value是否为有效 DOM 元素,若有效则获取其中所有带有.animation类的元素。

通过requestAnimationFrame确保在合适的时机调用html2canvas进行截图,设置了截图的宽度、缩放比例以及跨域相关配置。成功获取截图canvas后,将其转换为dataURL格式用于展示预览,失败则提示用户。

在错误处理部分,遍历animatedElements,将元素的animation属性还原为空字符串,恢复动画,保证页面交互正常。然而,这段代码存在不足,它没有在截图前暂停动画并固定样式,这可能导致截图时捕捉到非预期的动画状态。可在requestAnimationFrame之前添加如下代码来完善:

// 暂停动画并固定样式
animatedElements.forEach((element) => {
  const computedStyle = window.getComputedStyle(element);
  // 保存动态样式
  element.style.animation = 'none';
  element.style.transform = computedStyle.transform;
  element.style.opacity = computedStyle.opacity;
  element.style.position = computedStyle.position;
});

这样,在截图前先暂停动画并固定样式,就能有效避免动画状态捕捉异常的问题,待截图完成后再恢复动画,保障页面的正常交互。

在handleSave函数中补充暂停和恢复动画的操作,确保截图结果准确且不影响页面动画的正常展示。

希望可以帮助到遇到同样问题的大家。