【Dart Sass対応】URLエンコード形式のsvg画像でカスタムカーソルを試してみる


2020/11/10

前回の記事では、画像をbase64形式に変換してカスタムしたマウスカーソルを実装することをご紹介しました。

今回はURLエンコード形式のインラインsvgを利用したカスタムカーソルの作り方と注意点を解説します。


まずは注意点

ずばり、SassではURLエンコード形式のインラインsvgでのアニメーションの利用はブラウザごとに事情が異なるので、ユニバーサルデザインのウェブサイトでの使用は非推奨であるといえます。

理由としては
正しく動作するかはクライアント側の利用しているブラウザ次第であり、意図としない挙動なってしまう恐れがあるからで、プロダクトとしてリリースされるようなアプリには使用非推奨です。

とはいっても、svg画像は本来htmlを介して利用すべきであり、
cssのビルドインメソッドであるurl()で色々な複雑な処理はしないほうが良いよっ…というだけのお話程度の注意です。

参考までに興味があれば、
urlメソッドの詳細をご確認ください。

また、どうしてもインラインsvgで、アニメーションなどの複雑なことをさせたいんや!という情熱のある方は、
ここいらのサイトを参照して、ヒントを得てみるのもいいかもしれません...

いずれにしても、凝りに凝ったsvg要素はcss(scss)ではなくて、htmlの方へ実装しましょう。


svg要素のURLエンコード

とはいえそんなに複雑なsvg要素でなければcssのurlメソッドを利用できないこともありません。

早速マウスカーソル用の画像で検証してみましょう。

インラインsvgを準備

まずは、どのようなインライン画像でもよいですが、アイコンチックなものを以下のサイトから拝借しましょう。

Material Design Icons

以下のように、一例として、
ball > billiardsのアイコンのインラインsvgを利用させていただきましょう。

合同会社タコスキングダム|蛸壺の技術ブログ

たとえば適当なsvg画像をダウンロードしてみると、

            
            <svg style="width:24px;height:24px" viewBox="0 0 24 24">
    <path fill="#000000" d="M11,13H13V15H11M11,9H13V11H11M11,17H13A2,2 0 0,0 15,15V13.5A1.5,1.5 0 0,0 13.5,12A1.5,1.5 0 0,0 15,10.5V9A2,2 0 0,0 13,7H11A2,2 0 0,0 9,9V10.5A1.5,1.5 0 0,0 10.5,12A1.5,1.5 0 0,0 9,13.5V15A2,2 0 0,0 11,17M12,1C5.92,1 1,5.92 1,12C1,18.08 5.92,23 12,23C18.08,23 23,18.08 23,12C23,5.92 18.08,1 12,1M12,19A7,7 0 0,1 5,12A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19Z" />
</svg>
        
のようになっているはずです。

しかしながら、このままだと
urlメソッドでは読み込めませんので、きちんとUrl-encodeで処置する必要があります。

オンラインでUrlエンコードをしていただけるサイトは色々とあります。

例えば、
ここのサイトとかを利用させていただくと、上記のインラインsvgは、

            
            %3Csvg%20style%3D%22width%3A24px%3Bheight%3A24px%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000000%22%20d%3D%22M11%2C13H13V15H11M11%2C9H13V11H11M11%2C17H13A2%2C2%200%200%2C0%2015%2C15V13.5A1.5%2C1.5%200%200%2C0%2013.5%2C12A1.5%2C1.5%200%200%2C0%2015%2C10.5V9A2%2C2%200%200%2C0%2013%2C7H11A2%2C2%200%200%2C0%209%2C9V10.5A1.5%2C1.5%200%200%2C0%2010.5%2C12A1.5%2C1.5%200%200%2C0%209%2C13.5V15A2%2C2%200%200%2C0%2011%2C17M12%2C1C5.92%2C1%201%2C5.92%201%2C12C1%2C18.08%205.92%2C23%2012%2C23C18.08%2C23%2023%2C18.08%2023%2C12C23%2C5.92%2018.08%2C1%2012%2C1M12%2C19A7%2C7%200%200%2C1%205%2C12A7%2C7%200%200%2C1%2012%2C5A7%2C7%200%200%2C1%2019%2C12A7%2C7%200%200%2C1%2012%2C19Z%22%20%2F%3E%0A%3C%2Fsvg%3E
        
