孙世吉

2018-07-06 00:07

rem逐帧动画与像素精度计算

本文作者:IMWeb 孙世吉 原文出处:IMWeb社区 未经同意,禁止转载

目录

  • 1.一个大动图带来的思考
  • 2.rem与逐帧动画
  • 3.浏览器精度计算
  • 4.一个神奇的优化

    一个大动图带来的思考

前段时间(很长的一个时间区间),接手了一个产品的活动运营需求。设计要求要有精妙的动态效果,大概长这个样子:

是的,你没有看错,只有那个委屈的小脑斧的尾巴是会动的,其他内容基本都不动。

很简单嘛,三下五除二把图片往页面上一扔,就完事了。但是问题出现了,图片太大了!

动图 带透明背景的动图 动图 * 5 带透明背景的动图 * 5
250k 780k 1250k 3900k

看到上面的数据,一个页面如果带上五个带透明背景的动图,将会带来整整4M的移动端流量,哪怕在这个全国流量提速降费,省内流量升级全国流量,各种免流卡横行的年代,我依旧为广大用户感到小小的心疼。

相信大家也都知道,一般移动端场景下图片处理大多以压缩加分片为基本手段。这里呢,我们将这一张图片切成17张,主要包括1张不会动的透明底图,以及16张动画帧图片。

是的,你没有看错,可怜的小老虎不仅被打了,连尾巴都被我们卸下来了,同时还切成了16份,将这16张尾巴逐帧放映就可以完美的呈现出小老虎摇摆的尾巴了。同时图片被压缩到了(109k + 18.7k)的大小,只有原来的16%。

这里稍微跑一下题,给大家推荐一个在线合并雪碧图的网站 GoPng ,适合快速合并小图片。

回到我们的题目,图片处理完毕了,接下来就是把图片放置到合理的位置,并使用css切换图片位置就可以得到我们要的效果了。

.animation-img {
    width: 6.83rem;
    height: 12.8rem;
    background: url(animation.png) 0 0 no-repeat;
    background-size: 109.23rem 12.8rem;
    animation: flash 1s steps(16) infinite;
}
@keyframes flash {
  100% {
    background-position: -109.23rem 0;
  }
}

rem与逐帧动画

有一定实践经验的同学相信一定看出来上文的问题。

rem给我们带来了很大的便捷,颇有一招鲜吃遍天的架势,rem相关介绍可以参考我之前的文章——简单粗暴的移动端适配方案 - REM。但是既然说了只是“架势”,那它肯定有其局限性所在。这里我们就遇到了它的一大问题——“精度计算”。

既然我们通常是根据屏幕的尺寸,计算并设置根元素的font-size,从而影响rem的基准值。那不同的尺寸之间肯定是没有一个公约数的,也就是说我们没办法设定一个基准值来保证不同的屏幕尺寸下,rem值在换算成px值的时候是整数。

当320px的屏幕基准像素为12px时,iphone8(375px)下html的font-size 就是14.0625px,iphone8p下font-size就是15.525px。

浏览器在计算像素精度时,并不是直接全部取整或者取余的,这点其实你稍微想想一下就能得到结论。那我们上文这样在109rem的宽度下取16帧的时候,自然也就会出现多1px或者少1px的误差。这也就导致了我们逐帧动画出现了抖动!

此时我们有三个选择:

  • 1、这部分内容抛弃rem单位,使用绝对正确的px单位来设置动画区间;
  • 2、这部分内容抛弃雪碧图,使用16张相同大小的图片保证精度计算的误差是一致的(都向上或向下取整);
  • 3、寻求更多的解决方案;

经过仔细思考,为了保证项目代码一致性,保留rem单位处理是必须要保留的;同时我们也不能为了实现效果就忽略了性能,所以雪碧图也必须保留。那我们就只有唯一一条出路,寻求更多解决方案了。

三人行必有我师焉 —— 孔子

CSS技巧:逐帧动画抖动解决方案 这篇文章里详细的介绍了:

  • 抖动出现的原因 —— 逻辑像素映射到物理像素时出现“误差”
  • 解决方案构想与实践 —— svg自适应缩放尺寸 + foreignObject 内嵌html代码

详细的内容强烈推荐大家都去阅读一下。

同时使用svg设置外层尺寸(rem),再使用实际的大小设置内容的尺寸(px),我们保留rem自适应屏幕宽度特性的同时,也确保内部内容的大小计算不会出现精度问题(因为设定的都是整数的px),以下的最终的代码。

<svg viewBox="0, 0, 96, 180" class="tiger-tail">
    <foreignObject class="inner-html" width="96" height="180">
        <div class="inner-img"></div>
    </foreignObject>
