jsPsychでマウスをかざすとテキストの表示する & マウスの軌跡をトラッキングをする

jsPsychでマウスをかざすとテキストの表示する & マウスの軌跡をトラッキングをする

jsPsychには、マウスの軌跡を記録するマウストラッキング(mouse-tracking)が搭載されています。このマウストラッキング機能と、隠されたコンテンツをマウスホバーで表示するCSSを書いて、要素をどれだけ確認したかを確認する実験プログラムを備忘録的に書いてみました。

「マウスをかざすとでテキストの表示する」のは、jsPsychの機能ではなく、HTMLとCSSで実装できます。

デモ

デモはこちらから

デモは上記のリンクから

mouse-trackingプラグインを使う

マウストラッキングを使うにあたり必要なのは、Initialization Parametersの”initJsPsych()”での設定と、Trial Parameters(該当ブロック内)の設定の2つです。

“initJsPsych()”での設定

var jsPsych = initJsPsych({
      on_finish: function() {
        jsPsych.data.displayData();
      },
      extensions: [
        {type: jsPsychExtensionMouseTracking, params: {minimum_sample_time: 0}}
      ]
});

initJsPsych内に、どれくらいの頻度でマウスイベントを記録するかを設定します。minimum_sample_timeが0の場合、すべてのイベントが記録されます。例えば、ms単位で調整可能で、10ms単位で記録したい場合は、10に設定します。

ざっと使ってみた感じ、0ms設定だと記録されるデータサイズが大きくなるので、Qualtricsなどを使用して実施する場合は注意が必要です。

Trial Parameters(該当ブロック内)の設定

var trial_2 = {
  type: jsPsychHtmlButtonResponse,
  stimulus: '<div id="target"><div class="target_message"><h1>A</h1><div class="hover-effect">たけのこの里こそが素晴らしい食べ物だ</div></div><div class="target_message"><h1>B</h1><div class="hover-effect">きのこの山こそが素晴らしい食べ物だ</div></div></div>',
  choices: ['A', 'B'],
  prompt: "<p></br>マウスをかざすと説明を確認できます。</br>あなたの意見を選択してください。</p>",
  extensions: [
    {type: jsPsychExtensionMouseTracking, params: {targets: ['#target']}}
  ],
  data: {
    task: 'draw'
  }
};

実験のブロック内にも、extensionsを使ってmouse-trackingのプラグインを呼び出します。targetsには、トラックしたいオブジェクトのセレクタを入れます。ここでは、質問文が入っているボックスのtargetタグを指定しています(ただし、target内のボックス外のマウスの動きもトラックしてくれるようです)。

他にも、eventsパラメーターを使って、mousemoveやmouseup, mousedownなどどの動きを測定するかも指定できます。

トラッキングの履歴を表示する

