之前也在用 Sass 和 Stylus,不过局限在嵌套、变量之类的简单使用,对其他特性敬而远之,觉得在写样式的过程中过多去考虑这一块会降低页面重构效率。直到最近项目中出现了大量 css 动画,一想到要重复调整关键帧和统一多个元素的入场动画太麻烦啦,于是决定试试 mixin,然后发现效率提高了不止一丢丢这么厉害。

介绍

Sass is the most mature, stable, and powerful professional grade CSS extension language in the world.

渐现入场

滑屏的 H5, 每一屏会有文字渐现入场,然后这种类型的动画我习惯用 transition 并改变 opacity 或 transform 去实现类似的效果,然后写了这样的混合器。

@mixin fadeIn($class, $duration:0.8s, $delay: .5s, $tag: play) {
.#{$class} {
opacity: 0;
}
&.#{$tag} .#{$class} {
transition: opacity $duration $delay;
opacity: 1;
}
}

使用的时候,这样子

.slogan {
...
}
@include fadeIn(
$class: 'slogan',
$delay: 1s,
$duration: .8s
);

最后输出这样子

.slogan {
...
}
.slogan {
opacity: 0;
}
.play .slogan {
transition: opacity .8s 1s;
opacity: 1;
}

就这样把动画的部分和样式分离开了,至于为什么不把 transition 写在元素初始的样式里,这里考虑到的是翻屏之后不会出现动画回滚的状态,也就是排除了 opacity from 1 to 0 的渐变过程。

组帧动画

由于项目基于 FIS 开发,这里精灵图的处理也就没那么灵活了,因为没法产出多套精灵图片,也就无法实现图片的懒加载。对于精灵图的处理,暂时使用了这样的原始方式。

首先,通过 CssSprite在线生成器 生成垂直排列的精灵图,每一帧有固定的宽高,如下截取部分

keyframes

然后这里又写了个 mixin, 注释写成这样不要在意…写完回头看一下虽然逻辑不复杂但是很好用窝

// keyframes_ani
// ------------------------------------
// @param $animation_name 动画名称
// @param $background 精灵图片
// @param $height 容器高度
// @param $count 帧数
// @param $duration 动画时长,默认为 2s
// @param $delay 动画延时,默认为 0s
// @param $weight 每一帧在整个动画过程所占时间长短的权重,map 中必须包含 null 为缺省权重,默认为 1
// @param $times 播放次数,默认为 forwards
// ------------------------------------
@mixin keyframes_ani($animation_name, $background, $height, $count, $duration: 2s, $delay: 0s, $weight: (null: 1), $times: forwards) {
background: $background no-repeat;
animation: $animation_name $duration $delay steps(1) $times;
$vals: ();
$sum: 0;
@for $i from 0 to $count {
$key: $i + 1;
$vals: append($vals, $sum);
@if (map-has-key($weight, $key)) {
$sum: $sum + map-get($weight, $key);
} @else {
$sum: $sum + map-get($weight, null);
}
}
$avg: floor(100 / $sum);
@keyframes #{$animation_name} {
@for $i from 0 to $count {
#{nth($vals, $i + 1) * $avg * 1%} {
background-position: 0 ($i * $height * -1);
}
}
100% {
background-position: 0 (($count - 1) * $height * -1);
}
}
}

然后样式引用 mixin, 并提供必要参数

.people {
border: 1px solid #000;
width: 113px;
height: 113px;
@include keyframes_ani(
$animation_name: people-ani,
$background: url(./people_sprite.png),
$height: 118px,
$weight: (
2: 11,
11: 19,
23: 5,
null: 3
),
$count: 24,
$duration: 5s
);
}

然后就搞定啦,最终输出代码如下

