本文翻译自 Randomness in CSS using trigonometry,作者:Kacper 略有删改
在过去,我已经介绍了CSS中使用模运算的伪随机性的主题,我使用素数创建了一个自动计数器,可以用来为每个对象生成不同的值。因此,我们可以独立地计算每个元素的伪随机值。
尽管这个解决方案很强大,但它几乎没有缺点:
- 模函数不是连续的
- 它过于复杂:需要3个变量和@property定义每个我们想要生成的随机值
- 需要使用尚未广泛支持的@property
幸运的是,我们可以做得更好!在本文中提出一个更好的解决方案,使用三角函数。
更好的方法
自从我上次探索这个主题以来,CSS中出现了一些令人惊叹的新特性。其中最令人兴奋的是三角函数。他们解开了许多以前不可能完成的任务。它们也是CSS中第一个原生支持的有界连续函数,使它们成为创建伪随机生成器的工具。
随机性vs伪随机性
显然,这里提出的解决方案仅生成伪随机值。使用预定常数计算所有值。正如我在上一篇文章中所描述的,我们可以添加一个额外的--seed
变量,并从系统外部更改它(例如在加载时在JavaScript中设置它),以提供较少确定性的结果,但CSS不提供任何非确定性的方法。也就是说,该解决方案应该足以获得用于动画、位置等的足够的伪随机值。
正弦函数的特征
正弦函数和余弦函数有趣的原因有很多,它们可以在涉及圆和旋转的各种操作中非常有用。在我们的例子中,我们可以将它们的属性用于其他目的。
有界函数
正弦和余弦的一个重要性质是,结果值总是在-1和1之间有界。这意味着,无论传递给它的值有多大或小,结果总是在这个范围内的值。然后,我们可以对范围[0,1]
执行简单的归一化。有了归一化的值,我们可以使用它来表示使用简单的线性映射的任何值。
--x: calc(0.5 + 0.5 * sin(var(--n) * 342.06184 + 23.434));
/* Then we can use it as following */
background: rgb(calc(50 + var(--x) * 100), 0, 0);
/* Red will be in the range of 50-150.
上面的代码使用的是我在上一篇文章中介绍的counter var(--n)
,我使用素数创建了一种在CSS中自动创建计数器变量的有效方法。
然后将该值乘以一些任意值并偏移以提供伪随机大数(这些值实际上并不重要,您可以根据需要更改它们以获得不同结果)。之后我们使用正弦函数将其映射到范围[-1, 1]
。最后如下面的动画所示,我们可以通过应用简单的代数变换将其映射到范围[0, 1]
。一旦我们从范围[0, 1]
中获得值,我们可以使用线性映射将其映射到任何其他期望值。
连续性
正弦函数的另一个特性是连续性。简单理解,你可以认为正弦或余弦函数输入的微小变化最终会导致输出的微小变化。由于这一特性,我们可以在动画中实现逐渐变化的值,同时使系统表现出随机行为。
示例
下面是一些示例,展示了使用三角函数生成伪随机值的潜力。
圆网格动画
第一个示例显示了正弦属性的作用。生成的值是随机的,但我们仍然可以保持顺序和连续性的感觉,示例涉及到颜色和大小动画。
代码的关键部分是x,y,z和w变量的计算,这些变量分别用于表示红色,绿色,蓝色和宽度。
div::before {
--x: calc(0.5 + 0.5 * sin(4.284 * var(--n)));
--y: calc(0.5 + 0.5 * sin(7.284 * var(--n)));
--z: calc(0.5 + 0.5 * sin(4 * var(--n) + 2 * var(--t)));
--w: calc(0.5 + 0.5 * sin((0.2 * cos(var(--t)/100) + 0.8) * 49.123 * var(--n) + var(--t)));
background: rgb(
calc(50 + 100 * var(--x)),
calc(200 + 30 * var(--y)),
calc(120 + 100 * var(--z))
);
width: calc(50% + var(--w)*50%);
}
最后2个变量,除了我们的计数器--n之外,使用时间变量--t,其通过运行逐渐改变变量的动画来获得:
@property --t {
syntax: '<number>'; /* <- defined as type number for the transition to work */
initial-value: 0;
inherits: true;
}
:root {
--t: 0;
}
@keyframes timeOn {
50% {
--t: 30}
}
html {
animation: 30s timeOn infinite linear;
}
这是代码中唯一使用@property
的部分。为了让它在所有浏览器中工作,我们可以简单地在JavaScript
中更新这个变量,而不会失去在普通CSS中计算其他所有内容的能力。
斑点动画
随机性也可以与SVG元素一起使用,使其与SVG过滤器结合使用时成为一个强大的工具。下面的演示灵感来自于一篇令人惊叹的CSS技巧文章The Gooey Effect【https://css-tricks.com/gooey-effect/】。
使用简单公式确定每个单独的斑点的位置。唯一的区别是我们使用cx、cy、r和fill来设置它们的样式,因为它们是SVG元素。
.blobs > circle {
--x: calc(sin(var(--t) + var(--n) * 74.543 + 432.43312));
--y: calc(cos(var(--t) + var(--n) * 2.34 + 1.432));
--v: calc(0.5 + 0.5 * sin(var(--n) * var(--n) * 4.343 + 2.673));
cx: calc(10vw + var(--x) * 80vw);
cy: calc(10vh + var(--y) * 80vh);
r: calc(var(--v) * 5vw + 1vw);
}
为了实现粘性效果,我们使用以下SVG过滤器:
<filter id="goo">
<feGaussianBlur in="SourceGraphic" result="blur" stdDeviation="15" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 22 -11" result="goo" />
<feBlend in2="goo" in="SourceGraphic" result="mix" />
</filter>
孟菲斯风格
最后一个演示是一个更新版本的例子,我在之前尝试在CSS中实现随机性,在那里我使用了模运算符。使用新的解决方案计算更容易理解和修改。
核心代码:
div.obj {
--v: calc(0.5 + 0.5 * sin(var(--n) * 0.3141 + var(--n) * var(--n) * 0.9125 * var(--seed) + 0.531));
--x: calc(0.5 + 0.5 * cos(var(--seed) * var(--n) * 0.234 + var(--seed)));
--y: calc(0.5 + 0.5 * cos(var(--seed) * var(--n) * 432.32432 + var(--seed)));
--viz: calc(0.5 + 0.5 * sin(var(--seed) * var(--n) * 6901.525213542 + 432.5342));
--visible: calc(min(1, max(0, var(--viz) * 1)));
opacity: var(--visible);
display: inline-block;
position: absolute;
transform: rotate(calc(360deg * var(--v)));
top: calc(var(--y) * 100vh);
left: calc(var(--x) * 100vw);
}
最后
解析一下各案例中用到的公共代码:
判断一个数字是否为素数(质数):
@function isPrime({
mathJaxContainer[0]}primesList) {
@each {
mathJaxContainer[1]}primesList {
@if ({
mathJaxContainer[2]}prime == 0) {
@return false;
}
}
@return true;
}
函数接受两个参数:$n` 和 `$primesList
。$n`是要判断是否为素数的数字,`$primesList
是一个包含已知素数的列表。
循环体内部,通过 %
(取模) 运算符判断 $n` 是否能被当前的素数 `$prime
整除。如果 $n` 能整除,说明 `$n
不是素数,函数会立即返回 false
终止执行。否则则表示 $n` 不可被任何素数整除,因此 `$n
是素数,函数返回 true
。
计算素数并在CSS中生成相应的变量和样式规则
@mixin primeCounter({
mathJaxContainer[8]}upperBound: 2000) {
$curr: 2;
$primesList: [];
@while {
mathJaxContainer[9]}upperBound {
@if isPrime({
mathJaxContainer[10]}primesList) {
{
mathJaxContainer[11]}primesList, $curr);
}
{
mathJaxContainer[12]}curr + 1;
}
:root {
@each {
mathJaxContainer[13]}primesList {
--p#{
$prime}: 1;
}
}
$mult: "1";
@each {
mathJaxContainer[14]}primesList {
{
mathJaxContainer[15]}mult " * var(--p#{
$prime})";
{
mathJaxContainer[16]}prime;
@while {
mathJaxContainer[17]}upperBound {
#{
{
mathJaxContainer[18]}val}n) {
--p#{
{
mathJaxContainer[19]}val};
}
{
mathJaxContainer[20]}val * $prime;
}
}
#{
$selector} {
--n: calc(#{
$mult});
}
}
这个mixin
接受两个参数:$selector` 和 `$upperBound
。$selector` 是要添加素数样式的选择器,`$upperBound
是生成素数的上限,默认为 2000。
首先定义了两个变量 $curr` 和 `$primesList
,分别用于追踪当前数字和保存素数列表。
使用 @while
循环来遍历从 2 到 $upperBound` 的所有数字。在每次循环迭代中,通过调用外部函数 `isPrime` 来检查当前数字是否为素数。如果是素数,则将其添加到 `$primesList
列表。
接下来,定义了一个 :root
选择器,在其中使用 @each
循环遍历 $primesList
中的每个素数,并为每个素数生成一个CSS变量。生成的变量名称以 --p
开头,后面跟着素数的值。例如,对于素数 2,生成的变量名为 --p2
。
接下来是一个 $mult
变量和第二个 @each
循环。这个循环用于生成素数相关的样式规则。
在每次循环迭代中,将当前素数赋值给变量 $prime
。然后,使用 :nth-child()
伪类选择器来为每个素数的倍数生成样式规则。这些样式规则将设置对应素数的CSS变量值,以便在页面中应用相应的样式。
最后,在mixin末尾,为传入的选择器生成一个总计数变量 --n
。这个变量通过将所有素数的CSS变量值乘积计算而来。
可以在CSS中使用此mixin来生成素数相关的变量和样式规则。例如:
@include primeCounter(".container", 20);
这会将 .container
元素内部的所有素数倍数设置为相应的CSS变量值,并生成总计数变量 --n
。
到此本文结束,使用CSS三角函数产生随机值并结合mixin来生成素数相关的变量和样式规则达成最终的效果,有兴趣的可以看看原文体验看看。
看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~
专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)