如何做一个让人闻风丧胆的 H5
前言
最近火热的有声娱乐平台 APP,企鹅 FM,在8月28日鬼节前夕,联合《盗墓笔记》推出了“勇敢者的游戏”活动。作为一个 UI 工程师,在这个移动互联网叱咤风云的时代,每次看到朋友圈中被分享的各种花样 H5 页面,总是心痒难耐,也想做有着酷炫动画和带感声效的 H5 呢。回想到做鬼节活动页的时候,接近午夜零点还在调整页面效果,看着页面上渐隐渐现的可怕画面,活生生吓到了自己,也是蛮难忘的。作为刚刚来到活动页新手村的朋友,踩到了一些坑,所以让我进入正题吧。哦,等一下,请先扫一下二维码啦~
与设计师的沟通
在拿到视觉稿和需求单之后,我们需要了解整个活动的流程。有的时候,设计师并不会把每个页面的动画效果做成视频,而是用口述的方式和工程师进行沟通。这样就需要工程师结合活动页流程和设计稿之后,自己先构思一些动效再去同产品以及设计沟通,这样交流的效率才会比较高。
动画新手有的时候会天马行空想到一些奇怪的效果,可能会不符合整体设计风格,可能会违反现实物理规律,有自己的想法,还要和产品设计确认。比如这个页面:
一开始是做成了先出现手电筒,再出现光,在我的设想中刚开始手电筒上不会有那层黄绿色的光。后来经设计师提醒:如果完全没有光源,也应该看不到手电筒。才改成了现在的方案:灯光闪两下:物理定律什么的,我才记不清了呢T T。
P.S. 做动画的时候铭记 动效原则,基本上动画的效果不会跑偏。
具体实现
仔细想想,这个活动页面并没有用到什么高深技巧,基本上是用 position 定位和 CSS3 动画完成的。不过在写页面之前,还是有不少担心的:
- 页面兼容怎么做
按照 iPhone6 的尺寸确定元素的位置,然后用zoom
或者transform: scale(x)
拉伸页面或者拉伸元素。在实际开发的时候,我一度对这两个属性的用法产生混淆,所以作些了研究,稍后会详细说说这两个属性。 - 3D 变化效果怎么实现
大家应该早就听说过或者使用过perspective
和perspective-origin
这两个属性了,虽然教程和分享都看过不少,但真正写起来还是有一些摸不着头脑,各种搜索关于 3D 动画的解释之后我认为原理大概是这样的:简单粗暴地说,请想象你是站在上图中的红点位置,你与物体间的距离是 perspective,眼睛的位置是 perspective-origin。好了,现在站定位置,去看这个物体,想象物体投影在某个平面上的效果(这里的平面就是我们的显示屏),这就是 3D 投影的结果。
关于
perspective-origin
,可以看看这个 demo。
说了这么多,在实际运用中,我还是找了一个 生成器。
得到的效果大概是这样的:想要重点说一下磁带的实现,主要牵涉到的图片资源是下面这几个:
磁带被分成了3个部分,磁带底部,磁带封面和磁带。其中封面和磁带是正视图,需要使用 3D 旋转,让这两个元素“躺下去”,而且为了保证这三个元素之间不会因为屏幕的缩放,出现错位的问题,需要保证磁带封面和磁带的定位是基于磁带底部的。其实磁带上还有两片盖子,不知道大家有没有注意到。其实这个盖子是左右对称且中心对称的,所以完全可以只用一张图片完成这样的效果,用
transform: scale(-1, 1);
和transform: scale(1, -1);
实现。 - 动画如何才有代入感
这个活动页面我个人最喜欢的地方就是星星砸下去的动画,感觉所有的戏份都在它身上。开始的设想只是星星砸下去,感谢在制作过程中,经验丰富的同事所提的建议:星星砸下去的时候要有灰尘溅起或者火星四溅的效果。后来和设计商量,最后用了灰尘溅起的效果。不过本来还想做,星星背后的铁板应该要有震动的效果。由于时间关系,震动的效果有点不好添加,所以放弃了,这一点感觉有一些遗憾。可能页面新手的经验不够,很多动画的效果因为有限的想象力所以不太完善。这个时候和身边经验丰富的同事请教是很有效的方法。同时,一定要多看 B 站拓宽视野才行哦。
踩到了一些坑
页面布局并不复杂。前文也提到,基本上使用 position: absolute; 来实现的,不过遇到了一些问题就有了以下总结:
- 杀鸡就不要用牛刀,能又快又好解决问题才最重要
上图是首页的截图,页面加载之后应该可以看到“胆量测试”下面的蓝色线条有一个动画。
故事是这样的,自从 SVG 帝王小啪带着神器 svgartisan 降临(对 SVG 有兴趣的同学欢迎加群 426886128 一同讨论 SVG 技术),做页面的时候总想加上一点 SVG。然而这个蓝色线条上有特效,所以用 PS 导出 SVG 后还要做二次处理才能还原设计稿上的效果,而且 SVG 的代码你懂得,它总是有点长。其实有个简单粗暴的方法:改变蓝色线条的图片宽度,虽然这个方法从性能上看并不好,但考虑到这个页面也只有这里发生了重绘,所以还是用图片+宽度控制来实现了。
不知道大家会不会为了某一种技术而去做某一件事情,但其实完成那件事情才是真正的目的,却因为那个技术而耽误了不少进度,这样就有点进入了炫技的误区。
- 答应我,伪元素上就不要做动画了
伪元素真的是神一样的存在,一个标签自带两个儿子,不知道为什么就有种金闪闪的感觉。但是伪元素上的动画真的很坑,年少无知,页面都写完了,发现在 iOS 上美如画的动画效果,到了小米和魅族上就……总之看到屏幕那一刻我是这样的:Android 上坑多,不要一次应用太多新技术。比如在魅族上用 flexbox 布局,就要加上 display: -webkit-box。还有一个教训就是,页面果然应该做一个测试一个啊QAQ。因为这个项目是重构和前端并行开发的……把伪元素改成实际 DOM 元素的时候,是怀着一颗对不起前台的心的。
- 请写好注释
与UI工程师对接的前台同学需要看注释才知道什么时候要加什么class,想到刚刚开始接需求的时候,从来不写注释…真是对不起前台同学T T现在我个人的注释如上图所示。也看过组里不同同事的注释风格,大同小异,主体思想就是“在XX时候添加XX class”这样,和对接的同学约定好就可以~
zoom 和 transform: scale(x); 的使用
我将会在接下来详细谈到前文提到的 zoom 和 transform: scale(x); 问题。
为什么要使用缩放
现在不管是活动页的设计稿还是产品页的设计稿,逐渐以 375×667 的 iPhone6 为基础。但是实际生活里,这些页面是会出现在细细长长的 iPhone5、480px 高度的 iPhone4 还有大屏幕的 iPhone 6+,更不要说在三星小米魅族一加等等等等尺寸都不知道怎么办才好的 Android 系列上打开会是什么gui。
拿这次的活动页面设计稿来说,与用户产生交互的按钮在页面篇底部的位置。有一个前提,为了兼容不同宽度的屏幕,所以页面是基于 iPhone 6 的 375px 用 zoom 属性进行缩放,可以看出iPhone 5 和 iPhone 4 的宽度一样,而且看设计稿,页面元素是从上到下分布的:
也就是说,使用相同的 zoom 值,满足了 iPhone5 的页面效果,但是在 iPhone4 上,按钮就会偏底,页面整体感觉也偏底。但是 zoom 值不能随便更改,否则红框中的录音机图片的左右边界就会显示出来。所以要针对 iPhone 4 调整元素的之间的间距,最终的效果大概是这样的:
可以看得出效果并不是很好,整个页面非常的拥挤,所以在这个情况下,我觉得用统一 zoom 值来调整有点一棍子打倒了,如果一个个元素微调,那么最好效果会好得多。加上 zoom 会有一定的性能问题,组里的同事有些是 zoom 调整,也有给每个元素加 class 通过 transform: scale() 调整的。重构最神奇的就是,条条大路通罗马,怎么样都能达到自己想要的视觉效果,但是中间会因为各种原因,实现的方式不太一样。
拿到设计稿一开始就先看看这个设计稿的布局,有一些是从页面顶部到底部都有效果的,这个时候就要考虑在 iPhone4 这样屏幕不够高的设备上如何保证页面完整呈现;或者在不影响交互的情况下,隐藏哪些元素。有的时候页面上元素比较集中,这个时候就要考虑在 iPhone6+ 这样的大屏幕设备上,要不要调整间距使得页面不会太空旷。
要知晓设计稿背后的含义,不是一拿到就开始做了,有些元素其实是要整体考虑的。有些乍一看好像是用 position 定位,分别写 top 值就好。殊不知,设计师真正要表达的是,作为一个整体,它们在页面上要绝对居中。没有 get 到这个 point,兼容的时候就要哭了。
zoom 和 transform:scale 的概念
先来看一下 zoom
和 transform:scale
的说明:
Specifies the initial zoom factor for the window or viewing area. This is a magnifying glass type of zoom. Interactively changing the zoom factor from the initial zoom factor does not affect the size of the initial or the actual viewport.
从定义上看 zoom 缩放的是被 zoom 的容器的视口,可以把它想象成放大镜的效果,这个属性是可被继承的,所以我们做设备屏幕兼容的时候,可以在 body 标签下加一个 div 包裹住页面上的其他元素,然后在这个 div 上加 zoom,达到的视觉效果就是页面上其他元素也被缩放了。但是有些元素并不支持 zoom。
A two-dimensional transformation is applied to the coordinate system an element renders in through the ‘transform’ property. This property contains a list of transform functions. The final transformation value for a coordinate system is obtained by converting each function in the list to its corresponding matrix (either defined in this specification or by reference to the SVG specification), then multiplying the matrices.
在说 scale 应该要先看看 transform。transform 属性应用到元素的过程其实是矩阵变换的过程,在渲染的时候,元素的坐标就会被确定下来,然后和 transform 的属性值进行矩阵运算得到最终的坐标,不过你会发现,一个绝对定位的元素通过 transform 改变显示位置后,这个元素的 tbrl 值并不会被更新,且 transform 属性不可继承的。
The value of the transform property is a list of applied in the order provided. … specifies a scale operation using the [sx,1] scaling vector, where sx is given as the parameter. … specifies a scale operation using the [1,sy] scaling vector, where sy is given as the parameter.
scale 是 transform 的一个属性值,这是一个缩放矩阵。当一个元素被定义了 transfrom: scale(x); 后,还是再结合它的 transfrom-origin,才能确定最后的缩放效果。依然是兼容屏幕分辨率的问题,要想 transfrom: scale(x) 达到和 zoom 相似的效果,要记得把 transfrom-origin 设置成 0 0。这么设置的原因是,在文档流中的元素,是以它的左上角为中心进行 zoom 的,而当元素脱离文档流时,要使 transform: scale(x) 和 zoom 达到相同的效果,还要具体分析 transform-origin 要如何设置。
大概你也注意到了,在前一句中,我说的是“相似的效果”而不用“一样的效果”,这是因为使用 scale 的时候可能遇到下面这样的问题。下图中左边为 transform:scale(.85),右边为 zoom: .85:
下面这段是外层容器的样式,背景是定义在 switch-wh 动画中,通过绝对定位让浏览器自行计算,保证容器大小占满整个屏幕:
因为 zoom 是作用在 body 下面这个占满了屏幕空间的容器,所以根据定义以及 zoom 的继承性,我们可以说在这个页面上使用 zoom 其实是缩放了整个屏幕(也就是视口),可以想象成在浏览器中打开了页面,然后放大这个页面的效果:
为什么 scale 会留下右部和底部的迷の白色呢?让我们回到 transform 的定义中,“applied to the coordinate system an element renders in through the ‘transform’ property”。当元素都进行渲染了,坐标已经确定了,再进行缩放,也就是在原来元素基础上改变大小。所以 .sf-index 虽然在渲染时四个角的位置分别是(0,0)、(100%,0)、(0,100%)、(100%, 100%),经过以 (0,0) 为变换中心的 scale,就变成了(0,0)、(85%,0)、(0,85%)、(85%,85%)。我们就会看到页面右边出现宽度为15%的一条白边,以及页面下方高度为15%的白边。
好像 zoom 无敌了呢
看起来,好像兼容的时候应该用 zoom 呢。嗯看到 scale 之后的结果我就是这么想的。接着就发现 zoom 之后的页面,文字的显示不太正常。下图左边是被 zoom 的 iPhone4,右边是没有被 zoom 的 iPhone6:
由于页面是被整体缩放了,所以文字也自然出现了缩放,刚好这种好像被砍了一刀的文字效果还蛮适合鬼节的活动页面,所以我并没有做处理。正常来说,如果需要做处理就是调整文字的 line-height 和容器的 height,使其不出现遮挡。
正如页面元素经过 zoom 后,实际的大小会发生改变,图片的大小也发生了改变,使用雪碧图就出现了一些问题。雪碧图是把各种小图拼合到一张大图上面,通过 width、height 和 background-position 定位到图片,看下图可以发现相邻图片的边界也一起显示出来了。
审查元素发现,用于显示图片的元素尺寸也不对啊:
可以看出这个元素正确的尺寸应该是 198×52,经过 zoom: 1.10 放大后容器反而变小了,后来将 zoom 值调整到 1.104(414/375),图片的边界问题算是解决了。
关于图片没有正常显示的问题,我的推断是,原因在于 zoom 值设定偏小,图片经过 zoom 后没有被正确地计算,而图片的容器又偏大,所以相邻图片的边就被显示了出来。后来 zoom 值是根据比例设定了,就不会出现这个问题。
最后,zoom 对性能不友好,下面两个 gif 分别是 zoom 和 transform: scale 引起的重绘:
很明显,在文档流中 zoom 加在任意一个元素上都会引起 整个页面的重新渲染,而 transform: scale 只是在当前的元素上重绘。
还有没有更好的兼容方法呢
这样说来,简直两个方法都不能用了嘛…还有没有什么别的兼容的方法呢?
有的。
像需要大量图片的页面,做兼容的时候我们常常担心的是什么?当然是图片比例出问题咯,所以也会使用通过伪元素设置 padding-top 的方法,保证图片比例正常地显示出来。只是这样的写法通常要结合 background-size:cover; 而我们常用的工具 CssGaga 在生成雪碧图了之后会覆盖 background-size。目前的这个方案的话……就不合成雪碧图了。
还有一个方案是使用 media query
结合 rem (或百分比)完成这样的布局,不过目前 gaga 不支持 background-size 的 rem,所以要采用什么方式合成雪碧图以及如何生成新的样式,还需要寻找新的方向。在不需要合成雪碧图的时候,可以用这两种方法。
工具/网站推荐
- 查看设备的屏幕参数
- 设计keyframes
- 动画灵感哪里找: http://codepen.io/, https://dribbble.com/
- 无聊时候陪着你的企鹅 FM 退出《鬼吹灯》大结局啦,无聊时候陪着你的企鹅 FM 退出《鬼吹灯》大结局啦,无聊时候陪着你的企鹅 FM 退出《鬼吹灯》大结局啦。听完盗墓听鬼吹灯,根本停不下来!