Sass, 相对亮度和牛顿法开方

学习

标题看似是三个毫无关系的概念, 但这次的博客主题改造计划将它们紧密地关联到了一起。

Sass

事情的起因是 Bulma 终于发布了 v1 版本,添加了对 CSS Variables 的支持, 于是我也顺势对我的博客主题进行了升级和改造,最大的变化是添加了更为现代的暗色模式,以及搜索功能, 还顺道清理了一下屎山,解决了几个隐藏的 bug 。

改造的过程比较顺利暂且不谈,但中间发生了一件让我很好奇的事情,那就是原来 Bulma 默认主题色的前景色是白色, 升级之后变成了黑色。一番搜索后,终于解决了问题,也引出了今天这篇博客。

首先要看一下 Bulma 里前景色是怎么算出来的,Bulma 的源代码是 Sass (Syntactically Awesome Stylesheets) 编写的。Sass 提供了一种相对友好的编写 CSS 的方式,用它定义的语法写最后翻译成 CSS 。 在这次更新中,Bulma 作者提到升级到了 Dart Sass,而 Sass 历史比较悠久,之前还有一些被废弃了的版本, 比如 Hugo 里面集成的 libsass。这些被废弃的 Sass 实现就是导致了今天这一切的罪魁祸首。

相对亮度

通过查看代码,发现前景色是通过计算背景色的相对亮度(relative luminance)来完成的。

这个指标的物理含义我没有去深究,我只知道 Web 内容无障碍指南 (WCAG) 2.1 给出了一个计算公式, 代入 RGB 值可以得到一个 0 到 1 之间的值,0 最暗,1 最亮。 Bulma 作者正是依据背景色的这个值的大小来确定前景色该取黑还是白。 第一个问题就出在计算公式中有一个计算一个值的 2.4 次幂的操作。

在老版本的 Bulma 中,作者用了一个已经被废弃了的 Sass 实现,主要表现在不支持很多常见的数学计算, 比如浮点数的乘方。也许 Sass 在设计初期也想象不到有这种需求,就好比网络领域 P4 也缺失很多东西, 因为没人觉得一台交换机需要这些乱七八糟的功能。

Bulma 作者实现了一个简单的解决方案,算不出 2.4 次方,用 2 次方来代替。而 Dart Sass 提供了比较完善 的数学模块,因此 Bulma v1 能够精确算出颜色的相对光亮度。 巧合的是这看似不大的差异却刚好体现在了作者选的主题色上,不然我也发现不了这个问题。

牛顿法开方

我本来根本不想碰 Sass 的,但是我的主题允许自定义主题色,因此这方面不得不实现。 第二个问题出现了,Hugo 虽然支持 Sass,但如果要用 Dart Sass 必须要用 node 安装依赖。 以我的性格,能简单尽可能简单,我只是想写一个函数而已,需要引入新的依赖吗?

那么有没有用 libsass 实现准确计算相对亮度的方法呢? 我最早的想法是至少用 2.5 次方近似别用 2, 参考 libsass 的一个 issue, 发现 2.5 和 2.4 其实没差,都只需要实现一下开整数次方就可以了。 也就是说实现它只需要一些简单的初高中数学知识。

首先,原问题也就是求 $ x^{2.4} = x^2 * x^{0.4} = x^2 * (x^{\frac{1}{5}})^2 $, 也就是说只需要实现一个开五次方的函数。

编程实现开整数次方感觉大多数计算机系学生应该都接触过,可以用二分法或者牛顿法。

牛顿法 简单来说是一种利用函数的导数迭代找函数零点的方法。 而求 $a$ 的 $m$ 次根也就是找 $f(x)=x^m - a$ 的零点。

迭代公式是这样

$$ x_{n+1} = x_n - \frac{f(x_n)}{f’(x_n)} $$

代入函数可以得到

$$ x_{n+1} = \frac{1}{m}\left((m-1)x_n+\frac{a}{x_n^{m-1}}\right) $$

当然,牛顿法实际上还要复杂一些,应用也非常广泛, 但在我们这个非常简单的问题上理解到这个程度就足够了。

还好对于这个问题 libsass 只是乘方不太行,其他的一些加减乘除取整的计算还是支持的。 于是可以写出以下的 SCSS 代码,SCSS 我理解是 Sass 的语法上进行了一些修改,更接近 CSS 了。 测试了一些颜色的输出,感觉应该没啥问题。

// modify from bulma v0.9.4 and https://github.com/sass/sass/issues/684
// because using dart sass with hugo introduces extra dependency

@function pow($number, $exp) {
  $value: 1;
  @if $exp > 0 {
    @for $i from 1 through $exp {
      $value: $value * $number;
    }
  } @else if $exp < 0 {
    @for $i from 1 through -$exp {
      $value: $value / $number;
    }
  }
  @return $value;
}

@function nth-root-newton($A, $guess, $n) {
  @return (1 / $n) * (($n - 1) * $guess + ($A/pow($guess, $n - 1)));
}

@function nth-root($number, $n, $precision: 5) {
  $guess: 2.7;
  $previous-guess: 0;

  // While precision has not been met, keep guessing
  @while round($previous-guess * pow(10, $precision)) != round($guess * pow(10, $precision)) {
    $previous-guess: $guess;
    $guess: nth-root-newton($number, $guess, $n);
  }

  @return $guess;
}

@function pow2dot4($number) {
  // pow(x, 2.4) = pow(x, 2) * pow(pow(x, 1/5), 2)
  @return pow($number, 2) * pow(nth-root($number, 5), 2)
}

@function luminance($color) {

  @if type-of($color) != 'color' {
    @return 0.55;
  }
  $color-rgb: ('red': red($color),'green': green($color),'blue': blue($color));
  @each $name, $value in $color-rgb {
    $adjusted: 0;
    $value: $value / 255;
    @if $value < 0.03928 {
      $value: $value / 12.92;
    } @else {
      $value: ($value + .055) / 1.055;
      $value: pow2dot4($value);
    }
    $color-rgb: map-merge($color-rgb, ($name: $value));
  }
  @return (map-get($color-rgb, 'red') * .2126) + (map-get($color-rgb, 'green') * .7152) + (map-get($color-rgb, 'blue') * .0722);
}

顺便贴一下我的主题如何自定义主题色。CSS Variables 用着还是舒服啊,暗色模式的切换变得非常丝滑。 非常感谢 Bulma 这个项目,既符合我审美又很简单,我从大学用到了现在。 但是现在 Web 前端的工具链变化得越来越快,感觉 Bulma 的社区不如我上大学那会儿活跃了。

{{ if .Site.Params.primaryColor }}
  $primary: {{ .Site.Params.primaryColor }};
  $bg-is-light: luminance($primary) > 0.55;

  :root {
    --bulma-primary: #{$primary};
    --bulma-primary-h: #{hue($primary)};
    --bulma-primary-s: #{saturation($primary)};
    --bulma-primary-l: #{lightness($primary)};

    @if $bg-is-light {
      --bulma-primary-invert-l: var(--bulma-primary-05-l);
    } @else {
      --bulma-primary-invert-l: var(--bulma-primary-100-l);
    }
  }
{{ end }}

总结

虽然没啥技术含量,但整个过程感觉还是挺有意思的,于是又水了一篇博客,顺便给我的 Hugo 博客主题打个广告, 有可能是最先跟进 Bulma 更新的 Hugo 主题。