活动动画相关技术方案

罗列了下一些活动中用到动画的地方,以及踩的一些坑

三周年页面

背景动画 gif

这块一开始的思路是:用一个尽可能小的 gif 来保障用户加载的速度。

先尝试了下 12 帧的 gif (压缩完大小为 6mb),卡顿感很明显,体验不佳:

而且,当未完全加载完 gif 时,在加载的过程中还是会有过分明显的卡顿:

于是调整了下思路:

用静态图预先展示,向下保障用户最低体验;然后等高清 gif 完全加载完才展示,向上带给用户最佳体验。

代码实现上,只要用两层叠加的图片,控制上层 gif 的 visibility 属性即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const [isGifShow, setGifShow] = useState(false); // 是否展示 gif
const gifRef = useRef(); // gif 对象

useEffect(() => {
// 如果渲染页面时已经加载完,则直接展示
if (gifRef.current?.complete) {
setGifShow(true);
} else {
// 否则等待完全加载完才展示
gifRef.current.onload = () => setGifShow(true);
}
}, []);

<Image
className="f-pa bg"
src={IMG_BG}
client={{ paramWidth: 375, extraParam: { interlace: 1 } }} />
<Image
className={classNames('f-pa bg gif', {
'z-show': isGifShow, // 通过这个类来控制 visibility
})}
ref={gifRef}
src={IMG_BG_GIF}
client={{ paramWidth: 375, extraParam: { interlace: 1 } }} />

又和视觉尝试了几次,发现不用 24 帧 (压缩完是 14mb),20 帧 (压缩完是 10mb) 即可在视觉上体验完美:

进场视频 video

先抛出一个问题:

部分移动端浏览器,比如 iOS 微信,为了减少用户流量的浪费,只允许让用户交互完之后才可以播放 video,无法自动播放。

整体思路

正因为之前站外场景 video 比较难兼容,故头部视频都采用,站外直接展示静态图的方案,或者就是转化成 canvas 的方案,但体验不佳。

而这一次是全屏视频,好看得很,故尽管时间有限,但还是在临上线前一天,又和视觉商量了下加了个引导点击交互图,整体流程如下:

