前言
在前端开发领域,页面截图功能与页面进入动画都是极为常见的需求。页面截图能够满足用户留存页面信息、分享特定内容等需求,在产品展示、报告生成等场景中广泛应用;而页面进入动画则能有效提升用户体验,赋予页面灵动的开场,增强用户对页面的第一印象。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 设置动画延迟开始时间。
业务需求以及遇到的问题
笔者遇到的需求也是比较常见的,swiper 和 animition 相结合,每次切换swiper 的slide 会触发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, ' '))
}
}
我这里使用的swiper是 swiper/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函数中补充暂停和恢复动画的操作,确保截图结果准确且不影响页面动画的正常展示。
希望可以帮助到遇到同样问题的大家。