Sass中的反三角函数

有可能你会认为数学在CSS中用不上,但实际上,在写CSS时运用一些数学可以帮你做一些令人惊讶的事情。数学(尤其是三角函数)可以帮助你模拟一个真实的世界。如果你想做这样的事情,你需要了解复杂的三维变换。如果你只是想在朋友面前展示一下你的才华,这将是一件非常有兴趣的事情。

本文由大漠根据Ana Tudor的《Inverse trigonometric functions with Sass》所译,整个系列共分四章,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明原作者相关信息http://thesassway.com/advanced/inverse-trigonometric-functions-with-sass

——作者:Ana Tudor

——译者:大漠

有可能你会认为数学在CSS中用不上,但实际上,在写CSS时运用一些数学可以帮你做一些令人惊讶的事情。数学(尤其是三角函数)可以帮助你模拟一个真实的世界。如果你想做这样的事情,你需要了解复杂的三维变换。如果你只是想在朋友面前展示一下你的才华,这将是一件非常有兴趣的事情。

这里有一个示例:

这是一个旋转的三十二面体(由二十个三角形的页和十二个五角形的面组成),在CSS中可以说是一个使用三角学完成的一个高级示例。如果你为此感到头疼,你可以先看看Mason Wendell写的文本阴影的案例。Mason Wendell使用了Compass的sin()cos()函数来实现CSS的阴影重叠的事情。

我对三角函数并不太了解。但有时候三角函数对我来说并不够用,特别是在CSS中写2D和3D的案例时,我发现需要使用正弦,余弦和正切来计算一些值。我需要asin()acos()atan()函数。不幸的是,Compass并不提供这些函数功能,所以我有下面两个选择:

  • 通过计算器手动计算
  • 我自己写这些函数功能

那我肯定会选择第二条了。

幸运的我,偶然发现了一篇使用Sass写的正弦和余弦函数功能的文章。我认为使用同样的方法,也能实现我需要的功能。

声明:数学运算让人头疼,如果您仅仅是想看看效果,你可以跳过下面的内容,直接点击这里查看效果。

##三角101(Trigonometry 101)

先别深入,我们先回过头来回忆一下高中数学知识。

Sass中的反三角函数

这个图看起来有点熟悉。(如果不了解,可以看看YouTube上MathBFF的视频:基本的三角函数:Sin,Cos,Tan)。

根据上面的直角三角形,我们一起来回顾几个公式:

α+β=90°

用弧度来表示是这样的:

Sass中的反三角函数
大家可能还记得,毕达哥拉斯定理(Pythagorean theorem)告诉我们:

Sass中的反三角函数

还有一些基本的三角函数是这样定义的:

1
2
3
4
5
6
sin(α) = a/c
cos(α) = b/c
tan(α) = a/b
sin(β) = b/c
cos(β) = a/c
tan(β) = b/a

知道这些,我们就可以推导出其他的一些公式:

Sass中的反三角函数

头晕了?保持清醒的头脑继续往下。

##反正弦函数

什么是反正弦呢?好吧,如果:

sin(α)=z

那么反正弦是逆着的:

arcsin(z)=α

换句话说,给了一个角度的正弦,通过反正弦可以告诉你这个角度值。反余弦和反正切类似于在一个余弦或正切基础给了一个角度。

我们将用Sass来制作一个反正弦asin()的函数。我们将用级数来展开这一点。如果你不是一个数学奇才,泰勒级数展开是相当复杂的。我会尽我所能向大家介绍。对于反正弦,他看起来像这样:

Sass中的反三角函数

看到这个公式就吓坏了。让我们来分析这个结构:z是我们想要得到的α角的正弦。整个和是α的弧度值。当总和在[−π/2,π/2]之间时,z应该在[-1,1]之间。

每一个标签,包括第一个,你都可以写成(1)⋅z,它是被编造出来的两部分;第一部分是毕达哥拉斯定理内的部分,第二部分是毕达哥拉斯定理以外的部分。

对于每个i标签是第一位的,第一部分是(2i−1)/(2i),第二部分中的分子部分是z2i+1幂次方,而分母是2i+1

这可能会得到一个无限大的数,但是一旦得到期限值,对于条款中的值可能会变得非常小,小得我们甚至可以忽略他不计,而且还是安全的,不会影响其他任何东西。