(其中,因为时间原因,这次加载中进度条没实现,而是同样展示了点击引导图做替换

Video 相关基础知识

video 属性

首先我们的场景是自动播放视频的场景,不需要控件和声音,所以默认的配置如下:

属性 描述
id 自定义 识别视频的唯一标识。这边通过 Math.floor(Math.random()\*100000) 创建。
controls / 如果出现该属性,则向用户显示控件,比如播放按钮。
height / 设置视频播放器的高度。
width / 设置视频播放器的宽度。
src URL 要播放的视频的 URL。
poster ‘x’ 规定视频下载时显示的图像,或者在用户点击播放按钮前显示的图像。目前场景下只要黑屏即可,即给 video 设置 background-color: #000;。但如果不设置该属性,在某华为机型下,会有一个播放按钮一闪而逝,所以得随便设置个字符串。
autoplay true 如果出现该属性,则视频在就绪后马上播放。
preload / 如果出现该属性,则视频在页面加载时进行加载,并预备播放。如果使用 “autoplay”,则忽略该属性。
loop false 如果出现该属性,则当媒介文件完成播放后再次开始播放。
muted true 规定视频的音频输出应该被静音。在直播间打开的页面,不设置该属性的话,可能会使得主播的声音消失。
playsinline true 包括 webkit-playsinlinex5-playsinline。设置 iOS 微信浏览器支持行内播放,否则会全屏置顶播放且有控件。目前场景下,虽然同样是全屏播放,但如果不设置会盖住跳过按钮,影响功能

顺带一提,video 样式通过 object-fit 来控制视频大小的展示。目前场景因为需要视频全屏播放,在不同设备不同百分比下,用 cover 来保持视频的宽高比,并居中展示,体验最好。

video 方法

首先视频没有一个开始播放的回调,只能用 timeupdate 变相实现。

方法 描述
play 开始播放。
pause 暂停播放。
timeupdate 视频播放过程中会不停触发,每次 currentTime 都会更新,通过比较时间可以变相地实现开始播放 onStart 或即将结束播放 onNearlyEnd 等事件。
ended 结束播放后触发。
canplay 是否可以开始播放。 在 iOS 可能有兼容问题无法触发
canplaythrough 是否可以开始播放到结束。在 iOS 可能有兼容问题无法触发
complete 当离线音频加载完成时触发。

渐入渐出

这次开屏动画是刚启动就播放,所以不必加上渐入,如果是点击触发的全屏动画建议增加渐入,使得过渡更自然。

渐出的思路就是在 timeupdate 事件中计算时间,即将结束时给视频增加一个渐隐类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const [isSlowlyHidden, setSlowlyHidden] = useState(false); // 开始渐隐

const onTimeUpdate = useCallback(() => {
const value = videoRef.current?.currentTime;
if (value > 0) {
onStart && onStart(); // 因为 video 没有直白的开始事件,这里可以执行一些开始后的回调
}
if (value > 17) {
setSlowlyHidden(true); // 视频长度是 20s,在 17s 之后开始慢慢渐隐
}
}, [onStart]);

useEffect(() => {
video?.addEventListener('timeupdate', onTimeUpdate, false);

return () => {
video?.removeEventListener('timeupdate', onTimeUpdate);
};
}, [uuid, autoPlay, playHtmlVideo, onTimeUpdate, onEnd]);

<video
className={classNames(className, {
'z-hidden': isSlowlyHidden
})}
ref={videoRef}
{...props}
/>;
1
2
3
4
5
6
7
8
9
10
.headvideo {
opacity: 1;
transition: opacity 4.2s ease;

object-fit: cover;

&.z-hidden {
opacity: 0;
}
}

全屏展示

这块主要是通过,让 url 携带客户端全屏参数来支持实现:

?full_screen=true&nm_style=sbt&keep_status_bar=true&status_bar_type=light&bounces=false

这里的 bounces=false 是让客户端禁用边界滚动,毕竟 overscroll-behavior: none; 能力有限

然后在配合全屏组件增加后退分享功能,同时需要向下兼容站外非全屏场景。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const isInApp = Env.isInNEMapp() || Env.isInLOOKapp(); // 是否在 app 内

useEffect(() => {
// 非全屏时的分享,这块考虑直接加进 FullHeader 组件里,最新版已支持,不用单独编写
adapter.share(
window.location.href,
'LOOK3周年 一起狂欢吧!',
'https://p6.music.126.net/obj/wo3DlcOGw6DClTvDisK1/9848769524/db72/741e/a839/b4b1c199d71d64934635c9ac649934f1.jpg',
'参加3周年游园会,点亮徽章赢取限时好礼,珍稀实体奖励等你拿!'
);
}, []);

// 全屏时的头部栏组件
<Header
backgroundColor="#01224a"
show={isInApp} // 根据是否在 app 内来判断是否展示
shareConfig={{
url: window.location.href,
title: 'LOOK3周年 一起狂欢吧!',
content: '参加3周年游园会,点亮徽章赢取限时好礼,珍稀实体奖励等你拿!',
img: 'https://p6.music.126.net/obj/wo3DlcOGw6DClTvDisK1/9848769524/db72/741e/a839/b4b1c199d71d64934635c9ac649934f1.jpg'
}}>
LOOK直播三周年
</Header>;
  • 全屏组件的高度是写死的,因为目前客户端还没提供比较好的获取实际状态栏高度的方式,目前高度是 2rem,所以全屏时头部按钮还要额外往下移动一段距离
  • 全屏下要去除 iOS 底部的 padding-bottom 安全距离的处理。

强调动画

这块一般用 css animation 来实现,一般最常用的特效就是 放大抖动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@keyframes tada {
0% {
transform: scaleX(1);
}

10%,
20% {
transform: scale3d(0.9, 0.9, 0.9) rotate(-3deg);
}

30%,
50%,
70%,
90% {
transform: scale3d(1.1, 1.1, 1.1) rotate(3deg);
}

40%,
60%,
80% {
transform: scale3d(1.1, 1.1, 1.1) rotate(-3deg);
}

100% {
transform: scaleX(1);
}
}

.xxx {
/* 时长 1 秒,缓动 ease,延迟 0 秒,无限循环,正向播放,会恢复原始状态,正在运行,关键帧名 tada */
animation: 1s ease 0s infinite normal both running tada;
}

渐入和渐出

这块也用 css transition 配合动态添加 class 即可实现,比如:

1
2
3
4
5
6
7
8
.xxx {
opacity: 0;
transition: opacity 0.5s ease;

&.z-show {
opacity: 1;
}
}
1
2
3
4
5
6
7
8
9
10
const [show, setShow] = useState(false); // 是否展示

useEffect(() => {
setShow(true);
}, []);

<div
className={classNames('xxx', {
'z-show': show
})}></div>;

上下弹出的动画,也可以用这种方式,动态设置 bottom 来实现。

复杂的渐入渐出效果建议还是用 React Transition Group

元素层级

层级一般不用 z-index 来实现,这会给后期维护带来很大困难。

主要还是靠给父元素和浮于上层的元素都加上 position: relativeposition: absolute (即 f-pr 和 f-pa),然后通过声明位置的先后来控制层级。

总结

总结一下技术方案对比:

  • 前端动画工作流方案

    • 展示型动效
      • apng-canvas
      • video
      • jsmpeg
      • lottie很多效果实现不了, 不适合复杂动画,只适合简单几何动画
      • gif
    • 非展示型动效:
      • css 手写
      • pixi.js 手写
      • three.js 手写
  • 展示型动效实现

    • GIF
    • 动态图片APNG
      • 使用apng-canvas库,将其在canvas上进行绘制。
      • 优点:
        • 兼容性好,使用方便;
        • 控制方便;
      • 缺点:
        • 无npm包,需要引入js文件,使用window.APNG方法;
        • 初次解析apng文件,会有卡顿情况, 可以先预解析下;
        • 不能使用webgl render,帧率低;
    • 视频
      • web端/移动端有用户交互
        • 使用video标签播放;
      • 移动端无用户交互
        • 使用jsmpeg库转canvas播放;
      • 优点:
        • 可以使用webgl render和2d render;
      • 缺点:
        • 需要先转为MPEG-1,且转码后的大小会明显增大;
        • 素材上传中心不支持MPEG-1格式视频,需要自行上传cdn;
        • 低端机在播放时会出现页面崩溃(iphone5);
    • 透明视频
      • 使用video播放, 读取纹理,支持webgl时,采用webgl render,不支持时采用canvas 2d render
      • 优点
        • 由于video压缩率高,没有256色限制,所以自由度高。
      • 缺点
        • 只能在站内播放 微信不支持自动播放。
        • 另外对于循环动画: iOS下video循环动画头尾会存在一个gap
        • 开启页面后锁屏,打开后视频不会自动播放
        • 两个视频无法实现无缝切换,会有微小的空白出现
        • ios端半屏的boss动效放一段时间之后就会停了,安卓的可以一直播放boss动效(有待验证)
      • 暂时没有好的解决方法,可以考虑改写jsmsg读MPEG-1视频,然后再进行计算。