1
2

【Vue3】自作ライブラリをその場で実行できる多言語対応チュートリアルサイトを作った

Posted at

このサイトのお話をします:https://konbraphat51.github.io/HSS_examples/
これがレポジトリです:https://github.com/konbraphat51/HSS_examples

やりたいこと

動機

初心者向け自作ライブラリを作った!(Qiita記事へ飛ぶ) 布教したい!(GitHubレポジトリへ飛ぶ) チュートリアルサイトを作ろう! という経緯です。
ちなみに上記記事が2023/11/14トレンド1位でとても嬉しいです。みなさまありがとうございます。

どんな自作ライブラリか簡単に

https://qiita.com/konbraphat51/items/b138683db352afd77714 でも十分説明しましたが、HTMLの`canvas`要素に対する描画処理APIを単純化した初心者向けゲームプログラミングJavaScriptライブラリです。
すなわち、ブラウザで動作することを想定しております。

要件定義

まず、前提として

  1. 布教
  2. プログラミング初心者の手引き
  3. 多言語対応

をしたいというお気持ちがあります。

  1. 自分のライブラリを布教するには、 どのようなコードで何が実現できるのか ただちに提示できると訴求性がある。

  2. そして、プログラミング初心者を手引きするには、 コードがコピペでき、コメントによる説明が豊富 である必要があります。

  3. ついでに、名だたる列強国と戦うためには、言語切替 の機能をつけたいと申します。

1と2からは直ちに 「ちゃんと説明されたコードとその実行結果を同時に見せる」 という結論が得られますが、3により コードコメンテーションも対応言語に変化させる必要 があります。
言語ごとにjsファイルを書く?嫌だなあ。 綺麗に、変更対応がやりやすいように まとめます。

作りました

紹介

作りました。
https://konbraphat51.github.io/HSS_examples/

項目はまだ未完なので、「動く」→「キー入力」のコネクションが飛び過ぎ(2023/11/14現在)なのはご容赦ください。

image.png

まず簡単な紹介。

image.png

ちゃんと言語切り替えできる。

image.png

次に項目を選びます。
image.png

言語切り替えできる。

image.png

選ぶと、自動スクロールで対応するコードまで遷移。

image.png

コードも言語切り替えできる!!!

sasa_20231114_214716.gif

再生ボタンを押すと、対応した処理がcanvasになされる。

image.png

フッター。

実装

CDNでお届けしております

npm_modulesアンチなので、必要なもの全てをインストールしておくVue CLIではなく、ページを開いた際に、その都度どこか別のサーバーから必要物をダウンロードしてくるVue CDNで実装。

CDN Vueで、単一ファイルコンポーネント構成を使い、他言語対応する方法の記事はこちら

構成

フォルダー構成はこんな感じ。
image.png

srcフォルダに本アプリに必要な物品を。
srcは"Source Cord"の略なので、原義通りにきっちりソースコードのみを入れる人もいれば、画像でもなんでも必要なデータすべて入れ込む人もいますよね。
Vue CLIでは後者が主流なので、同じように。

index.html

GitHub Pagesに対応させるため、ルートディレクトリに配置。
先ほど紹介した記事のテンプレートとほぼほぼ同じで、appdivタグに、Appコンポーネントをマウントさせています。
(例の自作ライブラリを呼び出しているところだけ追加しています)

Examplesフォルダ

Examplesフォルダには、各項目のフォルダが入っていて、その中に

  • script.js: 例示するコード
  • description.json: 紹介文など
    が入っています。
description.json

各言語の説明情報をjsonで記述。これはi18nの記述と同じですね。

description.json
{
    "en": {
        "title": "Movement",
        "option_description": "Give your character a movement"
    },
    "ja": {
        "title": "動く",
        "option_description": "文字を動かそう!"
    }
}

script.js

初心者を手引きする感じのコメントとともに、動作させるコードを書きます。

script.js
async function main() {
    //en: This is the combination of "Infinite loop" and "Drawing a rectangle"
    //en: Try think how you can move a letter without leave a trace
    //en: It will be very satisfying if you come up with a solution
    //ja: 「無限ループ」と「四角形を描く」のコラボレーションです
    //ja: 先に、跡を残さずに文字を動かす方法を考えてみてください
    //ja: 自分で分かったら気持ちいいですよ

    //en: make variable + set initial position here
    //ja: 変数を作り、ついでに初期位置を設定
    var x = 100;
    var y = 100;

    //en: set speed here
    //ja: スピードを設定
    var speed = 5;

    //en: Adjust character size here
    //ja: 文字の大きさをここで調整する
    SetFont("20px Arial")

    while (true) {
        //en: clear all drawings of the previous frame
        //ja: 前のフレームの描画を消す
        SetColor("white")
        DrawRect(0,0,GetCanvasSize()[0],GetCanvasSize()[1])


        //en: move by key input
        //ja: キー入力によって移動
        SetColor("black")
        DrawText("a", x, y)

        //en: move
        //en: Because the `x` is altered, the position moves
        //ja: 移動する
        //ja: `x`が変化するので、位置が移動する
        x += speed;

        //en: cool time
        //ja: 冷却時間
        await Sleep(10);
    }
}

実行させるコンポーネントがこのmain()を呼び起こす、という感じです。
また、コメントに言語コード+:をつけ、対応した言語のコメントを記入。
表示時に、対応言語のみ残っていますね。こうすることで、言語ごとに.jsファイルを作らず、まとめて多言語対応コメントをつけることができました。

.vueファイル

基本記述

最初の説明カードのコンポーネントの例です。