とエンコードされます。

これで
urlメソッドで読み込め形式になりました。

あとは
urlメソッドに読み込む時に、上の文字列にdata:image/svg+xml,のプレフィックスを付与するとurl()関数で利用できるようになります。

インラインsvgの確認

インラインsvgを扱っている際に、コードを弄りながらインタラクティブに画像を確認したい場合があります。

そんなとき、
URL-encoder for SVGのようなオンラインで変換していただけるサイトを利用すると便利です。

合同会社タコスキングダム|蛸壺の技術ブログ

※このサイトでは
Take encodedのテキストエリアにエンコードも表示して貰えるようです。

URLエンコードを行うSass関数の利用

たくさんのインラインsvg画像を使いたい場合には、Sass自体にURLエンコード変換してくれるユーティリティ関数を用意していたほうがベターです。

幸いURLエンコード自体は特殊文字を置き換えるだけですのでさほど難しい実装をしなくても済むので、たとえば
こちらのサイトで紹介されているやり方をすこしアレンジしたものを利用します。

            
            @function _strReplace($substr, $newsubstr, $str, $all:false) {
    $pos : str-index($str, $substr);
    @while $pos != null {
        $strlen : str-length($substr);
        $start : str-slice($str, 0, $pos - 1);
        $end : str-slice($str, $pos + $strlen);
        $str : $start + $newsubstr + $end;
        @if $all == true {
            $pos : str-index($str, $substr);
        } @else {
            $pos : null;
        }
    }
    @return $str;
}

@function _svgUrlEncode($svg) {
    $repMap : (
        "<"  : "%3C",
        ">"  : "%3E",
        "#"  : "%23",
        "{"  : "%7B",
        "}"  : "%7D",
        "\"" : "'"
    );
    $enc : $svg;
    @each $s, $r in $repMap {
        $enc : _strReplace($s, $r, $enc, true);
    }
    @return $enc;
}

@function svgUrlFunc($svg, $enc:true) {
    @if $enc == true {
        $svg : _svgUrlEncode($svg);
    }
    @return url("data:image/svg+xml,#{$svg}");
}
        
たとえばこのソースコードを_util.scssという名前で保存しておくと、この関数の呼び出し側は以下のように<名前空間>.svgUrlFunc(<引数>)の形でURLエンコードを簡単に利用することができます。

            
            @use 'util';
///...
cursor: util.svgUrlFunc($svg);
        


scss部の実装例

コーディングの方法は前回のブログでご紹介して頂いたものと同様の手順です。

今回の主なフォルダ構造は以下の通りです。

            
            $ tree
.
├── my-custom-css-cursor.component.html # メインのhtmlファイル
└── my-custom-css-cursor.component.scss # メインのscssファイル
      └ cursor-images
        ├── _util.scss # 先程のユーティリティ関数
        ├── _baseball.my-custom-css-cursor.component.scss  # 野球のボールのインラインsvg画像を保持したカーソル
        ├── _billiards.my-custom-css-cursor.component.scss  # ビリアードのボールのインラインsvg画像を保持したカーソル
        ├── _volleyball.my-custom-css-cursor.component.scss # バレーのボールのインラインsvg画像を保持したカーソル
        └── _simple.my-custom-css-cursor.component.scss    # 丸と四角の図形のインラインsvg画像を保持したカーソル
        
まずメインのhtmlファイル・my-custom-css-cursor.component.htmlは以下のようにしておきます。

            
            <div class="custom-cursor-billiards">
    <p>Custom Cursor 1 by svg url-encode</p>
</div>
<div class=".custom-cursor-volleyball">
    <p>Custom Cursor 2 by svg url-encode</p>
</div>
<div class="custom-cursor-baseball">
    <p>Custom Cursor 3 by svg url-encode</p>
</div>
<div class="custom-cursor-img4">
    <p>Custom Cursor 4 by svg url-encode</p>
</div>
        
