Turbowarpの『カスタム拡張機能』を使おう【４】

# 本記事について
Turbowarpのカスタム拡張機能を作る方法の説明、その４です。

前記事までで、外部Javascriptファイルを読み込む処理、読み込むファイルをブロックで指定できるようにする、ところまでをやりました。

今回は、テスト用Javascriptのなかで『Scratch』操作をさせてみようと思います。

# やりたいことの提示

- ブロックで入力したSTEP数だけ動かす
～あらかじめ設定されている向きの方向に動かす
- 位置情報を言う（ふきだし）
～「〇〇と言う」と「〇〇と考える」をプルダウンで選択可能
～「〇〇秒の間だけ」の機能もつける

# 動画解説
![turbowarp004.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/feb7ecee-3a83-aab6-37f5-c5434b1c9a6d.gif)


# カスタム拡張機能コード

[コードGithub](https://github.com/amami-harhid/turbowarpExtensions/blob/main/_04_extension/Extension.js)

#### 拡張機能コンスタント

```javascript:Extension.js
/**
 * Turbowarpの『カスタム拡張機能』を使おう【４】
 * 外部JSファイルにて、Scratch標準ブロックを再現させる
 * １）指定されたSTEP数だけ動かす
 * ２）現在の位置を吹き出し表示する（言う、考える）
 */
((Scratch) => {
    const ExtensionID = 'MYEXTENSION';
    const ExtensionName = '独自拡張練習';
    // 歯車画像URL
    const GEAR_IMAGE_SVG_URI 
        = 'https://amami-harhid.github.io/turbowarpExtensions/assets/gear.svg';
    // テスト用JSファイルの場所 URL
    const TEST_URL 
        = 'http://127.0.0.1:5500/turbowarpExtensions/_04_extension';
```
#### 拡張機能定義情報（カラー付き）
```javascript
    const MyExtensionInfo = {
        id : ExtensionID, 
        name : ExtensionName,
        color1 : '#000000', // 背景を黒に( 文字色は白固定なので背景を白にすると文字が読めない )
        color2 : '#ffffff', // ブロックリストの円周の色( 白 )
        color3 : '#0000ff', // ブロックの周囲の線の色（ 青 )
```
#### color1, color2, color3 はオプション、省略可能。
```javascript
        blocks : [
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/7dcf794c-175d-c683-ebc7-93cdacb54a21.png)

```javascript
            {
                opcode: 'loadJSFileSetting',
                blockType: Scratch.BlockType.COMMAND,
                text: '[IMG_GEAR]JSファイルを指定する[JSURL]',
                arguments: {
                    IMG_GEAR: {
                        type: Scratch.ArgumentType.IMAGE,       //タイプ
                        dataURI: GEAR_IMAGE_SVG_URI,            //歯車画像のURI
                    },
                    JSURL: {
                        type: Scratch.ArgumentType.STRING,
                        defaultValue: `${TEST_URL}/sub.js`,
                    },
                },
            },
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/e0f663d1-5b4f-7a86-c296-4eb7f6c038c8.png)

```javascript
            {
                opcode: 'setup',
                blockType: Scratch.BlockType.COMMAND,
                text: '[IMG_GEAR]事前準備',
                arguments: {
                    IMG_GEAR: {
                        type: Scratch.ArgumentType.IMAGE,       //タイプ
                        dataURI: GEAR_IMAGE_SVG_URI,            //歯車画像のURI
                    },
                },
            },
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/ca365ced-6a3b-ad9e-31c1-a2edac2e7b81.png)

```javascript
            {
                opcode : 'moveStep',
                blockType : Scratch.BlockType.COMMAND,
                text : '[GEAR_IMAGE] 動かす [STEPS]',
                arguments: {
                    GEAR_IMAGE : {
                        type: Scratch.ArgumentType.IMAGE,
                        dataURI: GEAR_IMAGE_SVG_URI,
                    },
                    STEPS : {
                        type: Scratch.ArgumentType.NUMBER,
                        defaultValue: 10,
                    }
                },
            },
```
#### 位置を知らせる（プルダウン付き）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/e90124a2-2b72-9e2a-4ce6-ead636c15fe7.png)

```javascript
            {
                opcode : 'sayPosition',
                blockType : Scratch.BlockType.COMMAND,
                text : '[GEAR_IMAGE] 位置を知らせる [TYPE]',
                arguments: {
                    GEAR_IMAGE : {
                        type: Scratch.ArgumentType.IMAGE,
                        dataURI: GEAR_IMAGE_SVG_URI,
                    },
                    TYPE : {
                        type: Scratch.ArgumentType.STRING,
                        menu: 'FukidashiMenu',  // menusのキー
                        defaultValue: 'say',
                    },
                },
            },