.people {
border: 1px solid #000;
width: 113px;
height: 113px;
background: url(./people_sprite.png) no-repeat;
animation: people-ani 5s 0s steps(1) forwards;
}
@keyframes people-ani {
0% {
background-position: 0 0px;
}
3% {
background-position: 0 -118px;
}
14% {
background-position: 0 -236px;
}
17% {
background-position: 0 -354px;
}
20% {
background-position: 0 -472px;
}
23% {
background-position: 0 -590px;
}
26% {
background-position: 0 -708px;
}
29% {
background-position: 0 -826px;
}
32% {
background-position: 0 -944px;
}
35% {
background-position: 0 -1062px;
}
38% {
background-position: 0 -1180px;
}
57% {
background-position: 0 -1298px;
}
60% {
background-position: 0 -1416px;
}
63% {
background-position: 0 -1534px;
}
66% {
background-position: 0 -1652px;
}
69% {
background-position: 0 -1770px;
}
72% {
background-position: 0 -1888px;
}
75% {
background-position: 0 -2006px;
}
78% {
background-position: 0 -2124px;
}
81% {
background-position: 0 -2242px;
}
84% {
background-position: 0 -2360px;
}
87% {
background-position: 0 -2478px;
}
90% {
background-position: 0 -2596px;
}
95% {
background-position: 0 -2714px;
}
100% {
background-position: 0 -2714px;
}
}

这里想巴拉下 keyframe_ani 中的 $weight 参数,map 类型,表示每一帧在整个动画过程所占时间长短的权重,缺省的帧匹配为 null, 默认为 (null: 1)。这里的权重即是这一关键帧在整个 $duration 中所占的时间比,方便调整 $duration 而不对动画造成其他影响。下面 $weight 的两种写法的结果是一致的。

$weight: (
1: 3,
4: 3,
null: 1
)
$weight: (
1: 6,
4: 6,
null: 2
)

demo 见 codepen.

曲线运动

有一屏设计师说要让纸被风吹走,要飞着转几圈,要飞的优雅。这时候如果直接手写关键帧出来的效果应该是不满意的,曲线运动的动画会变的生硬;让我加一块 canvas 我也不干,纯粹强迫症?于是琢磨着也用 Sass 实现,然后就走了这样的歪路。

animation

整体的思路,我直接拿了之前星座项目用过的贝塞尔曲线相关的实现,通过关键点生成路径点集,然后粗暴地通过 node 直接生成 sass-variable.scss 文件,里面声明了四个变量,每个变量包含了每个纸片动画过程中50个关键帧的偏移量,然后再通过变量生成 keyframes, 就搞定啦。

代码如下,也很简单就是了

