From 1e2a47902c5819e5f278e825474cb9ae56c642e9 Mon Sep 17 00:00:00 2001 From: "Peter J. Kohler" Date: Sat, 13 Mar 2021 16:02:42 -0500 Subject: [PATCH 1/7] gitignore added --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2cdba4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.DS_Store +Thumbs.db +/.settings/ +/.project +/.tern-project +site/ From 9f5f33121523562093d38c88d08ac517f0a2c7ec Mon Sep 17 00:00:00 2001 From: "Peter J. Kohler" Date: Sat, 13 Mar 2021 16:03:19 -0500 Subject: [PATCH 2/7] ebbinghaus experiment added --- ebbinghaus/ebbinghaus.css | 1 + ebbinghaus/ebbinghaus.html | 198 ++ ebbinghaus/ebbinghaus_setup_EN.js | 19 + ebbinghaus/jspsych-6.3/css/jspsych.css | 206 ++ .../jspsych-psychophysics.js | 1419 ++++++++ ebbinghaus/jspsych-6.3/jspsych.js | 3015 +++++++++++++++++ .../plugins/jspsych-html-keyboard-response.js | 149 + .../jspsych-6.3/plugins/jspsych-preload.js | 345 ++ 8 files changed, 5352 insertions(+) create mode 100644 ebbinghaus/ebbinghaus.css create mode 100644 ebbinghaus/ebbinghaus.html create mode 100644 ebbinghaus/ebbinghaus_setup_EN.js create mode 100644 ebbinghaus/jspsych-6.3/css/jspsych.css create mode 100755 ebbinghaus/jspsych-6.3/jspsych-psychophysics/jspsych-psychophysics.js create mode 100755 ebbinghaus/jspsych-6.3/jspsych.js create mode 100644 ebbinghaus/jspsych-6.3/plugins/jspsych-html-keyboard-response.js create mode 100644 ebbinghaus/jspsych-6.3/plugins/jspsych-preload.js diff --git a/ebbinghaus/ebbinghaus.css b/ebbinghaus/ebbinghaus.css new file mode 100644 index 0000000..7a05af6 --- /dev/null +++ b/ebbinghaus/ebbinghaus.css @@ -0,0 +1 @@ +body {background-color: rgb(128, 128, 128)} \ No newline at end of file diff --git a/ebbinghaus/ebbinghaus.html b/ebbinghaus/ebbinghaus.html new file mode 100644 index 0000000..3670c0a --- /dev/null +++ b/ebbinghaus/ebbinghaus.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + diff --git a/ebbinghaus/ebbinghaus_setup_EN.js b/ebbinghaus/ebbinghaus_setup_EN.js new file mode 100644 index 0000000..95e2aa3 --- /dev/null +++ b/ebbinghaus/ebbinghaus_setup_EN.js @@ -0,0 +1,19 @@ +const color_inner = 'rgb(191, 191, 191)'; // color of inner discs +const color_outer = 'rgb(64, 64, 64)'; // color of outer discs +const fix_color = 'rgb(255, 255, 255)'; +const ref_inner = 25; // inner disc radius (reference) +const test_inner = [20, 23, 24, 25, 26, 27, 30] // (test) +const ref_outer = 35 // outer disc radius (reference) +const test_outer = [35, 15] // (test) +const dist_discs = 80; // distance between inner and outer discs +const dist_groups = 200; // distance from fixation for each disc group +const repeat_trials = 10; // how many times to repeat each trial type + // total_trials = repeat_trials * 2 * 2 * test_inner.lenght() +const block_number = 10; // how many blocks to include in the experiment + +const prompt_msg = "Keep staring at the cross in the center of the screen.\n\n"+ + "Use the left and right arrow keys to indicate "+ + "which of the center circles is biggest.\n\n" + // message to display before each trial. + // note: '\n\n' is needed at the end of each line + // because prompt is displayed using the psychophysics plugin \ No newline at end of file diff --git a/ebbinghaus/jspsych-6.3/css/jspsych.css b/ebbinghaus/jspsych-6.3/css/jspsych.css new file mode 100644 index 0000000..9a07da4 --- /dev/null +++ b/ebbinghaus/jspsych-6.3/css/jspsych.css @@ -0,0 +1,206 @@ +/* + * CSS for jsPsych experiments. + * + * This stylesheet provides minimal styling to make jsPsych + * experiments look polished without any additional styles. + */ + + @import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700); + +/* Container holding jsPsych content */ + + .jspsych-display-element { + display: flex; + flex-direction: column; + overflow-y: auto; + } + + .jspsych-display-element:focus { + outline: none; + } + + .jspsych-content-wrapper { + display: flex; + margin: auto; + flex: 1 1 100%; + width: 100%; + } + + .jspsych-content { + max-width: 95%; /* this is mainly an IE 10-11 fix */ + text-align: center; + margin: auto; /* this is for overflowing content */ + } + + .jspsych-top { + align-items: flex-start; + } + + .jspsych-middle { + align-items: center; + } + +/* fonts and type */ + +.jspsych-display-element { + font-family: 'Open Sans', 'Arial', sans-serif; + font-size: 18px; + line-height: 1.6em; +} + +/* Form elements like input fields and buttons */ + +.jspsych-display-element input[type="text"] { + font-family: 'Open Sans', 'Arial', sans-serif; + font-size: 14px; +} + +/* borrowing Bootstrap style for btn elements, but combining styles a bit */ +.jspsych-btn { + display: inline-block; + padding: 6px 12px; + margin: 0px; + font-size: 14px; + font-weight: 400; + font-family: 'Open Sans', 'Arial', sans-serif; + cursor: pointer; + line-height: 1.4; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; + color: #333; + background-color: #fff; + border-color: #ccc; +} + +/* only apply the hover style on devices with a mouse/pointer that can hover - issue #977 */ +@media (hover: hover) { + .jspsych-btn:hover { + background-color: #ddd; + border-color: #aaa; + } +} + +.jspsych-btn:active { + background-color: #ddd; + border-color:#000000; +} + +.jspsych-btn:disabled { + background-color: #eee; + color: #aaa; + border-color: #ccc; + cursor: not-allowed; +} + +/* custom style for input[type="range] (slider) to improve alignment between positions and labels */ + +.jspsych-slider { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + width: 100%; + background: transparent; +} +.jspsych-slider:focus { + outline: none; +} +/* track */ +.jspsych-slider::-webkit-slider-runnable-track { + appearance: none; + -webkit-appearance: none; + width: 100%; + height: 8px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +.jspsych-slider::-moz-range-track { + appearance: none; + width: 100%; + height: 8px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +.jspsych-slider::-ms-track { + appearance: none; + width: 99%; + height: 14px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +/* thumb */ +.jspsych-slider::-webkit-slider-thumb { + border: 1px solid #666; + height: 24px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; + -webkit-appearance: none; + margin-top: -9px; +} +.jspsych-slider::-moz-range-thumb { + border: 1px solid #666; + height: 24px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; +} +.jspsych-slider::-ms-thumb { + border: 1px solid #666; + height: 20px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; + margin-top: -2px; +} + +/* jsPsych progress bar */ + +#jspsych-progressbar-container { + color: #555; + border-bottom: 1px solid #dedede; + background-color: #f9f9f9; + margin-bottom: 1em; + text-align: center; + padding: 8px 0px; + width: 100%; + line-height: 1em; +} +#jspsych-progressbar-container span { + font-size: 14px; + padding-right: 14px; +} +#jspsych-progressbar-outer { + background-color: #eee; + width: 50%; + margin: auto; + height: 14px; + display: inline-block; + vertical-align: middle; + box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); +} +#jspsych-progressbar-inner { + background-color: #aaa; + width: 0%; + height: 100%; +} + +/* Control appearance of jsPsych.data.displayData() */ +#jspsych-data-display { + text-align: left; +} diff --git a/ebbinghaus/jspsych-6.3/jspsych-psychophysics/jspsych-psychophysics.js b/ebbinghaus/jspsych-6.3/jspsych-psychophysics/jspsych-psychophysics.js new file mode 100755 index 0000000..5e469ec --- /dev/null +++ b/ebbinghaus/jspsych-6.3/jspsych-psychophysics/jspsych-psychophysics.js @@ -0,0 +1,1419 @@ +/** + * jspsych-psychophysics + * Copyright (c) 2019 Daiichiro Kuroki + * Released under the MIT license + * + * jspsych-psychophysics is a plugin for conducting online/Web-based psychophysical experiments using jsPsych (de Leeuw, 2015). + * + * Please see + * http://jspsychophysics.hes.kyushu-u.ac.jp/ + * about how to use this plugin. + * + **/ + + /* global jsPsych, math, numeric */ + +jsPsych.plugins["psychophysics"] = (function() { + console.log(`jsPsych Version ${jsPsych.version()}`) + console.log('jspsych-psychophysics Version 2.2.1') + + let plugin = {}; + + plugin.info = { + name: 'psychophysics', + description: 'A plugin for conducting online/Web-based psychophysical experiments', + parameters: { + stimuli: { + type: jsPsych.plugins.parameterType.COMPLEX, // This is similar to the quesions of the survey-likert. + array: true, + pretty_name: 'Stimuli', + description: 'The objects will be presented in the canvas.', + nested: { + startX: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'startX', + default: 'center', + description: 'The horizontal start position.' + }, + startY: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'startY', + default: 'center', + description: 'The vertical start position.' + }, + endX: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'endX', + default: null, + description: 'The horizontal end position.' + }, + endY: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'endY', + default: null, + description: 'The vertical end position.' + }, + show_start_time: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Show start time', + default: 0, + description: 'Time to start presenting the stimuli' + }, + show_end_time: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Show end time', + default: null, + description: 'Time to end presenting the stimuli' + }, + show_start_frame: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Show start frame', + default: 0, + description: 'Time to start presenting the stimuli in frames' + }, + show_end_frame: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Show end frame', + default: null, + description: 'Time to end presenting the stimuli in frames' + }, + line_width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Line width', + default: 1, + description: 'The line width' + }, + lineJoin: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'lineJoin', + default: 'miter', + description: 'The type of the corner when two lines meet.' + }, + miterLimit: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'miterLimit', + default: 10, + description: 'The maximum miter length.' + }, + drawFunc: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Draw function', + default: null, + description: 'This function enables to move objects horizontally and vertically.' + }, + change_attr: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Change attributes', + default: null, + description: 'This function enables to change attributes of objects immediately before drawing.' + }, + is_frame: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'time is in frames', + default: false, + description: 'If true, time is treated in frames.' + }, + origin_center: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'origin_center', + default: false, + description: 'The origin is the center of the window.' + }, + is_presented: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'is_presented', + default: false, + description: 'This will be true when the stimulus is presented.' + }, + trial_ends_after_audio: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Trial ends after audio', + default: false, + description: 'If true, then the trial will end as soon as the audio file finishes playing.' + }, + tilt: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'tilt', + default: 0, + description: 'The tilt of the gabor patch.' + }, + sf: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'spatial frequency', + default: 0.05, + description: 'The spatial frequency of the gabor patch.' + }, + phase: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'phase', + default: 0, + description: 'The phase (degrees) of the gabor patch.' + }, + sc: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'standard deviation', + default: 20, + description: 'The standard deviation of the distribution.' + }, + contrast: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'contrast', + default: 20, + description: 'The contrast of the gabor patch.' + }, + drift: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'drift', + default: 0, + description: 'The velocity of the drifting gabor patch.' + }, + method: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'gabor_drawing_method', + default: 'numeric', + description: 'The method of drawing the gabor patch.' + }, + disableNorm: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'disableNorm', + default: false, + description: 'Disable normalization of the gaussian function.' + }, + mask_func: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Masking function', + default: null, + description: 'Masking the image manually.' + }, + + } + }, + choices: { + type: jsPsych.plugins.parameterType.KEYCODE, + array: true, + pretty_name: 'Choices', + default: jsPsych.ALL_KEYS, + description: 'The keys the subject is allowed to press to respond to the stimulus.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the stimulus.' + }, + canvas_width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Canvas width', + default: window.innerWidth, + description: 'The width of the canvas.' + }, + canvas_height: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Canvas height', + default: window.innerHeight, + description: 'The height of the canvas.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show trial before it ends.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, trial will end when subject makes a response.' + }, + background_color: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Background color', + default: 'grey', + description: 'The background color of the canvas.' + }, + response_type: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'key, mouse or button', + default: 'key', + description: 'How to make a response.' + }, + response_start_time: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Response start', + default: 0, + description: 'When the subject is allowed to respond to the stimulus.' + }, + raf_func: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Step function', + default: null, + description: 'This function enables to move objects as you wish.' + }, + mouse_down_func: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Mouse down function', + default: null, + description: 'This function is set to the event listener of the mousedown.' + }, + mouse_move_func: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Mouse move function', + default: null, + description: 'This function is set to the event listener of the mousemove.' + }, + mouse_up_func: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Mouse up function', + default: null, + description: 'This function is set to the event listener of the mouseup.' + }, + key_down_func:{ + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Key down function', + default: null, + description: 'This function is set to the event listener of the keydown.' + }, + key_up_func:{ + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Key up function', + default: null, + description: 'This function is set to the event listener of the keyup.' + }, + button_choices: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button choices', + // default: undefined, + default: ['Next'], + array: true, + description: 'The labels for the buttons.' + }, + button_html: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button HTML', + default: '', + array: true, + description: 'The html of the button. Can create own style.' + }, + vert_button_margin: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin vertical', + default: '0px', + description: 'The vertical margin of the button.' + }, + horiz_button_margin: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin horizontal', + default: '8px', + description: 'The horizontal margin of the button.' + }, + clear_canvas: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'clear_canvas', + default: true, + description: 'Clear the canvas per frame.' + } + } + } + + plugin.trial = function(display_element, trial) { + + // returns an array starting with 'start_num' of which length is 'count'. + function getNumbering(start_num, count) { + return [...Array(count)].map((_, i) => i + start_num) + } + + // Class for visual and audio stimuli + class psychophysics_stimulus { + constructor(stim) { + Object.assign(this, stim) + const keys = Object.keys(this) + for (var i = 0; i < keys.length; i++) { + if (typeof this[keys[i]] === "function") { + // オブジェクト内のfunctionはここで指定する必要がある。そうしないとここで即時に実行されて、その結果が関数名に代入される + if (keys[i] === "drawFunc") continue + if (keys[i] === "change_attr") continue + if (keys[i] === "mask_func") continue + + this[keys[i]] = this[keys[i]].call() + } + } + } + } + + class visual_stimulus extends psychophysics_stimulus { + constructor(stim) { + super(stim); + + if (this.startX === 'center') { + if (this.origin_center) { + this.startX = 0; + } else { + this.startX = centerX; + } + } + if (this.startY === 'center') { + if (this.origin_center) { + this.startY = 0; + } else { + this.startY = centerY; + } + } + if (this.endX === 'center') { + if (this.origin_center) { + this.endX = 0; + } else { + this.endX = centerX; + } + } + if (this.endY === 'center') { + if (this.origin_center) { + this.endY = 0; + } else { + this.endY = centerY; + } + } + + if (this.origin_center) { + this.startX = this.startX + centerX; + this.startY = this.startY + centerY; + if (this.endX !== null) this.endX = this.endX + centerX; + if (this.endY !== null) this.endY = this.endY + centerY; + } + + if (typeof this.motion_start_time === 'undefined') this.motion_start_time = this.show_start_time; // Motion will start at the same time as it is displayed. + if (typeof this.motion_end_time === 'undefined') this.motion_end_time = null; + if (typeof this.motion_start_frame === 'undefined') this.motion_start_frame = this.show_start_frame; // Motion will start at the same frame as it is displayed. + if (typeof this.motion_end_frame === 'undefined') this.motion_end_frame = null; + + if (trial.clear_canvas === false && this.show_end_time !== null) alert('You can not specify the show_end_time with the clear_canvas property.'); + + // calculate the velocity (pix/sec) using the distance and the time. + // If the pix_sec is specified, the calc_pix_per_sec returns the intact pix_sec. + // If the pix_frame is specified, the calc_pix_per_sec returns an undefined. + this.horiz_pix_sec = this.calc_pix_per_sec('horiz'); + this.vert_pix_sec = this.calc_pix_per_sec('vert'); + + // currentX/Y is changed per frame. + this.currentX = this.startX; + this.currentY = this.startY; + + } + + calc_pix_per_sec (direction){ + let pix_sec , pix_frame, startPos, endPos; + if (direction === 'horiz'){ + pix_sec = this.horiz_pix_sec; + pix_frame = this.horiz_pix_frame; + startPos = this.startX; + endPos = this.endX; + } else { + pix_sec = this.vert_pix_sec; + pix_frame = this.vert_pix_frame; + startPos = this.startY; + endPos = this.endY; + } + const motion_start_time = this.motion_start_time; + const motion_end_time = this.motion_end_time; + if ((typeof pix_sec !== 'undefined' || typeof pix_frame !== 'undefined') && endPos !== null && motion_end_time !== null) { + alert('You can not specify the speed, location, and time at the same time.'); + pix_sec = 0; // stop the motion + } + + if (typeof pix_sec !== 'undefined' || typeof pix_frame !== 'undefined') return pix_sec; // returns an 'undefined' when you specify the pix_frame. + + // The velocity is not specified + + if (endPos === null) return 0; // This is not motion. + + if (startPos === endPos) return 0; // This is not motion. + + + // The distance is specified + + if (motion_end_time === null) { // Only the distance is known + alert('Please specify the motion_end_time or the velocity when you use the endX/Y property.') + return 0; // stop the motion + } + + return (endPos - startPos)/(motion_end_time/1000 - motion_start_time/1000); + } + + calc_current_position (direction, elapsed){ + let pix_frame, pix_sec, current_pos, start_pos, end_pos; + + if (direction === 'horiz'){ + pix_frame = this.horiz_pix_frame + pix_sec = this.horiz_pix_sec + current_pos = this.currentX + start_pos = this.startX + end_pos = this.endX + } else { + pix_frame = this.vert_pix_frame + pix_sec = this.vert_pix_sec + current_pos = this.currentY + start_pos = this.startY + end_pos = this.endY + } + + const motion_start = this.is_frame ? this.motion_start_frame : this.motion_start_time; + const motion_end = this.is_frame ? this.motion_end_frame : this.motion_end_time; + + if (elapsed < motion_start) return current_pos + if (motion_end !== null && elapsed >= motion_end) return current_pos + + // Note that: You can not specify the speed, location, and time at the same time. + + let ascending = true; // true = The object moves from left to right, or from up to down. + + if (typeof pix_frame === 'undefined'){ // In this case, pix_sec is defined. + if (pix_sec < 0) ascending = false; + } else { + if (pix_frame < 0) ascending = false; + } + + if (end_pos === null || (ascending && current_pos <= end_pos) || (!ascending && current_pos >= end_pos)) { + if (typeof pix_frame === 'undefined'){ // In this case, pix_sec is defined. + return start_pos + Math.round(pix_sec * (elapsed - motion_start)/1000); // This should be calculated in seconds. + } else { + return current_pos + pix_frame; + } + } else { + return current_pos + } + } + + update_position(elapsed){ + this.currentX = this.calc_current_position ('horiz', elapsed) + this.currentY = this.calc_current_position ('vert', elapsed) + } + } + + class image_stimulus extends visual_stimulus { + constructor(stim){ + super(stim); + + if (typeof this.file === 'undefined') { + alert('You have to specify the file property.'); + return; + } + this.img = new Image(); + this.img.src = this.file; + + if (typeof this.mask !== 'undefined' || typeof this.filter !== 'undefined') { + // For masking and filtering, draw the image on another canvas and get its pixel data using the getImageData function. + // In addition, masking does work only online, that is, the javascript and image files must be uploaded on the web server. + + if (document.getElementById('invisible_canvas') === null) { + const canvas_element = document.createElement('canvas'); + canvas_element.id = 'invisible_canvas'; + display_element.appendChild(canvas_element) + canvas_element.style.display = 'none' + } + + const invisible_canvas = document.getElementById('invisible_canvas'); + invisible_canvas.width = this.img.width // The width/height of the canvas is not automatically adjusted. + invisible_canvas.height = this.img.height + const invisible_ctx = invisible_canvas.getContext('2d'); + invisible_ctx.clearRect(0, 0, invisible_canvas.width, invisible_canvas.height); + + if (typeof this.filter === 'undefined') { + invisible_ctx.filter = 'none' + } else { + invisible_ctx.filter = this.filter + } + + invisible_ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height); + + if (typeof this.mask === 'undefined'){ // Filtering only + const invisible_img = invisible_ctx.getImageData(0, 0, this.img.width, this.img.height); + this.masking_img = invisible_img; + return + } + + if (this.mask === 'manual'){ + if (this.mask_func === null) { + alert('You have to specify the mask_func when applying masking manually.'); + return; + } + this.masking_img = this.mask_func(invisible_canvas); + return + } + + if (this.mask === 'gauss'){ + if (typeof this.width === 'undefined') { + alert('You have to specify the width property for the gaussian mask. For example, 200.'); + return; + } + const gauss_width = this.width + + // 画像の全体ではなく、フィルタリングを行う部分だけを取り出す + // Getting only the areas to be filtered, not the whole image. + const invisible_img = invisible_ctx.getImageData(this.img.width/2 - gauss_width/2, this.img.height/2 - gauss_width/2, gauss_width, gauss_width); + + let coord_array = getNumbering(Math.round(0 - gauss_width/2), gauss_width) + let coord_matrix_x = [] + for (let i = 0; i< gauss_width; i++){ + coord_matrix_x.push(coord_array) + } + + coord_array = getNumbering(Math.round(0 - gauss_width/2), gauss_width) + let coord_matrix_y = [] + for (let i = 0; i< gauss_width; i++){ + coord_matrix_y.push(coord_array) + } + + let exp_value; + if (this.method === 'math') { + const matrix_x = math.matrix(coord_matrix_x) // Convert to Matrix data + const matrix_y = math.transpose(math.matrix(coord_matrix_y)) + const x_factor = math.multiply(-1, math.square(matrix_x)) + const y_factor = math.multiply(-1, math.square(matrix_y)) + const varScale = 2 * math.square(this.sc) + const tmp = math.add(math.divide(x_factor, varScale), math.divide(y_factor, varScale)); + exp_value = math.exp(tmp) + } else { // numeric + const matrix_x = coord_matrix_x + const matrix_y = numeric.transpose(coord_matrix_y) + const x_factor = numeric.mul(-1, numeric.pow(matrix_x, 2)) + const y_factor = numeric.mul(-1, numeric.pow(matrix_y, 2)) + const varScale = 2 * numeric.pow([this.sc], 2) + const tmp = numeric.add(numeric.div(x_factor, varScale), numeric.div(y_factor, varScale)); + exp_value = numeric.exp(tmp) + } + + let cnt = 3; + for (let i = 0; i < gauss_width; i++) { + for (let j = 0; j < gauss_width; j++) { + invisible_img.data[cnt] = exp_value[i][j] * 255 // 透明度を変更 + cnt = cnt + 4; + } + } + this.masking_img = invisible_img; + return + } + + if (this.mask === 'circle' || this.mask === 'rect'){ + if (typeof this.width === 'undefined') { + alert('You have to specify the width property for the circle/rect mask.'); + return; + } + if (typeof this.height === 'undefined') { + alert('You have to specify the height property for the circle/rect mask.'); + return; + } + if (typeof this.center_x === 'undefined') { + alert('You have to specify the center_x property for the circle/rect mask.'); + return; + } + if (typeof this.center_y === 'undefined') { + alert('You have to specify the center_y property for the circle/rect mask.'); + return; + } + + const oval_width = this.width + const oval_height = this.height + const oval_cx = this.center_x + const oval_cy = this.center_y + + // 画像の全体ではなく、フィルタリングを行う部分だけを取り出す + // Getting only the areas to be filtered, not the whole image. + const invisible_img = invisible_ctx.getImageData(oval_cx - oval_width/2, oval_cy - oval_height/2, oval_width, oval_height); + + const cx = invisible_img.width/2 + const cy = invisible_img.height/2 + + if (this.mask === 'circle'){ + let cnt = 3; + for (let j = 0; j < oval_height; j++) { + for (let i = 0; i < oval_width; i++) { + const tmp = Math.pow(i-cx, 2)/Math.pow(cx, 2) + Math.pow(j-cy, 2)/Math.pow(cy, 2) + if (tmp > 1){ + invisible_img.data[cnt] = 0 // invisible + } + cnt = cnt + 4; + } + } + } + + // When this.mask === 'rect', the alpha (transparency) value does not chage at all. + + this.masking_img = invisible_img; + return + } + } + } + + show(){ + if (this.mask || this.filter){ + // Note that filtering is done to the invisible_ctx. + ctx.putImageData(this.masking_img, this.currentX - this.masking_img.width/2, this.currentY - this.masking_img.height/2); + } else { + const scale = typeof this.scale === 'undefined' ? 1:this.scale; + const tmpW = this.img.width * scale; + const tmpH = this.img.height * scale; + ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height, this.currentX - tmpW / 2, this.currentY - tmpH / 2, tmpW, tmpH); + } + } + } + + class gabor_stimulus extends visual_stimulus { + constructor(stim){ + super(stim); + this.update_count = 0; + } + + show(){ + ctx.putImageData(this.img_data, this.currentX - this.img_data.width/2, this.currentY - this.img_data.height/2) + } + + update_position(elapsed){ + + this.currentX = this.calc_current_position ('horiz', elapsed) + this.currentY = this.calc_current_position ('vert', elapsed) + + if (typeof this.img_data !== 'undefined' && this.drift === 0) return + + let gabor_data; + // console.log(this.method) + + // The following calculation method is based on Psychtoolbox (MATLAB), + // although it doesn't use procedural texture mapping. + // I also have referenced the gaborgen-js code: https://github.com/jtth/gaborgen-js + + // You can choose either the numeric.js or the math.js as the method for drawing gabor patches. + // The numeric.js is considerably faster than the math.js, but the latter is being developed more aggressively than the former. + // Note that "Math" and "math" are not the same. + + let coord_array = getNumbering(Math.round(0 - this.width/2), this.width) + let coord_matrix_x = [] + for (let i = 0; i< this.width; i++){ + coord_matrix_x.push(coord_array) + } + + coord_array = getNumbering(Math.round(0 - this.width/2), this.width) + let coord_matrix_y = [] + for (let i = 0; i< this.width; i++){ + coord_matrix_y.push(coord_array) + } + + const tilt_rad = deg2rad(90 - this.tilt) + + // These values are scalars. + const a = Math.cos(tilt_rad) * this.sf * (2 * Math.PI) // radians + const b = Math.sin(tilt_rad) * this.sf * (2 * Math.PI) + let multConst = 1 / (Math.sqrt(2*Math.PI) * this.sc) + if (this.disableNorm) multConst = 1 + + + // const phase_rad = deg2rad(this.phase) + const phase_rad = deg2rad(this.phase + this.drift * this.update_count) + this.update_count += 1 + + if (this.method === 'math') { + const matrix_x = math.matrix(coord_matrix_x) // Convert to Matrix data + const matrix_y = math.transpose(math.matrix(coord_matrix_y)) + const x_factor = math.multiply(-1, math.square(matrix_x)) + const y_factor = math.multiply(-1, math.square(matrix_y)) + const tmp1 = math.add(math.multiply(a, matrix_x), math.multiply(b, matrix_y), phase_rad) // radians + const sinWave = math.sin(tmp1) + const varScale = 2 * math.square(this.sc) + const tmp2 = math.add(math.divide(x_factor, varScale), math.divide(y_factor, varScale)); + const exp_value = math.exp(tmp2) + const tmp3 = math.dotMultiply(exp_value, sinWave) + const tmp4 = math.multiply(multConst, tmp3) + const tmp5 = math.multiply(this.contrast, tmp4) + const m = math.multiply(256, math.add(0.5, tmp5)) + gabor_data = m._data + } else { // numeric + const matrix_x = coord_matrix_x + const matrix_y = numeric.transpose(coord_matrix_y) + const x_factor = numeric.mul(-1, numeric.pow(matrix_x, 2)) + const y_factor = numeric.mul(-1, numeric.pow(matrix_y, 2)) + const tmp1 = numeric.add(numeric.mul(a, matrix_x), numeric.mul(b, matrix_y), phase_rad) // radians + const sinWave = numeric.sin(tmp1) + const varScale = 2 * numeric.pow([this.sc], 2) + const tmp2 = numeric.add(numeric.div(x_factor, varScale), numeric.div(y_factor, varScale)); + const exp_value = numeric.exp(tmp2) + const tmp3 = numeric.mul(exp_value, sinWave) + const tmp4 = numeric.mul(multConst, tmp3) + const tmp5 = numeric.mul(this.contrast, tmp4) + const m = numeric.mul(256, numeric.add(0.5, tmp5)) + gabor_data = m + } + // console.log(gabor_data) + const imageData = ctx.createImageData(this.width, this.width); + let cnt = 0; + // Iterate through every pixel + for (let i = 0; i < this.width; i++) { + for (let j = 0; j < this.width; j++) { + // Modify pixel data + imageData.data[cnt] = Math.round(gabor_data[i][j]); // R value + cnt++; + imageData.data[cnt] = Math.round(gabor_data[i][j]); // G + cnt++; + imageData.data[cnt] = Math.round(gabor_data[i][j]); // B + cnt++; + imageData.data[cnt] = 255; // alpha + cnt++; + } + } + + this.img_data = imageData + } + } + + class line_stimulus extends visual_stimulus{ + constructor(stim){ + super(stim) + + if (typeof this.angle === 'undefined') { + if ((typeof this.x1 === 'undefined') || (typeof this.x2 === 'undefined') || (typeof this.y1 === 'undefined') || (typeof this.y2 === 'undefined')){ + alert('You have to specify the angle of lines, or the start (x1, y1) and end (x2, y2) coordinates.'); + return; + } + // The start (x1, y1) and end (x2, y2) coordinates are defined. + // For motion, startX/Y must be calculated. + this.startX = (this.x1 + this.x2)/2; + this.startY = (this.y1 + this.y2)/2; + if (this.origin_center) { + this.startX = this.startX + centerX; + this.startY = this.startY + centerY; + } + this.currentX = this.startX; + this.currentY = this.startY; + this.angle = Math.atan((this.y2 - this.y1)/(this.x2 - this.x1)) * (180 / Math.PI); + this.line_length = Math.sqrt((this.x2 - this.x1) ** 2 + (this.y2 - this.y1) ** 2); + } else { + if ((typeof this.x1 !== 'undefined') || (typeof this.x2 !== 'undefined') || (typeof this.y1 !== 'undefined') || (typeof this.y2 !== 'undefined')) + alert('You can not specify the angle and positions of the line at the same time.') + if (typeof this.line_length === 'undefined') alert('You have to specify the line_length property.'); + + } + if (typeof this.line_color === 'undefined') this.line_color = '#000000'; + + } + + show(){ + if (typeof this.filter === 'undefined') { + ctx.filter = 'none' + } else { + ctx.filter = this.filter + } + + // common + ctx.beginPath(); + ctx.lineWidth = this.line_width; + ctx.lineJoin = this.lineJoin; + ctx.miterLimit = this.miterLimit; + // + const theta = deg2rad(this.angle); + const x1 = this.currentX - this.line_length/2 * Math.cos(theta); + const y1 = this.currentY - this.line_length/2 * Math.sin(theta); + const x2 = this.currentX + this.line_length/2 * Math.cos(theta); + const y2 = this.currentY + this.line_length/2 * Math.sin(theta); + ctx.strokeStyle = this.line_color; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + + } + } + + class rect_stimulus extends visual_stimulus{ + constructor(stim){ + super(stim) + + if (typeof this.width === 'undefined') alert('You have to specify the width of the rectangle.'); + if (typeof this.height === 'undefined') alert('You have to specify the height of the rectangle.'); + if (typeof this.line_color === 'undefined' && typeof this.fill_color === 'undefined') alert('You have to specify the either of the line_color or fill_color property.'); + + } + + show(){ + if (typeof this.filter === 'undefined') { + ctx.filter = 'none' + } else { + ctx.filter = this.filter + } + + // common + // ctx.beginPath(); + ctx.lineWidth = this.line_width; + ctx.lineJoin = this.lineJoin; + ctx.miterLimit = this.miterLimit; + // + // First, draw a filled rectangle, then an edge. + if (typeof this.fill_color !== 'undefined') { + ctx.fillStyle = this.fill_color; + ctx.fillRect(this.currentX-this.width/2, this.currentY-this.height/2, this.width, this.height); + } + if (typeof this.line_color !== 'undefined') { + ctx.strokeStyle = this.line_color; + ctx.strokeRect(this.currentX-this.width/2, this.currentY-this.height/2, this.width, this.height); + } + + } + } + + class cross_stimulus extends visual_stimulus { + constructor(stim) { + super(stim); + + if (typeof this.line_length === 'undefined') alert('You have to specify the line_length of the fixation cross.'); + if (typeof this.line_color === 'undefined') this.line_color = '#000000'; + } + + show(){ + if (typeof this.filter === 'undefined') { + ctx.filter = 'none' + } else { + ctx.filter = this.filter + } + + // common + ctx.beginPath(); + ctx.lineWidth = this.line_width; + ctx.lineJoin = this.lineJoin; + ctx.miterLimit = this.miterLimit; + // + ctx.strokeStyle = this.line_color; + const x1 = this.currentX; + const y1 = this.currentY - this.line_length/2; + const x2 = this.currentX; + const y2 = this.currentY + this.line_length/2; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + const x3 = this.currentX - this.line_length/2; + const y3 = this.currentY; + const x4 = this.currentX + this.line_length/2; + const y4 = this.currentY; + ctx.moveTo(x3, y3); + ctx.lineTo(x4, y4); + // ctx.closePath(); + ctx.stroke(); + } + } + + class circle_stimulus extends visual_stimulus { + constructor(stim){ + super(stim); + + if (typeof this.radius === 'undefined') alert('You have to specify the radius of circles.'); + if (typeof this.line_color === 'undefined' && typeof this.fill_color === 'undefined') alert('You have to specify the either of line_color or fill_color.'); + } + + show(){ + if (typeof this.filter === 'undefined') { + ctx.filter = 'none' + } else { + ctx.filter = this.filter + } + + // common + ctx.beginPath(); + ctx.lineWidth = this.line_width; + ctx.lineJoin = this.lineJoin; + ctx.miterLimit = this.miterLimit; + // + if (typeof this.fill_color !== 'undefined') { + ctx.fillStyle = this.fill_color; + ctx.arc(this.currentX, this.currentY, this.radius, 0, Math.PI*2, false); + ctx.fill(); + } + if (typeof this.line_color !== 'undefined') { + ctx.strokeStyle = this.line_color; + ctx.arc(this.currentX, this.currentY, this.radius, 0, Math.PI*2, false); + ctx.stroke(); + } + + } + } + + class text_stimulus extends visual_stimulus { + constructor(stim){ + super(stim) + + if (typeof this.content === 'undefined') alert('You have to specify the content of texts.'); + if (typeof this.text_color === 'undefined') this.text_color = '#000000'; + if (typeof this.text_space === 'undefined') this.text_space = 20; + + } + + show(){ + if (typeof this.filter === 'undefined') { + ctx.filter = 'none' + } else { + ctx.filter = this.filter + } + + // common + // ctx.beginPath(); + ctx.lineWidth = this.line_width; + ctx.lineJoin = this.lineJoin; + ctx.miterLimit = this.miterLimit; + // + if (typeof this.font !== 'undefined') ctx.font = this.font; + + ctx.fillStyle = this.text_color; + ctx.textAlign = "center"; + ctx.textBaseline = "middle" + + let column = ['']; + let line = 0; + for (let i = 0; i < this.content.length; i++) { + let char = this.content.charAt(i); + + if (char == "\n") { + line++; + column[line] = ''; + } + column[line] += char; + } + + for (let i = 0; i < column.length; i++) { + ctx.fillText(column[i], this.currentX, this.currentY - this.text_space * (column.length-1) / 2 + this.text_space * i); + } + + } + } + + class manual_stimulus extends visual_stimulus{ + constructor(stim){ + super(stim) + } + + show(){} + } + + class audio_stimulus extends psychophysics_stimulus{ + constructor(stim){ + super(stim) + + if (typeof this.file === 'undefined') { + alert('You have to specify the file property.') + return; + } + + // setup stimulus + this.context = jsPsych.pluginAPI.audioContext(); + + // load audio file + jsPsych.pluginAPI.getAudioBuffer(this.file) + .then(function (buffer) { + if (this.context !== null) { + this.audio = this.context.createBufferSource(); + this.audio.buffer = buffer; + this.audio.connect(this.context.destination); + console.log('WebAudio') + } else { + this.audio = buffer; + this.audio.currentTime = 0; + console.log('HTML5 audio') + } + // setupTrial(); + }.bind(this)) + .catch(function (err) { + console.error(`Failed to load audio file "${this.file}". Try checking the file path. We recommend using the preload plugin to load audio files.`) + console.error(err) + }.bind(this)); + + + // set up end event if trial needs it + if (this.trial_ends_after_audio) { + this.audio.addEventListener('ended', end_trial); + } + } + + play(){ + // start audio + if(this.context !== null){ + //startTime = this.context.currentTime; + // オリジナルのjspsychではwebaudioが使えるときは時間のデータとしてcontext.currentTimeを使っている。 + // psychophysicsプラグインでは、performance.now()で統一している + this.audio.start(this.context.currentTime); + } else { + this.audio.play(); + } + } + + stop(){ + if(this.context !== null){ + this.audio.stop(); + // this.source.onended = function() { } + } else { + this.audio.pause(); + + } + this.audio.removeEventListener('ended', end_trial); + + } + } + + if (typeof trial.stepFunc !== 'undefined') alert(`The stepFunc is no longer supported. Please use the raf_func instead.`) + + const elm_jspsych_content = document.getElementById('jspsych-content'); + const style_jspsych_content = window.getComputedStyle(elm_jspsych_content); // stock + const default_maxWidth = style_jspsych_content.maxWidth; + elm_jspsych_content.style.maxWidth = 'none'; // The default value is '95%'. To fit the window. + + let new_html = ''; + + const motion_rt_method = 'performance'; // 'date' or 'performance'. 'performance' is better. + let start_time; // used for mouse and button responses. + let keyboardListener; + + // allow to respond using keyboard mouse or button + jsPsych.pluginAPI.setTimeout(function() { + if (trial.response_type === 'key'){ + if (trial.choices != jsPsych.NO_KEYS) { + keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: after_response, + valid_responses: trial.choices, + rt_method: motion_rt_method, + persist: false, + allow_held_key: false + }); + } + } else if (trial.response_type === 'mouse') { + + if (motion_rt_method == 'date') { + start_time = (new Date()).getTime(); + } else { + start_time = performance.now(); + } + + canvas.addEventListener("mousedown", mouseDownFunc); + } else { // button + start_time = performance.now(); + for (let i = 0; i < trial.button_choices.length; i++) { + display_element.querySelector('#jspsych-image-button-response-button-' + i).addEventListener('click', function(e){ + const choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility + // after_response(choice); + // console.log(performance.now()) + // console.log(start_time) + after_response({ + key: -1, + rt: performance.now() - start_time, + button: choice, + }); + + }); + } + } + }, trial.response_start_time); + + //display buttons + if (trial.response_type === 'button'){ + let buttons = []; + if (Array.isArray(trial.button_html)) { + if (trial.button_html.length == trial.button_choices.length) { + buttons = trial.button_html; + } else { + console.error('Error: The length of the button_html array does not equal the length of the button_choices array'); + } + } else { + for (let i = 0; i < trial.button_choices.length; i++) { + buttons.push(trial.button_html); + } + } + new_html += '
'; + for (let i = 0; i < trial.button_choices.length; i++) { + let str = buttons[i].replace(/%choice%/g, trial.button_choices[i]); + new_html += '
'+str+'
'; + } + new_html += '
'; + + } + + + // add prompt + if(trial.prompt !== null){ + new_html += trial.prompt; + } + + // draw + display_element.innerHTML = new_html; + + + const canvas = document.getElementById('myCanvas'); + if ( ! canvas || ! canvas.getContext ) { + alert('This browser does not support the canvas element.'); + return; + } + const ctx = canvas.getContext('2d'); + + trial.canvas = canvas; + trial.context = ctx; + + const centerX = canvas.width/2; + const centerY = canvas.height/2; + trial.centerX = centerX; + trial.centerY = centerY; + + // add event listeners defined by experimenters. + if (trial.mouse_down_func !== null){ + canvas.addEventListener("mousedown", trial.mouse_down_func); + } + + if (trial.mouse_move_func !== null){ + canvas.addEventListener("mousemove", trial.mouse_move_func); + } + + if (trial.mouse_up_func !== null){ + canvas.addEventListener("mouseup", trial.mouse_up_func); + } + + if (trial.key_down_func !== null){ + document.addEventListener("keydown", trial.key_down_func); // It doesn't work if the canvas is specified instead of the document. + } + + if (trial.key_up_func !== null){ + document.addEventListener("keyup", trial.key_up_func); + } + + if (typeof trial.stimuli === 'undefined' && trial.raf_func === null){ + alert('You have to specify the stimuli/raf_func parameter in the psychophysics plugin.') + return + } + + + ///////////////////////////////////////////////////////// + // make instances + const oop_stim = [] + const set_instance = { + sound: audio_stimulus, + image: image_stimulus, + line: line_stimulus, + rect: rect_stimulus, + circle: circle_stimulus, + text: text_stimulus, + cross: cross_stimulus, + manual: manual_stimulus, + gabor: gabor_stimulus + } + if (typeof trial.stimuli !== 'undefined') { // The stimuli could be 'undefined' if the raf_func is specified. + for (let i = 0; i < trial.stimuli.length; i++){ + const stim = trial.stimuli[i]; + if (typeof stim.obj_type === 'undefined'){ + alert('You have missed to specify the obj_type property in the ' + (i+1) + 'th object.'); + return + } + oop_stim.push(new set_instance[stim.obj_type](stim)) + } + } + trial.stim_array = oop_stim + // for (let i = 0; i < trial.stim_array.length; i++){ + // console.log(trial.stim_array[i].is_presented) + // } + + function mouseDownFunc(e){ + + let click_time; + + if (motion_rt_method == 'date') { + click_time = (new Date()).getTime(); + } else { + click_time = performance.now(); + } + + e.preventDefault(); + + after_response({ + key: -1, + rt: click_time - start_time, + // clickX: e.clientX, + // clickY: e.clientY, + clickX: e.offsetX, + clickY: e.offsetY, + }); + } + + let startStep = null; + let sumOfStep; + let elapsedTime; + //let currentX, currentY; + function step(timestamp){ + if (!startStep) { + startStep = timestamp; + sumOfStep = 0; + } else { + sumOfStep += 1; + } + elapsedTime = timestamp - startStep; // unit is ms. This can be used within the raf_func(). + + if (trial.clear_canvas) + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (trial.raf_func !== null) { + trial.raf_func(trial, elapsedTime, sumOfStep); // customize + frameRequestID = window.requestAnimationFrame(step); + return + } + + for (let i = 0; i < trial.stim_array.length; i++){ + const stim = trial.stim_array[i]; + const elapsed = stim.is_frame ? sumOfStep : elapsedTime; + const show_start = stim.is_frame ? stim.show_start_frame : stim.show_start_time; + const show_end = stim.is_frame ? stim.show_end_frame : stim.show_end_time; + + if (stim.obj_type === 'sound'){ + if (elapsed >= show_start && !stim.is_presented){ + stim.play(); // play the sound. + stim.is_presented = true; + } + continue; + } + + // visual stimuli + if (elapsed < show_start) continue; + if (show_end !== null && elapsed >= show_end) continue; + if (trial.clear_canvas === false && stim.is_presented) continue; + + stim.update_position(elapsed); + + if (stim.drawFunc !== null) { + stim.drawFunc(stim, canvas, ctx); + } else { + if (stim.change_attr != null) stim.change_attr(stim, elapsedTime, sumOfStep) + stim.show() + } + stim.is_presented = true; + } + frameRequestID = window.requestAnimationFrame(step); + } + + // Start the step function. + let frameRequestID = window.requestAnimationFrame(step); + + + function deg2rad(degrees){ + return degrees / 180 * Math.PI; + } + + // store response + let response = { + rt: null, + key: null + }; + + // function to end trial when it is time + // let end_trial = function() { // This causes an initialization error at stim.audio.addEventListener('ended', end_trial); + function end_trial(){ + // console.log(default_maxWidth) + document.getElementById('jspsych-content').style.maxWidth = default_maxWidth; // restore + window.cancelAnimationFrame(frameRequestID); //Cancels the frame request + canvas.removeEventListener("mousedown", mouseDownFunc); + + // remove event listeners defined by experimenters. + if (trial.mouse_down_func !== null){ + canvas.removeEventListener("mousedown", trial.mouse_down_func); + } + + if (trial.mouse_move_func !== null){ + canvas.removeEventListener("mousemove", trial.mouse_move_func); + } + + if (trial.mouse_up_func !== null){ + canvas.removeEventListener("mouseup", trial.mouse_up_func); + } + + if (trial.key_down_func !== null){ + document.removeEventListener("keydown", trial.key_down_func); + } + + if (trial.key_up_func !== null){ + document.removeEventListener("keyup", trial.key_up_func); + } + + // stop the audio file if it is playing + // remove end event listeners if they exist + if (typeof trial.stim_array !== 'undefined') { // The stimuli could be 'undefined' if the raf_func is specified. + for (let i = 0; i < trial.stim_array.length; i++){ + const stim = trial.stim_array[i]; + // stim.is_presented = false; + // if (typeof stim.context !== 'undefined') { // If the stimulus is audio data + if (stim.obj_type === 'sound') { // If the stimulus is audio data + stim.stop(); + } + } + } + + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + + // kill keyboard listeners + if (typeof keyboardListener !== 'undefined') { + jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); + } + + // gather the data to store for the trial //音の再生時からの反応時間をとるわけではないから不要? + // if(context !== null && response.rt !== null){ + // response.rt = Math.round(response.rt * 1000); + // } + + // gather the data to store for the trial + const trial_data = {} + trial_data['rt'] = response.rt; + trial_data['response_type'] = trial.response_type; + trial_data['key_press'] = response.key; + trial_data['response'] = response.key; // compatible with the jsPsych >= 6.3.0 + trial_data['avg_frame_time'] = elapsedTime/sumOfStep; + trial_data['center_x'] = centerX; + trial_data['center_y'] = centerY; + + if (trial.response_type === 'mouse'){ + trial_data['click_x'] = response.clickX; + trial_data['click_y'] = response.clickY; + } else if (trial.response_type === 'button'){ + trial_data['button_pressed'] = response.button; + } + + // clear the display + display_element.innerHTML = ''; + + // move on to the next trial + jsPsych.finishTrial(trial_data); + } + + // function to handle responses by the subject + // let after_response = function(info) { // This causes an initialization error at stim.audio.addEventListener('ended', end_trial); + function after_response(info) { + + // after a valid response, the stimulus will have the CSS class 'responded' + // which can be used to provide visual feedback that a response was recorded + //display_element.querySelector('#jspsych-html-keyboard-response-stimulus').className += ' responded'; + + // only record the first response + if (response.key == null) { + response = info; + } + + if (trial.response_type === 'button'){ + // after a valid response, the stimulus will have the CSS class 'responded' + // which can be used to provide visual feedback that a response was recorded + // display_element.querySelector('#jspsych-image-button-response-stimulus').className += ' responded'; + + // disable all the buttons after a response + let btns = document.querySelectorAll('.jspsych-image-button-response-button button'); + for(let i=0; i tag and the entire page + if(typeof opts.display_element == 'undefined'){ + // check if there is a body element on the page + var body = document.querySelector('body'); + if (body === null) { + document.documentElement.appendChild(document.createElement('body')); + } + // using the full page, so we need the HTML element to + // have 100% height, and body to be full width and height with + // no margin + document.querySelector('html').style.height = '100%'; + document.querySelector('body').style.margin = '0px'; + document.querySelector('body').style.height = '100%'; + document.querySelector('body').style.width = '100%'; + opts.display_element = document.querySelector('body'); + } else { + // make sure that the display element exists on the page + var display; + if (opts.display_element instanceof Element) { + var display = opts.display_element; + } else { + var display = document.querySelector('#' + opts.display_element); + } + if(display === null) { + console.error('The display_element specified in jsPsych.init() does not exist in the DOM.'); + } else { + opts.display_element = display; + } + } + opts.display_element.innerHTML = '
'; + DOM_container = opts.display_element; + DOM_target = document.querySelector('#jspsych-content'); + + + // add tabIndex attribute to scope event listeners + opts.display_element.tabIndex = 0; + + // add CSS class to DOM_target + if(opts.display_element.className.indexOf('jspsych-display-element') == -1){ + opts.display_element.className += ' jspsych-display-element'; + } + DOM_target.className += 'jspsych-content'; + + // set experiment_width if not null + if(opts.experiment_width !== null){ + DOM_target.style.width = opts.experiment_width + "px"; + } + + // create experiment timeline + timeline = new TimelineNode({ + timeline: opts.timeline + }); + + // initialize audio context based on options and browser capabilities + jsPsych.pluginAPI.initAudio(); + + // below code resets event listeners that may have lingered from + // a previous incomplete experiment loaded in same DOM. + jsPsych.pluginAPI.reset(opts.display_element); + // create keyboard event listeners + jsPsych.pluginAPI.createKeyboardEventListeners(opts.display_element); + // create listeners for user browser interaction + jsPsych.data.createInteractionListeners(); + + // add event for closing window + window.addEventListener('beforeunload', opts.on_close); + + // check exclusions before continuing + checkExclusions(opts.exclusions, + function(){ + // success! user can continue... + // start experiment + loadExtensions(); + }, + function(){ + // fail. incompatible user. + } + ); + + function loadExtensions() { + // run the .initialize method of any extensions that are in use + // these should return a Promise to indicate when loading is complete + if (opts.extensions.length == 0) { + startExperiment(); + } else { + var loaded_extensions = 0; + for (var i = 0; i < opts.extensions.length; i++) { + var ext_params = opts.extensions[i].params; + if (!ext_params) { + ext_params = {} + } + jsPsych.extensions[opts.extensions[i].type].initialize(ext_params) + .then(() => { + loaded_extensions++; + if (loaded_extensions == opts.extensions.length) { + startExperiment(); + } + }) + .catch((error_message) => { + console.error(error_message); + }) + } + } + } + + }; + + // execute init() when the document is ready + if (document.readyState === "complete") { + init(); + } else { + window.addEventListener("load", init); + } + } + + core.progress = function() { + + var percent_complete = typeof timeline == 'undefined' ? 0 : timeline.percentComplete(); + + var obj = { + "total_trials": typeof timeline == 'undefined' ? undefined : timeline.length(), + "current_trial_global": global_trial_index, + "percent_complete": percent_complete + }; + + return obj; + }; + + core.startTime = function() { + return exp_start_time; + }; + + core.totalTime = function() { + if(typeof exp_start_time == 'undefined'){ return 0; } + return (new Date()).getTime() - exp_start_time.getTime(); + }; + + core.getDisplayElement = function() { + return DOM_target; + }; + + core.getDisplayContainerElement = function(){ + return DOM_container; + } + + core.finishTrial = function(data) { + + if(current_trial_finished){ return; } + current_trial_finished = true; + + // remove any CSS classes that were added to the DOM via css_classes parameter + if(typeof current_trial.css_classes !== 'undefined' && Array.isArray(current_trial.css_classes)){ + DOM_target.classList.remove(...current_trial.css_classes); + } + + // write the data from the trial + data = typeof data == 'undefined' ? {} : data; + jsPsych.data.write(data); + + // get back the data with all of the defaults in + var trial_data = jsPsych.data.get().filter({trial_index: global_trial_index}); + + // for trial-level callbacks, we just want to pass in a reference to the values + // of the DataCollection, for easy access and editing. + var trial_data_values = trial_data.values()[0]; + + if(typeof current_trial.save_trial_parameters == 'object'){ + var keys = Object.keys(current_trial.save_trial_parameters); + for(var i=0; i 0) { + setTimeout(nextTrial, opts.default_iti); + } else { + nextTrial(); + } + } else { + if (current_trial.post_trial_gap > 0) { + setTimeout(nextTrial, current_trial.post_trial_gap); + } else { + nextTrial(); + } + } + } + + core.endExperiment = function(end_message) { + timeline.end_message = end_message; + timeline.end(); + jsPsych.pluginAPI.cancelAllKeyboardResponses(); + jsPsych.pluginAPI.clearAllTimeouts(); + core.finishTrial(); + } + + core.endCurrentTimeline = function() { + timeline.endActiveNode(); + } + + core.currentTrial = function() { + return current_trial; + }; + + core.initSettings = function() { + return opts; + }; + + core.currentTimelineNodeID = function() { + return timeline.activeID(); + }; + + core.timelineVariable = function(varname, immediate){ + if(typeof immediate == 'undefined'){ immediate = false; } + if(jsPsych.internal.call_immediate || immediate === true){ + return timeline.timelineVariable(varname); + } else { + return function() { return timeline.timelineVariable(varname); } + } + } + + core.allTimelineVariables = function(){ + return timeline.allTimelineVariables(); + } + + core.addNodeToEndOfTimeline = function(new_timeline, preload_callback){ + timeline.insert(new_timeline); + } + + core.pauseExperiment = function(){ + paused = true; + } + + core.resumeExperiment = function(){ + paused = false; + if(waiting){ + waiting = false; + nextTrial(); + } + } + + core.loadFail = function(message){ + message = message || '