```
#### 〇秒、位置を知らせる（プルダウン付き）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/29d69428-14ca-5324-b73c-17ca34272974.png)

```javascript
            {
                opcode : 'sayPositionForSec',
                blockType : Scratch.BlockType.COMMAND,
                text : '[GEAR_IMAGE] [SECS]秒、位置を知らせる [TYPE]',
                arguments: {
                    GEAR_IMAGE : {
                        type: Scratch.ArgumentType.IMAGE,
                        dataURI: GEAR_IMAGE_SVG_URI,
                    },
                    SECS : {
                        type: Scratch.ArgumentType.NUMBER,
                        defaultValue: 2,
                    },
                    TYPE : {
                        type: Scratch.ArgumentType.STRING,
                        menu: 'FukidashiMenu',  // menusのキー
                        defaultValue: 'say',
                    },
                },
            },
```
```javascript
        ],
```
#### プルダウンメニューの定義
```javascript
        // メニューの定義
        menus: {
            FukidashiMenu: {
                items: [
                    {'text':'話す','value':'say'},
                    {'text':'考える','value':'think'},
                ],
            }
        }
    }
```
#### 拡張機能のクラス
sayPositionForSecメソッドにはasyncをつけている。終わるまで待つ必要がある(await)ため。
```javascript
    class MyExtension {
        getInfo() {
            return MyExtensionInfo;
        }
        /**
         * ロードするJSファイルのURLを設定する
         * @param {*} args 
         * @param {*} util 
         */
        loadJSFileSetting( args, util ){
            this.jsUrl = args.JSURL;
        }
        /**
         * JSファイルをロードする
         * @param {*} args 
         * @param {*} util 
         */
        async setup( args, util ){
            console.log(this.jsUrl)
            try{
                const _t = new Date().getTime();
                const sub = await import(`${this.jsUrl}?_t=${_t}`);
                // 読み込むJSは export {TestJS} をしている前提。
                this.testJS = new sub.TestJS(); 
            }catch(e){
                const message = '読み込みに失敗した、'
                    +'もしくはクラス定義が存在しないみたいです';
                console.error( message, e );
                alert(message);
            }
        }
        moveStep( args, util ) {
            console.log( 'moveStep STEPS=', args.STEPS );
            // sub.js 内のメソッドを実行する
            this.testJS.moveStep(args, util);
        }
        sayPosition( args, util ) {
            console.log( 'sayPosition TYPE=', args.TYPE ); // say or think
            // sub.js 内のメソッドを実行する
            this.testJS.sayPosition(args, util);
        }
        async sayPositionForSec( args, util ) {
            console.log( 'sayPosition TYPE=', args.TYPE ); // say or think
            console.log( 'sayPosition SECS=', args.SECS);   // second
            // sub.js 内のメソッドを実行する
            await this.testJS.sayPositionForSec(args, util);
        }
    }
```
```javascript
    Scratch.extensions.register(new MyExtension());

})(Scratch);
```

# 外部ファイル
[コードGITHUB](https://github.com/amami-harhid/turbowarpExtensions/blob/main/_04_extension/sub.js)

#### 角度(°) を radianに直す共通処理を書いてます
```javascript
const MathUtil = class{
    static degToRad (deg) {
        return deg * Math.PI / 180;
    }
}
```
#### クラスの定義( TestJS )
```javascript
const TestJS = class{
```
#### 向きの方向へ指定したSTEPS分、位置を移動させる処理
Scratch VM のコードを真似しています。
```javascript
    moveStep(args, util){

        // 向きの方向へ STEPSの長さ分 位置を変える（移動させる）
        const steps = Scratch.Cast.toNumber(args.STEPS);
        const radians = MathUtil.degToRad(90 - util.target.direction);
        const dx = steps * Math.cos(radians);
        const dy = steps * Math.sin(radians);
        util.target.setXY(util.target.x + dx, util.target.y + dy);

    }
```
#### 話す処理（ 言う:say,  考える: think )
```javascript
    sayPosition(args, util){
        
        const type = args.TYPE;
        const position = {x:util.target.x, y:util.target.y};
        const message = `x=(${position.x}), y=(${position.y})`;
        util.runtime.emit('SAY', util.target, type, message);
    }
    
```
#### 〇秒間、話す処理（ 言う:say,  考える: think )
〇秒間経過した後に ブランクで言い直しています。
```javascript
    async sayPositionForSec(args, util){

        const type = args.TYPE;
        const position = {x:util.target.x, y:util.target.y};
        const message = `x=(${position.x}), y=(${position.y})`;
        util.runtime.emit('SAY', util.target, type, message);
        const secs = Scratch.Cast.toNumber(args.SECS);
        await this._sleep(secs*1000); // 秒数だけ待つ（次の処理へ進ませない）
        util.runtime.emit('SAY', util.target, type, ''); // ふきだしバブルを消す
    }

```
#### 〇ミリ秒停止させる処理 ( Promiseを使っています )
```javascript
    async _sleep(msec){
        return new Promise(resolve => setTimeout(resolve, msec));
    }
}

```
### 最後につける
```javascript
export {TestJS};
```



# 続き

[次の記事【５】](https://qiita.com/harhid/private/c1ccb5cc4af66ba0cac1)へ続きます。