カテゴリー
【Dart Sass対応】scssによる三角関数(cos,sin等)の取扱う方法 〜 基礎から応用まで
※ 当ページには【広告/PR】を含む場合があります。
2020/03/29
2021/06/07

nodejsでSassを扱いたい方向けには、
node-sassを使えば、以下のコマンドでお手元の環境にsassを直ぐにでも構築できます。
$ npm install node-sass
#OR
$ yarn add node-sass
以前以下のリンクでも特集しましたが、既にnode-sass(libsass)はSass公式から推奨されないライブラリ扱いになってしました。
既にnodejsユーザーでDart Sassへお引越しした方は、このブログの首題でもあった三角関数(sin/cos/tan等)が自作せずとも組込み関数として利用できるようになりましたが、袂を分かれたとはいえnode-sassも独自の実装とメンテナンスが今も継続しています。
node-sassユーザー向けとして以下で三角関数の実装方法を説明していきます。
またDart SassユーザーにとってもSassを用いた数学関数の実装方法の何かしらの参考になると思います。
Scssで独自関数を扱うための基本
まずはsassを用いた三角関数を実装して行く前に、基礎的な知識をざっとおさらいしておきます。
ローカル変数 ($)
独自の変数を定義したい場合、
$
$str: 'a strong guy';
$val: 1;
という用法で、
:
$str: 'I am ' + $str; // I am a strong guy
$val: $val * 2; // 2
というかんじに利用します。
@function (関数)
何らかの値を返したい場合は、独自の@functionを定義する必要があります。 例えば、円周率を返す関数を定義すると、
@function pi() {
@return 3.14159265359;
}
と定義できます。 引数をとる関数を定義する場合は、
@function add_one($num) {
@return $num + 1;
}
sassでは、引数に明示に型を記述することはできないので、注意が必要です。 実際には、変数の型(number, string, list, ...)など気を付けないと、演算時に思わぬ落とし穴にハマることがあります。
なお、
関数を使うときの呼び出し
上の関数を呼び出してみます。
$init_val: 1;
$init_val: add_one($init_val); // 2
$init_val: add_one($init_val); // 3
$init_val: add_one($init_val); // 4
$init_val: $init_val / $init_val; // 1
$init_val: $init_val * pi(); // 3.14159265359
$init_val: $init_val - pi(); // 0
$init_val: $init_val + pi(); // 3.14159265359
$init_val: $init_val % 3; // 0.14159265359
上の用に、関数を定義した後に呼び出して使うことが可能です。
配列 (list)
sassでは配列ライクな
List
[]
()
基本的な用法を以下に示します。
$hoge_list: (0, 1, 2, 3, 4);
$len: length($hoge_list); // 5
$val: nth($hoge_list, 1); // 0
$val: nth($hoge_list, 2); // 1
$val: nth($hoge_list, 3); // 2
$val: nth($hoge_list, 4); // 3
$val: nth($hoge_list, 5); // 4
このコードでは
$hoge_list
length
また、先頭からの要素順で値を取得するビルドイン関数
nth
@for
上の配列を
@for
$hoge_list: (0, 1, 2, 3, 4);
$val: 0;
@for $i from 1 through length($hoge_list) {
$val: nth($hoge_list, $i);
}
によって、繰り返し処理ができました。 なお、ループ内(波括弧
{}
$val
@if
sassでは、if文も備わっているので、以下の様に条件分岐を記述できます。
$result: '';
$mode: 1;
@if $mode == 0 {
$result: 'Good';
} @else if $mode == 1 {
$result: 'Not bad';
} @else {
$result: 'Worst';
}
上のソースでは、
$result
@debug
sassを開発して行く場合、せめてjsいうところの
console.log
そんな時に使うのが、ビルドイン
@debug
こいつはどこにでもおけますので、デバック中に気になった箇所に挿入しましょう。 すると、ビルドの際にターミナル上に出力される仕様です。
$misterous_val: 'Unknown';
// ...
@debug $misterous_val; // Unknown
実装編その1 〜 テイラー展開法による三角関数を作る
前節までは、sassでの基本構文・テクニックをおさらいしました。 それではそれらを用いてSassコードに三角関数を実装して行きます。
最初に三角関数の計算アルゴリズムで、パッと思い浮かぶのはテイラー展開による求解です。
とりあえず、
テイラー展開式とは
数学使うのなんて久しぶりな方にざっとテイラー展開がどんなのだったかおさらいです。
Eq. (1)
Eq. (2)
ということで、これら式(1)、式(2)とも、計算を実行するには、指数計算のパート(
指数計算するための関数
この関数は整数をとる指数計算なので、悩む必要はないでしょう。 以下のようにズバッと関数を書きます。
@function pow($bs, $exp) {
$val: $bs;
@if $exp > 1 {
@for $i from 2 through $exp {
$val: $val * $bs;
}
} @else if $exp < 1 {
@for $i from 0 through -$exp {
$val: $val / $bs;
}
}
@return $val;
}
なお
階乗の計算
こちらも上と同じように特に留意する点もなく以下のように書き下させます。
@function fact($num) {
$val: 1;
@if $num > 0 {
@for $i from 1 through $num {
$val: $val * $i;
}
}
@return $val;
}
角度からラジアンへの変換
上記の式(1)、式(2)とも変数はラジアンをとりますが、独自関数として呼び出す際には、角度表示で使う方が感覚的な面で利便性が良いのが確かです。 そんな時は、sassのビルドイン関数
unit()
@function pi() {
@return 3.14159265359;
}
@function rad($angle) {
$unit: unit($angle);
$val: $angle / (1 + 0 * $angle);
// 変数に角度(deg)が渡されていると、ラジアンに変換
// それ以外は、問答無用でラジアン
@if $unit == "deg" {
$val: $val / 180 * pi();
}
@return $val;
}
三角関数の計算
以上をもとに三角関数のテイラー展開式を実装しようと思います。
$accuracy: 10; // 計算精度
@function sin($angle) {
$theta: rad($angle);
$sin: $theta;
@for $n from 1 through $accuracy {
$sin: $sin + pow(-1, $n) / fact(2 * $n + 1) * pow($a, 2 * $n + 1);
}
@return $sin;
}
@function cos($angle) {
$a: rad($angle);
$cos: 1;
@for $n from 1 through $accuracy {
$cos: $cos + pow(-1, $n) / fact(2 * $n) * pow($a, 2 * $n);
}
@return $cos;
}
それでは関数の検証のため、0°から90°の範囲で10°ずつずらして計算値の方を出してみましょう。
// ...
@for $s1 from 0 through 9 {
$angle: 10deg * $s1;
@debug $angle, sin($angle), cos($angle);
}
// ...
出力結果は…
DEBUG: 0deg, 0, 1
DEBUG: 10deg, 0.1736481777, 0.984807753
DEBUG: 20deg, 0.3420201433, 0.9396926208
DEBUG: 30deg, 0.5, 0.8660254038
DEBUG: 40deg, 0.6427876097, 0.7660444431
DEBUG: 50deg, 0.7660444431, 0.6427876097
DEBUG: 60deg, 0.8660254038, 0.5
DEBUG: 70deg, 0.9396926208, 0.3420201433
DEBUG: 80deg, 0.984807753, 0.1736481777
DEBUG: 90deg, 1, 0
と、それらしい値がテーラー展開法で得られました。
実装編その2 〜 CORDICアルゴリズムで三角関数を作る
一通りは上記までの内容で三角関数が実装できたのですが、個人的な感想では、
sassでゴリゴリとテイラー展開やらせるのは思うほど実用性がないんではないか…
当初は、Sassはプリプロセッサなのでどんなに前処理が重くても、最終生成物のcssにきちんと計算した座標が書き込めてれば問題ないんではないか、と思っておりました。 でも、cssなどでパーティクルなど描画座標を計算させればさせるほど、何かビルド中に問題があるようで…生成されたcssコードが壊れていて描画できないブラウザでエラーを吐いてしまうことも...。
深くプリプロセッサ側の問題を追求しませんでしたが、このテイラー展開法をそのまま使うのは厳しいそうだなぁと判断し、また別の方法を模索します。
という訳で、もっと実用的で軽量な別の計算アルゴリズムとして、
ちょっとだけCORDICアルゴリズムの解説
いきなり話は逸れますが、私が高校生の頃なんて行列が数学の必修としてあったので、
さて、CORDICアルゴリズムを理解する上で、個人的に行列論には昔からかなりのこだわりがありやはり'行列'を使って厳密に…といきたいですが、
なお、
いきなりですが、複素平面上で得られる結論の式だけ、書き出させていただきます。
複素平面上のある点
Eq. (3)
一方で、時計回りの回転が次の式で求めることができます。
Eq. (4)
ここでの
Sassでビット演算できるのか?
現在のところ、Sass標準で残念ながらビット演算はサポートされていません。
少し古いプロジェクトで
ということは、ビット演算を使わずもうただの数値の割り算をするのと変わらないのなら、
'2で割る'
'自分の好みの数で割って'
改良CORDICの方法は後述します。
CORDICアルゴリズムの実装
それでは、奇を衒う前に、オーソドックスなCORDICアルゴリズムをsassで実装していきましょう。 まずは、既知として利用できる数値を連想配列で準備しておきます。
要素のキーの名前を
$cordic_dataset: (
0: (45, 1),
1: (26.56505118, 2),
2: (14.03624347, 4),
3: (7.125016349, 8),
4: (3.576334375, 16),
5: (1.789910608, 32),
6: (0.8951737102, 64),
7: (0.4476141709, 128),
8: (0.2238105004, 256),
9: (0.1119056771, 512),
10: (0.05595289189, 1024),
11: (0.02797645262, 2048),
12: (0.01398822714, 4096),
13: (0.006994113675, 8192),
14: (0.003497056851, 16384),
15: (0.001748528427, 32768),
16: (0.0008742642137, 65536),
17: (0.0004371321069, 131072),
18: (0.0002185660534, 262144),
);
$s_all: 1.646760258;
また、今回の計算コード例で言えば、データセット
$cordic_dataset
Eq. (5)
とできるので、これを定数として与えておきます。
範囲制限を考慮した補正
CORDICアルゴリズムの弱点として、0°から90°の間での三角関数の値を返すことになります(表の
あらゆる角度
@function check_domain($angle) {
$reduced_angle: $angle % 360;
$result: (0, 0, 0);
$constraint_angle: $reduced_angle % 90;
@if ($reduced_angle >= 0) and ($reduced_angle < 90) {
$result: ($constraint_angle, 1, 1);
} @else if ($reduced_angle >= 90) and ($reduced_angle < 180) {
$result: (90 - $constraint_angle, -1, 1);
} @else if ($reduced_angle >= 180) and ($reduced_angle < 270) {
$result: ($constraint_angle, -1, -1);
} @else {
$result: (90 - $constraint_angle, 1, -1);
}
@return $result;
}
上記の関数を用いれば、任意の角度を、1番目の要素で0°から90°の範囲におとし込めた値、2,3番目の要素に各象限を表す2つの極性を割り当てた配列を返します。
CORDICコード
それでは、CORDICアルゴリズムを実装してみます。
@function cossintan($angle, $mode) {
$modifier: check_domain($angle);
$p_c: 1 / $sum_all;
$q_c: 0;
$theta: 0;
$p_next: 0;
$q_next: 0;
$result: 0;
@for $n from 1 through length($cordic_dataset) {
$coeffecient: nth(nth($cordic_dataset, $n), 2);
@if nth($modifier, 1) > $theta {
$p_next: $p_c - ($q_c / nth($coeffecient, 2));
$q_next: $q_c + ($p_c / nth($coeffecient, 2));
$theta: $theta + nth($coeffecient, 1);
} @else {
$p_next: $p_c + ($q_c / nth($coeffecient, 2));
$q_next: $q_c - ($p_c / nth($coeffecient, 2));
$theta: $theta - nth($coeffecient, 1);
}
$p_c: $p_next;
$q_c: $q_next;
}
@if $mode == 0 {
$result: $p_c * nth($modifier, 2); // cos
} @else if $mode == 1 {
$result: $q_c * nth($modifier, 3); // sin
} @else {
$result: $q_c / $p_c * nth($modifier, 2) * nth($modifier, 3); // tan
}
@return $result;
}
ちなみに、この関数の第2引数で、0指定で
cos
sin
tan
検証
どこかの要素のスタイルにでも、以下のテストを貼り付けます。
@for $t from 0 through 18 {
$angle: 10 * $t;
@debug unquote('angle:') $angle, unquote('cos:') cossintan($angle, 0), unquote('sin:') cossintan($angle, 1);
}
ビルドすると、コンソールに計算結果が出力されます。
DEBUG: angle: 0, cos: 1.0000000001, sin: -0.0000014786
DEBUG: angle: 10, cos: 0.9848072373, sin: 0.1736511027
DEBUG: angle: 20, cos: 0.9396914558, sin: 0.3420233442
DEBUG: angle: 30, cos: 0.8660238404, sin: 0.500002708
DEBUG: angle: 40, cos: 0.7660461494, sin: 0.6427855763
DEBUG: angle: 50, cos: 0.6427855763, sin: 0.7660461494
DEBUG: angle: 60, cos: 0.500002708, sin: 0.8660238404
DEBUG: angle: 70, cos: 0.3420233442, sin: 0.9396914558
DEBUG: angle: 80, cos: 0.1736511027, sin: 0.9848072373
DEBUG: angle: 90, cos: -0.0000014786, sin: 1.0000000001
DEBUG: angle: 100, cos: -0.1736511027, sin: 0.9848072373
DEBUG: angle: 110, cos: -0.3420233442, sin: 0.9396914558
DEBUG: angle: 120, cos: -0.500002708, sin: 0.8660238404
DEBUG: angle: 130, cos: -0.6427855763, sin: 0.7660461494
DEBUG: angle: 140, cos: -0.7660461494, sin: 0.6427855763
DEBUG: angle: 150, cos: -0.8660238404, sin: 0.500002708
DEBUG: angle: 160, cos: -0.9396914558, sin: 0.3420233442
DEBUG: angle: 170, cos: -0.9848072373, sin: 0.1736511027
DEBUG: angle: 180, cos: -1.0000000001, sin: 0.0000014786
...テイラー展開の解法と比べて、計算精度のほどはお察しですが、計算負荷も少なく軽量になった分実用的かと思います。 cssはhtmlデザインの為の言語ですので、0.000001ピクセルの違いが気にならない限りは、さほどの厳密性は見過ごしてあげても良いと思います。
実装編その3 〜 改良版!CORDICアルゴリズムで三角関数を作る
ここまでで、CORDICアルゴリズムで三角関数の解を求められるようなプログラムをsassで実装しましたが、更にやりながら個人的に気になった点を改良したCORDICコードもご説明しましょう。 なお、改良CORDICアルゴリズムでちょっとだけ計算ステップが早くなることが期待されます。
式をちょっと修正
繰り返しになりますが、Sassではビット演算が標準で利用できません。なのでそのままだと、従来のCORDICアルゴリズムでの
ここでは、試しに
Eq. (6)
これにより、従来のCORDICアルゴリズムよりも細かい刻みの回転子が得られました。 この式をベースにコーディングをしていきます。
Sassの@while文を利用する
この
@while
@for
先ずはあらかじめ保持すべきデータセットを以下のような連想配列で用意していきます。 今回の改良コードでは
@while
よって、データセット内に規格化定数
$cordic_dataset: (
0: ( 45, 1, 1.414213562),
1: ( 26.56505118, 2, 1.118033989),
2: ( 6.340191746, 9, 1.006153904),
3: ( 0.8951737102, 64, 1.000122063),
4: ( 0.09167316899, 625, 1.00000128),
5: (7.368284362e-3, 7776, 1.000000008),
6: (4.870060902e-4, 117649, 1),
7: (2.732075668e-5, 2097152, 1),
8: (1.331013796e-6, 43046721, 1),
9: (5.729577951e-8, 1e9, 1),
10: ( 2.209e-9, 2.5937425e10, 1)
);
このデータをみると、
それでは前回のCORDICメソッドを改良するかたちで、各ステップ$$\theta_c
@function cordic_2($angle, $mode) {
$modifier: check_domain($angle);
$p_c: 1;
$q_c: 0;
$p_next: $p_c;
$q_next: $q_c;
$result: 0;
$delta: nth($modifier, 1);
$stage_index: 1;
$upper_limit: length($cordic_dataset);
$resolution: 1e-8;
@while $delta > $resolution {
$coefficient: nth(nth($cordic_dataset, $stage_index), 2);
@if $delta >= nth($coefficient, 1) {
$p_next: ($p_c - $q_c / nth($coefficient, 2)) / nth($coefficient, 3);
$q_next: ($q_c + $p_c / nth($coefficient, 2)) / nth($coefficient, 3);
$delta: $delta - nth($coefficient, 1);
} @else {
$stage_index: $stage_index + 1;
@if $stage_index > $upper_limit {
$delta: $resolution;
}
}
$p_c: $p_next;
$q_c: $q_next;
}
@if $mode == 0 {
$result: $p_c * nth($modifier, 2); // cos
} @else if $mode == 1 {
$result: $q_c * nth($modifier, 3); // sin
} @else {
$result: $q_c / $p_c * nth($modifier, 2) * nth($modifier, 3); // tan
}
@return $result;
}
ちなみにさらなる精度を求められる場合には、データセットのステップ毎の値を増やす必要がありますのでその点はご留意ください。
この関数を実行してみますと、
@for $s1 from 0 through 9 {
$angle: 10 * $s1;
@debug unquote('angle:') $angle, unquote('cos:') cordic_2($angle, 0), unquote('sin:') cordic_2($angle, 1);
}
実行結果は
DEBUG: angle: 0, cos: 1, sin: 0
DEBUG: angle: 10, cos: 0.9848077558, sin: 0.173648178
DEBUG: angle: 20, cos: 0.9396926244, sin: 0.3420201445
DEBUG: angle: 30, cos: 0.8660254039, sin: 0.4999999998
DEBUG: angle: 40, cos: 0.7660444442, sin: 0.6427876103
DEBUG: angle: 50, cos: 0.6427876113, sin: 0.7660444447
DEBUG: angle: 60, cos: 0.5000000017, sin: 0.8660254065
DEBUG: angle: 70, cos: 0.3420201443, sin: 0.9396926231
DEBUG: angle: 80, cos: 0.1736481781, sin: 0.9848077543
DEBUG: angle: 90, cos: 0, sin: 1.0000000005
と、従来のCORDICよりも、少ないステップ数で精度もグッと良くなって出力されます。 計算は少し重くなっておりますが精度とのトレードオフです。
まとめ
以上、テーラー展開法による三角関数の求解アルゴリズムと、CORDICアルゴリズムでの求解法を2つに関してSassで実装する方法をご紹介しました。
何はともあれ、LibSass単独で三角関数を使えるようになったので、色々とデザイン的な応用に重宝できると思います。
また、
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー