box-shadowプロパティでパーティクルを生成してみる


2019/09/12

例えば、cssで定義した円の一つを複数個に複製、それぞれを任意の方向に平行移動させたものをパーティクルと呼ぶことにしましょう。

すると、大抵は、円の形状と、
transformプロパティを指定したcssスタイルをhtml要素に割り当てて、パーティクルを作成する方法が考えられます。

こちらのサイトの記事で紹介されておりますが、この方法の一例として取り上げさせていただくと、

htmlコードの部分で、円を
<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要素の数だけ繰り返し */
        
という感じになります。

どうしても
translate()を活用したら、それに対応させる1つの要素にスタイルを割り当て無ければならず、htmlとcssの2つのソースコードの整合性を保って、コーディングする感じになります。

スタイルをどの要素に割り当てているのかは直感的に分かりやすい反面、cssを修正したら、htmlも修正しないといけなくなるかもしれません。

これは保守性が悪い…そう考えると、cssだけでパーティクルを描画する要素が出来ればこれを使うことにこしたことはありません。


box-shadowプロパティのoffsetを利用する

今回のお話は、まさにスタイルされた要素の'影分身の術'的な小技です。

なお、
このやり方を応用をされている方がドット絵をbox-shadowだけで描かれており、Sassとの組み合わせで奥深いテクニックになっております。

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座標を配置させるのもつまらないので、
こちらのブログの記事(scssによる三角関数(cos,sin等)の取扱う)で説明させていただいた、三角関数sin()cos()を用いて同一円周上に配置させております。

なお、三角関数
sin()cos()は現在、Sassの標準関数ではありません。

実装に興味がありましたら、下のパートに
sin()cos()のコードを張ってあります。ご参考ください。

            
            .circle {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: black;
    box-shadow: generate_positions(30);
}
        
円要素のbox-shadowに、この関数を呼び出すと、

という感じに、円を描くパーティクルを発生させることが出来ました。


付録〜sin関数とcos関数

ここでは詳しくは解説しませんが、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);
}
        

見た目の微調整

先ほど描画したパーティクルだと、コピー元となった起点が表示されていました。

なので、透明にして消しておきましょう。

            
            .circle {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: transparent; // 透明に
    box-shadow: generate_positions(24);
}
        
これで、複写した物だけが表示されます。

また、黒点だけでは色が地味でしたので、色相遷移させる
ビルドイン関数`adjust-hue()`を利用して、三角関数の角度変数と連動させてみます。

            
            @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);
}
        
以下のように鮮やかな色相環が描けました。


まとめ

box-shadowプロパティとsassの組み合わせで、色々なデザイン的な応用が出来そうなフトコロの深さを感じました。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。