</svg>
.tiger-tail {
    position: absolute;
    width: 3.41rem;
    height: 6.4rem;
    top: 6.6rem;
    left: 13.6rem;
    .inner-html {
        width: 96px;
        height: 180px;
    }
    .inner-img {
        width: 1536px;
        height: 180px;
        background: url(animation.png) 0 0 no-repeat;
        background-size: 1536px 180px;
        animation: flash1 1s steps(16) infinite;
    }
}

@keyframes flash {
  100% {
    background-position: -1536px 0;
  }
}

浏览器精度计算

问题解决后,不禁对浏览器精度计算的过程出现了一些兴趣。

rem 产生的小数像素问题一文对浏览器对小数像素的处理进行了猜测和时间。

如果一个元素尺寸是 0.625px,那么其渲染尺寸应该是 1px,空出的 0.375px 空间由其临近的元素填充;同样道理,如果一个元素尺寸是 0.375px,其渲染尺寸就应该是 0,但是其会占据临近元素 0.375px 的空间。

本人进行实验前,先阅读了WebKit关于LayoutUnit的wiki

当亚像素向物理像素进行映射时,有两种方法(蓝色表示亚像素块,黑色代表物理渲染块):

最小包含块

最小包含块保证逻辑像素的方块一定包含在物理像素块内,计算左边和上边位置的时候,对数值进行向下取整;计算右边和下边位置的时候,对数值进行向上取整,计算过程如下:

x: floor(x)
y: floor(y)
maxX: ceil(x + width)
maxY: ceil(y + height)
width: ceil(x + width) - floor(x)
height: ceil(y + height) - floor(y)

像素拟合块

像素拟合块不保证包含关系,只是找到周边最贴近的物理像素方块进行拟合。所以计算左/上位置的时候,是直接对数值进行取整,而右/下位置的取值是根据原左/上位置和宽/高的和值取整而来,计算过程如下:

x: round(x)
y: round(y)
maxX: round(x + width)
maxY: round(y + height)
width: round(x + width) - round(x)
height: round(y + height) - round(y)

有一个方式可以观测到这种拟合的过程,设定大小为4.5px的方块,然后并联十个放在同个容器里面。代码如下:

<div class='wrapper'>
    <div class="block"></div>
    <div class="block"></div>
    <div class="block"></div>
    <div class="block"></div>
    <div class="block"></div>
    <div class="block"></div>
    <div class="block"></div>
    <div class="block"></div>
    <div class="block"></div>
    <div class="block"></div>
</div>
.wrapper {
    font-size: 0;
    width: 45px
}
.block{
    display: inline-block;
    width: 4.5px;
    height: 4.5px;
    background: rgba(0, 0, 255, .5);         
}
.block:nth-of-type(2n){
    background: rgba(255, 0, 0, .5);
}

选中第二个block元素,浏览器截图后放大,如下图所示。

我们可以得到三点结论:

  • 1、色块有长有短,即符合前文说的“补偿”的过程
  • 2、 这个过程是散落在所有块间的,不是简单的从左到右的计算;
  • 3、选中的第二个dom元素的选中阴影说明其逻辑位置和渲染位置是不一致的;

    一个神奇的优化

    同时在进行试验的过程中发现一个很有意思的事情:

The current implementation represents values as multiples of 1/64th pixel . This allows us to use integer math and avoids floating point imprecision.

上面试验的过程中,发现当前实际计算基准值好像不是1/64像素,于是就写了一段代码来验证我的想法:

<!DOCTYPE html>
<html>
<meta charset="utf-8"></meta>
<meta name="viewport" content="width=device-width, initial-scale=1">

<head>
    <title>rem精度计算与逐帧动画</title>
</head>

<body>
    <div class="block"></div>
    <script type="text/javascript">
        const map = {};
        const block = document.querySelector('.block');
        let count = 0;
        var calc = function () {
            // 设置样式
            block.style.width = (count / 1000) + 'px';
            setTimeout(() => {
                // 获取计算后的宽度
                const width = parseFloat(getComputedStyle(block).width);
                map[width] = true;
                console.log(width)
                count++;
                if (count < 1000) {
                    calc();
                } else {
                    console.log(Object.keys(map).map(x => parseFloat(x)));
                }
            }, 0);
        };

        calc();
    </script>
</body>

</html>

在Chrome 67.0.3396.99 版本下

当html文件名为index.html时:

当html文件名为copy.html时:

Chrome浏览器在计算网站首页(index.html)并进行渲染时,是按照更高的精度进行计算的。

看来Chrome对网站的首页做了一个神奇的优化呢~

而在Firefox 47.0.2 版本下:基准值依旧是文档里描述的1/60。

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。