例題27-1:[上級]PsychoPy Builderが生成するPsychoJSのコードを読む

B: なんとなんと、これでまた数年は出番がないのかと思ったらいきなりの更新ですよ。

A: うむ。実に久しぶりに「怒り」を感じたのでな。クソ忙しい中この作業に時間を割くにはそうでもしないとやってられないような怒りが必要なのだ。

B: はぁ。なんだかよくわかりませんが、それってここ何年も怒る(?)ことがなかったってことですかね。ある意味幸せな話ですね。

A: 怒ることがないかだとぅぉおおぉ? そんなことあるもんか、そもそもだな、うちの×$%#*=…

B: (テキトーに相槌を打ちながら聞き流す)

A: …やっちゅうの。ほんまにふざけとんのか! はぁはぁ。

B: どうどうどう。そろそろ落ち着きましたか?

A: はぁはぁ。な、なんとか。

B: まぁAさんがいろいろアレでソレなことはわかりました。で、今回の怒りというのは何ですのん。

A: うむ。もうずいぶん前の話になるのだが、PsychoPy Builderにブラウザでオンライン実験を行うためのJavaScriptのコードを出力する機能が実装されたのだ。

B: ブラウザでオンライン実験?

A: インターネット上のサーバに実験のコードをアップロードして、実験参加者にはChromeやFirefoxといったインターネットブラウザでアクセスしてもらう。するとブラウザの画面内に実験情報ダイアログが表示されて、OKするとそのままブラウザ上で実験が始まる。

B: へ? ブラウザ上でっていうことは、実験参加者が自宅のPCで実行できる? 実験参加者のPCにはPsychoPyがなくてもいい?

A: Yes.

B: ちゃんとフルスクリーンで実行できるんですか? キーボードとかマウスで反応できる?

A: もちろんYes. そうじゃないと意味ないだろ。

B: ええええぇぇ。そんなことが出来るんですか!なんでもっと早く取り上げないんですか!

A: まぁ最初のうちは試験的なもので全然実用レベルじゃなかったからな。最近かなり安定してきたと思うがまだまだBuilderの実験が何でもそのままオンライン化というわけにはいかない。

B: むむぅ。そうなんですか。

A: 特に気に入らないのがJavaScriptのコードがよくわかんねーということ。最近になってようやくリポジトリにはまともなドキュメントが追加されたが、今までCodeコンポーネントを使ってあれこれしていた実験をJavaScriptにどう移植すればいいのかやっぱりよくわかんねーし。

B: ええと、確認させてください。JavaScriptってホームページ書いたりするときにコピペするアレですよね?

A: …その言い方はどうかと思うがまあいいとしよう。

B: CodeコンポーネントをJavaScriptに移植するとは…?

A: うむ、簡単に言うとだな、PsychoPyはPythonのパッケージであり、PsychoPy BuilderってのはPsychoPyを使って動作するPythonのスクリプトを人間に代わって書いてくれるわけだ。それはわかるか?

B: はあ。なんとなく。

A: で、今回取り上げる機能ってのは、Pythonのスクリプトの代わりにJavaScriptのスクリプトを出力するのだ。ブラウザで実行できるように。PsychoPyの機能のサブセットをJavaScriptに移植したPsychoJSというライブラリが並行して開発されていて、こいつを使ったコードが出力されるわけだね。

B: むむむむ。3%くらいわかったような気がします。

A: で、問題のCodeコンポーネントだが、Codeコンポーネントには実験作成者が自由にPythonのコードを記入することが出来る。このコードでは何が行われるのかBuilderはわかんないので、JavaScriptに変換したくてもどうしようもない。

B: 書いてあるんだからわかるんじゃないですか?

A: BuilderにCodeコンポーネントに記入されたコードを解釈せよと? そんなこと出来るもんかよ。

B: えー、出来ないんですかぁ?

A: 何が難しいのか説明する気力もないけど、とにかく難しいと思っておきなさい。まぁとにかくそんなわけで、Codeコンポーネントを使っている実験はそのままではJavaScriptの世界に持っていけない。そこで、最近のBuilderではCodeコンポーネントにJavaScriptのコードをそのまま入力できるようになっている。実験作成者が自分で翻訳してねというわけだ。

B: えー。じゃあJavaScriptも覚えないといけないんですか?

A: 今まで通り、PCにインストールされたPsychoPy Builderで実験を実行する場合はJavaScriptなんて全く覚えるは必要ない。あくまでオンライン実験を作成したい場合だけだ。

B: そうですか。じゃあぼくは今まで通りで我慢します。んじゃ失礼しますね。

A: こらこら待たんか。B君がいないと私がしゃべりにくいだろ。

B: そんなの知りませんよ。どうしてもというのなら今日の晩御飯で手を打ちましょう。

A: むむむ。仕方あるまい。

B: やった! それでは続きをどうぞ。

A: …それでだ。このオンライン実験についてはいずれちゃんとした解説を作らないといけないと思っていてな。でも全然時間がないんだわ。特に足枷になっているのがこのCodeコンポーネントの扱いでな。

B: ふむふむ。(とても意欲的に)