但我们应该从哪个地方开始停下来呢?比方说,在一个范围的十分之一。以弧度1度的值来计算,1度 = π/180 ≈ 3.14 / 180 ≈ .0175。所以这里的十分之一就是“.00175” 。所以当值小于“.00175”时,他就会停下来,就像我们起床的闹钟,到这个点就会停下来。

我们一起来看两示例。

z=0时,这个简单,因为所有条款都是0,所以根据级数展开,无单位的弧度值是0和度值是0⋅180°/π=0°。

z=1时,第一项是1,第二项是1/6=.167,第三项是3/40=0.075,第四项是.045,第五项是.030,第六项是.022,第七项是.017,第八项是.014。我们注意到我们有一个问题,每一项明显有下降,但这种放慢真的非常缓慢,离我们停下来的临界点.00175还好远。

但离我们需要的真正值有多远呢?我们把所有项做了一个总结,如下:

1+.167+.075+.045+.030+.022+.017+.014=1.496

这个弧度值转换成度值是85°。离正确的90°并不是太远,但现在变得越来越难接近准确值。这将会导致更多的循环和较慢。它的一个问题是,虽然在特定的情况下影响的程序较小,我们也应该保证每种情况下的绝对值在[0,π/2]上半部分。

我们要怎样解决这个问题呢?首先要检查产生的角度绝对值是否大于π/4,如果是,我们采用这种方法π/2−|α|计算他的补差。由于正弦函数在[0,π/2]之间是一个递增函数,所以我们要检查z的绝对值是否大于sin(π/4)

但是我们怎么知道正弦函数中的补差值中α的绝对值呢?嗯,他等于余弦的α的绝对值:sin(π/2−|α|)=cos(|α|)。根据相关公式,我们可以得知:

Sass中的反三角函数

##编写asin()函数

哎,好多数学公式呀!让我们来看看一些代码。

首先给所有项的总和设置一个默认的阈值:

$default-threshold: pi() / 180 / 10;

pi()函数是Compass内置的一个数学函数,其返回的值就是一个π值。

然后我们开始写我们的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
@function asin($z) {
$sum: 0;
//abs(),Sass内置函数,返回一个数的绝对值
//sin(),Compass内置函数,返回一个角度的正弦值
//sqrt(),Compass内置函数,返回一个数的平方根
//pow(),Compass内置函数,返回一个数的幂值,如pow($z,2)返回的是$z的二次方
@if abs($z) > sin(pi() / 4) {
$z: sqrt(1 - pow($z, 2));
}
@return $sum;
}

我们设计总和$sum的初始值为0,如果我们角度的绝对值大于pi()/4时,我们要确保计算他的补差。但在那之后,我们怎么知道我们已计算了补差呢?是切换到初始角度吗?

为了更好的跟踪,我引入了一个布尔变量$complement,并且定义其初始值为false,但在@if区块内通过true来切换返回的值。默认之下返回的是$sum值,如果检测到$complement变量值为true时,则返回pi()/2 - $sum

1
2
3
4
5
6
7
8
9
10
11
@function asin($z) {
$sum: 0;
$complement: false;
@if abs($z) > sin(pi() / 4) {
$complement: true;
$z: sqrt(1 - pow($z, 2));
}
//Miscellaneous函数,当$complement为true,返回pi()/2 - $sum,反之返回$sum
@return if($complement, pi() / 2 - $sum, $sum);
}

但这只适合正值,所以我们要引入变量$sign,它的值可以是1或者-1。我们还让$z值等于他的绝对值,并且让$sign值等于$z除以他自身的绝对值。代码变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
@function asin($z) {
$sum: 0;
$complement: false;
$sign: $z / abs($z);
$z: abs($z);
@if $z > sin(pi() / 4) {
$complement: true;
$z: sqrt(1 - pow($z, 2));
}
@return $sign * (if($complement, pi() / 2 - $sum, $sum));
}

