Sass 中的反三角函数
有可能你会认为数学在 CSS 中用不上,但实际上,在写 CSS 时运用一些数学可以帮你做一些令人惊讶的事情。数学(尤其是三角函数)可以帮助你模拟一个真实的世界。如果你想做这样的事情,你需要了解复杂的三维变换。如果你只是想在朋友面前展示一下你的才华,这将是一件非常有兴趣的事情。
这里有一个示例:https://codepen.io/anon/embed/kpCyx
这是一个旋转的三十二面体(由二十个三角形的页和十二个五角形的面组成),在CSS中可以说是一个使用三角学完成的一个高级示例。如果你为此感到头疼,你可以先看看Mason Wendell写的文本阴影的案例。Mason Wendell使用了Compass的sin()
和cos()
函数来实现CSS的阴影重叠的事情。
我对三角函数并不太了解。但有时候三角函数对我来说并不够用,特别是在CSS中写2D和3D的案例时,我发现需要使用正弦,余弦和正切来计算一些值。我需要asin()
、acos()
和atan()
函数。不幸的是,Compass并不提供这些函数功能,所以我有下面两个选择:
- 通过计算器手动计算
- 我自己写这些函数功能
那我肯定会选择第二条了。
幸运的我,偶然发现了一篇使用Sass写的正弦和余弦函数功能的文章。我认为使用同样的方法,也能实现我需要的功能。
声明:数学运算让人头疼,如果您仅仅是想看看效果,你可以跳过下面的内容,直接点击这里查看效果。
三角 101(Trigonometry 101)
先别深入,我们先回过头来回忆一下高中数学知识。
这个图看起来有点熟悉。如果不了解,可以看看 YouTube上MathBFF 的视频:基本的三角函数:Sin,Cos,Tan。
根据上面的直角三角形,我们一起来回顾几个公式:
α+β=90°
用弧度来表示是这样的:
大家可能还记得,毕达哥拉斯定理(Pythagorean theorem)告诉我们:
还有一些基本的三角函数是这样定义的:
sin(α) = a/c
cos(α) = b/c
tan(α) = a/b
sin(β) = b/c
cos(β) = a/c
tan(β) = b/a
知道这些,我们就可以推导出其他的一些公式:
头晕了?保持清醒的头脑继续往下。
反正弦函数
什么是反正弦呢?好吧,如果:
sin(α)=z
那么反正弦是逆着的:
arcsin(z)=α
换句话说,给了一个角度的正弦,通过反正弦可以告诉你这个角度值。反余弦和反正切类似于在一个余弦或正切基础给了一个角度。
我们将用Sass来制作一个反正弦asin()
的函数。我们将用级数来展开这一点。如果你不是一个数学奇才,泰勒级数展开是相当复杂的。我会尽我所能向大家介绍。对于反正弦,他看起来像这样:
看到这个公式就吓坏了。让我们来分析这个结构:z
是我们想要得到的α
角的正弦。整个和是α
的弧度值。当总和在[−π/2,π/2]
之间时,z
应该在[-1,1]
之间。
每一个标签,包括第一个,你都可以写成(1)⋅z
,它是被编造出来的两部分;第一部分是毕达哥拉斯定理内的部分,第二部分是毕达哥拉斯定理以外的部分。
对于每个i
标签是第一位的,第一部分是(2i−1)/(2i)
,第二部分中的分子部分是z
的2i+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(|α|)
。根据相关公式,我们可以得知:
编写 asin()
函数
哎,好多数学公式呀!让我们来看看一些代码。
首先给所有项的总和设置一个默认的阈值:
$default-threshold: pi() / 180 / 10;
然后我们开始写我们的函数:
@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
。
@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
除以他自身的绝对值。代码变成:
@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
的前面:
@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
的值:
@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)
。
@function acos($z, $threshold: $default-threshold) {
@return pi() / 2 - asin($z, $threshold);
}
编写 atan()
函数
对于 atan()
函数,我们从这样的事实开始 tan(α)=sin(α)/cos(α)
。我们也知道:
也知道:
根据这样的关系,我们可以得出正弦与正切之间的关系:
我们也知道 tan(α)=z
所以:
将上面公式简化一下:
因此得出我们的反正切 atan()
函数:
@function atan($z, $threshold: $default-threshold) {
@return asin($z / sqrt(1 + pow($z, 2)), $threshold);
}
将这些函数功能更好的用于 CSS 中
请记住,这些函数返回的值是一个无单位的弧度值。在我们CSS中是无法直接使用这些值,因为在CSS中至少需要有一个1rad
或者带有单位的换算值。但是,如果我们在调用函数时,能指定单位呢?如:
transform: rotate(asin(.5, 'deg'));
为了做到这一点,我们需要一个能转换角度的函数,它能接受一个无单位的弧度值,并且它能将其转换成我们指定的单位,如:
$in-degrees: convert-angle(pi() / 4, 'deg');
$in-turns: convert-angle(pi() / 2, turn); // 让单位带不带引号都能工作
编写角度转换函数
首先我们创建无单位的弧度与 CSS 单位之间的换算系数表。在 Sass 中,我们可以使用 Map 功能:
$factors: (
rad: 1rad,
deg: 180deg / pi(),
turn: .5turn / pi(),
grad: 200grad / pi()
);
这样,我们只需要使用无单位的弧度值乘以系数值,也就是:
@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 中无效的。因此我们需要完善此函数功能:
@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: $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
前面,因为它仍然有可能有人可能会改变单位。
总结
如果你能跟着看到这里,你是一个真正的爱学习的人。最后的功能如下:
$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 放了两个案例,以激发你的学习激情:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论