A: で、ちょっと今日は(自主規制)が(自主規制)でな、それでもうブチ切れてだな、いっぺんBuilderが吐いてるJavaScriptのコードを全部読んだろうやないか!と思ったわけだ。どんなコードを吐いてるかわかればCodeコンポーネントをどう翻訳すればええかわかるやろ。

B: ふむふむ、それで?

A: というわけでPsychoPy 3.1.2のBuilderでループもコンポーネントもない「空っぽ」な実験を作成してJavaScriptコードを出力させてみた。

/*****************
 * Hml_Test Test *
 *****************/

import { PsychoJS } from 'https://pavlovia.org/lib/core.js';
import * as core from 'https://pavlovia.org/lib/core.js';
import { TrialHandler } from 'https://pavlovia.org/lib/data.js';
import { Scheduler } from 'https://pavlovia.org/lib/util.js';
import * as util from 'https://pavlovia.org/lib/util.js';
import * as visual from 'https://pavlovia.org/lib/visual.js';
import { Sound } from 'https://pavlovia.org/lib/sound.js';

// init psychoJS:
var psychoJS = new PsychoJS({
  debug: true
});

// open window:
psychoJS.openWindow({
  fullscr: true,
  color: new util.Color([0, 0, 0]),
  units: 'height'
});

// store info about the experiment session:
let expName = 'hml_test';  // from the Builder filename that created this script
let expInfo = {'participant': '', 'session': '001'};

// schedule the experiment:
psychoJS.schedule(psychoJS.gui.DlgFromDict({
  dictionary: expInfo,
  title: expName
}));

const flowScheduler = new Scheduler(psychoJS);
const dialogCancelScheduler = new Scheduler(psychoJS);
psychoJS.scheduleCondition(function() { return (psychoJS.gui.dialogComponent.button === 'OK'); }, flowScheduler, dialogCancelScheduler);

// flowScheduler gets run if the participants presses OK
flowScheduler.add(updateInfo); // add timeStamp
flowScheduler.add(experimentInit);
flowScheduler.add(trialRoutineBegin);
flowScheduler.add(trialRoutineEachFrame);
flowScheduler.add(trialRoutineEnd);
flowScheduler.add(quitPsychoJS, '', true);

// quit if user presses Cancel in dialog box:
dialogCancelScheduler.add(quitPsychoJS, '', false);

psychoJS.start({expName, expInfo});

var frameDur;
function updateInfo() {
  expInfo['date'] = util.MonotonicClock.getDateStr();  // add a simple timestamp
  expInfo['expName'] = expName;
  expInfo['psychopyVersion'] = '3.1.1';

  // store frame rate of monitor if we can measure it successfully
  expInfo['frameRate'] = psychoJS.window.getActualFrameRate();
  if (typeof expInfo['frameRate'] !== 'undefined')
    frameDur = 1.0/Math.round(expInfo['frameRate']);
  else
    frameDur = 1.0/60.0; // couldn't get a reliable measure so guess

  // add info from the URL:
  util.addInfoFromUrl(expInfo);

  return Scheduler.Event.NEXT;
}

var trialClock;
var globalClock;
var routineTimer;
function experimentInit() {
  // Initialize components for Routine "trial"
  trialClock = new util.Clock();
  // Create some handy timers
  globalClock = new util.Clock();  // to track the time since experiment started
  routineTimer = new util.CountdownTimer();  // to track time remaining of each (non-slip) routine

  return Scheduler.Event.NEXT;
}

var t;
var frameN;
var trialComponents;
function trialRoutineBegin() {
  //------Prepare to start Routine 'trial'-------
  t = 0;
  trialClock.reset(); // clock
  frameN = -1;
  // update component parameters for each repeat
  // keep track of which components have finished
  trialComponents = [];

  for (const thisComponent of trialComponents)
    if ('status' in thisComponent)
      thisComponent.status = PsychoJS.Status.NOT_STARTED;

  return Scheduler.Event.NEXT;
}

var continueRoutine;
function trialRoutineEachFrame() {
  //------Loop for each frame of Routine 'trial'-------
  let continueRoutine = true; // until we're told otherwise
  // get current time
  t = trialClock.getTime();
  frameN = frameN + 1;// number of completed frames (so 0 is the first frame)
  // update/draw components on each frame
  // check for quit (typically the Esc key)
  if (psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({keyList:['escape']}).length > 0) {
    return psychoJS.quit('The [Escape] key was pressed. Goodbye!', false);
  }

  // check if the Routine should terminate
  if (!continueRoutine) {  // a component has requested a forced-end of Routine
    return Scheduler.Event.NEXT;
  }

  continueRoutine = false;  // reverts to True if at least one component still running
  for (const thisComponent of trialComponents)
    if ('status' in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED) {
      continueRoutine = true;
      break;
    }

  // refresh the screen if continuing
  if (continueRoutine) {
    return Scheduler.Event.FLIP_REPEAT;
  }
  else {
    return Scheduler.Event.NEXT;
  }
}


function trialRoutineEnd() {
  //------Ending Routine 'trial'-------
  for (const thisComponent of trialComponents) {
    if (typeof thisComponent.setAutoDraw === 'function') {
      thisComponent.setAutoDraw(false);
    }
  }
  // the Routine "trial" was not non-slip safe, so reset the non-slip timer
  routineTimer.reset();

  return Scheduler.Event.NEXT;
}


