【HTMLアプリ作成】SvelteでSVG要素を直接操作して制限時間表示できるタイマーを作ってみる


※ 当ページには【広告/PR】を含む場合があります。
2021/01/20
合同会社タコスキングダム|TacosKingdom,LLC.

JSフレームワークの中でもビルド後の生成ファイルのサイズが最軽量級で、コードもスッキリしていて、初学者に今もっともオススメの
『Svelte』でHTML&CSSミニゲームを作るための手順を考えていきます。

最近、別のブログの方で、Svelteアプリの開発環境をサクッと構築するやり方を解説してみました。

参考|【Svelte入門】Svelteの開発環境をDocker Alpine内で簡単に構築する

Svelteを始めてみたい方はそちらの記事をじっくり読んで頂くとして、今回はSvelteアプリ作成に慣れるために、HTML&CSSゲームなどに応用のできるタイマーっぽいものを一から作成する過程を解説してみます。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

Svelteアプリのデモから学ぶ

Svelteフレームワークの扱いに慣れていないうちは、公式の例題詰め合わせのページからおおよそのプログラミングの方向性を把握することも多いかと思います。

今回は、
クロックアプリのサンプルから着想を得まして、ゲームの制限時間表示に使えるようなSVG画像を作成してみます。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

SVGにおけるPathを使った直線(L)と円弧(A)の基本

SVGのPathは、柔軟に図形を描くために利用される要素です。

なお、SVGのビューポートの概念や座標定義に関して復習したい場合には、以前特集した以下の記事を参照してください。

合同会社タコスキングダム|タコキンのPスクール
【Scratch入門】SVG画像のviewportの意味とviewBox属性の使い方

ScratchにおけるSVG画像で重要なビューポート(Viewport)の概念と使い方の基本を説明します。

今回の例でいうと、Pathによる
直線(Lコマンド)円弧(Aコマンド)を組み合わせた図形で、簡単なタイマークロックを作成しています。

例えばPathのLコマンドで簡単な直線を連続的に描こうとすると、pathタグのd属性に描画コマンドを記述していくことになります。

            
            <svg width="1000" height="1000">
   <path d="
         M 10  0
         L 10  200
         L 100 200
         L 50  50
         L 30  250
         L 120 40"
    stroke="black"
    stroke-width="10"
    fill="none"/>
</svg>
        
とこのように書くと、最初の点(10, 0)をMコマンドで指定するところから始まって、Lコマンドを繋げることで、(10, 200) > (100, 200) > (50, 50) > (30, 250) > (120, 40)と連続して直線を描くことができます。

合同会社タコスキングダム|タコキンのPスクール

参考サイト|オンラインツールSVG viewer/editor

直線を描くLコマンドは終点座標を指定するだけですので利用はとても簡単でしたが、円弧コマンド(A)は少し厄介です。

            
            ...(MとかLとかのコマンドが存在)
A   [x軸方向の半径] [y軸方向の半径] [x軸からの傾き(角度)]
    [円弧選択フラグ1(large-arc-flag)] [円弧選択フラグ2(sweep-flag)]
    [終点のx座標] [終点のy座標]
        
見てのように、Aコマンドの引数は7つもあるので所見で混乱しやすいです。

最初に使うと、
「円弧選択フラグとは何ぞや?」となると思います。

Aコマンドは現在留まっている点から、次の終点までを結ぶ、楕円弧を計算して軌跡を描画してくれる機能です。

ですので、2点間の円弧というのは最大4通り描くことが可能ですので、どの円弧を選択するのかを伝えるのが
円弧選択フラグです。

            
            <svg width="1000" height="1000" viewBox="100 100 1000 1000">
    <path d="
          M 200 200
          L 300 300"
          stroke="black" stroke-width="10" fill="none"/>
    <path d="
          M 200 200
          A 80 50 40
            1 0
            300 300"
          stroke="blue" stroke-width="10" fill="none"/>
    <path d="
          M 200 200
          A 80 50 40
            0 0
            300 300"
          stroke="red" stroke-width="10" fill="none"/>
    <path d="
          M 200 200
          A 80 50 40
            0 1
            300 300"
          stroke="green" stroke-width="10" fill="none"/>
    <path d="
          M 200 200
          A 80 50 40
            1 1
            300 300"
          stroke="yellow" stroke-width="10" fill="none"/>