现在,让实际项目加起来得到总和,一旦这个这个总和的值比我们传递给函数的阈值小,我们设置他停止下来。首先设置第一个项$term,其值等于$z,并且放在循环语句@while的前面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@function asin($z, $threshold: $default-threshold) {
$sum: 0;
$complement: false;
$sign: $z / abs($z);
$z: abs($z);
@if $z > sin(pi() / 4) {
$complement: true;
$z: sqrt(1 - pow($z, 2));
}
$term: $z;
@while $term > $threshold {
$sum: $sum + $term;
}
@return $sign * (if($complement, pi() / 2 - $sum, $sum));
}

在这一点上,除非我们的$term值小于$threshold值,不然@while会一直循环计算,因为我们在里面没有改变$term值。因此,每一次迭代计算机都会计算一次。为了做到这一点,我们在循环前初始化两个变量。一个是$i,即当前项索引;另一个是$k,毕达哥拉斯定理里面的那部分。在此之后,我们在循环之内,不断递增$i的值和重新计算$k$term的值:

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
@function asin($z, $threshold: $default-threshold) {
$sum: 0;
$complement: false;
$sign: $z / abs($z);
$z: abs($z);
@if $z > sin(pi() / 4) {
$complement: true;
$z: sqrt(1 - pow($z, 2));
}
$term: $z;
$i: 0;
$k: 1;
@while $term > $threshold {
$sum: $sum + $term;
$i: $i + 1;
$k: $k * (2 * $i - 1) / (2 * $i);
$j: 2 * $i + 1;
$term: $k * pow($z, $j) / $j;
}
@return $sign * (if($complement, pi() / 2 - $sum, $sum));
}

到此就完成了,在Sass中我们有了自己的asin()函数功能。

我们是否有什么方法可以检测出abs($z) <= 1,如果他返回的是一个false,抛出一个错误。因为在这种情况之下,$term不会在$threshold值内返回false,我们的循环就是一个无限循环。

##编写acos()函数

现在我们有一个函数来计算反正弦,在此基础上,我们可以很容易写出反余弦函数acos()。事实上,在此示例中,角度α的值都是在[0,π]之间,不难得出cos(α)=sin(π / 2−α)。如果我们知道cos(α)=z,那么arcsin(z)=π/2−α,如此一来,给我们提供α=π/2−arcsin(z)

1
2
3
@function acos($z, $threshold: $default-threshold) {
@return pi() / 2 - asin($z, $threshold);
}

##编写atan()函数

对于atan()函数,我们从这样的事实开始tan(α)=sin(α)/cos(α)。我们也知道:

Sass中的反三角函数

也知道:

Sass中的反三角函数

根据这样的关系,我们可以得出正弦与正切之间的关系:

Sass中的反三角函数

我们也知道tan(α)=z,所以:

Sass中的反三角函数

将上面公式简化一下:

Sass中的反三角函数

因此得出我们的反正切atan()函数:

1
2
3
@function atan($z, $threshold: $default-threshold) {
@return asin($z / sqrt(1 + pow($z, 2)), $threshold);
}

##将这些函数功能更好的用于CSS中

请记住,这些函数返回的值是一个无单位的弧度值。在我们CSS中是无法直接使用这些值,因为在CSS中至少需要有一个1rad或者带有单位的换算值。但是,如果我们在调用函数时,能指定单位呢?如:

transform: rotate(asin(.5, 'deg'));

为了做到这一点,我们需要一个能转换角度的函数,它能接受一个无单位的弧度值,并且它能将其转换成我们指定的单位,如:

1
2
$in-degrees: convert-angle(pi() / 4, 'deg');
$in-turns: convert-angle(pi() / 2, turn); // 让单位带不带引号都能工作

##编写角度转换函数

首先我们创建无单位的弧度与CSS单位之间的换算系数表。在Sass中,我们可以使用Map功能:

1
2
3
4
5
6
$factors: (
rad: 1rad,
deg: 180deg / pi(),
turn: .5turn / pi(),
grad: 200grad / pi()
);

这样,我们只需要使用无单位的弧度值乘以系数值,也就是:

1
2
3
4
5
6
7
8
9
10
@function convert-angle($value, $unit-name) {
$factors: (
rad: 1rad,
deg: 180deg / pi(),
turn: .5turn / pi(),
grad: 200grad / pi()
);
@return $value * map-get($factors, $unit-name);
}