function endLoopIteration(thisScheduler, thisTrial) {
  // ------Prepare for next entry------
  return function () {
    // ------Check if user ended loop early------
    if (currentLoop.finished) {
      thisScheduler.stop();
    } else if (typeof thisTrial === 'undefined' || !('isTrials' in thisTrial) || thisTrial.isTrials) {
      psychoJS.experiment.nextEntry();
    }
  return Scheduler.Event.NEXT;
  };
}


function importConditions(loop) {
  const trialIndex = loop.getTrialIndex();
  return function () {
    loop.setTrialIndex(trialIndex);
    psychoJS.importAttributes(loop.getCurrentTrial());
    return Scheduler.Event.NEXT;
    };
}


function quitPsychoJS(message, isCompleted) {
  psychoJS.window.close();
  psychoJS.quit({message: message, isCompleted: isCompleted});

  return Scheduler.Event.QUIT;
}

B: うへあ! JavaScriptって今までコピペしたことしかないのでよくわかんないんですよね。

A: JavaScriptのことを解説している余裕はないので自分で何とかしてくれたまえ。ここでは流れを追ってみよう。

  1. モジュールをimportする(5-11行目)

  2. PsychoJSを初期化する(13-16行目)

  3. ウィンドウを開く(18-23行目)

  4. 実験ダイアログとスケジューラ準備する(25-37行目)

  5. フローのスケジューラに実験情報やルーチンなどを追加する(39-48行目)

  6. PsychoJSをスタートする(50行目)

A: メインの流れはこんなもんか。後は関数の定義。

B: まだ1/3も進んでいませんがもう終わりなんですか?

A: まあ待て。ここから先は関数の定義なわけだが、以下の関数が定義されている。関数の機能はコードを眺めながらテキトーに書いた。

  • updateInfo() 実行日時や実験名、フレームレートなどを登録する

  • experimentInit() タイマーの初期化などをする。

  • trialRoutineBegin() trialルーチン開始時の処理を定義する

  • trialRoutineEachFrame() trialルーチンのフレーム毎の処理を定義する

  • trialRoutineEnd() trialルーチンの終了時の処理を定義する

  • endLoopIteration() ループの繰り返し/終了時の処理を定義する

  • importConditions() 現在の繰り返しにおける条件の値を得る処理を定義する

  • quitPsychJS() 終了時の処理を定義する

B: テキトーに書いたって、あんた。

A: まぁ、読んでいけばそのうち正確なところはわかるだろ。

B: そんないい加減な。

A: 注目すべきはおなじみの変数がすでにいくつか見つかることだね。実験情報を保持しているexpInfoとか、ルーチンの終了判定に用いるcontinueRoutineとか。expInfo['participant']で実験情報ダイアログのparticipantの値が得られることとか、continueRoutine = falseでルーチンを強制終了できるのも同じだ。いいぞいいぞ。tやframeN、trialClockやglobalClockなんてのもあるな。よしよし。

B: むはー。よくこれだけでわかりますな。

A: さて、んじゃこのコードはこれくらいにしておいて、先に進もうか。この「空っぽ」の実験にTextコンポーネントを置いただけの実験を出力してみる。

B: えっ、もう終わりなんですか。もっと詳しく見なくていいんですか。

A: そんなんしんどいやん。処理を加えた時にコードがどう変わるのかを見た方が手っ取り早い。そら、もう出来たぞ。

B: 操作方法は説明しなくていいんですか。

A: それはもっと時間がある時に丁寧に。まずexperimentInit()に以下のコードが追加されたぞ。強調表示されている部分が追加された行だ。

