カテゴリー
【CSS/Sass】box-shadowプロパティでパーティクルを生成してみる
※ 当ページには【広告/PR】を含む場合があります。
2019/09/12
2023/12/21
CSSはコード実装者の工夫で、面白い視覚的なスタイルを比較的簡単に付けることができるのも魅力の一つです。
この記事では、CSS上で定義した一つの円から複数個に複製し、それぞれを任意の方向に平行移動させたものを「パーティクル」と呼ぶことにしましょう。
正攻法だと、円の形状に
<li>
<ul>
<li></li>
<li></li>
<li></li>
<!-- ... -->
<!-- とにかく欲しいだけli要素を記述 -->
<!-- ... -->
<li></li>
</ul>
そして、scssをビルドした後に生成されたcssファイルの中身で、
li:nth-child(1) {
/* 1つ目の円 = 1番目のli */
transform: translate(0px, 0px);
}
li:nth-child(2) {
/* 2つ目の円 = 2番目のli */
transform: translate(100px, 100px);
}
li:nth-child(3) {
/* 3つ目の円 = 3番目のli */
transform: translate(200px, 200px);
}
/* 以下、li要素の数だけ繰り返し */
という感じにするのが、オーソドックスな実装法になります。
しかしどうしても
transformプロパティ
スタイルをどの要素に割り当てているのかは直感的に分かりやすい反面、cssを修正したらそれに影響を受けるhtmlも修正しないといけなくなります。
これはどうもコード保守性が悪い…そう考えると、cssだけでパーティクルを描画する要素が出来ればこれを使うことにこしたことはありません。
box-shadowプロパティのoffsetを利用する
今回のお話は、まさにスタイルされた要素の
なお、
box-shadowの基本
本題に入る前に、この小技の根底の部分を理解してもらうため、基礎的なところから始めます。
html部分は、以下のようにします。
<div class="circle"></div>
最初に元となる円を一つ用意しましょう。
下のスタイルを割り当てます。
.circle {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
}
下のような黒ポチが一つできるはずです。
これを基点に用います。
通常なら、
box-shadow
ですがこの場合、以下のようにcssコードに'box-shadow'プロパティを加えることでオリジナルと同じ形状を複写・平行移動することが可能です。
.circle {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
box-shadow: 25px 0px black;
}
すると、box-shadowによる複製の方が、x軸方向に'+25px'、y軸方向に'0px'移動して、上手くいくと以下のように見えるでしょう。
box-shadowによる複写の真骨頂は、複数の複製が最小の記述で同時に行える点にあります。
先ほどのコードから更に、もう一つ複写移動してみます。
.circle-b3 {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
box-shadow: 25px 0px black,
50px 0px black;
}
すると、更にx軸方向に'+50px'、y軸方向に'0px'移動させた円が追加されています。
box-shadowプロパティは、コンマ(,)切りで移動量と配色指定を続けることで、連続複写に対応しています。
このことで
translate()
複写数を増やそうと思えば、ソースコードの変更点をcssコード内だけで完結させることが可能です。
.circle {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
box-shadow: 25px 0px black,
50px 0px black,
0px 25px black,
25px 25px black,
50px 25px black,
0px 50px black,
25px 50px black,
50px 50px black;
}
とすれば、
という感じになります。
一連の仕組みが今回の核となるテクニックです。
パーティクル計算座標との連動
いくらでもbox-shadowで増やせるとはいえ、座標の一つ一つをハードコーディングしていくのは厳しいです。
そこでSassの
@function
@function generate_positions($iter) {
$size: 4px; // スケール倍率とピクセル単位を記述
$r: 40; // 半径(単位無し)
$xoffset: 50; // 半径(単位無し)
$yoffset: 50; // 半径(単位無し)
$result: '';
@for $t from 1 through $iter {
$sep: ',';
@if $t == 1 {
$sep: '';
}
// 円周上の座標を設定
$angle: 360 / $iter * $t;
$x: $r * cos($angle) + $xoffset;
$y: $r * sin($angle) + $yoffset;
$color: black;
$result: $result + '#{$sep} #{$x * $size} #{$y * $size} #{$color}';
}
@return unquote($result);
}
この関数は先ほどまで手で入力していた座標と配色を記述した文字列部分を代わりにまとめて吐きだしてくれるだけのシンプルな関数です。
ただ直線的にx座標、y座標を配置させるのもつまらないので、CSS組込の三角関数
sin()
cos()
なお、この記事の執筆開始当時は、
sin()
cos()
未だに特殊な関数は、やはりSassで独自実装する必要があります。
詳しいアルゴリズムなどの解説は、
実装に興味がありましたら、ご参考ください。
.circle {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: black;
box-shadow: generate_positions(30);
}
円要素のbox-shadowに、この
generate_positions
という感じに、円を描くパーティクルを発生させることが出来ました。
見た目の微調整〜パーティクルに配色する
先ほど描画したパーティクルだと、コピー元となった起点のプロパティ値がそのまま表示されていました。
なので、オリジナルの要素は透明にして消しておきましょう。
.circle {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: transparent; // 透明に
box-shadow: generate_positions(24);
}
これで複写した物だけが表示されます。
また、黒点だけでは色が地味でしたので、色相遷移させるビルドイン関数
@function colorshift($input_color, $input_phase) {
$output_color: adjust-hue($input_color, $input_phase);
@return $output_color;
}
@function generate_positions($iter) {
$size: 4px;
$r: 40;
$xoffset: 50;
$yoffset: 50;
$result: '';
@for $t from 1 through $iter {
$sep: ',';
@if $t == 1 {
$sep: '';
}
$angle: 360 / $iter * $t;
$x: $r * cos($angle) + $xoffset;
$y: $r * sin($angle) + $yoffset;
// $color: black;
$color: colorshift( hsl(292, 89, 51), $angle); // 色相偏移させる
$result: $result + '#{$sep} #{$x * $size} #{$y * $size} #{$color}';
}
@return unquote($result);
}
以下のように鮮やかな色相環が描けました。
カスタムプロパティと連携して、パーティクル計算座標を設定する
先程の実装では、パーティクルの座標をSassの
@function
繰り返しますが、Sassはプリプロセッサですので、ソースコードビルド後にCSSに解釈されるため、パーティクルの座標値は静的な値に限られます。
もしも、パーティクルの座標そのものをリアルタイムに動かしたい場合に、「動的」に数値を書き換える仕組みが必要になります。
現状、CSS側には動的に値を変化させる機能はないので、そういったことをやりたい場合、Javascriptコードとの連携が不可欠です。
それを可能とするのが、別の記事で説明していた
カスタムプロパティの基本操作はそちらの記事に譲るとしまして、まずは
Sass
上記の例と同じ要領で、
box-shadow
generate_positions
@function generate_positions($iter) {
$result: '';
@for $t from 1 through $iter {
$sep: ',';
@if $t == 1 {
$sep: '';
}
$x: --xcoor_#{$t};
$y: --ycoor_#{$t};
$color: transparent;
$result: $result + '#{$sep} var(#{$x},0) var(#{$y},0) #{$color}';
}
@return unquote($result);
}
.circle {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: black;
//30点のパーティクルを生成
box-shadow: generate_positions(30);
}
これをcssコードにコンパイルすると、
.circle {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: transparent;
box-shadow: var(--xcoor_1,0) var(--ycoor_1,0) black, var(--xcoor_2,0) var(--ycoor_2,0) black, var(--xcoor_3,0) var(--ycoor_3,0) black, var(--xcoor_4,0) var(--ycoor_4,0) black, var(--xcoor_5,0) var(--ycoor_5,0) black, var(--xcoor_6,0) var(--ycoor_6,0) black, var(--xcoor_7,0) var(--ycoor_7,0) black, var(--xcoor_8,0) var(--ycoor_8,0) black, var(--xcoor_9,0) var(--ycoor_9,0) black, var(--xcoor_10,0) var(--ycoor_10,0) black, var(--xcoor_11,0) var(--ycoor_11,0) black, var(--xcoor_12,0) var(--ycoor_12,0) black, var(--xcoor_13,0) var(--ycoor_13,0) black, var(--xcoor_14,0) var(--ycoor_14,0) black, var(--xcoor_15,0) var(--ycoor_15,0) black, var(--xcoor_16,0) var(--ycoor_16,0) black, var(--xcoor_17,0) var(--ycoor_17,0) black, var(--xcoor_18,0) var(--ycoor_18,0) black, var(--xcoor_19,0) var(--ycoor_19,0) black, var(--xcoor_20,0) var(--ycoor_20,0) black, var(--xcoor_21,0) var(--ycoor_21,0) black, var(--xcoor_22,0) var(--ycoor_22,0) black, var(--xcoor_23,0) var(--ycoor_23,0) black, var(--xcoor_24,0) var(--ycoor_24,0) black, var(--xcoor_25,0) var(--ycoor_25,0) black, var(--xcoor_26,0) var(--ycoor_26,0) black, var(--xcoor_27,0) var(--ycoor_27,0) black, var(--xcoor_28,0) var(--ycoor_28,0) black, var(--xcoor_29,0) var(--ycoor_29,0) black, var(--xcoor_30,0) var(--ycoor_30,0) black;
}
と展開されることになります。
これで
box-shadow
あとはJavascript側でパーティクルを煮るなり焼くなりお好きなテイストで料理することができるようになりました。
例えば、以下のようなjsスクリプトを
<script>
const isSupported = window.CSS && window.CSS.supports && window.CSS.supports('--a', 0);
if (isSupported) {
const div = document.getElementById("...要素のID名...");
const _size= 4;
const _r = 40;
const _xoffset = 50;
const _yoffset = 50;
for (let i=0;i<30;i++) {
const _angle = 2*Math.PI/30*i;
const _x = Number(_size * (_r * Math.cos(_angle) + _xoffset));
const _y = Number(_size * (_r * Math.sin(_angle) + _yoffset))
div.style.setProperty(`--xcoor_${i+1}`, `${_x}px`);
div.style.setProperty(`--ycoor_${i+1}`, `${_y}px`);
}
}
else {
console.log('お使いのブラウザはカスタムプロパティ非対応です');
}
下のようなHTMLなります。
カスタムプロパティの優れている点として、jsスクリプトからHTML要素を操作する際に、CSS側の変数だけに注意を払えば良いだけなので、とても簡素なコードにできることにあります。
例えば以下のようなjsスクリプトで、パーティクルの座標値を動的に変化できるようにしてみましょう。
const isSupported = window.CSS && window.CSS.supports && window.CSS.supports('--a', 0);
if (isSupported) {
const div = document.getElementById("...要素のID名...");
const _size= 4;
const _r = 40;
const _xoffset = 50;
const _yoffset = 50;
let count = 0;
setInterval(() => {
for (let i=0;i<30;i++) {
const _angle = 2*Math.PI/30*i;
const _delay = 2*Math.PI/90*count;
if (_delay >= 2*Math.PI) { count = 0; }
const _x = Number(_size * (_r * Math.cos(_angle - _delay) + _xoffset));
const _y = Number(_size * (_r * Math.sin(_angle - _delay) + _yoffset))
div.style.setProperty(`--xcoor_${i+1}`, `${_x}px`);
div.style.setProperty(`--ycoor_${i+1}`, `${_y}px`);
}
count++;
}, 500);
}
else {
console.log('お使いのブラウザはカスタムプロパティ非対応です');
}
これを適用すると、以下のように動くHTMLとなります。
見ての通りで、
box-shadow
カスタムプロパティ
まとめ
box-shadowプロパティとsassの組み合わせで、色々なデザイン的な応用が出来そうなフトコロの深さを感じました。
付録〜sin関数とcos関数
※注意 ... この節の内容はCSSの組込関数に
sin
cos
既に自前で三角関数の計算アルゴリズムを実装する必要はありませんが、Sassでの独自実装の一例としては興味深くもあるので、sin関数とcos関数の実装コードの例を記録として残しておきます。
$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;
@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;
}
@function cossintan($angle, $mode) {
$modifier: check_domain($angle);
$p_c: 1 / $s_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;
}
@function sin($angle) {
@return cossintan($angle, 1);
}
@function cos($angle) {
@return cossintan($angle, 0);
}
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー