实现原理
雪碧图(sprite)
雪碧图也叫精灵图, 是一种CSS图像合成技术。通俗来说:将小图标合并在一起之后的图片称作雪碧图,每个小图标的使用配合background-position来获取。
雪碧图的作用有两个:
- 减少图片资源的请求数,提升页面加载速度
- 制作序列帧动画
这里我们要用第二种方式。假如我们有如下的一系列图片:

现在我们将它合到一张图片中。这时我们可以用ps一张一张拼,也可以用工具直接生成,如textpacker。
https://www.codeandweb.com/texturepacker

如下,我们可以拼成一个几行几列的图,或者都放在一行。


css实现序列帧动画
我们都用过css的transition和animation实现动画。这里我们要像幻灯片一样,在每一帧停顿一下,再切换到下一帧。
这里要用到animation的一个属性steps。
animation的工作原理是通过将元素的CSS样式从一个状态改变为另一个状态(我们称为线性变化)。默认情况下,它以<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">ease</font>方式(速度逐渐放慢)过渡,它会在每个关键帧之间插入补间动画,所以动画效果是连贯性的,但有些时候,我们不需要这种过渡效果,而是想实现跳跃式的效果,这时,<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">steps()</font>函数就出现了。
steps()是一个阶跃函数,用于把整个操作领域切分为相同大小的间隔,每个间隔都是相等的。
语法:
1
| steps(number[, end | start])
|
参数说明:
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">number</font>参数指定了时间函数中的间隔数量(必须是正整数)
- 第二个参数是可选的,可设值:
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">start</font>和<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">end</font>,表示在每个间隔的起点或是终点发生阶跃变化,如果忽略,默认是end。
注意:第二个参数还有两个内置值,<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">step-start</font>等同于<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">steps(1,start)</font>,动画分成1步,动画执行时以左侧端点为开始;<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">step-end</font>等同于<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">steps(1,end)</font>:动画分成1步,动画执行时以结尾端点为开始。

