本記事について
以前より Scratch3.x の『PEN機能で描く線が細くならない』ことが不思議であり、どうにか細くできないかと悩んでいました。
Scratch3.xのPEN機能:一番細い線でも2ピクセルの太さに見えます。
これが1ピクセルの線( P5JSで書いた LINE )
独自にCANVASへ描画すればよいのだろうけど、拡張機能を作ってScratch3.xをビルドしてしまうまではできていませんでした。
つい最近のことですが、Turbowarpの拡張機能として「カスタム拡張機能」が追加されていることに気づきました(いつ頃に追加されたのかわかりませんが)。Javascriptを使って独自に拡張機能を定義してブロック(及び処理)を定義できるとのことです。
これを使ったら独自にScratchのCANVASへ描画する実験ができるのではないかと思いつき、Turbowarpの拡張機能を作る練習をしてみました。
その過程で、Scratch3にもなじみがあるJavascriptの学習者にとって、学習のモチベーションをあげる(もしくは維持する)ための良いツールになるのではないかと直感をもちました。
Turbowarpの「カスタム拡張機能」の作り方を調べてみましたが、拡張機能の素人に優しい情報を見つけることができませんでした。同じことを知りたいと思う人たちが少しはいるだろうと思い、調査結果をまとめてみることにしました。
最終目標は p5jsを動かすこと
Turbowarpのカスタム拡張機能を使い、独自ブロックを作る方法、独自ブロック内にてスプライトを操作する方法、最後にはJavascriptライブラリ(P5.JS)を使えるようにする方法を紹介します。
記事構成
記事は複数に分割しています(【1】~【8】に分割して順次公開予定です)
- 『カスタム拡張機能』を使おう【1】:この記事です(外部JSファイルを動的Import)
- 『カスタム拡張機能』を使おう【2】:拡張機能定義の主要項目を補足したもの
- 『カスタム拡張機能』を使おう【3】:動的Importするファイルを変更できるようにする
- 『カスタム拡張機能』を使おう【4】:外部JSファイルにてScratchブロック処理を再現
- 『カスタム拡張機能』を使おう【5】:外部JSファイルにてスピーチ処理を再現
- 『カスタム拡張機能』を使おう【6】:P5.JSを使ってキャンバス上に『線』を引く
- 『カスタム拡張機能』を使おう【7】: P5.JSを使って『ボール(円)』を描きボールを動かす
TurbowarpでP5JSを動かす(OneBall自由落下)
TurbowarpでP5JSを動かす(複数のBall:自由落下) - 『カスタム拡張機能』を使おう【8】:P5.JSを使って『アートっぽい』作品を作る
主には拡張機能から外部JSファイルの処理を呼び出す方法を解説しています。
外部JSファイルの内容を修正することで、拡張機能本体を修正する必要がなくなります。
【7】【8】あたりまで進むと『試行錯誤』が強くなりますので、外部JSファイルを使うことで作業スピードがとてもあがります。ぜひ最後まで読んでみて試してみてください。
Turbowarpとは
世界中の有志たちにより開発が続けられているScratch3の上位互換のアプリです。
Scratch3で作ったプロジェクトはTurbowarpにてそのまま利用できます。Turbowarpで作成したプロジェクトはScratch3では動作しない可能性が高いので注意してください。名前の由来になっている「高速化」を実現しており、またスクリプト編集操作をより便利にしたり、描画FPSを標準30fpsから変えてみたり、録画をしてみたり、コスチューム編集を便利にしてみたり、「Scratch3にこんな機能があったらいいのに」というScratch3ヘビーユーザーの思いを取り込んでいるアプリみたいです。将来Scratch4を開発するときのための実験場の意味があるのでしょうね(これは個人的見解です)
TurbowarpとScratch3の違い
Turbowarpの拡張機能
Scratch3の拡張機能は全てTurbowarpにて利用可能です、それ以外にもTurbowarp独自の拡張機能がいろいろ用意されています。独自の拡張機能で改良を繰り返し、選りすぐりのものを 将来のScratch4へと取り込まれていくかもしれませんね。
Turbowarpの「カスタム拡張機能」
いつ頃なのかわかりませんが、Turbowarp拡張機能に「カスタム拡張機能」が追加されていました。
この拡張機能を使って独自に作った拡張機能をTurbowarpへ取り込むことができます。本体の再ビルドは不要なので簡単に独自拡張機能を試すことができます。
気をつけること
独自拡張機能を反映させた後に「拡張機能」を修正して再反映させるときには注意が必要です。大部分の場合、Turbowarp本体を再起動した後に カスタム拡張機能による取り込みをやり直す必要があります。
拡張機能の読み込み方法
カスタム拡張機能による Javascriptソースコードの読み込みは URL指定/ファイルアップロード/
テキスト貼り付けの3種類の方法があります。
「サンドボックス」を使うと 独自拡張機能を使う意味が薄れてしまいます。「サンドボックスなし」を選択したいので、ファイル、テキストのどちらかで拡張機能ソースを読み込むことを推奨します。
サンドボックスとは
直訳すると『砂場』ですが、ここでは『分離された環境』という意味です。
サンドボックス上で拡張機能を動かすとTurbowarp の情報へアクセスできませんので、Turbowarp への悪さをガードすることができて安心ですが、スプライトの情報や変数の情報、スプライトの動きの制御もできなくなりますし、キャンバスへの書き込みもできません。
拡張機能を「サンドボックスなし」で実行する場合は、Turbowarp内の情報にアクセスすることができますが、致命的なミスで Turbowarpの中を破壊することもありえます。「サンドボックスなし」の場合にはよくテストしてから公開しましょう。
なお、破壊したときは、Turbowarpを再起動すればたいていの破壊結果は元に戻せます。
私はキャンバスを消してしまう命令を実行してしまい、背景やスプライトを表示できなくなったことがありますが、Turbowarpを再起動すれば元通りでした。
独自拡張機能の保存
独自拡張機能を含めてプロジェクトを保存することができます。保存したプロジェクトを読み込むと、カスタム拡張機能も一緒に読み込みをします。次のように注意喚起される場合は、「許可」をクリックしてあげましょう。
独自の拡張機能で出来ること
Scratch3ブロックで実現できていることは、全部出来ます(それなりの苦労を要しますがとても勉強になりますよ)。
標準のブロック機能を改良して、別ブロックを用意することができます。
標準機能にない機能を書き下ろすこともできます。
Turbowarpのキャンバスへ「何かしらの」アクションをすることができます。スプライトを動かしたりできますし、Javascriptを使って線を書いたり図形を描いたりできます。
キャンバス
キャンバス(Canvas)への操作は 2D / WEBGL 系の2つの操作があります。
同じキャンバスへ対して 2D / WEBGL系 の操作を混在させることができません。操作をするためにはコンテキストを作り出す必要がありますが、WEBGL用のコンテキストを作った後に2D操作用のコンテキストを作ることができません。Scratch3 ( Turbowarpも同じ)では WEBGL2 による操作を標準としているので、独自に作る拡張機能にてキャンバス操作をする場合は、WEBGL2のコンテキストを経由させる必要があります。
独自拡張機能を使ってやってみたいこと
Javascriptを使った描画系のライブラリの有名どころとして p5.js があります。
p5.js を利用できるようにTurbowarpの独自機能を作りたいと思います。p5のJavascriptソースを作るとき、何回も修正を繰り返すことになると思いますが、その都度、拡張機能の読み取りを繰り返すわけにはいきません(面倒です)。
練習用のJavascriptを修正した後、Turbowarp本体の再起動無しで、ソース修正を反映できるようにしてみたいと思います。
独自機能の練習その1
最初に 簡単な独自ブロックを作ってみましょう
独自機能のソースを書くときの大事なお約束がありますので、それらの確認をしながら進めたいと思います。
基本構造
((Scratch) => {
})(Scratch);
クラスの定義と拡張機能登録の記述
((Scratch) => {
class MyExtension {
}
Scratch.extensions.register( new MyExtension() );
})(Scratch);
他の拡張機能と区別するための定義を書きましょう
((Scratch) => {
const MyExtensionInfo = {
// 拡張機能のID,他拡張機能と被るIDはNG である
// なおサンプルの値が大文字なのはあまり意味はない。
// これなら他拡張機能IDと被らないだろうと思う文字列を書くことだけがルール。
id : 'MYEXTENSION',
// 拡張機能のブロックリストに表記される名前。これが他機能と被っても拡張機能の動作には
// 影響はしないので気軽に命名してよい。
// 使ってくれるかもしれないお友達が「わかりにくい」といって怒るかもしれないので注意せよ。
name : '独自拡張練習',
}
class MyExtension {
getInfo() {
return MyExtensionInfo;
}
}
Scratch.extensions.register( new MyExtension() );
})(Scratch);
独自ブロックの定義
((Scratch) => {
const MyExtensionInfo = {
id : 'MYEXTENSION',
name : '独自拡張練習',
blocks : [
],
}
class MyExtension {
getInfo() {
return MyExtensionInfo;
}
}
Scratch.extensions.register( new MyExtension() );
})(Scratch);
ブロックの定義を書く
((Scratch) => {
const MyExtensionInfo = {
id : 'MYEXTENSION',
name : '独自拡張練習',
blocks : [
{
opcode : 'block01',
blockType : Scratch.BlockType.COMMAND,
text : 'ブロック01',
},
],
}
class MyExtension {
getInfo() {
return MyExtensionInfo;
}
block01 ( args, util ) { // 引数は2つ必要です。
// いまは何もしないので空っぽです。
}
}
Scratch.extensions.register( new MyExtension() );
})(Scratch);
独自拡張機能の定義を反映してみよう
ソースコードのテキストをコピー
Turbowarp の拡張機能
ブラウザで開いておくことを推奨します。
カスタム拡張機能を開く
テキストタブ
独自拡張機能のソースコードを張り付ける
『読み込み』をクリック
独自拡張機能を追加できると、ブロックが表示されます。
この段階では、Turbowarp上にブロックを表示してブロックを実行しても何も起こりません。ブロックには引数を与える場所もありません。
独自ブロックの定義が動作することを確認してみよう
class MyExtension {
getInfo() {
return MyExtensionInfo;
}
block01 ( args, util ) {
console.log('block01が動作したよ');
}
}
⇒ Turbowarp 本体を再起動したうえで、カスタム拡張機能 をやり直します。
ブラウザの『開発者ツール』(コンソール)にてログを確認できます。
独自機能の練習その2
他のブロックを追加します(ブロック02)
((Scratch) => {
const MyExtensionInfo = {
id : 'MYEXTENSION',
name : '独自拡張練習',
blocks : [
{
opcode : 'block01',
blockType : Scratch.BlockType.COMMAND,
text : 'ブロック01',
},
{
opcode : 'block02',
blockType : Scratch.BlockType.COMMAND,
text : 'ブロック02 [TEXT]',
arguments: {
TEXT : {
type: Scratch.ArgumentType.STRING,
defaultValue: 'あいうえお',
}
},
},
],
}
class MyExtension {
getInfo() {
return MyExtensionInfo;
}
block01 ( args, util ) {
console.log('block01が動作したよ');
}
block02( args, util ) {
console.log( 'block02 TEXT=', args.TEXT );
}
}
Scratch.extensions.register( new MyExtension() );
})(Scratch);
独自機能の練習その3
ブロックへ画像を埋め込みしてみましょう。コード
((Scratch) => {
const GEAR_IMAGE_SVG_URI = 'https://amami-harhid.github.io/turbowarpExtensions/assets/gear.svg';
const MyExtensionInfo = {
id : 'MYEXTENSION',
name : '独自拡張練習',
blocks : [
{
opcode : 'block01',
blockType : Scratch.BlockType.COMMAND,
text : '[GEAR_IMAGE] ブロック01',
arguments: {
GEAR_IMAGE : {
type: Scratch.ArgumentType.IMAGE,
dataURI: GEAR_IMAGE_SVG_URI,
},
},
},
{
opcode : 'block02',
blockType : Scratch.BlockType.COMMAND,
text : '[GEAR_IMAGE] ブロック02 [TEXT]',
arguments: {
GEAR_IMAGE : {
type: Scratch.ArgumentType.IMAGE,
dataURI: GEAR_IMAGE_SVG_URI,
},
TEXT : {
type: Scratch.ArgumentType.STRING,
defaultValue: 'あいうえお',
}
},
},
],
}
// 以下略
独自機能の練習その4
ブロックの色、周囲の色を変える。
コード
const MyExtensionInfo = {
id : 'MYEXTENSION',
name : '独自拡張練習',
color1 : '#000000', // 背景を黒に
color2 : '#ffffff', // ブロックリストの円周の色( 白 )
color3 : '#0000ff', // ブロックの周囲の線の色( 青 )
blocks : [
// 略
],
}
// 以下略
定義をまとめたもの
ここにまとめました。
Turbowarpの『カスタム拡張機能』を使おう【2】
独自機能の練習その5
拡張機能ソースから 外部JSファイルを読み込む練習をします。
ここでは、MS-Code で LiveServer を使い、http 通信可能としておくことにします。
適当な場所に ( ここでは拡張機能ソースファイルがある場所 ) 、ファイル sub.js を作っておきます。
const TestJS = class{
method01() {
console.log( 'Test.method01', '実行しました' );
}
}
export { TestJS };
これを 次の要領で読み込みをします。
const SUB = import('http://127.0.0.1:5500/~~/sub.js');
// SUB.TestJS の形で クラスを参照できます
独自拡張機能側
((Scratch) => {
const GEAR_IMAGE_SVG_URI = 'https://amami-harhid.github.io/turbowarpExtensions/assets/gear.svg';
// ★import用の定義
const SUB_HOST = 'http://127.0.0.1:5500';
const SUB_PATH = 'turbowarpGithub/turbowarpExtensions/_02_extension';
const SUB_FILE = 'sub.js';
// 途中省略
class MyExtension {
getInfo() {
return MyExtensionInfo;
}
block01( args, util ) {
console.log('block01が動作したよ');
// ★sub.jsを読み込む
const SUB = import(`${SUB_HOST}/${SUB_PATH}/${SUB_FILE}`);
// ★TestJS のインスタンス作成
this.testJS = new SUB.TestJS();
}
block02( args, util ) {
console.log( 'block02 TEXT=', args.TEXT );
// ★sub.js 内のメソッドを実行する
this.testJS.method01();
}
}
実行するとエラーが起きます。
import 処理は 時間がかかるので 非同期処理として並行して 次のステップが動作し始めます。
つまり import が終わっていないのに、sub.js 内のクラスを使おうとしています。
importが終わるまで、次のステップにいかないように待機するようにしましょう。
importの前に『await』をつけ、block01メソッドに『async』をつけましょう。
修正版
// 途中省略
class MyExtension {
getInfo() {
return MyExtensionInfo;
}
// ★ async をつける( await を使うとき async をつけないといけない )
async block01( args, util ) {
console.log('block01が動作したよ');
// ★sub.jsを読み込む
// ★await をつける
const SUB = await import(`${SUB_HOST}/${SUB_PATH}/${SUB_FILE}`);
// ★TestJS のインスタンス作成
this.testJS = new SUB.TestJS();
}
block02( args, util ) {
console.log( 'block02 TEXT=', args.TEXT );
// ★sub.js 内のメソッドを実行する
this.testJS.method01();
}
}
import が終わってから クラスのインスタンス化を行うことで、無事に sub.js のメソッドを
実行できました。
sub.js をちょっと修正してみましょう
const TestJS = class{
method01() {
console.log( 'TestJS.method01', '実行しましたよ~ん' ); // ⇒ 修正しました
}
}
export { TestJS };
それで、ブロック01、ブロック02を実行してみます。
sub.js を修正しているはずなのに、ログのテキストは変わりません。この原因は、読み込んだ JSファイルのキャッシュがあるからです。同じファイルを読み込むとき、前回読み込んであるファイルのデータをため込んでおき、次のアクセスのときには実際の読み込みは発生せずに、ため込んであるキャッシュの情報を使う仕組みであり、変更されたsub.js の内容を読み込めていないのです。
キャッシュ回避
キャッシュを使わないようにキャッシュ回避の技を使いましょう。
修正版
// 途中省略
class MyExtension {
getInfo() {
return MyExtensionInfo;
}
async block01( args, util ) {
console.log('block01が動作したよ');
// ★sub.jsを読み込む
// キャッシュ回避のためいつも違う文字列にする
const QUERY = new Date().getTime();
const SUB = await import(`${SUB_HOST}/${SUB_PATH}/${SUB_FILE}?_t=${QUERY}`);
this.testJS = new SUB.TestJS();
}
block02( args, util ) {
console.log( 'block02 TEXT=', args.TEXT );
this.testJS.method01();
}
}
new Date().getTime() は、「1970年1月1日 00:00:00」から経過しているミリ秒を返します。
import で読み込む URL の最後に、?_t=(経過ミリ秒) とすることで、import するたびに 異なるURLを作るとができます。これでキャッシュ回避を行います。
const TestJS = class{
method01() {
console.log( 'Test.method01', '実行しました' ); // ⇒ 修正しました
}
}
export { TestJS };
次に Turbowarp を再起動せずに sub.js を修正します。
const TestJS = class{
method01() {
console.log( 'TestJS.method01', '実行しましたよ~ん' ); // ⇒ 修正しました
}
}
export { TestJS };
これで実行します。
続き
次の記事【2】へ続きます。