mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 18:50:54 +00:00
added util module
This commit is contained in:
parent
9c859abe65
commit
10e2bc0650
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Manager handling the keyboard and mouse/touch events.
|
* Manager handling the keyboard and mouse/touch events.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
@ -207,6 +207,14 @@ export class EventManager {
|
|||||||
|
|
||||||
const view = renderer.view;
|
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 => {
|
view.addEventListener("pointerdown", event => {
|
||||||
self._mouseInfo.buttons.pressed[event.button] = 1;
|
self._mouseInfo.buttons.pressed[event.button] = 1;
|
||||||
self._mouseInfo.buttons.times[event.button] = self._psychoJS._monotonicClock.getTime() - self._mouseInfo.buttons.clocks[event.button].getLastResetTime();
|
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 => {
|
view.addEventListener("wheel", event => {
|
||||||
self._mouseInfo.wheelRel[0] += event.deltaX;
|
self._mouseInfo.wheelRel[0] += event.deltaX;
|
||||||
self._mouseInfo.wheelRel[1] += event.deltaY;
|
self._mouseInfo.wheelRel[1] += event.deltaY;
|
||||||
/*
|
|
||||||
var x = ev.offsetX;
|
//var x = ev.offsetX;
|
||||||
var y = ev.offsetY;
|
//var y = ev.offsetY;
|
||||||
var msg = "Mouse: wheel shift=(" + ev.deltaX + "," + ev.deltaY + "), pos=(" + x + "," + y + ")";
|
//var msg = "Mouse: wheel shift=(" + ev.deltaX + "," + ev.deltaY + "), pos=(" + x + "," + y + ")";
|
||||||
psychoJS.logging.data(msg);*/
|
//psychoJS.logging.data(msg);
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Graphic User Interface
|
* Graphic User Interface
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Base class for all stimuli.
|
* Base class for all stimuli.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -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
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @module core */
|
/** @module core */
|
||||||
/**
|
/**
|
||||||
* @file Main component of the PsychoJS library.
|
* Main component of the PsychoJS library.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
@ -217,7 +217,7 @@ export class PsychoJS {
|
|||||||
|
|
||||||
// setup the logger:
|
// setup the logger:
|
||||||
//my.logger.console.setLevel(psychoJS.logging.WARNING);
|
//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:
|
// open a new session:
|
||||||
await this._serverManager.openSession();
|
await this._serverManager.openSession();
|
||||||
@ -277,7 +277,7 @@ export class PsychoJS {
|
|||||||
await this._experiment.save();
|
await this._experiment.save();
|
||||||
|
|
||||||
// close the session:
|
// close the session:
|
||||||
await this._serverManager.closeSession();
|
await this._serverManager.closeSession(isCompleted);
|
||||||
|
|
||||||
// stop the main scheduler:
|
// stop the main scheduler:
|
||||||
this._scheduler.stop();
|
this._scheduler.stop();
|
||||||
@ -344,6 +344,12 @@ export class PsychoJS {
|
|||||||
if (!('URL' in this._config.psychoJsManager))
|
if (!('URL' in this._config.psychoJsManager))
|
||||||
throw 'missing URL in psychoJsManager block in configuration';
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
@ -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
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
@ -106,11 +106,16 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
this.setStatus(ServerManager.Status.BUSY);
|
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;
|
let self = this;
|
||||||
return new Promise((resolve, reject) => {
|
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')
|
$.post(this._psychoJS.config.psychoJsManager.URL + '?command=open_session', data, null, 'json')
|
||||||
.done((data, textStatus) => {
|
.done((data, textStatus) => {
|
||||||
// check for error:
|
// check for error:
|
||||||
@ -150,21 +155,28 @@ export class ServerManager extends PsychObject {
|
|||||||
* @name module:core.ServerManager#closeSession
|
* @name module:core.ServerManager#closeSession
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
|
* @param {boolean} [isCompleted= false] - whether or not the experiment was completed
|
||||||
* @returns {Promise<ServerManager.CloseSessionPromise>} the response
|
* @returns {Promise<ServerManager.CloseSessionPromise>} the response
|
||||||
*/
|
*/
|
||||||
closeSession() {
|
closeSession(isCompleted = false) {
|
||||||
let response = { origin: 'ServerManager.closeSession', context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.name };
|
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._psychoJS.logger.debug('closing the session for experiment: ' + this._psychoJS.config.experiment.name);
|
||||||
|
|
||||||
this.setStatus(ServerManager.Status.BUSY);
|
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;
|
let self = this;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const data = {
|
|
||||||
experimentFullPath: self._psychoJS.config.experiment.fullpath,
|
|
||||||
'token': self._psychoJS.config.experiment.token
|
|
||||||
};
|
|
||||||
$.post(this._psychoJS.config.psychoJsManager.URL + '?command=close_session', data, null, 'json')
|
$.post(this._psychoJS.config.psychoJsManager.URL + '?command=close_session', data, null, 'json')
|
||||||
.done((data, textStatus) => {
|
.done((data, textStatus) => {
|
||||||
// check for error:
|
// check for error:
|
||||||
@ -283,8 +295,6 @@ export class ServerManager extends PsychObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef ServerManager.UploadDataPromise
|
* @typedef ServerManager.UploadDataPromise
|
||||||
* @property {string} origin the calling method
|
* @property {string} origin the calling method
|
||||||
@ -297,8 +307,8 @@ export class ServerManager extends PsychObject {
|
|||||||
* @name module:core.ServerManager#uploadData
|
* @name module:core.ServerManager#uploadData
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {string} key the data key
|
* @param {string} key - the data key (e.g. the name of .csv file)
|
||||||
* @param {*} value the data value
|
* @param {string} value - the data value (e.g. a string containing the .csv header and records)
|
||||||
*
|
*
|
||||||
* @returns {Promise<ServerManager.UploadDataPromise>} the response
|
* @returns {Promise<ServerManager.UploadDataPromise>} the response
|
||||||
*/
|
*/
|
||||||
@ -312,14 +322,14 @@ export class ServerManager extends PsychObject {
|
|||||||
experimentFullPath: this._psychoJS.config.experiment.fullpath,
|
experimentFullPath: this._psychoJS.config.experiment.fullpath,
|
||||||
token: this._psychoJS.config.experiment.token,
|
token: this._psychoJS.config.experiment.token,
|
||||||
key,
|
key,
|
||||||
value
|
value,
|
||||||
|
saveFormat: Symbol.keyFor(this._psychoJS.config.experiment.saveFormat)
|
||||||
};
|
};
|
||||||
// add gitlab ID of experiment if there is one:
|
// add gitlab ID of experiment if there is one:
|
||||||
const gitlabConfig = this._psychoJS.config.gitlab;
|
const gitlabConfig = this._psychoJS.config.gitlab;
|
||||||
if (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined')
|
if (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined')
|
||||||
data.projectId = gitlabConfig.projectId;
|
data.projectId = gitlabConfig.projectId;
|
||||||
|
|
||||||
|
|
||||||
// (*) upload data:
|
// (*) upload data:
|
||||||
const self = this;
|
const self = this;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Window responsible for displaying the experiment stimuli
|
* Window responsible for displaying the experiment stimuli
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
@ -18,6 +18,7 @@ import * as util from '../util/Util';
|
|||||||
*
|
*
|
||||||
* @name module:core.Window
|
* @name module:core.Window
|
||||||
* @class
|
* @class
|
||||||
|
* @extends PsychObject
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
* @param {string} [options.name] the name of the window
|
* @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 {Color} [options.color= Color('black')] the background color of the window
|
||||||
* @param {string} [options.units= 'pix'] the units of the window
|
* @param {string} [options.units= 'pix'] the units of the window
|
||||||
* @param {boolean} [options.autoLog= true] whether or not to log
|
* @param {boolean} [options.autoLog= true] whether or not to log
|
||||||
*
|
|
||||||
* @extends PsychObject
|
|
||||||
*/
|
*/
|
||||||
export class Window extends PsychObject {
|
export class Window extends PsychObject {
|
||||||
|
|
||||||
@ -262,6 +261,7 @@ export class Window extends PsychObject {
|
|||||||
|
|
||||||
// top-level container:
|
// top-level container:
|
||||||
this._rootContainer = new PIXI.Container();
|
this._rootContainer = new PIXI.Container();
|
||||||
|
this._rootContainer.interactive = true;
|
||||||
|
|
||||||
// set size of renderer and position of root container:
|
// set size of renderer and position of root container:
|
||||||
this._onResize(this);
|
this._onResize(this);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Mixin implementing various unit-handling measurement methods.
|
* Mixin implementing various unit-handling measurement methods.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
@ -19,10 +19,8 @@
|
|||||||
* @mixin
|
* @mixin
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export let WindowMixin = (superclass) => class extends superclass
|
export let WindowMixin = (superclass) => class extends superclass {
|
||||||
{
|
constructor(args) {
|
||||||
constructor(args)
|
|
||||||
{
|
|
||||||
super(args);
|
super(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,8 +34,7 @@ export let WindowMixin = (superclass) => class extends superclass
|
|||||||
* @param {String} [units= this.win.units] - the units
|
* @param {String} [units= this.win.units] - the units
|
||||||
* @param {boolean} [log= false] - whether or not to log
|
* @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);
|
this._setAttribute('units', units, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +48,7 @@ export let WindowMixin = (superclass) => class extends superclass
|
|||||||
* @param {number} length - the length in stimulus units
|
* @param {number} length - the length in stimulus units
|
||||||
* @return {number} - the length in pixel 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' };
|
let errorPrefix = { origin: 'WindowMixin._getLengthPix', context: 'when converting a length from stimulus unit to pixel units' };
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix') {
|
||||||
@ -81,8 +77,7 @@ export let WindowMixin = (superclass) => class extends superclass
|
|||||||
* @param {number} length_px - the length in pixel units
|
* @param {number} length_px - the length in pixel units
|
||||||
* @return {number} - the length in stimulus 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' };
|
let errorPrefix = { origin: 'WindowMixin._getLengthUnits', context: 'when converting a length from pixel unit to stimulus units' };
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix') {
|
||||||
@ -111,8 +106,7 @@ export let WindowMixin = (superclass) => class extends superclass
|
|||||||
* @param {number} length_px - the length in pixel units
|
* @param {number} length_px - the length in pixel units
|
||||||
* @return {number} - the length in stimulus 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' };
|
let errorPrefix = { origin: 'WindowMixin._getHorLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix') {
|
||||||
@ -140,8 +134,7 @@ export let WindowMixin = (superclass) => class extends superclass
|
|||||||
* @param {number} length_px - the length in pixel units
|
* @param {number} length_px - the length in pixel units
|
||||||
* @return {number} - the length in stimulus 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' };
|
let errorPrefix = { origin: 'WindowMixin._getVerLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix') {
|
||||||
@ -160,5 +153,4 @@ export let WindowMixin = (superclass) => class extends superclass
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Experiment Handler
|
* Experiment Handler
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
@ -159,57 +159,84 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {PsychoJS} options.attributes - the attributes to be saved
|
* @param {PsychoJS} options.attributes - the attributes to be saved
|
||||||
*
|
|
||||||
* @todo deal with attributes
|
|
||||||
*/
|
*/
|
||||||
async save({
|
async save({
|
||||||
attributes = []
|
attributes = []
|
||||||
} = {}) {
|
} = {}) {
|
||||||
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
|
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
|
||||||
|
|
||||||
// key is based on extraInfo:
|
// (*) get attributes:
|
||||||
const info = this.extraInfo;
|
if (attributes.length == 0) {
|
||||||
let key = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name;
|
attributes = this._trialsKeys.slice();
|
||||||
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());
|
|
||||||
|
|
||||||
// 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++) {
|
for (let l = 0; l < this._loops.length; l++) {
|
||||||
const loop = this._loops[l];
|
const loop = this._loops[l];
|
||||||
|
|
||||||
const loopAttributes = this.getLoopAttributes(loop);
|
const loopAttributes = this.getLoopAttributes(loop);
|
||||||
for (let a in loopAttributes)
|
for (let a in loopAttributes)
|
||||||
if (loopAttributes.hasOwnProperty(a))
|
if (loopAttributes.hasOwnProperty(a))
|
||||||
header.push(a);
|
attributes.push(a);
|
||||||
}
|
}
|
||||||
for (let a in this.extraInfo) {
|
for (let a in this.extraInfo) {
|
||||||
if (this.extraInfo.hasOwnProperty(a))
|
if (this.extraInfo.hasOwnProperty(a))
|
||||||
header.push(a);
|
attributes.push(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let h = 0; h < header.length; h++) {
|
|
||||||
|
// (*) get various experiment info:
|
||||||
|
const info = this.extraInfo;
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
// (*) save to a .csv file on the remote server:
|
||||||
|
if (this._psychoJS.config.experiment.saveFormat == ExperimentHandler.SaveFormat.CSV) {
|
||||||
|
let csv = "";
|
||||||
|
|
||||||
|
// build the csv header:
|
||||||
|
for (let h = 0; h < attributes.length; h++) {
|
||||||
if (h > 0)
|
if (h > 0)
|
||||||
csv = csv + ', ';
|
csv = csv + ', ';
|
||||||
csv = csv + header[h];
|
csv = csv + attributes[h];
|
||||||
}
|
}
|
||||||
csv = csv + '\n';
|
csv = csv + '\n';
|
||||||
|
|
||||||
// build the records:
|
// build the records:
|
||||||
for (let r = 0; r < this._trialsData.length; r++) {
|
for (let r = 0; r < this._trialsData.length; r++) {
|
||||||
for (let h = 0; h < header.length; h++) {
|
for (let h = 0; h < attributes.length; h++) {
|
||||||
if (h > 0)
|
if (h > 0)
|
||||||
csv = csv + ', ';
|
csv = csv + ', ';
|
||||||
csv = csv + this._trialsData[r][header[h]];
|
csv = csv + this._trialsData[r][attributes[h]];
|
||||||
}
|
}
|
||||||
csv = csv + '\n';
|
csv = csv + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload data to the remote PsychoJS manager:
|
// upload data to the remote PsychoJS manager:
|
||||||
return await this._psychoJS.serverManager.uploadData(key + '.csv', csv);
|
const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
|
||||||
|
return await this._psychoJS.serverManager.uploadData(key, 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;
|
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')
|
||||||
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @module data */
|
/** @module data */
|
||||||
/**
|
/**
|
||||||
* @file Trial Handler
|
* Trial Handler
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @module sound */
|
/** @module sound */
|
||||||
/**
|
/**
|
||||||
* @file Sound stimulus.
|
* Sound stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Sound player interface
|
* Sound player interface
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Tone Player.
|
* Tone Player.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Track Player.
|
* Track Player.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
@ -136,7 +136,7 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
* @name module:sound.TrackPlayer#play
|
* @name module:sound.TrackPlayer#play
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @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) {
|
play(loops) {
|
||||||
if (typeof loops !== 'undefined')
|
if (typeof loops !== 'undefined')
|
||||||
|
202
js/util/Clock.js
Normal file
202
js/util/Clock.js
Normal file
@ -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
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <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>
|
||||||
|
*
|
||||||
|
* @name module:util.MonotonicClock
|
||||||
|
* @class
|
||||||
|
* @param {number} [startTime= <time elapsed since the reference point, i.e. the time when the module was loaded>] - the clock's start time (in ms)
|
||||||
|
*/
|
||||||
|
export class MonotonicClock {
|
||||||
|
constructor(startTime = MonotonicClock.getReferenceTime()) {
|
||||||
|
this._timeAtLastReset = startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current time on this clock.
|
||||||
|
*
|
||||||
|
* @name module:util.MonotonicClock#getTime
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @return {number} the current time (in seconds)
|
||||||
|
*/
|
||||||
|
getTime() {
|
||||||
|
return MonotonicClock.getReferenceTime() - this._timeAtLastReset;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current offset being applied to the high resolution timebase used by this Clock.
|
||||||
|
*
|
||||||
|
* @name module:util.MonotonicClock#getLastResetTime
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @return {number} the offset (in ms)
|
||||||
|
*/
|
||||||
|
getLastResetTime() {
|
||||||
|
return this._timeAtLastReset;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time elapsed since the reference point.
|
||||||
|
*
|
||||||
|
* @name module:util.MonotonicClock#getReferenceTime
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @return {number} the time elapsed since the reference point (in ms)
|
||||||
|
*/
|
||||||
|
static getReferenceTime() {
|
||||||
|
return (new Date().getTime() - MonotonicClock._referenceTime) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the clock's current time as a formatted string.
|
||||||
|
*
|
||||||
|
* <p>Note: this is mostly used as an appendix to the name of the keys save to the server.</p>
|
||||||
|
*
|
||||||
|
* @name module:util.MonotonicClock.getDateStr
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @static
|
||||||
|
* @param {string} [format= 'YYYY-MM-DD_HH[h]mm.ss.SSS'] - the format for the string (see [momentjs.com]{@link https://momentjs.com/docs/#/parsing/string-format/} for details)
|
||||||
|
* @return {string} a string representing the current time in the given format
|
||||||
|
*/
|
||||||
|
static getDateStr(format = 'YYYY-MM-DD_HH[h]mm.ss.SSS') {
|
||||||
|
return moment().format(format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The clock's referenceTime is the time when the module was loaded.
|
||||||
|
*
|
||||||
|
* @name module:util.MonotonicClock._referenceTime
|
||||||
|
* @readonly
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
MonotonicClock._referenceTime = new Date().getTime();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Clock is a MonotonicClock that also offers the possibility of being reset.</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Clock
|
||||||
|
* @class
|
||||||
|
* @extends MonotonicClock
|
||||||
|
*/
|
||||||
|
export class Clock extends MonotonicClock {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the time on the clock.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @name module:util.Clock#reset
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @param {number} [newTime= 0] the new time on the clock.
|
||||||
|
*/
|
||||||
|
reset(newTime = 0) {
|
||||||
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add more time to the clock's 'start' time (t0).
|
||||||
|
*
|
||||||
|
* <p>Note: by adding time to t0, you push the current time forward (make it
|
||||||
|
* smaller). As a consequence, getTime() may return a negative number.</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Clock#add
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||||
|
*/
|
||||||
|
add(deltaTime) {
|
||||||
|
this._timeAtLastReset += deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>CountdownTimer is a clock counts down from the time of last reset.</p.
|
||||||
|
*
|
||||||
|
* @name module:util.CountdownTimer
|
||||||
|
* @class
|
||||||
|
* @extends Clock
|
||||||
|
* @param {number} [startTime= 0] - the start time of the countdown
|
||||||
|
*/
|
||||||
|
export class CountdownTimer extends Clock {
|
||||||
|
constructor(startTime = 0) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._timeAtLastReset = MonotonicClock.getReferenceTime()
|
||||||
|
this._countdown_duration = startTime;
|
||||||
|
if (startTime) {
|
||||||
|
this.add(startTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add more time to the clock's 'start' time (t0).
|
||||||
|
*
|
||||||
|
* <p>Note: by adding time to t0, you push the current time forward (make it
|
||||||
|
* smaller). As a consequence, getTime() may return a negative number.</p>
|
||||||
|
*
|
||||||
|
* @name module:util.CountdownTimer#add
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||||
|
*/
|
||||||
|
add(deltaTime) {
|
||||||
|
this._timeAtLastReset += deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the time on the countdown.
|
||||||
|
*
|
||||||
|
* @name module:util.CountdownTimer#reset
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @param {number | undefined} t - if t is undefined, the coundown time is reset to zero, otherwise we set it to t
|
||||||
|
*/
|
||||||
|
reset(t) {
|
||||||
|
if (typeof t == 'undefined') {
|
||||||
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + this._countdown_duration;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._countdown_duration = t;
|
||||||
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time currently left on the countdown.
|
||||||
|
*
|
||||||
|
* @name module:util.CountdownTimer#getTime
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @return {number} the time left on the countdown (in seconds)
|
||||||
|
*/
|
||||||
|
getTime() {
|
||||||
|
return this._timeAtLastReset - MonotonicClock.getReferenceTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
613
js/util/Color.js
Normal file
613
js/util/Color.js
Normal file
@ -0,0 +1,613 @@
|
|||||||
|
/**
|
||||||
|
* Color management.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>This class handles multiple color spaces, and offers various
|
||||||
|
* static methods for converting colors from one space to another.</p>
|
||||||
|
*
|
||||||
|
* <p>The constructor accepts the following color representations:
|
||||||
|
* <ul>
|
||||||
|
* <li>a named color, e.g. 'aliceblue' (the colorspace must be RGB)</li>
|
||||||
|
* <li>an hexadecimal string representation, e.g. '#FF0000' (the colorspace must be RGB)</li>
|
||||||
|
* <li>an hexadecimal number representation, e.g. 0xFF0000 (the colorspace must be RGB)</li>
|
||||||
|
* <li>a triplet of numbers, e.g. [-1, 0, 1], [0, 128, 255] (the numbers must be within the range determined by the colorspace)</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>Note: internally, the Color is represented as a [r,g,b] triplet with r,g,b in [0,1]</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Color
|
||||||
|
* @class
|
||||||
|
* @param {string|number|Array.<number>|undefined} [obj= 'black'] - an object representing a color
|
||||||
|
* @param {module:util.Color#COLOR_SPACE|undefined} [colorspace=Color.COLOR_SPACE.RGB] - the colorspace of that color
|
||||||
|
*
|
||||||
|
* @todo implement HSV, DKL, and LMS colorspaces
|
||||||
|
*/
|
||||||
|
export class Color {
|
||||||
|
|
||||||
|
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB) {
|
||||||
|
let response = { origin: 'Color', context: 'when defining a color' };
|
||||||
|
|
||||||
|
// named color (e.g. 'seagreen') or string hexadecimal representation (e.g. '#FF0000'):
|
||||||
|
// 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' };
|
||||||
|
|
||||||
|
// hexademical representation:
|
||||||
|
if (obj[0] == '#') {
|
||||||
|
this._hex = obj;
|
||||||
|
}
|
||||||
|
// named color:
|
||||||
|
else {
|
||||||
|
if (!(obj in Color.NAMED_COLORS))
|
||||||
|
throw { ...response, error: 'unknown named color: ' + obj };
|
||||||
|
|
||||||
|
this._hex = Color.NAMED_COLORS[obj];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._rgb = Color.hexToRgb(this._hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hexadecimal number representation (e.g. 0xFF0000)
|
||||||
|
// 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' };
|
||||||
|
|
||||||
|
this._rgb = Color._intToRgb(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// array of numbers:
|
||||||
|
else if (Array.isArray(obj)) {
|
||||||
|
Color._checkTypeAndRange(obj);
|
||||||
|
let [a, b, c] = obj;
|
||||||
|
|
||||||
|
// check range and convert to [0,1]:
|
||||||
|
if (colorspace != Color.COLOR_SPACE.RGB255) {
|
||||||
|
Color._checkTypeAndRange(obj, [-1, 1]);
|
||||||
|
|
||||||
|
a = (a + 1.0) / 2.0;
|
||||||
|
b = (b + 1.0) / 2.0;
|
||||||
|
c = (c + 1.0) / 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get RGB components:
|
||||||
|
switch (colorspace) {
|
||||||
|
case Color.COLOR_SPACE.RGB255:
|
||||||
|
Color._checkTypeAndRange(obj, [0, 255]);
|
||||||
|
this._rgb = [a / 255.0, b / 255.0, c / 255.0];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Color.COLOR_SPACE.RGB:
|
||||||
|
this._rgb = [a, b, c];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Color.COLOR_SPACE.HSV:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Color.COLOR_SPACE.DKL:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Color.COLOR_SPACE.LMS:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw { ...response, error: 'unknown colorspace: ' + colorspace };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [0,1] RGB triplet equivalent of this Color.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.rgb
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
||||||
|
*/
|
||||||
|
get rgb() { return this._rgb; }
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [0,255] RGB triplet equivalent of this Color.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.rgb255
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
||||||
|
*/
|
||||||
|
get rgb255() { return [Math.round(this._rgb[0] * 255.0), Math.round(this._rgb[1] * 255.0), Math.round(this._rgb[2] * 255.0)]; }
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hexadecimal color code equivalent of this Color.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.hex
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @return {string} the hexadecimal color code equivalent
|
||||||
|
*/
|
||||||
|
get hex() {
|
||||||
|
if (typeof this._hex === 'undefined')
|
||||||
|
this._hex = Color._rgbToHex(this._rgb);
|
||||||
|
return this._hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the integer code equivalent of this Color.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.int
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @return {number} the integer code equivalent
|
||||||
|
*/
|
||||||
|
get int() {
|
||||||
|
if (typeof this._int === 'undefined')
|
||||||
|
this._int = Color._rgbToInt(this._rgb);
|
||||||
|
return this._int;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
get hsv() {
|
||||||
|
if (typeof this._hsv === 'undefined')
|
||||||
|
this._hsv = Color._rgbToHsv(this._rgb);
|
||||||
|
return this._hsv;
|
||||||
|
}
|
||||||
|
get dkl() {
|
||||||
|
if (typeof this._dkl === 'undefined')
|
||||||
|
this._dkl = Color._rgbToDkl(this._rgb);
|
||||||
|
return this._dkl;
|
||||||
|
}
|
||||||
|
get lms() {
|
||||||
|
if (typeof this._lms === 'undefined')
|
||||||
|
this._lms = Color._rgbToLms(this._rgb);
|
||||||
|
return this._lms;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [0,255] RGB triplet equivalent of the hexadecimal color code.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.hexToRgb255
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @public
|
||||||
|
* @param {string} hex - the hexadecimal color code
|
||||||
|
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
||||||
|
*/
|
||||||
|
static hexToRgb255(hex) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
if (result == null)
|
||||||
|
throw { origin: 'Color.hexToRgb255', context: 'when converting an hexadecimal color code to its 255- or [0,1]-based RGB color representation', error: 'unable to parse the argument: wrong type or wrong code' };
|
||||||
|
|
||||||
|
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [0,1] RGB triplet equivalent of the hexadecimal color code.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.hexToRgb
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @public
|
||||||
|
* @param {string} hex - the hexadecimal color code
|
||||||
|
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
||||||
|
*/
|
||||||
|
static hexToRgb(hex) {
|
||||||
|
const [r255, g255, b255] = Color.hexToRgb255(hex);
|
||||||
|
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.rgb255ToHex
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @public
|
||||||
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
|
* @return {string} the hexadecimal color code equivalent
|
||||||
|
*/
|
||||||
|
static rgb255ToHex(rgb255) {
|
||||||
|
const response = { origin : 'Color.rgb255ToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
Color._checkTypeAndRange(rgb255, [0, 255]);
|
||||||
|
return Color._rgb255ToHex(rgb255);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw { ...response, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.rgbToHex
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @public
|
||||||
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
|
* @return {string} the hexadecimal color code equivalent
|
||||||
|
*/
|
||||||
|
static rgbToHex(rgb) {
|
||||||
|
const response = { origin : 'Color.rgbToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
Color._checkTypeAndRange(rgb, [0, 1]);
|
||||||
|
return Color._rgbToHex(rgb);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw { ...response, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the integer equivalent of the [0, 1] RGB triplet.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.rgbToInt
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @public
|
||||||
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
|
* @return {number} the integer equivalent
|
||||||
|
*/
|
||||||
|
static rgbToInt(rgb) {
|
||||||
|
const response = { origin : 'Color.rgbToInt', context: 'when converting an rgb triplet to its integer representation' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
Color._checkTypeAndRange(rgb, [0, 1]);
|
||||||
|
return Color._rgbToInt(rgb);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw { ...response, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the integer equivalent of the [0, 255] RGB triplet.
|
||||||
|
*
|
||||||
|
* @name module:util.Color.rgb255ToInt
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @public
|
||||||
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
|
* @return {number} the integer equivalent
|
||||||
|
*/
|
||||||
|
static rgb255ToInt(rgb255) {
|
||||||
|
const response = { origin : 'Color.rgb255ToInt', context: 'when converting an rgb triplet to its integer representation' };
|
||||||
|
try {
|
||||||
|
Color._checkTypeAndRange(rgb255, [0, 255]);
|
||||||
|
return Color._rgb255ToInt(rgb255);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw { ...response, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
|
||||||
|
*
|
||||||
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Color._rgb255ToHex
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @private
|
||||||
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
|
* @return {string} the hexadecimal color code equivalent
|
||||||
|
*/
|
||||||
|
static _rgb255ToHex(rgb255) {
|
||||||
|
return "#" + ((1 << 24) + (rgb255[0] << 16) + (rgb255[1] << 8) + rgb255[2]).toString(16).slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
|
||||||
|
*
|
||||||
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Color._rgbToHex
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @private
|
||||||
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
|
* @return {string} the hexadecimal color code equivalent
|
||||||
|
*/
|
||||||
|
static _rgbToHex(rgb) {
|
||||||
|
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
||||||
|
return Color._rgb255ToHex(rgb255);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the integer equivalent of the [0, 1] RGB triplet.
|
||||||
|
*
|
||||||
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Color._rgbToInt
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @private
|
||||||
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
|
* @return {number} the integer equivalent
|
||||||
|
*/
|
||||||
|
static _rgbToInt(rgb) {
|
||||||
|
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
||||||
|
return Color._rgb255ToInt(rgb255);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the integer equivalent of the [0, 255] RGB triplet.
|
||||||
|
*
|
||||||
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Color._rgb255ToInt
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @private
|
||||||
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
|
* @return {number} the integer equivalent
|
||||||
|
*/
|
||||||
|
static _rgb255ToInt(rgb255) {
|
||||||
|
return rgb255[0] * 0x10000 + rgb255[1] * 0x100 + rgb255[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [0, 255] based RGB triplet equivalent of the integer color code.
|
||||||
|
*
|
||||||
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Color._intToRgb255
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @private
|
||||||
|
* @param {number} hex - the integer color code
|
||||||
|
* @return {Array.<number>} the [0, 255] RGB equivalent
|
||||||
|
*/
|
||||||
|
static _intToRgb255(hex) {
|
||||||
|
let r255 = hex >>> 0x10;
|
||||||
|
let g255 = (hex & 0xFF00) / 0x100;
|
||||||
|
let b255 = hex & 0xFF;
|
||||||
|
|
||||||
|
return [r255, g255, b255];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [0, 1] based RGB triplet equivalent of the integer color code.
|
||||||
|
*
|
||||||
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Color._intToRgb
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @private
|
||||||
|
* @param {number} hex - the integer color code
|
||||||
|
* @return {Array.<number>} the [0, 1] RGB equivalent
|
||||||
|
*/
|
||||||
|
static _intToRgb(hex) {
|
||||||
|
let [r255, g255, b255] = Color._intToRgb255(hex);
|
||||||
|
|
||||||
|
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the argument is an array of numbers of size 3, and, potentially, that its elements fall within the range.
|
||||||
|
*
|
||||||
|
* @name module:util.Color._checkTypeAndRange
|
||||||
|
* @function
|
||||||
|
* @static
|
||||||
|
* @private
|
||||||
|
* @param {any} arg - the argument
|
||||||
|
* @param {Array.<number>} [range] - the lower and higher bounds of the range
|
||||||
|
* @return {boolean} whether the argument is an array of numbers of size 3, and, potentially, whether its elements fall within the range (if range is not undefined)
|
||||||
|
*/
|
||||||
|
static _checkTypeAndRange(arg, range = undefined) {
|
||||||
|
if (!Array.isArray(arg) || arg.length != 3
|
||||||
|
|| typeof arg[0] !== 'number' || typeof arg[1] !== 'number' || typeof arg[2] !== 'number')
|
||||||
|
throw 'the argument should be an array of numbers of length 3';
|
||||||
|
|
||||||
|
if (typeof range !== 'undefined' && (arg[0] < range[0] || arg[0] > range[1] || arg[1] < range[0] || arg[1] > range[1] || arg[2] < range[0] || arg[2] > range[1]))
|
||||||
|
throw 'the color components should all belong to [' + range[0] + ', ' + range[1] + ']';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color spaces.
|
||||||
|
*
|
||||||
|
* @name module:util.Color#COLOR_SPACE
|
||||||
|
* @enum {Symbol}
|
||||||
|
* @readonly
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
Color.COLOR_SPACE = {
|
||||||
|
/**
|
||||||
|
* RGB colorspace: [r,g,b] with r,g,b in [-1, 1]
|
||||||
|
*/
|
||||||
|
RGB: Symbol.for('RGB'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RGB255 colorspace: [r,g,b] with r,g,b in [0, 255]
|
||||||
|
*/
|
||||||
|
RGB255: Symbol.for('RGB255'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
HSV: Symbol.for('HSV'),
|
||||||
|
DKL: Symbol.for('DKL'),
|
||||||
|
LMS: Symbol.for('LMS')
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Named colors.
|
||||||
|
*
|
||||||
|
* @name module:util.Color#NAMED_COLORS
|
||||||
|
* @enum {Symbol}
|
||||||
|
* @readonly
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
Color.NAMED_COLORS = {
|
||||||
|
'aliceblue': '#F0F8FF',
|
||||||
|
'antiquewhite': '#FAEBD7',
|
||||||
|
'aqua': '#00FFFF',
|
||||||
|
'aquamarine': '#7FFFD4',
|
||||||
|
'azure': '#F0FFFF',
|
||||||
|
'beige': '#F5F5DC',
|
||||||
|
'bisque': '#FFE4C4',
|
||||||
|
'black': '#000000',
|
||||||
|
'blanchedalmond': '#FFEBCD',
|
||||||
|
'blue': '#0000FF',
|
||||||
|
'blueviolet': '#8A2BE2',
|
||||||
|
'brown': '#A52A2A',
|
||||||
|
'burlywood': '#DEB887',
|
||||||
|
'cadetblue': '#5F9EA0',
|
||||||
|
'chartreuse': '#7FFF00',
|
||||||
|
'chocolate': '#D2691E',
|
||||||
|
'coral': '#FF7F50',
|
||||||
|
'cornflowerblue': '#6495ED',
|
||||||
|
'cornsilk': '#FFF8DC',
|
||||||
|
'crimson': '#DC143C',
|
||||||
|
'cyan': '#00FFFF',
|
||||||
|
'darkblue': '#00008B',
|
||||||
|
'darkcyan': '#008B8B',
|
||||||
|
'darkgoldenrod': '#B8860B',
|
||||||
|
'darkgray': '#A9A9A9',
|
||||||
|
'darkgreen': '#006400',
|
||||||
|
'darkkhaki': '#BDB76B',
|
||||||
|
'darkmagenta': '#8B008B',
|
||||||
|
'darkolivegreen': '#556B2F',
|
||||||
|
'darkorange': '#FF8C00',
|
||||||
|
'darkorchid': '#9932CC',
|
||||||
|
'darkred': '#8B0000',
|
||||||
|
'darksalmon': '#E9967A',
|
||||||
|
'darkseagreen': '#8FBC8B',
|
||||||
|
'darkslateblue': '#483D8B',
|
||||||
|
'darkslategray': '#2F4F4F',
|
||||||
|
'darkturquoise': '#00CED1',
|
||||||
|
'darkviolet': '#9400D3',
|
||||||
|
'deeppink': '#FF1493',
|
||||||
|
'deepskyblue': '#00BFFF',
|
||||||
|
'dimgray': '#696969',
|
||||||
|
'dodgerblue': '#1E90FF',
|
||||||
|
'firebrick': '#B22222',
|
||||||
|
'floralwhite': '#FFFAF0',
|
||||||
|
'forestgreen': '#228B22',
|
||||||
|
'fuchsia': '#FF00FF',
|
||||||
|
'gainsboro': '#DCDCDC',
|
||||||
|
'ghostwhite': '#F8F8FF',
|
||||||
|
'gold': '#FFD700',
|
||||||
|
'goldenrod': '#DAA520',
|
||||||
|
'gray': '#808080',
|
||||||
|
'green': '#008000',
|
||||||
|
'greenyellow': '#ADFF2F',
|
||||||
|
'honeydew': '#F0FFF0',
|
||||||
|
'hotpink': '#FF69B4',
|
||||||
|
'indianred': '#CD5C5C',
|
||||||
|
'indigo': '#4B0082',
|
||||||
|
'ivory': '#FFFFF0',
|
||||||
|
'khaki': '#F0E68C',
|
||||||
|
'lavender': '#E6E6FA',
|
||||||
|
'lavenderblush': '#FFF0F5',
|
||||||
|
'lawngreen': '#7CFC00',
|
||||||
|
'lemonchiffon': '#FFFACD',
|
||||||
|
'lightblue': '#ADD8E6',
|
||||||
|
'lightcoral': '#F08080',
|
||||||
|
'lightcyan': '#E0FFFF',
|
||||||
|
'lightgoldenrodyellow': '#FAFAD2',
|
||||||
|
'lightgray': '#D3D3D3',
|
||||||
|
'lightgreen': '#90EE90',
|
||||||
|
'lightpink': '#FFB6C1',
|
||||||
|
'lightsalmon': '#FFA07A',
|
||||||
|
'lightseagreen': '#20B2AA',
|
||||||
|
'lightskyblue': '#87CEFA',
|
||||||
|
'lightslategray': '#778899',
|
||||||
|
'lightsteelblue': '#B0C4DE',
|
||||||
|
'lightyellow': '#FFFFE0',
|
||||||
|
'lime': '#00FF00',
|
||||||
|
'limegreen': '#32CD32',
|
||||||
|
'linen': '#FAF0E6',
|
||||||
|
'magenta': '#FF00FF',
|
||||||
|
'maroon': '#800000',
|
||||||
|
'mediumaquamarine': '#66CDAA',
|
||||||
|
'mediumblue': '#0000CD',
|
||||||
|
'mediumorchid': '#BA55D3',
|
||||||
|
'mediumpurple': '#9370DB',
|
||||||
|
'mediumseagreen': '#3CB371',
|
||||||
|
'mediumslateblue': '#7B68EE',
|
||||||
|
'mediumspringgreen': '#00FA9A',
|
||||||
|
'mediumturquoise': '#48D1CC',
|
||||||
|
'mediumvioletred': '#C71585',
|
||||||
|
'midnightblue': '#191970',
|
||||||
|
'mintcream': '#F5FFFA',
|
||||||
|
'mistyrose': '#FFE4E1',
|
||||||
|
'moccasin': '#FFE4B5',
|
||||||
|
'navajowhite': '#FFDEAD',
|
||||||
|
'navy': '#000080',
|
||||||
|
'oldlace': '#FDF5E6',
|
||||||
|
'olive': '#808000',
|
||||||
|
'olivedrab': '#6B8E23',
|
||||||
|
'orange': '#FFA500',
|
||||||
|
'orangered': '#FF4500',
|
||||||
|
'orchid': '#DA70D6',
|
||||||
|
'palegoldenrod': '#EEE8AA',
|
||||||
|
'palegreen': '#98FB98',
|
||||||
|
'paleturquoise': '#AFEEEE',
|
||||||
|
'palevioletred': '#DB7093',
|
||||||
|
'papayawhip': '#FFEFD5',
|
||||||
|
'peachpuff': '#FFDAB9',
|
||||||
|
'peru': '#CD853F',
|
||||||
|
'pink': '#FFC0CB',
|
||||||
|
'plum': '#DDA0DD',
|
||||||
|
'powderblue': '#B0E0E6',
|
||||||
|
'purple': '#800080',
|
||||||
|
'red': '#FF0000',
|
||||||
|
'rosybrown': '#BC8F8F',
|
||||||
|
'royalblue': '#4169E1',
|
||||||
|
'saddlebrown': '#8B4513',
|
||||||
|
'salmon': '#FA8072',
|
||||||
|
'sandybrown': '#F4A460',
|
||||||
|
'seagreen': '#2E8B57',
|
||||||
|
'seashell': '#FFF5EE',
|
||||||
|
'sienna': '#A0522D',
|
||||||
|
'silver': '#C0C0C0',
|
||||||
|
'skyblue': '#87CEEB',
|
||||||
|
'slateblue': '#6A5ACD',
|
||||||
|
'slategray': '#708090',
|
||||||
|
'snow': '#FFFAFA',
|
||||||
|
'springgreen': '#00FF7F',
|
||||||
|
'steelblue': '#4682B4',
|
||||||
|
'tan': '#D2B48C',
|
||||||
|
'teal': '#008080',
|
||||||
|
'thistle': '#D8BFD8',
|
||||||
|
'tomato': '#FF6347',
|
||||||
|
'turquoise': '#40E0D0',
|
||||||
|
'violet': '#EE82EE',
|
||||||
|
'wheat': '#F5DEB3',
|
||||||
|
'white': '#FFFFFF',
|
||||||
|
'whitesmoke': '#F5F5F5',
|
||||||
|
'yellow': '#FFFF00',
|
||||||
|
'yellowgreen': '#9ACD32'
|
||||||
|
};
|
72
js/util/ColorMixin.js
Normal file
72
js/util/ColorMixin.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Color Mixin.
|
||||||
|
*
|
||||||
|
* @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 { Color } from './Color';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>This mixin implement color and contrast changes for visual stimuli</p>
|
||||||
|
*
|
||||||
|
* @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.<number>} 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.<number>} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
147
js/util/EventEmitter.js
Normal file
147
js/util/EventEmitter.js
Normal file
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>EventEmitter implements the classic observer/observable pattern.</p>
|
||||||
|
*
|
||||||
|
* <p>Note: this is heavily inspired by http://www.datchley.name/es6-eventemitter/</p>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
104
js/util/Logger.js
Normal file
104
js/util/Logger.js
Normal file
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>This class handles a variety of loggers, e.g. a browser console one (mostly for debugging), a remote one, etc.</p>
|
||||||
|
*
|
||||||
|
* <p>Note: we use log4javascript.</p>
|
||||||
|
*
|
||||||
|
* @name module:util.Logger
|
||||||
|
* @class
|
||||||
|
* @param {*} threshold - the logging threshold, e.g. log4javascript.Level.ERROR
|
||||||
|
*/
|
||||||
|
export class Logger {
|
||||||
|
|
||||||
|
constructor(threshold) {
|
||||||
|
// browser console logger:
|
||||||
|
this.consoleLogger = log4javascript.getLogger('psychojs');
|
||||||
|
|
||||||
|
const appender = new log4javascript.BrowserConsoleAppender();
|
||||||
|
appender.setLayout(this._customConsoleLayout());
|
||||||
|
appender.setThreshold(threshold);
|
||||||
|
|
||||||
|
this.consoleLogger.addAppender(appender);
|
||||||
|
this.consoleLogger.setLevel(threshold);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
let appender = new log4javascript.AjaxAppender('https://pavlovia.org/server?command=log');
|
||||||
|
appender.setBatchSize(5);
|
||||||
|
appender.setSendAllOnUnload(true);
|
||||||
|
//appender.setFailCallback();
|
||||||
|
|
||||||
|
let jsonLayout = new log4javascript.JsonLayout([false, true]);
|
||||||
|
appender.setLayout(jsonLayout);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a custom console layout.
|
||||||
|
*
|
||||||
|
* @name module:util.Logger#_customConsoleLayout
|
||||||
|
* @private
|
||||||
|
* @return {*} the custom layout
|
||||||
|
*/
|
||||||
|
_customConsoleLayout() {
|
||||||
|
const customLayout = new log4javascript.PatternLayout("%p %f{1} | %m");
|
||||||
|
|
||||||
|
customLayout.setCustomField('location', function (layout, loggingReference) {
|
||||||
|
// we throw a fake exception to retrieve the stack trace
|
||||||
|
try { (0)() } catch (e) {
|
||||||
|
const stackEntries = e.stack.replace(/^.*?\n/, '').replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anon}(').split("\n");
|
||||||
|
|
||||||
|
let relevantEntry;
|
||||||
|
const browser = util.detectBrowser();
|
||||||
|
if (browser == 'Firefox') {
|
||||||
|
// look for entry immediately after those of log4javascript:
|
||||||
|
for (let entry of stackEntries)
|
||||||
|
if (entry.indexOf('log4javascript.min.js') <= 0) {
|
||||||
|
relevantEntry = entry;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = relevantEntry.split(':');
|
||||||
|
const line = buf[buf.length - 2];
|
||||||
|
const file = buf[buf.length - 3].split('/').pop();
|
||||||
|
const method = relevantEntry.split('@')[0];
|
||||||
|
|
||||||
|
return method + ' ' + file + ' ' + line;
|
||||||
|
}
|
||||||
|
else if (browser == 'Safari') {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
else if (browser == 'Chrome') {
|
||||||
|
relevantEntry = stackEntries.pop();
|
||||||
|
|
||||||
|
let buf = relevantEntry.split(' ');
|
||||||
|
let fileLine = buf.pop();
|
||||||
|
const method = buf.pop();
|
||||||
|
buf = fileLine.split(':'); buf.pop();
|
||||||
|
const line = buf.pop();
|
||||||
|
const file = buf.pop().split('/').pop();
|
||||||
|
|
||||||
|
return method + ' ' + file + ' ' + line;
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return customLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
288
js/util/PsychObject.js
Normal file
288
js/util/PsychObject.js
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
/** @module util */
|
||||||
|
/**
|
||||||
|
* Core Object.
|
||||||
|
*
|
||||||
|
* @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 { EventEmitter } from './EventEmitter';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>PsychoObject is the base class for all PsychoJS objects.
|
||||||
|
* It is responsible for handling attributes.</p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>Note: (a) If the object already has a set<attributeName> method, we do not redefine it,
|
||||||
|
* and the setter for this attribute calls that method instead of _setAttribute.</p>
|
||||||
|
* <p> (b) _addAttributes is typically called in the constructor of an object, after
|
||||||
|
* the call to super (see module:visual.ImageStim for an illustration).</p>
|
||||||
|
*
|
||||||
|
* @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<attributeName> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
198
js/util/Scheduler.js
Normal file
198
js/util/Scheduler.js
Normal file
@ -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
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A scheduler helps run the main loop by managing scheduled functions,
|
||||||
|
* called tasks, after each frame is displayed.</p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Tasks are either another [Scheduler]{@link module:util.Scheduler}, or a
|
||||||
|
* javascript functions returning one of the following codes:
|
||||||
|
* <ul>
|
||||||
|
* <li>Scheduler.Event.NEXT: </li>
|
||||||
|
* <li>Scheduler.Event.FLIP_REPEAT: </li>
|
||||||
|
* <li>Scheduler.Event.FLIP_NEXT: </li>
|
||||||
|
* <li>Scheduler.Event.QUIT: </li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p> 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}.</p>
|
||||||
|
*
|
||||||
|
* <p> Conditional branching is also available:
|
||||||
|
* [scheduler.addConditionalBranches]{@link module:util.Scheduler#addConditionalBranches}</p>
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>Note: the tasks are [sub-schedulers]{@link module:util.Scheduler}.</p>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* <p>Note: tasks are run after each animation frame.</p>
|
||||||
|
*/
|
||||||
|
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')
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
/** @module visual */
|
/** @module visual */
|
||||||
/**
|
/**
|
||||||
* @file Basic Shape Stimulus.
|
* Basic Shape Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Base class for all visual stimuli.
|
* Base class for all visual stimuli.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Image Stimulus.
|
* Image Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Rectangular Stimulus.
|
* Rectangular Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @file Text Stimulus.
|
* Text Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 3.0.0b11
|
* @version 3.0.0b11
|
||||||
@ -240,7 +240,7 @@ export class TextStim extends util.mix(BaseVisualStim).with(ColorMixin)
|
|||||||
this._heightPix = this._getLengthPix(height);
|
this._heightPix = this._getLengthPix(height);
|
||||||
|
|
||||||
var fontSize = Math.round(this._heightPix);
|
var fontSize = Math.round(this._heightPix);
|
||||||
let color = this._getDesiredColor(this._color, this._contrast);
|
let color = this.getContrastedColor(this._color, this._contrast);
|
||||||
var font =
|
var font =
|
||||||
(this._bold ? 'bold ' : '') +
|
(this._bold ? 'bold ' : '') +
|
||||||
(this._italic ? 'italic ' : '') +
|
(this._italic ? 'italic ' : '') +
|
||||||
|
Loading…
Reference in New Issue
Block a user