这个功能是失败的,如果$unit-name不是$factors列表中对应的关键词,或者$value已经有单位了,在CSS中无效的。因此我们需要完善此函数功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@function convert-angle($value, $unit-name) {
$factors: (
rad: 1rad,
deg: 180deg / pi(),
grad: 200grad / pi(),
turn: .5turn / pi()
);
@if not unitless($value) {
@warn '`#{$value}` should be unitless';
@return false;
}
@if not map-has-key($factors, $unit-name) {
@warn 'unit `#{$unit-name}` is not a valid unit - please make sure it is either `deg`, `rad`, `grad` or `turn`';
@return false;
}
@return $value*map-get($factors, $unit-name);
}

##增强反三角函数功能

现在我们只需要完善我们的反三角函数具有单位换算功能:

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
@function asin($z, $unit-name: deg, $threshold: $default-threshold) {
$sum: 0;
$complement: false;
$sign: $z / abs($z);
$z: abs($z);
@if $z > sin(pi() / 4) {
$complement: true;
$z: sqrt(1 - pow($z, 2));
}
$term: $z;
$i: 0;
$k: 1;
@while $term > $threshold {
$sum: $sum + $term;
$i: $i + 1;
$k: $k * (2 * $i - 1) / (2 * $i);
$j: 2 * $i + 1;
$term: $k * pow($z, $j) / $j;
}
@return convert-angle($sign*(if($complement, pi()/2 - $sum, $sum)), $unit-name);
}
@function acos($z, $unit-name: deg, $threshold: $default-threshold) {
@return convert-angle(pi()/2, $unit-name) - asin($z, $unit-name, $threshold);
}
@function atan($z, $unit-name: deg, $threshold: $default-threshold) {
@return asin($z/sqrt(1 + pow($z, 2)), $unit-name, $threshold);
}

我将度deg设置为默认单位,因为这可以是大多数人比较了解和会使用的,并且将其放置在$threshold前面,因为它仍然有可能有人可能会改变单位。

##总结

如果你能跟着看到这里,你是一个真正的爱学习的人。最后的功能如下:

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
$default-threshold: pi() / 180 / 20;
@function convert-angle($value, $unit-name) {
$factors: (
rad: 1rad,
deg: 180deg / pi(),
grad: 200grad / pi(),
turn: .5turn / pi()
);
@if not unitless($value) {
@warn '`#{$value}` should be unitless';
@return false;
}
@if not map-has-key($factors, $unit-name) {
@warn 'unit `#{$unit-name}` is not a valid unit - please make sure it is either `deg`, `rad`, `grad` or `turn`';
@return false;
}
@return $value*map-get($factors, $unit-name);
}
@function asin($z, $unit-name: deg, $threshold: $default-threshold) {
$sum: 0;
$complement: false;
$sign: if($z != 0, $z / abs($z), 1);
$z: abs($z);
@if $z > 1 {
@warn 'illegal `#{$z}` value for function';
@return false;
}
@if $z > sin(pi() / 4) {
$complement: true;
$z: sqrt(1 - pow($z, 2));
}
$term: $z;
$i: 0;
$k: 1;
@while $term > $threshold {
$sum: $sum + $term;
$i: $i + 1;
$k: $k * (2 * $i - 1) / (2*$i);
$j: 2 * $i + 1;
$term: $k * pow($z, $j) / $j;
}
@return convert-angle($sign * (if($complement, pi() / 2 - $sum, $sum)), $unit-name);
}
@function acos($z, $unit-name: deg, $threshold: $default-threshold) {
@return convert-angle(pi() / 2, $unit-name) - asin($z, $unit-name, $threshold);
}
@function atan($z, $unit-name: deg, $threshold: $default-threshold) {
@return asin($z/sqrt(1 + pow($z, 2)), $unit-name, $threshold);
}

在结束这篇文章时,我在Codepen放了两个案例,以激发你的学习激情:

更多的案例可以到我的Codepen上查阅。

译者手语:整个翻译依照原文线路进行,并在翻译过程略加了个人对技术的理解。如果翻译有不对之处,还烦请同行朋友指点。谢谢!

如需转载烦请注明出处:

英文出处:http://thesassway.com/advanced/inverse-trigonometric-functions-with-sass

中文译文:http://www.w3cplus.com/preprocessor/advanced/inverse-trigonometric-functions-with-sass.html