var trialClock;
var text;
var globalClock;
var routineTimer;
function experimentInit() {
  // Initialize components for Routine "trial"
  trialClock = new util.Clock();
  text = new visual.TextStim({
    win: psychoJS.window,
    name: 'text',
    text: 'Message',
    font: 'Arial',
    units : undefined,
    pos: [0, 0], height: 0.1,  wrapWidth: undefined, ori: 0,
    color: new util.Color('white'),  opacity: 1,
    depth: 0.0
  });

B: TextStimオブジェクトが作成されて変数textに代入されている、でいいですか。

A: OK, OK. よくわかっているじゃないか。

B: それ以外にどう読めというんですか。バカにされている気がする。

A: 続いてRoutienBegin()。

var t;
var frameN;
var trialComponents;
function trialRoutineBegin() {
  //------Prepare to start Routine 'trial'-------
  t = 0;
  trialClock.reset(); // clock
  frameN = -1;
  routineTimer.add(1.000000);
  // update component parameters for each repeat
  // keep track of which components have finished
  trialComponents = [];
  trialComponents.push(text);

  for (const thisComponent of trialComponents)
    if ('status' in thisComponent)
      thisComponent.status = PsychoJS.Status.NOT_STARTED;

  return Scheduler.Event.NEXT;
}

B: routineTimerってルーチンの時間を測る時計ですか。

A: その通り。先ほどの空っぽのルーチンと違って、実行時間1秒(つまりは初期値)のTextコンポーネントが置かれたので時間を計測しなきゃいけなくなったのだな。だから追加されている。

B: あとはtrialComponents.push(text)というのは…

A: Pythonのappend()のようなものだと思えばよいな。このルーチンで使用するコンポーネントを配列に追加しているわけだ。

B: ふむふむ。

A: 続いてtrialRoutineEachFrame()。ちょっと長いが全部引用しよう。

var frameRemains;
var continueRoutine;
function trialRoutineEachFrame() {
  //------Loop for each frame of Routine 'trial'-------
  let continueRoutine = true; // until we're told otherwise
  // get current time
  t = trialClock.getTime();
  frameN = frameN + 1;// number of completed frames (so 0 is the first frame)
  // update/draw components on each frame

  // *text* updates
  if (t >= 0.0 && text.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    text.tStart = t;  // (not accounting for frame time here)
    text.frameNStart = frameN;  // exact frame index
    text.setAutoDraw(true);
  }

  frameRemains = 0.0 + 1.0 - psychoJS.window.monitorFramePeriod * 0.75;  // most of one frame period left
  if (text.status === PsychoJS.Status.STARTED && t >= frameRemains) {
    text.setAutoDraw(false);
  }
  // check for quit (typically the Esc key)
  if (psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({keyList:['escape']}).length > 0) {
    return psychoJS.quit('The [Escape] key was pressed. Goodbye!', false);
  }

  // check if the Routine should terminate
  if (!continueRoutine) {  // a component has requested a forced-end of Routine
    return Scheduler.Event.NEXT;
  }

  continueRoutine = false;  // reverts to True if at least one component still running
  for (const thisComponent of trialComponents)
    if ('status' in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED) {
      continueRoutine = true;
      break;
    }

  // refresh the screen if continuing
  if (continueRoutine && routineTimer.getTime() > 0) {
    return Scheduler.Event.FLIP_REPEAT;
  }
  else {
    return Scheduler.Event.NEXT;
  }
}

B: これは難しいな…

A: そうかね? まず// *text* updates とコメントがある部分。

B: あ、それコメントなんですか。

A: これは普段からBuilderが吐くPythonのコードを見ていれば「あぁ、なるほど」って感じだ。

B: (みんなAさんみたいな変態じゃないんだから…まったく…)

A: Textコンポーネントの開始、終了時刻などの条件を確認して描画のON/OFFを決めているわけだな。ここで見ておきたいのは、t >= 0.0とかいう具合に変数tを用いてルーチン開始後の経過時間を参照していること、そしてPsychoJS.Status.NOT_STARTEDという定数。つまり、Builderの内部変数tを使うテクニックは引き続き使える。そしてNOT_STARTEDなどの内部定数もちょっと書き方が違うが使えるということ。

B: tの話はぼくにもわかります。NOT_STARTEDとかいうのは…久しぶりでよく覚えていませんが、要するに良いんですね?

A: 良いんです。実に良い。あとは最後の方でルーチンの終了判定をしているところ。continueRoutine && routineTimer.getTime() > 0とあるんだから、まずcontinueRoutineを使ったテクニックも引き続きいける。時計オブジェクトから現在時刻を得るメソッドがgetTime()なのも同じ。後はコメント行がちょっと変わっている部分があるが、コード自体の変更点はこれだけ。

B: 意外と少なかったですね。

A: ほんじゃ次。この実験にtrialsというループを加えてみて、条件ファイルにcnd.xlsxを指定してJavaScriptを吐かせてみる。すると最初のスケジューラへいろいろ登録する部分に以下の行が追加される。

// flowScheduler gets run if the participants presses OK
flowScheduler.add(updateInfo); // add timeStamp
flowScheduler.add(experimentInit);
const trialsLoopScheduler = new Scheduler(psychoJS);
flowScheduler.add(trialsLoopBegin, trialsLoopScheduler);
flowScheduler.add(trialsLoopScheduler);
flowScheduler.add(trialsLoopEnd);
flowScheduler.add(quitPsychoJS, '', true);

B: えっと、これは…

A: 名前からしてだいたいわかるよな。ループを制御する新たなスケジューラであるtrialsLoopSchedulerというのが出来ていて、そしてtrialsLoopBegin、trialsLoopScheduler、trialsLoopEndというものをフローに登録している。

B: よくわかりませんが…

A: trialsLoopBegin、trialsLoopEndは、先ほどtrialルーチンに対して類似のものがあったように関数だ。追加されたコードで以下のように定義されている。

var trials;
var currentLoop;
function trialsLoopBegin(thisScheduler) {
  // set up handler to look after randomisation of conditions etc
  trials = new TrialHandler({
    psychoJS: psychoJS,
    nReps: 5, method: TrialHandler.Method.RANDOM,
    extraInfo: expInfo, originPath: undefined,
    trialList: 'cnd.xlsx',
    seed: undefined, name: 'trials'});
  psychoJS.experiment.addLoop(trials); // add the loop to the experiment
  currentLoop = trials;  // we're now the current loop

  // Schedule all the trials in the trialList:
  for (const thisTrial of trials) {
    thisScheduler.add(importConditions(trials));
    thisScheduler.add(trialRoutineBegin);
    thisScheduler.add(trialRoutineEachFrame);
    thisScheduler.add(trialRoutineEnd);
    thisScheduler.add(endLoopIteration(thisScheduler, thisTrial));
  }

  return Scheduler.Event.NEXT;
}


function trialsLoopEnd() {
  psychoJS.experiment.removeLoop(trials);

  return Scheduler.Event.NEXT;
}

B: 強調表示されていませんが、どの行が追加された行ですか?

A: 全部が追加された行である。

B: ふへっ、そうでしたか。

A: まあ、あまり見る部分はないが…と思ったがいろいろと面白いな。まずTrialHandlerというオブジェクトでExcelファイルから読み込んだ条件を管理していること。これもPythonのコードと同じだな。あとはループがなかった時にはフローに直接追加されていたtrialルーチンがtrialsLoopBegin()内でループに対して追加されていること。なるほどなるほど、そうやって階層的なループ構造を管理するのか。ふむふむ。

B: 一人で納得しないでください。

A: Builderに頼らずPsychoJSを使ったJavaScriptコードを自分で書こうという人にとってはとっても重要だが、Codeコンポーネントの書き換え方を探るにはとりあえずこんなもんで十分かな。次。

B: 早っ!

A: ここでTextコンポーネントの文字列を「繰り返し毎に更新」にして、コードがどう変わるのか見てみた。意外と簡単。本質的には以下の1行が増えただけ。

var t;
var frameN;
var trialComponents;
function trialRoutineBegin() {
  //------Prepare to start Routine 'trial'-------
  t = 0;
  trialClock.reset(); // clock
  frameN = -1;
  routineTimer.add(1.000000);
  // update component parameters for each repeat
  text.setText(message);
  // keep track of which components have finished
  trialComponents = [];
  trialComponents.push(text);

  for (const thisComponent of trialComponents)
    if ('status' in thisComponent)
      thisComponent.status = PsychoJS.Status.NOT_STARTED;

  return Scheduler.Event.NEXT;
}

B: 本質的に、とは?

A: もう一行、TextStimオブジェクトの初期化時にとりあえず渡しておく文字列が変わっているんだけど、これはまあなんでもいいから。ルーチンの開始時にsetText()を実行するだけだね。引数messageはExcelファイルの列名から来ていて、これはPythonのコードを出力する時と同じ。TextStimオブジェクトのsetText()メソッドで文字列を更新できるのも同じ。わかりやすい。

B: Aさんの様子を見ているとそんなに難しくないようですね。…ぼくにはさっぱりですが。

A: 調子に乗って、Textコンポーネントをもうひとつ追加して(名前はtext_2)、そっちは「フレーム毎に更新」にして内部変数tの値を表示するようにしてみた。変更点はこんな感じ。まずexperimentInit()にtext_2が追加された。これはすべてが追加された行。あともう省略するけどこの前にvar text_2も宣言されている。

text_2 = new visual.TextStim({
  win: psychoJS.window,
  name: 'text_2',
  text: 'default text',
  font: 'Arial',
  units : undefined,
  pos: [0, 0], height: 0.1,  wrapWidth: undefined, ori: 0,
  color: new util.Color('white'),  opacity: 1,
  depth: -1.0
});

B: 宣言というのはC言語みたいに変数を宣言するという事ですかね。

A: 細かい話は置いといて、まあそれでいいよ。trial_2が配置されているtrialルーチンの初期化部でpush(text_2)が追加されている。

var t;
var frameN;
var trialComponents;
function trialRoutineBegin() {
  //------Prepare to start Routine 'trial'-------
  t = 0;
  trialClock.reset(); // clock
  frameN = -1;
  routineTimer.add(1.000000);
  // update component parameters for each repeat
  text.setText(message);
  // keep track of which components have finished
  trialComponents = [];
  trialComponents.push(text);
  trialComponents.push(text_2);

  for (const thisComponent of trialComponents)
    if ('status' in thisComponent)
      thisComponent.status = PsychoJS.Status.NOT_STARTED;

  return Scheduler.Event.NEXT;
}

B: ふむふむ。

A: 続いてtrialRoutineEachFrame()への追加だが…。ちょっと長くなるけど挿入部がわかりやすいように全体を引用するか。

var frameRemains;
var continueRoutine;
function trialRoutineEachFrame() {
  //------Loop for each frame of Routine 'trial'-------
  let continueRoutine = true; // until we're told otherwise
  // get current time
  t = trialClock.getTime();
  frameN = frameN + 1;// number of completed frames (so 0 is the first frame)
  // update/draw components on each frame

  // *text* updates
  if (t >= 0.0 && text.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    text.tStart = t;  // (not accounting for frame time here)
    text.frameNStart = frameN;  // exact frame index
    text.setAutoDraw(true);
  }

  frameRemains = 0.0 + 1.0 - psychoJS.window.monitorFramePeriod * 0.75;  // most of one frame period left
  if (text.status === PsychoJS.Status.STARTED && t >= frameRemains) {
    text.setAutoDraw(false);
  }

  // *text_2* updates
  if (t >= 0.0 && text_2.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    text_2.tStart = t;  // (not accounting for frame time here)
    text_2.frameNStart = frameN;  // exact frame index
    text_2.setAutoDraw(true);
  }

  frameRemains = 0.0 + 1.0 - psychoJS.window.monitorFramePeriod * 0.75;  // most of one frame period left
  if (text_2.status === PsychoJS.Status.STARTED && t >= frameRemains) {
    text_2.setAutoDraw(false);
  }

  if (text_2.status === PsychoJS.Status.STARTED){ // only update if being drawn
    text_2.setText(t);
  }
  // check for quit (typically the Esc key)
  if (psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({keyList:['escape']}).length > 0) {
    return psychoJS.quit('The [Escape] key was pressed. Goodbye!', false);
  }

  // check if the Routine should terminate
  if (!continueRoutine) {  // a component has requested a forced-end of Routine
    return Scheduler.Event.NEXT;
  }

  continueRoutine = false;  // reverts to True if at least one component still running
  for (const thisComponent of trialComponents)
    if ('status' in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED) {
      continueRoutine = true;
      break;
    }

  // refresh the screen if continuing
  if (continueRoutine && routineTimer.getTime() > 0) {
    return Scheduler.Event.FLIP_REPEAT;
  }
  else {
    return Scheduler.Event.NEXT;
  }
}

B: 追加されたコードは全部まとまっているんですね。

A: まぁ、こうやって関数全体を引用しておいてなんだが、あまり語るべきことはない。注目は強調部分の最後、text_2.setText(t)だね。setTextの引数にそのままtをブチ込めるってこった。あとフレーム毎に更新にするとこの場所にコードが追加されるってことだね。

B: 変態だ…変態の世界だ。

A: まあこれで視覚刺激はだいたい良いとして、あとはキーボードとかの反応をとるコードがどうなるか、か。んじゃとりあえずKeyboardコンポーネントを追加してみて…ついでだから終了時刻が指定されないルーチンのテストもしておくか。Textコンポーネントの終了時刻を空欄にして、と。

B: (すでについていけなくなっている)

A: よし、出力。結構変更点があるな。まずルーチン開始時。BuilderKeyResponseというオブジェクトが作成されてkey_respという変数に格納されており、少し下でpushされている。まあ要はオブジェクトを作成して、このルーチンのコンポーネントとして登録しておくってところだな。

var t;
var frameN;
var key_resp;
var trialComponents;
function trialRoutineBegin() {
  //------Prepare to start Routine 'trial'-------
  t = 0;
  trialClock.reset(); // clock
  frameN = -1;

  // update component parameters for each repeat
  text.setText(message);
  key_resp = new core.BuilderKeyResponse(psychoJS);

  // keep track of which components have finished
  trialComponents = [];
  trialComponents.push(text);
  trialComponents.push(text_2);
  trialComponents.push(key_resp);

  for (const thisComponent of trialComponents)
    if ('status' in thisComponent)
      thisComponent.status = PsychoJS.Status.NOT_STARTED;

  return Scheduler.Event.NEXT;
}

B: Aさん。空白の行が強調されているのは?

A: ああ、ここにはroutineTimer.add(1.000000);という行があったのだ。今までは1秒間で終了するルーチンだったので時間を計測するためにここで時計がセットされていたのだが、今回はキーボードが押されるまでルーチンが終了しないので時間を測る必要がなくなったから省略されたわけだな。

B: ふむむむ。(ふむふむのタイプミスじゃないよ)

A: 続いてはルーチンのフレーム毎の処理。今回も全体を引用するかあ。空白行が強調されているのは、1秒経ったら文字の描画を終了する処理が書かれていた部分。これまた不要になったので省略されたわけだね。最後のif文のところが変更されているのも1秒経過で終了する処理が抜けただけ。

var continueRoutine;
function trialRoutineEachFrame() {
  //------Loop for each frame of Routine 'trial'-------
  let continueRoutine = true; // until we're told otherwise
  // get current time
  t = trialClock.getTime();
  frameN = frameN + 1;// number of completed frames (so 0 is the first frame)
  // update/draw components on each frame

  // *text* updates
  if (t >= 0.0 && text.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    text.tStart = t;  // (not accounting for frame time here)
    text.frameNStart = frameN;  // exact frame index
    text.setAutoDraw(true);
  }


  // *text_2* updates
  if (t >= 0.0 && text_2.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    text_2.tStart = t;  // (not accounting for frame time here)
    text_2.frameNStart = frameN;  // exact frame index
    text_2.setAutoDraw(true);
  }


  if (text_2.status === PsychoJS.Status.STARTED){ // only update if being drawn
    text_2.setText(t);
  }

  // *key_resp* updates
  if (t >= 0.0 && key_resp.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    key_resp.tStart = t;  // (not accounting for frame time here)
    key_resp.frameNStart = frameN;  // exact frame index
    key_resp.status = PsychoJS.Status.STARTED;
    // keyboard checking is just starting
    psychoJS.window.callOnFlip(function() { key_resp.clock.reset(); }); // t = 0 on screen flip
    psychoJS.eventManager.clearEvents({eventType:'keyboard'});
  }

  if (key_resp.status === PsychoJS.Status.STARTED) {
    let theseKeys = psychoJS.eventManager.getKeys({keyList:['y', 'n', 'left', 'right', 'space']});

    // check for quit:
    if (theseKeys.indexOf('escape') > -1) {
      psychoJS.experiment.experimentEnded = true;
    }

    if (theseKeys.length > 0) {  // at least one key was pressed
      key_resp.keys = theseKeys[theseKeys.length-1];  // just the last key pressed
      key_resp.rt = key_resp.clock.getTime();
      // a response ends the routine
      continueRoutine = false;
    }
  }

  // check for quit (typically the Esc key)
  if (psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({keyList:['escape']}).length > 0) {
    return psychoJS.quit('The [Escape] key was pressed. Goodbye!', false);
  }

  // check if the Routine should terminate
  if (!continueRoutine) {  // a component has requested a forced-end of Routine
    return Scheduler.Event.NEXT;
  }

  continueRoutine = false;  // reverts to True if at least one component still running
  for (const thisComponent of trialComponents)
    if ('status' in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED) {
      continueRoutine = true;
      break;
    }

  // refresh the screen if continuing
  if (continueRoutine) {
    return Scheduler.Event.FLIP_REPEAT;
  }
  else {
    return Scheduler.Event.NEXT;
  }
}

B: 今度の追加部分は長いですね。

A: んー。ここはまじめに解説すると面倒くさい。普段Builderが吐くPythonのコードを見ている人なら…

B: それはもういいですから。

A: そうか。注目点を挙げるとすれば、getKeys()というメソッド。これまたPythonの時と同じだね。theseKeysという変数に格納されるのも同じ。この例ではキーが押されるとキー名を保存してルーチンを終了するので、theseKeys.lenghが0より大きければkey_resp.keysに最後のキーの名前を保存してkey_resp.rtにルーチン開始からの時間を格納している。この辺りもだいたいPythonの時と同じ。そしてcontinueRoutineをfalseにしてルーチンが終了するようにしている!

B: ぽかーん

A: まあだいたいPythonの時と同じだという事だよ。続いてルーチン終了時の処理。addData()のお出ましだね!これもPythonの時と同じだ。ちなみに最初のif文ではキーが押される前にルーチンが終了した場合(Keyboardコンポーネントで終了時刻が指定されていてその時刻を超えてしまった場合など)にキー名をundefinedにしているね。

function trialRoutineEnd() {
  //------Ending Routine 'trial'-------
  for (const thisComponent of trialComponents) {
    if (typeof thisComponent.setAutoDraw === 'function') {
      thisComponent.setAutoDraw(false);
    }
  }

  // check responses
  if (key_resp.keys === undefined || key_resp.keys.length === 0) {    // No response was made
      key_resp.keys = undefined;
  }

  psychoJS.experiment.addData('key_resp.keys', key_resp.keys);
  if (typeof key_resp.keys !== 'undefined') {  // we had a response
      psychoJS.experiment.addData('key_resp.rt', key_resp.rt);
      routineTimer.reset();
      }

  // the Routine "trial" was not non-slip safe, so reset the non-slip timer
  routineTimer.reset();

  return Scheduler.Event.NEXT;
}

B: (そろそろ夕食はなにをおごってもらうかばかり考えている)

A: 続いてマウスを使う場合。Keyboardコンポーネントは削除してMouseコンポーネントを置いてJavaScriptを出力。まずはMouseオブジェクトが冒頭部で作成されている。Keyboardの場合とは違うな。

var trialClock;
var text;
var text_2;
var mouse;
var globalClock;
var routineTimer;
function experimentInit() {
  // Initialize components for Routine "trial"
  trialClock = new util.Clock();
  text = new visual.TextStim({
    win: psychoJS.window,
    name: 'text',
    text: 'default text',
    font: 'Arial',
    units : undefined,
    pos: [0, 0], height: 0.1,  wrapWidth: undefined, ori: 0,
    color: new util.Color('white'),  opacity: 1,
    depth: 0.0
  });

  text_2 = new visual.TextStim({
    win: psychoJS.window,
    name: 'text_2',
    text: 'default text',
    font: 'Arial',
    units : undefined,
    pos: [0, 0], height: 0.1,  wrapWidth: undefined, ori: 0,
    color: new util.Color('white'),  opacity: 1,
    depth: -1.0
  });

  mouse = new core.Mouse({
    win: psychoJS.window,
  });
  mouse.mouseClock = new util.Clock();

A: 続いてルーチン開始時。gotValidClickという変数を用意しているところが新しい。

var t;
var frameN;
var gotValidClick;
var trialComponents;
function trialRoutineBegin() {
  //------Prepare to start Routine 'trial'-------
  t = 0;
  trialClock.reset(); // clock
  frameN = -1;
  // update component parameters for each repeat
  text.setText(message);
  // setup some python lists for storing info about the mouse
  gotValidClick = false; // until a click is received
  // keep track of which components have finished
  trialComponents = [];
  trialComponents.push(text);
  trialComponents.push(text_2);
  trialComponents.push(mouse);

  for (const thisComponent of trialComponents)
    if ('status' in thisComponent)
      thisComponent.status = PsychoJS.Status.NOT_STARTED;

  return Scheduler.Event.NEXT;
}

A: そしてルーチンのフレーム毎の処理。直前のマウス押しの状態を保持するためのprevButtonStateに注目。あとは…そうだな、mouse.getPressed()でマウス押しの状態が得られること、マウスボタンが押されていない状態から押された状態なったことを判定する処理はまあ面白いが、Codeコンポーネントに書いたコードの移植という観点からは別に重要じゃないかな。

var prevButtonState;
var continueRoutine;
function trialRoutineEachFrame() {
  //------Loop for each frame of Routine 'trial'-------
  let continueRoutine = true; // until we're told otherwise
  // get current time
  t = trialClock.getTime();
  frameN = frameN + 1;// number of completed frames (so 0 is the first frame)
  // update/draw components on each frame

  // *text* updates
  if (t >= 0.0 && text.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    text.tStart = t;  // (not accounting for frame time here)
    text.frameNStart = frameN;  // exact frame index
    text.setAutoDraw(true);
  }

  // *text_2* updates
  if (t >= 0.0 && text_2.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    text_2.tStart = t;  // (not accounting for frame time here)
    text_2.frameNStart = frameN;  // exact frame index
    text_2.setAutoDraw(true);
  }

  if (text_2.status === PsychoJS.Status.STARTED){ // only update if being drawn
    text_2.setText(t);
  }
  // *mouse* updates
  if (t >= 0.0 && mouse.status === PsychoJS.Status.NOT_STARTED) {
    // keep track of start time/frame for later
    mouse.tStart = t;  // (not accounting for frame time here)
    mouse.frameNStart = frameN;  // exact frame index
    mouse.status = PsychoJS.Status.STARTED;
    mouse.mouseClock.reset();
    prevButtonState = mouse.getPressed();  // if button is down already this ISN'T a new click
    }
  if (mouse.status === PsychoJS.Status.STARTED) {  // only update if started and not finished!
    let buttons = mouse.getPressed();
    if (!buttons.every( (e,i,) => (e == prevButtonState[i]) )) { // button state changed?
      prevButtonState = buttons;
      if (buttons.reduce( (e, acc) => (e+acc) ) > 0) { // state changed to a new click
        // abort routine on response
        continueRoutine = false;
      }
    }
  }
  // check for quit (typically the Esc key)
  if (psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({keyList:['escape']}).length > 0) {
    return psychoJS.quit('The [Escape] key was pressed. Goodbye!', false);
  }

  // check if the Routine should terminate
  if (!continueRoutine) {  // a component has requested a forced-end of Routine
    return Scheduler.Event.NEXT;
  }

  continueRoutine = false;  // reverts to True if at least one component still running
  for (const thisComponent of trialComponents)
    if ('status' in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED) {
      continueRoutine = true;
      break;
    }

  // refresh the screen if continuing
  if (continueRoutine) {
    return Scheduler.Event.FLIP_REPEAT;
  }
  else {
    return Scheduler.Event.NEXT;
  }
}

A: そして最後にルーチン終了時。マウスカーソルの位置を得るgetPos()が登場している。getPos()とgetPressed()が用意されているということはCodeコンポーネントに書いたコードの移植はやりやすいはず。

function trialRoutineEnd() {
  //------Ending Routine 'trial'-------
  for (const thisComponent of trialComponents) {
    if (typeof thisComponent.setAutoDraw === 'function') {
      thisComponent.setAutoDraw(false);
    }
  }
  // store data for thisExp (ExperimentHandler)
  const xys = mouse.getPos();
  const buttons = mouse.getPressed();
  psychoJS.experiment.addData('mouse.x', xys[0]);
  psychoJS.experiment.addData('mouse.y', xys[1]);
  psychoJS.experiment.addData('mouse.leftButton', buttons[0]);
  psychoJS.experiment.addData('mouse.midButton', buttons[1]);
  psychoJS.experiment.addData('mouse.rightButton', buttons[2]);
  // the Routine "trial" was not non-slip safe, so reset the non-slip timer
  routineTimer.reset();

  return Scheduler.Event.NEXT;
}

B:

A: というわけで、まあこんなもんかな。ところでB君よ。

B: …はっ! なんでありますかAさん!

A: 最後の方はずいぶん静かであったな。

B: だってわかんないんですもん。そもそもスケジューラってなんですのん。

A: んー、私にもよくわからん!

B: へっ!わかんないのに今までしたり顔で解説してたんですか!

A: コードを見れば何をしているか明らかだろ。一応PsychoJSのドキュメントを数十秒眺めたんだけどそれだけじゃどこに解説があるのかよくわからんかったので後回しにした。

B: 数十秒で見つかる方がおかしいでしょ。忍耐なさすぎ。

A: せっかちなのはプログラマの美徳なのだ。今は時間をかける余裕がないのだ。

B: せっかちじゃなくて短気じゃなかったでしたっけ。少し違うような…

A: ちなみに他にもループの外にルーチンを追加した場合のコードとかも出力してみたんだが、あまり面白くないのでこれは省略。そろそろ1000行を超えてしまったので今日はこの辺りにしておこう。続きがあるか、あるとすればどういう形かはともかくとして。

B: やった! 晩ごはん♪晩ごはん♪ 晩ごはん……

A: ん?どうした?

B: (探るような目で)いや、ここで何かしょーもないどんでん返しがあっておごってもらえないのがいつものパターンかと思いまして…

A: ははっ、人聞きが悪い。まだ仕事があるから戻ってこにゃならんので、近場に限るけどB君が行きたい店に連れて行ってやろう。

B: やった! んじゃカバン取ってきますね。んじゃ!

A: やれやれ。