The experiment failed to load.

'; + loadfail = true; + DOM_target.innerHTML = message; + } + + core.getSafeModeStatus = function() { + return file_protocol; + } + + function TimelineNode(parameters, parent, relativeID) { + + // a unique ID for this node, relative to the parent + var relative_id; + + // store the parent for this node + var parent_node; + + // parameters for the trial if the node contains a trial + var trial_parameters; + + // parameters for nodes that contain timelines + var timeline_parameters; + + // stores trial information on a node that contains a timeline + // used for adding new trials + var node_trial_data; + + // track progress through the node + var progress = { + current_location: -1, // where on the timeline (which timelinenode) + current_variable_set: 0, // which set of variables to use from timeline_variables + current_repetition: 0, // how many times through the variable set on this run of the node + current_iteration: 0, // how many times this node has been revisited + done: false + } + + // reference to self + var self = this; + + // recursively get the next trial to run. + // if this node is a leaf (trial), then return the trial. + // otherwise, recursively find the next trial in the child timeline. + this.trial = function() { + if (typeof timeline_parameters == 'undefined') { + // returns a clone of the trial_parameters to + // protect functions. + return jsPsych.utils.deepCopy(trial_parameters); + } else { + if (progress.current_location >= timeline_parameters.timeline.length) { + return null; + } else { + return timeline_parameters.timeline[progress.current_location].trial(); + } + } + } + + this.markCurrentTrialComplete = function() { + if(typeof timeline_parameters == 'undefined'){ + progress.done = true; + } else { + timeline_parameters.timeline[progress.current_location].markCurrentTrialComplete(); + } + } + + this.nextRepetiton = function() { + this.setTimelineVariablesOrder(); + progress.current_location = -1; + progress.current_variable_set = 0; + progress.current_repetition++; + for (var i = 0; i < timeline_parameters.timeline.length; i++) { + timeline_parameters.timeline[i].reset(); + } + } + + // set the order for going through the timeline variables array + this.setTimelineVariablesOrder = function() { + + // check to make sure this node has variables + if(typeof timeline_parameters === 'undefined' || typeof timeline_parameters.timeline_variables === 'undefined'){ + return; + } + + var order = []; + for(var i=0; i 1, and only when on the first variable set + if (typeof timeline_parameters.conditional_function !== 'undefined' && progress.current_repetition == 0 && progress.current_variable_set == 0) { + jsPsych.internal.call_immediate = true; + var conditional_result = timeline_parameters.conditional_function(); + jsPsych.internal.call_immediate = false; + // if the conditional_function() returns false, then the timeline + // doesn't run and is marked as complete. + if (conditional_result == false) { + progress.done = true; + return true; + } + } + + // if we reach this point then the node has its own timeline and will start + // so we need to check if there is an on_timeline_start function if we are on the first variable set + if (typeof timeline_parameters.on_timeline_start !== 'undefined' && progress.current_variable_set == 0) { + timeline_parameters.on_timeline_start(); + } + + + } + // if we reach this point, then either the node doesn't have a timeline of the + // conditional function returned true and it can start + progress.current_location = 0; + // call advance again on this node now that it is pointing to a new location + return this.advance(); + } + + // if this node has a timeline, propogate down to the current trial. + if (typeof timeline_parameters !== 'undefined') { + + var have_node_to_run = false; + // keep incrementing the location in the timeline until one of the nodes reached is incomplete + while (progress.current_location < timeline_parameters.timeline.length && have_node_to_run == false) { + + // check to see if the node currently pointed at is done + var target_complete = timeline_parameters.timeline[progress.current_location].advance(); + if (!target_complete) { + have_node_to_run = true; + return false; + } else { + progress.current_location++; + } + + } + + // if we've reached the end of the timeline (which, if the code is here, we have) + + // there are a few steps to see what to do next... + + // first, check the timeline_variables to see if we need to loop through again + // with a new set of variables + if (progress.current_variable_set < progress.order.length - 1) { + // reset the progress of the node to be with the new set + this.nextSet(); + // then try to advance this node again. + return this.advance(); + } + + // if we're all done with the timeline_variables, then check to see if there are more repetitions + else if (progress.current_repetition < timeline_parameters.repetitions - 1) { + this.nextRepetiton(); + // check to see if there is an on_timeline_finish function + if (typeof timeline_parameters.on_timeline_finish !== 'undefined') { + timeline_parameters.on_timeline_finish(); + } + return this.advance(); + } + + + // if we're all done with the repetitions... + else { + // check to see if there is an on_timeline_finish function + if (typeof timeline_parameters.on_timeline_finish !== 'undefined') { + timeline_parameters.on_timeline_finish(); + } + + // if we're all done with the repetitions, check if there is a loop function. + if (typeof timeline_parameters.loop_function !== 'undefined') { + jsPsych.internal.call_immediate = true; + if (timeline_parameters.loop_function(this.generatedData())) { + this.reset(); + jsPsych.internal.call_immediate = false; + return parent_node.advance(); + } else { + progress.done = true; + jsPsych.internal.call_immediate = false; + return true; + } + } + + + } + + // no more loops on this timeline, we're done! + progress.done = true; + return true; + } + } + + // check the status of the done flag + this.isComplete = function() { + return progress.done; + } + + // getter method for timeline variables + this.getTimelineVariableValue = function(variable_name){ + if(typeof timeline_parameters == 'undefined'){ + return undefined; + } + var v = timeline_parameters.timeline_variables[progress.order[progress.current_variable_set]][variable_name]; + return v; + } + + // recursive upward search for timeline variables + this.findTimelineVariable = function(variable_name){ + var v = this.getTimelineVariableValue(variable_name); + if(typeof v == 'undefined'){ + if(typeof parent_node !== 'undefined'){ + return parent_node.findTimelineVariable(variable_name); + } else { + return undefined; + } + } else { + return v; + } + } + + // recursive downward search for active trial to extract timeline variable + this.timelineVariable = function(variable_name){ + if(typeof timeline_parameters == 'undefined'){ + return this.findTimelineVariable(variable_name); + } else { + // if progress.current_location is -1, then the timeline variable is being evaluated + // in a function that runs prior to the trial starting, so we should treat that trial + // as being the active trial for purposes of finding the value of the timeline variable + var loc = Math.max(0, progress.current_location); + // if loc is greater than the number of elements on this timeline, then the timeline + // variable is being evaluated in a function that runs after the trial on the timeline + // are complete but before advancing to the next (like a loop_function). + // treat the last active trial as the active trial for this purpose. + if(loc == timeline_parameters.timeline.length){ + loc = loc - 1; + } + // now find the variable + return timeline_parameters.timeline[loc].timelineVariable(variable_name); + } + } + + // recursively get all the timeline variables for this trial + this.allTimelineVariables = function(){ + var all_tvs = this.allTimelineVariablesNames(); + var all_tvs_vals = {}; + for(var i=0; i'+ + '

