【CSS/Sass】box-shadowプロパティでパーティクルを生成してみる


※ 当ページには【広告/PR】を含む場合があります。
2019/09/12
2023/12/21
【Dart Sass対応】scssによる三角関数(cos,sin等)の取扱う方法 〜 基礎から応用まで

CSSはコード実装者の工夫で、面白い視覚的なスタイルを比較的簡単に付けることができるのも魅力の一つです。

この記事では、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要素の数だけ繰り返し */
        
という感じにするのが、オーソドックスな実装法になります。

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

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

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


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】CSS/Sassをこれから学びたい人のためのオススメ書籍&教材特集

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;
}
        
とすれば、

という感じになります。

一連の仕組みが今回の核となるテクニックです。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】CSS/Sassをこれから学びたい人のためのオススメ書籍&教材特集

パーティクル計算座標との連動

いくらでも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()などの三角関数等はCSS非対応で利用できませんでした。

未だに特殊な関数は、やはりSassで独自実装する必要があります。

詳しいアルゴリズムなどの解説は、
こちらのブログの記事(scssによる三角関数(cos,sin等)の取扱う)で説明しておりますが、下の「付録」パートにもsin()cos()の自作コード例を載せておきます。

合同会社タコスキングダム|蛸壺の技術ブログ
【Dart Sass対応】scssによる三角関数(cos,sin等)の取扱う方法 〜 基礎から応用まで

基礎的なscssのテクニックから解説して、実践的な三角関数の具体的な実装法までをまとめて説明しています。

実装に興味がありましたら、ご参考ください。

            
            .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);
}
        
これで複写した物だけが表示されます。

また、黒点だけでは色が地味でしたので、色相遷移させるビルドイン関数
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);
}
        
以下のように鮮やかな色相環が描けました。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】CSS/Sassをこれから学びたい人のためのオススメ書籍&教材特集

カスタムプロパティと連携して、パーティクル計算座標を設定する

先程の実装では、パーティクルの座標をSassの@functionから計算させていました。

繰り返しますが、Sassはプリプロセッサですので、ソースコードビルド後にCSSに解釈されるため、パーティクルの座標値は静的な値に限られます。

もしも、パーティクルの座標そのものをリアルタイムに動かしたい場合に、「動的」に数値を書き換える仕組みが必要になります。

現状、CSS側には動的に値を変化させる機能はないので、そういったことをやりたい場合、Javascriptコードとの連携が不可欠です。

それを可能とするのが、別の記事で説明していた
「カスタムプロパティ」というテクニックになります。

合同会社タコスキングダム|蛸壺の技術ブログ
【CSS/Sassで作るミニゲーム】CSS変数(カスタムプロパティ)とシンプルなJSコードでHTML要素をコントロール

ピュアなJavascriptだけで簡単なHTMLアプリを作る際に便利な「CSSカスタムプロパティ」の使い方

カスタムプロパティの基本操作はそちらの記事に譲るとしまして、まずは
Sassからカスタムプロパティを使うためのCSSスタイルを出力してみます。

上記の例と同じ要領で、
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>タグに仕込んでHTMLを開くと、

            
            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カスタムプロパティの組み合わせは非常に相性の良い組み合わせテクニックと言えます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】CSS/Sassをこれから学びたい人のためのオススメ書籍&教材特集

まとめ

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


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】CSS/Sassをこれから学びたい人のためのオススメ書籍&教材特集

付録〜sin関数とcos関数

※注意 ... この節の内容はCSSの組込関数にsincosが正式に加えられる以前のものです。

参考|sin()

既に自前で三角関数の計算アルゴリズムを実装する必要はありませんが、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);
}
        
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

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

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】CSS/Sassをこれから学びたい人のためのオススメ書籍&教材特集