</svg>
        
Aコマンドを使うときには、この2つのフラグの組み合わせ(4通り)で、どの円弧を描きたいのかを明示に指定する必要があります。

合同会社タコスキングダム|タコキンのPスクール

一般に、
円弧選択フラグ1(large-arc-flag)では、フラグ値を1にすることで、2つの点のなす楕円弧のうち長い方(この例でいうと青か黄色の円弧)を指定できます。逆にフラグをゼロにすると短い方の円弧(ここでは赤か緑の円弧)を指定します。

次に、
円弧選択フラグ2(sweep-flag)では、始点から終点を円弧で結ぶ時に、時計回りの円弧か反時計周りの円弧かを指定するフラグです。

このフラグ値を1にすると時計回りの円弧(この場合緑か黄色の円弧)を選択でき、フラグ値0で反時計周りの円弧(赤および青の円弧)を指定することになります。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

残り時間をビジュアル化する

ということでSVGの直線・円弧の描き方のポイントを押さえて貰ったところで、以下のような制限時間表示するクロックを作ってみます。

このアプリは以下のソースコードをビルドしたものになります。

            
            <script lang="ts">
    import { onMount } from 'svelte';

    const period = 10; // [秒]
    const ticks = 300;
    const timerPeriod = period * 1000 / ticks;
    let timerCount = 0;

    //👇SVGの円弧を終点座標をリアクティブに更新する
    $: arcEndPosX = 50 * Math.sin(2*Math.PI*(timerCount / ticks));
    $: arcEndPosY = -50 * Math.cos(2*Math.PI*(timerCount / ticks));
    $: arcFlag = timerCount >= (ticks / 2) ? 1 : 0;

    let messageSpan: any;

    onMount(() => {
        const interval = setInterval(() => {
            timerCount = timerCount >= ticks - 1 ? 0 : timerCount + 1;
            if (timerCount < ticks * 0.5) {
                messageSpan.textContent = `所要時間は${period}秒です`;
            } else if (timerCount >= ticks * 0.5 && timerCount < ticks ) {
                const remainTime = period * (1 - timerCount / ticks);
                messageSpan.textContent = `残り ${Math.floor(remainTime + 1)} 秒!`;
            }
        }, timerPeriod);
        return () => {
            clearInterval(interval);
        };
    });
</script>

<svg width="100" height="100" viewBox='-100 -100 200 200'>
    <path class="arc1" d="
    M 0 0
    L 0 -50
    A 50 50 0
      {arcFlag} 1
      {arcEndPosX} {arcEndPosY}
    Z"/>
</svg>
<p>{Math.floor(period * timerCount / ticks)}秒</p>
<span bind:this="{messageSpan}"></span>

<style lang="scss">
    svg {
        width: 300px;
        height: auto;
        path.arc1 {
            fill: rgb(145, 145, 145);
        }
    }
</style>
        
ではこのコードの実装のポイントを簡単に解説していきましょう。

SvelteでSVG要素をリアクティブに操作する

他のJSフレームワークでも出来なくは無いのですが、Svelteはより直観的に動的な変化を伴うSVGの記述が出来ます。

            
            const period = 10; // [秒]
const ticks = 300;
const timerPeriod = period * 1000 / ticks;
let timerCount = 0;

//👇SVGの円弧を終点座標(と円弧の選択フラグ)をリアクティブに更新する
$: arcEndPosX = 50 * Math.sin(2*Math.PI*(timerCount / ticks));
$: arcEndPosY = -50 * Math.cos(2*Math.PI*(timerCount / ticks));
$: arcFlag = timerCount >= (ticks / 2) ? 1 : 0;