メインのスタイルファイル・my-custom-css-cursor.component.scssは以下です。

            
            @use 'cursor-images/baseball.my-custom-css-cursor.component';
@use 'cursor-images/simple.my-custom-css-cursor.component';
@use 'cursor-images/volleyball.my-custom-css-cursor.component';
@use 'cursor-images/billiards.my-custom-css-cursor.component';
        
@useを羅列しているのでスタイルファイルというよりは、使用するモジュールをリストしたライブラリ的な使い方です。

cursor-images以下に置いた各画像を記述したscssファイルの中身も個別に見ておきます。

_baseball.my-custom-css-cursor.component.scssはベースボールをアイコン画像です。

            
            @use 'util';

.custom-cursor-baseball {
    $svg: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#000000" d="M12,2C9.5,2 7.25,2.9 5.5,4.4C6,4.81 6.41,5.27 6.8,5.76L7.89,5.13L8.89,6.87L7.89,7.44C8.45,8.53 8.82,9.73 8.95,11H10V12L10,13H8.95C8.82,14.27 8.45,15.47 7.89,16.56L8.89,17.13L7.89,18.87L6.8,18.24C6.41,18.73 6,19.19 5.5,19.6C7.25,21.1 9.5,22 12,22C14.5,22 16.75,21.1 18.5,19.6C18,19.19 17.59,18.73 17.19,18.24L16.11,18.87L15.11,17.13L16.11,16.55C15.55,15.47 15.18,14.27 15.05,13H14V11H15.05C15.18,9.73 15.55,8.53 16.11,7.45L15.11,6.87L16.11,5.13L17.19,5.76C17.59,5.27 18,4.81 18.5,4.4C16.75,2.9 14.5,2 12,2M4.12,5.85C2.79,7.55 2,9.68 2,12C2,14.32 2.79,16.45 4.12,18.15C4.46,17.87 4.76,17.55 5.05,17.22L4.43,16.87L5.43,15.13L6.16,15.56C6.55,14.77 6.82,13.91 6.93,13H6V12L6,11H6.93C6.82,10.09 6.55,9.23 6.16,8.44L5.43,8.87L4.43,7.13L5.05,6.78C4.76,6.45 4.46,6.13 4.12,5.85M19.88,5.85C19.54,6.13 19.24,6.45 18.95,6.78L19.57,7.13L18.57,8.87L17.84,8.44C17.45,9.23 17.18,10.09 17.07,11H18V13H17.07C17.18,13.91 17.45,14.77 17.84,15.56L18.57,15.13L19.57,16.87L18.95,17.22C19.24,17.55 19.54,17.87 19.88,18.15C21.21,16.45 22,14.32 22,12C22,9.68 21.21,7.55 19.88,5.85Z" /></svg>';
    cursor: util.svgUrlFunc($svg), auto;
}
        
ここで、前節で用意していた_util.scssの中に収録していたユーティリティ関数を呼び出して利用していることに留意ください。

同様に、ビリヤード弾のアイコン画像・
_billiards.my-custom-css-cursor.component.scssは以下です。

            
            @use 'util';

.custom-cursor-billiards {
    $svg: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#000000" d="M11,13H13V15H11M11,9H13V11H11M11,17H13A2,2 0 0,0 15,15V13.5A1.5,1.5 0 0,0 13.5,12A1.5,1.5 0 0,0 15,10.5V9A2,2 0 0,0 13,7H11A2,2 0 0,0 9,9V10.5A1.5,1.5 0 0,0 10.5,12A1.5,1.5 0 0,0 9,13.5V15A2,2 0 0,0 11,17M12,1C5.92,1 1,5.92 1,12C1,18.08 5.92,23 12,23C18.08,23 23,18.08 23,12C23,5.92 18.08,1 12,1M12,19A7,7 0 0,1 5,12A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19Z" /></svg>';
    cursor: util.svgUrlFunc($svg), auto;
}
        
同様に_volleyball.my-custom-css-cursor.component.scssはバレーボールのアイコンで、

            
            @use 'util';

