mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 10:40:54 +00:00
version 3.1.4
This commit is contained in:
parent
9c4749d012
commit
d20a4cb0a5
@ -2,14 +2,13 @@
|
||||
* Manager handling the keyboard and mouse/touch events.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
import { MonotonicClock, Clock } from '../util/Clock';
|
||||
import { PsychoJS } from './PsychoJS';
|
||||
import * as util from '../util/Util';
|
||||
|
||||
|
||||
/**
|
||||
@ -27,7 +26,7 @@ export class EventManager {
|
||||
this._psychoJS = psychoJS;
|
||||
|
||||
// populate the reverse pyglet map:
|
||||
for (var keyName in EventManager._pygletMap)
|
||||
for (const keyName in EventManager._pygletMap)
|
||||
EventManager._reversePygletMap[EventManager._pygletMap[keyName]] = keyName;
|
||||
|
||||
// add key listeners:
|
||||
@ -63,14 +62,14 @@ export class EventManager {
|
||||
* @param {Object} options
|
||||
* @param {string[]} [options.keyList= null] - keyList allows the user to specify a set of keys to check for. Only keypresses from this set of keys will be removed from the keyboard buffer. If no keyList is given, all keys will be checked and the key buffer will be cleared completely.
|
||||
* @param {boolean} [options.timeStamped= false] - If true will return a list of tuples instead of a list of keynames. Each tuple has (keyname, time).
|
||||
* @return {Array.string} the list of keys that were pressed.
|
||||
* @return {string[]} the list of keys that were pressed.
|
||||
*/
|
||||
getKeys({
|
||||
keyList = null,
|
||||
timeStamped = false
|
||||
} = {}) {
|
||||
if (keyList != null)
|
||||
keyList = this._pyglet2w3c(keyList);
|
||||
keyList = EventManager.pyglet2w3c(keyList);
|
||||
|
||||
let newBuffer = [];
|
||||
let keys = [];
|
||||
@ -297,18 +296,20 @@ export class EventManager {
|
||||
* @private
|
||||
*/
|
||||
_addKeyListeners() {
|
||||
let self = this;
|
||||
const self = this;
|
||||
|
||||
// add a keydown listener:
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const timestamp = MonotonicClock.getReferenceTime();
|
||||
self._keyBuffer.push({
|
||||
code: e.code,
|
||||
key: e.key,
|
||||
keyCode: e.keyCode,
|
||||
timestamp: MonotonicClock.getReferenceTime() / 1000
|
||||
timestamp
|
||||
});
|
||||
self._psychoJS.logger.trace('keys pressed : ', util.toString(self._keyBuffer));
|
||||
self._psychoJS.logger.trace('keydown: ', e.key);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -317,13 +318,13 @@ export class EventManager {
|
||||
* Convert a keylist that uses pyglet key names to one that uses W3C KeyboardEvent.code values.
|
||||
* <p>This allows key lists that work in the builder environment to work in psychoJS web experiments.</p>
|
||||
*
|
||||
* @name module:core.EventManager#_pyglet2w3c
|
||||
* @name module:core.EventManager#pyglet2w3c
|
||||
* @function
|
||||
* @private
|
||||
* @param {Array.string} keyList - the array of pyglet key names
|
||||
* @public
|
||||
* @param {Array.string} pygletKeyList - the array of pyglet key names
|
||||
* @return {Array.string} the w3c keyList
|
||||
*/
|
||||
_pyglet2w3c(pygletKeyList) {
|
||||
static pyglet2w3c(pygletKeyList) {
|
||||
let w3cKeyList = [];
|
||||
for (let i = 0; i < pygletKeyList.length; i++) {
|
||||
if (typeof EventManager._pygletMap[pygletKeyList[i]] === 'undefined')
|
||||
@ -334,6 +335,23 @@ export class EventManager {
|
||||
|
||||
return w3cKeyList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert a W3C Key Code into a pyglet key.
|
||||
*
|
||||
* @name module:core.EventManager#w3c2pyglet
|
||||
* @function
|
||||
* @public
|
||||
* @param {string} code - W3C Key Code
|
||||
* @returns {string} corresponding pyglet key
|
||||
*/
|
||||
static w3c2pyglet(code) {
|
||||
if (code in EventManager._reversePygletMap)
|
||||
return EventManager._reversePygletMap[code];
|
||||
else
|
||||
return 'N/A';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -423,7 +441,8 @@ EventManager._keycodeMap = {
|
||||
|
||||
|
||||
/**
|
||||
* <p>This map associates pyglet key names to the corresponding W3C KeyboardEvent.codes.
|
||||
* This map associates pyglet key names to the corresponding W3C KeyboardEvent codes values.
|
||||
* <p>More information can be found [here]{@link https://www.w3.org/TR/uievents-code}</p>
|
||||
*
|
||||
* @name module:core.EventManager#_pygletMap
|
||||
* @readonly
|
||||
@ -431,7 +450,7 @@ EventManager._keycodeMap = {
|
||||
* @type {Object.<String,String>}
|
||||
*/
|
||||
EventManager._pygletMap = {
|
||||
// writing system keys
|
||||
// alphanumeric:
|
||||
"grave": "Backquote",
|
||||
"backslash": "Backslash",
|
||||
"backspace": "Backspace",
|
||||
@ -536,7 +555,6 @@ EventManager._reversePygletMap = {};
|
||||
|
||||
|
||||
/**
|
||||
* @class
|
||||
* Utility class used by the experiment scripts to keep track of a clock and of the current status (whether or not we are currently checking the keyboard)
|
||||
*
|
||||
* @name module:core.BuilderKeyResponse
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Graphic User Interface
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -145,11 +145,9 @@ export class GUI
|
||||
htmlCode += '</form>';
|
||||
|
||||
|
||||
// add a progress bar if the experiment is running on the server:
|
||||
if (this._psychoJS.config.environment === PsychoJS.Environment.SERVER) {
|
||||
htmlCode += '<hr><div id="progressMsg" class="progress">' + self._progressMsg + '</div>';
|
||||
htmlCode += '<div id="progressbar"></div></div>';
|
||||
}
|
||||
// add a progress bar:
|
||||
htmlCode += '<hr><div id="progressMsg" class="progress">' + self._progressMsg + '</div>';
|
||||
htmlCode += '<div id="progressbar"></div></div>';
|
||||
|
||||
|
||||
// replace root by the html code:
|
||||
@ -176,6 +174,7 @@ export class GUI
|
||||
autoOpen: true,
|
||||
modal: true,
|
||||
closeOnEscape: false,
|
||||
resizable: false,
|
||||
|
||||
buttons: [
|
||||
{
|
||||
@ -282,7 +281,7 @@ export class GUI
|
||||
this._psychoJS.logger.fatal(util.toString(error));
|
||||
|
||||
htmlCode = '<div id="msgDialog" title="Error">';
|
||||
htmlCode += '<p class="validateTips">Unfortunately we encountered an error:</p>';
|
||||
htmlCode += '<p class="validateTips">Unfortunately we encountered the following error:</p>';
|
||||
|
||||
// go through the error stack:
|
||||
htmlCode += '<ul>';
|
||||
|
382
js/core/Keyboard.js
Normal file
382
js/core/Keyboard.js
Normal file
@ -0,0 +1,382 @@
|
||||
/**
|
||||
* Manager handling the keyboard events.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
import {Clock, MonotonicClock} from "../util/Clock";
|
||||
import {PsychObject} from "../util/PsychObject";
|
||||
import {PsychoJS} from "./PsychoJS";
|
||||
import {EventManager} from "./EventManager";
|
||||
|
||||
|
||||
/**
|
||||
* @name module:core.KeyPress
|
||||
* @class
|
||||
*
|
||||
* @param {string} code - W3C Key Code
|
||||
* @param {number} tDown - time of key press (keydown event) relative to the global Monotonic Clock
|
||||
* @param {string | undefined} name - pyglet key name
|
||||
*/
|
||||
export class KeyPress {
|
||||
constructor(code, tDown, name) {
|
||||
this.code = code;
|
||||
this.tDown = tDown;
|
||||
this.name = (typeof name !== 'undefined') ? name : EventManager.w3c2pyglet(code);
|
||||
|
||||
// duration of the keypress (time between keydown and keyup events) or undefined if there was no keyup
|
||||
this.duration = undefined;
|
||||
|
||||
// time of keydown event relative to Keyboard clock:
|
||||
this.rt = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>This manager handles all keyboard events. It is a substitute for the keyboard component of EventManager. </p>
|
||||
*
|
||||
* @name module:core.Keyboard
|
||||
* @class
|
||||
* @param {Object} options
|
||||
* @param {PsychoJS} options.psychoJS - the PsychoJS instance
|
||||
* @param {number} [options.bufferSize= 10000] - the maximum size of the circular keyboard event buffer
|
||||
* @param {boolean} [options.waitForStart= false] - whether or not to wait for a call to module:core.Keyboard#start
|
||||
* before recording keyboard events
|
||||
* @param {Clock} [options.clock= undefined] - an optional clock
|
||||
* @param {boolean} options.autoLog - whether or not to log
|
||||
*/
|
||||
export class Keyboard extends PsychObject {
|
||||
|
||||
constructor({
|
||||
psychoJS,
|
||||
bufferSize = 10000,
|
||||
waitForStart = false,
|
||||
clock,
|
||||
autoLog = false,
|
||||
} = {}) {
|
||||
|
||||
super(psychoJS);
|
||||
|
||||
if (typeof clock === 'undefined')
|
||||
clock = this._psychoJS._monotonicClock;
|
||||
|
||||
this._addAttributes(Keyboard, bufferSize, waitForStart, clock, autoLog);
|
||||
|
||||
// setup circular buffer:
|
||||
this.clearEvents();
|
||||
|
||||
// add key listeners:
|
||||
this._addKeyListeners();
|
||||
|
||||
// start recording key events if need be:
|
||||
this._status = (waitForStart)?PsychoJS.Status.STOPPED:PsychoJS.Status.STARTED;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start recording keyboard events.
|
||||
*
|
||||
* @name module:core.Keyboard#start
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
*/
|
||||
start() {
|
||||
this._status = PsychoJS.Status.STARTED;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stop recording keyboard events.
|
||||
*
|
||||
* @name module:core.Keyboard#stop
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
*/
|
||||
stop() {
|
||||
this._status = PsychoJS.Status.STOPPED;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @typedef Keyboard.KeyEvent
|
||||
*
|
||||
* @property {string} W3C key code
|
||||
* @property {string} W3C key
|
||||
* @property {string} pyglet key
|
||||
* @property {module:core.Keyboard#KeyStatus} key status
|
||||
* @property {number} timestamp (in seconds)
|
||||
*/
|
||||
/**
|
||||
* Get the list of those keyboard events still in the buffer, i.e. those that have not been
|
||||
* previously cleared by calls to getKeys with clear = true.
|
||||
*
|
||||
* @name module:core.Keyboard#getEvents
|
||||
* @function
|
||||
* @public
|
||||
* @return {Keyboard.KeyEvent[]} the list of events still in the buffer
|
||||
*/
|
||||
getEvents() {
|
||||
if (this._bufferLength === 0)
|
||||
return [];
|
||||
|
||||
|
||||
// iterate over the buffer, from start to end, and discard the null event:
|
||||
let filteredEvents = [];
|
||||
const bufferWrap = (this._bufferLength === this._bufferSize);
|
||||
let i = bufferWrap ? this._bufferIndex : -1;
|
||||
do {
|
||||
i = (i + 1) % this._bufferSize;
|
||||
const keyEvent = this._circularBuffer[i];
|
||||
if (keyEvent)
|
||||
filteredEvents.push(keyEvent);
|
||||
} while (i !== this._bufferIndex);
|
||||
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the list of keys pressed or pushed by the participant.
|
||||
*
|
||||
* @name module:core.Keyboard#getKeys
|
||||
* @function
|
||||
* @public
|
||||
* @param {Object} options
|
||||
* @param {string[]} [options.keyList= []]] - the list of keys to consider. If keyList is empty, we consider all keys.
|
||||
* Note that we use pyglet keys here, to make the PsychoJs code more homogeneous with PsychoPy.
|
||||
* @param {boolean} [options.waitRelease= true] - whether or not to include those keys pressed but not released. If
|
||||
* waitRelease = false, key presses without a corresponding key release will have an undefined duration.
|
||||
* @param {boolean} [options.clear= false] - whether or not to keep in the buffer the key presses or pushes for a subsequent call to getKeys. If a keyList has been given and clear = true, we only remove from the buffer those keys in keyList
|
||||
* @return {KeyPress[]} the list of keys that were pressed (keydown followed by keyup) or pushed
|
||||
* (keydown with no subsequent keyup at the time getKeys is called).
|
||||
*/
|
||||
getKeys({
|
||||
keyList = [],
|
||||
waitRelease = true,
|
||||
clear = true
|
||||
} = {}) {
|
||||
|
||||
// if nothing in the buffer, return immediately:
|
||||
if (this._bufferLength === 0)
|
||||
return [];
|
||||
|
||||
let keyPresses = [];
|
||||
|
||||
// iterate over the circular buffer, looking for keyup events:
|
||||
const bufferWrap = (this._bufferLength === this._bufferSize);
|
||||
let i = bufferWrap ? this._bufferIndex : -1;
|
||||
do {
|
||||
i = (i + 1) % this._bufferSize;
|
||||
|
||||
const keyEvent = this._circularBuffer[i];
|
||||
if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP) {
|
||||
// check that the key is in the keyList:
|
||||
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
|
||||
// look for a corresponding, preceding keydown event:
|
||||
const precedingKeydownIndex = keyEvent.keydownIndex;
|
||||
if (typeof precedingKeydownIndex !== 'undefined') {
|
||||
const precedingKeydownEvent = this._circularBuffer[precedingKeydownIndex];
|
||||
if (precedingKeydownEvent) {
|
||||
// prepare KeyPress and add it to the array:
|
||||
const tDown = precedingKeydownEvent.timestamp;
|
||||
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
||||
keyPress.rt = tDown - this._clock.getLastResetTime();
|
||||
keyPress.duration = keyEvent.timestamp - precedingKeydownEvent.timestamp;
|
||||
keyPresses.push(keyPress);
|
||||
|
||||
if (clear)
|
||||
this._circularBuffer[precedingKeydownIndex] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* old approach: the circular buffer contains independent keydown and keyup events:
|
||||
let j = i - 1;
|
||||
do {
|
||||
if (j === -1 && bufferWrap)
|
||||
j = this._bufferSize - 1;
|
||||
|
||||
const precedingKeyEvent = this._circularBuffer[j];
|
||||
|
||||
if (precedingKeyEvent &&
|
||||
(precedingKeyEvent.key === keyEvent.key) &&
|
||||
(precedingKeyEvent.status === Keyboard.KeyStatus.KEY_DOWN)) {
|
||||
duration = keyEvent.timestamp - precedingKeyEvent.timestamp;
|
||||
|
||||
if (clear)
|
||||
// rather than modify the circular buffer, which is computationally expensive,
|
||||
// we simply nullify the keyEvent:
|
||||
this._circularBuffer[j] = null;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
j = j - 1;
|
||||
} while ((bufferWrap && j !== i) || (j > -1));*/
|
||||
|
||||
if (clear)
|
||||
this._circularBuffer[i] = null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} while (i !== this._bufferIndex);
|
||||
|
||||
|
||||
// if waitRelease = false, we iterate again over the map of unmatched keydown events:
|
||||
if (!waitRelease) {
|
||||
for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values()) {
|
||||
const keyEvent = this._circularBuffer[unmatchedKeyDownIndex];
|
||||
if (keyEvent) {
|
||||
// check that the key is in the keyList:
|
||||
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
|
||||
const tDown = keyEvent.timestamp;
|
||||
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
||||
keyPress.rt = tDown - this._clock.getLastResetTime();
|
||||
keyPresses.push(keyPress);
|
||||
|
||||
if (clear) {
|
||||
this._unmatchedKeydownMap.delete(keyEvent.code);
|
||||
this._circularBuffer[unmatchedKeyDownIndex] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* old approach: the circular buffer contains independent keydown and keyup events:
|
||||
let i = bufferWrap ? this._bufferIndex : -1;
|
||||
do {
|
||||
i = (i + 1) % this._bufferSize;
|
||||
|
||||
const keyEvent = this._circularBuffer[i];
|
||||
if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_DOWN) {
|
||||
// check that the key is in the keyList:
|
||||
const pigletKey = EventManager.w3c2pyglet(keyEvent.code);
|
||||
if (keyList.length === 0 || keyList.includes(pigletKey)) {
|
||||
keyPresses.push(new KeyPress(keyEvent.code, keyEvent.timestamp, pigletKey));
|
||||
|
||||
if (clear)
|
||||
// rather than modify the circular buffer, which is computationally expensive, we simply nullify
|
||||
// the keyEvent:
|
||||
this._circularBuffer[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
} while (i !== this._bufferIndex);*/
|
||||
}
|
||||
|
||||
|
||||
// if clear = true and the keyList is empty, we clear all the events:
|
||||
if (clear && keyList.length === 0)
|
||||
this.clearEvents();
|
||||
|
||||
|
||||
return keyPresses;
|
||||
}
|
||||
|
||||
|
||||
clearEvents() {
|
||||
// circular buffer of key events (keydown and keyup):
|
||||
this._circularBuffer = new Array(this._bufferSize);
|
||||
this._bufferLength = 0;
|
||||
this._bufferIndex = -1;
|
||||
|
||||
// (code => circular buffer index) map of keydown events not yet matched to keyup events:
|
||||
this._unmatchedKeydownMap = new Map();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add key listeners to the document.
|
||||
*
|
||||
* @name module:core.Keyboard#_addKeyListeners
|
||||
* @function
|
||||
* @private
|
||||
*/
|
||||
_addKeyListeners() {
|
||||
this._previousKeydownKey = undefined;
|
||||
const self = this;
|
||||
|
||||
|
||||
// add a keydown listener:
|
||||
document.addEventListener("keydown", (event) => {
|
||||
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
||||
|
||||
if (this._status !== PsychoJS.Status.STARTED)
|
||||
return;
|
||||
|
||||
// since keydown events will repeat as long as the key is pressed, we need to track the last pressed key:
|
||||
if (event.key === self._previousKeydownKey)
|
||||
return;
|
||||
self._previousKeydownKey = event.key;
|
||||
|
||||
self._bufferIndex = (self._bufferIndex + 1) % self._bufferSize;
|
||||
self._bufferLength = Math.min(self._bufferLength + 1, self._bufferSize);
|
||||
self._circularBuffer[self._bufferIndex] = {
|
||||
code: event.code,
|
||||
key: event.key,
|
||||
pigletKey: EventManager.w3c2pyglet(event.code),
|
||||
status: Keyboard.KeyStatus.KEY_DOWN,
|
||||
timestamp
|
||||
};
|
||||
|
||||
self._unmatchedKeydownMap.set(event.code, self._bufferIndex);
|
||||
|
||||
self._psychoJS.logger.trace('keydown: ', event.key);
|
||||
console.log(self._circularBuffer[self._bufferIndex]);
|
||||
});
|
||||
|
||||
|
||||
// add a keyup listener:
|
||||
document.addEventListener("keyup", (event) => {
|
||||
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
||||
|
||||
if (this._status !== PsychoJS.Status.STARTED)
|
||||
return;
|
||||
|
||||
self._previousKeydownKey = undefined;
|
||||
|
||||
|
||||
self._bufferIndex = (self._bufferIndex + 1) % self._bufferSize;
|
||||
self._bufferLength = Math.min(self._bufferLength + 1, self._bufferSize);
|
||||
self._circularBuffer[self._bufferIndex] = {
|
||||
code: event.code,
|
||||
key: event.key,
|
||||
pigletKey: EventManager.w3c2pyglet(event.code),
|
||||
status: Keyboard.KeyStatus.KEY_UP,
|
||||
timestamp
|
||||
};
|
||||
|
||||
// get the corresponding keydown event
|
||||
// note: if more keys are down than there are slots in the circular buffer, there might
|
||||
// not be a corresponding keydown event
|
||||
const correspondingKeydownIndex = self._unmatchedKeydownMap.get(event.code);
|
||||
if (typeof correspondingKeydownIndex !== 'undefined') {
|
||||
self._circularBuffer[self._bufferIndex].keydownIndex = correspondingKeydownIndex;
|
||||
self._unmatchedKeydownMap.delete(event.code);
|
||||
}
|
||||
|
||||
self._psychoJS.logger.trace('keyup: ', event.key);
|
||||
console.log(self._circularBuffer[self._bufferIndex]);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Keyboard KeyStatus.
|
||||
*
|
||||
* @name module:core.Keyboard#KeyStatus
|
||||
* @enum {Symbol}
|
||||
* @readonly
|
||||
* @public
|
||||
*/
|
||||
Keyboard.KeyStatus = {
|
||||
KEY_DOWN: Symbol.for('KEY_DOWN'),
|
||||
KEY_UP: Symbol.for('KEY_UP')
|
||||
};
|
@ -2,7 +2,7 @@
|
||||
* Base class for all stimuli.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -53,7 +53,7 @@ export class MinimalStim extends PsychObject {
|
||||
* @param {boolean} [log= false] - whether or not to log
|
||||
*/
|
||||
setAutoDraw(autoDraw, log = false) {
|
||||
let errorPrefix = { origin : 'MinimalStim.setAutoDraw', context: 'when setting the autoDraw attribute of stimulus: ' + this._name };
|
||||
let response = { origin : 'MinimalStim.setAutoDraw', context: 'when setting the autoDraw attribute of stimulus: ' + this._name };
|
||||
|
||||
this._setAttribute('autoDraw', autoDraw, log);
|
||||
|
||||
@ -67,7 +67,8 @@ export class MinimalStim extends PsychObject {
|
||||
// update the stimulus if need be before we add its PIXI representation to the window container:
|
||||
this._updateIfNeeded();
|
||||
if (typeof this._pixi === 'undefined')
|
||||
throw {...errorPrefix, error: 'the PIXI representation of the stimulus is unavailable'};
|
||||
// throw {...errorPrefix, error: 'the PIXI representation of the stimulus is unavailable'};
|
||||
throw Object.assign(response, { error: 'the PIXI representation of the stimulus is unavailable'});
|
||||
|
||||
this.win._rootContainer.addChild(this._pixi);
|
||||
this.win._drawList.push(this);
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Manager responsible for the interactions between the experiment's stimuli and the mouse.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -3,7 +3,7 @@
|
||||
* Main component of the PsychoJS library.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -144,6 +144,7 @@ export class PsychoJS {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the completion and cancellation URL to which the participant will be redirect at the end of the experiment.
|
||||
*
|
||||
@ -155,6 +156,7 @@ export class PsychoJS {
|
||||
this._cancellationUrl = cancellationUrl;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Schedule a task.
|
||||
*
|
||||
@ -195,10 +197,13 @@ export class PsychoJS {
|
||||
* @param {string} [options.configURL=config.json] - the URL of the configuration file
|
||||
* @param {string} [options.expName=UNKNOWN] - the name of the experiment
|
||||
* @param {Object.<string, *>} [options.expInfo] - additional information about the experiment
|
||||
* @param {Array.<{name: string, path: string}>} [resources=[]] - the list of resources
|
||||
* @async
|
||||
* @public
|
||||
*
|
||||
* @todo: close session on window or tab close
|
||||
*/
|
||||
async start({ configURL = 'config.json', expName = 'UNKNOWN', expInfo } = {}) {
|
||||
async start({ configURL = 'config.json', expName = 'UNKNOWN', expInfo, resources = [] } = {}) {
|
||||
this.logger.debug();
|
||||
|
||||
const response = { origin: 'PsychoJS.start', context: 'when starting the experiment' };
|
||||
@ -208,9 +213,9 @@ export class PsychoJS {
|
||||
await this._configure(configURL, expName);
|
||||
|
||||
// get the participant IP:
|
||||
if (this._collectIP) {
|
||||
if (this._collectIP)
|
||||
this._getParticipantIPInfo();
|
||||
} else {
|
||||
else {
|
||||
this._IP = {
|
||||
IP: 'X',
|
||||
hostname : 'X',
|
||||
@ -227,28 +232,34 @@ export class PsychoJS {
|
||||
extraInfo: expInfo
|
||||
});
|
||||
|
||||
// get potential information from the server:
|
||||
this._serverMsg = util.getUrlParameters().get('__msg');
|
||||
|
||||
// setup the logger:
|
||||
//my.logger.console.setLevel(psychoJS.logging.WARNING);
|
||||
//my.logger.server.set({'level':psychoJS.logging.WARNING, 'experimentInfo': my.expInfo});
|
||||
|
||||
// if the experiment is running on the server, we open a session and download the resources:
|
||||
if (this.getEnvironment() === PsychoJS.Environment.SERVER) {
|
||||
// open a new session:
|
||||
// if the experiment is running on the server, we open a session:
|
||||
if (this.getEnvironment() === PsychoJS.Environment.SERVER)
|
||||
await this._serverManager.openSession();
|
||||
|
||||
// start the asynchronous download of resources:
|
||||
this._serverManager.downloadResources();
|
||||
}
|
||||
|
||||
// attempt to close the session on onunload:
|
||||
const self = this;
|
||||
window.addEventListener("unload",() => {
|
||||
// we need to use either the Beacon API (https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon)
|
||||
// or synchronous requests
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
// start the asynchronous download of resources:
|
||||
this._serverManager.downloadResources(resources);
|
||||
|
||||
// start the experiment:
|
||||
this.logger.info('[PsychoJS] Start Experiment.');
|
||||
this._scheduler.start();
|
||||
}
|
||||
catch (error) {
|
||||
this._gui.dialog({ error: { ...response, error } });
|
||||
// this._gui.dialog({ error: { ...response, error } });
|
||||
this._gui.dialog({ error: Object.assign(response, { error }) });
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,8 +271,8 @@ export class PsychoJS {
|
||||
* <li>For an experiment running locally: the root directory for the specified resources is that of index.html
|
||||
* unless they are prepended with a protocol, such as http:// or https://.</li>
|
||||
* <li>For an experiment running on the server: if no resources are specified, all files in the resources directory
|
||||
* of the experiment are downloaded, otherwise we only download the specified resources.</li>
|
||||
* </ul>
|
||||
* of the experiment are downloaded, otherwise we only download the specified resources. All resources are assumed
|
||||
* local to index.html unless they are prepended with a protocol.</li>
|
||||
*
|
||||
* @param {Array.<{name: string, path: string}>} [resources=[]] - the list of resources
|
||||
* @async
|
||||
@ -272,7 +283,8 @@ export class PsychoJS {
|
||||
await this.serverManager.downloadResources(resources);
|
||||
}
|
||||
catch (error) {
|
||||
this._gui.dialog({ error: { ...response, error } });
|
||||
// this._gui.dialog({ error: { ...response, error } });
|
||||
this._gui.dialog({ error: Object.assign(response, { error }) });
|
||||
}
|
||||
}
|
||||
|
||||
@ -384,6 +396,15 @@ export class PsychoJS {
|
||||
const serverResponse = await this._serverManager.getConfiguration(configURL);
|
||||
this._config = serverResponse.config;
|
||||
|
||||
// legacy experiments had a psychoJsManager block instead of a pavlovia block, and the URL
|
||||
// pointed to https://pavlovia.org/server
|
||||
if ('psychoJsManager' in this._config) {
|
||||
delete this._config.psychoJsManager;
|
||||
this._config.pavlovia = {
|
||||
URL: 'https://pavlovia.org'
|
||||
};
|
||||
}
|
||||
|
||||
// tests for the presence of essential blocks in the configuration:
|
||||
if (!('experiment' in this._config))
|
||||
throw 'missing experiment block in configuration';
|
||||
@ -391,16 +412,10 @@ export class PsychoJS {
|
||||
throw 'missing name in experiment block in configuration';
|
||||
if (!('fullpath' in this._config.experiment))
|
||||
throw 'missing fullpath in experiment block in configuration';
|
||||
if (!('psychoJsManager' in this._config))
|
||||
throw 'missing psychoJsManager block in configuration';
|
||||
if (!('URL' in this._config.psychoJsManager))
|
||||
throw 'missing URL in psychoJsManager block in configuration';
|
||||
|
||||
// 'CSV' is the default format for the experiment results:
|
||||
if ('saveFormat' in this._config.experiment)
|
||||
this._config.experiment.saveFormat = Symbol.for(this._config.experiment.saveFormat);
|
||||
else
|
||||
this._config.experiment.saveFormat = ExperimentHandler.SaveFormat.CSV;
|
||||
if (!('pavlovia' in this._config))
|
||||
throw 'missing pavlovia block in configuration';
|
||||
if (!('URL' in this._config.pavlovia))
|
||||
throw 'missing URL in pavlovia block in configuration';
|
||||
|
||||
this._config.environment = PsychoJS.Environment.SERVER;
|
||||
|
||||
@ -413,11 +428,20 @@ export class PsychoJS {
|
||||
};
|
||||
}
|
||||
|
||||
// get the server parameters (those starting with a double underscore):
|
||||
this._serverMsg = new Map();
|
||||
util.getUrlParameters().forEach((value, key) => {
|
||||
if (key.indexOf('__') === 0)
|
||||
this._serverMsg.set(key, value);
|
||||
});
|
||||
|
||||
|
||||
this.status = PsychoJS.Status.CONFIGURED;
|
||||
this.logger.debug('configuration:', util.toString(this._config));
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
// throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -446,7 +470,8 @@ export class PsychoJS {
|
||||
this.logger.debug('IP information of the participant: ' + util.toString(this._IP));
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
// throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Manager responsible for the communication between the experiment running in the participant's browser and the remote PsychoJS manager running on the remote https://pavlovia.org server.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -11,6 +11,7 @@
|
||||
import { PsychoJS } from './PsychoJS';
|
||||
import { PsychObject } from '../util/PsychObject';
|
||||
import * as util from '../util/Util';
|
||||
import {ExperimentHandler} from "../data/ExperimentHandler";
|
||||
// import { Howl } from 'howler';
|
||||
|
||||
|
||||
@ -71,10 +72,12 @@ export class ServerManager extends PsychObject {
|
||||
return new Promise((resolve, reject) => {
|
||||
$.get(configURL, 'json')
|
||||
.done((config, textStatus) => {
|
||||
resolve({ ...response, config });
|
||||
// resolve({ ...response, config });
|
||||
resolve(Object.assign(response, { config }));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
reject({ ...response, error: errorThrown });
|
||||
// reject({ ...response, error: errorThrown });
|
||||
reject(Object.assign(response, { error: errorThrown }));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -96,45 +99,49 @@ export class ServerManager extends PsychObject {
|
||||
* @returns {Promise<ServerManager.OpenSessionPromise>} the response
|
||||
*/
|
||||
openSession() {
|
||||
const response = { origin: 'ServerManager.openSession', context: 'when opening a session for experiment: ' + this._psychoJS.config.experiment.name };
|
||||
const response = { origin: 'ServerManager.openSession', context: 'when opening a session for experiment: ' + this._psychoJS.config.experiment.fullpath };
|
||||
|
||||
this._psychoJS.logger.debug('opening a session for experiment: ' + this._psychoJS.config.experiment.name);
|
||||
this._psychoJS.logger.debug('opening a session for experiment: ' + this._psychoJS.config.experiment.fullpath);
|
||||
|
||||
this.setStatus(ServerManager.Status.BUSY);
|
||||
|
||||
let data = {
|
||||
experimentFullPath: this._psychoJS.config.experiment.fullpath
|
||||
};
|
||||
const gitlabConfig = this._psychoJS.config.gitlab;
|
||||
if (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined')
|
||||
data.projectId = gitlabConfig.projectId;
|
||||
// prepare POST query:
|
||||
let data = {};
|
||||
if (this._psychoJS._serverMsg.has('__pilotToken'))
|
||||
data.pilotToken = this._psychoJS._serverMsg.get('__pilotToken');
|
||||
|
||||
let self = this;
|
||||
// query pavlovia server:
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
$.post(this._psychoJS.config.psychoJsManager.URL + '?command=open_session', data, null, 'json')
|
||||
.done((data, textStatus) => {
|
||||
// check for error:
|
||||
if ('error' in data) {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: data.error });
|
||||
}
|
||||
|
||||
// get session token:
|
||||
if ('token' in data) {
|
||||
self._psychoJS.config.experiment.token = data.token;
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
resolve({ ...response, token: data.token });
|
||||
}
|
||||
else {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: 'unexpected answer from server: no token' });
|
||||
}
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
$.post(this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) + '/sessions', data, null, 'json')
|
||||
.done((data, textStatus) => {
|
||||
// check for required attributes:
|
||||
if (!('token' in data)) {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: 'request error: ' + textStatus });
|
||||
});
|
||||
reject(Object.assign(response, { error: 'unexpected answer from server: no token'}));
|
||||
// reject({...response, error: 'unexpected answer from server: no token'});
|
||||
}
|
||||
if (!('experiment' in data)) {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
// reject({...response, error: 'unexpected answer from server: no experiment'});
|
||||
reject(Object.assign(response, { error: 'unexpected answer from server: no experiment'}));
|
||||
}
|
||||
|
||||
// update the configuration:
|
||||
self._psychoJS.config.experiment.status = data.experiment.status2;
|
||||
self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
|
||||
self._psychoJS.config.session = { token: data.token };
|
||||
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
// resolve({ ...response, token: data.token, status: data.status });
|
||||
resolve(Object.assign(response, { token: data.token, status: data.status }));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
console.log('error:', jqXHR.responseText);
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
// reject({ ...response, error: jqXHR.responseJSON });
|
||||
reject(Object.assign(response, { error: jqXHR.responseJSON }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -155,39 +162,36 @@ export class ServerManager extends PsychObject {
|
||||
* @returns {Promise<ServerManager.CloseSessionPromise>} the response
|
||||
*/
|
||||
closeSession(isCompleted = false) {
|
||||
const response = { origin: 'ServerManager.closeSession', context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.name };
|
||||
const response = { origin: 'ServerManager.closeSession', context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath };
|
||||
|
||||
this._psychoJS.logger.debug('closing the session for experiment: ' + this._psychoJS.config.experiment.name);
|
||||
|
||||
this.setStatus(ServerManager.Status.BUSY);
|
||||
|
||||
let data = {
|
||||
experimentFullPath: this._psychoJS.config.experiment.fullpath,
|
||||
'token': this._psychoJS.config.experiment.token,
|
||||
'isCompleted': isCompleted
|
||||
};
|
||||
const gitlabConfig = this._psychoJS.config.gitlab;
|
||||
if (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined')
|
||||
data.projectId = gitlabConfig.projectId;
|
||||
// prepare DELETE query:
|
||||
const data = { isCompleted };
|
||||
|
||||
|
||||
let self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
$.post(this._psychoJS.config.psychoJsManager.URL + '?command=close_session', data, null, 'json')
|
||||
.done((data, textStatus) => {
|
||||
// check for error:
|
||||
if ('error' in data) {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: data.error });
|
||||
}
|
||||
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
resolve({ ...response, data });
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: errorThrown });
|
||||
});
|
||||
// query pavlovia server:
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) + '/sessions/' + self._psychoJS.config.session.token;
|
||||
$.ajax({
|
||||
url,
|
||||
type: 'delete',
|
||||
data,
|
||||
dataType: 'json'
|
||||
})
|
||||
.done((data, textStatus) => {
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
// resolve({ ...response, data });
|
||||
resolve(Object.assign(response, { data }));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
console.log('error:', jqXHR.responseText);
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
// reject({ ...response, error: jqXHR.responseJSON });
|
||||
reject(Object.assign(response, { error: jqXHR.responseJSON }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -207,7 +211,8 @@ export class ServerManager extends PsychObject {
|
||||
|
||||
const path_data = this._resources.get(name);
|
||||
if (typeof path_data === 'undefined')
|
||||
throw { ...response, error: 'unknown resource' };
|
||||
// throw { ...response, error: 'unknown resource' };
|
||||
throw Object.assign(response, { error: 'unknown resource' });
|
||||
|
||||
return path_data.data;
|
||||
}
|
||||
@ -226,9 +231,11 @@ export class ServerManager extends PsychObject {
|
||||
// check status:
|
||||
const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
|
||||
if (!statusKey)
|
||||
throw { ...response, error: 'status must be a symbol' };
|
||||
// throw { ...response, error: 'status must be a symbol' };
|
||||
throw Object.assign(response, { error: 'status must be a symbol' });
|
||||
if (!ServerManager.Status.hasOwnProperty(statusKey))
|
||||
throw { ...response, error: 'unknown status' };
|
||||
// throw { ...response, error: 'unknown status' };
|
||||
throw Object.assign(response, { error: 'unknown status' });
|
||||
|
||||
this._status = status;
|
||||
|
||||
@ -259,7 +266,8 @@ export class ServerManager extends PsychObject {
|
||||
* <li>For an experiment running locally: the root directory for the specified resources is that of index.html
|
||||
* unless they are prepended with a protocol, such as http:// or https://.</li>
|
||||
* <li>For an experiment running on the server: if no resources are specified, all files in the resources directory
|
||||
* of the experiment are downloaded, otherwise we only download the specified resources.</li>
|
||||
* of the experiment are downloaded, otherwise we only download the specified resources. All resources are assumed
|
||||
* local to index.html unless they are prepended with a protocol.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @name module:core.ServerManager#downloadResources
|
||||
@ -278,19 +286,24 @@ export class ServerManager extends PsychObject {
|
||||
let download = async () => {
|
||||
try {
|
||||
if (self._psychoJS.config.environment === PsychoJS.Environment.SERVER) {
|
||||
// list the resources from the resources directory of the experiment on the server:
|
||||
const serverResponse = await self._listResources();
|
||||
|
||||
if (resources.length === 0)
|
||||
// no resources specified, we register them all:
|
||||
if (resources.length === 0) {
|
||||
// list the resources from the resources directory of the experiment on the server:
|
||||
const serverResponse = await self._listResources();
|
||||
for (const name of serverResponse.resources)
|
||||
self._resources.set(name, {path: serverResponse.resourceDirectory + '/' + name});
|
||||
else
|
||||
self._resources.set(name, { path: serverResponse.resourceDirectory + '/' + name });
|
||||
}
|
||||
else {
|
||||
// only registered the specified resources:
|
||||
for (const {name, path} of resources)
|
||||
self._resources.set(name, {path: serverResponse.resourceDirectory + '/' + path });
|
||||
self._resources.set(name, { path });
|
||||
}
|
||||
} else {
|
||||
// register the specified resources:
|
||||
for (const {name, path} of resources)
|
||||
self._resources.set(name, { path });
|
||||
}
|
||||
|
||||
self._nbResources = self._resources.size;
|
||||
for (const name of self._resources.keys())
|
||||
this._psychoJS.logger.debug('resource:', name, self._resources.get(name).path);
|
||||
@ -302,7 +315,8 @@ export class ServerManager extends PsychObject {
|
||||
}
|
||||
catch (error) {
|
||||
console.log('error', error);
|
||||
throw { ...response, error: error };
|
||||
// throw { ...response, error: error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
};
|
||||
|
||||
@ -328,42 +342,33 @@ export class ServerManager extends PsychObject {
|
||||
* @returns {Promise<ServerManager.UploadDataPromise>} the response
|
||||
*/
|
||||
uploadData(key, value) {
|
||||
const response = { origin: 'ServerManager.uploadData', context: 'when uploading participant\'s results for experiment: ' + this._psychoJS.config.experiment.name };
|
||||
const response = { origin: 'ServerManager.uploadData', context: 'when uploading participant\'s results for experiment: ' + this._psychoJS.config.experiment.fullpath };
|
||||
|
||||
this._psychoJS.logger.debug('uploading data for experiment: ' + this._psychoJS.config.experiment.name);
|
||||
this._psychoJS.logger.debug('uploading data for experiment: ' + this._psychoJS.config.experiment.fullpath);
|
||||
this.setStatus(ServerManager.Status.BUSY);
|
||||
|
||||
let data = {
|
||||
experimentFullPath: this._psychoJS.config.experiment.fullpath,
|
||||
token: this._psychoJS.config.experiment.token,
|
||||
// prepare the POST query:
|
||||
const data = {
|
||||
key,
|
||||
value,
|
||||
saveFormat: Symbol.keyFor(this._psychoJS.config.experiment.saveFormat)
|
||||
value
|
||||
};
|
||||
// add gitlab ID of experiment if there is one:
|
||||
const gitlabConfig = this._psychoJS.config.gitlab;
|
||||
if (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined')
|
||||
data.projectId = gitlabConfig.projectId;
|
||||
|
||||
// (*) upload data:
|
||||
// query the pavlovia server:
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
$.post(this._psychoJS.config.psychoJsManager.URL + '?command=save_data', data, null, 'json')
|
||||
.done((data, textStatus) => {
|
||||
// check for error:
|
||||
if ('error' in data) {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: data.error });
|
||||
}
|
||||
|
||||
// return the response from the PsychoJS manager:
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
resolve({ ...response, data });
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: errorThrown });
|
||||
});
|
||||
const url = self._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) + '/sessions/' + self._psychoJS.config.session.token + '/results';
|
||||
$.post(url, data, null, 'json')
|
||||
.done((serverData, textStatus) => {
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
// resolve({ ...response, serverData });
|
||||
resolve(Object.assign(response, { serverData }));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
console.log('error:', jqXHR.responseText);
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
// reject({ ...response, error: jqXHR.responseJSON });
|
||||
reject(Object.assign(response, { error: jqXHR.responseJSON }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -376,41 +381,46 @@ export class ServerManager extends PsychObject {
|
||||
* @private
|
||||
*/
|
||||
_listResources() {
|
||||
const response = { origin: 'ServerManager._listResourcesSession', context: 'when listing the resources for experiment: ' + this._psychoJS.config.experiment.name };
|
||||
const response = { origin: 'ServerManager._listResourcesSession', context: 'when listing the resources for experiment: ' + this._psychoJS.config.experiment.fullpath };
|
||||
|
||||
this._psychoJS.logger.debug('listing the resources for experiment: ' + this._psychoJS.config.experiment.name);
|
||||
this._psychoJS.logger.debug('listing the resources for experiment: ' + this._psychoJS.config.experiment.fullpath);
|
||||
|
||||
this.setStatus(ServerManager.Status.BUSY);
|
||||
|
||||
// prepare GET data:
|
||||
const data = {
|
||||
'token': this._psychoJS.config.session.token
|
||||
};
|
||||
|
||||
// query pavlovia server:
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
$.get(self._psychoJS.config.psychoJsManager.URL, {
|
||||
'command': 'list_resources',
|
||||
'experimentFullPath': self._psychoJS.config.experiment.fullpath,
|
||||
'token': self._psychoJS.config.experiment.token
|
||||
}, null, 'json')
|
||||
$.get(this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) + '/resources', data, null, 'json')
|
||||
.done((data, textStatus) => {
|
||||
// check for error:
|
||||
if ('error' in data)
|
||||
reject({ ...response, error: data.error });
|
||||
|
||||
if (!('resources' in data)) {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: 'unexpected answer from server: no resources' });
|
||||
// reject({ ...response, error: 'unexpected answer from server: no resources' });
|
||||
reject(Object.assign(response, { error: 'unexpected answer from server: no resources' }));
|
||||
}
|
||||
if (!('resourceDirectory' in data)) {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
|
||||
// reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
|
||||
reject(Object.assign(response, { error: 'unexpected answer from server: no resourceDirectory' }));
|
||||
}
|
||||
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
|
||||
// resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
|
||||
resolve(Object.assign(response, { resources: data.resources, resourceDirectory: data.resourceDirectory }));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
console.log('error:', jqXHR.responseText);
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject({ ...response, error: errorThrown });
|
||||
// reject({ ...response, error: jqXHR.responseJSON });
|
||||
reject(Object.assign(response, { error: jqXHR.responseJSON }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -460,7 +470,8 @@ export class ServerManager extends PsychObject {
|
||||
this._resourceQueue.addEventListener("error", event => {
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
const resourceId = (typeof event.data !== 'undefined')?event.data.id:'UNKNOWN RESOURCE';
|
||||
throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
|
||||
// throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
|
||||
throw Object.assign(response, { error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' });
|
||||
});
|
||||
|
||||
|
||||
@ -515,7 +526,7 @@ export class ServerManager extends PsychObject {
|
||||
++self._nbLoadedResources;
|
||||
path_data.data = howl;
|
||||
// self._resources.set(resource.name, howl);
|
||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCE_DOWNLOADED, resource: resource.name });
|
||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCE_DOWNLOADED, resource: name });
|
||||
|
||||
if (self._nbLoadedResources === self._nbResources) {
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
@ -523,7 +534,8 @@ export class ServerManager extends PsychObject {
|
||||
}
|
||||
});
|
||||
howl.on('loaderror', (id, error) => {
|
||||
throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
|
||||
// throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
|
||||
throw Object.assign(response, { error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' });
|
||||
});
|
||||
|
||||
howl.load();
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Window responsible for displaying the experiment stimuli
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Mixin implementing various unit-handling measurement methods.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -49,7 +49,7 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @return {number} - the length in pixel units
|
||||
*/
|
||||
_getLengthPix(length) {
|
||||
let errorPrefix = { origin: 'WindowMixin._getLengthPix', context: 'when converting a length from stimulus unit to pixel units' };
|
||||
let response = { origin: 'WindowMixin._getLengthPix', context: 'when converting a length from stimulus unit to pixel units' };
|
||||
|
||||
if (this._units === 'pix') {
|
||||
return length;
|
||||
@ -63,7 +63,8 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
return length * minSize;
|
||||
}
|
||||
else {
|
||||
throw { ...errorPrefix, error: 'unable to deal with unit: ' + this._units };
|
||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +79,7 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getLengthUnits(length_px) {
|
||||
let errorPrefix = { origin: 'WindowMixin._getLengthUnits', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
let response = { origin: 'WindowMixin._getLengthUnits', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
|
||||
if (this._units === 'pix') {
|
||||
return length_px;
|
||||
@ -92,7 +93,8 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
return length_px / minSize;
|
||||
}
|
||||
else {
|
||||
throw { ...errorPrefix, error: 'unable to deal with unit: ' + this._units };
|
||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +109,7 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getHorLengthPix(length) {
|
||||
let errorPrefix = { origin: 'WindowMixin._getHorLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
let response = { origin: 'WindowMixin._getHorLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
|
||||
if (this._units === 'pix') {
|
||||
return length;
|
||||
@ -121,7 +123,8 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
return length * minSize;
|
||||
}
|
||||
else {
|
||||
throw { ...errorPrefix, error: 'unable to deal with unit: ' + this._units };
|
||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +138,7 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getVerLengthPix(length) {
|
||||
let errorPrefix = { origin: 'WindowMixin._getVerLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
let response = { origin: 'WindowMixin._getVerLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
|
||||
if (this._units === 'pix') {
|
||||
return length;
|
||||
@ -149,7 +152,8 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
return length * minSize;
|
||||
}
|
||||
else {
|
||||
throw { ...errorPrefix, error: 'unable to deal with unit: ' + this._units };
|
||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Experiment Handler
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -48,6 +48,13 @@ export class ExperimentHandler extends PsychObject {
|
||||
set experimentEnded(ended) { this._experimentEnded = ended; }
|
||||
|
||||
|
||||
/**
|
||||
* Legacy experiment getters.
|
||||
*/
|
||||
get _thisEntry() { return this._currentTrialData; }
|
||||
get _entries() { return this._trialsData; }
|
||||
|
||||
|
||||
constructor({
|
||||
psychoJS,
|
||||
name,
|
||||
@ -70,6 +77,20 @@ export class ExperimentHandler extends PsychObject {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether or not the current entry (i.e. trial data) is empty.
|
||||
* <p>Note: this is mostly useful at the end of an experiment, in order to ensure that the last entry is saved.</p>
|
||||
*
|
||||
* @name module:data.ExperimentHandler#isEntryEmtpy
|
||||
* @function
|
||||
* @public
|
||||
* @returns {boolean} whether or not the current entry is empty
|
||||
*/
|
||||
isEntryEmtpy() {
|
||||
return (Object.keys(this._currentTrialData).length > 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a loop.
|
||||
*
|
||||
@ -136,13 +157,20 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
nextEntry() {
|
||||
// fetch data from each (potentially-nested) loop:
|
||||
for (let loop of this._unfinishedLoops) {
|
||||
nextEntry(loop) {
|
||||
if (typeof loop !== 'undefined') {
|
||||
const attributes = ExperimentHandler._getLoopAttributes(loop);
|
||||
for (let a in attributes)
|
||||
if (attributes.hasOwnProperty(a))
|
||||
this._currentTrialData[a] = attributes[a];
|
||||
} else {
|
||||
// fetch data from each (potentially-nested) loop:
|
||||
for (let loop of this._unfinishedLoops) {
|
||||
const attributes = ExperimentHandler._getLoopAttributes(loop);
|
||||
for (let a in attributes)
|
||||
if (attributes.hasOwnProperty(a))
|
||||
this._currentTrialData[a] = attributes[a];
|
||||
}
|
||||
}
|
||||
|
||||
// add the extraInfo dict to the data:
|
||||
@ -204,7 +232,7 @@ export class ExperimentHandler extends PsychObject {
|
||||
const __projectId = (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined') ? gitlabConfig.projectId : undefined;
|
||||
|
||||
|
||||
// (*) save to a .csv file on the remote server:
|
||||
// (*) save to a .csv file:
|
||||
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV) {
|
||||
/*
|
||||
// a. manual approach
|
||||
@ -235,8 +263,8 @@ export class ExperimentHandler extends PsychObject {
|
||||
|
||||
// upload data to the pavlovia server or offer them for download:
|
||||
const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
|
||||
if (this._psychoJS.getEnvironment() === PsychoJS.Environment.SERVER)
|
||||
return await this._psychoJS.serverManager.uploadData(key, csv);
|
||||
if (this._psychoJS.getEnvironment() === PsychoJS.Environment.SERVER && this._psychoJS.config.experiment.status === 'RUNNING')
|
||||
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv);
|
||||
else
|
||||
util.offerDataForDownload(key, csv, 'text/csv');
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
* Trial Handler
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -148,6 +148,47 @@ export class TrialHandler extends PsychObject {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} Snapshot
|
||||
* @property {string} name - the trialHandler name
|
||||
* @property {number} nStim - the number of stimuli
|
||||
* @property {number} nTotal - the total number of trials that will be run
|
||||
* @property {number} nRemaining - the total number of trial remaining
|
||||
* @property {number} thisRepN - the current repeat
|
||||
* @property {number} thisTrialN - the current trial number within the current repeat
|
||||
* @property {number} thisN - the total number of trials completed so far
|
||||
* @property {number} thisIndex - the index of the current trial in the conditions list
|
||||
* @property {number} ran - whether or not the trial ran
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get a snapshot of the current internal state of the trial handler (e.g. current trial number,
|
||||
* number of trial remaining).
|
||||
*
|
||||
* <p>This is typically used in the LoopBegin function, in order to capture the current state of a TrialHandler</p>
|
||||
*
|
||||
* @public
|
||||
* @return {Snapshot} - the snapshot of the current internal state.
|
||||
*/
|
||||
getSnapshot() {
|
||||
const currentIndex = this.thisIndex;
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
nStim: this.nStim,
|
||||
nTotal: this.nTotal,
|
||||
nRemaining: this.nRemaining,
|
||||
thisRepN: this.thisRepN,
|
||||
thisTrialN: this.thisTrialN,
|
||||
thisN: this.thisN,
|
||||
thisIndex: this.thisIndex,
|
||||
ran: this.ran,
|
||||
|
||||
getCurrentTrial: () => this.getTrial(currentIndex)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the trial index.
|
||||
@ -180,7 +221,7 @@ export class TrialHandler extends PsychObject {
|
||||
* @return {Array.string} the attributes
|
||||
*/
|
||||
getAttributes() {
|
||||
if (!Array.isArray(this.trialList) || this.nStim == 0)
|
||||
if (!Array.isArray(this.trialList) || this.nStim === 0)
|
||||
return [];
|
||||
|
||||
const firstTrial = this.trialList[0];
|
||||
@ -202,6 +243,20 @@ export class TrialHandler extends PsychObject {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the nth trial.
|
||||
*
|
||||
* @param {number} index - the trial index
|
||||
* @return {Object|undefined} the requested trial or undefined if attempting to go beyond the last trial.
|
||||
*/
|
||||
getTrial(index = 0) {
|
||||
if (index < 0 || index > this.nTotal)
|
||||
return undefined;
|
||||
|
||||
return this.trialList[index];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the nth future or past trial, without advancing through the trial list.
|
||||
*
|
||||
@ -364,7 +419,8 @@ export class TrialHandler extends PsychObject {
|
||||
|
||||
// unknown type:
|
||||
else
|
||||
throw { ...response, error: 'unable to prepare trial list: unknown type: ' + (typeof trialList) };
|
||||
throw Object.assign(response, { error: 'unable to prepare trial list:' +
|
||||
' unknown type: ' + (typeof trialList) });
|
||||
}
|
||||
|
||||
|
||||
@ -433,7 +489,7 @@ export class TrialHandler extends PsychObject {
|
||||
this._trialSequence.push(flatSequence.slice(i * this.nStim, (i + 1) * this.nStim));
|
||||
}
|
||||
else {
|
||||
throw { ...response, error: 'unknown method' };
|
||||
throw Object.assign(response, { error: 'unknown method' });
|
||||
}
|
||||
|
||||
return this._trialSequence;
|
||||
|
@ -3,7 +3,7 @@
|
||||
* Sound stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Sound player interface
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -109,7 +109,7 @@ export class SoundPlayer extends PsychObject
|
||||
* @public
|
||||
* @abstract
|
||||
* @param {Integer} volume - the volume of the tone
|
||||
* @param {booleam} [mute= false] - whether or not to mute the tone
|
||||
* @param {boolean} [mute= false] - whether or not to mute the tone
|
||||
*/
|
||||
setVolume(volume, mute = false) {
|
||||
throw {origin: 'SoundPlayer.setVolume', context: 'when setting the volume of the sound', error: 'this method is abstract and should not be called.'};
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Tone Player.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -151,7 +151,7 @@ export class TonePlayer extends SoundPlayer {
|
||||
* @function
|
||||
* @public
|
||||
* @param {Integer} volume - the volume of the tone
|
||||
* @param {booleam} [mute= false] - whether or not to mute the tone
|
||||
* @param {boolean} [mute= false] - whether or not to mute the tone
|
||||
*/
|
||||
setVolume(volume, mute = false) {
|
||||
this._volume = volume;
|
||||
@ -178,9 +178,9 @@ export class TonePlayer extends SoundPlayer {
|
||||
const self = this;
|
||||
const callback = time => { self._synth.triggerAttackRelease(self._note, self.duration_s, Tone.now()); };
|
||||
|
||||
if (this.loops == 0)
|
||||
if (this.loops === 0)
|
||||
this._toneId = Tone.Transport.scheduleOnce(callback, Tone.now());
|
||||
else if (this.loops == -1)
|
||||
else if (this.loops === -1)
|
||||
this._toneId = Tone.Transport.scheduleRepeat(
|
||||
callback,
|
||||
this.duration_s,
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Track Player.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -100,7 +100,7 @@ export class TrackPlayer extends SoundPlayer {
|
||||
* @function
|
||||
* @public
|
||||
* @param {Integer} volume - the volume of the track (must be between 0 and 1.0)
|
||||
* @param {booleam} [mute= false] - whether or not to mute the track
|
||||
* @param {boolean} [mute= false] - whether or not to mute the track
|
||||
*/
|
||||
setVolume(volume, mute = false) {
|
||||
this._volume = volume;
|
||||
@ -123,7 +123,7 @@ export class TrackPlayer extends SoundPlayer {
|
||||
this._loops = loops;
|
||||
this._currentLoopIndex = -1;
|
||||
|
||||
if (loops == 0)
|
||||
if (loops === 0)
|
||||
this._howl.loop(false);
|
||||
else
|
||||
this._howl.loop(true);
|
||||
|
@ -2,14 +2,11 @@
|
||||
* Clock component.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
import * as util from '../util/Util';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* <p>MonotonicClock offers a convenient way to keep track of time during experiments. An experiment can have as many independent clocks as needed, e.g. one to time responses, another one to keep track of stimuli, etc.</p>
|
||||
@ -43,7 +40,7 @@ export class MonotonicClock {
|
||||
* @name module:util.MonotonicClock#getLastResetTime
|
||||
* @function
|
||||
* @public
|
||||
* @return {number} the offset (in ms)
|
||||
* @return {number} the offset (in seconds)
|
||||
*/
|
||||
getLastResetTime() {
|
||||
return this._timeAtLastReset;
|
||||
@ -56,10 +53,10 @@ export class MonotonicClock {
|
||||
* @name module:util.MonotonicClock#getReferenceTime
|
||||
* @function
|
||||
* @public
|
||||
* @return {number} the time elapsed since the reference point (in ms)
|
||||
* @return {number} the time elapsed since the reference point (in seconds)
|
||||
*/
|
||||
static getReferenceTime() {
|
||||
return (new Date().getTime() - MonotonicClock._referenceTime) / 1000;
|
||||
return (new Date().getTime()) / 1000.0 - MonotonicClock._referenceTime;
|
||||
}
|
||||
|
||||
|
||||
@ -82,14 +79,14 @@ export class MonotonicClock {
|
||||
|
||||
|
||||
/**
|
||||
* The clock's referenceTime is the time when the module was loaded.
|
||||
* The clock's referenceTime is the time when the module was loaded (in seconds).
|
||||
*
|
||||
* @name module:util.MonotonicClock._referenceTime
|
||||
* @readonly
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
MonotonicClock._referenceTime = new Date().getTime();
|
||||
MonotonicClock._referenceTime = new Date().getTime() / 1000.0;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Color management.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -39,7 +39,9 @@ export class Color {
|
||||
// note: we expect the color space to be RGB
|
||||
if (typeof obj == 'string') {
|
||||
if (colorspace !== Color.COLOR_SPACE.RGB)
|
||||
throw { ...response, error: 'the colorspace must be RGB for a named color' };
|
||||
throw Object.assign(response, { error: 'the colorspace must be RGB for' +
|
||||
' a' +
|
||||
' named color' });
|
||||
|
||||
// hexademical representation:
|
||||
if (obj[0] === '#') {
|
||||
@ -48,7 +50,7 @@ export class Color {
|
||||
// named color:
|
||||
else {
|
||||
if (!(obj.toLowerCase() in Color.NAMED_COLORS))
|
||||
throw { ...response, error: 'unknown named color: ' + obj };
|
||||
throw Object.assign(response, { error: 'unknown named color: ' + obj });
|
||||
|
||||
this._hex = Color.NAMED_COLORS[obj.toLowerCase()];
|
||||
}
|
||||
@ -60,7 +62,9 @@ export class Color {
|
||||
// note: we expect the color space to be RGB
|
||||
else if (typeof obj == 'number') {
|
||||
if (colorspace !== Color.COLOR_SPACE.RGB)
|
||||
throw { ...response, error: 'the colorspace must be RGB for a named color' };
|
||||
throw Object.assign(response, { error: 'the colorspace must be RGB for' +
|
||||
' a' +
|
||||
' named color' });
|
||||
|
||||
this._rgb = Color._intToRgb(obj);
|
||||
}
|
||||
@ -100,7 +104,7 @@ export class Color {
|
||||
break;
|
||||
|
||||
default:
|
||||
throw { ...response, error: 'unknown colorspace: ' + colorspace };
|
||||
throw Object.assign(response, { error: 'unknown colorspace: ' + colorspace });
|
||||
}
|
||||
|
||||
}
|
||||
@ -230,7 +234,7 @@ export class Color {
|
||||
return Color._rgb255ToHex(rgb255);
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,7 +257,7 @@ export class Color {
|
||||
return Color._rgbToHex(rgb);
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,7 +280,7 @@ export class Color {
|
||||
return Color._rgbToInt(rgb);
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,7 +302,7 @@ export class Color {
|
||||
return Color._rgb255ToInt(rgb255);
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Color Mixin.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Event Emitter.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Logger
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -3,7 +3,7 @@
|
||||
* Core Object.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -70,7 +70,8 @@ export class PsychObject extends EventEmitter {
|
||||
const response = { origin: 'PsychObject.setAttribute', context: 'when setting the attribute of an object' };
|
||||
|
||||
if (typeof attributeName == 'undefined')
|
||||
throw { ...response, error: 'the attribute name cannot be undefined' };
|
||||
throw Object.assign(response, { error: 'the attribute name cannot be' +
|
||||
' undefined' });
|
||||
if (typeof attributeValue == 'undefined') {
|
||||
this._psychoJS.logger.warn('setting the value of attribute: ' + attributeName + ' in PsychObject: ' + this._name + ' as: undefined');
|
||||
}
|
||||
@ -80,14 +81,16 @@ export class PsychObject extends EventEmitter {
|
||||
let oldValue = this['_' + attributeName];
|
||||
|
||||
// operations can only be applied to numbers and array of numbers (which can be empty):
|
||||
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length == 0 || typeof attributeValue[0] == 'number'))) {
|
||||
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == 'number'))) {
|
||||
|
||||
// value is an array:
|
||||
if (Array.isArray(attributeValue)) {
|
||||
// old value is also an array
|
||||
if (Array.isArray(oldValue)) {
|
||||
if (attributeValue.length !== oldValue.length)
|
||||
throw { ...response, error: 'old and new value should have the same size when they are both arrays' };
|
||||
throw Object.assign(response, { error: 'old and new' +
|
||||
' value should have' +
|
||||
' the same size when they are both arrays' });
|
||||
|
||||
switch (operation) {
|
||||
case '':
|
||||
@ -112,7 +115,8 @@ export class PsychObject extends EventEmitter {
|
||||
attributeValue = attributeValue.map((v, i) => oldValue[i] % v);
|
||||
break;
|
||||
default:
|
||||
throw { ...response, error: 'unsupported operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name };
|
||||
throw Object.assign(response, { error: 'unsupported' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
}
|
||||
|
||||
} else
|
||||
@ -141,7 +145,9 @@ export class PsychObject extends EventEmitter {
|
||||
attributeValue = attributeValue.map(v => oldValue % v);
|
||||
break;
|
||||
default:
|
||||
throw { ...response, error: 'unsupported value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name };
|
||||
throw Object.assign(response, { error: 'unsupported' +
|
||||
' value: ' + JSON.stringify(attributeValue) + ' for' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
}
|
||||
}
|
||||
} else
|
||||
@ -172,7 +178,8 @@ export class PsychObject extends EventEmitter {
|
||||
attributeValue = oldValue.map(v => v % attributeValue);
|
||||
break;
|
||||
default:
|
||||
throw { ...response, error: 'unsupported operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name };
|
||||
throw Object.assign(response, { error: 'unsupported' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
}
|
||||
|
||||
} else
|
||||
@ -201,13 +208,14 @@ export class PsychObject extends EventEmitter {
|
||||
attributeValue = oldValue % attributeValue;
|
||||
break;
|
||||
default:
|
||||
throw { ...response, error: 'unsupported value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name };
|
||||
throw Object.assign(response, { error: 'unsupported' +
|
||||
' value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else
|
||||
throw { ...response, error: 'operation: ' + operation + ' is invalid for old value: ' + JSON.stringify(oldValue) + ' and new value: ' + JSON.stringify(attributeValue) };
|
||||
throw Object.assign(response, { error: 'operation: ' + operation + ' is invalid for old value: ' + JSON.stringify(oldValue) + ' and new value: ' + JSON.stringify(attributeValue) });
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Scheduler.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -14,7 +14,7 @@
|
||||
*
|
||||
* <p>
|
||||
* Tasks are either another [Scheduler]{@link module:util.Scheduler}, or a
|
||||
* JavaScript function returning one of the following codes:
|
||||
* JavaScript functions returning one of the following codes:
|
||||
* <ul>
|
||||
* <li>Scheduler.Event.NEXT: Move onto the next task *without* rendering the scene first.</li>
|
||||
* <li>Scheduler.Event.FLIP_REPEAT: Render the scene and repeat the task.</li>
|
||||
@ -201,8 +201,14 @@ export class Scheduler {
|
||||
// we are repeating a task
|
||||
}
|
||||
|
||||
// if the current task is a scheduler, we run its tasks until a rendering of the scene is required:
|
||||
if (this._currentTask instanceof Scheduler) {
|
||||
// if the current task is a function, we run it:
|
||||
if (this._currentTask instanceof Function) {
|
||||
state = this._currentTask(...this._currentArgs);
|
||||
}
|
||||
// otherwise, we assume that the current task is a scheduler and we run its tasks until a rendering
|
||||
// of the scene is required.
|
||||
// note: "if (this._currentTask instanceof Scheduler)" does not work because of CORS...
|
||||
else {
|
||||
state = this._currentTask._runNextTasks();
|
||||
if (state === Scheduler.Event.QUIT) {
|
||||
// if the experiment has not ended, we move onto the next task:
|
||||
@ -211,18 +217,6 @@ export class Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// if the current task is a function, we run it:
|
||||
else if (this._currentTask instanceof Function) {
|
||||
state = this._currentTask(...this._currentArgs);
|
||||
}
|
||||
|
||||
// we should not be here...
|
||||
else { console.log(this._currentTask);
|
||||
throw { origin: 'Scheduler._runNextTasks', context: 'when running the scheduler\'s tasks', error: 'the next' +
|
||||
' task has unknown type (neither a Scheduler nor a Function)' };
|
||||
}
|
||||
|
||||
|
||||
// if the current task's return status is FLIP_REPEAT, we will re-run it, otherwise
|
||||
// we move onto the next task:
|
||||
if (state !== Scheduler.Event.FLIP_REPEAT) {
|
||||
@ -287,4 +281,4 @@ Scheduler.Status = {
|
||||
* The Scheduler is stopped.
|
||||
*/
|
||||
STOPPED: Symbol.for('STOPPED')
|
||||
};
|
||||
};
|
@ -2,7 +2,7 @@
|
||||
* Various utilities.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -193,7 +193,8 @@ export function toNumerical(obj)
|
||||
return obj.map( e => {
|
||||
let n = Number.parseFloat(e);
|
||||
if (Number.isNaN(n))
|
||||
throw { ...response, error: 'unable to convert: ' + e + ' to a number.'};
|
||||
Object.assign(response, { error: 'unable to convert: ' + e + ' to a' +
|
||||
' number.'});
|
||||
return n;
|
||||
});
|
||||
}
|
||||
@ -282,7 +283,7 @@ export function getPositionFromObject(object, units)
|
||||
return to_px(object, units, objectWin);
|
||||
}
|
||||
catch (error) {
|
||||
throw {...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,7 +312,7 @@ export function to_px(pos, posUnit, win)
|
||||
return [pos[0] * minSize, pos[1] * minSize];
|
||||
}
|
||||
else
|
||||
throw { ...response, error: `unknown position units: ${posUnit}` };
|
||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
||||
}
|
||||
|
||||
|
||||
@ -339,7 +340,7 @@ export function to_norm(pos, posUnit, win)
|
||||
return [pos[0] * minSize / (win.size[0]/2.0), pos[1] * minSize / (win.size[1]/2.0)];
|
||||
}
|
||||
|
||||
throw { ...response, error: `unknown position units: ${posUnit}` };
|
||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
||||
}
|
||||
|
||||
|
||||
@ -369,7 +370,7 @@ export function to_height(pos, posUnit, win)
|
||||
return [pos[0] * win.size[0]/2.0 / minSize, pos[1] * win.size[1]/2.0 / minSize];
|
||||
}
|
||||
|
||||
throw { ...response, error: `unknown position units: ${posUnit}` };
|
||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
||||
}
|
||||
|
||||
|
||||
@ -398,7 +399,7 @@ export function to_win(pos, posUnit, win)
|
||||
|
||||
throw `unknown window units: ${win._units}`;
|
||||
} catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { response, error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -429,7 +430,7 @@ export function to_unit(pos, posUnit, win, targetUnit)
|
||||
|
||||
throw `unknown target units: ${targetUnit}`;
|
||||
} catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -549,6 +550,9 @@ export function getUrlParameters()
|
||||
/**
|
||||
* Add info extracted from the URL to the given dictionary.
|
||||
*
|
||||
* <p>We exclude all URL parameters starting with a double underscore
|
||||
* since those are reserved for client/server communication</p>
|
||||
*
|
||||
* @name module:util.addInfoFromUrl
|
||||
* @function
|
||||
* @public
|
||||
@ -558,10 +562,10 @@ export function addInfoFromUrl(info)
|
||||
{
|
||||
const infoFromUrl = getUrlParameters();
|
||||
|
||||
// note: since __msg is a key reserved for communications between the pavlovia.org server
|
||||
// and the experiment running in the participant's browser, we do not add it to info.
|
||||
// note: parameters starting with a double underscore are reserved for client/server communication,
|
||||
// we do not add them to info
|
||||
for (const [key, value] of infoFromUrl)
|
||||
if (key !== '__msg')
|
||||
if (key.indexOf('__') !== 0)
|
||||
info[key] = value;
|
||||
|
||||
return info;
|
||||
@ -586,14 +590,14 @@ export function addInfoFromUrl(info)
|
||||
* @public
|
||||
* @param {Array.<Object>} array - the input array
|
||||
* @param {number | Array.<number> | string} selection - the selection
|
||||
* @returns {Array.<Object>} the array of selected items
|
||||
* @returns {Object | Array.<Object>} the array of selected items
|
||||
*/
|
||||
export function selectFromArray(array, selection) {
|
||||
|
||||
// if selection is an integer, or a string representing an integer, we treat it as an index in the array
|
||||
// and return that entry:
|
||||
if (isInt(selection))
|
||||
return [array[parseInt(selection)]];
|
||||
return array[parseInt(selection)];
|
||||
|
||||
// if selection is an array, we treat it as a list of indices
|
||||
// and return an array with the entries corresponding to those indices:
|
||||
@ -603,7 +607,8 @@ export function selectFromArray(array, selection) {
|
||||
// if selection is a string, we decode it:
|
||||
else if (typeof selection === 'string') {
|
||||
if (selection.indexOf(',') > -1)
|
||||
return flattenArray( selection.split(',').map(a => selectFromArray(array, a)) );
|
||||
return selection.split(',').map(a => selectFromArray(array, a));
|
||||
// return flattenArray( selection.split(',').map(a => selectFromArray(array, a)) );
|
||||
else if (selection.indexOf(':') > -1) {
|
||||
let sliceParams = selection.split(':').map(a => parseInt(a));
|
||||
if (sliceParams.length === 3)
|
||||
@ -628,7 +633,13 @@ export function selectFromArray(array, selection) {
|
||||
* @returns {Array.<Object>} the flatten array
|
||||
*/
|
||||
export function flattenArray(array) {
|
||||
return array.reduce( (flat, next) => flat.concat(Array.isArray(next) ? flattenArray(next) : next), [] );
|
||||
return array.reduce(
|
||||
(flat, next) => {
|
||||
flat.push( (Array.isArray(next) && Array.isArray(next[0])) ? flattenArray(next) : next );
|
||||
return flat;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -673,7 +684,7 @@ export function sliceArray(array, from = NaN, to = NaN, step = NaN)
|
||||
* @param {string} type - the MIME type of the data, e.g. 'text/csv' or 'application/json'
|
||||
*/
|
||||
export function offerDataForDownload(filename, data, type) {
|
||||
var blob = new Blob([data], { type });
|
||||
const blob = new Blob([data], { type });
|
||||
if (window.navigator.msSaveOrOpenBlob)
|
||||
window.navigator.msSaveBlob(blob, filename);
|
||||
else {
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Image Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -110,7 +110,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
this._needUpdate = true;
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,7 +150,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
this._needUpdate = true;
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Movie Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -120,7 +120,7 @@ export class MovieStim extends VisualStim {
|
||||
this._needUpdate = true;
|
||||
}
|
||||
catch (error) {
|
||||
throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Rectangular Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -76,7 +76,7 @@ export class Rect extends ShapeStim {
|
||||
* @name module:visual.Rect#setWidth
|
||||
* @public
|
||||
* @param {number} width - the rectange width
|
||||
* @param {boolean} [log= false] - whether or not to log
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setWidth(width, log = false) {
|
||||
this._psychoJS.logger.debug('set the width of Rect: ', this.name, 'to: ', width);
|
||||
@ -92,7 +92,7 @@ export class Rect extends ShapeStim {
|
||||
* @name module:visual.Rect#setHeight
|
||||
* @public
|
||||
* @param {number} height - the rectange height
|
||||
* @param {boolean} [log= false] - whether or not to log
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setHeight(height, log = false) {
|
||||
this._psychoJS.logger.debug('set the height of Rect: ', this.name, 'to: ', height);
|
||||
@ -101,23 +101,7 @@ export class Rect extends ShapeStim {
|
||||
this._updateVertices();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the size attribute.
|
||||
*
|
||||
* @name module:visual.Rect#setSize
|
||||
* @public
|
||||
* @param {number[]} size - the [x, y] size of the rectangle
|
||||
* @param {boolean} [log= false] - whether or not to log
|
||||
*/
|
||||
setSize(size, log = false) {
|
||||
this._psychoJS.logger.debug('set the size of Rect: ', this.name, 'to: ', size);
|
||||
|
||||
this._setAttribute('width', size[0], log);
|
||||
this._setAttribute('height', size[1], log);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the base shape vertices.
|
||||
*
|
||||
|
@ -3,7 +3,7 @@
|
||||
* Basic Shape Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Slider Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Text Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Base class for all visual stimuli.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 3.0.8
|
||||
* @version 3.1.4
|
||||
* @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user