onMount(() => {
    const interval = setInterval(() => {
        //👇300回(ticks)カウントしたらカウンターをゼロにリセット
        timerCount = timerCount >= ticks - 1 ? 0 : timerCount + 1;
        //...中略
        //👇300カウントが10秒になるようにインターバル時間(timerPeriod)を調整
    }, timerPeriod);
    //...以下略
        
こうしておくことで、timerCountがsetIntervalのコールバックの中で更新され続けると同時に、SVGの終点座標等(arcEndPosX / arcEndPosY / arcFlag)もSvelteコードの中で自動更新されるようになります。

            
            ...
<svg width="100" height="100" viewBox='-100 -100 200 200'>
    <path class="arc1" d="
    M 0 0
    L 0 -50
    A 50 50 0
      {arcFlag} 1
      {arcEndPosX} {arcEndPosY}
    Z"/>
</svg>
...
        
SVG画像自体は閉じたPath(最後のZを付ける)で描いています。

M 0 0で描き始めの点を指定し、そこからL 0 -50まで直線を描き、そこから終点(arcEndPosX, arcEndPosY)までのX軸からの傾き0で半径50の円弧を描きます。

前述したように、SVGの円弧は指定した2点から円弧を作る関係で潜在的に4通りの円弧が描けるため、円弧選択フラグの1・2で適切に選ばないといけません。

今回の場合には楕円ではないので、フラグ1か2のどちらか1つを切り替えるだけできちんとした円弧の切り替わりを作ることが出来ます。

タイマーカウンターを利用して表示内容を変える

所要時間を表したクロックがくるくるまわるだけだとつまらないので、残り時間に連動して表示内容も変わるようにしてみます。

            
            //...中略
onMount(() => {
    const interval = setInterval(() => {
        //👇300回(ticks)カウントしたらカウンターをゼロにリセット
        timerCount = timerCount >= ticks - 1 ? 0 : timerCount + 1;

        if (timerCount < ticks * 0.5) { //👈クロックが半分以下(~150カウント)の場合
            messageSpan.textContent = `所要時間は${period}秒です`;
        } else if (timerCount >= ticks * 0.5 && timerCount < ticks ) {
            //👇クロックが半分以上(>150)になった時の処理
            const remainTime = period * (1 - timerCount / ticks);
            messageSpan.textContent = `残り ${Math.floor(remainTime + 1)} 秒!`;
        }

        //👇300カウントが10秒になるようにインターバル時間(timerPeriod)を調整
    }, timerPeriod);
    //...以下略
        
これで残り時間が半分を切ったときに表示を切り替えることが出来ます。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

クロックの見栄えを改善する

さらにモノクロだと少し味気ないので、残り時間と連動するように背景色を変えるように改造してみます。

まず改造したアプリは以下のようになります。

ちょっと改造を施したSvelteコードは以下のようになります。

            
            <script lang="ts">
    import { onMount } from 'svelte';

    const period = 10;
    const ticks = 300;
    const timerPeriod = period * 1000 / ticks;
    let timerCount = 0;
    $: arcEndPosX = 50 * Math.sin(2*Math.PI*(timerCount / ticks));
    $: arcEndPosY = -50 * Math.cos(2*Math.PI*(timerCount / ticks));
    $: arcFlag = timerCount >= (ticks / 2) ? 1 : 0;

    //👇SVGを色遷移するためリアクティブ変数と関数を追加
    $: colorFill = rgb(0, 255, 255);
    function rgb(_r: number, _g: number, _b: number) {
        let code = (1 << 24) + (_r << 16) + (_g << 8) + _b;
        return `#${code.toString(16).replace(/^1/,'')}`;
    }

    let messageSpan: any;

    onMount(() => {
        const interval = setInterval(() => {
            timerCount = timerCount >= ticks - 1 ? 0 : timerCount + 1;

            if (timerCount < ticks * 0.5) {
                colorFill = rgb(0, 255, 255);
                messageSpan.textContent = `所要時間は${period}秒です`;
            } else if (timerCount >= ticks * 0.5 && timerCount < ticks*0.75 ) {
                const remainTime = period * (1 - timerCount / ticks);
                messageSpan.textContent = `残り ${Math.floor(remainTime + 1)} 秒!`;
                const colorShit = Math.round(255 * (remainTime/ (period / 4) - 1) );
                colorFill = rgb(255 - colorShit, 255, colorShit);
            } else if (timerCount >= ticks * 0.75 && timerCount < ticks) {
                const remainTime = period * (1 - timerCount / ticks);
                messageSpan.textContent = `残り ${Math.floor(remainTime + 1)} 秒!`;
                const colorShit = Math.round(255 * remainTime / (period / 4));
                colorFill = rgb(255, colorShit, 0);
            }
        }, timerPeriod);
        return () => {
            clearInterval(interval);
        };
    });
</script>

<svg width="100" height="100" viewBox='-100 -100 200 200'>
    <path class="arc1" d="
    M 0 0
    L 0 -50
    A 50 50
      0 {arcFlag} 1
      {arcEndPosX} {arcEndPosY}
    Z" fill="{colorFill}"/>
</svg>

<p>{Math.floor(period * timerCount / ticks)}秒</p>

<span bind:this="{messageSpan}"></span>

<style lang="scss">
    svg {
        width: 300px;
        height: auto;
    }
</style>
        

改造したSvelteコードの解説

まず、この改造では、SVG画像のPathに直接fill="{色}"でリアクティブなRGB色コード(16進法)を注入することで連続的に色が変わっています。

            
            ...
<svg width="100" height="100" viewBox='-100 -100 200 200'>
    <path class="arc1" d="
    M 0 0
    L 0 -50
    A 50 50
      0 {arcFlag} 1
      {arcEndPosX} {arcEndPosY}
    Z" fill="{colorFill}"/> //👈ここで色を注入
</svg>
...
        
ここで定義したRGB色コード文字列を与える以下の関数は、SvelteだけでなくJavascript全般に使えます。

            
            function rgb(_r: number, _g: number, _b: number) {
    let code = (1 << 24) + (_r << 16) + (_g << 8) + _b;
    return `#${code.toString(16).replace(/^1/,'')}`;
}

//使用例
console.log(rgb(124, 42, 55)); //👉#7c2a37
console.log(rgb(0xff, 0xc3, 0x20)); //👉#ffc320
        
使い方としては、0 ~ 255の値でRGB色を作るっているだけです。

むしろここから色をシフトさせていくの方が難しく、今回は残り時間に連動させて
水色 > 黄色 > 赤色にシームレスに変化していくように実装する感じで以下のようになっています。

            
            const interval = setInterval(() => {
    timerCount = timerCount >= ticks - 1 ? 0 : timerCount + 1;

    if (timerCount < ticks * 0.5) {
        //👇経過時間が半分以下なら色を固定
        colorFill = rgb(0, 255, 255);
        messageSpan.textContent = `所要時間は${period}秒です`;
    } else if (timerCount >= ticks * 0.5 && timerCount < ticks*0.75 ) {
        //👇経過時間が半分以上75%未満なら水色から黄色に色コードシフト
        const remainTime = period * (1 - timerCount / ticks);
        messageSpan.textContent = `残り ${Math.floor(remainTime + 1)} 秒!`;
        const colorShit = Math.round(255 * (remainTime/ (period / 4) - 1) );
        colorFill = rgb(255 - colorShit, 255, colorShit);
    } else if (timerCount >= ticks * 0.75 && timerCount < ticks) {
        //👇経過時間が75%以上なら黄色から赤色に色コードシフト
        const remainTime = period * (1 - timerCount / ticks);
        messageSpan.textContent = `残り ${Math.floor(remainTime + 1)} 秒!`;
        const colorShit = Math.round(255 * remainTime / (period / 4));
        colorFill = rgb(255, colorShit, 0);
    }
}, timerPeriod);
        
ここら辺はプログラマーの遊び心で色々と応用できると思いますので、もっと面白い効果などでも同じようなテクニックで作り込んでいけると思います。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

まとめ

今回は、SvelteでSVG画像を柔軟に扱うための話を具体例を示しながら解説していきました。

Svelteの公式ページに色々とサンプルが紹介されいます通り、もっと派手なアニメーション効果も直感として分かりやすく実装できるのが、Svelteアプリ開発の魅力です。

是非とも自分の指でキーボードをカタカタ叩いてみて、Svelteコードで出来ることを実感してみてはいかがでしょうか。