Description.vue
<template>
    <div class="warning">
        <p>
            <img src="src\images\description\mark_exclamation.png" 
                width="15" 
                height="15">
            {{ $t("description.warning") }}
        </p>
    </div>

    <div id="description">
        <h2>{{ $t("description.what_is") }}</h2>
        <p> {{ $t("description.what_is_description") }} </p>
    </div>
</template>

<script>
    export default Vue.defineComponent({
        name: 'Description',
        setup() {
            //set up i18n
            const { t } = VueI18n.useI18n()
            return { t }
        }
    })
</script>

<i18n>
    {
        "en": {
            "description": {
                "what_is": "What is HotSoupScript?",
                "what_is_description": "This library enables programming beginers to make an artistic (especially game) programming easily.",
                "warning": "If the \"Hello World\" script doesn't run (canvas shows nothing), refresh the page."
            }
        },
        "ja": {
            "description": {
                "what_is": "HotSoupScriptとは?",
                "what_is_description": "このライブラリは、プログラミング初心者が簡単にグラフィカルな(特にゲーム)プログラミングを行えるようにするものです。",
                "warning": "もし「Hello World」スクリプトが動かない(キャンバスに何も表示されない)場合は、ページをリロードしてください。"
            }
        }
    }
</i18n>

<style>
    #description {
        margin: 10px 20px 10px 20px;
        background-color: #faffdb;
        border-radius: 10px;
        padding: 10px;
        border: 1px solid #000000;
    }

    .warning {
        margin: 10px 20px 10px 20px;
        background-color: #ffdbdb;
        border-radius: 10px;
        padding: 10px;
        border: 1px solid #000000;
    }
</style>

まあ基本的なVueの単一ファイルコンポーネントですよね。
一番単純なものを選んでいるわけですが。

項目リスト

一番根本のコンポーネントであるAppに、存在する項目IDをメタ書き(別JSONに書き出してもよかったけど、App.vueに書き出すのとあまり違いが分からなかったため)

App.vue
        data() {
            return {
                examples: [
                    "HelloWorld",
                    "ChangeColor",
                    "ChangeFont",
                    "Calculation",
                    "Variables",
                    "Sleep",
                    "For",
                    "WhileTrue",
                    "Rect",
                    "Movement",
                    "MoveByKey",
                    "BulletGame"
                ],
                selected_example: "HelloWorld",
                example_descriptions: {}
            }
        },
Examplesデータの取得

Examplesフォルダの各項目の説明文を取得するにはfetchを利用。これも根本のAppにやらせています。神コンポーネント臭くなりましたね。

App.vue
            getExampleData() {
                //read description files
                for (let example of this.examples) {
                    const path = "src/Examples/" + example + "/description.json"
                    fetch(path)
                        .then(response => response.json())
                        .then(data => {
                            this.example_descriptions[example] = data
                        })
                }
            },

子コンポーネントにデータを配布するわけですが、Appの時点で選択言語のデータのみ抽出。computedに指定することで、言語切り替え時に自動で更新されるわけです。

App.vue
        computed: {
            example_descriptions_locale() {
                let locale = i18n.global.locale.value
                let descriptions = {}
                for (let example in this.example_descriptions) {
                    descriptions[example] = this.example_descriptions[example][locale]
                }
                return descriptions
            }
        }
スクリプトの表示

先ほどお見せしたjavascriptファイルを、対応言語のみ、en:などのシンボルを消して表示させます

ScriptViewer.vue
if (line.match("//")) {
    //commentation included in the line

    let another_language = false
    let line_splited = line.split("//")
    let line_edited = line_splited[0]
    for (let cnt = 1; cnt < line_splited.length; cnt++) {
        language_symbols.forEach(symbol=>{
            if (line_splited[cnt].match(symbol)) {
                if (i18n.global.locale.value == language_symbols_dictio[symbol]) {
                    //same language
                    line_splited[cnt] = line_splited[cnt].replace(symbol, "")
                } else {
                    //another language
                    another_language = true
                }
            }
        })

        line_edited += "//" + line_splited[cnt]
    }
    //続く
スクリプトを走らせる

まずは、読み込みです。

実は、実行に当たって一点ソースコードを改変する必要があります。というのは、サブルーチンを名指しで止める手段がないので、停止ボタンで停止できるように、停止フラグの受信機を作る必要があります。

そこで、自作ライブラリにwhile(true)をするときはSleep()を使うというお約束がありますが、Sleep()の直前に受信機を作ります。

ScriptLoader.vue
computed: {
    script_edited(){
        let output = this.script

        //define stop flag
        output = "var __stop = false\n" + output

        let returner = "if (__stop) { return }\n"

        //set return under all await() functions
        let lines = output.split(/\r\n|\r|\n/)
        let cnt = 0

        while (cnt < lines.length) {
            if (lines[cnt].match("Sleep")) {
                //await() function
                //set return under the await() function
                lines.splice(cnt + 1, 0, returner)
                cnt += 2
            } else {
                //not await() function
                cnt += 1
            }
        }
        output = lines.join("\n")

        return output
    }

そして再生。
再生ボタンのコンポーネントに実行処理を背負わせています。
ゲームプログラミングだと、こんな辺境のUIが重大な処理を背負っていると拡張性の観点から怒られますが、Webフロントエンドのコンポーネント指向の場合はどうなんでしょうね?多分ダメだと思う。

StartButton.vue
//start

//stop flag
if (typeof(__stop) == "boolean") {
    __stop = false
}

//reset HSS condition
SetColor("black")
SetFont("10px sans-serif")

//走らせる
StartAsync(main)

使用感調査

内容はまだ制作中なので置いておいて。

とりあえず個別でプログラミングを教えている方に、ちょっとやらせてみれば「やりやすーい!」とのこと。よかったです(n=1)

最後に

いいね頂ければ泣きながら喜びます。(Vue記事はあまり見向きされない傾向があります泣)

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2