.custom-cursor-volleyball {
    $svg: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#000000" d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M13.6,20.35C15.96,18.04 17.69,15.08 18.5,11.76C17.84,11.62 17.18,11.54 16.5,11.5C15.56,15.11 13.41,18.22 10.5,20.37C11,20.45 11.5,20.5 12,20.5C12.55,20.5 13.08,20.45 13.6,20.35M9.23,20.04C12.23,18.07 14.5,15.05 15.46,11.5C14.71,11.55 13.97,11.65 13.27,11.81C12.18,14.89 9.97,17.44 7.13,18.97C7.77,19.42 8.5,19.78 9.23,20.04M20.5,12.37C20.16,12.23 19.81,12.11 19.46,12C18.76,14.9 17.39,17.53 15.54,19.73C18.36,18.44 20.35,15.64 20.5,12.37M3.56,11.04C3.5,11.35 3.5,11.68 3.5,12C3.5,14.5 4.57,16.73 6.27,18.28C6.86,18 7.41,17.66 7.94,17.29C6.08,15.54 4.58,13.41 3.56,11.04M5.33,6.74C4.73,7.5 4.26,8.35 3.95,9.28C4.92,12.13 6.58,14.66 8.74,16.67C9.25,16.24 9.72,15.77 10.15,15.26C7.74,13.03 6,10.08 5.33,6.74M8.04,4.5C7.36,4.85 6.73,5.3 6.18,5.82C6.71,9.21 8.37,12.23 10.77,14.47C11.17,13.91 11.5,13.32 11.82,12.7C9.68,10.56 8.28,7.69 8.04,4.5M19.96,9.03C18.7,8.68 17.37,8.5 16,8.5C14.1,8.5 12.28,8.85 10.61,9.5C10.96,10.1 11.35,10.67 11.8,11.2C13.12,10.75 14.53,10.5 16,10.5C17.57,10.5 19.08,10.78 20.47,11.29C20.4,10.5 20.23,9.74 19.96,9.03M17.54,5.57C17.03,5.5 16.5,5.5 16,5.5C13.69,5.5 11.47,5.94 9.44,6.73C9.62,7.38 9.86,8 10.14,8.61C11.96,7.89 13.93,7.5 16,7.5C17.18,7.5 18.32,7.63 19.42,7.87C18.93,7 18.29,6.21 17.54,5.57M16,4.5C14.79,3.87 13.44,3.5 12,3.5C10.95,3.5 9.94,3.7 9,4.05C9.04,4.63 9.11,5.2 9.21,5.75C11.31,4.95 13.6,4.5 16,4.5Z" /></svg>';
    cursor: util.svgUrlFunc($svg) 10 10, auto;
}
        
残りの_simple.my-custom-css-cursor.component.scssはダウンロードしたものではなく、簡単な図形(四角と丸)で自作したsvgを使ってみたアイコンです。

            
            @use 'util';

.custom-cursor-simple {
    $svg: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 80 80"><g fill="#c44" stroke="#822" stroke-width="2"><circle cx="32" cy="32" r="30" /><rect x="40" y="30" width="80" height="50" /></g></svg>';
    cursor: util.svgUrlFunc($svg), auto;
}
        

完成品

上のカスタムカーソルをビルドすると、

となっていればうまくsvgを使ったアイコン表示が機能しております。


アニメーションを含む動的なインラインsvgを試してみる

冒頭でも述べましたように、svgアニメーションのような複雑なsvgをscssコードでurlメソッドに割り当ててみたときの表示の挙動を試してみます。

Sassのプロジェクト構造を以下のように修正してみます。

            
            $ tree
.
├── my-custom-css-cursor.component.html # メインのhtmlファイル
└── my-custom-css-cursor.component.scss # メインのscssファイル
      └ cursor-animation
        ├── _util.scss # 先程のユーティリティ関数
        ├── _star.my-custom-css-cursor.component.scss # 星をなぞるアニメーション
        └── _svg-animation.my-custom-css-cursor.component.scss # 複雑な軌道のアニメーション
        
メインのhtmlファイルは以下のようにします。

            
            <div class="custom-cursor-star">
    <p>Custom Cursor 5 by svg url-encode</p>
</div>
<div class="custom-cursor-complex">
    <p>Custom Cursor 6 by svg url-encode</p>
</div>
        
