Orion's Studio.

活动动画相关技术方案

2023/03/27

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

三周年页面

背景动画 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视频,然后再进行计算。
CATALOG
  1. 1. 背景动画 gif
  2. 2. 进场视频 video
    1. 2.1. 整体思路
    2. 2.2. Video 相关基础知识
    3. 2.3. 渐入渐出
    4. 2.4. 全屏展示
  3. 3. 强调动画
  4. 4. 渐入和渐出
  5. 5. 元素层级
  6. 6. 总结