var replay = {
  type: jsPsychHtmlButtonResponse,
  stimulus: '<div id="target"><div class="target_message"><h1>A</h1><div class="hover-effect">たけのこの里こそが素晴らしい食べ物だ</div></div><div class="target_message"><h1>B</h1><div class="hover-effect">きのこの山こそが素晴らしい食べ物だ</div></div></div>',
  choices: ['終了する'],
  prompt: "<p>あなたのマウスの動きです。</p>",
  on_load: function(){
    var mouseMovements = jsPsych.data.get().last(1).values()[0].mouse_tracking_data;
    var targetRect = jsPsych.data.get().last(1).values()[0].mouse_tracking_targets['#target'];

    var startTime = performance.now();

    function draw_frame() {
      var timeElapsed = performance.now() - startTime;
      var points = mouseMovements.filter((x) => x.t <= timeElapsed);
      var html = `<div class="target_message"><h1>A</h1><div class="hover-effect">たけのこの里こそが素晴らしい食べ物だ</div></div><div class="target_message"><h1>B</h1><div class="hover-effect">きのこの山こそが素晴らしい食べ物だ</div></div>`;
    for(var p of points){
      html += `<div id ="mouse_tracking" style="top: ${p.y - 1 - targetRect.top}px; left: ${p.x - 1 - targetRect.left}px;"></div>`
    }
    document.querySelector('#target').innerHTML = html;
    if(points.length < mouseMovements.length) {
      requestAnimationFrame(draw_frame);
    }
}

実験ブロックと同じブロックを作成し、”on_load”に追記することで、マウスの軌跡を描画することもできます。JavaScriptを使ってinnerHTMLを使って追記していくという形式になっています。ほとんどサンプルコードをコピペでOKです。
divで入れ子構造を作成した場合は、”var html = “に入れ子の要素を入れておきましょう。

雑感

使うのにちょっとした理解は必要ですが、比較的かんたんにマウストラッキングの機能が実装できるのはありがたいですね。

CSSのhoverとうまく組み合わせることで、参加者の項目の注視時間を計測したり、選択行動の迷いを計測したりするのに良いかもしれません(注視時間を計測するならhoverが動作している時間をJavaScriptで直に計測するコードを書くほうが良いかもしれませんが)。

ただし、上でも書いたように、経過時間が長くなればなるほど、トラッキングの履歴データが増えるので、使用するサービスによってはデータがちゃんと保存されるか気をつけたほうが良さそうです。

コード全部

HTML及びJavaScript

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="dist/jspsych.js"></script>
    <script src="dist/plugin-instructions.js"></script>
    <script src="dist/plugin-html-button-response.js"></script>
    <script src="dist/extension-mouse-tracking.js"></script>
    <link rel="stylesheet" href="dist/jspsych.css">
    <link rel="stylesheet" href="additional.css">

  </head>
  <body>

  </body>
  <script>

    var jsPsych = initJsPsych({
      on_finish: function() {
        jsPsych.data.displayData();
      },
      extensions: [
        {type: jsPsychExtensionMouseTracking, params: {minimum_sample_time: 0}}
      ]
    });

    var trial_1 = {
      type: jsPsychInstructions,
      pages: ['<p>実験を行います。</p><p>画面に選択と選択の説明が表示されます。</p><p>マウスカーソルをあわせると説明を読むことができます</p><p>説明をよく読んで、回答してください。</p><p>「次へ」を押すと、実験を開始します。</p>'],
      key_forward: ' ',
      allow_backward: false,
      button_label_next: '次へ',
      show_clickable_nav: true
    };

    var trial_2 = {
      type: jsPsychHtmlButtonResponse,
      stimulus: '<div id="target"><div class="target_message"><h1>A</h1><div class="hover-effect">たけのこの里こそが素晴らしい食べ物だ</div></div><div class="target_message"><h1>B</h1><div class="hover-effect">きのこの山こそが素晴らしい食べ物だ</div></div></div>',
      choices: ['A', 'B'],
      prompt: "<p></br>マウスをかざすと説明を確認できます。</br>あなたの意見を選択してください。</p>",
      extensions: [
        {type: jsPsychExtensionMouseTracking, params: {targets: ['#target']}}
      ],
      data: {
        task: 'draw'
      }
    };

    var replay = {
      type: jsPsychHtmlButtonResponse,
      stimulus: '<div id="target"><div class="target_message"><h1>A</h1><div class="hover-effect">たけのこの里こそが素晴らしい食べ物だ</div></div><div class="target_message"><h1>B</h1><div class="hover-effect">きのこの山こそが素晴らしい食べ物だ</div></div></div>',
      choices: ['終了する'],
      prompt: "<p>あなたのマウスの動きです。</p>",
      on_load: function(){
        var mouseMovements = jsPsych.data.get().last(1).values()[0].mouse_tracking_data;
        var targetRect = jsPsych.data.get().last(1).values()[0].mouse_tracking_targets['#target'];

        var startTime = performance.now();

        function draw_frame() {
          var timeElapsed = performance.now() - startTime;
          var points = mouseMovements.filter((x) => x.t <= timeElapsed);
          var html = `<div class="target_message"><h1>A</h1><div class="hover-effect">たけのこの里こそが素晴らしい食べ物だ</div></div><div class="target_message"><h1>B</h1><div class="hover-effect">きのこの山こそが素晴らしい食べ物だ</div></div>`;
          for(var p of points){
            html += `<div id ="mouse_tracking" style="top: ${p.y - 1 - targetRect.top}px; left: ${p.x - 1 - targetRect.left}px;"></div>`
          }
          document.querySelector('#target').innerHTML = html;
          if(points.length < mouseMovements.length) {
            requestAnimationFrame(draw_frame);
          }
        }

    requestAnimationFrame(draw_frame);

  },
  data: {
    task: 'replay'
  }
}




  // jsPsych.run([trial_1, trial_2, replay]);
  jsPsych.run([trial_1, trial_2, replay]);

  </script>
</html>

#target {
    width: 500px;
    height: 400px;
    /* background-color: #808080;  */
    display: flex;
    flex-direction: column;  /* 子要素を縦に並べる */
    justify-content: space-between; /* 上下に配置  */
    align-items: center; /*水平方向で中央揃え  */
    padding: 5px; /* 内側の余白 */
    position: relative;
}

.target_message{
    width: 100%;            /* 親コンテナの幅に合わせる */
    height: 50%;  
    /* background-color: #808080;  */
    border: 1px solid #000; /* 子要素の境界線 */
    display: flex;
    flex-direction: column; /* 見出しと説明を縦に配置 */
    justify-content: center; /* 垂直方向に中央揃え */
    align-items: center;     /* 水平方向に中央揃え */
    position: relative;      /* hover-effectの位置指定のために相対位置 */
    padding: 20px;           /* 内側の余白 */
    box-sizing: border-box;  /* パディングを含めたサイズ計算 */
}

.target_message h1 {
    font-size: 24px; /* フォントサイズ */
    color: black; /* 常時白文字で表示 */
    text-align: center; /* 中央揃え */
    background-color: transparent; /* 背景を透明に */
    margin: 0; /* 余白をなくす */
    position: relative;
}

.hover-effect {
    font-size: 24px;
    width: 95%;            /* 親コンテナの幅に合わせる */
    height: 90%;
    /* color: black;  */
    filter: blur(10px);
    background-color: rgba(0, 0, 0, 0.188);
    margin: 2px;            /* 周囲からの均等なマージン */
    padding: 2px 2px;      /* 内側の余白 */
    transition: color 0.3s ease; /* スムーズな変化 */
    text-align:center;
    padding:20px 0;
}

.hover-effect:hover {
    color: black; /* ホバー時に文字色を白に変更 */
    filter: none;
    background-color: white; /* 背景も黒で塗りつぶし */
    /* background-color: #808080; 背景も黒で塗りつぶし */
}

#mouse_tracking{
    width: 3px; 
    height: 3px; 
    background-color: blue; 
    position: absolute;
    pointer-events: none; /* イベントを下の要素に通す */
}