またメインのscssファイルは、

            
            @use 'cursor-animation/star.my-custom-css-cursor.component';
@use 'cursor-animation/svg-animation.my-custom-css-cursor.component';
        
どうせ動かないとは思うのですが、今回は2例のsvgアニメーションを試します。

ひとつは星の形をなぞるようなアニメーション・
_star.my-custom-css-cursor.component.scssは以下です。

            
            @use 'util';

.custom-cursor-star {
    $svg: '<svg xmlns="http://www.w3.org/2000/svg" class="svgLineMove" width="80" height="80" viewBox="0, 0, 160, 160"><defs><style>.svgLineMove{stroke-dasharray: 1800;stroke-dashoffset: 0;fill-opacity:1;animation: svgAnime 5s 0s ease-in infinite;} @keyframes svgAnime { 0% {stroke-dashoffset: 1800;fill-opacity:0;} 100% {stroke-dashoffset: 0;fill-opacity:1;}}</style></defs><path d="M75 0 L55 50 L0 50 L40 90 L15 150 L75 115 L135 150 L110 90 L150 50 L95 50 Z" fill="#ffff99" stroke="#ff4d4d" stroke-width="5"></path></svg>';
    cursor: util.svgUrlFunc($svg) 40 40, auto;
}
        
もうひとつは、ちょっと複雑そうなsvgを紹介されているサイトのコードをそのまま参考させてもらったものです。

以下の内容で
_svg-animation.my-custom-css-cursor.component.scssを編集しておきます。

            
            @use 'util';

.custom-cursor-complex {
    $svg: '<svg xmlns="http://www.w3.org/2000/svg" width="101" height="101" viewBox="0 0 352.74 333"><defs><style>.ellipse {fill:none;stroke-miterlimit:10;stroke-width:5px;opacity:0.6;} .ellipse1 { stroke:aqua;animation: rotation 1.5s ease infinite; transform-origin: center center;} .ellipse2 {stroke:#f6bbff;animation: rotation 1.5s ease infinite;animation-delay: -0.4s;transform-origin: center center;} .ellipse3 {stroke:#fff900;animation: rotation 1.5s ease infinite;animation-delay: -0.8s;transform-origin: center center;}.text {font-size:48px;} .loading-text{fill:#939393; animation: loading-text 2s ease infinite;transform-origin: center center;} .load-complete-text{fill:#727272;display: none;} @keyframes rotation { 0% {transform: scale(0.6) rotate(0deg);} 50% {transform: scale(1.0) rotate(180deg);} 100% {transform: scale(0.6) rotate(360deg);}} @keyframes loading-text { 0%,100% {transform: scale(0.8);opacity: 0;} 50% {transform: scale(1.0);opacity: 1;}}</style></defs><title>Loading</title><ellipse class="ellipse ellipse1" cx="178.74" cy="166.5" rx="146.5" ry="164"/><ellipse class="ellipse ellipse2" cx="178.74" cy="166.5" rx="164" ry="146.5" transform="translate(-59.3 111.68) rotate(-30)"/><ellipse class="ellipse ellipse3" cx="178.74" cy="166.5" rx="146.5" ry="164" transform="translate(-54.82 238.05) rotate(-60)"/><text class="text loading-text" x="89.68" y="178.49">Loading</text><text class="text load-complete-text" transform="translate(0 183.28)">Load Complete!!</text></svg>';
    cursor: util.svgUrlFunc($svg) 80 80, auto;
}
        

ビルド後の表示

これをビルドすると以下のようになります。

さてお手元のブラウザがFirefoxであれば、マウスを重ねても何も表示されないはずです。

他方Chromeでは、マウスを左右に動かすとアニメーションしながら表示されますが、なんかマウスを止めたらアニメーションも止まってしまうようです。

期待としては、マウスをホバーしたときに、アニメーションし始めるのが理想ですが、結果はやっての通りで残念なものになります。


まとめ

今回は、svgのUrlエンコードでカスタムカーソルを作ることをご紹介しました。

現状で、ブラウザによって動く動かないが依存してしまうのは致命的ですが、お戯れ程度に理解していただけたのではないかと思います。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

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