// buildSassVar.js
'use strict';
const pointCount = 50;
const points = [
[{x: 0, y: 0},{x: -40, y: -60}, {x: -20, y: 250}, {x: -500, y: 400}],
[{x: 0, y: 0},{x: -40, y: 140}, {x: 120, y: 300}, {x: -160, y: 800}],
[{x: 0, y: 0},{x: -100, y: 90}, {x: -240, y: 180}, {x: -500, y: 340}],
[{x: 0, y: 0},{x: 10, y: 120}, {x: 200, y: 220}, {x:150, y: 400}, {x:-300, y:600}],
[{x: 0, y: 0},{x: 50, y: 10}, {x: 50, y: 100}, {x: -50, y: 400}, {x: -200, y:600}, {x: -400, y:800}]
];
build();
function build() {
let content = '';
points.map((point, index) => {
let str = '';
// 创建点集
CreateBezierPoints(point, pointCount).map((pos, idx) => {
str += `${idx}:(${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}),`;
});
content += `$paper${index}: (${str});\n`;
});
// 写入文件
require('fs').writeFileSync('./components/css/sass-variable.scss', content, {
encoding: 'utf8'
});
}
function CreateBezierPoints(anchorpoints, pointsAmount) {
var points = [];
for (var i = 0; i < pointsAmount; i++) {
var point = MultiPointBezier(anchorpoints, i / pointsAmount);
points.push(point);
}
return points;
}
function MultiPointBezier(points, t) {
var len = points.length;
var x = 0, y = 0;
var erxiangshi = function (start, end) {
var cs = 1, bcs = 1;
while (end > 0) {
cs *= start;
bcs *= end;
start--;
end--;
}
return (cs / bcs);
};
for (var i = 0; i < len; i++) {
var point = points[i];
x += point.x * Math.pow((1 - t), (len - 1 - i)) * Math.pow(t, i) * (erxiangshi(len - 1, i));
y += point.y * Math.pow((1 - t), (len - 1 - i)) * Math.pow(t, i) * (erxiangshi(len - 1, i));
}
return { x: x, y: y };
}
// sass-variable.scss
$paper0: (0:(0.00, 0.00),1:(-2.33, -3.16),2:(-4.55, -5.46),3:(-6.67, -6.92),4:(-8.73, -7.57),5:(-10.76, -7.43),6:(-12.78, -6.53),7:(-14.81, -4.90),8:(-16.89, -2.55),9:(-19.03, 0.47),10:(-21.28, 4.16),11:(-23.65, 8.48),12:(-26.17, 13.41),13:(-28.87, 18.92),14:(-31.78, 24.99),15:(-34.92, 31.59),16:(-38.32, 38.70),17:(-42.00, 46.28),18:(-46.00, 54.33),19:(-50.34, 62.80),20:(-55.04, 71.68),21:(-60.14, 80.94),22:(-65.66, 90.55),23:(-71.62, 100.49),24:(-78.06, 110.73),25:(-85.00, 121.25),26:(-92.47, 132.02),27:(-100.49, 143.02),28:(-109.10, 154.22),29:(-118.31, 165.59),30:(-128.16, 177.12),31:(-138.67, 188.77),32:(-149.87, 200.52),33:(-161.79, 212.34),34:(-174.45, 224.22),35:(-187.88, 236.11),36:(-202.11, 248.00),37:(-217.16, 259.87),38:(-233.06, 271.68),39:(-249.84, 283.41),40:(-267.52, 295.04),41:(-286.13, 306.54),42:(-305.71, 317.88),43:(-326.26, 329.05),44:(-347.83, 340.00),45:(-370.44, 350.73),46:(-394.11, 361.20),47:(-418.88, 371.39),48:(-444.76, 381.27),49:(-471.80, 390.81),);
$paper1: (0:(0.00, 0.00),1:(-2.17, 8.43),2:(-3.88, 16.92),3:(-5.18, 25.49),4:(-6.09, 34.15),5:(-6.64, 42.92),6:(-6.87, 51.82),7:(-6.80, 60.85),8:(-6.46, 70.05),9:(-5.89, 79.41),10:(-5.12, 88.96),11:(-4.17, 98.71),12:(-3.09, 108.68),13:(-1.89, 118.88),14:(-0.61, 129.33),15:(0.72, 140.04),16:(2.07, 151.03),17:(3.41, 162.31),18:(4.70, 173.91),19:(5.92, 185.82),20:(7.04, 198.08),21:(8.02, 210.69),22:(8.84, 223.67),23:(9.46, 237.04),24:(9.86, 250.81),25:(10.00, 265.00),26:(9.85, 279.62),27:(9.38, 294.68),28:(8.57, 310.21),29:(7.37, 326.22),30:(5.76, 342.72),31:(3.71, 359.73),32:(1.19, 377.26),33:(-1.84, 395.33),34:(-5.40, 413.96),35:(-9.52, 433.16),36:(-14.24, 452.94),37:(-19.58, 473.33),38:(-25.58, 494.33),39:(-32.27, 515.96),40:(-39.68, 538.24),41:(-47.84, 561.18),42:(-56.77, 584.80),43:(-66.52, 609.11),44:(-77.10, 634.14),45:(-88.56, 659.88),46:(-100.92, 686.36),47:(-114.21, 713.60),48:(-128.47, 741.61),49:(-143.72, 770.41),);
$paper2: (0:(0.00, 0.00),1:(-6.05, 5.40),2:(-12.20, 10.80),3:(-18.45, 16.22),4:(-24.81, 21.64),5:(-31.28, 27.07),6:(-37.87, 32.52),7:(-44.57, 37.99),8:(-51.40, 43.49),9:(-58.35, 49.01),10:(-65.44, 54.56),11:(-72.66, 60.15),12:(-80.02, 65.77),13:(-87.52, 71.43),14:(-95.16, 77.14),15:(-102.96, 82.89),16:(-110.91, 88.69),17:(-119.02, 94.55),18:(-127.28, 100.47),19:(-135.72, 106.44),20:(-144.32, 112.48),21:(-153.10, 118.59),22:(-162.05, 124.76),23:(-171.18, 131.01),24:(-180.50, 137.34),25:(-190.00, 143.75),26:(-199.70, 150.24),27:(-209.59, 156.82),28:(-219.68, 163.49),29:(-229.98, 170.26),30:(-240.48, 177.12),31:(-251.19, 184.08),32:(-262.12, 191.15),33:(-273.27, 198.32),34:(-284.64, 205.61),35:(-296.24, 213.01),36:(-308.07, 220.53),37:(-320.13, 228.17),38:(-332.43, 235.93),39:(-344.97, 243.82),40:(-357.76, 251.84),41:(-370.80, 260.00),42:(-384.09, 268.29),43:(-397.64, 276.72),44:(-411.45, 285.30),45:(-425.52, 294.03),46:(-439.86, 302.91),47:(-454.48, 311.94),48:(-469.37, 321.13),49:(-484.54, 330.48),);
$paper3: (0:(0.00, 0.00),1:(1.22, 9.56),2:(3.22, 19.03),3:(5.93, 28.45),4:(9.26, 37.83),5:(13.15, 47.18),6:(17.50, 56.53),7:(22.26, 65.88),8:(27.34, 75.26),9:(32.67, 84.68),10:(38.18, 94.14),11:(43.79, 103.68),12:(49.45, 113.29),13:(55.07, 122.99),14:(60.59, 132.79),15:(65.95, 142.70),16:(71.07, 152.74),17:(75.89, 162.91),18:(80.35, 173.22),19:(84.39, 183.68),20:(87.94, 194.30),21:(90.93, 205.09),22:(93.32, 216.04),23:(95.04, 227.18),24:(96.04, 238.50),25:(96.25, 250.00),26:(95.62, 261.70),27:(94.10, 273.59),28:(91.62, 285.68),29:(88.15, 297.97),30:(83.62, 310.46),31:(77.98, 323.16),32:(71.19, 336.06),33:(63.19, 349.17),34:(53.94, 362.47),35:(43.39, 375.98),36:(31.49, 389.69),37:(18.20, 403.60),38:(3.47, 417.70),39:(-12.74, 431.99),40:(-30.46, 446.46),41:(-49.75, 461.12),42:(-70.65, 475.95),43:(-93.18, 490.95),44:(-117.40, 506.11),45:(-143.33, 521.42),46:(-171.02, 536.88),47:(-200.50, 552.48),48:(-231.80, 568.21),49:(-264.95, 584.05),);