The minimum width is '+mw+'px. Your current width is '+w+'px.

'+ + '

The minimum height is '+mh+'px. Your current height is '+h+'px.

'; + core.getDisplayElement().innerHTML = msg; + } else { + clearInterval(interval); + core.getDisplayElement().innerHTML = ''; + checkExclusions(exclusions, success, fail); + } + }, 100); + return; // prevents checking other exclusions while this is being fixed + } + } + + // WEB AUDIO API + if(typeof exclusions.audio !== 'undefined' && exclusions.audio) { + if(window.hasOwnProperty('AudioContext') || window.hasOwnProperty('webkitAudioContext')){ + // clear + } else { + clear = false; + var msg = '

Your browser does not support the WebAudio API, which means that you will not '+ + 'be able to complete the experiment.

Browsers that support the WebAudio API include '+ + 'Chrome, Firefox, Safari, and Edge.

'; + core.getDisplayElement().innerHTML = msg; + fail(); + return; + } + } + + // GO? + if(clear){ success(); } + } + + function drawProgressBar(msg) { + document.querySelector('.jspsych-display-element').insertAdjacentHTML('afterbegin', + '
'+ + ''+ + msg+ + ''+ + '
'+ + '
'+ + '
'); + } + + function updateProgressBar() { + var progress = jsPsych.progress().percent_complete; + core.setProgressBar(progress / 100); + } + + var progress_bar_amount = 0; + + core.setProgressBar = function(proportion_complete){ + proportion_complete = Math.max(Math.min(1,proportion_complete),0); + document.querySelector('#jspsych-progressbar-inner').style.width = (proportion_complete*100) + "%"; + progress_bar_amount = proportion_complete; + } + + core.getProgressBarCompleted = function(){ + return progress_bar_amount; + } + + //Leave a trace in the DOM that jspsych was loaded + document.documentElement.setAttribute('jspsych', 'present'); + + return core; +})(); + +jsPsych.internal = (function() { + var module = {}; + + // this flag is used to determine whether we are in a scope where + // jsPsych.timelineVariable() should be executed immediately or + // whether it should return a function to access the variable later. + module.call_immediate = false; + + return module; +})(); + +jsPsych.plugins = (function() { + + var module = {}; + + // enumerate possible parameter types for plugins + module.parameterType = { + BOOL: 0, + STRING: 1, + INT: 2, + FLOAT: 3, + FUNCTION: 4, + KEY: 5, + SELECT: 6, + HTML_STRING: 7, + IMAGE: 8, + AUDIO: 9, + VIDEO: 10, + OBJECT: 11, + COMPLEX: 12, + TIMELINE: 13 + } + + module.universalPluginParameters = { + data: { + type: module.parameterType.OBJECT, + pretty_name: 'Data', + default: {}, + description: 'Data to add to this trial (key-value pairs)' + }, + on_start: { + type: module.parameterType.FUNCTION, + pretty_name: 'On start', + default: function() { return; }, + description: 'Function to execute when trial begins' + }, + on_finish: { + type: module.parameterType.FUNCTION, + pretty_name: 'On finish', + default: function() { return; }, + description: 'Function to execute when trial is finished' + }, + on_load: { + type: module.parameterType.FUNCTION, + pretty_name: 'On load', + default: function() { return; }, + description: 'Function to execute after the trial has loaded' + }, + post_trial_gap: { + type: module.parameterType.INT, + pretty_name: 'Post trial gap', + default: null, + description: 'Length of gap between the end of this trial and the start of the next trial' + }, + css_classes: { + type: module.parameterType.STRING, + pretty_name: 'Custom CSS classes', + default: null, + description: 'A list of CSS classes to add to the jsPsych display element for the duration of this trial' + } + } + + return module; +})(); + +jsPsych.extensions = (function(){ + return {}; +})(); + +jsPsych.data = (function() { + + var module = {}; + + // data storage object + var allData = DataCollection(); + + // browser interaction event data + var interactionData = DataCollection(); + + // data properties for all trials + var dataProperties = {}; + + // cache the query_string + var query_string; + + // DataCollection + function DataCollection(data){ + + var data_collection = {}; + + var trials = typeof data === 'undefined' ? [] : data; + + data_collection.push = function(new_data){ + trials.push(new_data); + return data_collection; + } + + data_collection.join = function(other_data_collection){ + trials = trials.concat(other_data_collection.values()); + return data_collection; + } + + data_collection.top = function(){ + if(trials.length <= 1){ + return data_collection; + } else { + return DataCollection([trials[trials.length-1]]); + } + } + + /** + * Queries the first n elements in a collection of trials. + * + * @param {number} n A positive integer of elements to return. A value of + * n that is less than 1 will throw an error. + * + * @return {Array} First n objects of a collection of trials. If fewer than + * n trials are available, the trials.length elements will + * be returned. + * + */ + data_collection.first = function(n){ + if (typeof n == 'undefined') { n = 1 } + if (n < 1) { + throw `You must query with a positive nonzero integer. Please use a + different value for n.`; + } + if (trials.length == 0) return DataCollection([]); + if (n > trials.length) n = trials.length; + return DataCollection(trials.slice(0, n)); + } + + /** + * Queries the last n elements in a collection of trials. + * + * @param {number} n A positive integer of elements to return. A value of + * n that is less than 1 will throw an error. + * + * @return {Array} Last n objects of a collection of trials. If fewer than + * n trials are available, the trials.length elements will + * be returned. + * + */ + data_collection.last = function(n) { + if (typeof n == 'undefined') { n = 1 } + if (n < 1) { + throw `You must query with a positive nonzero integer. Please use a + different value for n.`; + } + if (trials.length == 0) return DataCollection([]); + if (n > trials.length) n = trials.length; + return DataCollection(trials.slice(trials.length - n, trials.length)); + } + + data_collection.values = function(){ + return trials; + } + + data_collection.count = function(){ + return trials.length; + } + + data_collection.readOnly = function(){ + return DataCollection(jsPsych.utils.deepCopy(trials)); + } + + data_collection.addToAll = function(properties){ + for (var i = 0; i < trials.length; i++) { + for (var key in properties) { + trials[i][key] = properties[key]; + } + } + return data_collection; + } + + data_collection.addToLast = function(properties){ + if(trials.length != 0){ + for (var key in properties) { + trials[trials.length-1][key] = properties[key]; + } + } + return data_collection; + } + + data_collection.filter = function(filters){ + // [{p1: v1, p2:v2}, {p1:v2}] + // {p1: v1} + if(!Array.isArray(filters)){ + var f = jsPsych.utils.deepCopy([filters]); + } else { + var f = jsPsych.utils.deepCopy(filters); + } + + var filtered_data = []; + for(var x=0; x < trials.length; x++){ + var keep = false; + for(var i=0; i