对于单行拼接的雪碧图(假设有n张图拼接,即帧数为n),我们将起始的x设为0,结束的x设为100%,中间经过的间隔应该是n-1,即steps(n-1, start)。
css单行实现
单张图片600px * 647px,共21张,拼在一起12600px * 647px。注意这里background-size要写完整大小。
此时动画就一个区间,然后steps分成20步。
1 2 3 4 5 6 7 8 9 10 11 12
| .duck{ width: 600px; height: 647px; background: url('https://xiaojinhe-cdn.iyoudui.cn/assets/run-duck/duck-1-row.png') no-repeat left center / 12600px 647px; background-position: 0 0; animation: duck-1-row infinite 1s steps(20,start); } @keyframes duck-1-row{ 0%{background-position: 0 0;} 100%{background-position: 100% 0;} }
|
codepen预览: https://codepen.io/ahao430/pen/xbKwEdv
css多行实现
此时动画要分为N个区间,每个区间一步。
less
less不支持for循环,但是可以通过while循环来模拟。
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
| @duck_width: 600px; @duck_height: 647px;
.duck-rows{ width: @duck_width; height: @duck_height; background: url('https://xiaojinhe-cdn.iyoudui.cn/assets/run-duck/duck-rows.png') no-repeat left top / @duck_width * 3 @duck_width * 7; background-position: 0 0; animation: duck-rows infinite 1s steps(1, start); }
.duck_rows_step(@row_count, @count, @total) when (@count <= @total) { @percent: (@count - 1) / (@total - 1) * 100%; @cur_row: ceil(@count / @row_count) - 1; @cur_col: mod(@count - 1, @row_count); @row: ceil(@total / @row_count); @col: @row_count; @x: (@cur_col ) / (@col - 1) * 100%; @y: (@cur_row ) / (@row - 1) * 100%; @{percent} { background-position: @x @y; } .duck_rows_step(@row_count, @count + 1, @total); } @keyframes duck-rows{ .duck_rows_step(3, 1, 21); }
|
codepen预览: https://codepen.io/ahao430/pen/zxOvKdQ
在线less转css工具:https://www.lesstester.com/
转换出来的css如下:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| .duck-rows { width: 600px; height: 647px; background: url('https://xiaojinhe-cdn.iyoudui.cn/assets/run-duck/duck-rows.png') no-repeat left top / 1800px 4200px; background-position: 0 0; animation: duck-rows infinite 1s steps(1, start); } @keyframes duck-rows { 0% { background-position: 0% 0%; } 5% { background-position: 50% 0%; } 10% { background-position: 100% 0%; } 15% { background-position: 0% 16.66666667%; } 20% { background-position: 50% 16.66666667%; } 25% { background-position: 100% 16.66666667%; } 30% { background-position: 0% 33.33333333%; } 35% { background-position: 50% 33.33333333%; } 40% { background-position: 100% 33.33333333%; } 45% { background-position: 0% 50%; } 50% { background-position: 50% 50%; } 55% { background-position: 100% 50%; } 60% { background-position: 0% 66.66666667%; } 65% { background-position: 50% 66.66666667%; } 70% { background-position: 100% 66.66666667%; } 75% { background-position: 0% 83.33333333%; } 80% { background-position: 50% 83.33333333%; } 85% { background-position: 100% 83.33333333%; } 90% { background-position: 0% 100%; } 95% { background-position: 50% 100%; } 100% { background-position: 100% 100%; } }
|
scss
scss支持for循环,实现起来更简单
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 33 34
| @use "sass:math"; $duck_width: 600px; $duck_height: 647px;
.duck-rows{ width: $duck_width; height: $duck_height; background: url('https://xiaojinhe-cdn.iyoudui.cn/assets/run-duck/duck-rows.png') no-repeat left top #{'/'} $duck_width * 3 $duck_width * 7; background-position: 0 0; animation: duck-rows infinite 1s steps(1, start); }
@function generate-step($i, $total, $row_count) { $percent: ($i - 1) / ($total - 1) * 100%; $cur_row: math.ceil($i / $row_count) - 1; $cur_col: ($i - 1) % $row_count; $row: math.ceil($total / $row_count); $col: $row_count; $x: ($cur_col) / ($col - 1) * 100%; $y: ($cur_row) / ($row - 1) * 100%; @return ($percent,$x,$y); }
@keyframes duck-rows{ $row_count: 3; $total: 21; @for $i from 1 through 21 { $result: generate-step($i, $total, $row_count); #{nth($result, 1)} { background-position: nth($result, 2) nth($result, 3); } } }
|
codepen预览: https://codepen.io/ahao430/pen/XJrmjaQ
转换出来的css同上。
gif vs 序列帧
我们知道,当我们有一系列图片,直接生成一个gif做动图更简单。那么为什么要用序列帧呢?
|
优点 |
缺点 |
| gif |
兼容性好,播放简单,可以多平台支持。 |
1. GIF格式仅支持 8 位,也就是256种不同的颜色会有色彩失真,所以效果差 2. 动画文件大,播放资源内存、CPU占用高。 |
| 序列帧 |
1. 制作方便简单,还原度高,颜色偏差损失基本没有(PNG图片压缩可能会有颜色偏差)。 2. 可以通过代码控制动画的播放、停止、循环、播放速度等。 |
需要加载全部图片,文件占用内存大,内存一大就容易造成打开页面的时候卡顿,大的文件用户体验感很差 |
跟GIF相比,序列帧不会产生边缘锯齿的效果,也比GIF支持的色彩范围大。当对效果要求高时,序列帧比gif更加合适。
问题处理
兼容性(白屏)
当拼接成一行时,texturepacker会提示这个警告。上网查了一些帖子提到一些设备对超过4k的图片展示空白,但是帖子的时间比较早,暂时未测试到这个问题。
兼容性(抖动)

在小程序中发生了抖动,因为小程序的单位是rpx,换算成px会有小数,雪碧图移动背景图位置的时候,就可能向左或向右取整。这里的解决办法是使用真实的px,再根据设备宽度进行缩放。
兼容代码如下:
1 2 3 4 5
| <view class="duck-scale-container"> <view class="duck-scale-container-inner" style="transform: translate(-50%, -50%) scale({{scale}});"> <view class="duck"></view> </view> </view>
|
1
| const scale = app.globalData.systemInfo.windowWidth / 750
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @duck_width: 457px; @duck_height: 457px;
.duck-scale-container{ width: 457rpx; height: 457rpx; margin: 0 auto; overflow: hidden; position: relative; } .duck-scale-container-inner{ position: absolute; left: 50%; top: 50%; }
|
注意,原先鸭子及雪碧图动画background-position等,单位都换成px。外层加容器,单位rpx。然后内层容器定位,通过scale缩放。
切换空白问题
需求中一个小鸭子的形象。有几套雪碧图进行切换。雪碧图比较大,切换过程中第一次加载图片等待会造成白屏。
处理办法是提前加载图片,并准备一张静态背景图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <view class="duck duck-1-row duck-a" onTap="handleClickDuck" a:if="{{action === 'a'}}"></view> <view class="duck duck-1-row duck-b" onTap="handleClickDuck" a:elif="{{action === 'b'}}"></view> <view class="duck duck-1-row duck-c" onTap="handleClickDuck" a:elif="{{action === 'c'}}"></view> <view class="duck duck-1-row duck-d" onTap="handleClickDuck" a:elif="{{action === 'd'}}"></view> <view class="duck duck-1-row" onTap="handleClickDuck" a:else></view>
<view style="visibility: hidden; width: 0; height: 0; overflow: hidden;"> <view class="duck duck-1-row duck-a"></view> <view class="duck duck-1-row duck-b"></view> <view class="duck duck-1-row duck-c"></view> <view class="duck duck-1-row duck-d"></view> </view>
|