相关的 mixin 如下

@mixin fly($animation_name, $pos) {
animation: $animation_name 2.5s 5s forwards;
@keyframes #{$animation_name} {
@each $time, $translate in $pos {
#{($time + 1)*2%} {
opacity: 1;
transform: translate(rem(nth($translate, 1)), rem(nth($translate, 2)));
}
}
}
}
...
&.play .paper i {
&:nth-child(1) {
@include fly(ani-paper-0, $paper0);
}
&:nth-child(2) {
@include fly(ani-paper-1, $paper1);
}
&:nth-child(3) {
@include fly(ani-paper-2, $paper2);
}
&:nth-child(4) {
@include fly(ani-paper-3, $paper3);
}
&:nth-child(5) {
@include fly(ani-paper-4, $paper4);
}
}

动画缩写

还写了个简单的 mixin, 用来写简单状态变化的动画,代码很简单就不继续巴拉了。(感觉自己写 mixin 写上瘾了噗)

@mixin animation ($animation_name, $duration, $delay: 0.5s, $bezier: linear, $times: infinite, $from: (), $middle: null, $to: ()) {
animation: $animation_name $duration $delay $bezier $times;
@include keyframes ($animation_name) {
0% {
@each $style, $val in $from {
#{$style}: $val;
}
}
@if($middle != null) {
50% {
@each $style, $val in $middle {
#{$style}: $val;
}
}
}
@if($to != null) {
100% {
@each $style, $val in $to {
#{$style}: $val;
}
}
}
}
}

小结

很遗憾之前没有开窍把这些特性用到项目中,仔细想想应该也是项目这么多动画需要才有了这些尝试。对于以往一些小需求来说,也就是杀鸡用不到屠龙宝刀这个意思吧~但其实用屠龙宝刀杀鸡也会很舒服的说,我现在是这样觉得的。

虽然上面的东西都是用的 Sass, 还是喜欢 Stylus 多一点的感觉🙊