From 7c0b1216503f883af4d629c967ada6e7b1590ab6 Mon Sep 17 00:00:00 2001 From: Francesca Date: Thu, 14 Mar 2024 16:59:00 +0100 Subject: [PATCH] Added custom plugin(7+) for reading-by-rotating paradigm --- .../reading-by-rotating/index.html | 23 ++ .../reading-by-rotating/knob-drag-response.js | 379 ++++++++++++++++++ .../knob-static-response.js | 252 ++++++++++++ .../test-reading-by-rotating.js | 76 ++++ 4 files changed, 730 insertions(+) create mode 100644 CustomPlugins7+/reading-by-rotating/index.html create mode 100644 CustomPlugins7+/reading-by-rotating/knob-drag-response.js create mode 100644 CustomPlugins7+/reading-by-rotating/knob-static-response.js create mode 100644 CustomPlugins7+/reading-by-rotating/test-reading-by-rotating.js diff --git a/CustomPlugins7+/reading-by-rotating/index.html b/CustomPlugins7+/reading-by-rotating/index.html new file mode 100644 index 00000000..7ba5400d --- /dev/null +++ b/CustomPlugins7+/reading-by-rotating/index.html @@ -0,0 +1,23 @@ + + + + + reading-by-rotating Test + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CustomPlugins7+/reading-by-rotating/knob-drag-response.js b/CustomPlugins7+/reading-by-rotating/knob-drag-response.js new file mode 100644 index 00000000..fe838d4b --- /dev/null +++ b/CustomPlugins7+/reading-by-rotating/knob-drag-response.js @@ -0,0 +1,379 @@ +var jsPsychKnobDragResponse = (function (jspsych) { + 'use strict'; + + const info = { + name: "knob-drag-response", + description: "", + parameters: { + canvas_colour: { + type: jspsych.ParameterType.STRING, + pretty_name: "Colour", + default: "white", + description: "Canvas colour.", + }, + canvas_size: { + type: jspsych.ParameterType.INT, + array: true, + pretty_name: "Size", + default: [1280, 960], + description: "Canvas size.", + }, + canvas_border: { + type: jspsych.ParameterType.STRING, + pretty_name: "Border", + default: "0px solid black", + description: "Border style", + }, + text: { + type: jspsych.ParameterType.STRING, + pretty_name: "Text", + default: "Hello, world!", + description: "StimulusText", + }, + rotation_direction: { + type: jspsych.ParameterType.STRING, + pretty_name: "RotationDirection", + default: "clockwise", + description: "Direction of rotation to move to the next segment", + }, + colour: { + type: jspsych.ParameterType.STRING, + pretty_name: "Colour", + default: "black", + description: "StimulusColour", + }, + font: { + type: jspsych.ParameterType.STRING, + pretty_name: "Colour", + default: "30px arial", + description: "Font", + }, + }, + }; + + class KnobDragResponse { + constructor(jsPsych) { + this.jsPsych = jsPsych; + } + + trial(display_element, trial) { + // setup canvas + display_element.innerHTML = + '
' + + '' + + '
'; + + let canvas = document.getElementById('canvas'); + let ctx = document.getElementById('canvas').getContext('2d'); + let rect = canvas.getBoundingClientRect(); + + // canvas mouse events + canvas.addEventListener('mousedown', handleMouseDown); + canvas.addEventListener('mousemove', handleMouseMove); + canvas.addEventListener('mouseup', handleMouseUp); + + let selectedTriangle = false; + let movement_initiated = false; + let start_rt; + let end_rt; + let n_presses = 0; + let mpos; + let start_angle = 0; + let mangle = 0; + let mrotation = 0; + let p0_angle = 0; + let n_p0_coords = 0; + let n_p1_coords = 0; + let n_p2_coords = 0; + let n_marker_coords = 0; + let current_rotation_direction = ""; + + let angles = []; + let x_coords = []; + let y_coords = []; + let time = []; + + // cirlce object (knob) + let circle = { + x: canvas.width / 2, + y: canvas.height / 2, + r: 50, + }; + + // marker object + let marker = { + x: circle.x, + y: circle.y - 30, + r: 10, + }; + + // triangle object (lever) + let triangle = { + p0: { + x: canvas.width / 2, + y: canvas.height / 2 - 50, + }, + p1: { + x: canvas.width / 2 + 25, + y: canvas.height / 2 - 75, + }, + p2: { + x: canvas.width / 2 - 25, + y: canvas.height / 2 - 75, + }, + }; + + // text object + let text = { + text: trial.text, + x: canvas.width / 2, + y: canvas.height - 300, + font: trial.font, + }; + + ctx.font = text.font; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // initial draw + let start_time = performance.now(); + draw(); + + // mouse functions + function handleMouseUp() { + selectedTriangle = false; + resetKnob(); + } + + function handleMouseDown(e) { + n_presses++; + mousePosition(e); + mouseAngle(); + start_angle = mangle; + selectedTriangle = triangleHittest(mpos.x, mpos.y); + } + + function handleMouseMove(e) { + if (selectedTriangle === false) { + return; + } + + mousePosition(e); + mouseAngle(); + + // store coordinates and time array + x_coords.push(mpos.x); + y_coords.push(mpos.y); + time.push(performance.now() - start_time); + + + // the points should rotate by the same angle as the mouse + mrotation = mangle - start_angle; + start_angle = mangle; + + // set new triangle position + n_p0_coords = rotate(circle.x, circle.y, triangle.p0.x, triangle.p0.y, mrotation); + triangle.p0.x = n_p0_coords[0]; + triangle.p0.y = n_p0_coords[1]; + n_p1_coords = rotate(circle.x, circle.y, triangle.p1.x, triangle.p1.y, mrotation); + triangle.p1.x = n_p1_coords[0]; + triangle.p1.y = n_p1_coords[1]; + n_p2_coords = rotate(circle.x, circle.y, triangle.p2.x, triangle.p2.y, mrotation); + triangle.p2.x = n_p2_coords[0]; + triangle.p2.y = n_p2_coords[1]; + + // set new marker position + n_marker_coords = rotate(circle.x, circle.y, marker.x, marker.y, mrotation); + marker.x = n_marker_coords[0]; + marker.y = n_marker_coords[1]; + + // determine direction of rotation of the mouse + mrotation > 0 ? current_rotation_direction = "clockwise" : current_rotation_direction = "counterclockwise"; + + // store current mouse angle + p0_angle = findAngleBetween(circle.x, circle.y, triangle.p0.x, triangle.p0.y); + angles.push(p0_angle); + + // monitor if rotation reached the target angle + if (rotationComplete()) { + end_trial(); + } else if (Math.abs(p0_angle) > 120) { + resetKnob(); + selectedTriangle = false; + } else { + draw(); + } + + if (!movement_initiated) { + start_rt = performance.now() - start_time; + movement_initiated = true; + } + } + + function mousePosition(e) { + mpos = { + x: ((e.clientX - rect.left) / (rect.right - rect.left)) * canvas.width, + y: ((e.clientY - rect.top) / (rect.bottom - rect.top)) * canvas.height, + }; + } + + function mouseAngle() { + mangle = findAngleBetween(circle.x, circle.y, mpos.x, mpos.y); + } + + // clear the canvas and draw text + function draw() { + + ctx.fillStyle = trial.canvas_colour; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.beginPath(); + ctx.arc(circle.x, circle.y, circle.r, 0, 2 * Math.PI); + ctx.fillStyle = "#c9c9c9"; + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + + ctx.beginPath(); + ctx.arc(marker.x, marker.y, marker.r, 0, 2 * Math.PI); + ctx.fillStyle = "#7d7a79"; + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(triangle.p0.x, triangle.p0.y); + ctx.lineTo(triangle.p1.x, triangle.p1.y); + ctx.lineTo(triangle.p2.x, triangle.p2.y); + ctx.fillStyle = "red"; + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + + ctx.fillStyle = trial.colour; + ctx.fillText(text.text, text.x, text.y); + } + + // function to end trial when it is time + let end_trial = function () { + end_rt = performance.now() - start_time; + + // gather the data to store for the trial + let trial_data = { + start_rt: start_rt, + start_x: Math.round(x_coords[0]), + start_y: Math.round(y_coords[0]), + end_rt: end_rt, + end_x: Math.round(x_coords[x_coords.length - 1]), + end_y: Math.round(y_coords[y_coords.length - 1]), + n_presses: n_presses, + x_coords: roundArray(x_coords), + y_coords: roundArray(y_coords), + time: time, + angles: roundArray(angles) + }; + + // remove event listeners + canvas.removeEventListener('mousemove', handleMouseMove); + canvas.removeEventListener('mousedown', handleMouseMove); + canvas.removeEventListener('mouseup', handleMouseMove); + + // clear the display and move on to the next trial + display_element.innerHTML = ''; + jsPsych.finishTrial(trial_data); + }; + + // rotation functions + function rotate(cx, cy, x, y, angle) { + var radians = (Math.PI / 180) * angle, + cos = Math.cos(radians), + sin = Math.sin(radians), + nx = (cos * (x - cx)) - (sin * (y - cy)) + cx, + ny = (sin * (x - cx)) + (cos * (y - cy)) + cy; + return [nx, ny]; + } + + function findAngleBetween(cx, cy, px, py) { + // Calculate the angle in radians using Math.atan2 + let radians = Math.atan2(px - cx, py - cy); + + // Convert radians to degrees + let degrees = radians * (180 / Math.PI); + + // Adjust the range to be between -180 and 180 + if (degrees > 180) { + degrees -= 360; + } else if (degrees < -180) { + degrees += 360; + } + + // Reverse the angle to be measured from the y-axis (0,1) + degrees = 180 - degrees; + + // Adjust the range again to be between -180 and 180 + if (degrees > 180) { + degrees -= 360; + } else if (degrees < -180) { + degrees += 360; + } + + return degrees; + } + + function rotationComplete() { + // mouse is currently rotating in the correct direction and reached the target rotation angle + return current_rotation_direction == trial.rotation_direction && 180 > Math.abs(p0_angle) && Math.abs(p0_angle) >= 120; + } + + function resetKnob() { + marker = { + x: circle.x, + y: circle.y - 30, + r: 10, + }; + triangle = { + p0: { + x: canvas.width / 2, + y: canvas.height / 2 - 50, + }, + p1: { + x: canvas.width / 2 + 25, + y: canvas.height / 2 - 75, + }, + p2: { + x: canvas.width / 2 - 25, + y: canvas.height / 2 - 75, + }, + }; + draw(); + } + + // other functions + // test if x, y is inside the triangle + function triangleHittest(x, y) { + return ( + ctx.isPointInPath(x, y) + ); + } + + // round array to integers + function roundArray(array) { + let len = array.length; + while (len--) { + array[len] = Math.round(array[len]); + } + return array; + } + + } + } + + KnobDragResponse.info = info; + return KnobDragResponse; +})(jsPsychModule); \ No newline at end of file diff --git a/CustomPlugins7+/reading-by-rotating/knob-static-response.js b/CustomPlugins7+/reading-by-rotating/knob-static-response.js new file mode 100644 index 00000000..1b67b08c --- /dev/null +++ b/CustomPlugins7+/reading-by-rotating/knob-static-response.js @@ -0,0 +1,252 @@ +// custom jspsych mouse drag response using code adapted from: +// https://stackoverflow.com/questions/21605942/drag-element-on-canvas +var jsPsychKnobStaticResponse = (function (jspsych) { + 'use strict'; + + const info = { + name: 'mouse-static-response', + description: '', + parameters: { + canvas_colour: { + type: jspsych.ParameterType.STRING, + array: false, + pretty_name: 'Colour', + default: 'white', + description: 'Canvas colour.', + }, + canvas_size: { + type: jspsych.ParameterType.INT, + array: true, + pretty_name: 'Size', + default: [1280, 960], + description: 'Canvas size.', + }, + canvas_border: { + type: jspsych.ParameterType.STRING, + pretty_name: 'Border', + default: '0px solid black', + description: 'Border style', + }, + text: { + type: jspsych.ParameterType.STRING, + pretty_name: 'Text', + default: 'Hello, world!', + description: 'StimulusText', + }, + colour: { + type: jspsych.ParameterType.STRING, + pretty_name: 'Colour', + default: 'black', + description: 'StimulusColour', + }, + font: { + type: jspsych.ParameterType.STRING, + pretty_name: 'Colour', + default: '30px arial', + description: 'Font', + }, + }, + }; + + class KnobStaticResponse { + constructor(jsPsych) { + this.jsPsych = jsPsych; + } + + trial(display_element, trial) { + // setup canvas + display_element.innerHTML = + '
' + + '' + + '
'; + + let canvas = document.getElementById('canvas'); + let ctx = document.getElementById('canvas').getContext('2d'); + let rect = canvas.getBoundingClientRect(); + + // canvas mouse events + canvas.addEventListener('mousedown', handleMouseDown); + canvas.addEventListener('mousemove', handleMouseMove); + + let selectedCircle = false; + let movement_initiated = false; + let start_rt; + let end_rt; + let n_presses = 0; + let mpos; + + let x_coords = []; + let y_coords = []; + let time = []; + + // cirlce object (knob) + let circle = { + x: canvas.width / 2, + y: canvas.height / 2, + r: 50, + }; + let circle_path = new Path2D(); + + // marker object + let marker = { + x: circle.x, + y: circle.y - 30, + r: 10, + }; + + // triangle object (lever) + let triangle = { + p0: { + x: canvas.width / 2, + y: canvas.height / 2 - 50, + }, + p1: { + x: canvas.width / 2 + 25, + y: canvas.height / 2 - 75, + }, + p2: { + x: canvas.width / 2 - 25, + y: canvas.height / 2 - 75, + }, + }; + + // text object + let text = { + text: trial.text, + x: canvas.width / 2, + y: canvas.height - 300, + font: trial.font, + }; + + ctx.font = text.font; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // initial draw + let start_time = performance.now(); + draw(); + + // mouse functions + function handleMouseDown(e) { + n_presses++; + mousePosition(e); + selectedCircle = circleHittest(circle_path, mpos.x, mpos.y); + if (selectedCircle === true) { + end_trial() + }; + selectedCircle = false; + } + + function handleMouseMove(e) { + + mousePosition(e); + + // store coordinates and time array + x_coords.push(mpos.x); + y_coords.push(mpos.y); + time.push(performance.now() - start_time); + + if (!movement_initiated) { + start_rt = performance.now() - start_time; + movement_initiated = true; + } + } + + function mousePosition(e) { + mpos = { + x: ((e.clientX - rect.left) / (rect.right - rect.left)) * canvas.width, + y: ((e.clientY - rect.top) / (rect.bottom - rect.top)) * canvas.height, + }; + } + + // clear the canvas and draw text + function draw() { + ctx.fillStyle = trial.canvas_colour; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.beginPath(); + ctx.moveTo(triangle.p0.x, triangle.p0.y); + ctx.lineTo(triangle.p1.x, triangle.p1.y); + ctx.lineTo(triangle.p2.x, triangle.p2.y); + ctx.fillStyle = "red"; + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + + ctx.beginPath(); + circle_path.arc(circle.x, circle.y, circle.r, 0, 2 * Math.PI); + ctx.fillStyle = "#c9c9c9"; + ctx.stroke(circle_path); + ctx.fill(circle_path); + + ctx.beginPath(); + ctx.arc(marker.x, marker.y, marker.r, 0, 2 * Math.PI); + ctx.fillStyle = "#7d7a79"; + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + + ctx.fillStyle = trial.colour; + ctx.fillText(text.text, text.x, text.y); + } + + // function to end trial when it is time + let end_trial = function () { + end_rt = performance.now() - start_time; + + // store coordinates and time array + x_coords.push(mpos.x); + y_coords.push(mpos.y); + time.push(end_rt); + + // gather the data to store for the trial + let trial_data = { + start_rt: start_rt, + start_x: Math.round(x_coords[0]), + start_y: Math.round(y_coords[0]), + end_rt: end_rt, + end_x: Math.round(x_coords[x_coords.length - 1]), + end_y: Math.round(y_coords[y_coords.length - 1]), + n_presses: n_presses, + x_coords: roundArray(x_coords), + y_coords: roundArray(y_coords), + time: time, + }; + + // remove event listeners + canvas.removeEventListener('mousedown', handleMouseDown); + canvas.removeEventListener('mousemove', handleMouseMove); + + // clear the display and move on to the next trial + display_element.innerHTML = ''; + jsPsych.finishTrial(trial_data); + }; + + // other functions + // test if x, y is inside the triangle + function circleHittest(path, x, y) { + return ( + ctx.isPointInPath(path, x, y) + ); + } + + // round array to integers + function roundArray(array) { + let len = array.length; + while (len--) { + array[len] = Math.round(array[len]); + } + return array; + } + } + } + + KnobStaticResponse.info = info; + return KnobStaticResponse; +})(jsPsychModule); \ No newline at end of file diff --git a/CustomPlugins7+/reading-by-rotating/test-reading-by-rotating.js b/CustomPlugins7+/reading-by-rotating/test-reading-by-rotating.js new file mode 100644 index 00000000..155901e9 --- /dev/null +++ b/CustomPlugins7+/reading-by-rotating/test-reading-by-rotating.js @@ -0,0 +1,76 @@ +var jsPsych = initJsPsych({ +}); + + +// Instructions +var instructions = { + type: jsPsychHtmlKeyboardResponse, + stimulus: '

ANSWEISUNGEN

Wenn das Wort "öffnet" erscheint, drehen Sie den Knopf im Uhrzeigersinn.

Wenn das Wort "schließt" erscheint, drehen Sie den Knopf gegen den Uhrzeigersinn.

Drücken Sie eine beliebige Taste, um fortzufahren.

' +}; + +// Fullscreen +var enter_fullscreen = { + type: jsPsychFullscreen, + fullscreen_mode: true, + message: "

Das Experiment wechselt in den Vollbildmodus, wenn Sie auf Weiter klicken.

", + button_label: "Weiter" +}; + + +//////////////////////////////////////////////////////////////////////////////// +////////////////////////////// SEGMENTS //////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +function segment(my_segment) { + + return { + timeline: [ + { + type: jsPsychKnobStaticResponse, + canvas_border: '5px solid black', + response_border_x: 100, + text: my_segment, + }]}; +}; + +function critical_segment(my_segment) { + + return { + timeline: [ + { + type: jsPsychKnobDragResponse, + canvas_border: '5px solid black', + response_border_x: 100, + text: my_segment, + rotation_direction: 'clockwise' + } + ]}; +}; + + +var segment1 = segment('Emilio'); +var segment2 = segment('hat dieses Wochenende'); +var segment3 = segment('Besuch von Freunden'); +var segment4 = segment('und bereitet'); +var segment5 = segment('mit viel Mühe'); +var segment6 = segment('ein üppiges Frühstück zu.'); +var segment7 = segment('Er'); +var critical_segment = critical_segment('öffnet ein Marmeladenglas.'); +var segment8 = segment('Er hofft'); +var segment9 = segment('dass allen Gästen'); +var segment10 = segment('sein Frühstück'); +var segment11 = segment('schmecken wird.'); + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + + + +// Timeline of one trial +var timeline = [instructions, enter_fullscreen, segment1, segment2, segment3, segment4, segment5, segment6, segment7, + critical_segment, segment8, segment9, segment10, segment11]; + + +// Start Experiment +jsPsych.run(timeline); \ No newline at end of file