diff --git a/js/core/EventManager.js b/js/core/EventManager.js
index 4bba799..8ebdda6 100644
--- a/js/core/EventManager.js
+++ b/js/core/EventManager.js
@@ -1,5 +1,5 @@
/**
- * @file Manager handling the keyboard and mouse/touch events.
+ * Manager handling the keyboard and mouse/touch events.
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -207,6 +207,14 @@ export class EventManager {
const view = renderer.view;
+ /*
+ // TEMPORARY DEBUG FOR IPAD/IPHONE:
+ for (let eventName of ['click', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mouseupoutside', 'pointercancel', 'pointerdown', 'pointermove', 'pointerout', 'pointerover', 'pointertap', 'pointerup', 'pointerupoutside', 'rightclick', 'rightdown', 'rightup', ' rightupoutside', 'tap', 'touchcancel', 'touchend', 'touchendoutside', 'touchmove', 'touchstart'])
+ view.addEventListener(eventName, event => {
+ console.log('event: ' + eventName + ' -> ', event);
+ });
+ */
+
view.addEventListener("pointerdown", event => {
self._mouseInfo.buttons.pressed[event.button] = 1;
self._mouseInfo.buttons.times[event.button] = self._psychoJS._monotonicClock.getTime() - self._mouseInfo.buttons.clocks[event.button].getLastResetTime();
@@ -232,12 +240,13 @@ export class EventManager {
view.addEventListener("wheel", event => {
self._mouseInfo.wheelRel[0] += event.deltaX;
self._mouseInfo.wheelRel[1] += event.deltaY;
- /*
- var x = ev.offsetX;
- var y = ev.offsetY;
- var msg = "Mouse: wheel shift=(" + ev.deltaX + "," + ev.deltaY + "), pos=(" + x + "," + y + ")";
- psychoJS.logging.data(msg);*/
+
+ //var x = ev.offsetX;
+ //var y = ev.offsetY;
+ //var msg = "Mouse: wheel shift=(" + ev.deltaX + "," + ev.deltaY + "), pos=(" + x + "," + y + ")";
+ //psychoJS.logging.data(msg);
}, false);
+
}
@@ -508,4 +517,4 @@ export class BuilderKeyResponse {
this.rt = []; // response time(s)
this.clock = new Clock(); // we'll use this to measure the rt
}
-}
\ No newline at end of file
+}
diff --git a/js/core/GUI.js b/js/core/GUI.js
index 785087a..14f27a8 100644
--- a/js/core/GUI.js
+++ b/js/core/GUI.js
@@ -1,5 +1,5 @@
/**
- * @file Graphic User Interface
+ * Graphic User Interface
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -340,4 +340,4 @@ export class GUI
}
}
-}
\ No newline at end of file
+}
diff --git a/js/core/MinimalStim.js b/js/core/MinimalStim.js
index 72366c8..f1a6207 100644
--- a/js/core/MinimalStim.js
+++ b/js/core/MinimalStim.js
@@ -1,5 +1,5 @@
/**
- * @file Base class for all stimuli.
+ * Base class for all stimuli.
*
* @author Alain Pitiot
* @version 3.0.0b11
diff --git a/js/core/Mouse.js b/js/core/Mouse.js
index 8da4bd9..cc10e95 100644
--- a/js/core/Mouse.js
+++ b/js/core/Mouse.js
@@ -1,5 +1,5 @@
/**
- * @file Manager responsible for the interactions between the experiment's stimuli and the mouse.
+ * Manager responsible for the interactions between the experiment's stimuli and the mouse.
*
* @author Alain Pitiot
* @version 3.0.0b11
diff --git a/js/core/PsychoJS.js b/js/core/PsychoJS.js
index 4ba067b..ff35c61 100644
--- a/js/core/PsychoJS.js
+++ b/js/core/PsychoJS.js
@@ -1,6 +1,6 @@
/** @module core */
/**
- * @file Main component of the PsychoJS library.
+ * Main component of the PsychoJS library.
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -217,7 +217,7 @@ export class PsychoJS {
// setup the logger:
//my.logger.console.setLevel(psychoJS.logging.WARNING);
- //my.logger.server.set({'level':psychoJS.logging.WARNING, 'saveTo':'EXPERIMENT_SERVER', 'experimentInfo': my.expInfo});
+ //my.logger.server.set({'level':psychoJS.logging.WARNING, 'experimentInfo': my.expInfo});
// open a new session:
await this._serverManager.openSession();
@@ -277,7 +277,7 @@ export class PsychoJS {
await this._experiment.save();
// close the session:
- await this._serverManager.closeSession();
+ await this._serverManager.closeSession(isCompleted);
// stop the main scheduler:
this._scheduler.stop();
@@ -344,6 +344,12 @@ export class PsychoJS {
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;
+
return response;
}
catch (error) {
diff --git a/js/core/ServerManager.js b/js/core/ServerManager.js
index 2d7855f..4d2c0e8 100644
--- a/js/core/ServerManager.js
+++ b/js/core/ServerManager.js
@@ -1,5 +1,5 @@
/**
- * @file 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.
+ * 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.0b11
@@ -106,11 +106,16 @@ export class ServerManager extends PsychObject {
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;
+
let self = this;
return new Promise((resolve, reject) => {
- const data = {
- experimentFullPath: self._psychoJS.config.experiment.fullpath
- };
+
$.post(this._psychoJS.config.psychoJsManager.URL + '?command=open_session', data, null, 'json')
.done((data, textStatus) => {
// check for error:
@@ -150,21 +155,28 @@ export class ServerManager extends PsychObject {
* @name module:core.ServerManager#closeSession
* @function
* @public
+ * @param {boolean} [isCompleted= false] - whether or not the experiment was completed
* @returns {Promise} the response
*/
- closeSession() {
+ closeSession(isCompleted = false) {
let response = { origin: 'ServerManager.closeSession', context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.name };
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;
+
+
let self = this;
- return new Promise((resolve, reject) => {
- const data = {
- experimentFullPath: self._psychoJS.config.experiment.fullpath,
- 'token': self._psychoJS.config.experiment.token
- };
+ return new Promise((resolve, reject) => {
$.post(this._psychoJS.config.psychoJsManager.URL + '?command=close_session', data, null, 'json')
.done((data, textStatus) => {
// check for error:
@@ -282,9 +294,7 @@ export class ServerManager extends PsychObject {
download();
}
-
-
-
+
/**
* @typedef ServerManager.UploadDataPromise
* @property {string} origin the calling method
@@ -297,8 +307,8 @@ export class ServerManager extends PsychObject {
* @name module:core.ServerManager#uploadData
* @function
* @public
- * @param {string} key the data key
- * @param {*} value the data value
+ * @param {string} key - the data key (e.g. the name of .csv file)
+ * @param {string} value - the data value (e.g. a string containing the .csv header and records)
*
* @returns {Promise} the response
*/
@@ -312,14 +322,14 @@ export class ServerManager extends PsychObject {
experimentFullPath: this._psychoJS.config.experiment.fullpath,
token: this._psychoJS.config.experiment.token,
key,
- value
+ value,
+ saveFormat: Symbol.keyFor(this._psychoJS.config.experiment.saveFormat)
};
// 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:
const self = this;
return new Promise((resolve, reject) => {
@@ -564,4 +574,4 @@ ServerManager.Status = {
* The manager has encountered an error, e.g. it was unable to download a resource.
*/
ERROR: Symbol.for('ERROR')
-};
\ No newline at end of file
+};
diff --git a/js/core/Window.js b/js/core/Window.js
index b1c314b..9398fb8 100644
--- a/js/core/Window.js
+++ b/js/core/Window.js
@@ -1,5 +1,5 @@
/**
- * @file Window responsible for displaying the experiment stimuli
+ * Window responsible for displaying the experiment stimuli
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -18,6 +18,7 @@ import * as util from '../util/Util';
*
* @name module:core.Window
* @class
+ * @extends PsychObject
* @param {Object} options
* @param {PsychoJS} options.psychoJS - the PsychoJS instance
* @param {string} [options.name] the name of the window
@@ -25,8 +26,6 @@ import * as util from '../util/Util';
* @param {Color} [options.color= Color('black')] the background color of the window
* @param {string} [options.units= 'pix'] the units of the window
* @param {boolean} [options.autoLog= true] whether or not to log
- *
- * @extends PsychObject
*/
export class Window extends PsychObject {
@@ -262,6 +261,7 @@ export class Window extends PsychObject {
// top-level container:
this._rootContainer = new PIXI.Container();
+ this._rootContainer.interactive = true;
// set size of renderer and position of root container:
this._onResize(this);
diff --git a/js/core/WindowMixin.js b/js/core/WindowMixin.js
index ca445d3..5b5bedb 100644
--- a/js/core/WindowMixin.js
+++ b/js/core/WindowMixin.js
@@ -1,5 +1,5 @@
/**
- * @file Mixin implementing various unit-handling measurement methods.
+ * Mixin implementing various unit-handling measurement methods.
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -19,10 +19,8 @@
* @mixin
*
*/
-export let WindowMixin = (superclass) => class extends superclass
-{
- constructor(args)
- {
+export let WindowMixin = (superclass) => class extends superclass {
+ constructor(args) {
super(args);
}
@@ -36,8 +34,7 @@ export let WindowMixin = (superclass) => class extends superclass
* @param {String} [units= this.win.units] - the units
* @param {boolean} [log= false] - whether or not to log
*/
- setUnits(units = this.win.units, log = false)
- {
+ setUnits(units = this.win.units, log = false) {
this._setAttribute('units', units, log);
}
@@ -51,8 +48,7 @@ export let WindowMixin = (superclass) => class extends superclass
* @param {number} length - the length in stimulus units
* @return {number} - the length in pixel units
*/
- _getLengthPix(length)
- {
+ _getLengthPix(length) {
let errorPrefix = { origin: 'WindowMixin._getLengthPix', context: 'when converting a length from stimulus unit to pixel units' };
if (this._units === 'pix') {
@@ -60,14 +56,14 @@ export let WindowMixin = (superclass) => class extends superclass
}
else if (typeof this._units === 'undefined' || this._units === 'norm') {
var winSize = this.win.size;
- return length * winSize[1]/2; // TODO: how do we handle norm when width != height?
+ return length * winSize[1] / 2; // TODO: how do we handle norm when width != height?
}
else if (this._units === 'height') {
const minSize = Math.min(this.win.size[0], this.win.size[1]);
return length * minSize;
}
else {
- throw {...errorPrefix, error: 'unable to deal with unit: ' + this._units};
+ throw { ...errorPrefix, error: 'unable to deal with unit: ' + this._units };
}
}
@@ -81,8 +77,7 @@ export let WindowMixin = (superclass) => class extends superclass
* @param {number} length_px - the length in pixel units
* @return {number} - the length in stimulus units
*/
- _getLengthUnits(length_px)
- {
+ _getLengthUnits(length_px) {
let errorPrefix = { origin: 'WindowMixin._getLengthUnits', context: 'when converting a length from pixel unit to stimulus units' };
if (this._units === 'pix') {
@@ -90,14 +85,14 @@ export let WindowMixin = (superclass) => class extends superclass
}
else if (typeof this._units === 'undefined' || this._units === 'norm') {
const winSize = this.win.size;
- return length_px / (winSize[1]/2); // TODO: how do we handle norm when width != height?
+ return length_px / (winSize[1] / 2); // TODO: how do we handle norm when width != height?
}
else if (this._units === 'height') {
const minSize = Math.min(this.win.size[0], this.win.size[1]);
return length_px / minSize;
}
else {
- throw {...errorPrefix, error: 'unable to deal with unit: ' + this._units};
+ throw { ...errorPrefix, error: 'unable to deal with unit: ' + this._units };
}
}
@@ -111,8 +106,7 @@ export let WindowMixin = (superclass) => class extends superclass
* @param {number} length_px - the length in pixel units
* @return {number} - the length in stimulus units
*/
- _getHorLengthPix(length)
- {
+ _getHorLengthPix(length) {
let errorPrefix = { origin: 'WindowMixin._getHorLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
if (this._units === 'pix') {
@@ -120,14 +114,14 @@ export let WindowMixin = (superclass) => class extends superclass
}
else if (typeof this._units === 'undefined' || this._units === 'norm') {
var winSize = this.win.size;
- return length * winSize[0]/2;
+ return length * winSize[0] / 2;
}
else if (this._units === 'height') {
const minSize = Math.min(this.win.size[0], this.win.size[1]);
return length * minSize;
}
else {
- throw {...errorPrefix, error: 'unable to deal with unit: ' + this._units};
+ throw { ...errorPrefix, error: 'unable to deal with unit: ' + this._units };
}
}
@@ -140,8 +134,7 @@ export let WindowMixin = (superclass) => class extends superclass
* @param {number} length_px - the length in pixel units
* @return {number} - the length in stimulus units
*/
- _getVerLengthPix(length)
- {
+ _getVerLengthPix(length) {
let errorPrefix = { origin: 'WindowMixin._getVerLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
if (this._units === 'pix') {
@@ -149,16 +142,15 @@ export let WindowMixin = (superclass) => class extends superclass
}
else if (typeof this._units === 'undefined' || this._units === 'norm') {
var winSize = this.win.size;
- return length * winSize[1]/2;
+ return length * winSize[1] / 2;
}
else if (this._units === 'height') {
const minSize = Math.min(this.win.size[0], this.win.size[1]);
return length * minSize;
}
else {
- throw {...errorPrefix, error: 'unable to deal with unit: ' + this._units};
+ throw { ...errorPrefix, error: 'unable to deal with unit: ' + this._units };
}
}
-
}
diff --git a/js/data/ExperimentHandler.js b/js/data/ExperimentHandler.js
index e6ce44e..8ebc5f9 100644
--- a/js/data/ExperimentHandler.js
+++ b/js/data/ExperimentHandler.js
@@ -1,5 +1,5 @@
/**
- * @file Experiment Handler
+ * Experiment Handler
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -159,57 +159,84 @@ export class ExperimentHandler extends PsychObject {
* @public
* @param {Object} options
* @param {PsychoJS} options.attributes - the attributes to be saved
- *
- * @todo deal with attributes
*/
async save({
attributes = []
} = {}) {
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
- // key is based on extraInfo:
+ // (*) get attributes:
+ if (attributes.length == 0) {
+ attributes = this._trialsKeys.slice();
+ for (let l = 0; l < this._loops.length; l++) {
+ const loop = this._loops[l];
+
+ const loopAttributes = this.getLoopAttributes(loop);
+ for (let a in loopAttributes)
+ if (loopAttributes.hasOwnProperty(a))
+ attributes.push(a);
+ }
+ for (let a in this.extraInfo) {
+ if (this.extraInfo.hasOwnProperty(a))
+ attributes.push(a);
+ }
+ }
+
+
+ // (*) get various experiment info:
const info = this.extraInfo;
- let key = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name;
- key += "_" + ((typeof info.participant === 'string' && info.participant.length > 0) ? info.participant : 'PARTICIPANT');
- key += "_" + ((typeof info.session === 'string' && info.session.length > 0) ? info.session : 'SESSION');
- key += "_" + ((typeof info.date !== 'undefined') ? info.date : MonotonicClock.getDateStr());
+ const __experimentName = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name;
+ const __participant = ((typeof info.participant === 'string' && info.participant.length > 0) ? info.participant : 'PARTICIPANT');
+ const __session = ((typeof info.session === 'string' && info.session.length > 0) ? info.session : 'SESSION');
+ const __datetime = ((typeof info.date !== 'undefined') ? info.date : MonotonicClock.getDateStr());
+ const gitlabConfig = this._psychoJS.config.gitlab;
+ const __projectId = (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined')?gitlabConfig.projectId:undefined;
- // data is in the csv format:
- // build the csv header:
- let csv = "";
- let header = this._trialsKeys.slice();
- for (let l = 0; l < this._loops.length; l++) {
- const loop = this._loops[l];
- const loopAttributes = this.getLoopAttributes(loop);
- for (let a in loopAttributes)
- if (loopAttributes.hasOwnProperty(a))
- header.push(a);
- }
- for (let a in this.extraInfo) {
- if (this.extraInfo.hasOwnProperty(a))
- header.push(a);
- }
+ // (*) save to a .csv file on the remote server:
+ if (this._psychoJS.config.experiment.saveFormat == ExperimentHandler.SaveFormat.CSV) {
+ let csv = "";
- for (let h = 0; h < header.length; h++) {
- if (h > 0)
- csv = csv + ', ';
- csv = csv + header[h];
- }
- csv = csv + '\n';
-
- // build the records:
- for (let r = 0; r < this._trialsData.length; r++) {
- for (let h = 0; h < header.length; h++) {
+ // build the csv header:
+ for (let h = 0; h < attributes.length; h++) {
if (h > 0)
csv = csv + ', ';
- csv = csv + this._trialsData[r][header[h]];
+ csv = csv + attributes[h];
}
csv = csv + '\n';
+
+ // build the records:
+ for (let r = 0; r < this._trialsData.length; r++) {
+ for (let h = 0; h < attributes.length; h++) {
+ if (h > 0)
+ csv = csv + ', ';
+ csv = csv + this._trialsData[r][attributes[h]];
+ }
+ csv = csv + '\n';
+ }
+
+ // upload data to the remote PsychoJS manager:
+ const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
+ return await this._psychoJS.serverManager.uploadData(key, csv);
}
- // upload data to the remote PsychoJS manager:
- return await this._psychoJS.serverManager.uploadData(key + '.csv', csv);
+
+ // (*) save in the database on the remote server:
+ else if (this._psychoJS.config.experiment.saveFormat == ExperimentHandler.SaveFormat.DATABASE) {
+ let documents = [];
+
+ for (let r = 0; r < this._trialsData.length; r++) {
+ let doc = { __projectId, __experimentName, __participant, __session, __datetime };
+ for (let h = 0; h < attributes.length; h++)
+ doc[attributes[h]] = this._trialsData[r][attributes[h]];
+
+ documents.push(doc);
+ }
+
+ // upload data to the remote PsychoJS manager:
+ const key = 'results'; // name of the mongoDB collection
+ return await this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents));
+ }
}
@@ -273,4 +300,25 @@ export class ExperimentHandler extends PsychObject {
return attributes;
}
-}
+};
+
+
+/**
+ * Experiment result format
+ *
+ * @name module:core.ServerManager#SaveFormat
+ * @enum {Symbol}
+ * @readonly
+ * @public
+ */
+ExperimentHandler.SaveFormat = {
+ /**
+ * Results are saved to a .csv file
+ */
+ CSV: Symbol.for('CSV'),
+
+ /**
+ * Results are saved to a database
+ */
+ DATABASE: Symbol.for('DATABASE')
+};
diff --git a/js/data/TrialHandler.js b/js/data/TrialHandler.js
index 0d6d594..8cdd36f 100644
--- a/js/data/TrialHandler.js
+++ b/js/data/TrialHandler.js
@@ -1,6 +1,6 @@
/** @module data */
/**
- * @file Trial Handler
+ * Trial Handler
*
* @author Alain Pitiot
* @version 3.0.0b11
diff --git a/js/sound/Sound.js b/js/sound/Sound.js
index 61ee7a4..38d2348 100644
--- a/js/sound/Sound.js
+++ b/js/sound/Sound.js
@@ -1,6 +1,6 @@
/** @module sound */
/**
- * @file Sound stimulus.
+ * Sound stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -171,4 +171,4 @@ export class Sound extends PsychObject {
}
-}
\ No newline at end of file
+}
diff --git a/js/sound/SoundPlayer.js b/js/sound/SoundPlayer.js
index 47dfc37..b5f700e 100644
--- a/js/sound/SoundPlayer.js
+++ b/js/sound/SoundPlayer.js
@@ -1,5 +1,5 @@
/**
- * @file Sound player interface
+ * Sound player interface
*
* @author Alain Pitiot
* @version 3.0.0b11
diff --git a/js/sound/TonePlayer.js b/js/sound/TonePlayer.js
index c33408b..62d6347 100644
--- a/js/sound/TonePlayer.js
+++ b/js/sound/TonePlayer.js
@@ -1,5 +1,5 @@
/**
- * @file Tone Player.
+ * Tone Player.
*
* @author Alain Pitiot
* @version 3.0.0b11
diff --git a/js/sound/TrackPlayer.js b/js/sound/TrackPlayer.js
index c58df7a..88dd2f6 100644
--- a/js/sound/TrackPlayer.js
+++ b/js/sound/TrackPlayer.js
@@ -1,5 +1,5 @@
/**
- * @file Track Player.
+ * Track Player.
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -136,7 +136,7 @@ export class TrackPlayer extends SoundPlayer {
* @name module:sound.TrackPlayer#play
* @function
* @public
- * @param {boolean} [loops] how many times to repeat the track after it has played once. If loops == -1, the track will repeat indefinitely until stopped.
+ * @param {number} loops - how many times to repeat the track after it has played once. If loops == -1, the track will repeat indefinitely until stopped.
*/
play(loops) {
if (typeof loops !== 'undefined')
@@ -173,4 +173,4 @@ export class TrackPlayer extends SoundPlayer {
this._howl.off('end');
}
-}
\ No newline at end of file
+}
diff --git a/js/util/Clock.js b/js/util/Clock.js
new file mode 100644
index 0000000..23449ad
--- /dev/null
+++ b/js/util/Clock.js
@@ -0,0 +1,202 @@
+/**
+ * Clock component.
+ *
+ * @author Alain Pitiot
+ * @version 3.0.0b11
+ * @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
+ * @license Distributed under the terms of the MIT License
+ */
+
+
+/**
+ *
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.
This mixin implement color and contrast changes for visual stimuli
+ *
+ * @name module:util.ColorMixin
+ * @mixin
+ */
+export let ColorMixin = (superclass) => class extends superclass {
+ constructor(args) {
+ super(args);
+ }
+
+
+ /**
+ * Setter for Color attribute.
+ *
+ * @name module:core.ColorMixin#setColor
+ * @function
+ * @public
+ * @param {string|number|Array.} color - the new color
+ * @param {boolean} [log= false] - whether or not to log
+ */
+ setColor(color, log) {
+ this._setAttribute('color', color, log);
+
+ this._needUpdate = true;
+ };
+
+
+ /**
+ * Setter for Contrast attribute.
+ *
+ * @name module:core.ColorMixin#setContrast
+ * @function
+ * @public
+ * @param {number} contrast - the new contrast (must be between 0 and 1)
+ * @param {boolean} [log= false] - whether or not to log
+ */
+ setContrast(contrast, log) {
+ this._setAttribute('contrast', contrast, log);
+
+ this._needUpdate = true;
+ }
+
+
+ /**
+ * Adjust the contrast of the color and convert it to [-1, 1] RGB
+ *
+ * @name module:core.ColorMixin#getContrastedColor
+ * @function
+ * @public
+ * @param {string|number|Array.} color - the color
+ * @param {number} contrast - the contrast (must be between 0 and 1)
+ */
+ getContrastedColor(color, contrast) {
+ let rgb = color.rgb.map(c => (c * 2.0 - 1.0) * contrast);
+ return new Color(rgb, Color.COLOR_SPACE.RGB);
+ }
+
+}
\ No newline at end of file
diff --git a/js/util/EventEmitter.js b/js/util/EventEmitter.js
new file mode 100644
index 0000000..12b013e
--- /dev/null
+++ b/js/util/EventEmitter.js
@@ -0,0 +1,147 @@
+/**
+ * Event Emitter.
+ *
+ * @author Alain Pitiot
+ * @version 3.0.0b11
+ * @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
+ * @license Distributed under the terms of the MIT License
+ */
+
+
+import * as util from './Util';
+
+
+/**
+ *
EventEmitter implements the classic observer/observable pattern.
+ *
+ *
Note: this is heavily inspired by http://www.datchley.name/es6-eventemitter/
+ *
+ * @name module:util.EventEmitter
+ * @class
+ *
+ * @example
+ * let observable = new EventEmitter();
+ * let uuid1 = observable.on('change', data => { console.log(data); });
+ * observable.emit("change", { a: 1 });
+ * observable.off("change", uuid1);
+ * observable.emit("change", { a: 1 });
+ */
+export class EventEmitter
+{
+ constructor()
+ {
+ this._listeners = new Map();
+ this._onceUuids = new Map();
+ }
+
+
+ /**
+ * Listener called when this instance emits an event for which it is registered.
+ *
+ * @callback module:util.EventEmitter~Listener
+ * @param {object} data - the data passed to the listener
+ */
+
+
+ /**
+ * Register a new listener for events with the given name emitted by this instance.
+ *
+ * @name module:util.EventEmitter#on
+ * @function
+ * @public
+ * @param {String} name - the name of the event
+ * @param {module:util.EventEmitter~Listener} listener - a listener called upon emission of the event
+ * @return string - the unique identifier associated with that (event, listener) pair (useful to remove the listener)
+ */
+ on(name, listener)
+ {
+ // check that the listener is a function:
+ if (typeof listener !== 'function')
+ throw new TypeError('listener must be a function');
+
+ // generate a new uuid:
+ let uuid = util.makeUuid();
+
+ // add the listener to the event map:
+ if (!this._listeners.has(name))
+ this._listeners.set(name, []);
+ this._listeners.get(name).push({uuid, listener});
+
+ return uuid;
+ }
+
+
+ /**
+ * Register a new listener for the given event name, and remove it as soon as the event has been emitted.
+ *
+ * @name module:util.EventEmitter#once
+ * @function
+ * @public
+ * @param {String} name - the name of the event
+ * @param {module:util.EventEmitter~Listener} listener - a listener called upon emission of the event
+ * @return string - the unique identifier associated with that (event, listener) pair (useful to remove the listener)
+ */
+ once(name, listener)
+ {
+ let uuid = this.on(name, listener);
+
+ if (!this._onceUuids.has(name))
+ this._onceUuids.set(name, []);
+ this._onceUuids.get(name).push(uuid);
+
+ return uuid;
+ }
+
+
+ /**
+ * Remove the listener with the given uuid associated to the given event name.
+ *
+ * @name module:util.EventEmitter#off
+ * @function
+ * @public
+ * @param {String} name - the name of the event
+ * @param {module:util.EventEmitter~Listener} listener - a listener called upon emission of the event
+ */
+ off(name, uuid)
+ {
+ let relevantUuidListeners = this._listeners.get(name);
+
+ if (relevantUuidListeners && relevantUuidListeners.length) {
+ this._listeners.set(name, relevantUuidListeners.filter( uuidlistener => (uuidlistener.uuid != uuid) ) );
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Emit an event with a given name and associated data.
+ *
+ * @name module:util.EventEmitter#emit
+ * @function
+ * @public
+ * @param {String} name - the name of the event
+ * @param {object} data - the data of the event
+ * @return {boolean} true if at least one listener has been registered for that event, and false otherwise
+ */
+ emit(name, data)
+ {
+ let relevantUuidListeners = this._listeners.get(name);
+ if (relevantUuidListeners && relevantUuidListeners.length)
+ {
+ let onceUuids = this._onceUuids.get(name);
+ let self = this;
+ relevantUuidListeners.forEach( ({uuid, listener}) => {
+ listener(data);
+
+ if (typeof onceUuids !== 'undefined' && onceUuids.includes(uuid))
+ self.off(name, uuid);
+ });
+ return true;
+ }
+
+ return false;
+ }
+
+
+}
\ No newline at end of file
diff --git a/js/util/Logger.js b/js/util/Logger.js
new file mode 100644
index 0000000..df94555
--- /dev/null
+++ b/js/util/Logger.js
@@ -0,0 +1,104 @@
+/**
+ * Logger
+ *
+ * @author Alain Pitiot
+ * @version 3.0.0b11
+ * @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
+ * @license Distributed under the terms of the MIT License
+ */
+
+
+import * as util from '../util/Util';
+
+
+/**
+ *
This class handles a variety of loggers, e.g. a browser console one (mostly for debugging), a remote one, etc.
PsychoObject is the base class for all PsychoJS objects.
+ * It is responsible for handling attributes.
+ *
+ * @class
+ * @extends EventEmitter
+ * @param {PsychoJS} psychoJS - the PsychoJS instance
+ * @param {string} name - the name of the object (mostly useful for debugging)
+ */
+export class PsychObject extends EventEmitter {
+ constructor(psychoJS, name) {
+ super();
+
+ this._psychoJS = psychoJS;
+
+ // name:
+ if (typeof name === 'undefined')
+ name = this.constructor.name;
+ this._addAttribute('name', name);
+ }
+
+
+ /**
+ * Get the PsychoJS instance.
+ *
+ * @public
+ * @return {PsychoJS} the PsychoJS instance
+ */
+ get psychoJS() { return this._psychoJS; }
+
+
+ /**
+ * Setter for the PsychoJS attribute.
+ *
+ * @public
+ * @param {PsychoJS} psychoJS - the PsychoJS instance
+ */
+ set psychoJS(psychoJS) {
+ this._psychoJS = psychoJS;
+ }
+
+
+ /**
+ * Set the value of an attribute.
+ *
+ * @private
+ * @param {string} attributeName - the name of the attribute
+ * @param {object} attributeValue - the value of the attribute
+ * @param {boolean} [log= false] - whether of not to log
+ * @param {string} [operation] - the binary operation such that the new value of the attribute is the result of the application of the operation to the current value of the attribute and attributeValue
+ * @param {boolean} [stealth= false] - whether or not to call the potential attribute setters when setting the value of this attribute
+ */
+ _setAttribute(attributeName, attributeValue, log = false, operation = undefined, stealth = false) {
+ let 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' };
+ if (typeof attributeValue == 'undefined') {
+ this._psychoJS.logger.warn('setting the value of attribute: ' + attributeName + ' in PsychObject: ' + this._name + ' as: undefined');
+ }
+
+ // (*) apply operation to old and new values:
+ if (typeof operation !== 'undefined' && this.hasOwnProperty('_' + attributeName)) {
+ 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'))) {
+
+ // 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' };
+
+ switch (operation) {
+ case '':
+ // no change to value;
+ break;
+ case '+':
+ attributeValue = attributeValue.map((v, i) => oldValue[i] + v);
+ break;
+ case '*':
+ attributeValue = attributeValue.map((v, i) => oldValue[i] * v);
+ break;
+ case '-':
+ attributeValue = attributeValue.map((v, i) => oldValue[i] - v);
+ break;
+ case '/':
+ attributeValue = attributeValue.map((v, i) => oldValue[i] / v);
+ break;
+ case '**':
+ attributeValue = attributeValue.map((v, i) => oldValue[i] ** v);
+ break;
+ case '%':
+ attributeValue = attributeValue.map((v, i) => oldValue[i] % v);
+ break;
+ default:
+ throw { ...response, error: 'unsupported operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name };
+ }
+
+ } else
+ // old value is a scalar
+ {
+ switch (operation) {
+ case '':
+ // no change to value;
+ break;
+ case '+':
+ attributeValue = attributeValue.map(v => oldValue + v);
+ break;
+ case '*':
+ attributeValue = attributeValue.map(v => oldValue * v);
+ break;
+ case '-':
+ attributeValue = attributeValue.map(v => oldValue - v);
+ break;
+ case '/':
+ attributeValue = attributeValue.map(v => oldValue / v);
+ break;
+ case '**':
+ attributeValue = attributeValue.map(v => oldValue ** v);
+ break;
+ case '%':
+ 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 };
+ }
+ }
+ } else
+ // value is a scalar
+ {
+ // old value is an array
+ if (Array.isArray(oldValue)) {
+ switch (operation) {
+ case '':
+ attributeValue = oldValue.map(v => attributeValue);
+ break;
+ case '+':
+ attributeValue = oldValue.map(v => v + attributeValue);
+ break;
+ case '*':
+ attributeValue = oldValue.map(v => v * attributeValue);
+ break;
+ case '-':
+ attributeValue = oldValue.map(v => v - attributeValue);
+ break;
+ case '/':
+ attributeValue = oldValue.map(v => v / attributeValue);
+ break;
+ case '**':
+ attributeValue = oldValue.map(v => v ** attributeValue);
+ break;
+ case '%':
+ attributeValue = oldValue.map(v => v % attributeValue);
+ break;
+ default:
+ throw { ...response, error: 'unsupported operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name };
+ }
+
+ } else
+ // old value is a scalar
+ {
+ switch (operation) {
+ case '':
+ // no change to value;
+ break;
+ case '+':
+ attributeValue = oldValue + attributeValue;
+ break;
+ case '*':
+ attributeValue = oldValue * attributeValue;
+ break;
+ case '-':
+ attributeValue = oldValue - attributeValue;
+ break;
+ case '/':
+ attributeValue = oldValue / attributeValue;
+ break;
+ case '**':
+ attributeValue = oldValue ** attributeValue;
+ break;
+ case '%':
+ attributeValue = oldValue % attributeValue;
+ break;
+ default:
+ throw { ...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) };
+ }
+
+
+ // (*) log if appropriate:
+ if (!stealth && (log || this._autoLog) && (typeof this.win !== 'undefined')) {
+ var message = this.name + ": " + attributeName + " = " + JSON.stringify(attributeValue);
+ //this.win.logOnFlip(message, psychoJS.logging.EXP, this);
+ }
+
+
+ // (*) set the value of the attribute:
+ if (stealth)
+ this['_' + attributeName] = attributeValue;
+ else
+ this[attributeName] = attributeValue;
+ }
+
+
+ /**
+ * Add attributes to this instance (e.g. define setters and getters) and affect values to them.
+ *
+ *
Note: (a) If the object already has a set method, we do not redefine it,
+ * and the setter for this attribute calls that method instead of _setAttribute.
+ *
(b) _addAttributes is typically called in the constructor of an object, after
+ * the call to super (see module:visual.ImageStim for an illustration).
+ *
+ * @private
+ * @param {Object} cls - the class object of the subclass of PsychoObject whose attributes we will set
+ * @param {...*} [args] - the values for the attributes (this also determines which attributes will be set)
+ *
+ */
+ _addAttributes(cls, ...args) {
+ // (*) look for the line in the subclass constructor where addAttributes is called
+ // and extract its arguments:
+ let callLine = cls.toString().match(/this.*\._addAttributes\(.*\;/)[0];
+ let startIndex = callLine.indexOf('._addAttributes(') + 16;
+ let endIndex = callLine.indexOf(');');
+ let callArgs = callLine.substr(startIndex, endIndex - startIndex).split(',').map((s) => s.trim());
+
+
+ // (*) add (argument name, argument value) pairs to the attribute map:
+ let attributeMap = new Map();
+ for (var i = 1; i < callArgs.length; ++i)
+ attributeMap.set(callArgs[i], args[i - 1]);
+
+ // (*) set the value, define the get/set properties and define the getter and setter:
+ for (let [name, value] of attributeMap.entries())
+ this._addAttribute(name, value);
+ }
+
+
+ /**
+ * Add an attribute to this instance (e.g. define setters and getters) and affect a value to it.
+ *
+ * @private
+ * @param {string} name - the name of the attribute
+ * @param {object} value - the value of the attribute
+ */
+ _addAttribute(name, value) {
+ let getPropertyName = 'get' + name[0].toUpperCase() + name.substr(1);
+ if (typeof this[getPropertyName] === 'undefined')
+ this[getPropertyName] = () => this['_' + name];
+
+ let setPropertyName = 'set' + name[0].toUpperCase() + name.substr(1);
+ if (typeof this[setPropertyName] === 'undefined')
+ this[setPropertyName] = (value, log = false) => {
+ this._setAttribute(name, value, log);
+ };
+
+ Object.defineProperty(this, name, {
+ configurable: true,
+ get() { return this[getPropertyName](); /* return this['_' + name];*/ },
+ set(value) { this[setPropertyName](value); }
+ });
+
+ //this['_' + name] = value;
+ this[name] = value;
+ }
+
+}
\ No newline at end of file
diff --git a/js/util/Scheduler.js b/js/util/Scheduler.js
new file mode 100644
index 0000000..5fc14f9
--- /dev/null
+++ b/js/util/Scheduler.js
@@ -0,0 +1,198 @@
+/**
+ * Scheduler.
+ *
+ * @author Alain Pitiot
+ * @version 3.0.0b11
+ * @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
+ * @license Distributed under the terms of the MIT License
+ */
+
+
+/**
+ *
A scheduler helps run the main loop by managing scheduled functions,
+ * called tasks, after each frame is displayed.
+ *
+ *
+ * Tasks are either another [Scheduler]{@link module:util.Scheduler}, or a
+ * javascript functions returning one of the following codes:
+ *
+ *
Scheduler.Event.NEXT:
+ *
Scheduler.Event.FLIP_REPEAT:
+ *
Scheduler.Event.FLIP_NEXT:
+ *
Scheduler.Event.QUIT:
+ *
+ *
+ *
+ *
It is possible to create sub-schedulers, e.g. to handle loops.
+ * Sub-schedulers are added to a parent scheduler as a normal
+ * task would be by calling [scheduler.add(subScheduler)]{@link module:util.Scheduler#add}.
+ *
+ *
Conditional branching is also available:
+ * [scheduler.addConditionalBranches]{@link module:util.Scheduler#addConditionalBranches}
+ *
+ *
+ * @name module:util.Scheduler
+ * @class
+ * @param {PsychoJS} psychoJS - the PsychoJS instance
+ *
+ */
+export class Scheduler {
+
+
+ constructor(psychoJS) {
+ this._psychoJS = psychoJS;
+
+ this._taskList = [];
+ this._currentTask = undefined;
+ this._argsList = [];
+ this._currentArgs = undefined;
+
+ this._stopAtNextUpdate = false;
+ }
+
+
+ /**
+ * Schedule a task.
+ *
+ * @name module:util.Scheduler#add
+ * @public
+ * @param task - the task to be scheduled
+ * @param args - arguments for that task
+ */
+ add(task, args) {
+ this._taskList.push(task);
+ this._argsList.push(args);
+ }
+
+
+ /**
+ * Condition evaluated when the task is run.
+ *
+ * @callback module:util.Scheduler~Condition
+ * @return {boolean}
+ */
+ /**
+ * Schedule a series of task or another, based on a condition.
+ *
+ *
Note: the tasks are [sub-schedulers]{@link module:util.Scheduler}.
+ *
+ * @name module:util.Scheduler#addConditional
+ * @public
+ * @param {module:util.Scheduler~Condition} condition - the condition
+ * @param {module:util.Scheduler} thenScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is satisfied
+ * @param {module:util.Scheduler} elseScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is not satisfied
+ */
+ addConditional(condition, thenScheduler, elseScheduler) {
+ let self = this;
+ let task = function () {
+ if (condition())
+ self.add(thenScheduler);
+ else
+ self.add(elseScheduler)
+
+ return Scheduler.Event.NEXT;
+ };
+
+ this.add(task);
+ }
+
+
+
+ /**
+ * Run the next scheduled tasks in sequence until one of them returns something other than module:util.Scheduler#Event.NEXT.
+ *
+ * @name module:util.Scheduler#run
+ * @public
+ * @return {module:util.Schedule#Event} the state of the scheduler after the task ran
+ */
+ run() {
+ let state = Scheduler.Event.NEXT;
+
+ while (state === Scheduler.Event.NEXT) {
+ if (typeof this._currentTask == 'undefined') {
+ if (this._taskList.length > 0) {
+ this._currentTask = this._taskList.shift();
+ this._currentArgs = this._argsList.shift();
+ }
+ else {
+ this._currentTask = undefined;
+ return Scheduler.Event.QUIT;
+ }
+ }
+ if (this._currentTask instanceof Function) {
+ state = this._currentTask(this._currentArgs);
+ }
+ // if currentTask is not a function, it can only be another scheduler:
+ else {
+ state = this._currentTask.run();
+ if (state === Scheduler.Event.QUIT) state = Scheduler.Event.NEXT;
+ }
+
+ if (state != Scheduler.Event.FLIP_REPEAT) {
+ this._currentTask = undefined;
+ this._currentArgs = undefined;
+ }
+ }
+
+ return state;
+ }
+
+
+ /**
+ * Start this scheduler.
+ *
+ * @name module:util.Scheduler#start
+ * @public
+ *
Note: tasks are run after each animation frame.
+ */
+ start() {
+ let self = this;
+ let update = () => {
+ // stop the animation is need be:
+ if (self._stopAtNextUpdate) return;
+
+ // self._psychoJS.window._writeLogOnFlip();
+
+ // run the next task:
+ let state = self.run();
+ if (state === Scheduler.Event.QUIT)
+ return;
+
+ // render the scene in the window:
+ self._psychoJS.window.render();
+
+ // request a new frame:
+ requestAnimationFrame(update);
+ }
+
+ // start the animation:
+ requestAnimationFrame(update);
+ }
+
+
+ /**
+ * Stop this scheduler at the next update.
+ *
+ * @name module:util.Scheduler#stop
+ * @public
+ */
+ stop() {
+ this._stopAtNextUpdate = true;
+ }
+}
+
+
+/**
+ * Events.
+ *
+ * @name module:util.Schedule#Event
+ * @enum {Symbol}
+ * @readonly
+ * @public
+ */
+Scheduler.Event = {
+ NEXT: Symbol.for('NEXT'),
+ FLIP_REPEAT: Symbol.for('FLIP_REPEAT'),
+ FLIP_NEXT: Symbol.for('FLIP_NEXT'),
+ QUIT: Symbol.for('QUIT')
+};
\ No newline at end of file
diff --git a/js/visual/BaseShapeStim.js b/js/visual/BaseShapeStim.js
index cc7a343..b87bf5d 100644
--- a/js/visual/BaseShapeStim.js
+++ b/js/visual/BaseShapeStim.js
@@ -1,6 +1,6 @@
/** @module visual */
/**
- * @file Basic Shape Stimulus.
+ * Basic Shape Stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
diff --git a/js/visual/BaseVisualStim.js b/js/visual/BaseVisualStim.js
index b00ffd1..44cc52e 100644
--- a/js/visual/BaseVisualStim.js
+++ b/js/visual/BaseVisualStim.js
@@ -1,5 +1,5 @@
/**
- * @file Base class for all visual stimuli.
+ * Base class for all visual stimuli.
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -160,4 +160,4 @@ export class BaseVisualStim extends util.mix(MinimalStim).with(WindowMixin)
return this._vertices_px;
}
-}
\ No newline at end of file
+}
diff --git a/js/visual/ImageStim.js b/js/visual/ImageStim.js
index 6d37625..db26a90 100644
--- a/js/visual/ImageStim.js
+++ b/js/visual/ImageStim.js
@@ -1,5 +1,5 @@
/**
- * @file Image Stimulus.
+ * Image Stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
diff --git a/js/visual/Rect.js b/js/visual/Rect.js
index 7fabfe2..9b09d46 100644
--- a/js/visual/Rect.js
+++ b/js/visual/Rect.js
@@ -1,5 +1,5 @@
/**
- * @file Rectangular Stimulus.
+ * Rectangular Stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -119,4 +119,4 @@ export class Rect extends BaseShapeStim {
]);
}
-}
\ No newline at end of file
+}
diff --git a/js/visual/TextStim.js b/js/visual/TextStim.js
index 1b7a88b..982211a 100644
--- a/js/visual/TextStim.js
+++ b/js/visual/TextStim.js
@@ -1,5 +1,5 @@
/**
- * @file Text Stimulus.
+ * Text Stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
@@ -240,7 +240,7 @@ export class TextStim extends util.mix(BaseVisualStim).with(ColorMixin)
this._heightPix = this._getLengthPix(height);
var fontSize = Math.round(this._heightPix);
- let color = this._getDesiredColor(this._color, this._contrast);
+ let color = this.getContrastedColor(this._color, this._contrast);
var font =
(this._bold ? 'bold ' : '') +
(this._italic ? 'italic ' : '') +