mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 02:30:53 +00:00
complete linting, saving of partial results, improved GUI, various bug fixes
This commit is contained in:
parent
8276bd790a
commit
1fc76d1461
@ -1,4 +1,4 @@
|
|||||||
Copyright 2019 Ilixa Ltd.
|
Copyright 2020 Ilixa Ltd.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
@ -1,33 +1,37 @@
|
|||||||
/**
|
/**
|
||||||
* Manager handling the keyboard and mouse/touch events.
|
* Manager handling the keyboard and mouse/touch events.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MonotonicClock, Clock } from '../util/Clock';
|
import {MonotonicClock, Clock} from '../util/Clock';
|
||||||
import { PsychoJS } from './PsychoJS';
|
import {PsychoJS} from './PsychoJS';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class
|
* @class
|
||||||
* <p>This manager handles all participant interactions with the experiment, i.e. keyboard, mouse and touch events.</p>
|
* <p>This manager handles all participant interactions with the experiment, i.e. keyboard, mouse and touch events.</p>
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager
|
* @name module:core.EventManager
|
||||||
* @class
|
* @class
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
*/
|
*/
|
||||||
export class EventManager {
|
export class EventManager
|
||||||
|
{
|
||||||
|
|
||||||
constructor(psychoJS) {
|
constructor(psychoJS)
|
||||||
|
{
|
||||||
this._psychoJS = psychoJS;
|
this._psychoJS = psychoJS;
|
||||||
|
|
||||||
// populate the reverse pyglet map:
|
// populate the reverse pyglet map:
|
||||||
for (const keyName in EventManager._pygletMap)
|
for (const keyName in EventManager._pygletMap)
|
||||||
|
{
|
||||||
EventManager._reversePygletMap[EventManager._pygletMap[keyName]] = keyName;
|
EventManager._reversePygletMap[EventManager._pygletMap[keyName]] = keyName;
|
||||||
|
}
|
||||||
|
|
||||||
// add key listeners:
|
// add key listeners:
|
||||||
this._keyBuffer = [];
|
this._keyBuffer = [];
|
||||||
@ -53,9 +57,9 @@ export class EventManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of keys pressed by the participant.
|
* Get the list of keys pressed by the participant.
|
||||||
*
|
*
|
||||||
* <p>Note: The w3c [key-event viewer]{@link https://w3c.github.io/uievents/tools/key-event-viewer.html} can be used to see possible values for the items in the keyList given the user's keyboard and chosen layout. The "key" and "code" columns in the UI Events fields are the relevant values for the keyList argument.</p>
|
* <p>Note: The w3c [key-event viewer]{@link https://w3c.github.io/uievents/tools/key-event-viewer.html} can be used to see possible values for the items in the keyList given the user's keyboard and chosen layout. The "key" and "code" columns in the UI Events fields are the relevant values for the keyList argument.</p>
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#getKeys
|
* @name module:core.EventManager#getKeys
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -65,36 +69,54 @@ export class EventManager {
|
|||||||
* @return {string[]} the list of keys that were pressed.
|
* @return {string[]} the list of keys that were pressed.
|
||||||
*/
|
*/
|
||||||
getKeys({
|
getKeys({
|
||||||
keyList = null,
|
keyList = null,
|
||||||
timeStamped = false
|
timeStamped = false
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
if (keyList != null)
|
if (keyList != null)
|
||||||
|
{
|
||||||
keyList = EventManager.pyglet2w3c(keyList);
|
keyList = EventManager.pyglet2w3c(keyList);
|
||||||
|
}
|
||||||
|
|
||||||
let newBuffer = [];
|
let newBuffer = [];
|
||||||
let keys = [];
|
let keys = [];
|
||||||
for (let i = 0; i < this._keyBuffer.length; ++i) {
|
for (let i = 0; i < this._keyBuffer.length; ++i)
|
||||||
|
{
|
||||||
const key = this._keyBuffer[i];
|
const key = this._keyBuffer[i];
|
||||||
let keyId = null;
|
let keyId = null;
|
||||||
|
|
||||||
if (keyList != null) {
|
if (keyList != null)
|
||||||
|
{
|
||||||
let index = keyList.indexOf(key.code);
|
let index = keyList.indexOf(key.code);
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
|
{
|
||||||
index = keyList.indexOf(EventManager._keycodeMap[key.keyCode]);
|
index = keyList.indexOf(EventManager._keycodeMap[key.keyCode]);
|
||||||
|
}
|
||||||
if (index >= 0)
|
if (index >= 0)
|
||||||
|
{
|
||||||
keyId = EventManager._reversePygletMap[keyList[index]];
|
keyId = EventManager._reversePygletMap[keyList[index]];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
keyId = EventManager._reversePygletMap[key.code];
|
keyId = EventManager._reversePygletMap[key.code];
|
||||||
|
}
|
||||||
|
|
||||||
if (keyId != null) {
|
if (keyId != null)
|
||||||
|
{
|
||||||
if (timeStamped)
|
if (timeStamped)
|
||||||
|
{
|
||||||
keys.push([keyId, key.timestamp]);
|
keys.push([keyId, key.timestamp]);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
keys.push(keyId);
|
keys.push(keyId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
newBuffer.push(key); // keep key press in buffer
|
{
|
||||||
|
newBuffer.push(key);
|
||||||
|
} // keep key press in buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
this._keyBuffer = newBuffer;
|
this._keyBuffer = newBuffer;
|
||||||
@ -108,6 +130,7 @@ export class EventManager {
|
|||||||
* @property {Array.Clock} clocks - the clocks associated to the mouse buttons, reset whenever the button is pressed
|
* @property {Array.Clock} clocks - the clocks associated to the mouse buttons, reset whenever the button is pressed
|
||||||
* @property {Array.number} times - the time elapsed since the last rest of the associated clock
|
* @property {Array.number} times - the time elapsed since the last rest of the associated clock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef EventManager.MouseInfo
|
* @typedef EventManager.MouseInfo
|
||||||
* @property {Array.number} pos - the position of the mouse [x, y]
|
* @property {Array.number} pos - the position of the mouse [x, y]
|
||||||
@ -117,94 +140,102 @@ export class EventManager {
|
|||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Get the mouse info.
|
* Get the mouse info.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#getMouseInfo
|
* @name module:core.EventManager#getMouseInfo
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {EventManager.MouseInfo} the mouse info.
|
* @return {EventManager.MouseInfo} the mouse info.
|
||||||
*/
|
*/
|
||||||
getMouseInfo() {
|
getMouseInfo()
|
||||||
|
{
|
||||||
return this._mouseInfo;
|
return this._mouseInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all events from the event buffer.
|
* Clear all events from the event buffer.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#clearEvents
|
* @name module:core.EventManager#clearEvents
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
* @todo handle the attribs argument
|
* @todo handle the attribs argument
|
||||||
*/
|
*/
|
||||||
clearEvents(attribs) {
|
clearEvents(attribs)
|
||||||
|
{
|
||||||
this.clearKeys();
|
this.clearKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all keys from the key buffer.
|
* Clear all keys from the key buffer.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#clearKeys
|
* @name module:core.EventManager#clearKeys
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
clearKeys() {
|
clearKeys()
|
||||||
|
{
|
||||||
this._keyBuffer = [];
|
this._keyBuffer = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the move clock.
|
* Start the move clock.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#startMoveClock
|
* @name module:core.EventManager#startMoveClock
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
* @todo not implemented
|
* @todo not implemented
|
||||||
*/
|
*/
|
||||||
startMoveClock() {
|
startMoveClock()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the move clock.
|
* Stop the move clock.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#stopMoveClock
|
* @name module:core.EventManager#stopMoveClock
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
* @todo not implemented
|
* @todo not implemented
|
||||||
*/
|
*/
|
||||||
stopMoveClock() {
|
stopMoveClock()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the move clock.
|
* Reset the move clock.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#resetMoveClock
|
* @name module:core.EventManager#resetMoveClock
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
* @todo not implemented
|
* @todo not implemented
|
||||||
*/
|
*/
|
||||||
resetMoveClock() {
|
resetMoveClock()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add various mouse listeners to the Pixi renderer of the {@link Window}.
|
* Add various mouse listeners to the Pixi renderer of the {@link Window}.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#addMouseListeners
|
* @name module:core.EventManager#addMouseListeners
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {PIXI.Renderer} renderer - The Pixi renderer
|
* @param {PIXI.Renderer} renderer - The Pixi renderer
|
||||||
*/
|
*/
|
||||||
addMouseListeners(renderer) {
|
addMouseListeners(renderer)
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
renderer.view.addEventListener("pointerdown", (event) => {
|
renderer.view.addEventListener("pointerdown", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.buttons.pressed[event.button] = 1;
|
self._mouseInfo.buttons.pressed[event.button] = 1;
|
||||||
@ -216,7 +247,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("touchstart", (event) => {
|
renderer.view.addEventListener("touchstart", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.buttons.pressed[0] = 1;
|
self._mouseInfo.buttons.pressed[0] = 1;
|
||||||
@ -230,7 +262,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("pointerup", (event) => {
|
renderer.view.addEventListener("pointerup", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.buttons.pressed[event.button] = 0;
|
self._mouseInfo.buttons.pressed[event.button] = 0;
|
||||||
@ -241,7 +274,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("touchend", (event) => {
|
renderer.view.addEventListener("touchend", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.buttons.pressed[0] = 0;
|
self._mouseInfo.buttons.pressed[0] = 0;
|
||||||
@ -255,7 +289,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("pointermove", (event) => {
|
renderer.view.addEventListener("pointermove", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.moveClock.reset();
|
self._mouseInfo.moveClock.reset();
|
||||||
@ -263,11 +298,12 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("touchmove", (event) => {
|
renderer.view.addEventListener("touchmove", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.moveClock.reset();
|
self._mouseInfo.moveClock.reset();
|
||||||
|
|
||||||
// we use the first touch, discarding all others:
|
// we use the first touch, discarding all others:
|
||||||
const touches = event.changedTouches;
|
const touches = event.changedTouches;
|
||||||
self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY];
|
self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY];
|
||||||
@ -275,19 +311,20 @@ export class EventManager {
|
|||||||
|
|
||||||
|
|
||||||
// (*) wheel
|
// (*) wheel
|
||||||
renderer.view.addEventListener("wheel", event => {
|
renderer.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;
|
||||||
|
|
||||||
this._psychoJS.experimentLogger.data("Mouse: wheel shift=(" + event.deltaX + "," + event.deltaY + "), pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
|
this._psychoJS.experimentLogger.data("Mouse: wheel shift=(" + event.deltaX + "," + event.deltaY + "), pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add key listeners to the document.
|
* Add key listeners to the document.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#_addKeyListeners
|
* @name module:core.EventManager#_addKeyListeners
|
||||||
* @function
|
* @function
|
||||||
* @private
|
* @private
|
||||||
@ -307,7 +344,9 @@ export class EventManager {
|
|||||||
|
|
||||||
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
||||||
if (typeof code === 'undefined')
|
if (typeof code === 'undefined')
|
||||||
|
{
|
||||||
code = EventManager.keycode2w3c(event.keyCode);
|
code = EventManager.keycode2w3c(event.keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
self._keyBuffer.push({
|
self._keyBuffer.push({
|
||||||
code,
|
code,
|
||||||
@ -324,24 +363,29 @@ export class EventManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a keylist that uses pyglet key names to one that uses W3C KeyboardEvent.code values.
|
* Convert a keylist that uses pyglet key names to one that uses W3C KeyboardEvent.code values.
|
||||||
* <p>This allows key lists that work in the builder environment to work in psychoJS web experiments.</p>
|
* <p>This allows key lists that work in the builder environment to work in psychoJS web experiments.</p>
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#pyglet2w3c
|
* @name module:core.EventManager#pyglet2w3c
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {Array.string} pygletKeyList - the array of pyglet key names
|
* @param {Array.string} pygletKeyList - the array of pyglet key names
|
||||||
* @return {Array.string} the w3c keyList
|
* @return {Array.string} the w3c keyList
|
||||||
*/
|
*/
|
||||||
static pyglet2w3c(pygletKeyList) {
|
static pyglet2w3c(pygletKeyList)
|
||||||
|
{
|
||||||
let w3cKeyList = [];
|
let w3cKeyList = [];
|
||||||
for (let i = 0; i < pygletKeyList.length; i++) {
|
for (let i = 0; i < pygletKeyList.length; i++)
|
||||||
|
{
|
||||||
if (typeof EventManager._pygletMap[pygletKeyList[i]] === 'undefined')
|
if (typeof EventManager._pygletMap[pygletKeyList[i]] === 'undefined')
|
||||||
|
{
|
||||||
w3cKeyList.push(pygletKeyList[i]);
|
w3cKeyList.push(pygletKeyList[i]);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
w3cKeyList.push(EventManager._pygletMap[pygletKeyList[i]]);
|
w3cKeyList.push(EventManager._pygletMap[pygletKeyList[i]]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return w3cKeyList;
|
return w3cKeyList;
|
||||||
@ -358,11 +402,16 @@ export class EventManager {
|
|||||||
* @param {string} code - W3C Key Code
|
* @param {string} code - W3C Key Code
|
||||||
* @returns {string} corresponding pyglet key
|
* @returns {string} corresponding pyglet key
|
||||||
*/
|
*/
|
||||||
static w3c2pyglet(code) {
|
static w3c2pyglet(code)
|
||||||
|
{
|
||||||
if (code in EventManager._reversePygletMap)
|
if (code in EventManager._reversePygletMap)
|
||||||
|
{
|
||||||
return EventManager._reversePygletMap[code];
|
return EventManager._reversePygletMap[code];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -377,7 +426,8 @@ export class EventManager {
|
|||||||
* @param {number} keycode - the keycode
|
* @param {number} keycode - the keycode
|
||||||
* @returns {string} corresponding W3C UI Event code
|
* @returns {string} corresponding W3C UI Event code
|
||||||
*/
|
*/
|
||||||
static keycode2w3c(keycode) {
|
static keycode2w3c(keycode)
|
||||||
|
{
|
||||||
return EventManager._keycodeMap[keycode];
|
return EventManager._keycodeMap[keycode];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -390,7 +440,7 @@ export class EventManager {
|
|||||||
*
|
*
|
||||||
* <p>Unfortunately, it is not very fine-grained: for instance, there is no difference between Alt Left and Alt
|
* <p>Unfortunately, it is not very fine-grained: for instance, there is no difference between Alt Left and Alt
|
||||||
* Right, or between Enter and Numpad Enter. Use at your own risk (or upgrade your browser...).</p>
|
* Right, or between Enter and Numpad Enter. Use at your own risk (or upgrade your browser...).</p>
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#_keycodeMap
|
* @name module:core.EventManager#_keycodeMap
|
||||||
* @readonly
|
* @readonly
|
||||||
* @private
|
* @private
|
||||||
@ -479,7 +529,7 @@ EventManager._keycodeMap = {
|
|||||||
/**
|
/**
|
||||||
* This map associates pyglet key names to the corresponding W3C KeyboardEvent codes values.
|
* This map associates pyglet key names to the corresponding W3C KeyboardEvent codes values.
|
||||||
* <p>More information can be found [here]{@link https://www.w3.org/TR/uievents-code}</p>
|
* <p>More information can be found [here]{@link https://www.w3.org/TR/uievents-code}</p>
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#_pygletMap
|
* @name module:core.EventManager#_pygletMap
|
||||||
* @readonly
|
* @readonly
|
||||||
* @private
|
* @private
|
||||||
@ -581,10 +631,10 @@ EventManager._pygletMap = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This map associates W3C KeyboardEvent.codes to the corresponding pyglet key names.
|
* <p>This map associates W3C KeyboardEvent.codes to the corresponding pyglet key names.
|
||||||
*
|
*
|
||||||
* @name module:core.EventManager#_reversePygletMap
|
* @name module:core.EventManager#_reversePygletMap
|
||||||
* @readonly
|
* @readonly
|
||||||
* @private
|
* @private
|
||||||
* @type {Object.<String,String>}
|
* @type {Object.<String,String>}
|
||||||
*/
|
*/
|
||||||
EventManager._reversePygletMap = {};
|
EventManager._reversePygletMap = {};
|
||||||
@ -592,14 +642,16 @@ EventManager._reversePygletMap = {};
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class used by the experiment scripts to keep track of a clock and of the current status (whether or not we are currently checking the keyboard)
|
* Utility class used by the experiment scripts to keep track of a clock and of the current status (whether or not we are currently checking the keyboard)
|
||||||
*
|
*
|
||||||
* @name module:core.BuilderKeyResponse
|
* @name module:core.BuilderKeyResponse
|
||||||
* @class
|
* @class
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
*/
|
*/
|
||||||
export class BuilderKeyResponse {
|
export class BuilderKeyResponse
|
||||||
constructor(psychoJS) {
|
{
|
||||||
|
constructor(psychoJS)
|
||||||
|
{
|
||||||
this._psychoJS = psychoJS;
|
this._psychoJS = psychoJS;
|
||||||
|
|
||||||
this.status = PsychoJS.Status.NOT_STARTED;
|
this.status = PsychoJS.Status.NOT_STARTED;
|
||||||
|
196
js/core/GUI.js
196
js/core/GUI.js
@ -2,7 +2,7 @@
|
|||||||
* Graphic User Interface
|
* Graphic User Interface
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -12,7 +12,7 @@ import {PsychoJS} from './PsychoJS';
|
|||||||
import {ServerManager} from './ServerManager';
|
import {ServerManager} from './ServerManager';
|
||||||
import {Scheduler} from '../util/Scheduler';
|
import {Scheduler} from '../util/Scheduler';
|
||||||
import {Clock} from '../util/Clock';
|
import {Clock} from '../util/Clock';
|
||||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -27,14 +27,18 @@ import * as util from '../util/Util';
|
|||||||
export class GUI
|
export class GUI
|
||||||
{
|
{
|
||||||
|
|
||||||
get dialogComponent() { return this._dialogComponent; }
|
get dialogComponent()
|
||||||
|
{
|
||||||
|
return this._dialogComponent;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(psychoJS)
|
constructor(psychoJS)
|
||||||
{
|
{
|
||||||
this._psychoJS = psychoJS;
|
this._psychoJS = psychoJS;
|
||||||
|
|
||||||
// gui listens to RESOURCE events from the server manager:
|
// gui listens to RESOURCE events from the server manager:
|
||||||
psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) => {
|
psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) =>
|
||||||
|
{
|
||||||
this._onResourceEvents(signal);
|
this._onResourceEvents(signal);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,11 +70,11 @@ export class GUI
|
|||||||
* @param {String} options.title - name of the project
|
* @param {String} options.title - name of the project
|
||||||
*/
|
*/
|
||||||
DlgFromDict({
|
DlgFromDict({
|
||||||
logoUrl,
|
logoUrl,
|
||||||
text,
|
text,
|
||||||
dictionary,
|
dictionary,
|
||||||
title
|
title
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
// get info from URL:
|
// get info from URL:
|
||||||
const infoFromUrl = util.getUrlParameters();
|
const infoFromUrl = util.getUrlParameters();
|
||||||
@ -116,7 +120,7 @@ export class GUI
|
|||||||
// logo:
|
// logo:
|
||||||
if (typeof logoUrl === 'string')
|
if (typeof logoUrl === 'string')
|
||||||
{
|
{
|
||||||
htmlCode += '<img id="dialog-logo" class="logo" src="' + logoUrl + '">';
|
htmlCode += '<img id="dialog-logo" class="logo" alt="logo" src="' + logoUrl + '">';
|
||||||
}
|
}
|
||||||
|
|
||||||
// information text:
|
// information text:
|
||||||
@ -128,17 +132,19 @@ export class GUI
|
|||||||
|
|
||||||
// add a combobox or text areas for each entry in the dictionary:
|
// add a combobox or text areas for each entry in the dictionary:
|
||||||
htmlCode += '<form>';
|
htmlCode += '<form>';
|
||||||
for (const key in dictionary) {
|
for (const key in dictionary)
|
||||||
|
{
|
||||||
const value = dictionary[key];
|
const value = dictionary[key];
|
||||||
const keyId = CSS.escape(key) + '_id';
|
const keyId = CSS.escape(key) + '_id';
|
||||||
|
|
||||||
// only create an input if the key is not in the URL:
|
// only create an input if the key is not in the URL:
|
||||||
let inUrl = false;
|
let inUrl = false;
|
||||||
const cleanedDictKey = key.trim().toLowerCase();
|
const cleanedDictKey = key.trim().toLowerCase();
|
||||||
infoFromUrl.forEach( (urlValue, urlKey) =>
|
infoFromUrl.forEach((urlValue, urlKey) =>
|
||||||
{
|
{
|
||||||
const cleanedUrlKey = urlKey.trim().toLowerCase();
|
const cleanedUrlKey = urlKey.trim().toLowerCase();
|
||||||
if (cleanedUrlKey === cleanedDictKey) {
|
if (cleanedUrlKey === cleanedDictKey)
|
||||||
|
{
|
||||||
inUrl = true;
|
inUrl = true;
|
||||||
// break;
|
// break;
|
||||||
}
|
}
|
||||||
@ -150,19 +156,26 @@ export class GUI
|
|||||||
|
|
||||||
// if the field is required:
|
// if the field is required:
|
||||||
if (key.slice(-1) === '*')
|
if (key.slice(-1) === '*')
|
||||||
|
{
|
||||||
self._requiredKeys.push(key);
|
self._requiredKeys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
// if value is an array, we create a select drop-down menu:
|
// if value is an array, we create a select drop-down menu:
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value))
|
||||||
|
{
|
||||||
htmlCode += '<select name="' + key + '" id="' + keyId + '" class="text ui-widget-content' +
|
htmlCode += '<select name="' + key + '" id="' + keyId + '" class="text ui-widget-content' +
|
||||||
' ui-corner-all">';
|
' ui-corner-all">';
|
||||||
|
|
||||||
// if the field is required, we add an empty option and select it:
|
// if the field is required, we add an empty option and select it:
|
||||||
if (key.slice(-1) === '*')
|
if (key.slice(-1) === '*')
|
||||||
|
{
|
||||||
htmlCode += '<option disabled selected>...</option>';
|
htmlCode += '<option disabled selected>...</option>';
|
||||||
|
}
|
||||||
|
|
||||||
for (const option of value)
|
for (const option of value)
|
||||||
|
{
|
||||||
htmlCode += '<option>' + option + '</option>';
|
htmlCode += '<option>' + option + '</option>';
|
||||||
|
}
|
||||||
|
|
||||||
htmlCode += '</select>';
|
htmlCode += '</select>';
|
||||||
$('#' + keyId).selectmenu({classes: {}});
|
$('#' + keyId).selectmenu({classes: {}});
|
||||||
@ -170,8 +183,10 @@ export class GUI
|
|||||||
|
|
||||||
// otherwise we use a single string input:
|
// otherwise we use a single string input:
|
||||||
else /*if (typeof value === 'string')*/
|
else /*if (typeof value === 'string')*/
|
||||||
htmlCode += '<input type="text" name="' + key + '" id="' + keyId + '" value="' + value + '" class="text ui-widget-content ui-corner-all">';
|
{
|
||||||
|
htmlCode += '<input type="text" name="' + key + '" id="' + keyId;
|
||||||
|
htmlCode += '" value="' + value + '" class="text ui-widget-content ui-corner-all">';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
htmlCode += '</form>';
|
htmlCode += '</form>';
|
||||||
@ -199,11 +214,14 @@ export class GUI
|
|||||||
|
|
||||||
|
|
||||||
// setup change event handlers for all required keys:
|
// setup change event handlers for all required keys:
|
||||||
for (const key of this._requiredKeys) {
|
for (const key of this._requiredKeys)
|
||||||
|
{
|
||||||
const keyId = CSS.escape(key) + '_id';
|
const keyId = CSS.escape(key) + '_id';
|
||||||
const input = document.getElementById(keyId);
|
const input = document.getElementById(keyId);
|
||||||
if (input)
|
if (input)
|
||||||
|
{
|
||||||
input.oninput = (event) => GUI._onKeyChange(self, event);
|
input.oninput = (event) => GUI._onKeyChange(self, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// init and open the dialog box:
|
// init and open the dialog box:
|
||||||
@ -223,17 +241,21 @@ export class GUI
|
|||||||
{
|
{
|
||||||
id: "buttonOk",
|
id: "buttonOk",
|
||||||
text: "Ok",
|
text: "Ok",
|
||||||
click: function () {
|
click: function ()
|
||||||
|
{
|
||||||
|
|
||||||
// update dictionary:
|
// update dictionary:
|
||||||
for (const key in dictionary) {
|
for (const key in dictionary)
|
||||||
|
{
|
||||||
const input = document.getElementById(CSS.escape(key) + "_id");
|
const input = document.getElementById(CSS.escape(key) + "_id");
|
||||||
if (input)
|
if (input)
|
||||||
|
{
|
||||||
dictionary[key] = input.value;
|
dictionary[key] = input.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self._dialogComponent.button = 'OK';
|
self._dialogComponent.button = 'OK';
|
||||||
$("#expDialog").dialog("close");
|
$("#expDialog").dialog('close');
|
||||||
|
|
||||||
// switch to full screen if requested:
|
// switch to full screen if requested:
|
||||||
self._psychoJS.window.adjustScreenSize();
|
self._psychoJS.window.adjustScreenSize();
|
||||||
@ -242,9 +264,10 @@ export class GUI
|
|||||||
{
|
{
|
||||||
id: "buttonCancel",
|
id: "buttonCancel",
|
||||||
text: "Cancel",
|
text: "Cancel",
|
||||||
click: function () {
|
click: function ()
|
||||||
|
{
|
||||||
self._dialogComponent.button = 'Cancel';
|
self._dialogComponent.button = 'Cancel';
|
||||||
$("#expDialog").dialog("close");
|
$("#expDialog").dialog('close');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -253,7 +276,8 @@ export class GUI
|
|||||||
open: self._onDialogOpen('#expDialog'),
|
open: self._onDialogOpen('#expDialog'),
|
||||||
|
|
||||||
// close is called by both buttons and when the user clicks on the cross:
|
// close is called by both buttons and when the user clicks on the cross:
|
||||||
close: function () {
|
close: function ()
|
||||||
|
{
|
||||||
//$.unblockUI();
|
//$.unblockUI();
|
||||||
$(this).dialog('destroy').remove();
|
$(this).dialog('destroy').remove();
|
||||||
self._dialogComponent.status = PsychoJS.Status.FINISHED;
|
self._dialogComponent.status = PsychoJS.Status.FINISHED;
|
||||||
@ -282,9 +306,13 @@ export class GUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (self._dialogComponent.status === PsychoJS.Status.FINISHED)
|
if (self._dialogComponent.status === PsychoJS.Status.FINISHED)
|
||||||
|
{
|
||||||
return Scheduler.Event.NEXT;
|
return Scheduler.Event.NEXT;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return Scheduler.Event.FLIP_REPEAT;
|
return Scheduler.Event.FLIP_REPEAT;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,12 +336,25 @@ export class GUI
|
|||||||
* @param {GUI.onOK} [options.onOK] - function called when the participant presses the OK button
|
* @param {GUI.onOK} [options.onOK] - function called when the participant presses the OK button
|
||||||
*/
|
*/
|
||||||
dialog({
|
dialog({
|
||||||
message,
|
message,
|
||||||
warning,
|
warning,
|
||||||
error,
|
error,
|
||||||
showOK = true,
|
showOK = true,
|
||||||
onOK
|
onOK
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
|
|
||||||
|
// close the previously opened dialog box, if there is one:
|
||||||
|
const expDialog = $("#expDialog");
|
||||||
|
if (expDialog.length)
|
||||||
|
{
|
||||||
|
expDialog.dialog("destroy").remove();
|
||||||
|
}
|
||||||
|
const msgDialog = $("#msgDialog");
|
||||||
|
if (msgDialog.length)
|
||||||
|
{
|
||||||
|
msgDialog.dialog("destroy").remove();
|
||||||
|
}
|
||||||
|
|
||||||
let htmlCode;
|
let htmlCode;
|
||||||
let titleColour;
|
let titleColour;
|
||||||
@ -325,13 +366,16 @@ export class GUI
|
|||||||
|
|
||||||
// deal with null error:
|
// deal with null error:
|
||||||
if (!error)
|
if (!error)
|
||||||
|
{
|
||||||
error = 'Unspecified JavaScript error';
|
error = 'Unspecified JavaScript error';
|
||||||
|
}
|
||||||
|
|
||||||
let errorCode = null;
|
let errorCode = null;
|
||||||
|
|
||||||
// go through the error stack and look for errorCode if there is one:
|
// go through the error stack and look for errorCode if there is one:
|
||||||
let stackCode = '<ul>';
|
let stackCode = '<ul>';
|
||||||
while (true) {
|
while (true)
|
||||||
|
{
|
||||||
|
|
||||||
if (typeof error === 'object' && 'errorCode' in error)
|
if (typeof error === 'object' && 'errorCode' in error)
|
||||||
{
|
{
|
||||||
@ -340,12 +384,12 @@ export class GUI
|
|||||||
|
|
||||||
if (typeof error === 'object' && 'context' in error)
|
if (typeof error === 'object' && 'context' in error)
|
||||||
{
|
{
|
||||||
stackCode += '<li>' + error.context + '</li>';
|
stackCode += '<li>' + error.context + '</li>';
|
||||||
error = error.error;
|
error = error.error;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
stackCode += '<li><b>' + error + '</b></li>';
|
stackCode += '<li><b>' + error + '</b></li>';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -371,7 +415,8 @@ export class GUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we are displaying a message:
|
// we are displaying a message:
|
||||||
else if (typeof message !== 'undefined') {
|
else if (typeof message !== 'undefined')
|
||||||
|
{
|
||||||
htmlCode = '<div id="msgDialog" title="Message">' +
|
htmlCode = '<div id="msgDialog" title="Message">' +
|
||||||
'<p class="validateTips">' + message + '</p>' +
|
'<p class="validateTips">' + message + '</p>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@ -379,7 +424,8 @@ export class GUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we are displaying a warning:
|
// we are displaying a warning:
|
||||||
else if (typeof warning !== 'undefined') {
|
else if (typeof warning !== 'undefined')
|
||||||
|
{
|
||||||
htmlCode = '<div id="msgDialog" title="Warning">' +
|
htmlCode = '<div id="msgDialog" title="Warning">' +
|
||||||
'<p class="validateTips">' + warning + '</p>' +
|
'<p class="validateTips">' + warning + '</p>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@ -395,7 +441,7 @@ export class GUI
|
|||||||
this._estimateDialogScalingFactor();
|
this._estimateDialogScalingFactor();
|
||||||
const dialogSize = this._getDialogSize();
|
const dialogSize = this._getDialogSize();
|
||||||
const self = this;
|
const self = this;
|
||||||
$('#msgDialog').dialog({
|
$("#msgDialog").dialog({
|
||||||
dialogClass: 'no-close',
|
dialogClass: 'no-close',
|
||||||
|
|
||||||
width: dialogSize[0],
|
width: dialogSize[0],
|
||||||
@ -405,15 +451,18 @@ export class GUI
|
|||||||
modal: true,
|
modal: true,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
|
|
||||||
buttons: (!showOK)?[]:[{
|
buttons: (!showOK) ? [] : [{
|
||||||
id: "buttonOk",
|
id: "buttonOk",
|
||||||
text: "Ok",
|
text: "Ok",
|
||||||
click: function() {
|
click: function ()
|
||||||
|
{
|
||||||
$(this).dialog("destroy").remove();
|
$(this).dialog("destroy").remove();
|
||||||
|
|
||||||
// execute callback function:
|
// execute callback function:
|
||||||
if (typeof onOK !== 'undefined')
|
if (typeof onOK !== 'undefined')
|
||||||
|
{
|
||||||
onOK();
|
onOK();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|
||||||
@ -422,7 +471,7 @@ export class GUI
|
|||||||
|
|
||||||
})
|
})
|
||||||
// change colour of title bar
|
// change colour of title bar
|
||||||
.prev(".ui-dialog-titlebar").css("background", titleColour);
|
.prev(".ui-dialog-titlebar").css("background", titleColour);
|
||||||
|
|
||||||
|
|
||||||
// when the browser window is resize, we redimension and reposition the dialog:
|
// when the browser window is resize, we redimension and reposition the dialog:
|
||||||
@ -471,10 +520,12 @@ export class GUI
|
|||||||
* @param {String} dialogId - the dialog ID
|
* @param {String} dialogId - the dialog ID
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_dialogResize(dialogId) {
|
_dialogResize(dialogId)
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
$(window).resize( function() {
|
$(window).resize(function ()
|
||||||
|
{
|
||||||
const parent = $(dialogId).parent();
|
const parent = $(dialogId).parent();
|
||||||
const windowSize = [$(window).width(), $(window).height()];
|
const windowSize = [$(window).width(), $(window).height()];
|
||||||
|
|
||||||
@ -486,7 +537,8 @@ export class GUI
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isDifferent = self._estimateDialogScalingFactor();
|
const isDifferent = self._estimateDialogScalingFactor();
|
||||||
if (!isDifferent) {
|
if (!isDifferent)
|
||||||
|
{
|
||||||
$(dialogId).css({
|
$(dialogId).css({
|
||||||
width: dialogSize[0] - self._contentDelta[0],
|
width: dialogSize[0] - self._contentDelta[0],
|
||||||
maxHeight: dialogSize[1] - self._contentDelta[1]
|
maxHeight: dialogSize[1] - self._contentDelta[1]
|
||||||
@ -499,7 +551,7 @@ export class GUI
|
|||||||
left: Math.max(0, (windowSize[0] - parent.outerWidth()) / 2.0),
|
left: Math.max(0, (windowSize[0] - parent.outerWidth()) / 2.0),
|
||||||
top: Math.max(0, (windowSize[1] - parent.outerHeight()) / 2.0),
|
top: Math.max(0, (windowSize[1] - parent.outerHeight()) / 2.0),
|
||||||
});
|
});
|
||||||
} );
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -511,11 +563,13 @@ export class GUI
|
|||||||
* @private
|
* @private
|
||||||
* @param {Object.<string, string|Symbol>} signal the signal
|
* @param {Object.<string, string|Symbol>} signal the signal
|
||||||
*/
|
*/
|
||||||
_onResourceEvents(signal) {
|
_onResourceEvents(signal)
|
||||||
|
{
|
||||||
this._psychoJS.logger.debug('signal: ' + util.toString(signal));
|
this._psychoJS.logger.debug('signal: ' + util.toString(signal));
|
||||||
|
|
||||||
// all resources have been registered:
|
// all resources have been registered:
|
||||||
if (signal.message === ServerManager.Event.RESOURCES_REGISTERED) {
|
if (signal.message === ServerManager.Event.RESOURCES_REGISTERED)
|
||||||
|
{
|
||||||
// for each resource, we have a 'downloading resource' and a 'resource downloaded' message:
|
// for each resource, we have a 'downloading resource' and a 'resource downloaded' message:
|
||||||
this._progressBarMax = signal.count * 2;
|
this._progressBarMax = signal.count * 2;
|
||||||
$("#progressbar").progressbar("option", "max", this._progressBarMax);
|
$("#progressbar").progressbar("option", "max", this._progressBarMax);
|
||||||
@ -525,7 +579,8 @@ export class GUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// all the resources have been downloaded: show the ok button
|
// all the resources have been downloaded: show the ok button
|
||||||
else if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED) {
|
else if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED)
|
||||||
|
{
|
||||||
this._allResourcesDownloaded = true;
|
this._allResourcesDownloaded = true;
|
||||||
$("#progressMsg").text('all resources downloaded.');
|
$("#progressMsg").text('all resources downloaded.');
|
||||||
this._updateOkButtonStatus();
|
this._updateOkButtonStatus();
|
||||||
@ -535,21 +590,27 @@ export class GUI
|
|||||||
else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
||||||
{
|
{
|
||||||
if (typeof this._progressBarCurrentIncrement === 'undefined')
|
if (typeof this._progressBarCurrentIncrement === 'undefined')
|
||||||
|
{
|
||||||
this._progressBarCurrentIncrement = 0;
|
this._progressBarCurrentIncrement = 0;
|
||||||
++ this._progressBarCurrentIncrement;
|
}
|
||||||
|
++this._progressBarCurrentIncrement;
|
||||||
|
|
||||||
if (signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
if (signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
||||||
$("#progressMsg").text('downloaded ' + this._progressBarCurrentIncrement/2 + ' / ' + this._progressBarMax/2);
|
{
|
||||||
|
$("#progressMsg").text('downloaded ' + this._progressBarCurrentIncrement / 2 + ' / ' + this._progressBarMax / 2);
|
||||||
|
}
|
||||||
// $("#progressMsg").text(signal.resource + ': downloaded.');
|
// $("#progressMsg").text(signal.resource + ': downloaded.');
|
||||||
// else
|
// else
|
||||||
// $("#progressMsg").text(signal.resource + ': downloading...');
|
// $("#progressMsg").text(signal.resource + ': downloading...');
|
||||||
|
|
||||||
$("#progressbar").progressbar("option", "value", this._progressBarCurrentIncrement);
|
$("#progressbar").progressbar("option", "value", this._progressBarCurrentIncrement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// unknown message: we just display it
|
// unknown message: we just display it
|
||||||
else
|
else
|
||||||
|
{
|
||||||
$("#progressMsg").text(signal.message);
|
$("#progressMsg").text(signal.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -562,15 +623,21 @@ export class GUI
|
|||||||
*/
|
*/
|
||||||
_updateOkButtonStatus()
|
_updateOkButtonStatus()
|
||||||
{
|
{
|
||||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL || (this._allResourcesDownloaded && this._setRequiredKeys.size >= this._requiredKeys.length) )
|
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL || (this._allResourcesDownloaded && this._setRequiredKeys.size >= this._requiredKeys.length))
|
||||||
{
|
{
|
||||||
$("#buttonOk").button("option", "disabled", false);
|
$("#buttonOk").button("option", "disabled", false);
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
$("#buttonOk").button("option", "disabled", true);
|
$("#buttonOk").button("option", "disabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
// strangely, changing the disabled option sometimes fails to update the ui,
|
// strangely, changing the disabled option sometimes fails to update the ui,
|
||||||
// so we need to hide it and show it again:
|
// so we need to hide it and show it again:
|
||||||
$("#buttonOk").hide(0, () => { $("#buttonOk").show(); });
|
$("#buttonOk").hide(0, () =>
|
||||||
|
{
|
||||||
|
$("#buttonOk").show();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -582,20 +649,25 @@ export class GUI
|
|||||||
* @private
|
* @private
|
||||||
* @returns {boolean} whether or not the scaling factor is different from the previously estimated one
|
* @returns {boolean} whether or not the scaling factor is different from the previously estimated one
|
||||||
*/
|
*/
|
||||||
_estimateDialogScalingFactor() {
|
_estimateDialogScalingFactor()
|
||||||
|
{
|
||||||
const windowSize = [$(window).width(), $(window).height()];
|
const windowSize = [$(window).width(), $(window).height()];
|
||||||
|
|
||||||
// desktop:
|
// desktop:
|
||||||
let dialogScalingFactor = 1.0;
|
let dialogScalingFactor = 1.0;
|
||||||
|
|
||||||
// mobile or tablet:
|
// mobile or tablet:
|
||||||
if (windowSize[0] < 1080) {
|
if (windowSize[0] < 1080)
|
||||||
|
{
|
||||||
// landscape:
|
// landscape:
|
||||||
if (windowSize[0] > windowSize[1])
|
if (windowSize[0] > windowSize[1])
|
||||||
|
{
|
||||||
dialogScalingFactor = 1.5;
|
dialogScalingFactor = 1.5;
|
||||||
// portrait:
|
}// portrait:
|
||||||
else
|
else
|
||||||
|
{
|
||||||
dialogScalingFactor = 2.0;
|
dialogScalingFactor = 2.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDifferent = (dialogScalingFactor !== this._dialogScalingFactor);
|
const isDifferent = (dialogScalingFactor !== this._dialogScalingFactor);
|
||||||
@ -612,13 +684,14 @@ export class GUI
|
|||||||
* @private
|
* @private
|
||||||
* @returns {number[]} the size of the popup dialog window
|
* @returns {number[]} the size of the popup dialog window
|
||||||
*/
|
*/
|
||||||
_getDialogSize() {
|
_getDialogSize()
|
||||||
|
{
|
||||||
const windowSize = [$(window).width(), $(window).height()];
|
const windowSize = [$(window).width(), $(window).height()];
|
||||||
this._estimateDialogScalingFactor();
|
this._estimateDialogScalingFactor();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Math.min(GUI.dialogMaxSize[0], (windowSize[0]-GUI.dialogMargin[0]) / this._dialogScalingFactor),
|
Math.min(GUI.dialogMaxSize[0], (windowSize[0] - GUI.dialogMargin[0]) / this._dialogScalingFactor),
|
||||||
Math.min(GUI.dialogMaxSize[1], (windowSize[1]-GUI.dialogMargin[1]) / this._dialogScalingFactor)];
|
Math.min(GUI.dialogMaxSize[1], (windowSize[1] - GUI.dialogMargin[1]) / this._dialogScalingFactor)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -632,14 +705,19 @@ export class GUI
|
|||||||
* @param {module:core.GUI} gui - this GUI
|
* @param {module:core.GUI} gui - this GUI
|
||||||
* @param {Event} event - event
|
* @param {Event} event - event
|
||||||
*/
|
*/
|
||||||
static _onKeyChange(gui, event) {
|
static _onKeyChange(gui, event)
|
||||||
|
{
|
||||||
const element = event.target;
|
const element = event.target;
|
||||||
const value = element.value;
|
const value = element.value;
|
||||||
|
|
||||||
if (typeof value !== 'undefined' && value.length > 0)
|
if (typeof value !== 'undefined' && value.length > 0)
|
||||||
|
{
|
||||||
gui._setRequiredKeys.set(event.target, true);
|
gui._setRequiredKeys.set(event.target, true);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
gui._setRequiredKeys.delete(event.target);
|
gui._setRequiredKeys.delete(event.target);
|
||||||
|
}
|
||||||
|
|
||||||
gui._updateOkButtonStatus();
|
gui._updateOkButtonStatus();
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Manager handling the keyboard events.
|
* Manager handling the keyboard events.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -21,8 +21,10 @@ import {EventManager} from "./EventManager";
|
|||||||
* @param {number} tDown - time of key press (keydown event) relative to the global Monotonic Clock
|
* @param {number} tDown - time of key press (keydown event) relative to the global Monotonic Clock
|
||||||
* @param {string | undefined} name - pyglet key name
|
* @param {string | undefined} name - pyglet key name
|
||||||
*/
|
*/
|
||||||
export class KeyPress {
|
export class KeyPress
|
||||||
constructor(code, tDown, name) {
|
{
|
||||||
|
constructor(code, tDown, name)
|
||||||
|
{
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.tDown = tDown;
|
this.tDown = tDown;
|
||||||
this.name = (typeof name !== 'undefined') ? name : EventManager.w3c2pyglet(code);
|
this.name = (typeof name !== 'undefined') ? name : EventManager.w3c2pyglet(code);
|
||||||
@ -49,7 +51,8 @@ export class KeyPress {
|
|||||||
* @param {Clock} [options.clock= undefined] - an optional clock
|
* @param {Clock} [options.clock= undefined] - an optional clock
|
||||||
* @param {boolean} options.autoLog - whether or not to log
|
* @param {boolean} options.autoLog - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class Keyboard extends PsychObject {
|
export class Keyboard extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
@ -57,16 +60,19 @@ export class Keyboard extends PsychObject {
|
|||||||
waitForStart = false,
|
waitForStart = false,
|
||||||
clock,
|
clock,
|
||||||
autoLog = false,
|
autoLog = false,
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
|
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
if (typeof clock === 'undefined')
|
if (typeof clock === 'undefined')
|
||||||
clock = new Clock(); //this._psychoJS.monotonicClock;
|
{
|
||||||
|
clock = new Clock();
|
||||||
|
} //this._psychoJS.monotonicClock;
|
||||||
|
|
||||||
this._addAttributes(Keyboard, bufferSize, waitForStart, clock, autoLog);
|
this._addAttributes(Keyboard, bufferSize, waitForStart, clock, autoLog);
|
||||||
// start recording key events if need be:
|
// start recording key events if need be:
|
||||||
this._addAttribute('status', (waitForStart)?PsychoJS.Status.NOT_STARTED:PsychoJS.Status.STARTED);
|
this._addAttribute('status', (waitForStart) ? PsychoJS.Status.NOT_STARTED : PsychoJS.Status.STARTED);
|
||||||
|
|
||||||
// setup circular buffer:
|
// setup circular buffer:
|
||||||
this.clearEvents();
|
this.clearEvents();
|
||||||
@ -85,7 +91,8 @@ export class Keyboard extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
start() {
|
start()
|
||||||
|
{
|
||||||
this._status = PsychoJS.Status.STARTED;
|
this._status = PsychoJS.Status.STARTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +105,8 @@ export class Keyboard extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop()
|
||||||
|
{
|
||||||
this._status = PsychoJS.Status.STOPPED;
|
this._status = PsychoJS.Status.STOPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,20 +129,26 @@ export class Keyboard extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Keyboard.KeyEvent[]} the list of events still in the buffer
|
* @return {Keyboard.KeyEvent[]} the list of events still in the buffer
|
||||||
*/
|
*/
|
||||||
getEvents() {
|
getEvents()
|
||||||
|
{
|
||||||
if (this._bufferLength === 0)
|
if (this._bufferLength === 0)
|
||||||
|
{
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// iterate over the buffer, from start to end, and discard the null event:
|
// iterate over the buffer, from start to end, and discard the null event:
|
||||||
let filteredEvents = [];
|
let filteredEvents = [];
|
||||||
const bufferWrap = (this._bufferLength === this._bufferSize);
|
const bufferWrap = (this._bufferLength === this._bufferSize);
|
||||||
let i = bufferWrap ? this._bufferIndex : -1;
|
let i = bufferWrap ? this._bufferIndex : -1;
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
i = (i + 1) % this._bufferSize;
|
i = (i + 1) % this._bufferSize;
|
||||||
const keyEvent = this._circularBuffer[i];
|
const keyEvent = this._circularBuffer[i];
|
||||||
if (keyEvent)
|
if (keyEvent)
|
||||||
|
{
|
||||||
filteredEvents.push(keyEvent);
|
filteredEvents.push(keyEvent);
|
||||||
|
}
|
||||||
} while (i !== this._bufferIndex);
|
} while (i !== this._bufferIndex);
|
||||||
|
|
||||||
return filteredEvents;
|
return filteredEvents;
|
||||||
@ -160,29 +174,37 @@ export class Keyboard extends PsychObject {
|
|||||||
keyList = [],
|
keyList = [],
|
||||||
waitRelease = true,
|
waitRelease = true,
|
||||||
clear = true
|
clear = true
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
|
|
||||||
// if nothing in the buffer, return immediately:
|
// if nothing in the buffer, return immediately:
|
||||||
if (this._bufferLength === 0)
|
if (this._bufferLength === 0)
|
||||||
|
{
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
let keyPresses = [];
|
let keyPresses = [];
|
||||||
|
|
||||||
// iterate over the circular buffer, looking for keyup events:
|
// iterate over the circular buffer, looking for keyup events:
|
||||||
const bufferWrap = (this._bufferLength === this._bufferSize);
|
const bufferWrap = (this._bufferLength === this._bufferSize);
|
||||||
let i = bufferWrap ? this._bufferIndex : -1;
|
let i = bufferWrap ? this._bufferIndex : -1;
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
i = (i + 1) % this._bufferSize;
|
i = (i + 1) % this._bufferSize;
|
||||||
|
|
||||||
const keyEvent = this._circularBuffer[i];
|
const keyEvent = this._circularBuffer[i];
|
||||||
if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP) {
|
if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP)
|
||||||
|
{
|
||||||
// check that the key is in the keyList:
|
// check that the key is in the keyList:
|
||||||
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
|
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey))
|
||||||
|
{
|
||||||
// look for a corresponding, preceding keydown event:
|
// look for a corresponding, preceding keydown event:
|
||||||
const precedingKeydownIndex = keyEvent.keydownIndex;
|
const precedingKeydownIndex = keyEvent.keydownIndex;
|
||||||
if (typeof precedingKeydownIndex !== 'undefined') {
|
if (typeof precedingKeydownIndex !== 'undefined')
|
||||||
|
{
|
||||||
const precedingKeydownEvent = this._circularBuffer[precedingKeydownIndex];
|
const precedingKeydownEvent = this._circularBuffer[precedingKeydownIndex];
|
||||||
if (precedingKeydownEvent) {
|
if (precedingKeydownEvent)
|
||||||
|
{
|
||||||
// prepare KeyPress and add it to the array:
|
// prepare KeyPress and add it to the array:
|
||||||
const tDown = precedingKeydownEvent.timestamp;
|
const tDown = precedingKeydownEvent.timestamp;
|
||||||
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
||||||
@ -191,7 +213,9 @@ export class Keyboard extends PsychObject {
|
|||||||
keyPresses.push(keyPress);
|
keyPresses.push(keyPress);
|
||||||
|
|
||||||
if (clear)
|
if (clear)
|
||||||
|
{
|
||||||
this._circularBuffer[precedingKeydownIndex] = null;
|
this._circularBuffer[precedingKeydownIndex] = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +244,9 @@ export class Keyboard extends PsychObject {
|
|||||||
} while ((bufferWrap && j !== i) || (j > -1));*/
|
} while ((bufferWrap && j !== i) || (j > -1));*/
|
||||||
|
|
||||||
if (clear)
|
if (clear)
|
||||||
|
{
|
||||||
this._circularBuffer[i] = null;
|
this._circularBuffer[i] = null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,18 +255,23 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
|
|
||||||
// if waitRelease = false, we iterate again over the map of unmatched keydown events:
|
// if waitRelease = false, we iterate again over the map of unmatched keydown events:
|
||||||
if (!waitRelease) {
|
if (!waitRelease)
|
||||||
for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values()) {
|
{
|
||||||
|
for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values())
|
||||||
|
{
|
||||||
const keyEvent = this._circularBuffer[unmatchedKeyDownIndex];
|
const keyEvent = this._circularBuffer[unmatchedKeyDownIndex];
|
||||||
if (keyEvent) {
|
if (keyEvent)
|
||||||
|
{
|
||||||
// check that the key is in the keyList:
|
// check that the key is in the keyList:
|
||||||
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
|
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey))
|
||||||
|
{
|
||||||
const tDown = keyEvent.timestamp;
|
const tDown = keyEvent.timestamp;
|
||||||
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
||||||
keyPress.rt = tDown - this._clock.getLastResetTime();
|
keyPress.rt = tDown - this._clock.getLastResetTime();
|
||||||
keyPresses.push(keyPress);
|
keyPresses.push(keyPress);
|
||||||
|
|
||||||
if (clear) {
|
if (clear)
|
||||||
|
{
|
||||||
this._unmatchedKeydownMap.delete(keyEvent.code);
|
this._unmatchedKeydownMap.delete(keyEvent.code);
|
||||||
this._circularBuffer[unmatchedKeyDownIndex] = null;
|
this._circularBuffer[unmatchedKeyDownIndex] = null;
|
||||||
}
|
}
|
||||||
@ -272,14 +303,15 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
// if clear = true and the keyList is empty, we clear all the events:
|
// if clear = true and the keyList is empty, we clear all the events:
|
||||||
if (clear && keyList.length === 0)
|
if (clear && keyList.length === 0)
|
||||||
|
{
|
||||||
this.clearEvents();
|
this.clearEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return keyPresses;
|
return keyPresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all events and resets the circular buffers.
|
* Clear all events and resets the circular buffers.
|
||||||
*
|
*
|
||||||
@ -299,7 +331,6 @@ export class Keyboard extends PsychObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test whether a list of KeyPress's contains one with a particular name.
|
* Test whether a list of KeyPress's contains one with a particular name.
|
||||||
*
|
*
|
||||||
@ -317,12 +348,11 @@ export class Keyboard extends PsychObject {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = keypressList.find( (keypress) => keypress.name === keyName );
|
const value = keypressList.find((keypress) => keypress.name === keyName);
|
||||||
return (typeof value !== 'undefined');
|
return (typeof value !== 'undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add key listeners to the document.
|
* Add key listeners to the document.
|
||||||
*
|
*
|
||||||
@ -338,17 +368,21 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
// add a keydown listener:
|
// add a keydown listener:
|
||||||
window.addEventListener("keydown", (event) =>
|
window.addEventListener("keydown", (event) =>
|
||||||
// document.addEventListener("keydown", (event) =>
|
// document.addEventListener("keydown", (event) =>
|
||||||
{
|
{
|
||||||
// only consider non-repeat events, i.e. only the first keydown event associated with a participant
|
// only consider non-repeat events, i.e. only the first keydown event associated with a participant
|
||||||
// holding a key down:
|
// holding a key down:
|
||||||
if (event.repeat)
|
if (event.repeat)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
||||||
|
|
||||||
if (this._status !== PsychoJS.Status.STARTED)
|
if (this._status !== PsychoJS.Status.STARTED)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DEPRECATED: we now use event.repeat
|
* DEPRECATED: we now use event.repeat
|
||||||
@ -362,7 +396,9 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
||||||
if (typeof code === 'undefined')
|
if (typeof code === 'undefined')
|
||||||
|
{
|
||||||
code = EventManager.keycode2w3c(event.keyCode);
|
code = EventManager.keycode2w3c(event.keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
let pigletKey = EventManager.w3c2pyglet(code);
|
let pigletKey = EventManager.w3c2pyglet(code);
|
||||||
|
|
||||||
@ -387,12 +423,14 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
// add a keyup listener:
|
// add a keyup listener:
|
||||||
window.addEventListener("keyup", (event) =>
|
window.addEventListener("keyup", (event) =>
|
||||||
// document.addEventListener("keyup", (event) =>
|
// document.addEventListener("keyup", (event) =>
|
||||||
{
|
{
|
||||||
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
||||||
|
|
||||||
if (this._status !== PsychoJS.Status.STARTED)
|
if (this._status !== PsychoJS.Status.STARTED)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self._previousKeydownKey = undefined;
|
self._previousKeydownKey = undefined;
|
||||||
|
|
||||||
@ -400,7 +438,9 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
// take care of legacy Microsoft Edge:
|
// take care of legacy Microsoft Edge:
|
||||||
if (typeof code === 'undefined')
|
if (typeof code === 'undefined')
|
||||||
|
{
|
||||||
code = EventManager.keycode2w3c(event.keyCode);
|
code = EventManager.keycode2w3c(event.keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
let pigletKey = EventManager.w3c2pyglet(code);
|
let pigletKey = EventManager.w3c2pyglet(code);
|
||||||
|
|
||||||
@ -418,7 +458,8 @@ export class Keyboard extends PsychObject {
|
|||||||
// note: if more keys are down than there are slots in the circular buffer, there might
|
// note: if more keys are down than there are slots in the circular buffer, there might
|
||||||
// not be a corresponding keydown event
|
// not be a corresponding keydown event
|
||||||
const correspondingKeydownIndex = self._unmatchedKeydownMap.get(event.code);
|
const correspondingKeydownIndex = self._unmatchedKeydownMap.get(event.code);
|
||||||
if (typeof correspondingKeydownIndex !== 'undefined') {
|
if (typeof correspondingKeydownIndex !== 'undefined')
|
||||||
|
{
|
||||||
self._circularBuffer[self._bufferIndex].keydownIndex = correspondingKeydownIndex;
|
self._circularBuffer[self._bufferIndex].keydownIndex = correspondingKeydownIndex;
|
||||||
self._unmatchedKeydownMap.delete(event.code);
|
self._unmatchedKeydownMap.delete(event.code);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Logger
|
* Logger
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -10,20 +10,21 @@
|
|||||||
|
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
import {MonotonicClock} from '../util/Clock';
|
import {MonotonicClock} from '../util/Clock';
|
||||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This class handles a variety of loggers, e.g. a browser console one (mostly for debugging),
|
* <p>This class handles a variety of loggers, e.g. a browser console one (mostly for debugging),
|
||||||
* a remote one, etc.</p>
|
* a remote one, etc.</p>
|
||||||
*
|
*
|
||||||
* <p>Note: we use log4javascript for the console logger, and our own for the server logger.</p>
|
* <p>Note: we use log4javascript for the console logger, and our own for the server logger.</p>
|
||||||
*
|
*
|
||||||
* @name module:core.Logger
|
* @name module:core.Logger
|
||||||
* @class
|
* @class
|
||||||
* @param {*} threshold - the logging threshold, e.g. log4javascript.Level.ERROR
|
* @param {*} threshold - the logging threshold, e.g. log4javascript.Level.ERROR
|
||||||
*/
|
*/
|
||||||
export class Logger {
|
export class Logger
|
||||||
|
{
|
||||||
|
|
||||||
constructor(psychoJS, threshold)
|
constructor(psychoJS, threshold)
|
||||||
{
|
{
|
||||||
@ -80,11 +81,10 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a server message at the DATA level.
|
* Log a server message at the DATA level.
|
||||||
*
|
*
|
||||||
* @name module:core.Logger#data
|
* @name module:core.Logger#data
|
||||||
* @public
|
* @public
|
||||||
* @param {string} msg - the message to be logged.
|
* @param {string} msg - the message to be logged.
|
||||||
* @param {number} [time] - the logging time
|
* @param {number} [time] - the logging time
|
||||||
@ -96,7 +96,6 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a server message.
|
* Log a server message.
|
||||||
*
|
*
|
||||||
@ -110,7 +109,9 @@ export class Logger {
|
|||||||
log(msg, level, time, obj)
|
log(msg, level, time, obj)
|
||||||
{
|
{
|
||||||
if (typeof time === 'undefined')
|
if (typeof time === 'undefined')
|
||||||
|
{
|
||||||
time = MonotonicClock.getReferenceTime();
|
time = MonotonicClock.getReferenceTime();
|
||||||
|
}
|
||||||
|
|
||||||
this._serverLogs.push({
|
this._serverLogs.push({
|
||||||
msg,
|
msg,
|
||||||
@ -122,7 +123,6 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush all server logs to the server.
|
* Flush all server logs to the server.
|
||||||
*
|
*
|
||||||
@ -149,7 +149,9 @@ export class Logger {
|
|||||||
'\t' + Symbol.keyFor(log.level) +
|
'\t' + Symbol.keyFor(log.level) +
|
||||||
'\t' + log.msg;
|
'\t' + log.msg;
|
||||||
if (log.obj !== 'undefined')
|
if (log.obj !== 'undefined')
|
||||||
|
{
|
||||||
formattedLog += '\t' + log.obj;
|
formattedLog += '\t' + log.obj;
|
||||||
|
}
|
||||||
formattedLog += '\n';
|
formattedLog += '\n';
|
||||||
|
|
||||||
formattedLogs += formattedLog;
|
formattedLogs += formattedLog;
|
||||||
@ -157,7 +159,8 @@ export class Logger {
|
|||||||
|
|
||||||
// send logs to the server or display them in the console:
|
// send logs to the server or display them in the console:
|
||||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
||||||
this._psychoJS.config.experiment.status === 'RUNNING')
|
this._psychoJS.config.experiment.status === 'RUNNING' &&
|
||||||
|
!this._psychoJS._serverMsg.has('__pilotToken'))
|
||||||
{
|
{
|
||||||
// if the pako compression library is present, we compress the logs:
|
// if the pako compression library is present, we compress the logs:
|
||||||
if (typeof pako !== 'undefined')
|
if (typeof pako !== 'undefined')
|
||||||
@ -189,10 +192,9 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a custom console layout.
|
* Create a custom console layout.
|
||||||
*
|
*
|
||||||
* @name module:core.Logger#_customConsoleLayout
|
* @name module:core.Logger#_customConsoleLayout
|
||||||
* @private
|
* @private
|
||||||
* @return {*} the custom layout
|
* @return {*} the custom layout
|
||||||
@ -219,10 +221,13 @@ export class Logger {
|
|||||||
{
|
{
|
||||||
// look for entry immediately after those of log4javascript:
|
// look for entry immediately after those of log4javascript:
|
||||||
for (let entry of stackEntries)
|
for (let entry of stackEntries)
|
||||||
if (entry.indexOf('log4javascript.min.js') <= 0) {
|
{
|
||||||
|
if (entry.indexOf('log4javascript.min.js') <= 0)
|
||||||
|
{
|
||||||
relevantEntry = entry;
|
relevantEntry = entry;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const buf = relevantEntry.split(':');
|
const buf = relevantEntry.split(':');
|
||||||
const line = buf[buf.length - 2];
|
const line = buf[buf.length - 2];
|
||||||
@ -242,15 +247,17 @@ export class Logger {
|
|||||||
let buf = relevantEntry.split(' ');
|
let buf = relevantEntry.split(' ');
|
||||||
let fileLine = buf.pop();
|
let fileLine = buf.pop();
|
||||||
const method = buf.pop();
|
const method = buf.pop();
|
||||||
buf = fileLine.split(':'); buf.pop();
|
buf = fileLine.split(':');
|
||||||
|
buf.pop();
|
||||||
const line = buf.pop();
|
const line = buf.pop();
|
||||||
const file = buf.pop().split('/').pop();
|
const file = buf.pop().split('/').pop();
|
||||||
|
|
||||||
return method + ' ' + file + ' ' + line;
|
return method + ' ' + file + ' ' + line;
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* Base class for all stimuli.
|
* Base class for all stimuli.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import { PsychoJS } from './PsychoJS';
|
import {PsychoJS} from './PsychoJS';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>MinimalStim is the base class for all stimuli.</p>
|
* <p>MinimalStim is the base class for all stimuli.</p>
|
||||||
*
|
*
|
||||||
* @name module:core.MinimalStim
|
* @name module:core.MinimalStim
|
||||||
* @class
|
* @class
|
||||||
* @extends PsychObject
|
* @extends PsychObject
|
||||||
@ -26,11 +26,11 @@ import { PsychoJS } from './PsychoJS';
|
|||||||
export class MinimalStim extends PsychObject
|
export class MinimalStim extends PsychObject
|
||||||
{
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
autoDraw = false,
|
autoDraw = false,
|
||||||
autoLog = win.autoLog
|
autoLog = win.autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super(win._psychoJS, name);
|
super(win._psychoJS, name);
|
||||||
|
|
||||||
@ -55,31 +55,41 @@ export class MinimalStim extends PsychObject
|
|||||||
*/
|
*/
|
||||||
setAutoDraw(autoDraw, log = false)
|
setAutoDraw(autoDraw, log = false)
|
||||||
{
|
{
|
||||||
let response = { origin : 'MinimalStim.setAutoDraw', context: 'when setting the autoDraw attribute of stimulus: ' + this._name };
|
let response = {
|
||||||
|
origin: 'MinimalStim.setAutoDraw',
|
||||||
|
context: 'when setting the autoDraw attribute of stimulus: ' + this._name
|
||||||
|
};
|
||||||
|
|
||||||
this._setAttribute('autoDraw', autoDraw, log);
|
this._setAttribute('autoDraw', autoDraw, log);
|
||||||
|
|
||||||
const index = this.win._drawList.indexOf(this);
|
const index = this.win._drawList.indexOf(this);
|
||||||
|
|
||||||
// autoDraw = true: add the stimulus to the draw list if it's not there already
|
// autoDraw = true: add the stimulus to the draw list if it's not there already
|
||||||
if (this._autoDraw) {
|
if (this._autoDraw)
|
||||||
if (this.win) {
|
{
|
||||||
|
if (this.win)
|
||||||
|
{
|
||||||
// if the stimilus is not already in the draw list:
|
// if the stimilus is not already in the draw list:
|
||||||
if (index < 0) {
|
if (index < 0)
|
||||||
|
{
|
||||||
// update the stimulus if need be before we add its PIXI representation to the window container:
|
// update the stimulus if need be before we add its PIXI representation to the window container:
|
||||||
this._updateIfNeeded();
|
this._updateIfNeeded();
|
||||||
if (typeof this._pixi === 'undefined')
|
if (typeof this._pixi === 'undefined')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('the Pixi.js representation of this stimulus is undefined.');
|
this.psychoJS.logger.warn('the Pixi.js representation of this stimulus is undefined.');
|
||||||
// throw Object.assign(response, { error: 'the PIXI representation of the stimulus is unavailable'});
|
}// throw Object.assign(response, { error: 'the PIXI representation of the stimulus is unavailable'});
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
this.win._rootContainer.addChild(this._pixi);
|
this.win._rootContainer.addChild(this._pixi);
|
||||||
this.win._drawList.push(this);
|
this.win._drawList.push(this);
|
||||||
}
|
}
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
// the stimulus is already in the list, if it needs to be updated, we remove it
|
// the stimulus is already in the list, if it needs to be updated, we remove it
|
||||||
// from the window container, update it, then put it back:
|
// from the window container, update it, then put it back:
|
||||||
if (this._needUpdate && typeof this._pixi !== 'undefined') {
|
if (this._needUpdate && typeof this._pixi !== 'undefined')
|
||||||
|
{
|
||||||
this.win._rootContainer.removeChild(this._pixi);
|
this.win._rootContainer.removeChild(this._pixi);
|
||||||
this._updateIfNeeded();
|
this._updateIfNeeded();
|
||||||
this.win._rootContainer.addChild(this._pixi);
|
this.win._rootContainer.addChild(this._pixi);
|
||||||
@ -91,13 +101,18 @@ export class MinimalStim extends PsychObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
// autoDraw = false: remove the stimulus from the draw list and window container if it's already there
|
// autoDraw = false: remove the stimulus from the draw list and window container if it's already there
|
||||||
else {
|
else
|
||||||
if (this.win) {
|
{
|
||||||
|
if (this.win)
|
||||||
|
{
|
||||||
// if the stimulus is in the draw list, remove it from the list and from the window container:
|
// if the stimulus is in the draw list, remove it from the list and from the window container:
|
||||||
if (index >= 0) {
|
if (index >= 0)
|
||||||
|
{
|
||||||
this.win._drawList.splice(index, 1);
|
this.win._drawList.splice(index, 1);
|
||||||
if (typeof this._pixi !== 'undefined')
|
if (typeof this._pixi !== 'undefined')
|
||||||
|
{
|
||||||
this.win._rootContainer.removeChild(this._pixi);
|
this.win._rootContainer.removeChild(this._pixi);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +123,7 @@ export class MinimalStim extends PsychObject
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw this stimulus on the next frame draw.
|
* Draw this stimulus on the next frame draw.
|
||||||
*
|
*
|
||||||
* @name module:core.MinimalStim#draw
|
* @name module:core.MinimalStim#draw
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -127,7 +142,7 @@ export class MinimalStim extends PsychObject
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether an object is inside this stimulus.
|
* Determine whether an object is inside this stimulus.
|
||||||
*
|
*
|
||||||
* @name module:core.MinimalStim#contains
|
* @name module:core.MinimalStim#contains
|
||||||
* @function
|
* @function
|
||||||
* @abstract
|
* @abstract
|
||||||
@ -137,7 +152,11 @@ export class MinimalStim extends PsychObject
|
|||||||
*/
|
*/
|
||||||
contains(object, units)
|
contains(object, units)
|
||||||
{
|
{
|
||||||
throw {origin: 'MinimalStim.contains', context: `when determining whether stimulus: ${this._name} contains object: ${util.toString(object)}`, error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'MinimalStim.contains',
|
||||||
|
context: `when determining whether stimulus: ${this._name} contains object: ${util.toString(object)}`,
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -145,7 +164,7 @@ export class MinimalStim extends PsychObject
|
|||||||
* Update the stimulus, if necessary.
|
* Update the stimulus, if necessary.
|
||||||
*
|
*
|
||||||
* Note: this is an abstract function, which should not be called.
|
* Note: this is an abstract function, which should not be called.
|
||||||
*
|
*
|
||||||
* @name module:core.MinimalStim#_updateIfNeeded
|
* @name module:core.MinimalStim#_updateIfNeeded
|
||||||
* @function
|
* @function
|
||||||
* @abstract
|
* @abstract
|
||||||
@ -153,6 +172,10 @@ export class MinimalStim extends PsychObject
|
|||||||
*/
|
*/
|
||||||
_updateIfNeeded()
|
_updateIfNeeded()
|
||||||
{
|
{
|
||||||
throw {origin: 'MinimalStim._updateIfNeeded', context: 'when updating stimulus: ' + this._name, error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'MinimalStim._updateIfNeeded',
|
||||||
|
context: 'when updating stimulus: ' + this._name,
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
127
js/core/Mouse.js
127
js/core/Mouse.js
@ -1,21 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* 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 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PsychoJS } from './PsychoJS';
|
import {PsychoJS} from './PsychoJS';
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This manager handles the interactions between the experiment's stimuli and the mouse.</p>
|
* <p>This manager handles the interactions between the experiment's stimuli and the mouse.</p>
|
||||||
* <p>Note: the unit of Mouse is that of its associated Window.</p>
|
* <p>Note: the unit of Mouse is that of its associated Window.</p>
|
||||||
*
|
*
|
||||||
* @name module:core.Mouse
|
* @name module:core.Mouse
|
||||||
* @class
|
* @class
|
||||||
* @extends PsychObject
|
* @extends PsychObject
|
||||||
@ -23,16 +23,18 @@ import * as util from '../util/Util';
|
|||||||
* @param {String} options.name - the name used when logging messages from this stimulus
|
* @param {String} options.name - the name used when logging messages from this stimulus
|
||||||
* @param {Window} options.win - the associated Window
|
* @param {Window} options.win - the associated Window
|
||||||
* @param {boolean} [options.autoLog= true] - whether or not to log
|
* @param {boolean} [options.autoLog= true] - whether or not to log
|
||||||
*
|
*
|
||||||
* @todo visible is not handled at the moment (mouse is always visible)
|
* @todo visible is not handled at the moment (mouse is always visible)
|
||||||
*/
|
*/
|
||||||
export class Mouse extends PsychObject {
|
export class Mouse extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
autoLog = true
|
autoLog = true
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(win._psychoJS, name);
|
super(win._psychoJS, name);
|
||||||
|
|
||||||
// note: those are in window units:
|
// note: those are in window units:
|
||||||
@ -50,13 +52,14 @@ export class Mouse extends PsychObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current position of the mouse in mouse/Window units.
|
* Get the current position of the mouse in mouse/Window units.
|
||||||
*
|
*
|
||||||
* @name module:core.Mouse#getPos
|
* @name module:core.Mouse#getPos
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {Array.number} the position of the mouse in mouse/Window units
|
* @return {Array.number} the position of the mouse in mouse/Window units
|
||||||
*/
|
*/
|
||||||
getPos() {
|
getPos()
|
||||||
|
{
|
||||||
// get mouse position in the canvas:
|
// get mouse position in the canvas:
|
||||||
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
||||||
let pos_px = mouseInfo.pos.slice();
|
let pos_px = mouseInfo.pos.slice();
|
||||||
@ -75,16 +78,20 @@ export class Mouse extends PsychObject {
|
|||||||
/**
|
/**
|
||||||
* Get the position of the mouse relative to that at the last call to getRel
|
* Get the position of the mouse relative to that at the last call to getRel
|
||||||
* or getPos, in mouse/Window units.
|
* or getPos, in mouse/Window units.
|
||||||
*
|
*
|
||||||
* @name module:core.Mouse#getRel
|
* @name module:core.Mouse#getRel
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {Array.number} the relation position of the mouse in mouse/Window units.
|
* @return {Array.number} the relation position of the mouse in mouse/Window units.
|
||||||
*/
|
*/
|
||||||
getRel() {
|
getRel()
|
||||||
|
{
|
||||||
if (typeof this._lastPos === 'undefined')
|
if (typeof this._lastPos === 'undefined')
|
||||||
|
{
|
||||||
return this.getPos();
|
return this.getPos();
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// note: (this.getPos()-lastPos) would not work here since getPos changes this._lastPos
|
// note: (this.getPos()-lastPos) would not work here since getPos changes this._lastPos
|
||||||
const lastPos = this._lastPos;
|
const lastPos = this._lastPos;
|
||||||
const pos = this.getPos();
|
const pos = this.getPos();
|
||||||
@ -95,10 +102,10 @@ export class Mouse extends PsychObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the travel of the mouse scroll wheel since the last call to getWheelRel.
|
* Get the travel of the mouse scroll wheel since the last call to getWheelRel.
|
||||||
*
|
*
|
||||||
* <p>Note: Even though this method returns a [x, y] array, for most wheels/systems y is the only
|
* <p>Note: Even though this method returns a [x, y] array, for most wheels/systems y is the only
|
||||||
* value that varies.</p>
|
* value that varies.</p>
|
||||||
*
|
*
|
||||||
* @name module:core.Mouse#getWheelRel
|
* @name module:core.Mouse#getWheelRel
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -119,20 +126,24 @@ export class Mouse extends PsychObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the status of each button (pressed or released) and, optionally, the time elapsed between the last call to [clickReset]{@link module:core.Mouse#clickReset} and the pressing or releasing of the buttons.
|
* Get the status of each button (pressed or released) and, optionally, the time elapsed between the last call to [clickReset]{@link module:core.Mouse#clickReset} and the pressing or releasing of the buttons.
|
||||||
*
|
*
|
||||||
* <p>Note: clickReset is typically called at stimulus onset. When the participant presses a button, the time elapsed since the clickReset is stored internally and can be accessed any time afterwards with getPressed.</p>
|
* <p>Note: clickReset is typically called at stimulus onset. When the participant presses a button, the time elapsed since the clickReset is stored internally and can be accessed any time afterwards with getPressed.</p>
|
||||||
*
|
*
|
||||||
* @name module:core.Mouse#getPressed
|
* @name module:core.Mouse#getPressed
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} [getTime= false] whether or not to also return timestamps
|
* @param {boolean} [getTime= false] whether or not to also return timestamps
|
||||||
* @return {Array.number | Array.<Array.number>} either an array of size 3 with the status (1 for pressed, 0 for released) of each mouse button [left, center, right], or a tuple with that array and another array of size 3 with the timestamps.
|
* @return {Array.number | Array.<Array.number>} either an array of size 3 with the status (1 for pressed, 0 for released) of each mouse button [left, center, right], or a tuple with that array and another array of size 3 with the timestamps.
|
||||||
*/
|
*/
|
||||||
getPressed(getTime = false) {
|
getPressed(getTime = false)
|
||||||
|
{
|
||||||
const buttonPressed = this.psychoJS.eventManager.getMouseInfo().buttons.pressed.slice();
|
const buttonPressed = this.psychoJS.eventManager.getMouseInfo().buttons.pressed.slice();
|
||||||
if (!getTime)
|
if (!getTime)
|
||||||
|
{
|
||||||
return buttonPressed;
|
return buttonPressed;
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
const buttonTimes = this.psychoJS.eventManager.getMouseInfo().buttons.times.slice();
|
const buttonTimes = this.psychoJS.eventManager.getMouseInfo().buttons.times.slice();
|
||||||
return [buttonPressed, buttonTimes];
|
return [buttonPressed, buttonTimes];
|
||||||
}
|
}
|
||||||
@ -141,7 +152,7 @@ export class Mouse extends PsychObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the mouse has moved beyond a certain distance.
|
* Determine whether the mouse has moved beyond a certain distance.
|
||||||
*
|
*
|
||||||
* <p><b>distance</b>
|
* <p><b>distance</b>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>mouseMoved() or mouseMoved(undefined, false): determine whether the mouse has moved at all since the last
|
* <li>mouseMoved() or mouseMoved(undefined, false): determine whether the mouse has moved at all since the last
|
||||||
@ -149,14 +160,14 @@ export class Mouse extends PsychObject {
|
|||||||
* <li>mouseMoved(distance: number, false): determine whether the mouse has travelled further than distance, in terms of line of sight</li>
|
* <li>mouseMoved(distance: number, false): determine whether the mouse has travelled further than distance, in terms of line of sight</li>
|
||||||
* <li>mouseMoved(distance: [number,number], false): determine whether the mouse has travelled horizontally or vertically further then the given horizontal and vertical distances</li>
|
* <li>mouseMoved(distance: [number,number], false): determine whether the mouse has travelled horizontally or vertically further then the given horizontal and vertical distances</li>
|
||||||
* </ul></p>
|
* </ul></p>
|
||||||
*
|
*
|
||||||
* <p><b>reset</b>
|
* <p><b>reset</b>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>mouseMoved(distance, true): reset the mouse move clock, return false</li>
|
* <li>mouseMoved(distance, true): reset the mouse move clock, return false</li>
|
||||||
* <li>mouseMoved(distance, 'here'): return false</li>
|
* <li>mouseMoved(distance, 'here'): return false</li>
|
||||||
* <li>mouseMoved(distance, [x: number, y: number]: artifically set the previous mouse position to the given coordinates and determine whether the mouse moved further than the given distance</li>
|
* <li>mouseMoved(distance, [x: number, y: number]: artifically set the previous mouse position to the given coordinates and determine whether the mouse moved further than the given distance</li>
|
||||||
* </ul></p>
|
* </ul></p>
|
||||||
*
|
*
|
||||||
* @name module:core.Mouse#mouseMoved
|
* @name module:core.Mouse#mouseMoved
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -164,92 +175,120 @@ export class Mouse extends PsychObject {
|
|||||||
* @param {boolean|String|Array.number} [reset= false] - see above for a full description
|
* @param {boolean|String|Array.number} [reset= false] - see above for a full description
|
||||||
* @return {boolean} see above for a full description
|
* @return {boolean} see above for a full description
|
||||||
*/
|
*/
|
||||||
mouseMoved(distance, reset = false) {
|
mouseMoved(distance, reset = false)
|
||||||
|
{
|
||||||
// make sure that _lastPos is defined:
|
// make sure that _lastPos is defined:
|
||||||
if (typeof this._lastPos === 'undefined')
|
if (typeof this._lastPos === 'undefined')
|
||||||
|
{
|
||||||
this.getPos();
|
this.getPos();
|
||||||
|
}
|
||||||
this._prevPos = this._lastPos.slice();
|
this._prevPos = this._lastPos.slice();
|
||||||
this.getPos();
|
this.getPos();
|
||||||
|
|
||||||
if (typeof reset === 'boolean' && reset == false) {
|
if (typeof reset === 'boolean' && reset == false)
|
||||||
|
{
|
||||||
if (typeof distance === 'undefined')
|
if (typeof distance === 'undefined')
|
||||||
|
{
|
||||||
return (this._prevPos[0] != this._lastPos[0]) || (this._prevPos[1] != this._lastPos[1]);
|
return (this._prevPos[0] != this._lastPos[0]) || (this._prevPos[1] != this._lastPos[1]);
|
||||||
else {
|
}
|
||||||
if (typeof distance === 'number') {
|
else
|
||||||
|
{
|
||||||
|
if (typeof distance === 'number')
|
||||||
|
{
|
||||||
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
|
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
|
||||||
return (this._movedistance > distance);
|
return (this._movedistance > distance);
|
||||||
}
|
}
|
||||||
if (this._prevPos[0] + distance[0] - this._lastPos[0] > 0.0)
|
if (this._prevPos[0] + distance[0] - this._lastPos[0] > 0.0)
|
||||||
return true; // moved on X-axis
|
{
|
||||||
|
return true;
|
||||||
|
} // moved on X-axis
|
||||||
if (this._prevPos[1] + distance[1] - this._lastPos[0] > 0.0)
|
if (this._prevPos[1] + distance[1] - this._lastPos[0] > 0.0)
|
||||||
return true; // moved on Y-axis
|
{
|
||||||
|
return true;
|
||||||
|
} // moved on Y-axis
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (typeof reset === 'boolean' && reset == true) {
|
else if (typeof reset === 'boolean' && reset == true)
|
||||||
|
{
|
||||||
// reset the moveClock:
|
// reset the moveClock:
|
||||||
this.psychoJS.eventManager.getMouseInfo().moveClock.reset();
|
this.psychoJS.eventManager.getMouseInfo().moveClock.reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (reset === 'here') {
|
else if (reset === 'here')
|
||||||
|
{
|
||||||
// set to wherever we are
|
// set to wherever we are
|
||||||
this._prevPos = this._lastPos.clone();
|
this._prevPos = this._lastPos.clone();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (reset instanceof Array) {
|
else if (reset instanceof Array)
|
||||||
|
{
|
||||||
// an (x,y) array
|
// an (x,y) array
|
||||||
// reset to (x,y) to check movement from there
|
// reset to (x,y) to check movement from there
|
||||||
this._prevPos = reset.slice();
|
this._prevPos = reset.slice();
|
||||||
if (!distance)
|
if (!distance)
|
||||||
return false; // just resetting prevPos, not checking distance
|
{
|
||||||
else {
|
return false;
|
||||||
|
}// just resetting prevPos, not checking distance
|
||||||
|
else
|
||||||
|
{
|
||||||
// checking distance of current pos to newly reset prevposition
|
// checking distance of current pos to newly reset prevposition
|
||||||
if (typeof distance === 'number') {
|
if (typeof distance === 'number')
|
||||||
|
{
|
||||||
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
|
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
|
||||||
return (this._movedistance > distance);
|
return (this._movedistance > distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.abs(this._lastPos[0] - this._prevPos[0]) > distance[0])
|
if (Math.abs(this._lastPos[0] - this._prevPos[0]) > distance[0])
|
||||||
return true; // moved on X-axis
|
{
|
||||||
|
return true;
|
||||||
|
} // moved on X-axis
|
||||||
if (Math.abs(this._lastPos[1] - this._prevPos[1]) > distance[1])
|
if (Math.abs(this._lastPos[1] - this._prevPos[1]) > distance[1])
|
||||||
return true; // moved on Y-axis
|
{
|
||||||
|
return true;
|
||||||
|
} // moved on Y-axis
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the amount of time elapsed since the last mouse movement.
|
* Get the amount of time elapsed since the last mouse movement.
|
||||||
*
|
*
|
||||||
* @name module:core.Mouse#mouseMoveTime
|
* @name module:core.Mouse#mouseMoveTime
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {number} the time elapsed since the last mouse movement
|
* @return {number} the time elapsed since the last mouse movement
|
||||||
*/
|
*/
|
||||||
mouseMoveTime() {
|
mouseMoveTime()
|
||||||
|
{
|
||||||
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
|
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the clocks associated to the given mouse buttons.
|
* Reset the clocks associated to the given mouse buttons.
|
||||||
*
|
*
|
||||||
* @name module:core.Mouse#clickReset
|
* @name module:core.Mouse#clickReset
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {Array.number} [buttons= [0,1,2]] the buttons to reset (0: left, 1: center, 2: right)
|
* @param {Array.number} [buttons= [0,1,2]] the buttons to reset (0: left, 1: center, 2: right)
|
||||||
*/
|
*/
|
||||||
clickReset(buttons = [0, 1, 2]) {
|
clickReset(buttons = [0, 1, 2])
|
||||||
|
{
|
||||||
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
||||||
for (const b of buttons) {
|
for (const b of buttons)
|
||||||
|
{
|
||||||
mouseInfo.buttons.clocks[b].reset();
|
mouseInfo.buttons.clocks[b].reset();
|
||||||
mouseInfo.buttons.times[b] = 0.0;
|
mouseInfo.buttons.times[b] = 0.0;
|
||||||
}
|
}
|
||||||
|
@ -3,24 +3,23 @@
|
|||||||
* Main component of the PsychoJS library.
|
* Main component of the PsychoJS library.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { Scheduler } from '../util/Scheduler';
|
import {Scheduler} from '../util/Scheduler';
|
||||||
import { ServerManager } from './ServerManager';
|
import {ServerManager} from './ServerManager';
|
||||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||||
import { EventManager } from './EventManager';
|
import {EventManager} from './EventManager';
|
||||||
import { Window } from './Window';
|
import {Window} from './Window';
|
||||||
import { GUI } from './GUI';
|
import {GUI} from './GUI';
|
||||||
import { MonotonicClock } from '../util/Clock';
|
import {MonotonicClock} from '../util/Clock';
|
||||||
import { Logger } from './Logger';
|
import {Logger} from './Logger';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>PsychoJS manages the lifecycle of an experiment. It initialises the PsychoJS library and its various components (e.g. the {@link ServerManager}, the {@link EventManager}), and is used by the experiment to schedule the various tasks.</p>
|
* <p>PsychoJS manages the lifecycle of an experiment. It initialises the PsychoJS library and its various components (e.g. the {@link ServerManager}, the {@link EventManager}), and is used by the experiment to schedule the various tasks.</p>
|
||||||
*
|
*
|
||||||
@ -34,25 +33,81 @@ export class PsychoJS
|
|||||||
/**
|
/**
|
||||||
* Properties
|
* Properties
|
||||||
*/
|
*/
|
||||||
get status() { return this._status; }
|
get status()
|
||||||
set status(status) {
|
{
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
set status(status)
|
||||||
|
{
|
||||||
this._status = status;
|
this._status = status;
|
||||||
}
|
}
|
||||||
get config() { return this._config; }
|
|
||||||
get window() { return this._window; }
|
|
||||||
get serverManager() { return this._serverManager; }
|
|
||||||
get experiment() { return this._experiment; }
|
|
||||||
get scheduler() { return this._scheduler; }
|
|
||||||
get monotonicClock() { return this._monotonicClock; }
|
|
||||||
get logger() { return this._logger.consoleLogger; }
|
|
||||||
get experimentLogger() { return this._logger; }
|
|
||||||
get eventManager() { return this._eventManager; }
|
|
||||||
get gui() { return this._gui; }
|
|
||||||
get IP() { return this._IP; }
|
|
||||||
// this._serverMsg is a bi-directional message board for communications with the pavlovia.org server:
|
|
||||||
get serverMsg() { return this._serverMsg; }
|
|
||||||
get browser() { return this._browser; }
|
|
||||||
|
|
||||||
|
get config()
|
||||||
|
{
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
get window()
|
||||||
|
{
|
||||||
|
return this._window;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverManager()
|
||||||
|
{
|
||||||
|
return this._serverManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
get experiment()
|
||||||
|
{
|
||||||
|
return this._experiment;
|
||||||
|
}
|
||||||
|
|
||||||
|
get scheduler()
|
||||||
|
{
|
||||||
|
return this._scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
get monotonicClock()
|
||||||
|
{
|
||||||
|
return this._monotonicClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
get logger()
|
||||||
|
{
|
||||||
|
return this._logger.consoleLogger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get experimentLogger()
|
||||||
|
{
|
||||||
|
return this._logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get eventManager()
|
||||||
|
{
|
||||||
|
return this._eventManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
get gui()
|
||||||
|
{
|
||||||
|
return this._gui;
|
||||||
|
}
|
||||||
|
|
||||||
|
get IP()
|
||||||
|
{
|
||||||
|
return this._IP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this._serverMsg is a bi-directional message board for communications with the pavlovia.org server:
|
||||||
|
get serverMsg()
|
||||||
|
{
|
||||||
|
return this._serverMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
get browser()
|
||||||
|
{
|
||||||
|
return this._browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,7 +118,7 @@ export class PsychoJS
|
|||||||
debug = true,
|
debug = true,
|
||||||
collectIP = false,
|
collectIP = false,
|
||||||
topLevelStatus = true
|
topLevelStatus = true
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
// logging:
|
// logging:
|
||||||
this._logger = new Logger(this, (debug) ? log4javascript.Level.DEBUG : log4javascript.Level.INFO);
|
this._logger = new Logger(this, (debug) ? log4javascript.Level.DEBUG : log4javascript.Level.INFO);
|
||||||
@ -104,13 +159,15 @@ export class PsychoJS
|
|||||||
// make the PsychoJS.Status accessible from the top level of the generated experiment script
|
// make the PsychoJS.Status accessible from the top level of the generated experiment script
|
||||||
// in order to accommodate PsychoPy's Code Components
|
// in order to accommodate PsychoPy's Code Components
|
||||||
if (topLevelStatus)
|
if (topLevelStatus)
|
||||||
|
{
|
||||||
this._makeStatusTopLevel();
|
this._makeStatusTopLevel();
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.info('[PsychoJS] Initialised.');
|
this.logger.info('[PsychoJS] Initialised.');
|
||||||
|
this.logger.info('[PsychoJS] @version 2020.5');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the experiment's environment.
|
* Get the experiment's environment.
|
||||||
*
|
*
|
||||||
@ -119,10 +176,13 @@ export class PsychoJS
|
|||||||
getEnvironment()
|
getEnvironment()
|
||||||
{
|
{
|
||||||
if (typeof this._config === 'undefined')
|
if (typeof this._config === 'undefined')
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
return this._config.environment;
|
return this._config.environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a PsychoJS Window.
|
* Open a PsychoJS Window.
|
||||||
*
|
*
|
||||||
@ -142,17 +202,24 @@ export class PsychoJS
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
openWindow({
|
openWindow({
|
||||||
name,
|
name,
|
||||||
fullscr,
|
fullscr,
|
||||||
color,
|
color,
|
||||||
units,
|
units,
|
||||||
waitBlanking,
|
waitBlanking,
|
||||||
autoLog
|
autoLog
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
this.logger.info('[PsychoJS] Open Window.');
|
this.logger.info('[PsychoJS] Open Window.');
|
||||||
|
|
||||||
if (typeof this._window !== 'undefined')
|
if (typeof this._window !== 'undefined')
|
||||||
throw { origin : 'PsychoJS.openWindow', context : 'when opening a Window', error : 'A Window has already been opened.' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'PsychoJS.openWindow',
|
||||||
|
context: 'when opening a Window',
|
||||||
|
error: 'A Window has already been opened.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this._window = new Window({
|
this._window = new Window({
|
||||||
psychoJS: this,
|
psychoJS: this,
|
||||||
@ -172,7 +239,8 @@ export class PsychoJS
|
|||||||
* @param {string} completionUrl - the completion URL
|
* @param {string} completionUrl - the completion URL
|
||||||
* @param {string} cancellationUrl - the cancellation URL
|
* @param {string} cancellationUrl - the cancellation URL
|
||||||
*/
|
*/
|
||||||
setRedirectUrls(completionUrl, cancellationUrl) {
|
setRedirectUrls(completionUrl, cancellationUrl)
|
||||||
|
{
|
||||||
this._completionUrl = completionUrl;
|
this._completionUrl = completionUrl;
|
||||||
this._cancellationUrl = cancellationUrl;
|
this._cancellationUrl = cancellationUrl;
|
||||||
}
|
}
|
||||||
@ -185,7 +253,8 @@ export class PsychoJS
|
|||||||
* @param args - arguments for that task
|
* @param args - arguments for that task
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
schedule(task, args) {
|
schedule(task, args)
|
||||||
|
{
|
||||||
this.logger.debug('schedule task: ', task.toString().substring(0, 50), '...');
|
this.logger.debug('schedule task: ', task.toString().substring(0, 50), '...');
|
||||||
|
|
||||||
this._scheduler.add(task, args);
|
this._scheduler.add(task, args);
|
||||||
@ -204,7 +273,8 @@ export class PsychoJS
|
|||||||
* @param {Scheduler} elseScheduler scheduler to run if the condition is false
|
* @param {Scheduler} elseScheduler scheduler to run if the condition is false
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
scheduleCondition(condition, thenScheduler, elseScheduler) {
|
scheduleCondition(condition, thenScheduler, elseScheduler)
|
||||||
|
{
|
||||||
this.logger.debug('schedule condition: ', condition.toString().substring(0, 50), '...');
|
this.logger.debug('schedule condition: ', condition.toString().substring(0, 50), '...');
|
||||||
|
|
||||||
this._scheduler.addConditional(condition, thenScheduler, elseScheduler);
|
this._scheduler.addConditional(condition, thenScheduler, elseScheduler);
|
||||||
@ -224,27 +294,31 @@ export class PsychoJS
|
|||||||
*
|
*
|
||||||
* @todo: close session on window or tab close
|
* @todo: close session on window or tab close
|
||||||
*/
|
*/
|
||||||
async start({ configURL = 'config.json', expName = 'UNKNOWN', expInfo, resources = [] } = {})
|
async start({configURL = 'config.json', expName = 'UNKNOWN', expInfo, resources = []} = {})
|
||||||
{
|
{
|
||||||
this.logger.debug();
|
this.logger.debug();
|
||||||
|
|
||||||
const response = { origin: 'PsychoJS.start', context: 'when starting the experiment' };
|
const response = {origin: 'PsychoJS.start', context: 'when starting the experiment'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// configure the experiment:
|
// configure the experiment:
|
||||||
await this._configure(configURL, expName);
|
await this._configure(configURL, expName);
|
||||||
|
|
||||||
// get the participant IP:
|
// get the participant IP:
|
||||||
if (this._collectIP)
|
if (this._collectIP)
|
||||||
|
{
|
||||||
this._getParticipantIPInfo();
|
this._getParticipantIPInfo();
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this._IP = {
|
this._IP = {
|
||||||
IP: 'X',
|
IP: 'X',
|
||||||
hostname : 'X',
|
hostname: 'X',
|
||||||
city : 'X',
|
city: 'X',
|
||||||
region : 'X',
|
region: 'X',
|
||||||
country : 'X',
|
country: 'X',
|
||||||
location : 'X'
|
location: 'X'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,21 +338,42 @@ export class PsychoJS
|
|||||||
// open a session:
|
// open a session:
|
||||||
await this._serverManager.openSession();
|
await this._serverManager.openSession();
|
||||||
|
|
||||||
// attempt to close the session on beforeunload/unload (we use a synchronous request since
|
// warn the user when they attempt to close the tab or browser:
|
||||||
// the Beacon API only allows POST and we need DELETE ) and release the WebGL context:
|
this.beforeunloadCallback = (event) =>
|
||||||
const self = this;
|
{
|
||||||
window.onbeforeunload = () => {
|
// preventDefault should ensure that the user gets prompted:
|
||||||
self._serverManager.closeSession(false, true);
|
event.preventDefault();
|
||||||
|
|
||||||
if (typeof self._window !== 'undefined')
|
// Chrome requires returnValue to be set:
|
||||||
self._window.close();
|
event.returnValue = '';
|
||||||
};
|
};
|
||||||
window.addEventListener('unload', function(event) {
|
window.addEventListener('beforeunload', this.beforeunloadCallback);
|
||||||
self._serverManager.closeSession(false, true);
|
|
||||||
|
|
||||||
|
// when the user closes the tab or browser, we attempt to close the session, optionally save the results,
|
||||||
|
// and release the WebGL context
|
||||||
|
// note: we communicate with the server using the Beacon API
|
||||||
|
const self = this;
|
||||||
|
window.addEventListener('unload', (event) =>
|
||||||
|
{
|
||||||
|
if (self._config.session.status === 'OPEN')
|
||||||
|
{
|
||||||
|
// save the incomplete results if need be:
|
||||||
|
if (self._config.experiment.saveIncompleteResults)
|
||||||
|
{
|
||||||
|
self._experiment.save({sync: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the session:
|
||||||
|
self._serverManager.closeSession(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof self._window !== 'undefined')
|
if (typeof self._window !== 'undefined')
|
||||||
|
{
|
||||||
self._window.close();
|
self._window.close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -289,9 +384,10 @@ export class PsychoJS
|
|||||||
this.logger.info('[PsychoJS] Start Experiment.');
|
this.logger.info('[PsychoJS] Start Experiment.');
|
||||||
this._scheduler.start();
|
this._scheduler.start();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
// this._gui.dialog({ error: { ...response, error } });
|
// this._gui.dialog({ error: { ...response, error } });
|
||||||
this._gui.dialog({ error: Object.assign(response, { error }) });
|
this._gui.dialog({error: Object.assign(response, {error})});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,13 +406,16 @@ export class PsychoJS
|
|||||||
* @async
|
* @async
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
async downloadResources(resources = []) {
|
async downloadResources(resources = [])
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
await this.serverManager.downloadResources(resources);
|
await this.serverManager.downloadResources(resources);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
// this._gui.dialog({ error: { ...response, error } });
|
// this._gui.dialog({ error: { ...response, error } });
|
||||||
this._gui.dialog({ error: Object.assign(response, { error }) });
|
this._gui.dialog({error: Object.assign(response, {error})});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,13 +427,17 @@ export class PsychoJS
|
|||||||
* @param {Object.<string, *>} obj the object whose attributes we will mirror
|
* @param {Object.<string, *>} obj the object whose attributes we will mirror
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
importAttributes(obj) {
|
importAttributes(obj)
|
||||||
|
{
|
||||||
this.logger.debug('import attributes from: ', util.toString(obj));
|
this.logger.debug('import attributes from: ', util.toString(obj));
|
||||||
|
|
||||||
if (typeof obj === 'undefined')
|
if (typeof obj === 'undefined')
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const attribute in obj) {
|
for (const attribute in obj)
|
||||||
|
{
|
||||||
// this[attribute] = obj[attribute];
|
// this[attribute] = obj[attribute];
|
||||||
window[attribute] = obj[attribute];
|
window[attribute] = obj[attribute];
|
||||||
}
|
}
|
||||||
@ -354,26 +457,38 @@ export class PsychoJS
|
|||||||
* @async
|
* @async
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
async quit({ message, isCompleted = false } = {}) {
|
async quit({message, isCompleted = false} = {})
|
||||||
|
{
|
||||||
this.logger.info('[PsychoJS] Quit.');
|
this.logger.info('[PsychoJS] Quit.');
|
||||||
|
|
||||||
this._experiment.experimentEnded = true;
|
this._experiment.experimentEnded = true;
|
||||||
this._status = PsychoJS.Status.FINISHED;
|
this._status = PsychoJS.Status.FINISHED;
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// stop the main scheduler:
|
// stop the main scheduler:
|
||||||
this._scheduler.stop();
|
this._scheduler.stop();
|
||||||
|
|
||||||
|
// remove the beforeunload listener:
|
||||||
|
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
|
||||||
|
{
|
||||||
|
window.removeEventListener('beforeunload', this.beforeunloadCallback);
|
||||||
|
}
|
||||||
|
|
||||||
// save the results and the logs of the experiment:
|
// save the results and the logs of the experiment:
|
||||||
this.gui.dialog({
|
this.gui.dialog({
|
||||||
warning: 'Closing the session. Please wait a few moments.',
|
warning: 'Closing the session. Please wait a few moments.',
|
||||||
showOK: false
|
showOK: false
|
||||||
});
|
});
|
||||||
await this._experiment.save();
|
if (isCompleted || this._config.experiment.saveIncompleteResults)
|
||||||
await this._logger.flush();
|
{
|
||||||
|
await this._experiment.save();
|
||||||
|
await this._logger.flush();
|
||||||
|
}
|
||||||
|
|
||||||
// close the session:
|
// close the session:
|
||||||
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER) {
|
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
|
||||||
|
{
|
||||||
await this._serverManager.closeSession(isCompleted);
|
await this._serverManager.closeSession(isCompleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,29 +498,37 @@ export class PsychoJS
|
|||||||
const self = this;
|
const self = this;
|
||||||
this._gui.dialog({
|
this._gui.dialog({
|
||||||
message: text,
|
message: text,
|
||||||
onOK: () => {
|
onOK: () =>
|
||||||
|
{
|
||||||
// close the window:
|
// close the window:
|
||||||
self._window.close();
|
self._window.close();
|
||||||
|
|
||||||
// remove everything from the browser window:
|
// remove everything from the browser window:
|
||||||
while (document.body.hasChildNodes())
|
while (document.body.hasChildNodes())
|
||||||
|
{
|
||||||
document.body.removeChild(document.body.lastChild);
|
document.body.removeChild(document.body.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
// return from fullscreen if we were there:
|
// return from fullscreen if we were there:
|
||||||
this._window.closeFullScreen();
|
this._window.closeFullScreen();
|
||||||
|
|
||||||
// redirect if redirection URLs have been provided:
|
// redirect if redirection URLs have been provided:
|
||||||
if (isCompleted && typeof self._completionUrl !== 'undefined')
|
if (isCompleted && typeof self._completionUrl !== 'undefined')
|
||||||
|
{
|
||||||
window.location = self._completionUrl;
|
window.location = self._completionUrl;
|
||||||
|
}
|
||||||
else if (!isCompleted && typeof self._cancellationUrl !== 'undefined')
|
else if (!isCompleted && typeof self._cancellationUrl !== 'undefined')
|
||||||
|
{
|
||||||
window.location = self._cancellationUrl;
|
window.location = self._cancellationUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this._gui.dialog({ error });
|
this._gui.dialog({error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,21 +541,25 @@ export class PsychoJS
|
|||||||
* @param {string} configURL - the URL of the configuration file
|
* @param {string} configURL - the URL of the configuration file
|
||||||
* @param {string} name - the name of the experiment
|
* @param {string} name - the name of the experiment
|
||||||
*/
|
*/
|
||||||
async _configure(configURL, name) {
|
async _configure(configURL, name)
|
||||||
const response = { origin: 'PsychoJS.configure', context: 'when configuring PsychoJS for the experiment' };
|
{
|
||||||
|
const response = {origin: 'PsychoJS.configure', context: 'when configuring PsychoJS for the experiment'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.CONFIGURING;
|
this.status = PsychoJS.Status.CONFIGURING;
|
||||||
|
|
||||||
// if the experiment is running from the pavlovia.org server, we read the configuration file:
|
// if the experiment is running from the pavlovia.org server, we read the configuration file:
|
||||||
const experimentUrl = window.location.href;
|
const experimentUrl = window.location.href;
|
||||||
if (experimentUrl.indexOf('https://run.pavlovia.org/') === 0 || experimentUrl.indexOf('https://pavlovia.org/run/') === 0) {
|
if (experimentUrl.indexOf('https://run.pavlovia.org/') === 0 || experimentUrl.indexOf('https://pavlovia.org/run/') === 0)
|
||||||
|
{
|
||||||
const serverResponse = await this._serverManager.getConfiguration(configURL);
|
const serverResponse = await this._serverManager.getConfiguration(configURL);
|
||||||
this._config = serverResponse.config;
|
this._config = serverResponse.config;
|
||||||
|
|
||||||
// legacy experiments had a psychoJsManager block instead of a pavlovia block, and the URL
|
// legacy experiments had a psychoJsManager block instead of a pavlovia block, and the URL
|
||||||
// pointed to https://pavlovia.org/server
|
// pointed to https://pavlovia.org/server
|
||||||
if ('psychoJsManager' in this._config) {
|
if ('psychoJsManager' in this._config)
|
||||||
|
{
|
||||||
delete this._config.psychoJsManager;
|
delete this._config.psychoJsManager;
|
||||||
this._config.pavlovia = {
|
this._config.pavlovia = {
|
||||||
URL: 'https://pavlovia.org'
|
URL: 'https://pavlovia.org'
|
||||||
@ -441,41 +568,56 @@ export class PsychoJS
|
|||||||
|
|
||||||
// tests for the presence of essential blocks in the configuration:
|
// tests for the presence of essential blocks in the configuration:
|
||||||
if (!('experiment' in this._config))
|
if (!('experiment' in this._config))
|
||||||
|
{
|
||||||
throw 'missing experiment block in configuration';
|
throw 'missing experiment block in configuration';
|
||||||
|
}
|
||||||
if (!('name' in this._config.experiment))
|
if (!('name' in this._config.experiment))
|
||||||
|
{
|
||||||
throw 'missing name in experiment block in configuration';
|
throw 'missing name in experiment block in configuration';
|
||||||
|
}
|
||||||
if (!('fullpath' in this._config.experiment))
|
if (!('fullpath' in this._config.experiment))
|
||||||
|
{
|
||||||
throw 'missing fullpath in experiment block in configuration';
|
throw 'missing fullpath in experiment block in configuration';
|
||||||
|
}
|
||||||
if (!('pavlovia' in this._config))
|
if (!('pavlovia' in this._config))
|
||||||
|
{
|
||||||
throw 'missing pavlovia block in configuration';
|
throw 'missing pavlovia block in configuration';
|
||||||
|
}
|
||||||
if (!('URL' in this._config.pavlovia))
|
if (!('URL' in this._config.pavlovia))
|
||||||
|
{
|
||||||
throw 'missing URL in pavlovia block in configuration';
|
throw 'missing URL in pavlovia block in configuration';
|
||||||
|
}
|
||||||
|
|
||||||
this._config.environment = ExperimentHandler.Environment.SERVER;
|
this._config.environment = ExperimentHandler.Environment.SERVER;
|
||||||
|
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
// otherwise we create an ad-hoc configuration:
|
// otherwise we create an ad-hoc configuration:
|
||||||
{
|
{
|
||||||
this._config = {
|
this._config = {
|
||||||
environment: ExperimentHandler.Environment.LOCAL,
|
environment: ExperimentHandler.Environment.LOCAL,
|
||||||
experiment: { name, saveFormat: ExperimentHandler.SaveFormat.CSV }
|
experiment: {name, saveFormat: ExperimentHandler.SaveFormat.CSV}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the server parameters (those starting with a double underscore):
|
// get the server parameters (those starting with a double underscore):
|
||||||
this._serverMsg = new Map();
|
this._serverMsg = new Map();
|
||||||
util.getUrlParameters().forEach((value, key) => {
|
util.getUrlParameters().forEach((value, key) =>
|
||||||
|
{
|
||||||
if (key.indexOf('__') === 0)
|
if (key.indexOf('__') === 0)
|
||||||
|
{
|
||||||
this._serverMsg.set(key, value);
|
this._serverMsg.set(key, value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.status = PsychoJS.Status.CONFIGURED;
|
this.status = PsychoJS.Status.CONFIGURED;
|
||||||
this.logger.debug('configuration:', util.toString(this._config));
|
this.logger.debug('configuration:', util.toString(this._config));
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
// throw { ...response, error };
|
// throw { ...response, error };
|
||||||
throw Object.assign(response, { error });
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,13 +628,18 @@ export class PsychoJS
|
|||||||
* <p>Note: we use [http://www.geoplugin.net/json.gp]{@link http://www.geoplugin.net/json.gp}.</p>
|
* <p>Note: we use [http://www.geoplugin.net/json.gp]{@link http://www.geoplugin.net/json.gp}.</p>
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
async _getParticipantIPInfo() {
|
async _getParticipantIPInfo()
|
||||||
const response = { origin: 'PsychoJS._getParticipantIPInfo', context: 'when getting the IP information of the participant' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'PsychoJS._getParticipantIPInfo',
|
||||||
|
context: 'when getting the IP information of the participant'
|
||||||
|
};
|
||||||
|
|
||||||
this.logger.debug('getting the IP information of the participant');
|
this.logger.debug('getting the IP information of the participant');
|
||||||
|
|
||||||
this._IP = {};
|
this._IP = {};
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
const geoResponse = await $.get('http://www.geoplugin.net/json.gp');
|
const geoResponse = await $.get('http://www.geoplugin.net/json.gp');
|
||||||
const geoData = JSON.parse(geoResponse);
|
const geoData = JSON.parse(geoResponse);
|
||||||
this._IP = {
|
this._IP = {
|
||||||
@ -503,9 +650,10 @@ export class PsychoJS
|
|||||||
};
|
};
|
||||||
this.logger.debug('IP information of the participant: ' + util.toString(this._IP));
|
this.logger.debug('IP information of the participant: ' + util.toString(this._IP));
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
// throw { ...response, error };
|
// throw { ...response, error };
|
||||||
throw Object.assign(response, { error });
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,13 +663,15 @@ export class PsychoJS
|
|||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_captureErrors() {
|
_captureErrors()
|
||||||
|
{
|
||||||
this.logger.debug('capturing all errors using window.onerror');
|
this.logger.debug('capturing all errors using window.onerror');
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
window.onerror = function (message, source, lineno, colno, error) {
|
window.onerror = function (message, source, lineno, colno, error)
|
||||||
|
{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
self._gui.dialog({ "error": error });
|
self._gui.dialog({"error": error});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -539,8 +689,10 @@ export class PsychoJS
|
|||||||
* Make the various Status top level, in order to accommodate PsychoPy's Code Components.
|
* Make the various Status top level, in order to accommodate PsychoPy's Code Components.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_makeStatusTopLevel() {
|
_makeStatusTopLevel()
|
||||||
for (const status in PsychoJS.Status) {
|
{
|
||||||
|
for (const status in PsychoJS.Status)
|
||||||
|
{
|
||||||
window[status] = PsychoJS.Status[status];
|
window[status] = PsychoJS.Status[status];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,19 @@
|
|||||||
* 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 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { PsychoJS } from './PsychoJS';
|
import {PsychoJS} from './PsychoJS';
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
import {ExperimentHandler} from "../data/ExperimentHandler";
|
import {ExperimentHandler} from "../data/ExperimentHandler";
|
||||||
import {MonotonicClock} from "../util/Clock";
|
import {MonotonicClock} from "../util/Clock";
|
||||||
// import { Howl } from 'howler';
|
|
||||||
|
|
||||||
|
// import { Howl } from 'howler';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,12 +29,14 @@ import {MonotonicClock} from "../util/Clock";
|
|||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class ServerManager extends PsychObject {
|
export class ServerManager extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
autoLog = false
|
autoLog = false
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
// session:
|
// session:
|
||||||
@ -66,15 +68,22 @@ export class ServerManager extends PsychObject {
|
|||||||
*
|
*
|
||||||
* @returns {Promise<ServerManager.GetConfigurationPromise>} the response
|
* @returns {Promise<ServerManager.GetConfigurationPromise>} the response
|
||||||
*/
|
*/
|
||||||
getConfiguration(configURL) {
|
getConfiguration(configURL)
|
||||||
const response = { origin: 'ServerManager.getConfiguration', context: 'when reading the configuration file: ' + configURL };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.getConfiguration',
|
||||||
|
context: 'when reading the configuration file: ' + configURL
|
||||||
|
};
|
||||||
|
|
||||||
this._psychoJS.logger.debug('reading the configuration file: ' + configURL);
|
this._psychoJS.logger.debug('reading the configuration file: ' + configURL);
|
||||||
return new Promise((resolve, reject) => {
|
const self = this;
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
{
|
||||||
$.get(configURL, 'json')
|
$.get(configURL, 'json')
|
||||||
.done((config, textStatus) => {
|
.done((config, textStatus) =>
|
||||||
|
{
|
||||||
// resolve({ ...response, config });
|
// resolve({ ...response, config });
|
||||||
resolve(Object.assign(response, { config }));
|
resolve(Object.assign(response, {config}));
|
||||||
})
|
})
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -83,7 +92,7 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -118,7 +127,9 @@ export class ServerManager extends PsychObject {
|
|||||||
// prepare POST query:
|
// prepare POST query:
|
||||||
let data = {};
|
let data = {};
|
||||||
if (this._psychoJS._serverMsg.has('__pilotToken'))
|
if (this._psychoJS._serverMsg.has('__pilotToken'))
|
||||||
|
{
|
||||||
data.pilotToken = this._psychoJS._serverMsg.get('__pilotToken');
|
data.pilotToken = this._psychoJS._serverMsg.get('__pilotToken');
|
||||||
|
}
|
||||||
|
|
||||||
// query pavlovia server:
|
// query pavlovia server:
|
||||||
const self = this;
|
const self = this;
|
||||||
@ -126,38 +137,44 @@ export class ServerManager extends PsychObject {
|
|||||||
{
|
{
|
||||||
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) + '/sessions';
|
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) + '/sessions';
|
||||||
$.post(url, data, null, 'json')
|
$.post(url, data, null, 'json')
|
||||||
.done((data, textStatus) =>
|
.done((data, textStatus) =>
|
||||||
{
|
{
|
||||||
if (!('token' in data)) {
|
if (!('token' in data))
|
||||||
|
{
|
||||||
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
|
reject(Object.assign(response, {error: 'unexpected answer from server: no token'}));
|
||||||
|
// reject({...response, error: 'unexpected answer from server: no token'});
|
||||||
|
}
|
||||||
|
if (!('experiment' in data))
|
||||||
|
{
|
||||||
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
|
// reject({...response, error: 'unexpected answer from server: no experiment'});
|
||||||
|
reject(Object.assign(response, {error: 'unexpected answer from server: no experiment'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
self._psychoJS.config.session = {
|
||||||
|
token: data.token,
|
||||||
|
status: 'OPEN'
|
||||||
|
};
|
||||||
|
self._psychoJS.config.experiment.status = data.experiment.status2;
|
||||||
|
self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
|
||||||
|
self._psychoJS.config.experiment.saveIncompleteResults = data.experiment.saveIncompleteResults;
|
||||||
|
self._psychoJS.config.experiment.license = data.experiment.license;
|
||||||
|
self._psychoJS.config.experiment.runMode = data.experiment.runMode;
|
||||||
|
|
||||||
|
self.setStatus(ServerManager.Status.READY);
|
||||||
|
// resolve({ ...response, token: data.token, status: data.status });
|
||||||
|
resolve(Object.assign(response, {token: data.token, status: data.status}));
|
||||||
|
})
|
||||||
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
reject(Object.assign(response, { error: 'unexpected answer from server: no token'}));
|
|
||||||
// reject({...response, error: 'unexpected answer from server: no token'});
|
|
||||||
}
|
|
||||||
self._psychoJS.config.session = { token: data.token };
|
|
||||||
|
|
||||||
if (!('experiment' in data)) {
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
console.error('error:', errorMsg);
|
||||||
// reject({...response, error: 'unexpected answer from server: no experiment'});
|
|
||||||
reject(Object.assign(response, { error: 'unexpected answer from server: no experiment'}));
|
|
||||||
}
|
|
||||||
self._psychoJS.config.experiment.status = data.experiment.status2;
|
|
||||||
self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
|
|
||||||
self._psychoJS.config.experiment.license = data.experiment.license;
|
|
||||||
self._psychoJS.config.experiment.runMode = data.experiment.runMode;
|
|
||||||
|
|
||||||
self.setStatus(ServerManager.Status.READY);
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
// resolve({ ...response, token: data.token, status: data.status });
|
});
|
||||||
resolve(Object.assign(response, { token: data.token, status: data.status }));
|
|
||||||
})
|
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
|
||||||
{
|
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
|
||||||
|
|
||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
|
||||||
console.error('error:', errorMsg);
|
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,8 +195,12 @@ export class ServerManager extends PsychObject {
|
|||||||
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
|
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
|
||||||
* @returns {Promise<ServerManager.CloseSessionPromise> | void} the response
|
* @returns {Promise<ServerManager.CloseSessionPromise> | void} the response
|
||||||
*/
|
*/
|
||||||
closeSession(isCompleted = false, sync = false) {
|
async closeSession(isCompleted = false, sync = false)
|
||||||
const response = { origin: 'ServerManager.closeSession', context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.closeSession',
|
||||||
|
context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath
|
||||||
|
};
|
||||||
|
|
||||||
this._psychoJS.logger.debug('closing the session for experiment: ' + this._psychoJS.config.experiment.name);
|
this._psychoJS.logger.debug('closing the session for experiment: ' + this._psychoJS.config.experiment.name);
|
||||||
|
|
||||||
@ -187,44 +208,61 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
// prepare DELETE query:
|
// prepare DELETE query:
|
||||||
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) + '/sessions/' + this._psychoJS.config.session.token;
|
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) + '/sessions/' + this._psychoJS.config.session.token;
|
||||||
const data = { isCompleted };
|
|
||||||
|
|
||||||
// synchronous query the pavlovia server:
|
// synchronous query the pavlovia server:
|
||||||
if (sync)
|
if (sync)
|
||||||
{
|
{
|
||||||
|
/* This is now deprecated in most browsers.
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
request.open("DELETE", url, false);
|
request.open("DELETE", url, false);
|
||||||
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
||||||
request.send(JSON.stringify(data));
|
request.send(JSON.stringify(data));
|
||||||
|
*/
|
||||||
return;
|
/* This does not work in Chrome before of a CORS bug
|
||||||
}
|
await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
||||||
// asynchronously query the pavlovia server:
|
body: JSON.stringify(data),
|
||||||
const self = this;
|
// keepalive makes it possible for the request to outlive the page (e.g. when the participant closes the tab)
|
||||||
return new Promise((resolve, reject) => {
|
keepalive: true
|
||||||
$.ajax({
|
|
||||||
url,
|
|
||||||
type: 'delete',
|
|
||||||
data,
|
|
||||||
dataType: 'json'
|
|
||||||
})
|
|
||||||
.done((data, textStatus) => {
|
|
||||||
self.setStatus(ServerManager.Status.READY);
|
|
||||||
// resolve({ ...response, data });
|
|
||||||
resolve(Object.assign(response, { data }));
|
|
||||||
})
|
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
|
||||||
{
|
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
|
||||||
|
|
||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
|
||||||
console.error('error:', errorMsg);
|
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
|
||||||
});
|
});
|
||||||
});
|
*/
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('isCompleted', isCompleted);
|
||||||
|
navigator.sendBeacon(url + '/delete', formData);
|
||||||
|
this._psychoJS.config.session.status = 'CLOSED';
|
||||||
|
}
|
||||||
|
// asynchronously query the pavlovia server:
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const self = this;
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
{
|
||||||
|
$.ajax({
|
||||||
|
url,
|
||||||
|
type: 'delete',
|
||||||
|
data: {isCompleted},
|
||||||
|
dataType: 'json'
|
||||||
|
})
|
||||||
|
.done((data, textStatus) =>
|
||||||
|
{
|
||||||
|
self.setStatus(ServerManager.Status.READY);
|
||||||
|
self._psychoJS.config.session.status = 'CLOSED';
|
||||||
|
|
||||||
|
// resolve({ ...response, data });
|
||||||
|
resolve(Object.assign(response, {data}));
|
||||||
|
})
|
||||||
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
|
{
|
||||||
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
|
|
||||||
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -238,13 +276,19 @@ export class ServerManager extends PsychObject {
|
|||||||
* @return {Object} value of the resource
|
* @return {Object} value of the resource
|
||||||
* @throws {Object.<string, *>} exception if no resource with that name has previously been registered
|
* @throws {Object.<string, *>} exception if no resource with that name has previously been registered
|
||||||
*/
|
*/
|
||||||
getResource(name) {
|
getResource(name)
|
||||||
const response = { origin: 'ServerManager.getResource', context: 'when getting the value of resource: ' + name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.getResource',
|
||||||
|
context: 'when getting the value of resource: ' + name
|
||||||
|
};
|
||||||
|
|
||||||
const path_data = this._resources.get(name);
|
const path_data = this._resources.get(name);
|
||||||
if (typeof path_data === 'undefined')
|
if (typeof path_data === 'undefined')
|
||||||
// throw { ...response, error: 'unknown resource' };
|
// throw { ...response, error: 'unknown resource' };
|
||||||
throw Object.assign(response, { error: 'unknown resource' });
|
{
|
||||||
|
throw Object.assign(response, {error: 'unknown resource'});
|
||||||
|
}
|
||||||
|
|
||||||
return path_data.data;
|
return path_data.data;
|
||||||
}
|
}
|
||||||
@ -257,17 +301,25 @@ export class ServerManager extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
setStatus(status) {
|
setStatus(status)
|
||||||
const response = { origin: 'ServerManager.setStatus', context: 'when changing the status of the server manager to: ' + util.toString(status) };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.setStatus',
|
||||||
|
context: 'when changing the status of the server manager to: ' + util.toString(status)
|
||||||
|
};
|
||||||
|
|
||||||
// check status:
|
// check status:
|
||||||
const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
|
const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
|
||||||
if (!statusKey)
|
if (!statusKey)
|
||||||
// throw { ...response, error: 'status must be a symbol' };
|
// throw { ...response, error: 'status must be a symbol' };
|
||||||
throw Object.assign(response, { error: 'status must be a symbol' });
|
{
|
||||||
|
throw Object.assign(response, {error: 'status must be a symbol'});
|
||||||
|
}
|
||||||
if (!ServerManager.Status.hasOwnProperty(statusKey))
|
if (!ServerManager.Status.hasOwnProperty(statusKey))
|
||||||
// throw { ...response, error: 'unknown status' };
|
// throw { ...response, error: 'unknown status' };
|
||||||
throw Object.assign(response, { error: 'unknown status' });
|
{
|
||||||
|
throw Object.assign(response, {error: 'unknown status'});
|
||||||
|
}
|
||||||
|
|
||||||
this._status = status;
|
this._status = status;
|
||||||
|
|
||||||
@ -286,7 +338,8 @@ export class ServerManager extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {ServerManager.Status.READY} the new status
|
* @return {ServerManager.Status.READY} the new status
|
||||||
*/
|
*/
|
||||||
resetStatus() {
|
resetStatus()
|
||||||
|
{
|
||||||
return this.setStatus(ServerManager.Status.READY);
|
return this.setStatus(ServerManager.Status.READY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,8 +360,12 @@ export class ServerManager extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
downloadResources(resources = []) {
|
downloadResources(resources = [])
|
||||||
const response = { origin: 'ServerManager.downloadResources', context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.downloadResources',
|
||||||
|
context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name
|
||||||
|
};
|
||||||
|
|
||||||
this._psychoJS.logger.debug('downloading resources for experiment: ' + this._psychoJS.config.experiment.name);
|
this._psychoJS.logger.debug('downloading resources for experiment: ' + this._psychoJS.config.experiment.name);
|
||||||
|
|
||||||
@ -316,26 +373,37 @@ export class ServerManager extends PsychObject {
|
|||||||
// but we want to run the asynchronous _listResources and _downloadResources in sequence
|
// but we want to run the asynchronous _listResources and _downloadResources in sequence
|
||||||
const self = this;
|
const self = this;
|
||||||
const newResources = new Map();
|
const newResources = new Map();
|
||||||
let download = async () => {
|
let download = async () =>
|
||||||
try {
|
{
|
||||||
if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER) {
|
try
|
||||||
|
{
|
||||||
|
if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER)
|
||||||
|
{
|
||||||
// no resources specified, we register them all:
|
// no resources specified, we register them all:
|
||||||
if (resources.length === 0) {
|
if (resources.length === 0)
|
||||||
|
{
|
||||||
// list the resources from the resources directory of the experiment on the server:
|
// list the resources from the resources directory of the experiment on the server:
|
||||||
const serverResponse = await self._listResources();
|
const serverResponse = await self._listResources();
|
||||||
for (const name of serverResponse.resources)
|
for (const name of serverResponse.resources)
|
||||||
self._resources.set(name, { path: serverResponse.resourceDirectory + '/' + name });
|
{
|
||||||
|
self._resources.set(name, {path: serverResponse.resourceDirectory + '/' + name});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// only registered the specified resources:
|
// only registered the specified resources:
|
||||||
for (const {name, path} of resources) {
|
for (const {name, path} of resources)
|
||||||
|
{
|
||||||
self._resources.set(name, {path});
|
self._resources.set(name, {path});
|
||||||
newResources.set(name, {path});
|
newResources.set(name, {path});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// register the specified resources:
|
// register the specified resources:
|
||||||
for (const {name, path} of resources) {
|
for (const {name, path} of resources)
|
||||||
|
{
|
||||||
self._resources.set(name, {path});
|
self._resources.set(name, {path});
|
||||||
newResources.set(name, {path});
|
newResources.set(name, {path});
|
||||||
}
|
}
|
||||||
@ -343,17 +411,23 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
self._nbResources = self._resources.size;
|
self._nbResources = self._resources.size;
|
||||||
for (const name of self._resources.keys())
|
for (const name of self._resources.keys())
|
||||||
|
{
|
||||||
this._psychoJS.logger.debug('resource:', name, self._resources.get(name).path);
|
this._psychoJS.logger.debug('resource:', name, self._resources.get(name).path);
|
||||||
|
}
|
||||||
|
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCES_REGISTERED, count: self._nbResources });
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.RESOURCES_REGISTERED,
|
||||||
|
count: self._nbResources
|
||||||
|
});
|
||||||
|
|
||||||
// download the registered resources:
|
// download the registered resources:
|
||||||
await self._downloadRegisteredResources(newResources);
|
await self._downloadRegisteredResources(newResources);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
// throw { ...response, error: error };
|
// throw { ...response, error: error };
|
||||||
throw Object.assign(response, { error });
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -375,10 +449,11 @@ export class ServerManager extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {string} key - the data key (e.g. the name of .csv file)
|
* @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)
|
* @param {string} value - the data value (e.g. a string containing the .csv header and records)
|
||||||
|
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
|
||||||
*
|
*
|
||||||
* @returns {Promise<ServerManager.UploadDataPromise>} the response
|
* @returns {Promise<ServerManager.UploadDataPromise>} the response
|
||||||
*/
|
*/
|
||||||
uploadData(key, value)
|
uploadData(key, value, sync = false)
|
||||||
{
|
{
|
||||||
const response = {
|
const response = {
|
||||||
origin: 'ServerManager.uploadData',
|
origin: 'ServerManager.uploadData',
|
||||||
@ -388,41 +463,50 @@ export class ServerManager extends PsychObject {
|
|||||||
this._psychoJS.logger.debug('uploading data for experiment: ' + this._psychoJS.config.experiment.fullpath);
|
this._psychoJS.logger.debug('uploading data for experiment: ' + this._psychoJS.config.experiment.fullpath);
|
||||||
this.setStatus(ServerManager.Status.BUSY);
|
this.setStatus(ServerManager.Status.BUSY);
|
||||||
|
|
||||||
// prepare the POST query:
|
const url = this._psychoJS.config.pavlovia.URL +
|
||||||
const data = {
|
'/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) +
|
||||||
key,
|
'/sessions/' + this._psychoJS.config.session.token +
|
||||||
value
|
'/results';
|
||||||
};
|
|
||||||
|
|
||||||
// query the pavlovia server:
|
// synchronous query the pavlovia server:
|
||||||
const self = this;
|
if (sync)
|
||||||
return new Promise((resolve, reject) =>
|
|
||||||
{
|
{
|
||||||
const url = self._psychoJS.config.pavlovia.URL +
|
const formData = new FormData();
|
||||||
'/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) +
|
formData.append('key', key);
|
||||||
'/sessions/' + self._psychoJS.config.session.token +
|
formData.append('value', value);
|
||||||
'/results';
|
navigator.sendBeacon(url, formData);
|
||||||
|
}
|
||||||
$.post(url, data, null, 'json')
|
// asynchronously query the pavlovia server:
|
||||||
.done((serverData, textStatus) =>
|
else
|
||||||
|
{
|
||||||
|
const self = this;
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
{
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
const data = {
|
||||||
resolve(Object.assign(response, { serverData }));
|
key,
|
||||||
})
|
value
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
};
|
||||||
{
|
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
|
||||||
|
|
||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
$.post(url, data, null, 'json')
|
||||||
console.error('error:', errorMsg);
|
.done((serverData, textStatus) =>
|
||||||
|
{
|
||||||
|
self.setStatus(ServerManager.Status.READY);
|
||||||
|
resolve(Object.assign(response, {serverData}));
|
||||||
|
})
|
||||||
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
|
{
|
||||||
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously upload experiment logs to the remote PsychoJS manager.
|
* Asynchronously upload experiment logs to the remote PsychoJS manager.
|
||||||
*
|
*
|
||||||
@ -468,7 +552,7 @@ export class ServerManager extends PsychObject {
|
|||||||
.done((serverData, textStatus) =>
|
.done((serverData, textStatus) =>
|
||||||
{
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
resolve(Object.assign(response, { serverData }));
|
resolve(Object.assign(response, {serverData}));
|
||||||
})
|
})
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -477,14 +561,12 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List the resources available to the experiment.
|
* List the resources available to the experiment.
|
||||||
|
|
||||||
@ -524,18 +606,21 @@ export class ServerManager extends PsychObject {
|
|||||||
{
|
{
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
// reject({ ...response, error: 'unexpected answer from server: no resources' });
|
// reject({ ...response, error: 'unexpected answer from server: no resources' });
|
||||||
reject(Object.assign(response, { error: 'unexpected answer from server: no resources' }));
|
reject(Object.assign(response, {error: 'unexpected answer from server: no resources'}));
|
||||||
}
|
}
|
||||||
if (!('resourceDirectory' in data))
|
if (!('resourceDirectory' in data))
|
||||||
{
|
{
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
// reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
|
// reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
|
||||||
reject(Object.assign(response, { error: 'unexpected answer from server: no resourceDirectory' }));
|
reject(Object.assign(response, {error: 'unexpected answer from server: no resourceDirectory'}));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
// resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
|
// resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
|
||||||
resolve(Object.assign(response, { resources: data.resources, resourceDirectory: data.resourceDirectory }));
|
resolve(Object.assign(response, {
|
||||||
|
resources: data.resources,
|
||||||
|
resourceDirectory: data.resourceDirectory
|
||||||
|
}));
|
||||||
})
|
})
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -544,14 +629,13 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the resources previously registered.
|
* Download the resources previously registered.
|
||||||
*
|
*
|
||||||
@ -563,7 +647,10 @@ export class ServerManager extends PsychObject {
|
|||||||
*/
|
*/
|
||||||
_downloadRegisteredResources(resources = new Map())
|
_downloadRegisteredResources(resources = new Map())
|
||||||
{
|
{
|
||||||
const response = { origin: 'ServerManager._downloadResources', context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name };
|
const response = {
|
||||||
|
origin: 'ServerManager._downloadResources',
|
||||||
|
context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name
|
||||||
|
};
|
||||||
|
|
||||||
this._psychoJS.logger.debug('downloading the registered resources for experiment: ' + this._psychoJS.config.experiment.name);
|
this._psychoJS.logger.debug('downloading the registered resources for experiment: ' + this._psychoJS.config.experiment.name);
|
||||||
|
|
||||||
@ -578,32 +665,43 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
const filesToDownload = resources.size ? resources : this._resources;
|
const filesToDownload = resources.size ? resources : this._resources;
|
||||||
|
|
||||||
this._resourceQueue.addEventListener("filestart", event => {
|
this._resourceQueue.addEventListener("filestart", event =>
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOADING_RESOURCE, resource: event.item.id });
|
{
|
||||||
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.DOWNLOADING_RESOURCE,
|
||||||
|
resource: event.item.id
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._resourceQueue.addEventListener("fileload", event => {
|
this._resourceQueue.addEventListener("fileload", event =>
|
||||||
|
{
|
||||||
++self._nbLoadedResources;
|
++self._nbLoadedResources;
|
||||||
let path_data = self._resources.get(event.item.id);
|
let path_data = self._resources.get(event.item.id);
|
||||||
path_data.data = event.result;
|
path_data.data = event.result;
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCE_DOWNLOADED, resource: event.item.id });
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.RESOURCE_DOWNLOADED,
|
||||||
|
resource: event.item.id
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// loading completed:
|
// loading completed:
|
||||||
this._resourceQueue.addEventListener("complete", event => {
|
this._resourceQueue.addEventListener("complete", event =>
|
||||||
|
{
|
||||||
self._resourceQueue.close();
|
self._resourceQueue.close();
|
||||||
if (self._nbLoadedResources === filesToDownload.size) {
|
if (self._nbLoadedResources === filesToDownload.size)
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
|
self.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// error: we throw an exception
|
// error: we throw an exception
|
||||||
this._resourceQueue.addEventListener("error", event => {
|
this._resourceQueue.addEventListener("error", event =>
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
const resourceId = (typeof event.data !== 'undefined')?event.data.id:'UNKNOWN RESOURCE';
|
const resourceId = (typeof event.data !== 'undefined') ? event.data.id : 'UNKNOWN RESOURCE';
|
||||||
// throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
|
// throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
|
||||||
throw Object.assign(response, { error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' });
|
throw Object.assign(response, {error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -623,9 +721,9 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
// preload.js with forced binary for xls and xlsx:
|
// preload.js with forced binary for xls and xlsx:
|
||||||
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(extension) > -1)
|
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(extension) > -1)
|
||||||
manifest.push({ id: name, src: path_data.path, type: createjs.Types.BINARY });
|
{
|
||||||
|
manifest.push({id: name, src: path_data.path, type: createjs.Types.BINARY});
|
||||||
/* ascii .csv are adequately handled in binary format
|
}/* ascii .csv are adequately handled in binary format
|
||||||
// forced text for .csv:
|
// forced text for .csv:
|
||||||
else if (['csv'].indexOf(resourceExtension) > -1)
|
else if (['csv'].indexOf(resourceExtension) > -1)
|
||||||
manifest.push({ id: resourceName, src: resourceName, type: createjs.Types.TEXT });
|
manifest.push({ id: resourceName, src: resourceName, type: createjs.Types.TEXT });
|
||||||
@ -637,29 +735,41 @@ export class ServerManager extends PsychObject {
|
|||||||
soundResources.push(name);
|
soundResources.push(name);
|
||||||
|
|
||||||
if (extension === 'wav')
|
if (extension === 'wav')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn(`wav files are not supported by all browsers. We recommend you convert "${name}" to another format, e.g. mp3`);
|
this.psychoJS.logger.warn(`wav files are not supported by all browsers. We recommend you convert "${name}" to another format, e.g. mp3`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// preload.js for the other extensions (download type decided by preload.js):
|
// preload.js for the other extensions (download type decided by preload.js):
|
||||||
else
|
else
|
||||||
manifest.push({ id: name, src: path_data.path });
|
{
|
||||||
|
manifest.push({id: name, src: path_data.path});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// (*) start loading non-sound resources:
|
// (*) start loading non-sound resources:
|
||||||
if (manifest.length > 0)
|
if (manifest.length > 0)
|
||||||
|
{
|
||||||
this._resourceQueue.loadManifest(manifest);
|
this._resourceQueue.loadManifest(manifest);
|
||||||
else {
|
}
|
||||||
if (this._nbLoadedResources === filesToDownload.size) {
|
else
|
||||||
|
{
|
||||||
|
if (this._nbLoadedResources === filesToDownload.size)
|
||||||
|
{
|
||||||
this.setStatus(ServerManager.Status.READY);
|
this.setStatus(ServerManager.Status.READY);
|
||||||
this.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
|
this.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// (*) prepare and start loading sound resources:
|
// (*) prepare and start loading sound resources:
|
||||||
for (const name of soundResources) {
|
for (const name of soundResources)
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOADING_RESOURCE, resource: name });
|
{
|
||||||
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.DOWNLOADING_RESOURCE,
|
||||||
|
resource: name
|
||||||
|
});
|
||||||
const path_data = self._resources.get(name);
|
const path_data = self._resources.get(name);
|
||||||
const howl = new Howl({
|
const howl = new Howl({
|
||||||
src: path_data.path,
|
src: path_data.path,
|
||||||
@ -667,20 +777,26 @@ export class ServerManager extends PsychObject {
|
|||||||
autoplay: false
|
autoplay: false
|
||||||
});
|
});
|
||||||
|
|
||||||
howl.on('load', (event) => {
|
howl.on('load', (event) =>
|
||||||
|
{
|
||||||
++self._nbLoadedResources;
|
++self._nbLoadedResources;
|
||||||
path_data.data = howl;
|
path_data.data = howl;
|
||||||
// self._resources.set(resource.name, howl);
|
// self._resources.set(resource.name, howl);
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCE_DOWNLOADED, resource: name });
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.RESOURCE_DOWNLOADED,
|
||||||
|
resource: name
|
||||||
|
});
|
||||||
|
|
||||||
if (self._nbLoadedResources === filesToDownload.size) {
|
if (self._nbLoadedResources === filesToDownload.size)
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
|
self.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
howl.on('loaderror', (id, error) => {
|
howl.on('loaderror', (id, error) =>
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
|
// throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
|
||||||
throw Object.assign(response, { error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' });
|
throw Object.assign(response, {error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')'});
|
||||||
});
|
});
|
||||||
|
|
||||||
howl.load();
|
howl.load();
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
* Window responsible for displaying the experiment stimuli
|
* Window responsible for displaying the experiment stimuli
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import { MonotonicClock } from '../util/Clock';
|
import {MonotonicClock} from '../util/Clock';
|
||||||
import { Logger } from "./Logger";
|
import {Logger} from "./Logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Window displays the various stimuli of the experiment.</p>
|
* <p>Window displays the various stimuli of the experiment.</p>
|
||||||
@ -29,7 +29,8 @@ import { Logger } from "./Logger";
|
|||||||
* before flipping
|
* before flipping
|
||||||
* @param {boolean} [options.autoLog= true] whether or not to log
|
* @param {boolean} [options.autoLog= true] whether or not to log
|
||||||
*/
|
*/
|
||||||
export class Window extends PsychObject {
|
export class Window extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter for monitorFramePeriod.
|
* Getter for monitorFramePeriod.
|
||||||
@ -38,17 +39,20 @@ export class Window extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get monitorFramePeriod() { return this._monitorFramePeriod; }
|
get monitorFramePeriod()
|
||||||
|
{
|
||||||
|
return this._monitorFramePeriod;
|
||||||
|
}
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
name,
|
name,
|
||||||
fullscr = false,
|
fullscr = false,
|
||||||
color = new Color('black'),
|
color = new Color('black'),
|
||||||
units = 'pix',
|
units = 'pix',
|
||||||
waitBlanking = false,
|
waitBlanking = false,
|
||||||
autoLog = true
|
autoLog = true
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super(psychoJS, name);
|
super(psychoJS, name);
|
||||||
|
|
||||||
@ -76,7 +80,8 @@ export class Window extends PsychObject {
|
|||||||
// fullscreen listener:
|
// fullscreen listener:
|
||||||
this._windowAlreadyInFullScreen = false;
|
this._windowAlreadyInFullScreen = false;
|
||||||
const self = this;
|
const self = this;
|
||||||
document.addEventListener('fullscreenchange', (event) => {
|
document.addEventListener('fullscreenchange', (event) =>
|
||||||
|
{
|
||||||
self._windowAlreadyInFullScreen = !!document.fullscreenElement;
|
self._windowAlreadyInFullScreen = !!document.fullscreenElement;
|
||||||
|
|
||||||
console.log('windowAlreadyInFullScreen:', self._windowAlreadyInFullScreen);
|
console.log('windowAlreadyInFullScreen:', self._windowAlreadyInFullScreen);
|
||||||
@ -84,12 +89,16 @@ export class Window extends PsychObject {
|
|||||||
// the Window and all of the stimuli need to be updated:
|
// the Window and all of the stimuli need to be updated:
|
||||||
self._needUpdate = true;
|
self._needUpdate = true;
|
||||||
for (const stimulus of self._drawList)
|
for (const stimulus of self._drawList)
|
||||||
|
{
|
||||||
stimulus._needUpdate = true;
|
stimulus._needUpdate = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -105,10 +114,14 @@ export class Window extends PsychObject {
|
|||||||
close()
|
close()
|
||||||
{
|
{
|
||||||
if (!this._renderer)
|
if (!this._renderer)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (document.body.contains(this._renderer.view))
|
if (document.body.contains(this._renderer.view))
|
||||||
|
{
|
||||||
document.body.removeChild(this._renderer.view);
|
document.body.removeChild(this._renderer.view);
|
||||||
|
}
|
||||||
|
|
||||||
// destroy the renderer and the WebGL context:
|
// destroy the renderer and the WebGL context:
|
||||||
if (typeof this._renderer.gl !== 'undefined')
|
if (typeof this._renderer.gl !== 'undefined')
|
||||||
@ -173,16 +186,21 @@ export class Window extends PsychObject {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (typeof document.documentElement.mozRequestFullScreen === 'function')
|
else if (typeof document.documentElement.mozRequestFullScreen === 'function')
|
||||||
|
{
|
||||||
document.documentElement.mozRequestFullScreen();
|
document.documentElement.mozRequestFullScreen();
|
||||||
|
}
|
||||||
else if (typeof document.documentElement.webkitRequestFullscreen === 'function')
|
else if (typeof document.documentElement.webkitRequestFullscreen === 'function')
|
||||||
|
{
|
||||||
document.documentElement.webkitRequestFullscreen();
|
document.documentElement.webkitRequestFullscreen();
|
||||||
|
}
|
||||||
else if (typeof document.documentElement.msRequestFullscreen === 'function')
|
else if (typeof document.documentElement.msRequestFullscreen === 'function')
|
||||||
|
{
|
||||||
document.documentElement.msRequestFullscreen();
|
document.documentElement.msRequestFullscreen();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('Unable to go fullscreen.');
|
this.psychoJS.logger.warn('Unable to go fullscreen.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -210,16 +228,21 @@ export class Window extends PsychObject {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (typeof document.mozCancelFullScreen === 'function')
|
else if (typeof document.mozCancelFullScreen === 'function')
|
||||||
|
{
|
||||||
document.mozCancelFullScreen();
|
document.mozCancelFullScreen();
|
||||||
|
}
|
||||||
else if (typeof document.webkitExitFullscreen === 'function')
|
else if (typeof document.webkitExitFullscreen === 'function')
|
||||||
|
{
|
||||||
document.webkitExitFullscreen();
|
document.webkitExitFullscreen();
|
||||||
|
}
|
||||||
else if (typeof document.msExitFullscreen === 'function')
|
else if (typeof document.msExitFullscreen === 'function')
|
||||||
|
{
|
||||||
document.msExitFullscreen();
|
document.msExitFullscreen();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('Unable to close fullscreen.');
|
this.psychoJS.logger.warn('Unable to close fullscreen.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -241,9 +264,10 @@ export class Window extends PsychObject {
|
|||||||
logOnFlip({
|
logOnFlip({
|
||||||
msg,
|
msg,
|
||||||
level = Logger.ServerLevel.EXP,
|
level = Logger.ServerLevel.EXP,
|
||||||
obj} = {})
|
obj
|
||||||
|
} = {})
|
||||||
{
|
{
|
||||||
this._msgToBeLogged.push({ msg, level, obj });
|
this._msgToBeLogged.push({msg, level, obj});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -281,7 +305,9 @@ export class Window extends PsychObject {
|
|||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
if (!this._renderer)
|
if (!this._renderer)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
this._frameCount++;
|
this._frameCount++;
|
||||||
@ -297,12 +323,16 @@ export class Window extends PsychObject {
|
|||||||
|
|
||||||
// blocks execution until the rendering is fully done:
|
// blocks execution until the rendering is fully done:
|
||||||
if (this._waitBlanking)
|
if (this._waitBlanking)
|
||||||
|
{
|
||||||
this._renderer.gl.finish();
|
this._renderer.gl.finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// call the callOnFlip functions and remove them:
|
// call the callOnFlip functions and remove them:
|
||||||
for (let callback of this._flipCallbacks)
|
for (let callback of this._flipCallbacks)
|
||||||
|
{
|
||||||
callback['function'](...callback['arguments']);
|
callback['function'](...callback['arguments']);
|
||||||
|
}
|
||||||
this._flipCallbacks = [];
|
this._flipCallbacks = [];
|
||||||
|
|
||||||
// log:
|
// log:
|
||||||
@ -325,7 +355,9 @@ export class Window extends PsychObject {
|
|||||||
if (this._needUpdate)
|
if (this._needUpdate)
|
||||||
{
|
{
|
||||||
if (this._renderer)
|
if (this._renderer)
|
||||||
|
{
|
||||||
this._renderer.backgroundColor = this._color.int;
|
this._renderer.backgroundColor = this._color.int;
|
||||||
|
}
|
||||||
|
|
||||||
// we also change the background color of the body since the dialog popup may be longer than the window's height:
|
// we also change the background color of the body since the dialog popup may be longer than the window's height:
|
||||||
document.body.style.backgroundColor = this._color.hex;
|
document.body.style.backgroundColor = this._color.hex;
|
||||||
@ -348,11 +380,14 @@ export class Window extends PsychObject {
|
|||||||
|
|
||||||
// if a stimuli needs to be updated, we remove it from the window container, update it, then put it back
|
// if a stimuli needs to be updated, we remove it from the window container, update it, then put it back
|
||||||
for (const stimulus of this._drawList)
|
for (const stimulus of this._drawList)
|
||||||
if (stimulus._needUpdate && typeof stimulus._pixi !== 'undefined') {
|
{
|
||||||
|
if (stimulus._needUpdate && typeof stimulus._pixi !== 'undefined')
|
||||||
|
{
|
||||||
this._rootContainer.removeChild(stimulus._pixi);
|
this._rootContainer.removeChild(stimulus._pixi);
|
||||||
stimulus._updateIfNeeded();
|
stimulus._updateIfNeeded();
|
||||||
this._rootContainer.addChild(stimulus._pixi);
|
this._rootContainer.addChild(stimulus._pixi);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -368,7 +403,9 @@ export class Window extends PsychObject {
|
|||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
|
|
||||||
for (const stimulus of this._drawList)
|
for (const stimulus of this._drawList)
|
||||||
|
{
|
||||||
stimulus.refresh();
|
stimulus.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
this._refresh();
|
this._refresh();
|
||||||
}
|
}
|
||||||
@ -413,7 +450,8 @@ export class Window extends PsychObject {
|
|||||||
this.psychoJS.eventManager.addMouseListeners(this._renderer);
|
this.psychoJS.eventManager.addMouseListeners(this._renderer);
|
||||||
|
|
||||||
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
|
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
|
||||||
this._resizeCallback = (e) => {
|
this._resizeCallback = (e) =>
|
||||||
|
{
|
||||||
Window._resizePixiRenderer(this, e);
|
Window._resizePixiRenderer(this, e);
|
||||||
this._fullRefresh();
|
this._fullRefresh();
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Mixin implementing various unit-handling measurement methods.
|
* Mixin implementing various unit-handling measurement methods.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -10,150 +10,185 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This mixin implements various unit-handling measurement methods.</p>
|
* <p>This mixin implements various unit-handling measurement methods.</p>
|
||||||
*
|
*
|
||||||
* <p>Note: (a) this is the equivalent of PsychoPY's WindowMixin.
|
* <p>Note: (a) this is the equivalent of PsychoPY's WindowMixin.
|
||||||
* (b) it will most probably be made obsolete by a fully-integrated unit approach.
|
* (b) it will most probably be made obsolete by a fully-integrated unit approach.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @name module:core.WindowMixin
|
* @name module:core.WindowMixin
|
||||||
* @mixin
|
* @mixin
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export let WindowMixin = (superclass) => class extends superclass {
|
export let WindowMixin = (superclass) => class extends superclass
|
||||||
constructor(args) {
|
{
|
||||||
|
constructor(args)
|
||||||
|
{
|
||||||
super(args);
|
super(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for units attribute.
|
* Setter for units attribute.
|
||||||
*
|
*
|
||||||
* @name module:core.WindowMixin#setUnits
|
* @name module:core.WindowMixin#setUnits
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the given length from stimulus unit to pixel units.
|
* Convert the given length from stimulus unit to pixel units.
|
||||||
*
|
*
|
||||||
* @name module:core.WindowMixin#_getLengthPix
|
* @name module:core.WindowMixin#_getLengthPix
|
||||||
* @function
|
* @function
|
||||||
* @protected
|
* @protected
|
||||||
* @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 response = { origin: 'WindowMixin._getLengthPix', context: 'when converting a length from stimulus unit to pixel units' };
|
{
|
||||||
|
let response = {
|
||||||
|
origin: 'WindowMixin._getLengthPix',
|
||||||
|
context: 'when converting a length from stimulus unit to pixel units'
|
||||||
|
};
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix')
|
||||||
|
{
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||||
|
{
|
||||||
var winSize = this.win.size;
|
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') {
|
else if (this._units === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||||
return length * minSize;
|
return length * minSize;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the given length from pixel units to the stimulus units
|
* Convert the given length from pixel units to the stimulus units
|
||||||
*
|
*
|
||||||
* @name module:core.WindowMixin#_getLengthUnits
|
* @name module:core.WindowMixin#_getLengthUnits
|
||||||
* @function
|
* @function
|
||||||
* @protected
|
* @protected
|
||||||
* @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 response = { origin: 'WindowMixin._getLengthUnits', context: 'when converting a length from pixel unit to stimulus units' };
|
{
|
||||||
|
let response = {
|
||||||
|
origin: 'WindowMixin._getLengthUnits',
|
||||||
|
context: 'when converting a length from pixel unit to stimulus units'
|
||||||
|
};
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix')
|
||||||
|
{
|
||||||
return length_px;
|
return length_px;
|
||||||
}
|
}
|
||||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||||
|
{
|
||||||
const winSize = this.win.size;
|
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') {
|
else if (this._units === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||||
return length_px / minSize;
|
return length_px / minSize;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the given length from pixel units to the stimulus units
|
* Convert the given length from pixel units to the stimulus units
|
||||||
*
|
*
|
||||||
* @name module:core.WindowMixin#_getHorLengthPix
|
* @name module:core.WindowMixin#_getHorLengthPix
|
||||||
* @function
|
* @function
|
||||||
* @protected
|
* @protected
|
||||||
* @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 response = { origin: 'WindowMixin._getHorLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
{
|
||||||
|
let response = {
|
||||||
|
origin: 'WindowMixin._getHorLengthPix',
|
||||||
|
context: 'when converting a length from pixel unit to stimulus units'
|
||||||
|
};
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix')
|
||||||
|
{
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||||
|
{
|
||||||
var winSize = this.win.size;
|
var winSize = this.win.size;
|
||||||
return length * winSize[0] / 2;
|
return length * winSize[0] / 2;
|
||||||
}
|
}
|
||||||
else if (this._units === 'height') {
|
else if (this._units === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||||
return length * minSize;
|
return length * minSize;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the given length from pixel units to the stimulus units
|
* Convert the given length from pixel units to the stimulus units
|
||||||
*
|
*
|
||||||
* @name module:core.WindowMixin#_getVerLengthPix
|
* @name module:core.WindowMixin#_getVerLengthPix
|
||||||
* @function
|
* @function
|
||||||
* @protected
|
* @protected
|
||||||
* @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 response = { origin: 'WindowMixin._getVerLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
{
|
||||||
|
let response = {
|
||||||
|
origin: 'WindowMixin._getVerLengthPix',
|
||||||
|
context: 'when converting a length from pixel unit to stimulus units'
|
||||||
|
};
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix')
|
||||||
|
{
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||||
|
{
|
||||||
var winSize = this.win.size;
|
var winSize = this.win.size;
|
||||||
return length * winSize[1] / 2;
|
return length * winSize[1] / 2;
|
||||||
}
|
}
|
||||||
else if (this._units === 'height') {
|
else if (this._units === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||||
return length * minSize;
|
return length * minSize;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Experiment Handler
|
* Experiment Handler
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import { MonotonicClock } from '../util/Clock';
|
import {MonotonicClock} from '../util/Clock';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -17,48 +17,63 @@ import * as util from '../util/Util';
|
|||||||
* <p>An ExperimentHandler keeps track of multiple loops and handlers. It is particularly useful
|
* <p>An ExperimentHandler keeps track of multiple loops and handlers. It is particularly useful
|
||||||
* for generating a single data file from an experiment with many different loops (e.g. interleaved
|
* for generating a single data file from an experiment with many different loops (e.g. interleaved
|
||||||
* staircases or loops within loops.</p>
|
* staircases or loops within loops.</p>
|
||||||
*
|
*
|
||||||
* @name module:data.ExperimentHandler
|
* @name module:data.ExperimentHandler
|
||||||
* @class
|
* @class
|
||||||
* @extends PsychObject
|
* @extends PsychObject
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
* @param {string} options.name - name of the experiment
|
* @param {string} options.name - name of the experiment
|
||||||
* @param {Object} options.extraInfo - additional information, such as session name, participant name, etc.
|
* @param {Object} options.extraInfo - additional information, such as session name, participant name, etc.
|
||||||
*/
|
*/
|
||||||
export class ExperimentHandler extends PsychObject {
|
export class ExperimentHandler extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter for experimentEnded.
|
* Getter for experimentEnded.
|
||||||
*
|
*
|
||||||
* @name module:core.Window#experimentEnded
|
* @name module:core.Window#experimentEnded
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get experimentEnded() { return this._experimentEnded; }
|
get experimentEnded()
|
||||||
|
{
|
||||||
|
return this._experimentEnded;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for experimentEnded.
|
* Setter for experimentEnded.
|
||||||
*
|
*
|
||||||
* @name module:core.Window#experimentEnded
|
* @name module:core.Window#experimentEnded
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
set experimentEnded(ended) { this._experimentEnded = ended; }
|
set experimentEnded(ended)
|
||||||
|
{
|
||||||
|
this._experimentEnded = ended;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy experiment getters.
|
* Legacy experiment getters.
|
||||||
*/
|
*/
|
||||||
get _thisEntry() { return this._currentTrialData; }
|
get _thisEntry()
|
||||||
get _entries() { return this._trialsData; }
|
{
|
||||||
|
return this._currentTrialData;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _entries()
|
||||||
|
{
|
||||||
|
return this._trialsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
name,
|
name,
|
||||||
extraInfo
|
extraInfo
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(psychoJS, name);
|
super(psychoJS, name);
|
||||||
|
|
||||||
this._addAttributes(ExperimentHandler, extraInfo);
|
this._addAttributes(ExperimentHandler, extraInfo);
|
||||||
@ -85,10 +100,13 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @returns {boolean} whether or not the current entry is empty
|
* @returns {boolean} whether or not the current entry is empty
|
||||||
*/
|
*/
|
||||||
isEntryEmpty() {
|
isEntryEmpty()
|
||||||
|
{
|
||||||
return (Object.keys(this._currentTrialData).length > 0);
|
return (Object.keys(this._currentTrialData).length > 0);
|
||||||
}
|
}
|
||||||
isEntryEmtpy() {
|
|
||||||
|
isEntryEmtpy()
|
||||||
|
{
|
||||||
return (Object.keys(this._currentTrialData).length > 0);
|
return (Object.keys(this._currentTrialData).length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +122,8 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
||||||
*/
|
*/
|
||||||
addLoop(loop) {
|
addLoop(loop)
|
||||||
|
{
|
||||||
this._loops.push(loop);
|
this._loops.push(loop);
|
||||||
this._unfinishedLoops.push(loop);
|
this._unfinishedLoops.push(loop);
|
||||||
loop.experimentHandler = this;
|
loop.experimentHandler = this;
|
||||||
@ -119,10 +138,13 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
||||||
*/
|
*/
|
||||||
removeLoop(loop) {
|
removeLoop(loop)
|
||||||
|
{
|
||||||
const index = this._unfinishedLoops.indexOf(loop);
|
const index = this._unfinishedLoops.indexOf(loop);
|
||||||
if (index !== -1)
|
if (index !== -1)
|
||||||
|
{
|
||||||
this._unfinishedLoops.splice(index, 1);
|
this._unfinishedLoops.splice(index, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -138,14 +160,18 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @param {Object} key - the key
|
* @param {Object} key - the key
|
||||||
* @param {Object} value - the value
|
* @param {Object} value - the value
|
||||||
*/
|
*/
|
||||||
addData(key, value) {
|
addData(key, value)
|
||||||
if (this._trialsKeys.indexOf(key) === -1) {
|
{
|
||||||
|
if (this._trialsKeys.indexOf(key) === -1)
|
||||||
|
{
|
||||||
this._trialsKeys.push(key);
|
this._trialsKeys.push(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// turn arrays into their json equivalent:
|
// turn arrays into their json equivalent:
|
||||||
if (Array.isArray(value))
|
if (Array.isArray(value))
|
||||||
|
{
|
||||||
value = JSON.stringify(value);
|
value = JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
this._currentTrialData[key] = value;
|
this._currentTrialData[key] = value;
|
||||||
}
|
}
|
||||||
@ -166,13 +192,20 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
{
|
{
|
||||||
// turn single snapshot into a one-element array:
|
// turn single snapshot into a one-element array:
|
||||||
if (!Array.isArray(snapshots))
|
if (!Array.isArray(snapshots))
|
||||||
|
{
|
||||||
snapshots = [snapshots];
|
snapshots = [snapshots];
|
||||||
|
}
|
||||||
|
|
||||||
for (const snapshot of snapshots) {
|
for (const snapshot of snapshots)
|
||||||
|
{
|
||||||
const attributes = ExperimentHandler._getLoopAttributes(snapshot);
|
const attributes = ExperimentHandler._getLoopAttributes(snapshot);
|
||||||
for (let a in attributes)
|
for (let a in attributes)
|
||||||
|
{
|
||||||
if (attributes.hasOwnProperty(a))
|
if (attributes.hasOwnProperty(a))
|
||||||
|
{
|
||||||
this._currentTrialData[a] = attributes[a];
|
this._currentTrialData[a] = attributes[a];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -184,15 +217,23 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
{
|
{
|
||||||
const attributes = ExperimentHandler._getLoopAttributes(loop);
|
const attributes = ExperimentHandler._getLoopAttributes(loop);
|
||||||
for (const a in attributes)
|
for (const a in attributes)
|
||||||
|
{
|
||||||
if (attributes.hasOwnProperty(a))
|
if (attributes.hasOwnProperty(a))
|
||||||
|
{
|
||||||
this._currentTrialData[a] = attributes[a];
|
this._currentTrialData[a] = attributes[a];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the extraInfo dict to the data:
|
// add the extraInfo dict to the data:
|
||||||
for (let a in this.extraInfo)
|
for (let a in this.extraInfo)
|
||||||
|
{
|
||||||
if (this.extraInfo.hasOwnProperty(a))
|
if (this.extraInfo.hasOwnProperty(a))
|
||||||
|
{
|
||||||
this._currentTrialData[a] = this.extraInfo[a];
|
this._currentTrialData[a] = this.extraInfo[a];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._trialsData.push(this._currentTrialData);
|
this._trialsData.push(this._currentTrialData);
|
||||||
|
|
||||||
@ -214,26 +255,38 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {Array.<Object>} [options.attributes] - the attributes to be saved
|
* @param {Array.<Object>} [options.attributes] - the attributes to be saved
|
||||||
|
* @param {Array.<Object>} [options.sync] - whether or not to communicate with the server in a synchronous manner
|
||||||
*/
|
*/
|
||||||
async save({
|
async save({
|
||||||
attributes = []
|
attributes = [],
|
||||||
} = {}) {
|
sync = false
|
||||||
|
} = {})
|
||||||
|
{
|
||||||
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
|
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
|
||||||
|
|
||||||
// (*) get attributes:
|
// (*) get attributes:
|
||||||
if (attributes.length === 0) {
|
if (attributes.length === 0)
|
||||||
|
{
|
||||||
attributes = this._trialsKeys.slice();
|
attributes = 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 = ExperimentHandler._getLoopAttributes(loop);
|
const loopAttributes = ExperimentHandler._getLoopAttributes(loop);
|
||||||
for (let a in loopAttributes)
|
for (let a in loopAttributes)
|
||||||
|
{
|
||||||
if (loopAttributes.hasOwnProperty(a))
|
if (loopAttributes.hasOwnProperty(a))
|
||||||
|
{
|
||||||
attributes.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))
|
||||||
|
{
|
||||||
attributes.push(a);
|
attributes.push(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,40 +302,25 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
|
|
||||||
|
|
||||||
// (*) save to a .csv file:
|
// (*) save to a .csv file:
|
||||||
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV) {
|
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV)
|
||||||
/*
|
{
|
||||||
// a. manual approach
|
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
|
||||||
let csv = "";
|
// newlines, etc.
|
||||||
|
|
||||||
// build the csv header:
|
|
||||||
for (let h = 0; h < attributes.length; h++) {
|
|
||||||
if (h > 0)
|
|
||||||
csv = csv + ', ';
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// b. XLSX approach (automatically deal with header, takes care of quotes, newlines, etc.)
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
|
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
|
||||||
const csv = XLSX.utils.sheet_to_csv(worksheet);
|
const csv = XLSX.utils.sheet_to_csv(worksheet);
|
||||||
|
|
||||||
// upload data to the pavlovia server or offer them for download:
|
// upload data to the pavlovia server or offer them for download:
|
||||||
const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
|
const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
|
||||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER && this._psychoJS.config.experiment.status === 'RUNNING')
|
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
||||||
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv);
|
this._psychoJS.config.experiment.status === 'RUNNING' &&
|
||||||
|
!this._psychoJS._serverMsg.has('__pilotToken'))
|
||||||
|
{
|
||||||
|
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv, sync);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
util.offerDataForDownload(key, csv, 'text/csv');
|
util.offerDataForDownload(key, csv, 'text/csv');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -291,20 +329,29 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
{
|
{
|
||||||
let documents = [];
|
let documents = [];
|
||||||
|
|
||||||
for (let r = 0; r < this._trialsData.length; r++) {
|
for (let r = 0; r < this._trialsData.length; r++)
|
||||||
|
{
|
||||||
let doc = {__projectId, __experimentName, __participant, __session, __datetime};
|
let doc = {__projectId, __experimentName, __participant, __session, __datetime};
|
||||||
for (let h = 0; h < attributes.length; h++)
|
for (let h = 0; h < attributes.length; h++)
|
||||||
|
{
|
||||||
doc[attributes[h]] = this._trialsData[r][attributes[h]];
|
doc[attributes[h]] = this._trialsData[r][attributes[h]];
|
||||||
|
}
|
||||||
|
|
||||||
documents.push(doc);
|
documents.push(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload data to the pavlovia server or offer them for download:
|
// upload data to the pavlovia server or offer them for download:
|
||||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) {
|
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
||||||
|
this._psychoJS.config.experiment.status === 'RUNNING' &&
|
||||||
|
!this._psychoJS._serverMsg.has('__pilotToken'))
|
||||||
|
{
|
||||||
const key = 'results'; // name of the mongoDB collection
|
const key = 'results'; // name of the mongoDB collection
|
||||||
return await this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents));
|
return /*await*/ this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents), sync);
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
util.offerDataForDownload('results.json', JSON.stringify(documents), 'application/json');
|
util.offerDataForDownload('results.json', JSON.stringify(documents), 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,29 +360,36 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
/**
|
/**
|
||||||
* Get the attribute names and values for the current trial of a given loop.
|
* Get the attribute names and values for the current trial of a given loop.
|
||||||
* <p> Only only info relating to the trial execution are returned.</p>
|
* <p> Only only info relating to the trial execution are returned.</p>
|
||||||
*
|
*
|
||||||
* @name module:data.ExperimentHandler#_getLoopAttributes
|
* @name module:data.ExperimentHandler#_getLoopAttributes
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
* @protected
|
* @protected
|
||||||
* @param {Object} loop - the loop
|
* @param {Object} loop - the loop
|
||||||
*/
|
*/
|
||||||
static _getLoopAttributes(loop) {
|
static _getLoopAttributes(loop)
|
||||||
|
{
|
||||||
// standard trial attributes:
|
// standard trial attributes:
|
||||||
const properties = ['thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'stepSizeCurrent', 'ran', 'order'];
|
const properties = ['thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'stepSizeCurrent', 'ran', 'order'];
|
||||||
let attributes = {};
|
let attributes = {};
|
||||||
const loopName = loop.name;
|
const loopName = loop.name;
|
||||||
for (const loopProperty in loop)
|
for (const loopProperty in loop)
|
||||||
if (properties.includes(loopProperty)) {
|
{
|
||||||
const key = (loopProperty === 'stepSizeCurrent')? loopName + '.stepSize' : loopName + '.' + loopProperty;
|
if (properties.includes(loopProperty))
|
||||||
|
{
|
||||||
|
const key = (loopProperty === 'stepSizeCurrent') ? loopName + '.stepSize' : loopName + '.' + loopProperty;
|
||||||
attributes[key] = loop[loopProperty];
|
attributes[key] = loop[loopProperty];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// specific trial attributes:
|
// specific trial attributes:
|
||||||
if (typeof loop.getCurrentTrial === 'function') {
|
if (typeof loop.getCurrentTrial === 'function')
|
||||||
|
{
|
||||||
const currentTrial = loop.getCurrentTrial();
|
const currentTrial = loop.getCurrentTrial();
|
||||||
for (const trialProperty in currentTrial)
|
for (const trialProperty in currentTrial)
|
||||||
|
{
|
||||||
attributes[trialProperty] = currentTrial[trialProperty];
|
attributes[trialProperty] = currentTrial[trialProperty];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO
|
/* TODO
|
||||||
@ -370,7 +424,7 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Experiment result format
|
* Experiment result format
|
||||||
*
|
*
|
||||||
* @name module:core.ServerManager#SaveFormat
|
* @name module:core.ServerManager#SaveFormat
|
||||||
* @enum {Symbol}
|
* @enum {Symbol}
|
||||||
* @readonly
|
* @readonly
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
/** @module data */
|
/** @module data */
|
||||||
/**
|
/**
|
||||||
* Trial Handler
|
* Trial Handler
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>A Trial Handler handles the importing and sequencing of conditions.</p>
|
* <p>A Trial Handler handles the importing and sequencing of conditions.</p>
|
||||||
*
|
*
|
||||||
* @class
|
* @class
|
||||||
* @extends PsychObject
|
* @extends PsychObject
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -27,25 +27,30 @@ import * as util from '../util/Util';
|
|||||||
* @param {number} options.seed - seed for the random number generator
|
* @param {number} options.seed - seed for the random number generator
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class TrialHandler extends PsychObject {
|
export class TrialHandler extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter for experimentHandler.
|
* Getter for experimentHandler.
|
||||||
*
|
*
|
||||||
* @name module:core.Window#experimentHandler
|
* @name module:core.Window#experimentHandler
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get experimentHandler() { return this._experimentHandler; }
|
get experimentHandler()
|
||||||
|
{
|
||||||
|
return this._experimentHandler;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for experimentHandler.
|
* Setter for experimentHandler.
|
||||||
*
|
*
|
||||||
* @name module:core.Window#experimentHandler
|
* @name module:core.Window#experimentHandler
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
set experimentHandler(exp) {
|
set experimentHandler(exp)
|
||||||
|
{
|
||||||
this._experimentHandler = exp;
|
this._experimentHandler = exp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,15 +62,16 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @todo extraInfo is not taken into account, we use the expInfo of the ExperimentHandler instead
|
* @todo extraInfo is not taken into account, we use the expInfo of the ExperimentHandler instead
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
trialList = [undefined],
|
trialList = [undefined],
|
||||||
nReps,
|
nReps,
|
||||||
method = TrialHandler.Method.RANDOM,
|
method = TrialHandler.Method.RANDOM,
|
||||||
extraInfo = [],
|
extraInfo = [],
|
||||||
seed,
|
seed,
|
||||||
name,
|
name,
|
||||||
autoLog = true
|
autoLog = true
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
this._addAttributes(TrialHandler, trialList, nReps, method, extraInfo, seed, name, autoLog);
|
this._addAttributes(TrialHandler, trialList, nReps, method, extraInfo, seed, name, autoLog);
|
||||||
@ -117,13 +123,15 @@ export class TrialHandler extends PsychObject {
|
|||||||
[Symbol.iterator]()
|
[Symbol.iterator]()
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
next: () => {
|
next: () =>
|
||||||
|
{
|
||||||
this.thisTrialN++;
|
this.thisTrialN++;
|
||||||
this.thisN++;
|
this.thisN++;
|
||||||
this.nRemaining--;
|
this.nRemaining--;
|
||||||
|
|
||||||
// check for the last trial:
|
// check for the last trial:
|
||||||
if (this.nRemaining === 0) {
|
if (this.nRemaining === 0)
|
||||||
|
{
|
||||||
this.finished = true;
|
this.finished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,9 +143,10 @@ export class TrialHandler extends PsychObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if we have completed the sequence:
|
// check if we have completed the sequence:
|
||||||
if (this.thisRepN >= this.nReps) {
|
if (this.thisRepN >= this.nReps)
|
||||||
|
{
|
||||||
this.thisTrial = null;
|
this.thisTrial = null;
|
||||||
return { done: true };
|
return {done: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.thisIndex = this._trialSequence[this.thisRepN][this.thisTrialN];
|
this.thisIndex = this._trialSequence[this.thisRepN][this.thisTrialN];
|
||||||
@ -150,7 +159,7 @@ export class TrialHandler extends PsychObject {
|
|||||||
vals = (self.thisRepN, self.thisTrialN, self.thisTrial)
|
vals = (self.thisRepN, self.thisTrialN, self.thisTrial)
|
||||||
logging.exp(msg % vals, obj=self.thisTrial)*/
|
logging.exp(msg % vals, obj=self.thisTrial)*/
|
||||||
|
|
||||||
return { value: this.thisTrial, done: false };
|
return {value: this.thisTrial, done: false};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -158,24 +167,26 @@ export class TrialHandler extends PsychObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the callback for each trial in the sequence.
|
* Execute the callback for each trial in the sequence.
|
||||||
*
|
*
|
||||||
* @param callback
|
* @param callback
|
||||||
*/
|
*/
|
||||||
forEach(callback)
|
forEach(callback)
|
||||||
{
|
{
|
||||||
const trialIterator = this[Symbol.iterator]();
|
const trialIterator = this[Symbol.iterator]();
|
||||||
|
|
||||||
while(true)
|
while (true)
|
||||||
{
|
{
|
||||||
const result = trialIterator.next();
|
const result = trialIterator.next();
|
||||||
if (result.done)
|
if (result.done)
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
callback(result.value);
|
callback(result.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Snapshot
|
* @typedef {Object} Snapshot
|
||||||
* @property {string} name - the trialHandler name
|
* @property {string} name - the trialHandler name
|
||||||
@ -225,25 +236,27 @@ export class TrialHandler extends PsychObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the trial index.
|
* Get the trial index.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @return {number} the current trial index
|
* @return {number} the current trial index
|
||||||
*/
|
*/
|
||||||
getTrialIndex() {
|
getTrialIndex()
|
||||||
|
{
|
||||||
return this.thisIndex;
|
return this.thisIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the trial index.
|
* Set the trial index.
|
||||||
*
|
*
|
||||||
* @param {number} index - the new trial index
|
* @param {number} index - the new trial index
|
||||||
*/
|
*/
|
||||||
setTrialIndex(index) {
|
setTrialIndex(index)
|
||||||
|
{
|
||||||
this.thisIndex = index;
|
this.thisIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attributes of the trials.
|
* Get the attributes of the trials.
|
||||||
*
|
*
|
||||||
@ -253,13 +266,18 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Array.string} the attributes
|
* @return {Array.string} the attributes
|
||||||
*/
|
*/
|
||||||
getAttributes() {
|
getAttributes()
|
||||||
|
{
|
||||||
if (!Array.isArray(this.trialList) || this.nStim === 0)
|
if (!Array.isArray(this.trialList) || this.nStim === 0)
|
||||||
|
{
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const firstTrial = this.trialList[0];
|
const firstTrial = this.trialList[0];
|
||||||
if (!firstTrial)
|
if (!firstTrial)
|
||||||
|
{
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(this.trialList[0]);
|
return Object.keys(this.trialList[0]);
|
||||||
}
|
}
|
||||||
@ -267,11 +285,12 @@ export class TrialHandler extends PsychObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current trial.
|
* Get the current trial.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @return {Object} the current trial
|
* @return {Object} the current trial
|
||||||
*/
|
*/
|
||||||
getCurrentTrial() {
|
getCurrentTrial()
|
||||||
|
{
|
||||||
return this.trialList[this.thisIndex];
|
return this.trialList[this.thisIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,9 +301,12 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @param {number} index - the trial index
|
* @param {number} index - the trial index
|
||||||
* @return {Object|undefined} the requested trial or undefined if attempting to go beyond the last trial.
|
* @return {Object|undefined} the requested trial or undefined if attempting to go beyond the last trial.
|
||||||
*/
|
*/
|
||||||
getTrial(index = 0) {
|
getTrial(index = 0)
|
||||||
|
{
|
||||||
if (index < 0 || index > this.nTotal)
|
if (index < 0 || index > this.nTotal)
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return this.trialList[index];
|
return this.trialList[index];
|
||||||
}
|
}
|
||||||
@ -298,11 +320,14 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @return {Object|undefined} the future trial (if n is positive) or past trial (if n is negative)
|
* @return {Object|undefined} the future trial (if n is positive) or past trial (if n is negative)
|
||||||
* or undefined if attempting to go beyond the last trial.
|
* or undefined if attempting to go beyond the last trial.
|
||||||
*/
|
*/
|
||||||
getFutureTrial(n = 1) {
|
getFutureTrial(n = 1)
|
||||||
if (this.thisIndex+n < 0 || n > this.nRemaining)
|
{
|
||||||
|
if (this.thisIndex + n < 0 || n > this.nRemaining)
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return this.trialList[this.thisIndex+n];
|
return this.trialList[this.thisIndex + n];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -314,7 +339,8 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @param {number} [n = -1] - increment
|
* @param {number} [n = -1] - increment
|
||||||
* @return {Object|undefined} the past trial or undefined if attempting to go prior to the first trial.
|
* @return {Object|undefined} the past trial or undefined if attempting to go prior to the first trial.
|
||||||
*/
|
*/
|
||||||
getEarlierTrial(n = -1) {
|
getEarlierTrial(n = -1)
|
||||||
|
{
|
||||||
return getFutureTrial(-abs(n));
|
return getFutureTrial(-abs(n));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,9 +352,12 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @param {Object} key - the key
|
* @param {Object} key - the key
|
||||||
* @param {Object} value - the value
|
* @param {Object} value - the value
|
||||||
*/
|
*/
|
||||||
addData(key, value) {
|
addData(key, value)
|
||||||
|
{
|
||||||
if (this._experimentHandler)
|
if (this._experimentHandler)
|
||||||
|
{
|
||||||
this._experimentHandler.addData(key, value);
|
this._experimentHandler.addData(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -367,23 +396,28 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @return {Object} the parsed conditions as an array of 'object as map'
|
* @return {Object} the parsed conditions as an array of 'object as map'
|
||||||
* @throws {Object} Throws an exception if importing the conditions failed.
|
* @throws {Object} Throws an exception if importing the conditions failed.
|
||||||
*/
|
*/
|
||||||
static importConditions(serverManager, resourceName, selection = null) {
|
static importConditions(serverManager, resourceName, selection = null)
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
let resourceExtension = resourceName.split('.').pop();
|
let resourceExtension = resourceName.split('.').pop();
|
||||||
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(resourceExtension) > -1) {
|
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(resourceExtension) > -1)
|
||||||
|
{
|
||||||
// (*) read conditions from resource:
|
// (*) read conditions from resource:
|
||||||
const resourceValue = serverManager.getResource(resourceName);
|
const resourceValue = serverManager.getResource(resourceName);
|
||||||
const workbook = XLSX.read(new Uint8Array(resourceValue), { type: "array" });
|
const workbook = XLSX.read(new Uint8Array(resourceValue), {type: "array"});
|
||||||
// const workbook = XLSX.read(resourceValue, { type: "buffer" }); // would work for ascii .csv
|
// const workbook = XLSX.read(resourceValue, { type: "buffer" }); // would work for ascii .csv
|
||||||
|
|
||||||
// we consider only the first worksheet:
|
// we consider only the first worksheet:
|
||||||
if (workbook.SheetNames.length === 0)
|
if (workbook.SheetNames.length === 0)
|
||||||
|
{
|
||||||
throw 'workbook should contain at least one worksheet';
|
throw 'workbook should contain at least one worksheet';
|
||||||
|
}
|
||||||
const sheetName = workbook.SheetNames[0];
|
const sheetName = workbook.SheetNames[0];
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
|
||||||
// worksheet to array of arrays (the first array contains the fields):
|
// worksheet to array of arrays (the first array contains the fields):
|
||||||
const sheet = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false });
|
const sheet = XLSX.utils.sheet_to_json(worksheet, {header: 1, blankrows: false});
|
||||||
const fields = sheet.shift();
|
const fields = sheet.shift();
|
||||||
|
|
||||||
// (*) select conditions:
|
// (*) select conditions:
|
||||||
@ -396,14 +430,17 @@ export class TrialHandler extends PsychObject {
|
|||||||
// ...
|
// ...
|
||||||
// ]
|
// ]
|
||||||
let trialList = new Array(selectedRows.length - 1);
|
let trialList = new Array(selectedRows.length - 1);
|
||||||
for (let r = 0; r < selectedRows.length; ++r) {
|
for (let r = 0; r < selectedRows.length; ++r)
|
||||||
|
{
|
||||||
let row = selectedRows[r];
|
let row = selectedRows[r];
|
||||||
let trial = {};
|
let trial = {};
|
||||||
for (let l = 0; l < fields.length; ++l) {
|
for (let l = 0; l < fields.length; ++l)
|
||||||
|
{
|
||||||
let value = row[l];
|
let value = row[l];
|
||||||
|
|
||||||
// if value is a numerical string, convert it to a number:
|
// if value is a numerical string, convert it to a number:
|
||||||
if (typeof value === 'string' && !isNaN(value)) {
|
if (typeof value === 'string' && !isNaN(value))
|
||||||
|
{
|
||||||
value = Number.parseFloat(value);
|
value = Number.parseFloat(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,12 +452,18 @@ export class TrialHandler extends PsychObject {
|
|||||||
return trialList;
|
return trialList;
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
throw 'extension: ' + resourceExtension + ' currently not supported.';
|
throw 'extension: ' + resourceExtension + ' currently not supported.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw { origin: 'TrialHandler.importConditions', context: `when importing condition: ${resourceName}`, error};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'TrialHandler.importConditions',
|
||||||
|
context: `when importing condition: ${resourceName}`,
|
||||||
|
error
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,27 +474,35 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @protected
|
* @protected
|
||||||
* @param {Array.<Object> | String} trialList - a list of trials, or the name of a condition resource
|
* @param {Array.<Object> | String} trialList - a list of trials, or the name of a condition resource
|
||||||
*/
|
*/
|
||||||
_prepareTrialList(trialList) {
|
_prepareTrialList(trialList)
|
||||||
const response = { origin : 'TrialHandler._prepareTrialList', context : 'when preparing the trial list' };
|
{
|
||||||
|
const response = {origin: 'TrialHandler._prepareTrialList', context: 'when preparing the trial list'};
|
||||||
|
|
||||||
// we treat undefined trialList as a list with a single empty entry:
|
// we treat undefined trialList as a list with a single empty entry:
|
||||||
if (typeof trialList === 'undefined')
|
if (typeof trialList === 'undefined')
|
||||||
|
{
|
||||||
this.trialList = [undefined];
|
this.trialList = [undefined];
|
||||||
|
}// if trialList is an array, we make sure it is not empty:
|
||||||
// if trialList is an array, we make sure it is not empty:
|
else if (Array.isArray(trialList))
|
||||||
else if (Array.isArray(trialList)) {
|
{
|
||||||
if (trialList.length === 0)
|
if (trialList.length === 0)
|
||||||
|
{
|
||||||
this.trialList = [undefined];
|
this.trialList = [undefined];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if trialList is a string, we treat it as the name of the condition resource:
|
// if trialList is a string, we treat it as the name of the condition resource:
|
||||||
else if (typeof trialList === 'string')
|
else if (typeof trialList === 'string')
|
||||||
|
{
|
||||||
this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, trialList);
|
this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, trialList);
|
||||||
|
}// unknown type:
|
||||||
// unknown type:
|
|
||||||
else
|
else
|
||||||
throw Object.assign(response, { error: 'unable to prepare trial list:' +
|
{
|
||||||
' unknown type: ' + (typeof trialList) });
|
throw Object.assign(response, {
|
||||||
|
error: 'unable to prepare trial list:' +
|
||||||
|
' unknown type: ' + (typeof trialList)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -481,35 +532,50 @@ export class TrialHandler extends PsychObject {
|
|||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_prepareSequence() {
|
_prepareSequence()
|
||||||
const response = { origin : 'TrialHandler._prepareSequence', context : 'when preparing a sequence of trials' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'TrialHandler._prepareSequence',
|
||||||
|
context: 'when preparing a sequence of trials'
|
||||||
|
};
|
||||||
|
|
||||||
// get an array of the indices of the elements of trialList :
|
// get an array of the indices of the elements of trialList :
|
||||||
const indices = Array.from(this.trialList.keys());
|
const indices = Array.from(this.trialList.keys());
|
||||||
|
|
||||||
// seed the random number generator:
|
// seed the random number generator:
|
||||||
if (typeof (this.seed) !== 'undefined')
|
if (typeof (this.seed) !== 'undefined')
|
||||||
|
{
|
||||||
Math.seedrandom(this.seed);
|
Math.seedrandom(this.seed);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
Math.seedrandom();
|
Math.seedrandom();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.method === TrialHandler.Method.SEQUENTIAL) {
|
if (this.method === TrialHandler.Method.SEQUENTIAL)
|
||||||
|
{
|
||||||
this._trialSequence = Array(this.nReps).fill(indices);
|
this._trialSequence = Array(this.nReps).fill(indices);
|
||||||
// transposed version:
|
// transposed version:
|
||||||
//this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] );
|
//this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] );
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (this.method === TrialHandler.Method.RANDOM) {
|
else if (this.method === TrialHandler.Method.RANDOM)
|
||||||
|
{
|
||||||
this._trialSequence = [];
|
this._trialSequence = [];
|
||||||
for (let i = 0; i < this.nReps; ++i)
|
for (let i = 0; i < this.nReps; ++i)
|
||||||
|
{
|
||||||
this._trialSequence.push(util.shuffle(indices.slice()));
|
this._trialSequence.push(util.shuffle(indices.slice()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (this.method === TrialHandler.Method.FULL_RANDOM) {
|
else if (this.method === TrialHandler.Method.FULL_RANDOM)
|
||||||
|
{
|
||||||
// create a flat sequence with nReps repeats of indices:
|
// create a flat sequence with nReps repeats of indices:
|
||||||
let flatSequence = [];
|
let flatSequence = [];
|
||||||
for (let i = 0; i < this.nReps; ++i)
|
for (let i = 0; i < this.nReps; ++i)
|
||||||
|
{
|
||||||
flatSequence.push.apply(flatSequence, indices);
|
flatSequence.push.apply(flatSequence, indices);
|
||||||
|
}
|
||||||
|
|
||||||
// shuffle the sequence:
|
// shuffle the sequence:
|
||||||
util.shuffle(flatSequence);
|
util.shuffle(flatSequence);
|
||||||
@ -517,10 +583,13 @@ export class TrialHandler extends PsychObject {
|
|||||||
// reshape it into the trialSequence:
|
// reshape it into the trialSequence:
|
||||||
this._trialSequence = [];
|
this._trialSequence = [];
|
||||||
for (let i = 0; i < this.nReps; i++)
|
for (let i = 0; i < this.nReps; i++)
|
||||||
|
{
|
||||||
this._trialSequence.push(flatSequence.slice(i * this.nStim, (i + 1) * this.nStim));
|
this._trialSequence.push(flatSequence.slice(i * this.nStim, (i + 1) * this.nStim));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
throw Object.assign(response, { error: 'unknown method' });
|
{
|
||||||
|
throw Object.assign(response, {error: 'unknown method'});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._trialSequence;
|
return this._trialSequence;
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
/** @module sound */
|
/** @module sound */
|
||||||
/**
|
/**
|
||||||
* Sound stimulus.
|
* Sound stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PsychoJS } from '../core/PsychoJS';
|
import {PsychoJS} from '../core/PsychoJS';
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import { TonePlayer } from './TonePlayer';
|
import {TonePlayer} from './TonePlayer';
|
||||||
import { TrackPlayer } from './TrackPlayer';
|
import {TrackPlayer} from './TrackPlayer';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This class handles sound playing (tones and tracks)</p>
|
* <p>This class handles sound playing (tones and tracks)</p>
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li> If value is a number then a tone will be generated at that frequency in Hz.</li>
|
* <li> If value is a number then a tone will be generated at that frequency in Hz.</li>
|
||||||
* <li> It value is a string, it must either be a note in the PsychoPy format (e.g 'A', 'Bfl', 'B', 'C', 'Csh'), in which case an octave must also be given, or the name of the resource track.</li>
|
* <li> It value is a string, it must either be a note in the PsychoPy format (e.g 'A', 'Bfl', 'B', 'C', 'Csh'), in which case an octave must also be given, or the name of the resource track.</li>
|
||||||
@ -24,7 +24,7 @@ import { TrackPlayer } from './TrackPlayer';
|
|||||||
*
|
*
|
||||||
* <p> Note: the PsychoPy hamming parameter has not been implemented yet. It might be rather tricky to do so using
|
* <p> Note: the PsychoPy hamming parameter has not been implemented yet. It might be rather tricky to do so using
|
||||||
* Tone.js</p>
|
* Tone.js</p>
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* [...]
|
* [...]
|
||||||
* const track = new Sound({
|
* const track = new Sound({
|
||||||
@ -49,22 +49,24 @@ import { TrackPlayer } from './TrackPlayer';
|
|||||||
* @param {number} [options.volume= 1.0] - volume of the sound (must be between 0 and 1.0)
|
* @param {number} [options.volume= 1.0] - volume of the sound (must be between 0 and 1.0)
|
||||||
* @param {number} [options.loops= 0] - how many times to repeat the track or tone after it has played once. If loops == -1, the track or tone will repeat indefinitely until stopped.
|
* @param {number} [options.loops= 0] - how many times to repeat the track or tone after it has played once. If loops == -1, the track or tone will repeat indefinitely until stopped.
|
||||||
* @param {boolean} [options.autoLog= true] whether or not to log
|
* @param {boolean} [options.autoLog= true] whether or not to log
|
||||||
*/
|
*/
|
||||||
export class Sound extends PsychObject {
|
export class Sound extends PsychObject
|
||||||
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
value = 'C',
|
value = 'C',
|
||||||
octave = 4,
|
octave = 4,
|
||||||
secs = 0.5,
|
secs = 0.5,
|
||||||
startTime = 0,
|
startTime = 0,
|
||||||
stopTime = -1,
|
stopTime = -1,
|
||||||
stereo = true,
|
stereo = true,
|
||||||
volume = 1.0,
|
volume = 1.0,
|
||||||
loops = 0,
|
loops = 0,
|
||||||
//hamming = true,
|
//hamming = true,
|
||||||
autoLog = true
|
autoLog = true
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(win._psychoJS, name);
|
super(win._psychoJS, name);
|
||||||
|
|
||||||
// the SoundPlayer, e.g. TonePlayer:
|
// the SoundPlayer, e.g. TonePlayer:
|
||||||
@ -84,7 +86,7 @@ export class Sound extends PsychObject {
|
|||||||
*
|
*
|
||||||
* <p> Note: Sounds are played independently from the stimuli of the experiments, i.e. the experiment will not stop until the sound is finished playing.
|
* <p> Note: Sounds are played independently from the stimuli of the experiments, i.e. the experiment will not stop until the sound is finished playing.
|
||||||
* Repeat calls to play may results in the sounds being played on top of each other.</p>
|
* Repeat calls to play may results in the sounds being played on top of each other.</p>
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @param {number} loops how many times to repeat the sound after it plays once. If loops == -1, the sound will repeat indefinitely until stopped.
|
* @param {number} loops how many times to repeat the sound after it plays once. If loops == -1, the sound will repeat indefinitely until stopped.
|
||||||
* @param {boolean} [log= true] whether or not to log
|
* @param {boolean} [log= true] whether or not to log
|
||||||
@ -104,8 +106,8 @@ export class Sound extends PsychObject {
|
|||||||
* @param {boolean} [options.log= true] - whether or not to log
|
* @param {boolean} [options.log= true] - whether or not to log
|
||||||
*/
|
*/
|
||||||
stop({
|
stop({
|
||||||
log = true
|
log = true
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
this._player.stop();
|
this._player.stop();
|
||||||
this.status = PsychoJS.Status.STOPPED;
|
this.status = PsychoJS.Status.STOPPED;
|
||||||
@ -118,7 +120,8 @@ export class Sound extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {number} the duration of the sound, in seconds
|
* @return {number} the duration of the sound, in seconds
|
||||||
*/
|
*/
|
||||||
getDuration() {
|
getDuration()
|
||||||
|
{
|
||||||
return this._player.getDuration();
|
return this._player.getDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +129,7 @@ export class Sound extends PsychObject {
|
|||||||
/**
|
/**
|
||||||
* Set the playing volume of the sound.
|
* Set the playing volume of the sound.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @param {number} volume - the volume (values should be between 0 and 1)
|
* @param {number} volume - the volume (values should be between 0 and 1)
|
||||||
* @param {boolean} [mute= false] - whether or not to mute the sound
|
* @param {boolean} [mute= false] - whether or not to mute the sound
|
||||||
* @param {boolean} [log= true] - whether of not to log
|
* @param {boolean} [log= true] - whether of not to log
|
||||||
@ -136,14 +139,16 @@ export class Sound extends PsychObject {
|
|||||||
this._setAttribute('volume', volume, log);
|
this._setAttribute('volume', volume, log);
|
||||||
|
|
||||||
if (typeof this._player !== 'undefined')
|
if (typeof this._player !== 'undefined')
|
||||||
|
{
|
||||||
this._player.setVolume(volume, mute);
|
this._player.setVolume(volume, mute);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the number of loops.
|
* Set the number of loops.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @param {number} [loops=0] - how many times to repeat the sound after it has played once. If loops == -1, the sound will repeat indefinitely until stopped.
|
* @param {number} [loops=0] - how many times to repeat the sound after it has played once. If loops == -1, the sound will repeat indefinitely until stopped.
|
||||||
* @param {boolean} [log=true] - whether of not to log
|
* @param {boolean} [log=true] - whether of not to log
|
||||||
*/
|
*/
|
||||||
@ -152,11 +157,12 @@ export class Sound extends PsychObject {
|
|||||||
this._setAttribute('loops', loops, log);
|
this._setAttribute('loops', loops, log);
|
||||||
|
|
||||||
if (typeof this._player !== 'undefined')
|
if (typeof this._player !== 'undefined')
|
||||||
|
{
|
||||||
this._player.setLoops(loops);
|
this._player.setLoops(loops);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the duration (in seconds)
|
* Set the duration (in seconds)
|
||||||
*
|
*
|
||||||
@ -169,11 +175,12 @@ export class Sound extends PsychObject {
|
|||||||
this._setAttribute('secs', secs, log);
|
this._setAttribute('secs', secs, log);
|
||||||
|
|
||||||
if (typeof this._player !== 'undefined')
|
if (typeof this._player !== 'undefined')
|
||||||
|
{
|
||||||
this._player.setDuration(secs);
|
this._player.setDuration(secs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identify the appropriate player for the sound.
|
* Identify the appropriate player for the sound.
|
||||||
*
|
*
|
||||||
@ -181,19 +188,27 @@ export class Sound extends PsychObject {
|
|||||||
* @return {SoundPlayer} the appropriate SoundPlayer
|
* @return {SoundPlayer} the appropriate SoundPlayer
|
||||||
* @throws {Object.<string, *>} exception if no appropriate SoundPlayer could be found for the sound
|
* @throws {Object.<string, *>} exception if no appropriate SoundPlayer could be found for the sound
|
||||||
*/
|
*/
|
||||||
_getPlayer() {
|
_getPlayer()
|
||||||
|
{
|
||||||
const acceptFns = [
|
const acceptFns = [
|
||||||
sound => TonePlayer.accept(sound),
|
sound => TonePlayer.accept(sound),
|
||||||
sound => TrackPlayer.accept(sound)
|
sound => TrackPlayer.accept(sound)
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const acceptFn of acceptFns) {
|
for (const acceptFn of acceptFns)
|
||||||
|
{
|
||||||
this._player = acceptFn(this);
|
this._player = acceptFn(this);
|
||||||
if (typeof this._player !== 'undefined')
|
if (typeof this._player !== 'undefined')
|
||||||
|
{
|
||||||
return this._player;
|
return this._player;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { origin: 'SoundPlayer._getPlayer', context: 'when finding a player for the sound', error: 'could not find an appropriate player.' };
|
throw {
|
||||||
|
origin: 'SoundPlayer._getPlayer',
|
||||||
|
context: 'when finding a player for the sound',
|
||||||
|
error: 'could not find an appropriate player.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Sound player interface
|
* Sound player interface
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +39,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
static accept(sound)
|
static accept(sound)
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.accept', context: 'when evaluating whether this player can play a given sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.accept',
|
||||||
|
context: 'when evaluating whether this player can play a given sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -54,7 +58,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
play(loops)
|
play(loops)
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.play', context: 'when starting the playback of a sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.play',
|
||||||
|
context: 'when starting the playback of a sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -68,7 +76,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
stop()
|
stop()
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.stop', context: 'when stopping the playback of a sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.stop',
|
||||||
|
context: 'when stopping the playback of a sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -82,7 +94,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
getDuration()
|
getDuration()
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.getDuration', context: 'when getting the duration of the sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.getDuration',
|
||||||
|
context: 'when getting the duration of the sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -96,11 +112,14 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
setDuration(duration_s)
|
setDuration(duration_s)
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.setDuration', context: 'when setting the duration of the sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.setDuration',
|
||||||
|
context: 'when setting the duration of the sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the number of loops.
|
* Set the number of loops.
|
||||||
*
|
*
|
||||||
@ -112,13 +131,17 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
setLoops(loops)
|
setLoops(loops)
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.setLoops', context: 'when setting the number of loops', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.setLoops',
|
||||||
|
context: 'when setting the number of loops',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the volume of the tone.
|
* Set the volume of the tone.
|
||||||
*
|
*
|
||||||
* @name module:sound.SoundPlayer#setVolume
|
* @name module:sound.SoundPlayer#setVolume
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -126,8 +149,13 @@ export class SoundPlayer extends PsychObject
|
|||||||
* @param {Integer} volume - the volume of the tone
|
* @param {Integer} volume - the volume of the tone
|
||||||
* @param {boolean} [mute= false] - whether or not to mute the tone
|
* @param {boolean} [mute= false] - whether or not to mute the tone
|
||||||
*/
|
*/
|
||||||
setVolume(volume, mute = false) {
|
setVolume(volume, mute = false)
|
||||||
throw {origin: 'SoundPlayer.setVolume', context: 'when setting the volume of the sound', error: 'this method is abstract and should not be called.'};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'SoundPlayer.setVolume',
|
||||||
|
context: 'when setting the volume of the sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,17 @@
|
|||||||
* Tone Player.
|
* Tone Player.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SoundPlayer } from './SoundPlayer';
|
import {SoundPlayer} from './SoundPlayer';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This class handles the playing of tones.</p>
|
* <p>This class handles the playing of tones.</p>
|
||||||
*
|
*
|
||||||
* @name module:sound.TonePlayer
|
* @name module:sound.TonePlayer
|
||||||
* @class
|
* @class
|
||||||
* @extends SoundPlayer
|
* @extends SoundPlayer
|
||||||
@ -26,14 +26,14 @@ import { SoundPlayer } from './SoundPlayer';
|
|||||||
export class TonePlayer extends SoundPlayer
|
export class TonePlayer extends SoundPlayer
|
||||||
{
|
{
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
note = 'C4',
|
note = 'C4',
|
||||||
duration_s = 0.5,
|
duration_s = 0.5,
|
||||||
volume = 1.0,
|
volume = 1.0,
|
||||||
loops = 0,
|
loops = 0,
|
||||||
soundLibrary = TonePlayer.SoundLibrary.TONE_JS,
|
soundLibrary = TonePlayer.SoundLibrary.TONE_JS,
|
||||||
autoLog = true
|
autoLog = true
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
@ -84,7 +84,8 @@ export class TonePlayer extends SoundPlayer
|
|||||||
{
|
{
|
||||||
// mapping between the PsychoPY notes and the standard ones:
|
// mapping between the PsychoPY notes and the standard ones:
|
||||||
let psychopyToToneMap = new Map();
|
let psychopyToToneMap = new Map();
|
||||||
for (const note of ['A', 'B', 'C', 'D', 'E', 'F', 'G']) {
|
for (const note of ['A', 'B', 'C', 'D', 'E', 'F', 'G'])
|
||||||
|
{
|
||||||
psychopyToToneMap.set(note, note);
|
psychopyToToneMap.set(note, note);
|
||||||
psychopyToToneMap.set(note + 'fl', note + 'b');
|
psychopyToToneMap.set(note + 'fl', note + 'b');
|
||||||
psychopyToToneMap.set(note + 'sh', note + '#');
|
psychopyToToneMap.set(note + 'sh', note + '#');
|
||||||
@ -123,7 +124,6 @@ export class TonePlayer extends SoundPlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the duration of the tone.
|
* Set the duration of the tone.
|
||||||
*
|
*
|
||||||
@ -138,7 +138,6 @@ export class TonePlayer extends SoundPlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the number of loops.
|
* Set the number of loops.
|
||||||
*
|
*
|
||||||
@ -155,7 +154,7 @@ export class TonePlayer extends SoundPlayer
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the volume of the tone.
|
* Set the volume of the tone.
|
||||||
*
|
*
|
||||||
* @name module:sound.TonePlayer#setVolume
|
* @name module:sound.TonePlayer#setVolume
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -197,7 +196,9 @@ export class TonePlayer extends SoundPlayer
|
|||||||
play(loops)
|
play(loops)
|
||||||
{
|
{
|
||||||
if (typeof loops !== 'undefined')
|
if (typeof loops !== 'undefined')
|
||||||
|
{
|
||||||
this._loops = loops;
|
this._loops = loops;
|
||||||
|
}
|
||||||
|
|
||||||
// if duration_s == -1, the sound should play indefinitely, therefore we use an arbitrarily long playing time
|
// if duration_s == -1, the sound should play indefinitely, therefore we use an arbitrarily long playing time
|
||||||
const actualDuration_s = (this._duration_s === -1) ? 1000000 : this._duration_s;
|
const actualDuration_s = (this._duration_s === -1) ? 1000000 : this._duration_s;
|
||||||
@ -250,7 +251,7 @@ export class TonePlayer extends SoundPlayer
|
|||||||
this.duration_s * (this._loops + 1)
|
this.duration_s * (this._loops + 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -269,7 +270,9 @@ export class TonePlayer extends SoundPlayer
|
|||||||
|
|
||||||
// clear the repeat event if need be:
|
// clear the repeat event if need be:
|
||||||
if (this._toneId)
|
if (this._toneId)
|
||||||
|
{
|
||||||
Tone.Transport.clear(this._toneId);
|
Tone.Transport.clear(this._toneId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -369,4 +372,4 @@ export class TonePlayer extends SoundPlayer
|
|||||||
TonePlayer.SoundLibrary = {
|
TonePlayer.SoundLibrary = {
|
||||||
AUDIO_CONTEXT: Symbol.for('AUDIO_CONTEXT'),
|
AUDIO_CONTEXT: Symbol.for('AUDIO_CONTEXT'),
|
||||||
TONE_JS: Symbol.for('TONE_JS')
|
TONE_JS: Symbol.for('TONE_JS')
|
||||||
};
|
};
|
||||||
|
@ -2,17 +2,17 @@
|
|||||||
* Track Player.
|
* Track Player.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SoundPlayer } from './SoundPlayer';
|
import {SoundPlayer} from './SoundPlayer';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This class handles the playback of sound tracks.</p>
|
* <p>This class handles the playback of sound tracks.</p>
|
||||||
*
|
*
|
||||||
* @name module:sound.TrackPlayer
|
* @name module:sound.TrackPlayer
|
||||||
* @class
|
* @class
|
||||||
* @extends SoundPlayer
|
* @extends SoundPlayer
|
||||||
@ -23,20 +23,22 @@ import { SoundPlayer } from './SoundPlayer';
|
|||||||
* @param {number} [options.stopTime= -1] - end of playback (in seconds)
|
* @param {number} [options.stopTime= -1] - end of playback (in seconds)
|
||||||
* @param {boolean} [options.stereo= true] whether or not to play the sound or track in stereo
|
* @param {boolean} [options.stereo= true] whether or not to play the sound or track in stereo
|
||||||
* @param {number} [options.volume= 1.0] - volume of the sound (must be between 0 and 1.0)
|
* @param {number} [options.volume= 1.0] - volume of the sound (must be between 0 and 1.0)
|
||||||
* @param {number} [options.loops= 0] - how many times to repeat the track or tone after it has played *
|
* @param {number} [options.loops= 0] - how many times to repeat the track or tone after it has played *
|
||||||
* @todo stopTime is currently not implemented (tracks will play from startTime to finish)
|
* @todo stopTime is currently not implemented (tracks will play from startTime to finish)
|
||||||
* @todo stereo is currently not implemented
|
* @todo stereo is currently not implemented
|
||||||
*/
|
*/
|
||||||
export class TrackPlayer extends SoundPlayer {
|
export class TrackPlayer extends SoundPlayer
|
||||||
|
{
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
howl,
|
howl,
|
||||||
startTime = 0,
|
startTime = 0,
|
||||||
stopTime = -1,
|
stopTime = -1,
|
||||||
stereo = true,
|
stereo = true,
|
||||||
volume = 0,
|
volume = 0,
|
||||||
loops = 0
|
loops = 0
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
this._addAttributes(TrackPlayer, howl, startTime, stopTime, stereo, loops, volume);
|
this._addAttributes(TrackPlayer, howl, startTime, stopTime, stereo, loops, volume);
|
||||||
@ -55,11 +57,14 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
* @param {module:sound.Sound} - the sound
|
* @param {module:sound.Sound} - the sound
|
||||||
* @return {Object|undefined} an instance of TrackPlayer that can play the given sound or undefined otherwise
|
* @return {Object|undefined} an instance of TrackPlayer that can play the given sound or undefined otherwise
|
||||||
*/
|
*/
|
||||||
static accept(sound) {
|
static accept(sound)
|
||||||
|
{
|
||||||
// if the sound's value is a string, we check whether it is the name of a resource:
|
// if the sound's value is a string, we check whether it is the name of a resource:
|
||||||
if (typeof sound.value === 'string') {
|
if (typeof sound.value === 'string')
|
||||||
|
{
|
||||||
const howl = sound.psychoJS.serverManager.getResource(sound.value);
|
const howl = sound.psychoJS.serverManager.getResource(sound.value);
|
||||||
if (typeof howl !== 'undefined') {
|
if (typeof howl !== 'undefined')
|
||||||
|
{
|
||||||
// build the player:
|
// build the player:
|
||||||
const player = new TrackPlayer({
|
const player = new TrackPlayer({
|
||||||
psychoJS: sound.psychoJS,
|
psychoJS: sound.psychoJS,
|
||||||
@ -95,16 +100,17 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the volume of the tone.
|
* Set the volume of the tone.
|
||||||
*
|
*
|
||||||
* @name module:sound.TrackPlayer#setVolume
|
* @name module:sound.TrackPlayer#setVolume
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {Integer} volume - the volume of the track (must be between 0 and 1.0)
|
* @param {Integer} volume - the volume of the track (must be between 0 and 1.0)
|
||||||
* @param {boolean} [mute= false] - whether or not to mute the track
|
* @param {boolean} [mute= false] - whether or not to mute the track
|
||||||
*/
|
*/
|
||||||
setVolume(volume, mute = false) {
|
setVolume(volume, mute = false)
|
||||||
|
{
|
||||||
this._volume = volume;
|
this._volume = volume;
|
||||||
|
|
||||||
this._howl.volume(volume);
|
this._howl.volume(volume);
|
||||||
this._howl.mute(mute);
|
this._howl.mute(mute);
|
||||||
}
|
}
|
||||||
@ -124,9 +130,13 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
this._currentLoopIndex = -1;
|
this._currentLoopIndex = -1;
|
||||||
|
|
||||||
if (loops === 0)
|
if (loops === 0)
|
||||||
|
{
|
||||||
this._howl.loop(false);
|
this._howl.loop(false);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
this._howl.loop(true);
|
this._howl.loop(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -138,18 +148,26 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
* @public
|
* @public
|
||||||
* @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.
|
* @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')
|
||||||
|
{
|
||||||
this.setLoops(loops);
|
this.setLoops(loops);
|
||||||
|
}
|
||||||
|
|
||||||
// handle repeats:
|
// handle repeats:
|
||||||
if (loops > 0) {
|
if (loops > 0)
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
this._howl.on('end', (event) => {
|
this._howl.on('end', (event) =>
|
||||||
|
{
|
||||||
++this._currentLoopIndex;
|
++this._currentLoopIndex;
|
||||||
if (self._currentLoopIndex > self._loops)
|
if (self._currentLoopIndex > self._loops)
|
||||||
|
{
|
||||||
self.stop();
|
self.stop();
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
self._howl.seek(self._startTime);
|
self._howl.seek(self._startTime);
|
||||||
self._howl.play();
|
self._howl.play();
|
||||||
}
|
}
|
||||||
@ -168,7 +186,8 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop()
|
||||||
|
{
|
||||||
this._howl.stop();
|
this._howl.stop();
|
||||||
this._howl.off('end');
|
this._howl.off('end');
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Clock component.
|
* Clock component.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -10,46 +10,50 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* <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>
|
* <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
|
* @name module:util.MonotonicClock
|
||||||
* @class
|
* @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)
|
* @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 {
|
export class MonotonicClock
|
||||||
constructor(startTime = MonotonicClock.getReferenceTime()) {
|
{
|
||||||
|
constructor(startTime = MonotonicClock.getReferenceTime())
|
||||||
|
{
|
||||||
this._timeAtLastReset = startTime;
|
this._timeAtLastReset = startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current time on this clock.
|
* Get the current time on this clock.
|
||||||
*
|
*
|
||||||
* @name module:util.MonotonicClock#getTime
|
* @name module:util.MonotonicClock#getTime
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {number} the current time (in seconds)
|
* @return {number} the current time (in seconds)
|
||||||
*/
|
*/
|
||||||
getTime() {
|
getTime()
|
||||||
|
{
|
||||||
return MonotonicClock.getReferenceTime() - this._timeAtLastReset;
|
return MonotonicClock.getReferenceTime() - this._timeAtLastReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current offset being applied to the high resolution timebase used by this Clock.
|
* Get the current offset being applied to the high resolution timebase used by this Clock.
|
||||||
*
|
*
|
||||||
* @name module:util.MonotonicClock#getLastResetTime
|
* @name module:util.MonotonicClock#getLastResetTime
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {number} the offset (in seconds)
|
* @return {number} the offset (in seconds)
|
||||||
*/
|
*/
|
||||||
getLastResetTime() {
|
getLastResetTime()
|
||||||
|
{
|
||||||
return this._timeAtLastReset;
|
return this._timeAtLastReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the time elapsed since the reference point.
|
* Get the time elapsed since the reference point.
|
||||||
*
|
*
|
||||||
* @name module:util.MonotonicClock#getReferenceTime
|
* @name module:util.MonotonicClock#getReferenceTime
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -64,9 +68,9 @@ export class MonotonicClock {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the clock's current time as a formatted string.
|
* 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>
|
* <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
|
* @name module:util.MonotonicClock.getDateStr
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -83,54 +87,59 @@ export class MonotonicClock {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The clock's referenceTime is the time when the module was loaded (in seconds).
|
* The clock's referenceTime is the time when the module was loaded (in seconds).
|
||||||
*
|
*
|
||||||
* @name module:util.MonotonicClock._referenceTime
|
* @name module:util.MonotonicClock._referenceTime
|
||||||
* @readonly
|
* @readonly
|
||||||
* @private
|
* @private
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
MonotonicClock._referenceTime = performance.now() / 1000.0;
|
MonotonicClock._referenceTime = performance.now() / 1000.0;
|
||||||
|
|
||||||
// MonotonicClock._referenceTime = new Date().getTime() / 1000.0;
|
// MonotonicClock._referenceTime = new Date().getTime() / 1000.0;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Clock is a MonotonicClock that also offers the possibility of being reset.</p>
|
* <p>Clock is a MonotonicClock that also offers the possibility of being reset.</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Clock
|
* @name module:util.Clock
|
||||||
* @class
|
* @class
|
||||||
* @extends MonotonicClock
|
* @extends MonotonicClock
|
||||||
*/
|
*/
|
||||||
export class Clock extends MonotonicClock {
|
export class Clock extends MonotonicClock
|
||||||
constructor() {
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the time on the clock.
|
* Reset the time on the clock.
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @name module:util.Clock#reset
|
* @name module:util.Clock#reset
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {number} [newTime= 0] the new time on the clock.
|
* @param {number} [newTime= 0] the new time on the clock.
|
||||||
*/
|
*/
|
||||||
reset(newTime = 0) {
|
reset(newTime = 0)
|
||||||
|
{
|
||||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add more time to the clock's 'start' time (t0).
|
* Add more time to the clock's 'start' time (t0).
|
||||||
*
|
*
|
||||||
* <p>Note: by adding time to t0, the current time is pushed forward (it becomes
|
* <p>Note: by adding time to t0, the current time is pushed forward (it becomes
|
||||||
* smaller). As a consequence, getTime() may return a negative number.</p>
|
* smaller). As a consequence, getTime() may return a negative number.</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Clock#add
|
* @name module:util.Clock#add
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||||
*/
|
*/
|
||||||
add(deltaTime) {
|
add(deltaTime)
|
||||||
|
{
|
||||||
this._timeAtLastReset += deltaTime;
|
this._timeAtLastReset += deltaTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,19 +147,22 @@ export class Clock extends MonotonicClock {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>CountdownTimer is a clock counts down from the time of last reset.</p.
|
* <p>CountdownTimer is a clock counts down from the time of last reset.</p.
|
||||||
*
|
*
|
||||||
* @name module:util.CountdownTimer
|
* @name module:util.CountdownTimer
|
||||||
* @class
|
* @class
|
||||||
* @extends Clock
|
* @extends Clock
|
||||||
* @param {number} [startTime= 0] - the start time of the countdown
|
* @param {number} [startTime= 0] - the start time of the countdown
|
||||||
*/
|
*/
|
||||||
export class CountdownTimer extends Clock {
|
export class CountdownTimer extends Clock
|
||||||
constructor(startTime = 0) {
|
{
|
||||||
|
constructor(startTime = 0)
|
||||||
|
{
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._timeAtLastReset = MonotonicClock.getReferenceTime();
|
this._timeAtLastReset = MonotonicClock.getReferenceTime();
|
||||||
this._countdown_duration = startTime;
|
this._countdown_duration = startTime;
|
||||||
if (startTime) {
|
if (startTime)
|
||||||
|
{
|
||||||
this.add(startTime);
|
this.add(startTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,34 +170,38 @@ export class CountdownTimer extends Clock {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add more time to the clock's 'start' time (t0).
|
* 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
|
* <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>
|
* smaller). As a consequence, getTime() may return a negative number.</p>
|
||||||
*
|
*
|
||||||
* @name module:util.CountdownTimer#add
|
* @name module:util.CountdownTimer#add
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||||
*/
|
*/
|
||||||
add(deltaTime) {
|
add(deltaTime)
|
||||||
|
{
|
||||||
this._timeAtLastReset += deltaTime;
|
this._timeAtLastReset += deltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the time on the countdown.
|
* Reset the time on the countdown.
|
||||||
*
|
*
|
||||||
* @name module:util.CountdownTimer#reset
|
* @name module:util.CountdownTimer#reset
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {number} [newTime] - if newTime is undefined, the countdown time is reset to zero, otherwise we set it
|
* @param {number} [newTime] - if newTime is undefined, the countdown time is reset to zero, otherwise we set it
|
||||||
* to newTime
|
* to newTime
|
||||||
*/
|
*/
|
||||||
reset(newTime = undefined) {
|
reset(newTime = undefined)
|
||||||
if (typeof newTime == 'undefined') {
|
{
|
||||||
|
if (typeof newTime == 'undefined')
|
||||||
|
{
|
||||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + this._countdown_duration;
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + this._countdown_duration;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
this._countdown_duration = newTime;
|
this._countdown_duration = newTime;
|
||||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
||||||
}
|
}
|
||||||
@ -194,13 +210,14 @@ export class CountdownTimer extends Clock {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the time currently left on the countdown.
|
* Get the time currently left on the countdown.
|
||||||
*
|
*
|
||||||
* @name module:util.CountdownTimer#getTime
|
* @name module:util.CountdownTimer#getTime
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {number} the time left on the countdown (in seconds)
|
* @return {number} the time left on the countdown (in seconds)
|
||||||
*/
|
*/
|
||||||
getTime() {
|
getTime()
|
||||||
|
{
|
||||||
return this._timeAtLastReset - MonotonicClock.getReferenceTime();
|
return this._timeAtLastReset - MonotonicClock.getReferenceTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
237
js/util/Color.js
237
js/util/Color.js
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Color management.
|
* Color management.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -11,7 +11,7 @@
|
|||||||
/**
|
/**
|
||||||
* <p>This class handles multiple color spaces, and offers various
|
* <p>This class handles multiple color spaces, and offers various
|
||||||
* static methods for converting colors from one space to another.</p>
|
* static methods for converting colors from one space to another.</p>
|
||||||
*
|
*
|
||||||
* <p>The constructor accepts the following color representations:
|
* <p>The constructor accepts the following color representations:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>a named color, e.g. 'aliceblue' (the colorspace must be RGB)</li>
|
* <li>a named color, e.g. 'aliceblue' (the colorspace must be RGB)</li>
|
||||||
@ -20,37 +20,48 @@
|
|||||||
* <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>
|
* <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>
|
* </ul>
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <p>Note: internally, colors are represented as a [r,g,b] triplet with r,g,b in [0,1].</p>
|
* <p>Note: internally, colors are represented as a [r,g,b] triplet with r,g,b in [0,1].</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Color
|
* @name module:util.Color
|
||||||
* @class
|
* @class
|
||||||
* @param {string|number|Array.<number>|undefined} [obj= 'black'] - an object representing a color
|
* @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
|
* @param {module:util.Color#COLOR_SPACE|undefined} [colorspace=Color.COLOR_SPACE.RGB] - the colorspace of that color
|
||||||
*
|
*
|
||||||
* @todo implement HSV, DKL, and LMS colorspaces
|
* @todo implement HSV, DKL, and LMS colorspaces
|
||||||
*/
|
*/
|
||||||
export class Color {
|
export class Color
|
||||||
|
{
|
||||||
|
|
||||||
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB) {
|
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB)
|
||||||
const response = { origin: 'Color', context: 'when defining a color' };
|
{
|
||||||
|
const response = {origin: 'Color', context: 'when defining a color'};
|
||||||
|
|
||||||
// named color (e.g. 'seagreen') or string hexadecimal representation (e.g. '#FF0000'):
|
// named color (e.g. 'seagreen') or string hexadecimal representation (e.g. '#FF0000'):
|
||||||
// note: we expect the color space to be RGB
|
// note: we expect the color space to be RGB
|
||||||
if (typeof obj == 'string') {
|
if (typeof obj == 'string')
|
||||||
|
{
|
||||||
if (colorspace !== Color.COLOR_SPACE.RGB)
|
if (colorspace !== Color.COLOR_SPACE.RGB)
|
||||||
throw Object.assign(response, { error: 'the colorspace must be RGB for' +
|
{
|
||||||
|
throw Object.assign(response, {
|
||||||
|
error: 'the colorspace must be RGB for' +
|
||||||
' a' +
|
' a' +
|
||||||
' named color' });
|
' named color'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// hexademical representation:
|
// hexademical representation:
|
||||||
if (obj[0] === '#') {
|
if (obj[0] === '#')
|
||||||
|
{
|
||||||
this._hex = obj;
|
this._hex = obj;
|
||||||
}
|
}
|
||||||
// named color:
|
// named color:
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
if (!(obj.toLowerCase() in Color.NAMED_COLORS))
|
if (!(obj.toLowerCase() in Color.NAMED_COLORS))
|
||||||
throw Object.assign(response, { error: 'unknown named color: ' + obj });
|
{
|
||||||
|
throw Object.assign(response, {error: 'unknown named color: ' + obj});
|
||||||
|
}
|
||||||
|
|
||||||
this._hex = Color.NAMED_COLORS[obj.toLowerCase()];
|
this._hex = Color.NAMED_COLORS[obj.toLowerCase()];
|
||||||
}
|
}
|
||||||
@ -60,22 +71,29 @@ export class Color {
|
|||||||
|
|
||||||
// hexadecimal number representation (e.g. 0xFF0000)
|
// hexadecimal number representation (e.g. 0xFF0000)
|
||||||
// note: we expect the color space to be RGB
|
// note: we expect the color space to be RGB
|
||||||
else if (typeof obj == 'number') {
|
else if (typeof obj == 'number')
|
||||||
|
{
|
||||||
if (colorspace !== Color.COLOR_SPACE.RGB)
|
if (colorspace !== Color.COLOR_SPACE.RGB)
|
||||||
throw Object.assign(response, { error: 'the colorspace must be RGB for' +
|
{
|
||||||
|
throw Object.assign(response, {
|
||||||
|
error: 'the colorspace must be RGB for' +
|
||||||
' a' +
|
' a' +
|
||||||
' named color' });
|
' named color'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this._rgb = Color._intToRgb(obj);
|
this._rgb = Color._intToRgb(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// array of numbers:
|
// array of numbers:
|
||||||
else if (Array.isArray(obj)) {
|
else if (Array.isArray(obj))
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(obj);
|
Color._checkTypeAndRange(obj);
|
||||||
let [a, b, c] = obj;
|
let [a, b, c] = obj;
|
||||||
|
|
||||||
// check range and convert to [0,1]:
|
// check range and convert to [0,1]:
|
||||||
if (colorspace !== Color.COLOR_SPACE.RGB255) {
|
if (colorspace !== Color.COLOR_SPACE.RGB255)
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(obj, [-1, 1]);
|
Color._checkTypeAndRange(obj, [-1, 1]);
|
||||||
|
|
||||||
a = (a + 1.0) / 2.0;
|
a = (a + 1.0) / 2.0;
|
||||||
@ -84,7 +102,8 @@ export class Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get RGB components:
|
// get RGB components:
|
||||||
switch (colorspace) {
|
switch (colorspace)
|
||||||
|
{
|
||||||
case Color.COLOR_SPACE.RGB255:
|
case Color.COLOR_SPACE.RGB255:
|
||||||
Color._checkTypeAndRange(obj, [0, 255]);
|
Color._checkTypeAndRange(obj, [0, 255]);
|
||||||
this._rgb = [a / 255.0, b / 255.0, c / 255.0];
|
this._rgb = [a / 255.0, b / 255.0, c / 255.0];
|
||||||
@ -104,7 +123,7 @@ export class Color {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unknown colorspace: ' + colorspace });
|
throw Object.assign(response, {error: 'unknown colorspace: ' + colorspace});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -113,51 +132,63 @@ export class Color {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [0,1] RGB triplet equivalent of this Color.
|
* Get the [0,1] RGB triplet equivalent of this Color.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.rgb
|
* @name module:util.Color.rgb
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
||||||
*/
|
*/
|
||||||
get rgb() { return this._rgb; }
|
get rgb()
|
||||||
|
{
|
||||||
|
return this._rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [0,255] RGB triplet equivalent of this Color.
|
* Get the [0,255] RGB triplet equivalent of this Color.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.rgb255
|
* @name module:util.Color.rgb255
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
* @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 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.
|
* Get the hexadecimal color code equivalent of this Color.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.hex
|
* @name module:util.Color.hex
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
get hex() {
|
get hex()
|
||||||
|
{
|
||||||
if (typeof this._hex === 'undefined')
|
if (typeof this._hex === 'undefined')
|
||||||
|
{
|
||||||
this._hex = Color._rgbToHex(this._rgb);
|
this._hex = Color._rgbToHex(this._rgb);
|
||||||
|
}
|
||||||
return this._hex;
|
return this._hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the integer code equivalent of this Color.
|
* Get the integer code equivalent of this Color.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.int
|
* @name module:util.Color.int
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @return {number} the integer code equivalent
|
* @return {number} the integer code equivalent
|
||||||
*/
|
*/
|
||||||
get int() {
|
get int()
|
||||||
|
{
|
||||||
if (typeof this._int === 'undefined')
|
if (typeof this._int === 'undefined')
|
||||||
|
{
|
||||||
this._int = Color._rgbToInt(this._rgb);
|
this._int = Color._rgbToInt(this._rgb);
|
||||||
|
}
|
||||||
return this._int;
|
return this._int;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +228,7 @@ export class Color {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [0,255] RGB triplet equivalent of the hexadecimal color code.
|
* Get the [0,255] RGB triplet equivalent of the hexadecimal color code.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.hexToRgb255
|
* @name module:util.Color.hexToRgb255
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -205,10 +236,17 @@ export class Color {
|
|||||||
* @param {string} hex - the hexadecimal color code
|
* @param {string} hex - the hexadecimal color code
|
||||||
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
||||||
*/
|
*/
|
||||||
static hexToRgb255(hex) {
|
static hexToRgb255(hex)
|
||||||
|
{
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
if (result == null)
|
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' };
|
{
|
||||||
|
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)];
|
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
|
||||||
}
|
}
|
||||||
@ -216,7 +254,7 @@ export class Color {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [0,1] RGB triplet equivalent of the hexadecimal color code.
|
* Get the [0,1] RGB triplet equivalent of the hexadecimal color code.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.hexToRgb
|
* @name module:util.Color.hexToRgb
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -224,7 +262,8 @@ export class Color {
|
|||||||
* @param {string} hex - the hexadecimal color code
|
* @param {string} hex - the hexadecimal color code
|
||||||
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
||||||
*/
|
*/
|
||||||
static hexToRgb(hex) {
|
static hexToRgb(hex)
|
||||||
|
{
|
||||||
const [r255, g255, b255] = Color.hexToRgb255(hex);
|
const [r255, g255, b255] = Color.hexToRgb255(hex);
|
||||||
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
||||||
}
|
}
|
||||||
@ -232,7 +271,7 @@ export class Color {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
|
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.rgb255ToHex
|
* @name module:util.Color.rgb255ToHex
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -240,22 +279,28 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
static rgb255ToHex(rgb255) {
|
static rgb255ToHex(rgb255)
|
||||||
const response = { origin : 'Color.rgb255ToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'Color.rgb255ToHex',
|
||||||
|
context: 'when converting an rgb triplet to its hexadecimal color representation'
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(rgb255, [0, 255]);
|
Color._checkTypeAndRange(rgb255, [0, 255]);
|
||||||
return Color._rgb255ToHex(rgb255);
|
return Color._rgb255ToHex(rgb255);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
|
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.rgbToHex
|
* @name module:util.Color.rgbToHex
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -263,22 +308,28 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
static rgbToHex(rgb) {
|
static rgbToHex(rgb)
|
||||||
const response = { origin : 'Color.rgbToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'Color.rgbToHex',
|
||||||
|
context: 'when converting an rgb triplet to its hexadecimal color representation'
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(rgb, [0, 1]);
|
Color._checkTypeAndRange(rgb, [0, 1]);
|
||||||
return Color._rgbToHex(rgb);
|
return Color._rgbToHex(rgb);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the integer equivalent of the [0, 1] RGB triplet.
|
* Get the integer equivalent of the [0, 1] RGB triplet.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.rgbToInt
|
* @name module:util.Color.rgbToInt
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -286,22 +337,28 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
* @return {number} the integer equivalent
|
* @return {number} the integer equivalent
|
||||||
*/
|
*/
|
||||||
static rgbToInt(rgb) {
|
static rgbToInt(rgb)
|
||||||
const response = { origin : 'Color.rgbToInt', context: 'when converting an rgb triplet to its integer representation' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'Color.rgbToInt',
|
||||||
|
context: 'when converting an rgb triplet to its integer representation'
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(rgb, [0, 1]);
|
Color._checkTypeAndRange(rgb, [0, 1]);
|
||||||
return Color._rgbToInt(rgb);
|
return Color._rgbToInt(rgb);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the integer equivalent of the [0, 255] RGB triplet.
|
* Get the integer equivalent of the [0, 255] RGB triplet.
|
||||||
*
|
*
|
||||||
* @name module:util.Color.rgb255ToInt
|
* @name module:util.Color.rgb255ToInt
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -309,23 +366,29 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
* @return {number} the integer equivalent
|
* @return {number} the integer equivalent
|
||||||
*/
|
*/
|
||||||
static rgb255ToInt(rgb255) {
|
static rgb255ToInt(rgb255)
|
||||||
const response = { origin : 'Color.rgb255ToInt', context: 'when converting an rgb triplet to its integer representation' };
|
{
|
||||||
try {
|
const response = {
|
||||||
|
origin: 'Color.rgb255ToInt',
|
||||||
|
context: 'when converting an rgb triplet to its integer representation'
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(rgb255, [0, 255]);
|
Color._checkTypeAndRange(rgb255, [0, 255]);
|
||||||
return Color._rgb255ToInt(rgb255);
|
return Color._rgb255ToInt(rgb255);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
|
* 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>
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Color._rgb255ToHex
|
* @name module:util.Color._rgb255ToHex
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -333,16 +396,17 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
static _rgb255ToHex(rgb255) {
|
static _rgb255ToHex(rgb255)
|
||||||
|
{
|
||||||
return "#" + ((1 << 24) + (rgb255[0] << 16) + (rgb255[1] << 8) + rgb255[2]).toString(16).slice(1);
|
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.
|
* 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>
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Color._rgbToHex
|
* @name module:util.Color._rgbToHex
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -350,7 +414,8 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
static _rgbToHex(rgb) {
|
static _rgbToHex(rgb)
|
||||||
|
{
|
||||||
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
||||||
return Color._rgb255ToHex(rgb255);
|
return Color._rgb255ToHex(rgb255);
|
||||||
}
|
}
|
||||||
@ -358,9 +423,9 @@ export class Color {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the integer equivalent of the [0, 1] RGB triplet.
|
* 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>
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Color._rgbToInt
|
* @name module:util.Color._rgbToInt
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -368,7 +433,8 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
* @return {number} the integer equivalent
|
* @return {number} the integer equivalent
|
||||||
*/
|
*/
|
||||||
static _rgbToInt(rgb) {
|
static _rgbToInt(rgb)
|
||||||
|
{
|
||||||
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
||||||
return Color._rgb255ToInt(rgb255);
|
return Color._rgb255ToInt(rgb255);
|
||||||
}
|
}
|
||||||
@ -376,9 +442,9 @@ export class Color {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the integer equivalent of the [0, 255] RGB triplet.
|
* 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>
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Color._rgb255ToInt
|
* @name module:util.Color._rgb255ToInt
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -386,16 +452,17 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
* @return {number} the integer equivalent
|
* @return {number} the integer equivalent
|
||||||
*/
|
*/
|
||||||
static _rgb255ToInt(rgb255) {
|
static _rgb255ToInt(rgb255)
|
||||||
|
{
|
||||||
return rgb255[0] * 0x10000 + rgb255[1] * 0x100 + rgb255[2];
|
return rgb255[0] * 0x10000 + rgb255[1] * 0x100 + rgb255[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [0, 255] based RGB triplet equivalent of the integer color code.
|
* 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>
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Color._intToRgb255
|
* @name module:util.Color._intToRgb255
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -403,7 +470,8 @@ export class Color {
|
|||||||
* @param {number} hex - the integer color code
|
* @param {number} hex - the integer color code
|
||||||
* @return {Array.<number>} the [0, 255] RGB equivalent
|
* @return {Array.<number>} the [0, 255] RGB equivalent
|
||||||
*/
|
*/
|
||||||
static _intToRgb255(hex) {
|
static _intToRgb255(hex)
|
||||||
|
{
|
||||||
const r255 = hex >>> 0x10;
|
const r255 = hex >>> 0x10;
|
||||||
const g255 = (hex & 0xFF00) / 0x100;
|
const g255 = (hex & 0xFF00) / 0x100;
|
||||||
const b255 = hex & 0xFF;
|
const b255 = hex & 0xFF;
|
||||||
@ -414,9 +482,9 @@ export class Color {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [0, 1] based RGB triplet equivalent of the integer color code.
|
* 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>
|
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Color._intToRgb
|
* @name module:util.Color._intToRgb
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -424,7 +492,8 @@ export class Color {
|
|||||||
* @param {number} hex - the integer color code
|
* @param {number} hex - the integer color code
|
||||||
* @return {Array.<number>} the [0, 1] RGB equivalent
|
* @return {Array.<number>} the [0, 1] RGB equivalent
|
||||||
*/
|
*/
|
||||||
static _intToRgb(hex) {
|
static _intToRgb(hex)
|
||||||
|
{
|
||||||
const [r255, g255, b255] = Color._intToRgb255(hex);
|
const [r255, g255, b255] = Color._intToRgb255(hex);
|
||||||
|
|
||||||
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
||||||
@ -432,7 +501,7 @@ export class Color {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that the argument is an array of numbers of size 3, and, potentially, that its elements fall within the range.
|
* 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
|
* @name module:util.Color._checkTypeAndRange
|
||||||
* @function
|
* @function
|
||||||
* @static
|
* @static
|
||||||
@ -443,21 +512,23 @@ export class Color {
|
|||||||
*/
|
*/
|
||||||
static _checkTypeAndRange(arg, range = undefined)
|
static _checkTypeAndRange(arg, range = undefined)
|
||||||
{
|
{
|
||||||
if (!Array.isArray(arg) || arg.length !== 3 ||
|
if (!Array.isArray(arg) || arg.length !== 3 ||
|
||||||
typeof arg[0] !== 'number' || typeof arg[1] !== 'number' || typeof arg[2] !== 'number')
|
typeof arg[0] !== 'number' || typeof arg[1] !== 'number' || typeof arg[2] !== 'number')
|
||||||
{
|
{
|
||||||
throw 'the argument should be an array of numbers of length 3';
|
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]))
|
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] + ']';
|
throw 'the color components should all belong to [' + range[0] + ', ' + range[1] + ']';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Color spaces.
|
* Color spaces.
|
||||||
*
|
*
|
||||||
* @name module:util.Color#COLOR_SPACE
|
* @name module:util.Color#COLOR_SPACE
|
||||||
* @enum {Symbol}
|
* @enum {Symbol}
|
||||||
* @readonly
|
* @readonly
|
||||||
@ -484,7 +555,7 @@ Color.COLOR_SPACE = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Named colors.
|
* Named colors.
|
||||||
*
|
*
|
||||||
* @name module:util.Color#NAMED_COLORS
|
* @name module:util.Color#NAMED_COLORS
|
||||||
* @enum {Symbol}
|
* @enum {Symbol}
|
||||||
* @readonly
|
* @readonly
|
||||||
|
@ -2,37 +2,40 @@
|
|||||||
* Color Mixin.
|
* Color Mixin.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { Color } from './Color';
|
import {Color} from './Color';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This mixin implement color and contrast changes for visual stimuli</p>
|
* <p>This mixin implement color and contrast changes for visual stimuli</p>
|
||||||
*
|
*
|
||||||
* @name module:util.ColorMixin
|
* @name module:util.ColorMixin
|
||||||
* @mixin
|
* @mixin
|
||||||
*/
|
*/
|
||||||
export let ColorMixin = (superclass) => class extends superclass {
|
export let ColorMixin = (superclass) => class extends superclass
|
||||||
constructor(args) {
|
{
|
||||||
|
constructor(args)
|
||||||
|
{
|
||||||
super(args);
|
super(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for Color attribute.
|
* Setter for Color attribute.
|
||||||
*
|
*
|
||||||
* @name module:util.ColorMixin#setColor
|
* @name module:util.ColorMixin#setColor
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {Color} color - the new color
|
* @param {Color} color - the new color
|
||||||
* @param {boolean} [log= false] - whether or not to log
|
* @param {boolean} [log= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
setColor(color, log) {
|
setColor(color, log)
|
||||||
|
{
|
||||||
this._setAttribute('color', color, log);
|
this._setAttribute('color', color, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -41,14 +44,15 @@ export let ColorMixin = (superclass) => class extends superclass {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for Contrast attribute.
|
* Setter for Contrast attribute.
|
||||||
*
|
*
|
||||||
* @name module:util.ColorMixin#setContrast
|
* @name module:util.ColorMixin#setContrast
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {number} contrast - the new contrast (must be between 0 and 1)
|
* @param {number} contrast - the new contrast (must be between 0 and 1)
|
||||||
* @param {boolean} [log= false] - whether or not to log
|
* @param {boolean} [log= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
setContrast(contrast, log) {
|
setContrast(contrast, log)
|
||||||
|
{
|
||||||
this._setAttribute('contrast', contrast, log);
|
this._setAttribute('contrast', contrast, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -57,14 +61,15 @@ export let ColorMixin = (superclass) => class extends superclass {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjust the contrast of the color and convert it to [-1, 1] RGB
|
* Adjust the contrast of the color and convert it to [-1, 1] RGB
|
||||||
*
|
*
|
||||||
* @name module:util.ColorMixin#getContrastedColor
|
* @name module:util.ColorMixin#getContrastedColor
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param {string|number|Array.<number>} color - the color
|
* @param {string|number|Array.<number>} color - the color
|
||||||
* @param {number} contrast - the contrast (must be between 0 and 1)
|
* @param {number} contrast - the contrast (must be between 0 and 1)
|
||||||
*/
|
*/
|
||||||
getContrastedColor(color, contrast) {
|
getContrastedColor(color, contrast)
|
||||||
|
{
|
||||||
const rgb = color.rgb.map(c => (c * 2.0 - 1.0) * contrast);
|
const rgb = color.rgb.map(c => (c * 2.0 - 1.0) * contrast);
|
||||||
return new Color(rgb, Color.COLOR_SPACE.RGB);
|
return new Color(rgb, Color.COLOR_SPACE.RGB);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Event Emitter.
|
* Event Emitter.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -18,7 +18,7 @@ import * as util from './Util';
|
|||||||
*
|
*
|
||||||
* @name module:util.EventEmitter
|
* @name module:util.EventEmitter
|
||||||
* @class
|
* @class
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* let observable = new EventEmitter();
|
* let observable = new EventEmitter();
|
||||||
* let uuid1 = observable.on('change', data => { console.log(data); });
|
* let uuid1 = observable.on('change', data => { console.log(data); });
|
||||||
@ -28,7 +28,7 @@ import * as util from './Util';
|
|||||||
*/
|
*/
|
||||||
export class EventEmitter
|
export class EventEmitter
|
||||||
{
|
{
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
this._listeners = new Map();
|
this._listeners = new Map();
|
||||||
this._onceUuids = new Map();
|
this._onceUuids = new Map();
|
||||||
@ -37,7 +37,7 @@ export class EventEmitter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener called when this instance emits an event for which it is registered.
|
* Listener called when this instance emits an event for which it is registered.
|
||||||
*
|
*
|
||||||
* @callback module:util.EventEmitter~Listener
|
* @callback module:util.EventEmitter~Listener
|
||||||
* @param {object} data - the data passed to the listener
|
* @param {object} data - the data passed to the listener
|
||||||
*/
|
*/
|
||||||
@ -45,7 +45,7 @@ export class EventEmitter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new listener for events with the given name emitted by this instance.
|
* Register a new listener for events with the given name emitted by this instance.
|
||||||
*
|
*
|
||||||
* @name module:util.EventEmitter#on
|
* @name module:util.EventEmitter#on
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -57,14 +57,18 @@ export class EventEmitter
|
|||||||
{
|
{
|
||||||
// check that the listener is a function:
|
// check that the listener is a function:
|
||||||
if (typeof listener !== 'function')
|
if (typeof listener !== 'function')
|
||||||
|
{
|
||||||
throw new TypeError('listener must be a function');
|
throw new TypeError('listener must be a function');
|
||||||
|
}
|
||||||
|
|
||||||
// generate a new uuid:
|
// generate a new uuid:
|
||||||
let uuid = util.makeUuid();
|
let uuid = util.makeUuid();
|
||||||
|
|
||||||
// add the listener to the event map:
|
// add the listener to the event map:
|
||||||
if (!this._listeners.has(name))
|
if (!this._listeners.has(name))
|
||||||
|
{
|
||||||
this._listeners.set(name, []);
|
this._listeners.set(name, []);
|
||||||
|
}
|
||||||
this._listeners.get(name).push({uuid, listener});
|
this._listeners.get(name).push({uuid, listener});
|
||||||
|
|
||||||
return uuid;
|
return uuid;
|
||||||
@ -73,7 +77,7 @@ export class EventEmitter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new listener for the given event name, and remove it as soon as the event has been emitted.
|
* 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
|
* @name module:util.EventEmitter#once
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -86,7 +90,9 @@ export class EventEmitter
|
|||||||
let uuid = this.on(name, listener);
|
let uuid = this.on(name, listener);
|
||||||
|
|
||||||
if (!this._onceUuids.has(name))
|
if (!this._onceUuids.has(name))
|
||||||
|
{
|
||||||
this._onceUuids.set(name, []);
|
this._onceUuids.set(name, []);
|
||||||
|
}
|
||||||
this._onceUuids.get(name).push(uuid);
|
this._onceUuids.get(name).push(uuid);
|
||||||
|
|
||||||
return uuid;
|
return uuid;
|
||||||
@ -95,7 +101,7 @@ export class EventEmitter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the listener with the given uuid associated to the given event name.
|
* Remove the listener with the given uuid associated to the given event name.
|
||||||
*
|
*
|
||||||
* @name module:util.EventEmitter#off
|
* @name module:util.EventEmitter#off
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
@ -106,8 +112,9 @@ export class EventEmitter
|
|||||||
{
|
{
|
||||||
let relevantUuidListeners = this._listeners.get(name);
|
let relevantUuidListeners = this._listeners.get(name);
|
||||||
|
|
||||||
if (relevantUuidListeners && relevantUuidListeners.length) {
|
if (relevantUuidListeners && relevantUuidListeners.length)
|
||||||
this._listeners.set(name, relevantUuidListeners.filter( uuidlistener => (uuidlistener.uuid != uuid) ) );
|
{
|
||||||
|
this._listeners.set(name, relevantUuidListeners.filter(uuidlistener => (uuidlistener.uuid != uuid)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -131,11 +138,14 @@ export class EventEmitter
|
|||||||
{
|
{
|
||||||
let onceUuids = this._onceUuids.get(name);
|
let onceUuids = this._onceUuids.get(name);
|
||||||
let self = this;
|
let self = this;
|
||||||
relevantUuidListeners.forEach( ({uuid, listener}) => {
|
relevantUuidListeners.forEach(({uuid, listener}) =>
|
||||||
|
{
|
||||||
listener(data);
|
listener(data);
|
||||||
|
|
||||||
if (typeof onceUuids !== 'undefined' && onceUuids.includes(uuid))
|
if (typeof onceUuids !== 'undefined' && onceUuids.includes(uuid))
|
||||||
|
{
|
||||||
self.off(name, uuid);
|
self.off(name, uuid);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -144,4 +154,4 @@ export class EventEmitter
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,20 +3,20 @@
|
|||||||
* Core Object.
|
* Core Object.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { EventEmitter } from './EventEmitter';
|
import {EventEmitter} from './EventEmitter';
|
||||||
import * as util from './Util';
|
import * as util from './Util';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>PsychoObject is the base class for all PsychoJS objects.
|
* <p>PsychoObject is the base class for all PsychoJS objects.
|
||||||
* It is responsible for handling attributes.</p>
|
* It is responsible for handling attributes.</p>
|
||||||
*
|
*
|
||||||
* @class
|
* @class
|
||||||
* @extends EventEmitter
|
* @extends EventEmitter
|
||||||
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
||||||
@ -33,14 +33,16 @@ export class PsychObject extends EventEmitter
|
|||||||
|
|
||||||
// name:
|
// name:
|
||||||
if (typeof name === 'undefined')
|
if (typeof name === 'undefined')
|
||||||
|
{
|
||||||
name = this.constructor.name;
|
name = this.constructor.name;
|
||||||
|
}
|
||||||
this._addAttribute('name', name);
|
this._addAttribute('name', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the PsychoJS instance.
|
* Get the PsychoJS instance.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @return {PsychoJS} the PsychoJS instance
|
* @return {PsychoJS} the PsychoJS instance
|
||||||
*/
|
*/
|
||||||
@ -52,7 +54,7 @@ export class PsychObject extends EventEmitter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the PsychoJS attribute.
|
* Setter for the PsychoJS attribute.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
||||||
*/
|
*/
|
||||||
@ -77,17 +79,23 @@ export class PsychObject extends EventEmitter
|
|||||||
for (const attribute of this._userAttributes)
|
for (const attribute of this._userAttributes)
|
||||||
{
|
{
|
||||||
if (addComma)
|
if (addComma)
|
||||||
|
{
|
||||||
representation += ', ';
|
representation += ', ';
|
||||||
|
}
|
||||||
addComma = true;
|
addComma = true;
|
||||||
|
|
||||||
let value = util.toString(this['_'+attribute]);
|
let value = util.toString(this['_' + attribute]);
|
||||||
const l = value.length;
|
const l = value.length;
|
||||||
if (l > 50)
|
if (l > 50)
|
||||||
{
|
{
|
||||||
if (value[l-1] === ')')
|
if (value[l - 1] === ')')
|
||||||
|
{
|
||||||
value = value.substring(0, 50) + '~)';
|
value = value.substring(0, 50) + '~)';
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
value = value.substring(0, 50) + '~';
|
value = value.substring(0, 50) + '~';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
representation += attribute + '=' + value;
|
representation += attribute + '=' + value;
|
||||||
@ -98,47 +106,60 @@ export class PsychObject extends EventEmitter
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value of an attribute.
|
* Set the value of an attribute.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {string} attributeName - the name of the attribute
|
* @param {string} attributeName - the name of the attribute
|
||||||
* @param {object} attributeValue - the value of the attribute
|
* @param {object} attributeValue - the value of the attribute
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @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 {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
|
* @param {boolean} [stealth= false] - whether or not to call the potential attribute setters when setting the value of this attribute
|
||||||
* @return {boolean} whether or not the value of that attribute has changed (false if the attribute
|
* @return {boolean} whether or not the value of that attribute has changed (false if the attribute
|
||||||
* was not previously set)
|
* was not previously set)
|
||||||
*/
|
*/
|
||||||
_setAttribute(attributeName, attributeValue, log = false, operation = undefined, stealth = false)
|
_setAttribute(attributeName, attributeValue, log = false, operation = undefined, stealth = false)
|
||||||
{
|
{
|
||||||
const response = { origin: 'PsychObject.setAttribute', context: 'when setting the attribute of an object' };
|
const response = {origin: 'PsychObject.setAttribute', context: 'when setting the attribute of an object'};
|
||||||
|
|
||||||
if (typeof attributeName == 'undefined')
|
if (typeof attributeName == 'undefined')
|
||||||
throw Object.assign(response, { error: 'the attribute name cannot be' +
|
{
|
||||||
' undefined' });
|
throw Object.assign(response, {
|
||||||
if (typeof attributeValue == 'undefined') {
|
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');
|
this._psychoJS.logger.warn('setting the value of attribute: ' + attributeName + ' in PsychObject: ' + this._name + ' as: undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
// (*) apply operation to old and new values:
|
// (*) apply operation to old and new values:
|
||||||
if (typeof operation !== 'undefined' && this.hasOwnProperty('_' + attributeName)) {
|
if (typeof operation !== 'undefined' && this.hasOwnProperty('_' + attributeName))
|
||||||
|
{
|
||||||
let oldValue = this['_' + attributeName];
|
let oldValue = this['_' + attributeName];
|
||||||
|
|
||||||
// operations can only be applied to numbers and array of numbers (which can be empty):
|
// operations can only be applied to numbers and array of numbers (which can be empty):
|
||||||
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == 'number'))) {
|
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == 'number')))
|
||||||
|
{
|
||||||
|
|
||||||
// value is an array:
|
// value is an array:
|
||||||
if (Array.isArray(attributeValue)) {
|
if (Array.isArray(attributeValue))
|
||||||
|
{
|
||||||
// old value is also an array
|
// old value is also an array
|
||||||
if (Array.isArray(oldValue)) {
|
if (Array.isArray(oldValue))
|
||||||
|
{
|
||||||
if (attributeValue.length !== oldValue.length)
|
if (attributeValue.length !== oldValue.length)
|
||||||
throw Object.assign(response, { error: 'old and new' +
|
{
|
||||||
|
throw Object.assign(response, {
|
||||||
|
error: 'old and new' +
|
||||||
' value should have' +
|
' value should have' +
|
||||||
' the same size when they are both arrays' });
|
' the same size when they are both arrays'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
switch (operation) {
|
switch (operation)
|
||||||
|
{
|
||||||
case '':
|
case '':
|
||||||
// no change to value;
|
// no change to value;
|
||||||
break;
|
break;
|
||||||
@ -161,14 +182,18 @@ export class PsychObject extends EventEmitter
|
|||||||
attributeValue = attributeValue.map((v, i) => oldValue[i] % v);
|
attributeValue = attributeValue.map((v, i) => oldValue[i] % v);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unsupported' +
|
throw Object.assign(response, {
|
||||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
error: 'unsupported' +
|
||||||
|
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
// old value is a scalar
|
// old value is a scalar
|
||||||
{
|
{
|
||||||
switch (operation) {
|
switch (operation)
|
||||||
|
{
|
||||||
case '':
|
case '':
|
||||||
// no change to value;
|
// no change to value;
|
||||||
break;
|
break;
|
||||||
@ -191,17 +216,22 @@ export class PsychObject extends EventEmitter
|
|||||||
attributeValue = attributeValue.map(v => oldValue % v);
|
attributeValue = attributeValue.map(v => oldValue % v);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unsupported' +
|
throw Object.assign(response, {
|
||||||
' value: ' + JSON.stringify(attributeValue) + ' for' +
|
error: 'unsupported' +
|
||||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
' value: ' + JSON.stringify(attributeValue) + ' for' +
|
||||||
|
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
// value is a scalar
|
// value is a scalar
|
||||||
{
|
{
|
||||||
// old value is an array
|
// old value is an array
|
||||||
if (Array.isArray(oldValue)) {
|
if (Array.isArray(oldValue))
|
||||||
switch (operation) {
|
{
|
||||||
|
switch (operation)
|
||||||
|
{
|
||||||
case '':
|
case '':
|
||||||
attributeValue = oldValue.map(v => attributeValue);
|
attributeValue = oldValue.map(v => attributeValue);
|
||||||
break;
|
break;
|
||||||
@ -224,14 +254,18 @@ export class PsychObject extends EventEmitter
|
|||||||
attributeValue = oldValue.map(v => v % attributeValue);
|
attributeValue = oldValue.map(v => v % attributeValue);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unsupported' +
|
throw Object.assign(response, {
|
||||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
error: 'unsupported' +
|
||||||
|
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
// old value is a scalar
|
// old value is a scalar
|
||||||
{
|
{
|
||||||
switch (operation) {
|
switch (operation)
|
||||||
|
{
|
||||||
case '':
|
case '':
|
||||||
// no change to value;
|
// no change to value;
|
||||||
break;
|
break;
|
||||||
@ -254,14 +288,19 @@ export class PsychObject extends EventEmitter
|
|||||||
attributeValue = oldValue % attributeValue;
|
attributeValue = oldValue % attributeValue;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unsupported' +
|
throw Object.assign(response, {
|
||||||
' value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
error: 'unsupported' +
|
||||||
|
' value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
}
|
||||||
throw Object.assign(response, { error: 'operation: ' + operation + ' is invalid for old value: ' + JSON.stringify(oldValue) + ' and new value: ' + JSON.stringify(attributeValue) });
|
else
|
||||||
|
{
|
||||||
|
throw Object.assign(response, {error: 'operation: ' + operation + ' is invalid for old value: ' + JSON.stringify(oldValue) + ' and new value: ' + JSON.stringify(attributeValue)});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -286,7 +325,7 @@ export class PsychObject extends EventEmitter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add attributes to this instance (e.g. define setters and getters) and affect values to them.
|
* Add attributes to this instance (e.g. define setters and getters) and affect values to them.
|
||||||
*
|
*
|
||||||
* <p>Notes:
|
* <p>Notes:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li> If the object already has a set<attributeName> method, we do not redefine it,
|
* <li> If the object already has a set<attributeName> method, we do not redefine it,
|
||||||
@ -295,7 +334,7 @@ export class PsychObject extends EventEmitter
|
|||||||
* the call to super (see module:visual.ImageStim for an illustration).</li>
|
* the call to super (see module:visual.ImageStim for an illustration).</li>
|
||||||
* </ul></p>
|
* </ul></p>
|
||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
* @param {Object} cls - the class object of the subclass of PsychoObject whose attributes we will set
|
* @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)
|
* @param {...*} [args] - the values for the attributes (this also determines which attributes will be set)
|
||||||
*
|
*
|
||||||
@ -313,18 +352,22 @@ export class PsychObject extends EventEmitter
|
|||||||
// (*) add (argument name, argument value) pairs to the attribute map:
|
// (*) add (argument name, argument value) pairs to the attribute map:
|
||||||
let attributeMap = new Map();
|
let attributeMap = new Map();
|
||||||
for (let i = 1; i < callArgs.length; ++i)
|
for (let i = 1; i < callArgs.length; ++i)
|
||||||
|
{
|
||||||
attributeMap.set(callArgs[i], args[i - 1]);
|
attributeMap.set(callArgs[i], args[i - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
// (*) set the value, define the get/set<attributeName> properties and define the getter and setter:
|
// (*) set the value, define the get/set<attributeName> properties and define the getter and setter:
|
||||||
for (let [name, value] of attributeMap.entries())
|
for (let [name, value] of attributeMap.entries())
|
||||||
|
{
|
||||||
this._addAttribute(name, value);
|
this._addAttribute(name, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an attribute to this instance (e.g. define setters and getters) and affect a value to it.
|
* Add an attribute to this instance (e.g. define setters and getters) and affect a value to it.
|
||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
* @param {string} name - the name of the attribute
|
* @param {string} name - the name of the attribute
|
||||||
* @param {object} value - the value of the attribute
|
* @param {object} value - the value of the attribute
|
||||||
*/
|
*/
|
||||||
@ -332,18 +375,29 @@ export class PsychObject extends EventEmitter
|
|||||||
{
|
{
|
||||||
const getPropertyName = 'get' + name[0].toUpperCase() + name.substr(1);
|
const getPropertyName = 'get' + name[0].toUpperCase() + name.substr(1);
|
||||||
if (typeof this[getPropertyName] === 'undefined')
|
if (typeof this[getPropertyName] === 'undefined')
|
||||||
|
{
|
||||||
this[getPropertyName] = () => this['_' + name];
|
this[getPropertyName] = () => this['_' + name];
|
||||||
|
}
|
||||||
|
|
||||||
const setPropertyName = 'set' + name[0].toUpperCase() + name.substr(1);
|
const setPropertyName = 'set' + name[0].toUpperCase() + name.substr(1);
|
||||||
if (typeof this[setPropertyName] === 'undefined')
|
if (typeof this[setPropertyName] === 'undefined')
|
||||||
this[setPropertyName] = (value, log = false) => {
|
{
|
||||||
|
this[setPropertyName] = (value, log = false) =>
|
||||||
|
{
|
||||||
this._setAttribute(name, value, log);
|
this._setAttribute(name, value, log);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Object.defineProperty(this, name, {
|
Object.defineProperty(this, name, {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() { return this[getPropertyName](); /* return this['_' + name];*/ },
|
get()
|
||||||
set(value) { this[setPropertyName](value); }
|
{
|
||||||
|
return this[getPropertyName](); /* return this['_' + name];*/
|
||||||
|
},
|
||||||
|
set(value)
|
||||||
|
{
|
||||||
|
this[setPropertyName](value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// note: we use this[name] instead of this['_' + name] since a this.set<Name> method may available
|
// note: we use this[name] instead of this['_' + name] since a this.set<Name> method may available
|
||||||
@ -354,4 +408,4 @@ export class PsychObject extends EventEmitter
|
|||||||
this._userAttributes.add(name);
|
this._userAttributes.add(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Scheduler.
|
* Scheduler.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -11,7 +11,7 @@
|
|||||||
/**
|
/**
|
||||||
* <p>A scheduler helps run the main loop by managing scheduled functions,
|
* <p>A scheduler helps run the main loop by managing scheduled functions,
|
||||||
* called tasks, after each frame is displayed.</p>
|
* called tasks, after each frame is displayed.</p>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Tasks are either another [Scheduler]{@link module:util.Scheduler}, or a
|
* Tasks are either another [Scheduler]{@link module:util.Scheduler}, or a
|
||||||
* JavaScript functions returning one of the following codes:
|
* JavaScript functions returning one of the following codes:
|
||||||
@ -22,19 +22,19 @@
|
|||||||
* <li>Scheduler.Event.QUIT: Quit the scheduler.</li>
|
* <li>Scheduler.Event.QUIT: Quit the scheduler.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <p> It is possible to create sub-schedulers, e.g. to handle loops.
|
* <p> It is possible to create sub-schedulers, e.g. to handle loops.
|
||||||
* Sub-schedulers are added to a parent scheduler as a normal
|
* 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>
|
* task would be by calling [scheduler.add(subScheduler)]{@link module:util.Scheduler#add}.</p>
|
||||||
*
|
*
|
||||||
* <p> Conditional branching is also available:
|
* <p> Conditional branching is also available:
|
||||||
* [scheduler.addConditionalBranches]{@link module:util.Scheduler#addConditional}</p>
|
* [scheduler.addConditionalBranches]{@link module:util.Scheduler#addConditional}</p>
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @name module:util.Scheduler
|
* @name module:util.Scheduler
|
||||||
* @class
|
* @class
|
||||||
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class Scheduler
|
export class Scheduler
|
||||||
{
|
{
|
||||||
@ -61,24 +61,28 @@ export class Scheduler
|
|||||||
* @public
|
* @public
|
||||||
* @returns {module:util.Scheduler#Status} the status of the scheduler
|
* @returns {module:util.Scheduler#Status} the status of the scheduler
|
||||||
*/
|
*/
|
||||||
get status() { return this._status; }
|
get status()
|
||||||
|
{
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task to be run by the scheduler.
|
* Task to be run by the scheduler.
|
||||||
*
|
*
|
||||||
* @callback module:util.Scheduler~Task
|
* @callback module:util.Scheduler~Task
|
||||||
* @param {*} [args] optional arguments
|
* @param {*} [args] optional arguments
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Schedule a new task.
|
* Schedule a new task.
|
||||||
*
|
*
|
||||||
* @name module:util.Scheduler#add
|
* @name module:util.Scheduler#add
|
||||||
* @public
|
* @public
|
||||||
* @param {module:util.Scheduler~Task | module:util.Scheduler} task - the task to be scheduled
|
* @param {module:util.Scheduler~Task | module:util.Scheduler} task - the task to be scheduled
|
||||||
* @param {...*} args - arguments for that task
|
* @param {...*} args - arguments for that task
|
||||||
*/
|
*/
|
||||||
add(task, ...args) {
|
add(task, ...args)
|
||||||
|
{
|
||||||
this._taskList.push(task);
|
this._taskList.push(task);
|
||||||
this._argsList.push(args);
|
this._argsList.push(args);
|
||||||
}
|
}
|
||||||
@ -86,28 +90,34 @@ export class Scheduler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Condition evaluated when the task is run.
|
* Condition evaluated when the task is run.
|
||||||
*
|
*
|
||||||
* @callback module:util.Scheduler~Condition
|
* @callback module:util.Scheduler~Condition
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Schedule a series of task or another, based on a condition.
|
* Schedule a series of task or another, based on a condition.
|
||||||
*
|
*
|
||||||
* <p>Note: the tasks are [sub-schedulers]{@link module:util.Scheduler}.</p>
|
* <p>Note: the tasks are [sub-schedulers]{@link module:util.Scheduler}.</p>
|
||||||
*
|
*
|
||||||
* @name module:util.Scheduler#addConditional
|
* @name module:util.Scheduler#addConditional
|
||||||
* @public
|
* @public
|
||||||
* @param {module:util.Scheduler~Condition} condition - the condition
|
* @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} 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
|
* @param {module:util.Scheduler} elseScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is not satisfied
|
||||||
*/
|
*/
|
||||||
addConditional(condition, thenScheduler, elseScheduler) {
|
addConditional(condition, thenScheduler, elseScheduler)
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
let task = function () {
|
let task = function ()
|
||||||
|
{
|
||||||
if (condition())
|
if (condition())
|
||||||
|
{
|
||||||
self.add(thenScheduler);
|
self.add(thenScheduler);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
self.add(elseScheduler);
|
self.add(elseScheduler);
|
||||||
|
}
|
||||||
|
|
||||||
return Scheduler.Event.NEXT;
|
return Scheduler.Event.NEXT;
|
||||||
};
|
};
|
||||||
@ -124,11 +134,14 @@ export class Scheduler
|
|||||||
* @name module:util.Scheduler#start
|
* @name module:util.Scheduler#start
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
start() {
|
start()
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
let update = () => {
|
let update = () =>
|
||||||
|
{
|
||||||
// stop the animation if need be:
|
// stop the animation if need be:
|
||||||
if (self._stopAtNextUpdate) {
|
if (self._stopAtNextUpdate)
|
||||||
|
{
|
||||||
self._status = Scheduler.Status.STOPPED;
|
self._status = Scheduler.Status.STOPPED;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -137,7 +150,8 @@ export class Scheduler
|
|||||||
|
|
||||||
// run the next scheduled tasks until a scene render is requested:
|
// run the next scheduled tasks until a scene render is requested:
|
||||||
const state = self._runNextTasks();
|
const state = self._runNextTasks();
|
||||||
if (state === Scheduler.Event.QUIT) {
|
if (state === Scheduler.Event.QUIT)
|
||||||
|
{
|
||||||
self._status = Scheduler.Status.STOPPED;
|
self._status = Scheduler.Status.STOPPED;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -156,11 +170,12 @@ export class Scheduler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop this scheduler.
|
* Stop this scheduler.
|
||||||
*
|
*
|
||||||
* @name module:util.Scheduler#stop
|
* @name module:util.Scheduler#stop
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop()
|
||||||
|
{
|
||||||
this._status = Scheduler.Status.STOPPED;
|
this._status = Scheduler.Status.STOPPED;
|
||||||
this._stopAtNextTask = true;
|
this._stopAtNextTask = true;
|
||||||
this._stopAtNextUpdate = true;
|
this._stopAtNextUpdate = true;
|
||||||
@ -174,52 +189,67 @@ export class Scheduler
|
|||||||
* @private
|
* @private
|
||||||
* @return {module:util.Scheduler#Event} the state of the scheduler after the last task ran
|
* @return {module:util.Scheduler#Event} the state of the scheduler after the last task ran
|
||||||
*/
|
*/
|
||||||
_runNextTasks() {
|
_runNextTasks()
|
||||||
|
{
|
||||||
this._status = Scheduler.Status.RUNNING;
|
this._status = Scheduler.Status.RUNNING;
|
||||||
|
|
||||||
let state = Scheduler.Event.NEXT;
|
let state = Scheduler.Event.NEXT;
|
||||||
while (state === Scheduler.Event.NEXT) {
|
while (state === Scheduler.Event.NEXT)
|
||||||
|
{
|
||||||
// check if we need to quit:
|
// check if we need to quit:
|
||||||
if (this._stopAtNextTask)
|
if (this._stopAtNextTask)
|
||||||
|
{
|
||||||
return Scheduler.Event.QUIT;
|
return Scheduler.Event.QUIT;
|
||||||
|
}
|
||||||
|
|
||||||
// if there is no current task, we look for the next one in the list or quit if there is none:
|
// if there is no current task, we look for the next one in the list or quit if there is none:
|
||||||
if (typeof this._currentTask == 'undefined') {
|
if (typeof this._currentTask == 'undefined')
|
||||||
|
{
|
||||||
|
|
||||||
// a task is available in the taskList:
|
// a task is available in the taskList:
|
||||||
if (this._taskList.length > 0) {
|
if (this._taskList.length > 0)
|
||||||
|
{
|
||||||
this._currentTask = this._taskList.shift();
|
this._currentTask = this._taskList.shift();
|
||||||
this._currentArgs = this._argsList.shift();
|
this._currentArgs = this._argsList.shift();
|
||||||
}
|
}
|
||||||
// the taskList is empty: we quit
|
// the taskList is empty: we quit
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
this._currentTask = undefined;
|
this._currentTask = undefined;
|
||||||
this._currentArgs = undefined;
|
this._currentArgs = undefined;
|
||||||
return Scheduler.Event.QUIT;
|
return Scheduler.Event.QUIT;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// we are repeating a task
|
// we are repeating a task
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the current task is a function, we run it:
|
// if the current task is a function, we run it:
|
||||||
if (this._currentTask instanceof Function) {
|
if (this._currentTask instanceof Function)
|
||||||
|
{
|
||||||
state = this._currentTask(...this._currentArgs);
|
state = this._currentTask(...this._currentArgs);
|
||||||
}
|
}
|
||||||
// otherwise, we assume that the current task is a scheduler and we run its tasks until a rendering
|
// otherwise, we assume that the current task is a scheduler and we run its tasks until a rendering
|
||||||
// of the scene is required.
|
// of the scene is required.
|
||||||
// note: "if (this._currentTask instanceof Scheduler)" does not work because of CORS...
|
// note: "if (this._currentTask instanceof Scheduler)" does not work because of CORS...
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
state = this._currentTask._runNextTasks();
|
state = this._currentTask._runNextTasks();
|
||||||
if (state === Scheduler.Event.QUIT) {
|
if (state === Scheduler.Event.QUIT)
|
||||||
|
{
|
||||||
// if the experiment has not ended, we move onto the next task:
|
// if the experiment has not ended, we move onto the next task:
|
||||||
if (!this._psychoJS.experiment.experimentEnded)
|
if (!this._psychoJS.experiment.experimentEnded)
|
||||||
|
{
|
||||||
state = Scheduler.Event.NEXT;
|
state = Scheduler.Event.NEXT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the current task's return status is FLIP_REPEAT, we will re-run it, otherwise
|
// if the current task's return status is FLIP_REPEAT, we will re-run it, otherwise
|
||||||
// we move onto the next task:
|
// we move onto the next task:
|
||||||
if (state !== Scheduler.Event.FLIP_REPEAT) {
|
if (state !== Scheduler.Event.FLIP_REPEAT)
|
||||||
|
{
|
||||||
this._currentTask = undefined;
|
this._currentTask = undefined;
|
||||||
this._currentArgs = undefined;
|
this._currentArgs = undefined;
|
||||||
}
|
}
|
||||||
@ -234,7 +264,7 @@ export class Scheduler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Events.
|
* Events.
|
||||||
*
|
*
|
||||||
* @name module:util.Scheduler#Event
|
* @name module:util.Scheduler#Event
|
||||||
* @enum {Symbol}
|
* @enum {Symbol}
|
||||||
* @readonly
|
* @readonly
|
||||||
@ -248,7 +278,7 @@ Scheduler.Event = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the scene and repeat the task.
|
* Render the scene and repeat the task.
|
||||||
*/
|
*/
|
||||||
FLIP_REPEAT: Symbol.for('FLIP_REPEAT'),
|
FLIP_REPEAT: Symbol.for('FLIP_REPEAT'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -281,4 +311,4 @@ Scheduler.Status = {
|
|||||||
* The Scheduler is stopped.
|
* The Scheduler is stopped.
|
||||||
*/
|
*/
|
||||||
STOPPED: Symbol.for('STOPPED')
|
STOPPED: Symbol.for('STOPPED')
|
||||||
};
|
};
|
||||||
|
383
js/util/Util.js
383
js/util/Util.js
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Various utilities.
|
* Various utilities.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -24,19 +24,23 @@
|
|||||||
* class NewClass extends mix(BaseClass).with(Mixin1, Mixin2) { ... }
|
* class NewClass extends mix(BaseClass).with(Mixin1, Mixin2) { ... }
|
||||||
*/
|
*/
|
||||||
export let mix = (superclass) => new MixinBuilder(superclass);
|
export let mix = (superclass) => new MixinBuilder(superclass);
|
||||||
class MixinBuilder {
|
|
||||||
constructor(superclass) {
|
class MixinBuilder
|
||||||
this.superclass = superclass;
|
{
|
||||||
}
|
constructor(superclass)
|
||||||
|
{
|
||||||
|
this.superclass = superclass;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param mixins
|
* @param mixins
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
with(...mixins) {
|
with(...mixins)
|
||||||
return mixins.reduce((c, mixin) => mixin(c), this.superclass);
|
{
|
||||||
}
|
return mixins.reduce((c, mixin) => mixin(c), this.superclass);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -50,7 +54,8 @@ class MixinBuilder {
|
|||||||
* @return {Object[]} the resulting value in the format [error, return data]
|
* @return {Object[]} the resulting value in the format [error, return data]
|
||||||
* where error is null if there was no error
|
* where error is null if there was no error
|
||||||
*/
|
*/
|
||||||
export function promiseToTupple(promise) {
|
export function promiseToTupple(promise)
|
||||||
|
{
|
||||||
return promise
|
return promise
|
||||||
.then(data => [null, data])
|
.then(data => [null, data])
|
||||||
.catch(error => [error, null]);
|
.catch(error => [error, null]);
|
||||||
@ -68,7 +73,8 @@ export function promiseToTupple(promise) {
|
|||||||
*/
|
*/
|
||||||
export function makeUuid()
|
export function makeUuid()
|
||||||
{
|
{
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c)
|
||||||
|
{
|
||||||
const r = Math.random() * 16 | 0, v = (c === 'x') ? r : (r & 0x3 | 0x8);
|
const r = Math.random() * 16 | 0, v = (c === 'x') ? r : (r & 0x3 | 0x8);
|
||||||
return v.toString(16);
|
return v.toString(16);
|
||||||
});
|
});
|
||||||
@ -85,13 +91,16 @@ export function makeUuid()
|
|||||||
*/
|
*/
|
||||||
export function getErrorStack()
|
export function getErrorStack()
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
throw Error('');
|
throw Error('');
|
||||||
} catch(error) {
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
// we need to remove the second line since it references getErrorStack:
|
// we need to remove the second line since it references getErrorStack:
|
||||||
let stack = error.stack.split("\n");
|
let stack = error.stack.split("\n");
|
||||||
stack.splice(1, 1);
|
stack.splice(1, 1);
|
||||||
|
|
||||||
return JSON.stringify(stack.join('\n'));
|
return JSON.stringify(stack.join('\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,10 +117,22 @@ export function getErrorStack()
|
|||||||
*/
|
*/
|
||||||
export function isEmpty(x)
|
export function isEmpty(x)
|
||||||
{
|
{
|
||||||
if (typeof x === 'undefined') return true;
|
if (typeof x === 'undefined')
|
||||||
if (!Array.isArray(x)) return false;
|
{
|
||||||
if (x.length === 0) return true;
|
return true;
|
||||||
if (x.length === 1 && typeof x[0] === 'undefined') return true;
|
}
|
||||||
|
if (!Array.isArray(x))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (x.length === 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (x.length === 1 && typeof x[0] === 'undefined')
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -133,38 +154,65 @@ export function detectBrowser()
|
|||||||
{
|
{
|
||||||
// Opera 8.0+
|
// Opera 8.0+
|
||||||
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
||||||
if (isOpera) return 'Opera';
|
if (isOpera)
|
||||||
|
{
|
||||||
|
return 'Opera';
|
||||||
|
}
|
||||||
|
|
||||||
// Firefox 1.0+
|
// Firefox 1.0+
|
||||||
const isFirefox = (typeof InstallTrigger !== 'undefined');
|
const isFirefox = (typeof InstallTrigger !== 'undefined');
|
||||||
if (isFirefox) return 'Firefox';
|
if (isFirefox)
|
||||||
|
{
|
||||||
|
return 'Firefox';
|
||||||
|
}
|
||||||
|
|
||||||
// Safari 3.0+ "[object HTMLElementConstructor]"
|
// Safari 3.0+ "[object HTMLElementConstructor]"
|
||||||
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification));
|
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p)
|
||||||
if (isSafari) return 'Safari';
|
{
|
||||||
|
return p.toString() === "[object SafariRemoteNotification]";
|
||||||
|
})(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification));
|
||||||
|
if (isSafari)
|
||||||
|
{
|
||||||
|
return 'Safari';
|
||||||
|
}
|
||||||
|
|
||||||
// Internet Explorer 6-11
|
// Internet Explorer 6-11
|
||||||
// const isIE6 = !window.XMLHttpRequest;
|
// const isIE6 = !window.XMLHttpRequest;
|
||||||
// const isIE7 = document.all && window.XMLHttpRequest && !XDomainRequest && !window.opera;
|
// const isIE7 = document.all && window.XMLHttpRequest && !XDomainRequest && !window.opera;
|
||||||
// const isIE8 = document.documentMode==8;
|
// const isIE8 = document.documentMode==8;
|
||||||
const isIE = /*@cc_on!@*/false || !!document.documentMode;
|
const isIE = /*@cc_on!@*/false || !!document.documentMode;
|
||||||
if (isIE) return 'IE';
|
if (isIE)
|
||||||
|
{
|
||||||
|
return 'IE';
|
||||||
|
}
|
||||||
|
|
||||||
// Edge 20+
|
// Edge 20+
|
||||||
const isEdge = !isIE && !!window.StyleMedia;
|
const isEdge = !isIE && !!window.StyleMedia;
|
||||||
if (isEdge) return 'Edge';
|
if (isEdge)
|
||||||
|
{
|
||||||
|
return 'Edge';
|
||||||
|
}
|
||||||
|
|
||||||
// Chrome 1+
|
// Chrome 1+
|
||||||
const isChrome = window.chrome;
|
const isChrome = window.chrome;
|
||||||
if (isChrome) return 'Chrome';
|
if (isChrome)
|
||||||
|
{
|
||||||
|
return 'Chrome';
|
||||||
|
}
|
||||||
|
|
||||||
// Chromium-based Edge:
|
// Chromium-based Edge:
|
||||||
const isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") !== -1);
|
const isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") !== -1);
|
||||||
if (isEdgeChromium) return 'EdgeChromium';
|
if (isEdgeChromium)
|
||||||
|
{
|
||||||
|
return 'EdgeChromium';
|
||||||
|
}
|
||||||
|
|
||||||
// Blink engine detection
|
// Blink engine detection
|
||||||
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
||||||
if (isBlink) return 'Blink';
|
if (isBlink)
|
||||||
|
{
|
||||||
|
return 'Blink';
|
||||||
|
}
|
||||||
|
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
@ -188,20 +236,30 @@ export function detectBrowser()
|
|||||||
*/
|
*/
|
||||||
export function toNumerical(obj)
|
export function toNumerical(obj)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.toNumerical', context: 'when converting an object to its numerical form' };
|
const response = {origin: 'util.toNumerical', context: 'when converting an object to its numerical form'};
|
||||||
|
|
||||||
if (typeof obj === 'number')
|
if (typeof obj === 'number')
|
||||||
|
{
|
||||||
return obj;
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof obj === 'string')
|
if (typeof obj === 'string')
|
||||||
|
{
|
||||||
obj = [obj];
|
obj = [obj];
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj))
|
||||||
return obj.map( e => {
|
{
|
||||||
|
return obj.map(e =>
|
||||||
|
{
|
||||||
let n = Number.parseFloat(e);
|
let n = Number.parseFloat(e);
|
||||||
if (Number.isNaN(n))
|
if (Number.isNaN(n))
|
||||||
Object.assign(response, { error: 'unable to convert: ' + e + ' to a' +
|
{
|
||||||
' number.'});
|
Object.assign(response, {
|
||||||
|
error: 'unable to convert: ' + e + ' to a' +
|
||||||
|
' number.'
|
||||||
|
});
|
||||||
|
}
|
||||||
return n;
|
return n;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -224,17 +282,20 @@ export function IsPointInsidePolygon(point, vertices)
|
|||||||
{
|
{
|
||||||
const x = point[0];
|
const x = point[0];
|
||||||
const y = point[1];
|
const y = point[1];
|
||||||
|
|
||||||
let isInside = false;
|
let isInside = false;
|
||||||
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++)
|
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++)
|
||||||
{
|
{
|
||||||
const xi = vertices[i][0], yi = vertices[i][1];
|
const xi = vertices[i][0], yi = vertices[i][1];
|
||||||
const xj = vertices[j][0], yj = vertices[j][1];
|
const xj = vertices[j][0], yj = vertices[j][1];
|
||||||
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||||
if (intersect) isInside = !isInside;
|
if (intersect)
|
||||||
}
|
{
|
||||||
|
isInside = !isInside;
|
||||||
return isInside;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -269,18 +330,22 @@ export function shuffle(array)
|
|||||||
* @param {string} units - the units
|
* @param {string} units - the units
|
||||||
* @returns {number[]} the position of the object in pixel units
|
* @returns {number[]} the position of the object in pixel units
|
||||||
*/
|
*/
|
||||||
export function getPositionFromObject(object, units)
|
export function getPositionFromObject(object, units)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.getPositionFromObject', context: 'when getting the position of an object' };
|
const response = {origin: 'util.getPositionFromObject', context: 'when getting the position of an object'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
if (typeof object === 'undefined')
|
if (typeof object === 'undefined')
|
||||||
|
{
|
||||||
throw 'cannot get the position of an undefined object';
|
throw 'cannot get the position of an undefined object';
|
||||||
|
}
|
||||||
|
|
||||||
let objectWin = undefined;
|
let objectWin = undefined;
|
||||||
|
|
||||||
// object has a getPos function:
|
// object has a getPos function:
|
||||||
if (typeof object.getPos === 'function') {
|
if (typeof object.getPos === 'function')
|
||||||
|
{
|
||||||
units = object.units;
|
units = object.units;
|
||||||
objectWin = object.win;
|
objectWin = object.win;
|
||||||
object = object.getPos();
|
object = object.getPos();
|
||||||
@ -289,8 +354,9 @@ export function getPositionFromObject(object, units)
|
|||||||
// convert object to pixel units:
|
// convert object to pixel units:
|
||||||
return to_px(object, units, objectWin);
|
return to_px(object, units, objectWin);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,18 +374,25 @@ export function getPositionFromObject(object, units)
|
|||||||
*/
|
*/
|
||||||
export function to_px(pos, posUnit, win)
|
export function to_px(pos, posUnit, win)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_px', context: 'when converting a position to pixel units' };
|
const response = {origin: 'util.to_px', context: 'when converting a position to pixel units'};
|
||||||
|
|
||||||
if (posUnit === 'pix')
|
if (posUnit === 'pix')
|
||||||
|
{
|
||||||
return pos;
|
return pos;
|
||||||
|
}
|
||||||
else if (posUnit === 'norm')
|
else if (posUnit === 'norm')
|
||||||
return [pos[0] * win.size[0]/2.0, pos[1] * win.size[1]/2.0];
|
{
|
||||||
else if (posUnit === 'height') {
|
return [pos[0] * win.size[0] / 2.0, pos[1] * win.size[1] / 2.0];
|
||||||
|
}
|
||||||
|
else if (posUnit === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(win.size[0], win.size[1]);
|
const minSize = Math.min(win.size[0], win.size[1]);
|
||||||
return [pos[0] * minSize, pos[1] * minSize];
|
return [pos[0] * minSize, pos[1] * minSize];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
{
|
||||||
|
throw Object.assign(response, {error: `unknown position units: ${posUnit}`});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -336,18 +409,23 @@ export function to_px(pos, posUnit, win)
|
|||||||
*/
|
*/
|
||||||
export function to_norm(pos, posUnit, win)
|
export function to_norm(pos, posUnit, win)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_norm', context: 'when converting a position to norm units' };
|
const response = {origin: 'util.to_norm', context: 'when converting a position to norm units'};
|
||||||
|
|
||||||
if (posUnit === 'norm')
|
if (posUnit === 'norm')
|
||||||
|
{
|
||||||
return pos;
|
return pos;
|
||||||
|
}
|
||||||
if (posUnit === 'pix')
|
if (posUnit === 'pix')
|
||||||
return [pos[0] / (win.size[0]/2.0), pos[1] / (win.size[1]/2.0)];
|
{
|
||||||
if (posUnit === 'height') {
|
return [pos[0] / (win.size[0] / 2.0), pos[1] / (win.size[1] / 2.0)];
|
||||||
|
}
|
||||||
|
if (posUnit === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(win.size[0], win.size[1]);
|
const minSize = Math.min(win.size[0], win.size[1]);
|
||||||
return [pos[0] * minSize / (win.size[0]/2.0), pos[1] * minSize / (win.size[1]/2.0)];
|
return [pos[0] * minSize / (win.size[0] / 2.0), pos[1] * minSize / (win.size[1] / 2.0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
throw Object.assign(response, {error: `unknown position units: ${posUnit}`});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -364,20 +442,24 @@ export function to_norm(pos, posUnit, win)
|
|||||||
*/
|
*/
|
||||||
export function to_height(pos, posUnit, win)
|
export function to_height(pos, posUnit, win)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_height', context: 'when converting a position to height units' };
|
const response = {origin: 'util.to_height', context: 'when converting a position to height units'};
|
||||||
|
|
||||||
if (posUnit === 'height')
|
if (posUnit === 'height')
|
||||||
|
{
|
||||||
return pos;
|
return pos;
|
||||||
if (posUnit === 'pix') {
|
}
|
||||||
|
if (posUnit === 'pix')
|
||||||
|
{
|
||||||
const minSize = Math.min(win.size[0], win.size[1]);
|
const minSize = Math.min(win.size[0], win.size[1]);
|
||||||
return [pos[0] / minSize, pos[1] / minSize];
|
return [pos[0] / minSize, pos[1] / minSize];
|
||||||
}
|
}
|
||||||
if (posUnit === 'norm') {
|
if (posUnit === 'norm')
|
||||||
|
{
|
||||||
const minSize = Math.min(win.size[0], win.size[1]);
|
const minSize = Math.min(win.size[0], win.size[1]);
|
||||||
return [pos[0] * win.size[0]/2.0 / minSize, pos[1] * win.size[1]/2.0 / minSize];
|
return [pos[0] * win.size[0] / 2.0 / minSize, pos[1] * win.size[1] / 2.0 / minSize];
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
throw Object.assign(response, {error: `unknown position units: ${posUnit}`});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -394,19 +476,28 @@ export function to_height(pos, posUnit, win)
|
|||||||
*/
|
*/
|
||||||
export function to_win(pos, posUnit, win)
|
export function to_win(pos, posUnit, win)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_win', context: 'when converting a position to window units' };
|
const response = {origin: 'util.to_win', context: 'when converting a position to window units'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
if (win._units === 'pix')
|
if (win._units === 'pix')
|
||||||
|
{
|
||||||
return to_px(pos, posUnit, win);
|
return to_px(pos, posUnit, win);
|
||||||
|
}
|
||||||
if (win._units === 'norm')
|
if (win._units === 'norm')
|
||||||
|
{
|
||||||
return to_norm(pos, posUnit, win);
|
return to_norm(pos, posUnit, win);
|
||||||
|
}
|
||||||
if (win._units === 'height')
|
if (win._units === 'height')
|
||||||
|
{
|
||||||
return to_height(pos, posUnit, win);
|
return to_height(pos, posUnit, win);
|
||||||
|
}
|
||||||
|
|
||||||
throw `unknown window units: ${win._units}`;
|
throw `unknown window units: ${win._units}`;
|
||||||
} catch (error) {
|
}
|
||||||
throw Object.assign(response, { response, error });
|
catch (error)
|
||||||
|
{
|
||||||
|
throw Object.assign(response, {response, error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,19 +516,28 @@ export function to_win(pos, posUnit, win)
|
|||||||
*/
|
*/
|
||||||
export function to_unit(pos, posUnit, win, targetUnit)
|
export function to_unit(pos, posUnit, win, targetUnit)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_unit', context: 'when converting a position to different units' };
|
const response = {origin: 'util.to_unit', context: 'when converting a position to different units'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
if (targetUnit === 'pix')
|
if (targetUnit === 'pix')
|
||||||
|
{
|
||||||
return to_px(pos, posUnit, win);
|
return to_px(pos, posUnit, win);
|
||||||
|
}
|
||||||
if (targetUnit === 'norm')
|
if (targetUnit === 'norm')
|
||||||
|
{
|
||||||
return to_norm(pos, posUnit, win);
|
return to_norm(pos, posUnit, win);
|
||||||
|
}
|
||||||
if (targetUnit === 'height')
|
if (targetUnit === 'height')
|
||||||
|
{
|
||||||
return to_height(pos, posUnit, win);
|
return to_height(pos, posUnit, win);
|
||||||
|
}
|
||||||
|
|
||||||
throw `unknown target units: ${targetUnit}`;
|
throw `unknown target units: ${targetUnit}`;
|
||||||
} catch (error) {
|
}
|
||||||
throw Object.assign(response, { error });
|
catch (error)
|
||||||
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,26 +574,39 @@ export function to_pixiPoint(pos, posUnit, win)
|
|||||||
export function toString(object)
|
export function toString(object)
|
||||||
{
|
{
|
||||||
if (typeof object === 'undefined')
|
if (typeof object === 'undefined')
|
||||||
|
{
|
||||||
return 'undefined';
|
return 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
if (!object)
|
if (!object)
|
||||||
|
{
|
||||||
return 'null';
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof object === 'string')
|
if (typeof object === 'string')
|
||||||
|
{
|
||||||
return object;
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
// if the object is a class and has a toString method:
|
// if the object is a class and has a toString method:
|
||||||
if (object.constructor.toString().substring(0, 5) === 'class' && typeof object.toString === 'function')
|
if (object.constructor.toString().substring(0, 5) === 'class' && typeof object.toString === 'function')
|
||||||
|
{
|
||||||
return object.toString();
|
return object.toString();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try
|
||||||
const symbolReplacer = (key, value) => {
|
{
|
||||||
|
const symbolReplacer = (key, value) =>
|
||||||
|
{
|
||||||
if (typeof value === 'symbol')
|
if (typeof value === 'symbol')
|
||||||
|
{
|
||||||
value = Symbol.keyFor(value);
|
value = Symbol.keyFor(value);
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
return JSON.stringify(object, symbolReplacer);
|
return JSON.stringify(object, symbolReplacer);
|
||||||
} catch (e)
|
}
|
||||||
|
catch (e)
|
||||||
{
|
{
|
||||||
return 'Object (circular)';
|
return 'Object (circular)';
|
||||||
}
|
}
|
||||||
@ -502,20 +615,20 @@ export function toString(object)
|
|||||||
|
|
||||||
if (!String.prototype.format)
|
if (!String.prototype.format)
|
||||||
{
|
{
|
||||||
String.prototype.format = function()
|
String.prototype.format = function ()
|
||||||
{
|
{
|
||||||
var args = arguments;
|
var args = arguments;
|
||||||
return this
|
return this
|
||||||
.replace(/{(\d+)}/g, function(match, number)
|
.replace(/{(\d+)}/g, function (match, number)
|
||||||
{
|
{
|
||||||
return typeof args[number] != 'undefined' ? args[number] : match;
|
return typeof args[number] != 'undefined' ? args[number] : match;
|
||||||
})
|
})
|
||||||
.replace(/{([$_a-zA-Z][$_a-zA-Z0-9]*)}/g, function(match, name)
|
.replace(/{([$_a-zA-Z][$_a-zA-Z0-9]*)}/g, function (match, name)
|
||||||
{
|
{
|
||||||
//console.log("n=" + name + " args[0][name]=" + args[0][name]);
|
//console.log("n=" + name + " args[0][name]=" + args[0][name]);
|
||||||
return args.length > 0 && args[0][name] !== undefined ? args[0][name] : match;
|
return args.length > 0 && args[0][name] !== undefined ? args[0][name] : match;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -534,19 +647,22 @@ export function getRequestError(jqXHR, textStatus, errorThrown)
|
|||||||
let errorMsg = 'unknown error';
|
let errorMsg = 'unknown error';
|
||||||
|
|
||||||
if (typeof jqXHR.responseJSON !== 'undefined')
|
if (typeof jqXHR.responseJSON !== 'undefined')
|
||||||
|
{
|
||||||
errorMsg = jqXHR.responseJSON;
|
errorMsg = jqXHR.responseJSON;
|
||||||
|
}
|
||||||
else if (typeof jqXHR.responseText !== 'undefined')
|
else if (typeof jqXHR.responseText !== 'undefined')
|
||||||
|
{
|
||||||
errorMsg = jqXHR.responseText;
|
errorMsg = jqXHR.responseText;
|
||||||
|
}
|
||||||
else if (typeof errorThrown !== 'undefined')
|
else if (typeof errorThrown !== 'undefined')
|
||||||
|
{
|
||||||
errorMsg = errorThrown;
|
errorMsg = errorThrown;
|
||||||
|
}
|
||||||
|
|
||||||
return errorMsg;
|
return errorMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test whether an object is either an integer or the string representation of an integer.
|
* Test whether an object is either an integer or the string representation of an integer.
|
||||||
* <p>This is adapted from: https://stackoverflow.com/a/14794066</p>
|
* <p>This is adapted from: https://stackoverflow.com/a/14794066</p>
|
||||||
@ -557,9 +673,12 @@ export function getRequestError(jqXHR, textStatus, errorThrown)
|
|||||||
* @param {Object} obj - the input object
|
* @param {Object} obj - the input object
|
||||||
* @returns {boolean} whether or not the object is an integer or the string representation of an integer
|
* @returns {boolean} whether or not the object is an integer or the string representation of an integer
|
||||||
*/
|
*/
|
||||||
export function isInt(obj) {
|
export function isInt(obj)
|
||||||
|
{
|
||||||
if (isNaN(obj))
|
if (isNaN(obj))
|
||||||
return false;
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const x = parseFloat(obj);
|
const x = parseFloat(obj);
|
||||||
return (x | 0) === x;
|
return (x | 0) === x;
|
||||||
@ -573,12 +692,12 @@ export function isInt(obj) {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @returns {URLSearchParams} the iterable URLSearchParams
|
* @returns {URLSearchParams} the iterable URLSearchParams
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const urlParameters = util.getUrlParameters();
|
* const urlParameters = util.getUrlParameters();
|
||||||
* for (const [key, value] of urlParameters)
|
* for (const [key, value] of urlParameters)
|
||||||
* console.log(key + ' = ' + value);
|
* console.log(key + ' = ' + value);
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function getUrlParameters()
|
export function getUrlParameters()
|
||||||
{
|
{
|
||||||
@ -611,10 +730,12 @@ export function addInfoFromUrl(info)
|
|||||||
// note: parameters starting with a double underscore are reserved for client/server communication,
|
// note: parameters starting with a double underscore are reserved for client/server communication,
|
||||||
// we do not add them to info
|
// we do not add them to info
|
||||||
// for (const [key, value] of infoFromUrl)
|
// for (const [key, value] of infoFromUrl)
|
||||||
infoFromUrl.forEach( (value, key) =>
|
infoFromUrl.forEach((value, key) =>
|
||||||
{
|
{
|
||||||
if (key.indexOf('__') !== 0)
|
if (key.indexOf('__') !== 0)
|
||||||
|
{
|
||||||
info[key] = value;
|
info[key] = value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
@ -641,34 +762,48 @@ export function addInfoFromUrl(info)
|
|||||||
* @param {number | Array.<number> | string} selection - the selection
|
* @param {number | Array.<number> | string} selection - the selection
|
||||||
* @returns {Object | Array.<Object>} the array of selected items
|
* @returns {Object | Array.<Object>} the array of selected items
|
||||||
*/
|
*/
|
||||||
export function selectFromArray(array, selection) {
|
export function selectFromArray(array, selection)
|
||||||
|
{
|
||||||
|
|
||||||
// if selection is an integer, or a string representing an integer, we treat it as an index in the array
|
// if selection is an integer, or a string representing an integer, we treat it as an index in the array
|
||||||
// and return that entry:
|
// and return that entry:
|
||||||
if (isInt(selection))
|
if (isInt(selection))
|
||||||
|
{
|
||||||
return array[parseInt(selection)];
|
return array[parseInt(selection)];
|
||||||
|
}// if selection is an array, we treat it as a list of indices
|
||||||
// if selection is an array, we treat it as a list of indices
|
|
||||||
// and return an array with the entries corresponding to those indices:
|
// and return an array with the entries corresponding to those indices:
|
||||||
else if (Array.isArray(selection))
|
else if (Array.isArray(selection))
|
||||||
return array.filter( (e,i) => (selection.includes(i)) );
|
{
|
||||||
|
return array.filter((e, i) => (selection.includes(i)));
|
||||||
// if selection is a string, we decode it:
|
}// if selection is a string, we decode it:
|
||||||
else if (typeof selection === 'string') {
|
else if (typeof selection === 'string')
|
||||||
|
{
|
||||||
if (selection.indexOf(',') > -1)
|
if (selection.indexOf(',') > -1)
|
||||||
|
{
|
||||||
return selection.split(',').map(a => selectFromArray(array, a));
|
return selection.split(',').map(a => selectFromArray(array, a));
|
||||||
// return flattenArray( selection.split(',').map(a => selectFromArray(array, a)) );
|
}// return flattenArray( selection.split(',').map(a => selectFromArray(array, a)) );
|
||||||
else if (selection.indexOf(':') > -1) {
|
else if (selection.indexOf(':') > -1)
|
||||||
|
{
|
||||||
let sliceParams = selection.split(':').map(a => parseInt(a));
|
let sliceParams = selection.split(':').map(a => parseInt(a));
|
||||||
if (sliceParams.length === 3)
|
if (sliceParams.length === 3)
|
||||||
|
{
|
||||||
return sliceArray(array, sliceParams[0], sliceParams[2], sliceParams[1]);
|
return sliceArray(array, sliceParams[0], sliceParams[2], sliceParams[1]);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return sliceArray(array, ...sliceParams);
|
return sliceArray(array, ...sliceParams);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
throw { origin: 'selectFromArray', context: 'when selecting entries from an array', error: 'unknown selection type: ' + (typeof selection)};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'selectFromArray',
|
||||||
|
context: 'when selecting entries from an array',
|
||||||
|
error: 'unknown selection type: ' + (typeof selection)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -681,10 +816,12 @@ export function selectFromArray(array, selection) {
|
|||||||
* @param {Array.<Object>} array - the input array of arrays
|
* @param {Array.<Object>} array - the input array of arrays
|
||||||
* @returns {Array.<Object>} the flatten array
|
* @returns {Array.<Object>} the flatten array
|
||||||
*/
|
*/
|
||||||
export function flattenArray(array) {
|
export function flattenArray(array)
|
||||||
|
{
|
||||||
return array.reduce(
|
return array.reduce(
|
||||||
(flat, next) => {
|
(flat, next) =>
|
||||||
flat.push( (Array.isArray(next) && Array.isArray(next[0])) ? flattenArray(next) : next );
|
{
|
||||||
|
flat.push((Array.isArray(next) && Array.isArray(next[0])) ? flattenArray(next) : next);
|
||||||
return flat;
|
return flat;
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@ -706,22 +843,36 @@ export function flattenArray(array) {
|
|||||||
*/
|
*/
|
||||||
export function sliceArray(array, from = NaN, to = NaN, step = NaN)
|
export function sliceArray(array, from = NaN, to = NaN, step = NaN)
|
||||||
{
|
{
|
||||||
if (isNaN(from)) from = 0;
|
if (isNaN(from))
|
||||||
if (isNaN(to)) to = array.length;
|
{
|
||||||
|
from = 0;
|
||||||
|
}
|
||||||
|
if (isNaN(to))
|
||||||
|
{
|
||||||
|
to = array.length;
|
||||||
|
}
|
||||||
|
|
||||||
let arraySlice = array.slice(from, to);
|
let arraySlice = array.slice(from, to);
|
||||||
|
|
||||||
if (isNaN(step))
|
if (isNaN(step))
|
||||||
|
{
|
||||||
return arraySlice;
|
return arraySlice;
|
||||||
|
}
|
||||||
|
|
||||||
if (step < 0)
|
if (step < 0)
|
||||||
|
{
|
||||||
arraySlice.reverse();
|
arraySlice.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
step = Math.abs(step);
|
step = Math.abs(step);
|
||||||
if (step == 1)
|
if (step == 1)
|
||||||
|
{
|
||||||
return arraySlice;
|
return arraySlice;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
return arraySlice.filter( (e,i) => (i % step == 0) );
|
{
|
||||||
|
return arraySlice.filter((e, i) => (i % step == 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -735,11 +886,15 @@ export function sliceArray(array, from = NaN, to = NaN, step = NaN)
|
|||||||
* @param {*} data - the data
|
* @param {*} data - the data
|
||||||
* @param {string} type - the MIME type of the data, e.g. 'text/csv' or 'application/json'
|
* @param {string} type - the MIME type of the data, e.g. 'text/csv' or 'application/json'
|
||||||
*/
|
*/
|
||||||
export function offerDataForDownload(filename, data, type) {
|
export function offerDataForDownload(filename, data, type)
|
||||||
const blob = new Blob([data], { type });
|
{
|
||||||
|
const blob = new Blob([data], {type});
|
||||||
if (window.navigator.msSaveOrOpenBlob)
|
if (window.navigator.msSaveOrOpenBlob)
|
||||||
|
{
|
||||||
window.navigator.msSaveBlob(blob, filename);
|
window.navigator.msSaveBlob(blob, filename);
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
let elem = window.document.createElement('a');
|
let elem = window.document.createElement('a');
|
||||||
elem.href = window.URL.createObjectURL(blob);
|
elem.href = window.URL.createObjectURL(blob);
|
||||||
elem.download = filename;
|
elem.download = filename;
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* Image Stimulus.
|
* Image Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { VisualStim } from './VisualStim';
|
import {VisualStim} from './VisualStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { ColorMixin } from '../util/ColorMixin';
|
import {ColorMixin} from '../util/ColorMixin';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Stimulus.
|
* Image Stimulus.
|
||||||
*
|
*
|
||||||
* @name module:visual.ImageStim
|
* @name module:visual.ImageStim
|
||||||
* @class
|
* @class
|
||||||
* @extends VisualStim
|
* @extends VisualStim
|
||||||
@ -25,7 +25,7 @@ import * as util from '../util/Util';
|
|||||||
* @param {String} options.name - the name used when logging messages from this stimulus
|
* @param {String} options.name - the name used when logging messages from this stimulus
|
||||||
* @param {Window} options.win - the associated Window
|
* @param {Window} options.win - the associated Window
|
||||||
* @param {string | HTMLImageElement} options.image - the name of the image resource or the HTMLImageElement corresponding to the image
|
* @param {string | HTMLImageElement} options.image - the name of the image resource or the HTMLImageElement corresponding to the image
|
||||||
* @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask
|
* @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask
|
||||||
* @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
|
* @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
|
||||||
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
|
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
|
||||||
* @param {string} [options.units= 'norm'] - the units of the stimulus vertices, size and position
|
* @param {string} [options.units= 'norm'] - the units of the stimulus vertices, size and position
|
||||||
@ -39,66 +39,79 @@ import * as util from '../util/Util';
|
|||||||
* @param {boolean} [options.interpolate= false] - whether or not the image is interpolated
|
* @param {boolean} [options.interpolate= false] - whether or not the image is interpolated
|
||||||
* @param {boolean} [options.flipHoriz= false] - whether or not to flip horizontally
|
* @param {boolean} [options.flipHoriz= false] - whether or not to flip horizontally
|
||||||
* @param {boolean} [options.flipVert= false] - whether or not to flip vertically
|
* @param {boolean} [options.flipVert= false] - whether or not to flip vertically
|
||||||
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||||
{
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
image,
|
image,
|
||||||
mask,
|
mask,
|
||||||
pos,
|
pos,
|
||||||
units,
|
units,
|
||||||
ori,
|
ori,
|
||||||
size,
|
size,
|
||||||
color = new Color('white'),
|
color = new Color('white'),
|
||||||
opacity = 1.0,
|
opacity = 1.0,
|
||||||
contrast = 1.0,
|
contrast = 1.0,
|
||||||
texRes = 128,
|
texRes = 128,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
interpolate = false,
|
interpolate = false,
|
||||||
flipHoriz = false,
|
flipHoriz = false,
|
||||||
flipVert = false,
|
flipVert = false,
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {}) {
|
} = {})
|
||||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
{
|
||||||
|
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||||
|
|
||||||
this.psychoJS.logger.debug('create a new ImageStim with name: ', name);
|
this.psychoJS.logger.debug('create a new ImageStim with name: ', name);
|
||||||
|
|
||||||
this._addAttributes(ImageStim, image, mask, color, contrast, texRes, interpolate, depth, flipHoriz, flipVert);
|
this._addAttributes(ImageStim, image, mask, color, contrast, texRes, interpolate, depth, flipHoriz, flipVert);
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the image attribute.
|
* Setter for the image attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.ImageStim#setImage
|
* @name module:visual.ImageStim#setImage
|
||||||
* @public
|
* @public
|
||||||
* @param {HTMLImageElement | string} image - the name of the image resource or HTMLImageElement corresponding to the image
|
* @param {HTMLImageElement | string} image - the name of the image resource or HTMLImageElement corresponding to the image
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setImage(image, log = false) {
|
setImage(image, log = false)
|
||||||
const response = { origin: 'ImageStim.setImage', context: 'when setting the image of ImageStim: ' + this._name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ImageStim.setImage',
|
||||||
|
context: 'when setting the image of ImageStim: ' + this._name
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// image is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
|
// image is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
|
||||||
if (typeof image === 'undefined') {
|
if (typeof image === 'undefined')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('setting the image of ImageStim: ' + this._name + ' with argument: undefined.');
|
this.psychoJS.logger.warn('setting the image of ImageStim: ' + this._name + ' with argument: undefined.');
|
||||||
this.psychoJS.logger.debug('set the image of ImageStim: ' + this._name + ' as: undefined');
|
this.psychoJS.logger.debug('set the image of ImageStim: ' + this._name + ' as: undefined');
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// image is a string: it should be the name of a resource, which we load
|
// image is a string: it should be the name of a resource, which we load
|
||||||
if (typeof image === 'string')
|
if (typeof image === 'string')
|
||||||
|
{
|
||||||
image = this.psychoJS.serverManager.getResource(image);
|
image = this.psychoJS.serverManager.getResource(image);
|
||||||
|
}
|
||||||
|
|
||||||
// image should now be an actual HTMLImageElement: we raise an error if it is not
|
// image should now be an actual HTMLImageElement: we raise an error if it is not
|
||||||
if (!(image instanceof HTMLImageElement)) {
|
if (!(image instanceof HTMLImageElement))
|
||||||
|
{
|
||||||
throw 'the argument: ' + image.toString() + ' is not an image" }';
|
throw 'the argument: ' + image.toString() + ' is not an image" }';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,36 +122,47 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the mask attribute.
|
* Setter for the mask attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.ImageStim#setImage
|
* @name module:visual.ImageStim#setImage
|
||||||
* @public
|
* @public
|
||||||
* @param {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask
|
* @param {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setMask(mask, log = false) {
|
setMask(mask, log = false)
|
||||||
const response = { origin: 'ImageStim.setMask', context: 'when setting the mask of ImageStim: ' + this._name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ImageStim.setMask',
|
||||||
|
context: 'when setting the mask of ImageStim: ' + this._name
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// mask is undefined: that's fine but we raise a warning in case this is a sympton of an actual problem
|
// mask is undefined: that's fine but we raise a warning in case this is a sympton of an actual problem
|
||||||
if (typeof mask === 'undefined') {
|
if (typeof mask === 'undefined')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('setting the mask of ImageStim: ' + this._name + ' with argument: undefined.');
|
this.psychoJS.logger.warn('setting the mask of ImageStim: ' + this._name + ' with argument: undefined.');
|
||||||
this.psychoJS.logger.debug('set the mask of ImageStim: ' + this._name + ' as: undefined');
|
this.psychoJS.logger.debug('set the mask of ImageStim: ' + this._name + ' as: undefined');
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// mask is a string: it should be the name of a resource, which we load
|
// mask is a string: it should be the name of a resource, which we load
|
||||||
if (typeof mask === 'string')
|
if (typeof mask === 'string')
|
||||||
|
{
|
||||||
mask = this.psychoJS.serverManager.getResource(mask);
|
mask = this.psychoJS.serverManager.getResource(mask);
|
||||||
|
}
|
||||||
|
|
||||||
// mask should now be an actual HTMLImageElement: we raise an error if it is not
|
// mask should now be an actual HTMLImageElement: we raise an error if it is not
|
||||||
if (!(mask instanceof HTMLImageElement)) {
|
if (!(mask instanceof HTMLImageElement))
|
||||||
|
{
|
||||||
throw 'the argument: ' + mask.toString() + ' is not an image" }';
|
throw 'the argument: ' + mask.toString() + ' is not an image" }';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,21 +173,23 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the flipVert attribute.
|
* Setter for the flipVert attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.ImageStim#setFlipVert
|
* @name module:visual.ImageStim#setFlipVert
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} flipVert - whether or not to flip vertically
|
* @param {boolean} flipVert - whether or not to flip vertically
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipVert(flipVert, log = false) {
|
setFlipVert(flipVert, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('flipVert', flipVert, log);
|
this._setAttribute('flipVert', flipVert, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -172,13 +198,14 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the flipHoriz attribute.
|
* Setter for the flipHoriz attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.ImageStim#setFlipHoriz
|
* @name module:visual.ImageStim#setFlipHoriz
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipHoriz(flipHoriz, log = false) {
|
setFlipHoriz(flipHoriz, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -187,7 +214,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the given object is inside this image.
|
* Determine whether the given object is inside this image.
|
||||||
*
|
*
|
||||||
* @name module:visual.ImageStim#contains
|
* @name module:visual.ImageStim#contains
|
||||||
* @public
|
* @public
|
||||||
* @param {Object} object - the object
|
* @param {Object} object - the object
|
||||||
@ -197,12 +224,20 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
contains(object, units)
|
contains(object, units)
|
||||||
{
|
{
|
||||||
if (typeof this._image === 'undefined')
|
if (typeof this._image === 'undefined')
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// get position of object:
|
// get position of object:
|
||||||
let objectPos_px = util.getPositionFromObject(object, units);
|
let objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined')
|
if (typeof objectPos_px === 'undefined')
|
||||||
throw { origin : 'ImageStim.contains', context : 'when determining whether ImageStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'ImageStim.contains',
|
||||||
|
context: 'when determining whether ImageStim: ' + this._name + ' contains object: ' + util.toString(object),
|
||||||
|
error: 'unable to determine the position of the object'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// test for inclusion:
|
// test for inclusion:
|
||||||
// note: since _pixi.anchor is [0.5, 0.5] the image is actually centered on pos
|
// note: since _pixi.anchor is [0.5, 0.5] the image is actually centered on pos
|
||||||
@ -221,20 +256,25 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the stimulus, if necessary.
|
* Update the stimulus, if necessary.
|
||||||
*
|
*
|
||||||
* @name module:visual.ImageStim#_updateIfNeeded
|
* @name module:visual.ImageStim#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded() {
|
_updateIfNeeded()
|
||||||
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._pixi = undefined;
|
this._pixi = undefined;
|
||||||
|
|
||||||
// no image to draw: return immediately
|
// no image to draw: return immediately
|
||||||
if (typeof this._image === 'undefined')
|
if (typeof this._image === 'undefined')
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// prepare the image:
|
// prepare the image:
|
||||||
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image));
|
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image));
|
||||||
@ -243,7 +283,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._pixi.zOrder = this.depth;
|
this._pixi.zOrder = this.depth;
|
||||||
|
|
||||||
// add a mask if need be:
|
// add a mask if need be:
|
||||||
if (typeof this._mask !== 'undefined') {
|
if (typeof this._mask !== 'undefined')
|
||||||
|
{
|
||||||
this._maskTexture = new PIXI.Texture(new PIXI.BaseTexture(this._mask));
|
this._maskTexture = new PIXI.Texture(new PIXI.BaseTexture(this._mask));
|
||||||
this._pixi.mask = new PIXI.Sprite(this._maskTexture); //PIXI.Sprite.fromImage(this._mask);
|
this._pixi.mask = new PIXI.Sprite(this._maskTexture); //PIXI.Sprite.fromImage(this._mask);
|
||||||
|
|
||||||
@ -255,7 +296,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// since _texture.width may not be immediately available but the rest of the code needs its value
|
// since _texture.width may not be immediately available but the rest of the code needs its value
|
||||||
// we arrange for repeated calls to _updateIfNeeded until we have a width:
|
// we arrange for repeated calls to _updateIfNeeded until we have a width:
|
||||||
if (this._texture.width === 0) {
|
if (this._texture.width === 0)
|
||||||
|
{
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -284,7 +326,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
|
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
|
||||||
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
|
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
|
||||||
|
|
||||||
// set the position, rotation, and anchor (image centered on pos):
|
// set the position, rotation, and anchor (image centered on pos):
|
||||||
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
|
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
|
||||||
this._pixi.rotation = this.ori * Math.PI / 180;
|
this._pixi.rotation = this.ori * Math.PI / 180;
|
||||||
this._pixi.anchor.x = 0.5;
|
this._pixi.anchor.x = 0.5;
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Movie Stimulus.
|
* Movie Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { VisualStim } from './VisualStim';
|
import {VisualStim} from './VisualStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { ColorMixin } from '../util/ColorMixin';
|
import {ColorMixin} from '../util/ColorMixin';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
import { PsychoJS } from "../core/PsychoJS";
|
import {PsychoJS} from "../core/PsychoJS";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,29 +45,31 @@ import { PsychoJS } from "../core/PsychoJS";
|
|||||||
*
|
*
|
||||||
* @todo autoPlay does not work for the moment.
|
* @todo autoPlay does not work for the moment.
|
||||||
*/
|
*/
|
||||||
export class MovieStim extends VisualStim {
|
export class MovieStim extends VisualStim
|
||||||
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
movie,
|
movie,
|
||||||
pos,
|
pos,
|
||||||
units,
|
units,
|
||||||
ori,
|
ori,
|
||||||
size,
|
size,
|
||||||
color = new Color('white'),
|
color = new Color('white'),
|
||||||
opacity = 1.0,
|
opacity = 1.0,
|
||||||
contrast = 1.0,
|
contrast = 1.0,
|
||||||
interpolate = false,
|
interpolate = false,
|
||||||
flipHoriz = false,
|
flipHoriz = false,
|
||||||
flipVert = false,
|
flipVert = false,
|
||||||
loop = false,
|
loop = false,
|
||||||
volume = 1.0,
|
volume = 1.0,
|
||||||
noAudio = false,
|
noAudio = false,
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {}) {
|
} = {})
|
||||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
{
|
||||||
|
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||||
|
|
||||||
this.psychoJS.logger.debug('create a new MovieStim with name: ', name);
|
this.psychoJS.logger.debug('create a new MovieStim with name: ', name);
|
||||||
|
|
||||||
@ -78,35 +80,48 @@ export class MovieStim extends VisualStim {
|
|||||||
this._hasFastSeek = (typeof videoElement.fastSeek === 'function');
|
this._hasFastSeek = (typeof videoElement.fastSeek === 'function');
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the movie attribute.
|
* Setter for the movie attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.MovieStim#setMovie
|
* @name module:visual.MovieStim#setMovie
|
||||||
* @public
|
* @public
|
||||||
* @param {string | HTMLVideoElement} movie - the name of the movie resource or the HTMLVideoElement corresponding to the movie
|
* @param {string | HTMLVideoElement} movie - the name of the movie resource or the HTMLVideoElement corresponding to the movie
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setMovie(movie, log = false) {
|
setMovie(movie, log = false)
|
||||||
const response = { origin: 'MovieStim.setMovie', context: 'when setting the movie of MovieStim: ' + this._name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'MovieStim.setMovie',
|
||||||
|
context: 'when setting the movie of MovieStim: ' + this._name
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// movie is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
|
// movie is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
|
||||||
if (typeof movie === 'undefined') {
|
if (typeof movie === 'undefined')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('setting the movie of MovieStim: ' + this._name + ' with argument: undefined.');
|
this.psychoJS.logger.warn('setting the movie of MovieStim: ' + this._name + ' with argument: undefined.');
|
||||||
this.psychoJS.logger.debug('set the movie of MovieStim: ' + this._name + ' as: undefined');
|
this.psychoJS.logger.debug('set the movie of MovieStim: ' + this._name + ' as: undefined');
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// movie is a string: it should be the name of a resource, which we load
|
// movie is a string: it should be the name of a resource, which we load
|
||||||
if (typeof movie === 'string')
|
if (typeof movie === 'string')
|
||||||
|
{
|
||||||
movie = this.psychoJS.serverManager.getResource(movie);
|
movie = this.psychoJS.serverManager.getResource(movie);
|
||||||
|
}
|
||||||
|
|
||||||
// movie should now be an actual HTMLVideoElement: we raise an error if it is not
|
// movie should now be an actual HTMLVideoElement: we raise an error if it is not
|
||||||
if (!(movie instanceof HTMLVideoElement))
|
if (!(movie instanceof HTMLVideoElement))
|
||||||
|
{
|
||||||
throw 'the argument: ' + movie.toString() + ' is not a video" }';
|
throw 'the argument: ' + movie.toString() + ' is not a video" }';
|
||||||
|
}
|
||||||
|
|
||||||
this.psychoJS.logger.debug(`set the movie of MovieStim: ${this._name} as: src= ${movie.src}, size= ${movie.videoWidth}x${movie.videoHeight}, duration= ${movie.duration}s`);
|
this.psychoJS.logger.debug(`set the movie of MovieStim: ${this._name} as: src= ${movie.src}, size= ${movie.videoWidth}x${movie.videoHeight}, duration= ${movie.duration}s`);
|
||||||
}
|
}
|
||||||
@ -114,12 +129,16 @@ export class MovieStim extends VisualStim {
|
|||||||
this._setAttribute('movie', movie, log);
|
this._setAttribute('movie', movie, log);
|
||||||
|
|
||||||
// change status of stimulus when movie finish playing:
|
// change status of stimulus when movie finish playing:
|
||||||
this._movie.onended = () => { this.status = PsychoJS.Status.FINISHED; };
|
this._movie.onended = () =>
|
||||||
|
{
|
||||||
|
this.status = PsychoJS.Status.FINISHED;
|
||||||
|
};
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,19 +149,21 @@ export class MovieStim extends VisualStim {
|
|||||||
* @param {number} volume - the volume of the audio track (must be between 0.0 and 1.0)
|
* @param {number} volume - the volume of the audio track (must be between 0.0 and 1.0)
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setVolume(volume, log = false) {
|
setVolume(volume, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('volume', volume, log);
|
this._setAttribute('volume', volume, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the noAudio attribute.
|
* Setter for the noAudio attribute.
|
||||||
*
|
*
|
||||||
* @param {boolean} noAudio - whether or not to mute the audio
|
* @param {boolean} noAudio - whether or not to mute the audio
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setNoAudio(noAudio, log = false) {
|
setNoAudio(noAudio, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('noAudio', noAudio, log);
|
this._setAttribute('noAudio', noAudio, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -150,13 +171,14 @@ export class MovieStim extends VisualStim {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the flipVert attribute.
|
* Setter for the flipVert attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.MovieStim#setFlipVert
|
* @name module:visual.MovieStim#setFlipVert
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} flipVert - whether or not to flip vertically
|
* @param {boolean} flipVert - whether or not to flip vertically
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipVert(flipVert, log = false) {
|
setFlipVert(flipVert, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('flipVert', flipVert, log);
|
this._setAttribute('flipVert', flipVert, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -165,13 +187,14 @@ export class MovieStim extends VisualStim {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the flipHoriz attribute.
|
* Setter for the flipHoriz attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.MovieStim#setFlipHoriz
|
* @name module:visual.MovieStim#setFlipHoriz
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipHoriz(flipHoriz, log = false) {
|
setFlipHoriz(flipHoriz, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -179,34 +202,40 @@ export class MovieStim extends VisualStim {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the stimulus.
|
* Reset the stimulus.
|
||||||
*
|
*
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
reset(log = false) {
|
reset(log = false)
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.NOT_STARTED;
|
this.status = PsychoJS.Status.NOT_STARTED;
|
||||||
this._movie.pause();
|
this._movie.pause();
|
||||||
if (this._hasFastSeek) this._movie.fastSeek(0);
|
if (this._hasFastSeek)
|
||||||
|
{
|
||||||
|
this._movie.fastSeek(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start playing the movie.
|
* Start playing the movie.
|
||||||
*
|
*
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
play(log = false) {
|
play(log = false)
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.STARTED;
|
this.status = PsychoJS.Status.STARTED;
|
||||||
this._movie.play();
|
this._movie.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pause the movie.
|
* Pause the movie.
|
||||||
*
|
*
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
pause(log = false) {
|
pause(log = false)
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.STOPPED;
|
this.status = PsychoJS.Status.STOPPED;
|
||||||
this._movie.pause();
|
this._movie.pause();
|
||||||
}
|
}
|
||||||
@ -217,14 +246,17 @@ export class MovieStim extends VisualStim {
|
|||||||
*
|
*
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
stop(log = false) {
|
stop(log = false)
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.STOPPED;
|
this.status = PsychoJS.Status.STOPPED;
|
||||||
this._movie.pause();
|
this._movie.pause();
|
||||||
if (this._hasFastSeek) this._movie.fastSeek(0);
|
if (this._hasFastSeek)
|
||||||
|
{
|
||||||
|
this._movie.fastSeek(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jump to a specific timepoint
|
* Jump to a specific timepoint
|
||||||
*
|
*
|
||||||
@ -233,8 +265,10 @@ export class MovieStim extends VisualStim {
|
|||||||
* @param {number} timePoint - the timepoint to which to jump (in second)
|
* @param {number} timePoint - the timepoint to which to jump (in second)
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
seek(timePoint, log = false) {
|
seek(timePoint, log = false)
|
||||||
if (timePoint < 0 || timePoint > this._movie.duration) {
|
{
|
||||||
|
if (timePoint < 0 || timePoint > this._movie.duration)
|
||||||
|
{
|
||||||
throw {
|
throw {
|
||||||
origin: 'MovieStim.seek',
|
origin: 'MovieStim.seek',
|
||||||
context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`,
|
context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`,
|
||||||
@ -243,23 +277,28 @@ export class MovieStim extends VisualStim {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (this._hasFastSeek) this._movie.fastSeek(timePoint);
|
if (this._hasFastSeek)
|
||||||
|
{
|
||||||
|
this._movie.fastSeek(timePoint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the given object is inside this movie.
|
* Determine whether the given object is inside this movie.
|
||||||
*
|
*
|
||||||
* @name module:visual.MovieStim#contains
|
* @name module:visual.MovieStim#contains
|
||||||
* @public
|
* @public
|
||||||
* @param {Object} object - the object
|
* @param {Object} object - the object
|
||||||
* @param {string} units - the units
|
* @param {string} units - the units
|
||||||
* @return {boolean} whether or not the image contains the object
|
* @return {boolean} whether or not the image contains the object
|
||||||
*/
|
*/
|
||||||
contains(object, units) {
|
contains(object, units)
|
||||||
|
{
|
||||||
// get position of object:
|
// get position of object:
|
||||||
let objectPos_px = util.getPositionFromObject(object, units);
|
let objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined') {
|
if (typeof objectPos_px === 'undefined')
|
||||||
|
{
|
||||||
throw {
|
throw {
|
||||||
origin: 'MovieStim.contains',
|
origin: 'MovieStim.contains',
|
||||||
context: `when determining whether MovieStim: ${this._name} contains object: ${util.toString(object)}`,
|
context: `when determining whether MovieStim: ${this._name} contains object: ${util.toString(object)}`,
|
||||||
@ -283,20 +322,25 @@ export class MovieStim extends VisualStim {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the stimulus, if necessary.
|
* Update the stimulus, if necessary.
|
||||||
*
|
*
|
||||||
* @name module:visual.MovieStim#_updateIfNeeded
|
* @name module:visual.MovieStim#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded() {
|
_updateIfNeeded()
|
||||||
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._pixi = undefined;
|
this._pixi = undefined;
|
||||||
|
|
||||||
// no movie to draw: return immediately
|
// no movie to draw: return immediately
|
||||||
if (typeof this._movie === 'undefined')
|
if (typeof this._movie === 'undefined')
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// create a PixiJS video sprite:
|
// create a PixiJS video sprite:
|
||||||
this._texture = PIXI.Texture.fromVideo(this._movie);
|
this._texture = PIXI.Texture.fromVideo(this._movie);
|
||||||
@ -305,7 +349,8 @@ export class MovieStim extends VisualStim {
|
|||||||
|
|
||||||
// since _texture.width may not be immedialy available but the rest of the code needs its value
|
// since _texture.width may not be immedialy available but the rest of the code needs its value
|
||||||
// we arrange for repeated calls to _updateIfNeeded until we have a width:
|
// we arrange for repeated calls to _updateIfNeeded until we have a width:
|
||||||
if (this._texture.width === 0) {
|
if (this._texture.width === 0)
|
||||||
|
{
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -325,7 +370,8 @@ export class MovieStim extends VisualStim {
|
|||||||
// stimulus size:
|
// stimulus size:
|
||||||
// note: we use the size of the texture if MovieStim has no specified size:
|
// note: we use the size of the texture if MovieStim has no specified size:
|
||||||
let stimSize = this.size;
|
let stimSize = this.size;
|
||||||
if (typeof stimSize === 'undefined') {
|
if (typeof stimSize === 'undefined')
|
||||||
|
{
|
||||||
const textureSize = [this._texture.width, this._texture.height];
|
const textureSize = [this._texture.width, this._texture.height];
|
||||||
stimSize = util.to_unit(textureSize, 'pix', this.win, this.units);
|
stimSize = util.to_unit(textureSize, 'pix', this.win, this.units);
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
* Polygonal Stimulus.
|
* Polygonal Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { ShapeStim } from './ShapeStim';
|
import {ShapeStim} from './ShapeStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,7 +59,23 @@ export class Polygon extends ShapeStim
|
|||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({ name, win, lineWidth, lineColor, fillColor, opacity, pos, ori, size, units, contrast, depth, interpolate, autoDraw, autoLog });
|
super({
|
||||||
|
name,
|
||||||
|
win,
|
||||||
|
lineWidth,
|
||||||
|
lineColor,
|
||||||
|
fillColor,
|
||||||
|
opacity,
|
||||||
|
pos,
|
||||||
|
ori,
|
||||||
|
size,
|
||||||
|
units,
|
||||||
|
contrast,
|
||||||
|
depth,
|
||||||
|
interpolate,
|
||||||
|
autoDraw,
|
||||||
|
autoLog
|
||||||
|
});
|
||||||
|
|
||||||
this._psychoJS.logger.debug('create a new Polygon with name: ', name);
|
this._psychoJS.logger.debug('create a new Polygon with name: ', name);
|
||||||
|
|
||||||
@ -68,11 +84,12 @@ export class Polygon extends ShapeStim
|
|||||||
this._updateVertices();
|
this._updateVertices();
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the radius attribute.
|
* Setter for the radius attribute.
|
||||||
*
|
*
|
||||||
@ -90,7 +107,6 @@ export class Polygon extends ShapeStim
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the edges attribute.
|
* Setter for the edges attribute.
|
||||||
*
|
*
|
||||||
@ -121,7 +137,9 @@ export class Polygon extends ShapeStim
|
|||||||
const angle = 2.0 * Math.PI / this._edges;
|
const angle = 2.0 * Math.PI / this._edges;
|
||||||
const vertices = [];
|
const vertices = [];
|
||||||
for (let v = 0; v < this._edges; ++v)
|
for (let v = 0; v < this._edges; ++v)
|
||||||
vertices.push([ Math.sin(v * angle) * this._radius, Math.cos(v * angle) * this._radius ]);
|
{
|
||||||
|
vertices.push([Math.sin(v * angle) * this._radius, Math.cos(v * angle) * this._radius]);
|
||||||
|
}
|
||||||
this.setVertices(vertices);
|
this.setVertices(vertices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,19 +2,19 @@
|
|||||||
* Rectangular Stimulus.
|
* Rectangular Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { ShapeStim } from './ShapeStim';
|
import {ShapeStim} from './ShapeStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Rectangular visual stimulus.</p>
|
* <p>Rectangular visual stimulus.</p>
|
||||||
*
|
*
|
||||||
* @name module:visual.Rect
|
* @name module:visual.Rect
|
||||||
* @class
|
* @class
|
||||||
* @extends ShapeStim
|
* @extends ShapeStim
|
||||||
@ -22,7 +22,7 @@ import { Color } from '../util/Color';
|
|||||||
* @param {String} options.name - the name used when logging messages from this stimulus
|
* @param {String} options.name - the name used when logging messages from this stimulus
|
||||||
* @param {Window} options.win - the associated Window
|
* @param {Window} options.win - the associated Window
|
||||||
* @param {number} [options.lineWidth= 1.5] - the line width
|
* @param {number} [options.lineWidth= 1.5] - the line width
|
||||||
* @param {Color} [options.lineColor= Color('white')] the line color
|
* @param {Color} [options.lineColor= Color('white')] the line color
|
||||||
* @param {Color} options.fillColor - the fill color
|
* @param {Color} options.fillColor - the fill color
|
||||||
* @param {number} [options.opacity= 1.0] - the opacity
|
* @param {number} [options.opacity= 1.0] - the opacity
|
||||||
* @param {number} [options.width= 0.5] - the width of the rectangle
|
* @param {number} [options.width= 0.5] - the width of the rectangle
|
||||||
@ -34,32 +34,48 @@ import { Color } from '../util/Color';
|
|||||||
* @param {number} [options.contrast= 1.0] - the contrast
|
* @param {number} [options.contrast= 1.0] - the contrast
|
||||||
* @param {number} [options.depth= 0] - the depth
|
* @param {number} [options.depth= 0] - the depth
|
||||||
* @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated
|
* @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated
|
||||||
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class Rect extends ShapeStim
|
export class Rect extends ShapeStim
|
||||||
{
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
lineWidth = 1.5,
|
lineWidth = 1.5,
|
||||||
lineColor = new Color('white'),
|
lineColor = new Color('white'),
|
||||||
fillColor,
|
fillColor,
|
||||||
opacity = 1.0,
|
opacity = 1.0,
|
||||||
width = 0.5,
|
width = 0.5,
|
||||||
height = 0.5,
|
height = 0.5,
|
||||||
pos = [0, 0],
|
pos = [0, 0],
|
||||||
size = 1.0,
|
size = 1.0,
|
||||||
ori = 0.0,
|
ori = 0.0,
|
||||||
units,
|
units,
|
||||||
contrast = 1.0,
|
contrast = 1.0,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
interpolate = true,
|
interpolate = true,
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({ name, win, lineWidth, lineColor, fillColor, opacity, pos, ori, size, units, contrast, depth, interpolate, autoDraw, autoLog });
|
super({
|
||||||
|
name,
|
||||||
|
win,
|
||||||
|
lineWidth,
|
||||||
|
lineColor,
|
||||||
|
fillColor,
|
||||||
|
opacity,
|
||||||
|
pos,
|
||||||
|
ori,
|
||||||
|
size,
|
||||||
|
units,
|
||||||
|
contrast,
|
||||||
|
depth,
|
||||||
|
interpolate,
|
||||||
|
autoDraw,
|
||||||
|
autoLog
|
||||||
|
});
|
||||||
|
|
||||||
this._psychoJS.logger.debug('create a new Rect with name: ', name);
|
this._psychoJS.logger.debug('create a new Rect with name: ', name);
|
||||||
|
|
||||||
@ -68,16 +84,17 @@ export class Rect extends ShapeStim
|
|||||||
this._updateVertices();
|
this._updateVertices();
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the width attribute.
|
* Setter for the width attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.Rect#setWidth
|
* @name module:visual.Rect#setWidth
|
||||||
* @public
|
* @public
|
||||||
* @param {number} width - the rectange width
|
* @param {number} width - the rectange width
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
@ -90,12 +107,11 @@ export class Rect extends ShapeStim
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the height attribute.
|
* Setter for the height attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.Rect#setHeight
|
* @name module:visual.Rect#setHeight
|
||||||
* @public
|
* @public
|
||||||
* @param {number} height - the rectange height
|
* @param {number} height - the rectange height
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
@ -110,9 +126,9 @@ export class Rect extends ShapeStim
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the vertices.
|
* Update the vertices.
|
||||||
*
|
*
|
||||||
* @name module:visual.Rect#_updateVertices
|
* @name module:visual.Rect#_updateVertices
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateVertices()
|
_updateVertices()
|
||||||
{
|
{
|
||||||
|
@ -3,21 +3,21 @@
|
|||||||
* Basic Shape Stimulus.
|
* Basic Shape Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { VisualStim } from './VisualStim';
|
import {VisualStim} from './VisualStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { ColorMixin } from '../util/ColorMixin';
|
import {ColorMixin} from '../util/ColorMixin';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This class provides the basic functionalities of shape stimuli.</p>
|
* <p>This class provides the basic functionalities of shape stimuli.</p>
|
||||||
*
|
*
|
||||||
* @class
|
* @class
|
||||||
* @extends VisualStim
|
* @extends VisualStim
|
||||||
* @mixes ColorMixin
|
* @mixes ColorMixin
|
||||||
@ -25,7 +25,7 @@ import * as util from '../util/Util';
|
|||||||
* @param {String} options.name - the name used when logging messages from this stimulus
|
* @param {String} options.name - the name used when logging messages from this stimulus
|
||||||
* @param {Window} options.win - the associated Window
|
* @param {Window} options.win - the associated Window
|
||||||
* @param {number} options.lineWidth - the line width
|
* @param {number} options.lineWidth - the line width
|
||||||
* @param {Color} [options.lineColor= Color('white')] the line color
|
* @param {Color} [options.lineColor= Color('white')] the line color
|
||||||
* @param {Color} options.fillColor - the fill color
|
* @param {Color} options.fillColor - the fill color
|
||||||
* @param {number} [options.opacity= 1.0] - the opacity
|
* @param {number} [options.opacity= 1.0] - the opacity
|
||||||
* @param {Array.<Array.<number>>} [options.vertices= [[-0.5, 0], [0, 0.5], [0.5, 0]]] - the shape vertices
|
* @param {Array.<Array.<number>>} [options.vertices= [[-0.5, 0], [0, 0.5], [0.5, 0]]] - the shape vertices
|
||||||
@ -37,32 +37,32 @@ import * as util from '../util/Util';
|
|||||||
* @param {number} [options.contrast= 1.0] - the contrast
|
* @param {number} [options.contrast= 1.0] - the contrast
|
||||||
* @param {number} [options.depth= 0] - the depth
|
* @param {number} [options.depth= 0] - the depth
|
||||||
* @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated
|
* @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated
|
||||||
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||||
{
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
lineWidth = 1.5,
|
lineWidth = 1.5,
|
||||||
lineColor = new Color('white'),
|
lineColor = new Color('white'),
|
||||||
fillColor,
|
fillColor,
|
||||||
opacity = 1.0,
|
opacity = 1.0,
|
||||||
vertices = [[-0.5, 0], [0, 0.5], [0.5, 0]],
|
vertices = [[-0.5, 0], [0, 0.5], [0.5, 0]],
|
||||||
closeShape = true,
|
closeShape = true,
|
||||||
pos = [0, 0],
|
pos = [0, 0],
|
||||||
size = 1.0,
|
size = 1.0,
|
||||||
ori = 0.0,
|
ori = 0.0,
|
||||||
units,
|
units,
|
||||||
contrast = 1.0,
|
contrast = 1.0,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
interpolate = true,
|
interpolate = true,
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||||
|
|
||||||
// the PIXI polygon corresponding to the vertices, in pixel units:
|
// the PIXI polygon corresponding to the vertices, in pixel units:
|
||||||
this._pixiPolygon_px = undefined;
|
this._pixiPolygon_px = undefined;
|
||||||
@ -76,7 +76,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force a refresh of the stimulus.
|
* Force a refresh of the stimulus.
|
||||||
*
|
*
|
||||||
@ -91,7 +90,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the size attribute.
|
* Setter for the size attribute.
|
||||||
*
|
*
|
||||||
@ -108,12 +106,11 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the line width attribute.
|
* Setter for the line width attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.ShapeStim#setLineWidth
|
* @name module:visual.ShapeStim#setLineWidth
|
||||||
* @public
|
* @public
|
||||||
* @param {number} lineWidth - the line width
|
* @param {number} lineWidth - the line width
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
@ -180,9 +177,13 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
if (typeof vertices === 'string')
|
if (typeof vertices === 'string')
|
||||||
{
|
{
|
||||||
if (vertices in ShapeStim.KnownShapes)
|
if (vertices in ShapeStim.KnownShapes)
|
||||||
|
{
|
||||||
vertices = ShapeStim.KnownShapes[vertices];
|
vertices = ShapeStim.KnownShapes[vertices];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
throw 'unknown shape';
|
throw 'unknown shape';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setAttribute('vertices', vertices, log);
|
this._setAttribute('vertices', vertices, log);
|
||||||
@ -197,7 +198,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
throw Object.assign(response, { error: error });
|
throw Object.assign(response, {error: error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,13 +212,20 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {string} units - the units
|
* @param {string} units - the units
|
||||||
* @return {boolean} whether or not the stimulus contains the object
|
* @return {boolean} whether or not the stimulus contains the object
|
||||||
*/
|
*/
|
||||||
contains(object, units) {
|
contains(object, units)
|
||||||
|
{
|
||||||
this._psychoJS.logger.debug('test whether BaseShameStim:', this.name, 'contains object: ', ('name' in object) ? object.name : object);
|
this._psychoJS.logger.debug('test whether BaseShameStim:', this.name, 'contains object: ', ('name' in object) ? object.name : object);
|
||||||
|
|
||||||
// get position of object:
|
// get position of object:
|
||||||
const objectPos_px = util.getPositionFromObject(object, units);
|
const objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined')
|
if (typeof objectPos_px === 'undefined')
|
||||||
throw { origin : 'ShapeStim.contains', context : 'when determining whether BaseShameStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'ShapeStim.contains',
|
||||||
|
context: 'when determining whether BaseShameStim: ' + this._name + ' contains object: ' + util.toString(object),
|
||||||
|
error: 'unable to determine the position of the object'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// test for inclusion
|
// test for inclusion
|
||||||
// note: the vertices are centered around (0, 0) so we need to add to them the stimulus' position
|
// note: the vertices are centered around (0, 0) so we need to add to them the stimulus' position
|
||||||
@ -234,9 +242,12 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.ShapeStim#_updateIfNeeded
|
* @name module:visual.ShapeStim#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded() {
|
_updateIfNeeded()
|
||||||
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._getPolygon(/*true*/); // this also updates _vertices_px
|
this._getPolygon(/*true*/); // this also updates _vertices_px
|
||||||
@ -245,16 +256,22 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// no polygon to draw: return immediately
|
// no polygon to draw: return immediately
|
||||||
if (typeof this._pixiPolygon_px === 'undefined')
|
if (typeof this._pixiPolygon_px === 'undefined')
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// prepare the polygon in the given color and opacity:
|
// prepare the polygon in the given color and opacity:
|
||||||
this._pixi = new PIXI.Graphics();
|
this._pixi = new PIXI.Graphics();
|
||||||
this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5);
|
this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5);
|
||||||
if (typeof this._fillColor !== 'undefined')
|
if (typeof this._fillColor !== 'undefined')
|
||||||
|
{
|
||||||
this._pixi.beginFill(this._fillColor.int, this._opacity);
|
this._pixi.beginFill(this._fillColor.int, this._opacity);
|
||||||
|
}
|
||||||
this._pixi.drawPolygon(this._pixiPolygon_px);
|
this._pixi.drawPolygon(this._pixiPolygon_px);
|
||||||
if (typeof this._fillColor !== 'undefined')
|
if (typeof this._fillColor !== 'undefined')
|
||||||
|
{
|
||||||
this._pixi.endFill();
|
this._pixi.endFill();
|
||||||
|
}
|
||||||
|
|
||||||
// set polygon position and rotation:
|
// set polygon position and rotation:
|
||||||
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
|
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
|
||||||
@ -262,7 +279,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the PIXI polygon (in pixel units) corresponding to the vertices.
|
* Get the PIXI polygon (in pixel units) corresponding to the vertices.
|
||||||
*
|
*
|
||||||
@ -270,9 +286,12 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @private
|
* @private
|
||||||
* @return {Object} the PIXI polygon corresponding to this stimulus vertices.
|
* @return {Object} the PIXI polygon corresponding to this stimulus vertices.
|
||||||
*/
|
*/
|
||||||
_getPolygon(/*force = false*/) {
|
_getPolygon(/*force = false*/)
|
||||||
|
{
|
||||||
if (!this._needVertexUpdate)
|
if (!this._needVertexUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needVertexUpdate = false;
|
this._needVertexUpdate = false;
|
||||||
|
|
||||||
console.log('>>>>>>>>> CREATING PIXI POLYGON!!!!');
|
console.log('>>>>>>>>> CREATING PIXI POLYGON!!!!');
|
||||||
@ -285,13 +304,17 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._getVertices_px(/*force*/);
|
this._getVertices_px(/*force*/);
|
||||||
let coords_px = [];
|
let coords_px = [];
|
||||||
for (const vertex_px of this._vertices_px)
|
for (const vertex_px of this._vertices_px)
|
||||||
|
{
|
||||||
coords_px.push.apply(coords_px, vertex_px);
|
coords_px.push.apply(coords_px, vertex_px);
|
||||||
|
}
|
||||||
|
|
||||||
// close the polygon if need be:
|
// close the polygon if need be:
|
||||||
if (coords_px.length >= 6 && this._closeShape) {
|
if (coords_px.length >= 6 && this._closeShape)
|
||||||
|
{
|
||||||
// note: we first check whether the vertices already define a closed polygon:
|
// note: we first check whether the vertices already define a closed polygon:
|
||||||
const n = coords_px.length;
|
const n = coords_px.length;
|
||||||
if (coords_px[0] !== coords_px[n - 2] || coords_px[1] !== coords_px[n - 1]) {
|
if (coords_px[0] !== coords_px[n - 2] || coords_px[1] !== coords_px[n - 1])
|
||||||
|
{
|
||||||
coords_px.push(coords_px[0]);
|
coords_px.push(coords_px[0]);
|
||||||
coords_px.push(coords_px[1]);
|
coords_px.push(coords_px[1]);
|
||||||
}
|
}
|
||||||
@ -303,7 +326,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the vertices in pixel units.
|
* Get the vertices in pixel units.
|
||||||
*
|
*
|
||||||
@ -319,12 +341,16 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
// handle flipping:
|
// handle flipping:
|
||||||
let flip = [1.0, 1.0];
|
let flip = [1.0, 1.0];
|
||||||
if ('_flipHoriz' in this && this._flipHoriz)
|
if ('_flipHoriz' in this && this._flipHoriz)
|
||||||
|
{
|
||||||
flip[0] = -1.0;
|
flip[0] = -1.0;
|
||||||
|
}
|
||||||
if ('_flipVert' in this && this._flipVert)
|
if ('_flipVert' in this && this._flipVert)
|
||||||
|
{
|
||||||
flip[1] = -1.0;
|
flip[1] = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
// handle size, flipping, and convert to pixel units:
|
// handle size, flipping, and convert to pixel units:
|
||||||
this._vertices_px = this._vertices.map( v => util.to_px([v[0] * this._size[0] * flip[0], v[1] * this._size[1] * flip[1]], this._units, this._win) );
|
this._vertices_px = this._vertices.map(v => util.to_px([v[0] * this._size[0] * flip[0], v[1] * this._size[1] * flip[1]], this._units, this._win));
|
||||||
|
|
||||||
return this._vertices_px;
|
return this._vertices_px;
|
||||||
}
|
}
|
||||||
@ -355,20 +381,20 @@ ShapeStim.KnownShapes = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
star7: [
|
star7: [
|
||||||
[0.0,0.5],
|
[0.0, 0.5],
|
||||||
[0.09,0.18],
|
[0.09, 0.18],
|
||||||
[0.39,0.31],
|
[0.39, 0.31],
|
||||||
[0.19,0.04],
|
[0.19, 0.04],
|
||||||
[0.49,-0.11],
|
[0.49, -0.11],
|
||||||
[0.16,-0.12],
|
[0.16, -0.12],
|
||||||
[0.22,-0.45],
|
[0.22, -0.45],
|
||||||
[0.0,-0.2],
|
[0.0, -0.2],
|
||||||
[-0.22,-0.45],
|
[-0.22, -0.45],
|
||||||
[-0.16,-0.12],
|
[-0.16, -0.12],
|
||||||
[-0.49,-0.11],
|
[-0.49, -0.11],
|
||||||
[-0.19,0.04],
|
[-0.19, 0.04],
|
||||||
[-0.39,0.31],
|
[-0.39, 0.31],
|
||||||
[-0.09,0.18]
|
[-0.09, 0.18]
|
||||||
]
|
]
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Slider Stimulus.
|
* Slider Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -62,34 +62,35 @@ import {PsychoJS} from "../core/PsychoJS";
|
|||||||
export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||||
{
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
pos,
|
pos,
|
||||||
size,
|
size,
|
||||||
ori,
|
ori,
|
||||||
units = 'height',
|
units = 'height',
|
||||||
|
|
||||||
color = new Color('LightGray'),
|
color = new Color('LightGray'),
|
||||||
contrast = 1.0,
|
contrast = 1.0,
|
||||||
opacity,
|
opacity,
|
||||||
|
|
||||||
style = [Slider.Style.RATING],
|
style = [Slider.Style.RATING],
|
||||||
ticks = [1,2,3,4,5],
|
ticks = [1, 2, 3, 4, 5],
|
||||||
labels = [],
|
labels = [],
|
||||||
labelHeight,
|
labelHeight,
|
||||||
granularity = 0,
|
granularity = 0,
|
||||||
flip = false,
|
flip = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
|
||||||
fontFamily = 'Helvetica',
|
fontFamily = 'Helvetica',
|
||||||
bold = true,
|
bold = true,
|
||||||
italic = false,
|
italic = false,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {}) {
|
} = {})
|
||||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
{
|
||||||
|
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||||
|
|
||||||
this._needMarkerUpdate = false;
|
this._needMarkerUpdate = false;
|
||||||
|
|
||||||
@ -113,7 +114,9 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._isCategorical = (this._ticks.length === 0);
|
this._isCategorical = (this._ticks.length === 0);
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -128,12 +131,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
*
|
*
|
||||||
* @todo this is currently not implemented and always returns false
|
* @todo this is currently not implemented and always returns false
|
||||||
*/
|
*/
|
||||||
contains(object, units) {
|
contains(object, units)
|
||||||
|
{
|
||||||
// get position of object:
|
// get position of object:
|
||||||
let objectPos_px = util.getPositionFromObject(object, units);
|
let objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined')
|
if (typeof objectPos_px === 'undefined')
|
||||||
throw { origin : 'Slider.contains', context : `when determining whether Slider: ${this._name} contains
|
{
|
||||||
object: ${util.toString(object)}`, error : 'unable to determine the position of the object' };
|
throw {
|
||||||
|
origin: 'Slider.contains', context: `when determining whether Slider: ${this._name} contains
|
||||||
|
object: ${util.toString(object)}`, error: 'unable to determine the position of the object'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -145,7 +153,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#reset
|
* @name module:visual.Slider#reset
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset()
|
||||||
|
{
|
||||||
this.psychoJS.logger.debug('reset Slider: ', this._name);
|
this.psychoJS.logger.debug('reset Slider: ', this._name);
|
||||||
|
|
||||||
this._markerPos = undefined;
|
this._markerPos = undefined;
|
||||||
@ -159,7 +168,9 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// the marker should be invisible when markerPos is undefined:
|
// the marker should be invisible when markerPos is undefined:
|
||||||
if (typeof this._marker !== 'undefined')
|
if (typeof this._marker !== 'undefined')
|
||||||
|
{
|
||||||
this._marker.alpha = 0;
|
this._marker.alpha = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -170,12 +181,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @public
|
* @public
|
||||||
* @returns {number | undefined} the rating or undefined if there is none
|
* @returns {number | undefined} the rating or undefined if there is none
|
||||||
*/
|
*/
|
||||||
getRating() {
|
getRating()
|
||||||
|
{
|
||||||
const historyLength = this._history.length;
|
const historyLength = this._history.length;
|
||||||
if (historyLength > 0)
|
if (historyLength > 0)
|
||||||
return this._history[historyLength-1]['rating'];
|
{
|
||||||
|
return this._history[historyLength - 1]['rating'];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -186,12 +202,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @public
|
* @public
|
||||||
* @returns {number | undefined} the response time (in second) or undefined if there is none
|
* @returns {number | undefined} the response time (in second) or undefined if there is none
|
||||||
*/
|
*/
|
||||||
getRT() {
|
getRT()
|
||||||
|
{
|
||||||
const historyLength = this._history.length;
|
const historyLength = this._history.length;
|
||||||
if (historyLength > 0)
|
if (historyLength > 0)
|
||||||
return this._history[historyLength-1]['responseTime'];
|
{
|
||||||
|
return this._history[historyLength - 1]['responseTime'];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -205,14 +226,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} [fontSize] - the font size
|
* @param {number} [fontSize] - the font size
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFontSize(fontSize, log = false) {
|
setFontSize(fontSize, log = false)
|
||||||
if (typeof fontSize === 'undefined') {
|
{
|
||||||
|
if (typeof fontSize === 'undefined')
|
||||||
|
{
|
||||||
fontSize = (this._units === 'pix') ? 14 : 0.03;
|
fontSize = (this._units === 'pix') ? 14 : 0.03;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanged = this._setAttribute('fontSize', fontSize, log);
|
const hasChanged = this._setAttribute('fontSize', fontSize, log);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged)
|
||||||
|
{
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
this._needVertexUpdate = true;
|
this._needVertexUpdate = true;
|
||||||
}
|
}
|
||||||
@ -226,11 +250,13 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} [bold= true] - whether or not the font of the labels is bold
|
* @param {boolean} [bold= true] - whether or not the font of the labels is bold
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setBold(bold = true, log = false) {
|
setBold(bold = true, log = false)
|
||||||
|
{
|
||||||
const hasChanged = this._setAttribute('bold', bold, log);
|
const hasChanged = this._setAttribute('bold', bold, log);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged)
|
||||||
this._fontWeight = (bold)?'bold':'normal';
|
{
|
||||||
|
this._fontWeight = (bold) ? 'bold' : 'normal';
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
this._needVertexUpdate = true;
|
this._needVertexUpdate = true;
|
||||||
}
|
}
|
||||||
@ -245,11 +271,13 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} [italic= false] - whether or not the font of the labels is italic
|
* @param {boolean} [italic= false] - whether or not the font of the labels is italic
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setItalic(italic = false, log = false) {
|
setItalic(italic = false, log = false)
|
||||||
|
{
|
||||||
const hasChanged = this._setAttribute('italic', italic, log);
|
const hasChanged = this._setAttribute('italic', italic, log);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged)
|
||||||
this._fontStyle = (italic)?'italic':'normal';
|
{
|
||||||
|
this._fontStyle = (italic) ? 'italic' : 'normal';
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
this._needVertexUpdate = true;
|
this._needVertexUpdate = true;
|
||||||
}
|
}
|
||||||
@ -265,17 +293,23 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#setReadOnly
|
* @name module:visual.Slider#setReadOnly
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} [readOnly= true] - whether or not the slider is read-only
|
* @param {boolean} [readOnly= true] - whether or not the slider is read-only
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setReadOnly(readOnly = true, log = false) {
|
setReadOnly(readOnly = true, log = false)
|
||||||
|
{
|
||||||
const hasChanged = this._setAttribute('readOnly', readOnly, log);
|
const hasChanged = this._setAttribute('readOnly', readOnly, log);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged)
|
||||||
|
{
|
||||||
// halve the opacity:
|
// halve the opacity:
|
||||||
if (readOnly)
|
if (readOnly)
|
||||||
|
{
|
||||||
this._opacity /= 2.0;
|
this._opacity /= 2.0;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
this._opacity *= 2.0;
|
this._opacity *= 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
@ -294,12 +328,14 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} displayedRating - the displayed rating
|
* @param {number} displayedRating - the displayed rating
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setMarkerPos(displayedRating, log = false) {
|
setMarkerPos(displayedRating, log = false)
|
||||||
|
{
|
||||||
const previousMarkerPos = this._markerPos;
|
const previousMarkerPos = this._markerPos;
|
||||||
this._markerPos = this._granularise(displayedRating);
|
this._markerPos = this._granularise(displayedRating);
|
||||||
|
|
||||||
// if the displayed rating has changed, we need to update the pixi representation:
|
// if the displayed rating has changed, we need to update the pixi representation:
|
||||||
if (previousMarkerPos !== this._markerPos) {
|
if (previousMarkerPos !== this._markerPos)
|
||||||
|
{
|
||||||
this._needMarkerUpdate = true;
|
this._needMarkerUpdate = true;
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
@ -314,13 +350,16 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#setRating
|
* @name module:visual.Slider#setRating
|
||||||
* @public
|
* @public
|
||||||
* @param {number} rating - the rating
|
* @param {number} rating - the rating
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setRating(rating, log = false) {
|
setRating(rating, log = false)
|
||||||
|
{
|
||||||
rating = this._granularise(rating);
|
rating = this._granularise(rating);
|
||||||
this._markerPos = rating;
|
this._markerPos = rating;
|
||||||
if (this._isCategorical)
|
if (this._isCategorical)
|
||||||
|
{
|
||||||
rating = this._labels[Math.round(rating)];
|
rating = this._labels[Math.round(rating)];
|
||||||
|
}
|
||||||
|
|
||||||
this._setAttribute('rating', rating, log);
|
this._setAttribute('rating', rating, log);
|
||||||
}
|
}
|
||||||
@ -337,10 +376,13 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} [responseTime] - the reaction time
|
* @param {number} [responseTime] - the reaction time
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
_recordRating(rating, responseTime = undefined, log = false) {
|
_recordRating(rating, responseTime = undefined, log = false)
|
||||||
|
{
|
||||||
// get response time:
|
// get response time:
|
||||||
if (typeof responseTime === 'undefined')
|
if (typeof responseTime === 'undefined')
|
||||||
|
{
|
||||||
responseTime = this._responseClock.getTime();
|
responseTime = this._responseClock.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
// set rating:
|
// set rating:
|
||||||
// rating = this._granularise(rating);
|
// rating = this._granularise(rating);
|
||||||
@ -363,9 +405,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#_updateIfNeeded
|
* @name module:visual.Slider#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded() {
|
_updateIfNeeded()
|
||||||
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._buildSlider();
|
this._buildSlider();
|
||||||
@ -387,23 +432,30 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#_updateMarker
|
* @name module:visual.Slider#_updateMarker
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateMarker() {
|
_updateMarker()
|
||||||
|
{
|
||||||
if (!this._needMarkerUpdate)
|
if (!this._needMarkerUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needMarkerUpdate = false;
|
this._needMarkerUpdate = false;
|
||||||
|
|
||||||
if (typeof this._marker !== 'undefined') {
|
if (typeof this._marker !== 'undefined')
|
||||||
if (typeof this._markerPos !== 'undefined') {
|
{
|
||||||
|
if (typeof this._markerPos !== 'undefined')
|
||||||
|
{
|
||||||
const visibleMarkerPos = this._ratingToPos([this._markerPos]);
|
const visibleMarkerPos = this._ratingToPos([this._markerPos]);
|
||||||
this._marker.position = util.to_pixiPoint(visibleMarkerPos[0], this.units, this.win);
|
this._marker.position = util.to_pixiPoint(visibleMarkerPos[0], this.units, this.win);
|
||||||
this._marker.alpha = 1;
|
this._marker.alpha = 1;
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this._marker.alpha = 0;
|
this._marker.alpha = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the PIXI components of the slider (bar, ticks, labels, marker, etc.).
|
* Setup the PIXI components of the slider (bar, ticks, labels, marker, etc.).
|
||||||
*
|
*
|
||||||
@ -412,9 +464,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#_buildSlider
|
* @name module:visual.Slider#_buildSlider
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_buildSlider() {
|
_buildSlider()
|
||||||
|
{
|
||||||
if (!this._needVertexUpdate)
|
if (!this._needVertexUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needVertexUpdate = false;
|
this._needVertexUpdate = false;
|
||||||
|
|
||||||
this._applyStyle();
|
this._applyStyle();
|
||||||
@ -430,30 +485,39 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// (*) central bar:
|
// (*) central bar:
|
||||||
const barSize_px = util.to_px(this._barSize, this._units, this._win).map(v => Math.max(1, v));
|
const barSize_px = util.to_px(this._barSize, this._units, this._win).map(v => Math.max(1, v));
|
||||||
if (this._barLineWidth_px > 0) {
|
if (this._barLineWidth_px > 0)
|
||||||
|
{
|
||||||
this._body.lineStyle(this._barLineWidth_px, this._barLineColor.int, this._opacity, 0.5);
|
this._body.lineStyle(this._barLineWidth_px, this._barLineColor.int, this._opacity, 0.5);
|
||||||
if (typeof this._barFillColor !== 'undefined')
|
if (typeof this._barFillColor !== 'undefined')
|
||||||
|
{
|
||||||
this._body.beginFill(this._barFillColor.int, this._opacity);
|
this._body.beginFill(this._barFillColor.int, this._opacity);
|
||||||
|
}
|
||||||
this._body.drawRect(-barSize_px[0] / 2, -barSize_px[1] / 2, barSize_px[0], barSize_px[1]);
|
this._body.drawRect(-barSize_px[0] / 2, -barSize_px[1] / 2, barSize_px[0], barSize_px[1]);
|
||||||
if (typeof this._barFillColor !== 'undefined')
|
if (typeof this._barFillColor !== 'undefined')
|
||||||
|
{
|
||||||
this._body.endFill();
|
this._body.endFill();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// (*) ticks:
|
// (*) ticks:
|
||||||
if (this._isCategorical) {
|
if (this._isCategorical)
|
||||||
|
{
|
||||||
this._ticks = [...Array(this._labels.length)].map((_, i) => i);
|
this._ticks = [...Array(this._labels.length)].map((_, i) => i);
|
||||||
this._granularity = 1.0;
|
this._granularity = 1.0;
|
||||||
}
|
}
|
||||||
const tickPositions = this._ratingToPos(this._ticks);
|
const tickPositions = this._ratingToPos(this._ticks);
|
||||||
const tickPositions_px = tickPositions.map( p => util.to_px(p, this._units, this._win));
|
const tickPositions_px = tickPositions.map(p => util.to_px(p, this._units, this._win));
|
||||||
this._body.lineStyle(this._barLineWidth_px*2, this._tickColor.int, this._opacity, 0.5);
|
this._body.lineStyle(this._barLineWidth_px * 2, this._tickColor.int, this._opacity, 0.5);
|
||||||
const tickSize_px = util.to_px(this._tickSize, this._units, this._win);
|
const tickSize_px = util.to_px(this._tickSize, this._units, this._win);
|
||||||
for (let tickPosition_px of tickPositions_px) {
|
for (let tickPosition_px of tickPositions_px)
|
||||||
if (this._tickType === Slider.Shape.LINE) {
|
{
|
||||||
this._body.moveTo(tickPosition_px[0] - tickSize_px[0]/2, tickPosition_px[1] - tickSize_px[1]/2);
|
if (this._tickType === Slider.Shape.LINE)
|
||||||
this._body.lineTo(tickPosition_px[0] + tickSize_px[0]/2, tickPosition_px[1] + tickSize_px[1]/2);
|
{
|
||||||
|
this._body.moveTo(tickPosition_px[0] - tickSize_px[0] / 2, tickPosition_px[1] - tickSize_px[1] / 2);
|
||||||
|
this._body.lineTo(tickPosition_px[0] + tickSize_px[0] / 2, tickPosition_px[1] + tickSize_px[1] / 2);
|
||||||
}
|
}
|
||||||
else if (this._tickType === Slider.Shape.DISC) {
|
else if (this._tickType === Slider.Shape.DISC)
|
||||||
|
{
|
||||||
this._body.beginFill(this._tickColor.int, this._opacity);
|
this._body.beginFill(this._tickColor.int, this._opacity);
|
||||||
this._body.drawCircle(tickPosition_px[0], tickPosition_px[1], Math.max(tickSize_px[0], tickSize_px[1]));
|
this._body.drawCircle(tickPosition_px[0], tickPosition_px[1], Math.max(tickSize_px[0], tickSize_px[1]));
|
||||||
this._body.endFill();
|
this._body.endFill();
|
||||||
@ -465,7 +529,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
// outside of marker and labels:
|
// outside of marker and labels:
|
||||||
const eventCaptureRectangle = new PIXI.Graphics();
|
const eventCaptureRectangle = new PIXI.Graphics();
|
||||||
eventCaptureRectangle.beginFill(0, 0);
|
eventCaptureRectangle.beginFill(0, 0);
|
||||||
eventCaptureRectangle.drawRect(-barSize_px[0]/2 - tickSize_px[0]/2, -barSize_px[1]/2 - tickSize_px[1]/2,
|
eventCaptureRectangle.drawRect(-barSize_px[0] / 2 - tickSize_px[0] / 2, -barSize_px[1] / 2 - tickSize_px[1] / 2,
|
||||||
barSize_px[0] + tickSize_px[0], barSize_px[1] + tickSize_px[1]);
|
barSize_px[0] + tickSize_px[0], barSize_px[1] + tickSize_px[1]);
|
||||||
eventCaptureRectangle.endFill();
|
eventCaptureRectangle.endFill();
|
||||||
this._pixi.addChild(eventCaptureRectangle);
|
this._pixi.addChild(eventCaptureRectangle);
|
||||||
@ -473,17 +537,19 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// (*) labels:
|
// (*) labels:
|
||||||
const labelPositions_px = [...Array(this._labels.length)].map(
|
const labelPositions_px = [...Array(this._labels.length)].map(
|
||||||
(_, i) => tickPositions_px[Math.round(i / (this._labels.length-1) * (this._ticks.length-1))]);
|
(_, i) => tickPositions_px[Math.round(i / (this._labels.length - 1) * (this._ticks.length - 1))]);
|
||||||
|
|
||||||
const fontSize_px = util.to_px([this._fontSize, this._fontSize], this._units, this._win);
|
const fontSize_px = util.to_px([this._fontSize, this._fontSize], this._units, this._win);
|
||||||
for (let l = 0; l < labelPositions_px.length; ++l) {
|
for (let l = 0; l < labelPositions_px.length; ++l)
|
||||||
|
{
|
||||||
const labelText = new PIXI.Text(this._labels[l], {
|
const labelText = new PIXI.Text(this._labels[l], {
|
||||||
fontFamily : this._fontFamily,
|
fontFamily: this._fontFamily,
|
||||||
fontWeight: this._fontWeight,
|
fontWeight: this._fontWeight,
|
||||||
fontStyle: this._fontStyle,
|
fontStyle: this._fontStyle,
|
||||||
fontSize: Math.round(fontSize_px[0]),
|
fontSize: Math.round(fontSize_px[0]),
|
||||||
fill: this._labelColor.hex,
|
fill: this._labelColor.hex,
|
||||||
align: this._labelAlign});
|
align: this._labelAlign
|
||||||
|
});
|
||||||
|
|
||||||
const labelBounds = labelText.getBounds(true);
|
const labelBounds = labelText.getBounds(true);
|
||||||
labelText.position.x = labelPositions_px[l][0];
|
labelText.position.x = labelPositions_px[l][0];
|
||||||
@ -491,19 +557,31 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
labelText.anchor.x = this._labelAnchor.x;
|
labelText.anchor.x = this._labelAnchor.x;
|
||||||
labelText.anchor.y = this._labelAnchor.y;
|
labelText.anchor.y = this._labelAnchor.y;
|
||||||
|
|
||||||
if (this._isHorizontal()) {
|
if (this._isHorizontal())
|
||||||
|
{
|
||||||
if (this._flip)
|
if (this._flip)
|
||||||
|
{
|
||||||
labelText.position.y -= labelBounds.height + tickSize_px[1];
|
labelText.position.y -= labelBounds.height + tickSize_px[1];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
labelText.position.y += tickSize_px[1];
|
labelText.position.y += tickSize_px[1];
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (this._flip)
|
if (this._flip)
|
||||||
|
{
|
||||||
labelText.position.x += tickSize_px[0];
|
labelText.position.x += tickSize_px[0];
|
||||||
|
}
|
||||||
|
else if (this._labelOri === 0)
|
||||||
|
{
|
||||||
|
labelText.position.x -= labelBounds.width + tickSize_px[0];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
if (this._labelOri === 0)
|
{
|
||||||
labelText.position.x -= labelBounds.width + tickSize_px[0];
|
labelText.position.x -= tickSize_px[0];
|
||||||
else
|
}
|
||||||
labelText.position.x -= tickSize_px[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
labelText.rotation = (this._ori + this._labelOri) * Math.PI / 180;
|
labelText.rotation = (this._ori + this._labelOri) * Math.PI / 180;
|
||||||
@ -519,31 +597,42 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._marker.interactive = true;
|
this._marker.interactive = true;
|
||||||
this._pixi.addChild(this._marker);
|
this._pixi.addChild(this._marker);
|
||||||
|
|
||||||
if (this._markerType === Slider.Shape.DISC) {
|
if (this._markerType === Slider.Shape.DISC)
|
||||||
|
{
|
||||||
this._marker.lineStyle(1, this._markerColor.int, this._opacity, 0.5);
|
this._marker.lineStyle(1, this._markerColor.int, this._opacity, 0.5);
|
||||||
this._marker.beginFill(this._markerColor.int, this._opacity);
|
this._marker.beginFill(this._markerColor.int, this._opacity);
|
||||||
this._marker.drawCircle(0, 0, markerSize_px/2);
|
this._marker.drawCircle(0, 0, markerSize_px / 2);
|
||||||
this._marker.endFill();
|
this._marker.endFill();
|
||||||
}
|
}
|
||||||
else if (this._markerType === Slider.Shape.TRIANGLE) {
|
else if (this._markerType === Slider.Shape.TRIANGLE)
|
||||||
|
{
|
||||||
this._marker.lineStyle(1, this._markerColor.int, this._opacity, 0.5);
|
this._marker.lineStyle(1, this._markerColor.int, this._opacity, 0.5);
|
||||||
this._marker.beginFill(this._markerColor.int, this._opacity);
|
this._marker.beginFill(this._markerColor.int, this._opacity);
|
||||||
this._marker.moveTo(0, 0);
|
this._marker.moveTo(0, 0);
|
||||||
if (this._isHorizontal()) {
|
if (this._isHorizontal())
|
||||||
if (this._flip) {
|
{
|
||||||
this._marker.lineTo(markerSize_px/2, markerSize_px/2);
|
if (this._flip)
|
||||||
this._marker.lineTo(-markerSize_px/2, markerSize_px/2);
|
{
|
||||||
} else {
|
this._marker.lineTo(markerSize_px / 2, markerSize_px / 2);
|
||||||
this._marker.lineTo(markerSize_px/2, -markerSize_px/2);
|
this._marker.lineTo(-markerSize_px / 2, markerSize_px / 2);
|
||||||
this._marker.lineTo(-markerSize_px/2, -markerSize_px/2);
|
|
||||||
}
|
}
|
||||||
} else {
|
else
|
||||||
if (this._flip) {
|
{
|
||||||
this._marker.lineTo(-markerSize_px/2, markerSize_px/2);
|
this._marker.lineTo(markerSize_px / 2, -markerSize_px / 2);
|
||||||
this._marker.lineTo(-markerSize_px/2, -markerSize_px/2);
|
this._marker.lineTo(-markerSize_px / 2, -markerSize_px / 2);
|
||||||
} else {
|
}
|
||||||
this._marker.lineTo(markerSize_px/2, markerSize_px/2);
|
}
|
||||||
this._marker.lineTo(markerSize_px/2, -markerSize_px/2);
|
else
|
||||||
|
{
|
||||||
|
if (this._flip)
|
||||||
|
{
|
||||||
|
this._marker.lineTo(-markerSize_px / 2, markerSize_px / 2);
|
||||||
|
this._marker.lineTo(-markerSize_px / 2, -markerSize_px / 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._marker.lineTo(markerSize_px / 2, markerSize_px / 2);
|
||||||
|
this._marker.lineTo(markerSize_px / 2, -markerSize_px / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._marker.endFill();
|
this._marker.endFill();
|
||||||
@ -554,25 +643,29 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
const self = this;
|
const self = this;
|
||||||
self._markerDragging = false;
|
self._markerDragging = false;
|
||||||
|
|
||||||
this._marker.pointerdown = this._marker.mousedown = this._marker.touchstart = (event) => {
|
this._marker.pointerdown = this._marker.mousedown = this._marker.touchstart = (event) =>
|
||||||
if (event.data.button === 0) {
|
{
|
||||||
|
if (event.data.button === 0)
|
||||||
|
{
|
||||||
self._markerDragging = true;
|
self._markerDragging = true;
|
||||||
/* not quite right, just yet (as of May 2020)
|
/* not quite right, just yet (as of May 2020)
|
||||||
// set markerPos, but not rating:
|
// set markerPos, but not rating:
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||||
self._markerPos = self._granularise(rating);
|
self._markerPos = self._granularise(rating);
|
||||||
|
|
||||||
self._needMarkerUpdate = true;
|
self._needMarkerUpdate = true;
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// pointer was released inside the marker: if we were dragging, we record the rating
|
// pointer was released inside the marker: if we were dragging, we record the rating
|
||||||
this._marker.pointerup = this._marker.mouseup = this._marker.touchend = (event) => {
|
this._marker.pointerup = this._marker.mouseup = this._marker.touchend = (event) =>
|
||||||
if (self._markerDragging) {
|
{
|
||||||
|
if (self._markerDragging)
|
||||||
|
{
|
||||||
self._markerDragging = false;
|
self._markerDragging = false;
|
||||||
|
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||||
@ -584,8 +677,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
};
|
};
|
||||||
|
|
||||||
// pointer was released outside of the marker: cancel the dragging
|
// pointer was released outside of the marker: cancel the dragging
|
||||||
this._marker.pointerupoutside = this._marker.mouseupoutside = this._marker.touchendoutside = (event) => {
|
this._marker.pointerupoutside = this._marker.mouseupoutside = this._marker.touchendoutside = (event) =>
|
||||||
if (self._markerDragging) {
|
{
|
||||||
|
if (self._markerDragging)
|
||||||
|
{
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||||
self._recordRating(rating);
|
self._recordRating(rating);
|
||||||
@ -597,8 +692,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
};
|
};
|
||||||
|
|
||||||
// pointer is moving: if we are dragging, we move the marker position
|
// pointer is moving: if we are dragging, we move the marker position
|
||||||
this._marker.pointermove = (event) => {
|
this._marker.pointermove = (event) =>
|
||||||
if (self._markerDragging) {
|
{
|
||||||
|
if (self._markerDragging)
|
||||||
|
{
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||||
self.setMarkerPos(rating);
|
self.setMarkerPos(rating);
|
||||||
@ -631,7 +728,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
};
|
};
|
||||||
*/
|
*/
|
||||||
|
|
||||||
this._pixi.pointerup = this._pixi.mouseup = this._pixi.touchend = (event) => {
|
this._pixi.pointerup = this._pixi.mouseup = this._pixi.touchend = (event) =>
|
||||||
|
{
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._body);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._body);
|
||||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||||
self._recordRating(rating);
|
self._recordRating(rating);
|
||||||
@ -650,14 +748,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#_applyStyle
|
* @name module:visual.Slider#_applyStyle
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_applyStyle() {
|
_applyStyle()
|
||||||
|
{
|
||||||
|
|
||||||
// default style:
|
// default style:
|
||||||
if (this._isHorizontal()) {
|
if (this._isHorizontal())
|
||||||
|
{
|
||||||
this._barSize = [this._size[0], 0];
|
this._barSize = [this._size[0], 0];
|
||||||
this._tickSize = [0, this._size[1]];
|
this._tickSize = [0, this._size[1]];
|
||||||
this._labelAnchor = new PIXI.Point(0.5, 0);
|
this._labelAnchor = new PIXI.Point(0.5, 0);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this._barSize = [0, this._size[1]];
|
this._barSize = [0, this._size[1]];
|
||||||
this._tickSize = [this._size[0], 0];
|
this._tickSize = [this._size[0], 0];
|
||||||
this._labelAnchor = new PIXI.Point(0, 0.5);
|
this._labelAnchor = new PIXI.Point(0, 0.5);
|
||||||
@ -668,7 +770,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._barFillColor = undefined; //new Color('darkgray');
|
this._barFillColor = undefined; //new Color('darkgray');
|
||||||
|
|
||||||
this._tickType = Slider.Shape.LINE;
|
this._tickType = Slider.Shape.LINE;
|
||||||
this._tickColor = this._color;
|
this._tickColor = this._color;
|
||||||
|
|
||||||
this._markerColor = new Color('red');
|
this._markerColor = new Color('red');
|
||||||
this._markerType = Slider.Shape.DISC;
|
this._markerType = Slider.Shape.DISC;
|
||||||
@ -681,37 +783,45 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
|
|
||||||
// rating:
|
// rating:
|
||||||
if (this._style.indexOf(Slider.Style.RATING) > -1) {
|
if (this._style.indexOf(Slider.Style.RATING) > -1)
|
||||||
|
{
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
// triangleMarker:
|
// triangleMarker:
|
||||||
if (this._style.indexOf(Slider.Style.TRIANGLE_MARKER) > -1) {
|
if (this._style.indexOf(Slider.Style.TRIANGLE_MARKER) > -1)
|
||||||
|
{
|
||||||
this._markerType = Slider.Shape.TRIANGLE;
|
this._markerType = Slider.Shape.TRIANGLE;
|
||||||
this._markerSize = this._markerSize.map( s => s*2 );
|
this._markerSize = this._markerSize.map(s => s * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// slider:
|
// slider:
|
||||||
if (this._style.indexOf(Slider.Style.SLIDER) > -1) {
|
if (this._style.indexOf(Slider.Style.SLIDER) > -1)
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('"slider" style not implemented!');
|
this.psychoJS.logger.warn('"slider" style not implemented!');
|
||||||
//TODO
|
//TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
// whiteOnBlack:
|
// whiteOnBlack:
|
||||||
if (this._style.indexOf(Slider.Style.WHITE_ON_BLACK) > -1) {
|
if (this._style.indexOf(Slider.Style.WHITE_ON_BLACK) > -1)
|
||||||
|
{
|
||||||
this._barLineColor = new Color('black');
|
this._barLineColor = new Color('black');
|
||||||
// this._barFillColor = new Color('black');
|
// this._barFillColor = new Color('black');
|
||||||
this._tickColor = new Color('black');
|
this._tickColor = new Color('black');
|
||||||
this._markerColor = new Color('white');
|
this._markerColor = new Color('white');
|
||||||
this._labelColor = new Color('black');
|
this._labelColor = new Color('black');
|
||||||
}
|
}
|
||||||
|
|
||||||
// labels45:
|
// labels45:
|
||||||
if (this._style.indexOf(Slider.Style.LABELS45) > -1) {
|
if (this._style.indexOf(Slider.Style.LABELS45) > -1)
|
||||||
if (this._flip) {
|
{
|
||||||
|
if (this._flip)
|
||||||
|
{
|
||||||
this._labelAnchor = new PIXI.Point(0, 0.5);
|
this._labelAnchor = new PIXI.Point(0, 0.5);
|
||||||
this._labelAlign = 'left';
|
this._labelAlign = 'left';
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this._labelAnchor = new PIXI.Point(1, 0.5);
|
this._labelAnchor = new PIXI.Point(1, 0.5);
|
||||||
this._labelAlign = 'right';
|
this._labelAlign = 'right';
|
||||||
}
|
}
|
||||||
@ -719,7 +829,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// radio:
|
// radio:
|
||||||
if (this._style.indexOf(Slider.Style.RADIO) > -1) {
|
if (this._style.indexOf(Slider.Style.RADIO) > -1)
|
||||||
|
{
|
||||||
this._barLineWidth_px = 0;
|
this._barLineWidth_px = 0;
|
||||||
this._tickType = Slider.Shape.DISC;
|
this._tickType = Slider.Shape.DISC;
|
||||||
|
|
||||||
@ -739,13 +850,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @returns {Array.<Array.<number>>} the positions corresponding to the ratings (in Slider units,
|
* @returns {Array.<Array.<number>>} the positions corresponding to the ratings (in Slider units,
|
||||||
* with 0 at the center of the Slider)
|
* with 0 at the center of the Slider)
|
||||||
*/
|
*/
|
||||||
_ratingToPos(ratings) {
|
_ratingToPos(ratings)
|
||||||
const range = this._ticks[this._ticks.length-1] - this._ticks[0];
|
{
|
||||||
|
const range = this._ticks[this._ticks.length - 1] - this._ticks[0];
|
||||||
if (this._isHorizontal())
|
if (this._isHorizontal())
|
||||||
return ratings.map( v => [((v-this._ticks[0])/range-0.5) * this._size[0], 0]);
|
{
|
||||||
|
return ratings.map(v => [((v - this._ticks[0]) / range - 0.5) * this._size[0], 0]);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
return ratings.map( v => [0, (1.0 - (v-this._ticks[0])/range - 0.5) * this._size[1]]);
|
{
|
||||||
// return ratings.map( v => [0, ((v-this._ticks[0])/range-0.5) * this._size[1]]);
|
return ratings.map(v => [0, (1.0 - (v - this._ticks[0]) / range - 0.5) * this._size[1]]);
|
||||||
|
}
|
||||||
|
// return ratings.map( v => [0, ((v-this._ticks[0])/range-0.5) * this._size[1]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -757,16 +873,20 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number[]} pos_px - the [x,y] position, in pixel units, relative to the slider.
|
* @param {number[]} pos_px - the [x,y] position, in pixel units, relative to the slider.
|
||||||
* @returns {number} the corresponding rating.
|
* @returns {number} the corresponding rating.
|
||||||
*/
|
*/
|
||||||
_posToRating(pos_px) {
|
_posToRating(pos_px)
|
||||||
const range = this._ticks[this._ticks.length-1] - this._ticks[0];
|
{
|
||||||
|
const range = this._ticks[this._ticks.length - 1] - this._ticks[0];
|
||||||
const size_px = util.to_px(this._size, this._units, this._win);
|
const size_px = util.to_px(this._size, this._units, this._win);
|
||||||
if (this._isHorizontal())
|
if (this._isHorizontal())
|
||||||
|
{
|
||||||
return (pos_px[0] / size_px[0] + 0.5) * range + this._ticks[0];
|
return (pos_px[0] / size_px[0] + 0.5) * range + this._ticks[0];
|
||||||
// return ((pos_px[0]-this._pixi.position.x) / size_px[0] + 0.5) * range + this._ticks[0];
|
}// return ((pos_px[0]-this._pixi.position.x) / size_px[0] + 0.5) * range + this._ticks[0];
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return (1.0 - (pos_px[1] / size_px[1] + 0.5)) * range + this._ticks[0];
|
return (1.0 - (pos_px[1] / size_px[1] + 0.5)) * range + this._ticks[0];
|
||||||
// return (pos_px[1] / size_px[1] + 0.5) * range + this._ticks[0];
|
}
|
||||||
// return ((pos_px[1]-this._pixi.position.y) / size_px[1] + 0.5) * range + this._ticks[0];
|
// return (pos_px[1] / size_px[1] + 0.5) * range + this._ticks[0];
|
||||||
|
// return ((pos_px[1]-this._pixi.position.y) / size_px[1] + 0.5) * range + this._ticks[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -779,7 +899,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @private
|
* @private
|
||||||
* @returns {boolean} whether or not the slider is horizontal
|
* @returns {boolean} whether or not the slider is horizontal
|
||||||
*/
|
*/
|
||||||
_isHorizontal() {
|
_isHorizontal()
|
||||||
|
{
|
||||||
return (this._size[0] > this._size[1]);
|
return (this._size[0] > this._size[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -792,13 +913,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} rating - the input rating
|
* @param {number} rating - the input rating
|
||||||
* @returns {number} the new rating with granularity applied
|
* @returns {number} the new rating with granularity applied
|
||||||
*/
|
*/
|
||||||
_granularise(rating) {
|
_granularise(rating)
|
||||||
|
{
|
||||||
if (typeof rating === 'undefined')
|
if (typeof rating === 'undefined')
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._granularity > 0)
|
if (this._granularity > 0)
|
||||||
rating = Math.round( rating / this._granularity ) * this._granularity;
|
{
|
||||||
rating = Math.min( Math.max(this._ticks[0], rating), this._ticks[this._ticks.length-1]);
|
rating = Math.round(rating / this._granularity) * this._granularity;
|
||||||
|
}
|
||||||
|
rating = Math.min(Math.max(this._ticks[0], rating), this._ticks[this._ticks.length - 1]);
|
||||||
|
|
||||||
return rating;
|
return rating;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Text Stimulus.
|
* Text Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { VisualStim } from './VisualStim';
|
import {VisualStim} from './VisualStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { ColorMixin } from '../util/ColorMixin';
|
import {ColorMixin} from '../util/ColorMixin';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ import * as util from '../util/Util';
|
|||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {String} options.name - the name used when logging messages from this stimulus
|
* @param {String} options.name - the name used when logging messages from this stimulus
|
||||||
* @param {Window} options.win - the associated Window
|
* @param {Window} options.win - the associated Window
|
||||||
* @param {string} [options.text="Hello World"] - the text to be rendered
|
* @param {string} [options.text="Hello World"] - the text to be rendered
|
||||||
* @param {string} [options.font= "Arial"] - the text font
|
* @param {string} [options.font= "Arial"] - the text font
|
||||||
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the text
|
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the text
|
||||||
* @param {Color} [options.color= Color('white')] the background color
|
* @param {Color} [options.color= Color('white')] the background color
|
||||||
@ -38,48 +38,50 @@ import * as util from '../util/Util';
|
|||||||
* @param {boolean} wrapWidth - whether or not to wrap the text horizontally
|
* @param {boolean} wrapWidth - whether or not to wrap the text horizontally
|
||||||
* @param {boolean} [flipHoriz= false] - whether or not to flip the text horizontally
|
* @param {boolean} [flipHoriz= false] - whether or not to flip the text horizontally
|
||||||
* @param {boolean} [flipVert= false] - whether or not to flip the text vertically
|
* @param {boolean} [flipVert= false] - whether or not to flip the text vertically
|
||||||
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*
|
*
|
||||||
* @todo vertical alignment, and orientation are currently NOT implemented
|
* @todo vertical alignment, and orientation are currently NOT implemented
|
||||||
*/
|
*/
|
||||||
export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||||
{
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
text = 'Hello World',
|
text = 'Hello World',
|
||||||
font = 'Arial',
|
font = 'Arial',
|
||||||
pos,
|
pos,
|
||||||
color = new Color('white'),
|
color = new Color('white'),
|
||||||
opacity,
|
opacity,
|
||||||
contrast = 1.0,
|
contrast = 1.0,
|
||||||
units,
|
units,
|
||||||
ori,
|
ori,
|
||||||
height = 0.1,
|
height = 0.1,
|
||||||
bold = false,
|
bold = false,
|
||||||
italic = false,
|
italic = false,
|
||||||
alignHoriz = 'left',
|
alignHoriz = 'left',
|
||||||
alignVert = 'center',
|
alignVert = 'center',
|
||||||
wrapWidth,
|
wrapWidth,
|
||||||
flipHoriz = false,
|
flipHoriz = false,
|
||||||
flipVert = false,
|
flipVert = false,
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({ name, win, units, ori, opacity, pos, autoDraw, autoLog });
|
super({name, win, units, ori, opacity, pos, autoDraw, autoLog});
|
||||||
|
|
||||||
this._addAttributes(TextStim, text, font, color, contrast, height, bold, italic, alignHoriz, alignVert, wrapWidth, flipHoriz, flipVert);
|
this._addAttributes(TextStim, text, font, color, contrast, height, bold, italic, alignHoriz, alignVert, wrapWidth, flipHoriz, flipVert);
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the text attribute.
|
* Setter for the text attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#setText
|
* @name module:visual.TextStim#setText
|
||||||
* @public
|
* @public
|
||||||
* @param {string} text - the text
|
* @param {string} text - the text
|
||||||
@ -96,13 +98,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the alignHoriz attribute.
|
* Setter for the alignHoriz attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#setAlignHoriz
|
* @name module:visual.TextStim#setAlignHoriz
|
||||||
* @public
|
* @public
|
||||||
* @param {string} alignHoriz - the text horizontal alignment, e.g. 'center'
|
* @param {string} alignHoriz - the text horizontal alignment, e.g. 'center'
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setAlignHoriz(alignHoriz, log) {
|
setAlignHoriz(alignHoriz, log)
|
||||||
|
{
|
||||||
this._setAttribute('alignHoriz', alignHoriz, log);
|
this._setAttribute('alignHoriz', alignHoriz, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -112,16 +115,24 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the wrapWidth attribute.
|
* Setter for the wrapWidth attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#setWrapWidth
|
* @name module:visual.TextStim#setWrapWidth
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} wrapWidth - whether or not to wrap the text at the given width
|
* @param {boolean} wrapWidth - whether or not to wrap the text at the given width
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setWrapWidth(wrapWidth, log) {
|
setWrapWidth(wrapWidth, log)
|
||||||
if (typeof wrapWidth === 'undefined') {
|
{
|
||||||
|
if (typeof wrapWidth === 'undefined')
|
||||||
|
{
|
||||||
if (!TextStim._defaultWrapWidthMap.has(this._units))
|
if (!TextStim._defaultWrapWidthMap.has(this._units))
|
||||||
throw { origin : 'TextStim.setWrapWidth', context : 'when setting the wrap width of TextStim: ' + this._name, error : 'no default wrap width for unit: ' + this._units};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'TextStim.setWrapWidth',
|
||||||
|
context: 'when setting the wrap width of TextStim: ' + this._name,
|
||||||
|
error: 'no default wrap width for unit: ' + this._units
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
wrapWidth = TextStim._defaultWrapWidthMap.get(this._units);
|
wrapWidth = TextStim._defaultWrapWidthMap.get(this._units);
|
||||||
}
|
}
|
||||||
@ -135,16 +146,24 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the height attribute.
|
* Setter for the height attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#setHeight
|
* @name module:visual.TextStim#setHeight
|
||||||
* @public
|
* @public
|
||||||
* @param {number} height - text height
|
* @param {number} height - text height
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setHeight(height, log) {
|
setHeight(height, log)
|
||||||
if (typeof height === 'undefined') {
|
{
|
||||||
|
if (typeof height === 'undefined')
|
||||||
|
{
|
||||||
if (!TextStim._defaultLetterHeightMap.has(this._units))
|
if (!TextStim._defaultLetterHeightMap.has(this._units))
|
||||||
throw { origin : 'TextStim.setHeight', context : 'when setting the height of TextStim: ' + this._name, error : 'no default letter height for unit: ' + this._units};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'TextStim.setHeight',
|
||||||
|
context: 'when setting the height of TextStim: ' + this._name,
|
||||||
|
error: 'no default letter height for unit: ' + this._units
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
height = TextStim._defaultLetterHeightMap.get(this._units);
|
height = TextStim._defaultLetterHeightMap.get(this._units);
|
||||||
}
|
}
|
||||||
@ -158,13 +177,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the italic attribute.
|
* Setter for the italic attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#setItalic
|
* @name module:visual.TextStim#setItalic
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} italic - whether or not the text is italic
|
* @param {boolean} italic - whether or not the text is italic
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setItalic(italic, log) {
|
setItalic(italic, log)
|
||||||
|
{
|
||||||
this._setAttribute('italic', italic, log);
|
this._setAttribute('italic', italic, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -174,13 +194,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the bold attribute.
|
* Setter for the bold attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#setBold
|
* @name module:visual.TextStim#setBold
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} bold - whether or not the text is bold
|
* @param {boolean} bold - whether or not the text is bold
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setBold(bold, log) {
|
setBold(bold, log)
|
||||||
|
{
|
||||||
this._setAttribute('bold', bold, log);
|
this._setAttribute('bold', bold, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -190,13 +211,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the flipVert attribute.
|
* Setter for the flipVert attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#setFlipVert
|
* @name module:visual.TextStim#setFlipVert
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} flipVert - whether or not to flip vertically
|
* @param {boolean} flipVert - whether or not to flip vertically
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipVert(flipVert, log) {
|
setFlipVert(flipVert, log)
|
||||||
|
{
|
||||||
this._setAttribute('flipVert', flipVert, log);
|
this._setAttribute('flipVert', flipVert, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -206,13 +228,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the flipHoriz attribute.
|
* Setter for the flipHoriz attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#setFlipHoriz
|
* @name module:visual.TextStim#setFlipHoriz
|
||||||
* @public
|
* @public
|
||||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipHoriz(flipHoriz, log) {
|
setFlipHoriz(flipHoriz, log)
|
||||||
|
{
|
||||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -222,20 +245,27 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether an object is inside the bounding box of the text.
|
* Determine whether an object is inside the bounding box of the text.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#contains
|
* @name module:visual.TextStim#contains
|
||||||
* @public
|
* @public
|
||||||
* @param {Object} object - the object
|
* @param {Object} object - the object
|
||||||
* @param {string} units - the units
|
* @param {string} units - the units
|
||||||
* @return {boolean} whether or not the object is inside the bounding box of the text
|
* @return {boolean} whether or not the object is inside the bounding box of the text
|
||||||
*
|
*
|
||||||
* @todo this is currently NOT implemented
|
* @todo this is currently NOT implemented
|
||||||
*/
|
*/
|
||||||
contains(object, units) {
|
contains(object, units)
|
||||||
|
{
|
||||||
// get position of object:
|
// get position of object:
|
||||||
let objectPos_px = util.getPositionFromObject(object, units);
|
let objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined')
|
if (typeof objectPos_px === 'undefined')
|
||||||
throw { origin : 'TextStim.contains', context : 'when determining whether TextStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'TextStim.contains',
|
||||||
|
context: 'when determining whether TextStim: ' + this._name + ' contains object: ' + util.toString(object),
|
||||||
|
error: 'unable to determine the position of the object'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// test for inclusion:
|
// test for inclusion:
|
||||||
// TODO
|
// TODO
|
||||||
@ -245,16 +275,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the stimulus, if necessary.
|
* Update the stimulus, if necessary.
|
||||||
*
|
*
|
||||||
* @name module:visual.TextStim#_updateIfNeeded
|
* @name module:visual.TextStim#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @todo take size into account
|
* @todo take size into account
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded()
|
_updateIfNeeded()
|
||||||
{
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._heightPix = this._getLengthPix(this._height);
|
this._heightPix = this._getLengthPix(this._height);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Base class for all visual stimuli.
|
* Base class for all visual stimuli.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -15,7 +15,7 @@ import * as util from '../util/Util';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all visual stimuli.
|
* Base class for all visual stimuli.
|
||||||
*
|
*
|
||||||
* @name module:visual.VisualStim
|
* @name module:visual.VisualStim
|
||||||
* @class
|
* @class
|
||||||
* @extends MinimalStim
|
* @extends MinimalStim
|
||||||
@ -28,7 +28,7 @@ import * as util from '../util/Util';
|
|||||||
* @param {number} [options.opacity= 1.0] - the opacity
|
* @param {number} [options.opacity= 1.0] - the opacity
|
||||||
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
|
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
|
||||||
* @param {number} [options.size= 1.0] - the size
|
* @param {number} [options.size= 1.0] - the size
|
||||||
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
||||||
@ -43,7 +43,7 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
|||||||
size,
|
size,
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({win, name, autoDraw, autoLog});
|
super({win, name, autoDraw, autoLog});
|
||||||
|
|
||||||
@ -67,19 +67,22 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the size attribute.
|
* Setter for the size attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.VisualStim#setSize
|
* @name module:visual.VisualStim#setSize
|
||||||
* @public
|
* @public
|
||||||
* @param {number | number[]} size - the stimulus size
|
* @param {number | number[]} size - the stimulus size
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setSize(size, log = false)
|
setSize(size, log = false)
|
||||||
{
|
{
|
||||||
// size is either undefined or a tuple of numbers:
|
// size is either undefined or a tuple of numbers:
|
||||||
if (typeof size !== 'undefined') {
|
if (typeof size !== 'undefined')
|
||||||
|
{
|
||||||
size = util.toNumerical(size);
|
size = util.toNumerical(size);
|
||||||
if (!Array.isArray(size))
|
if (!Array.isArray(size))
|
||||||
|
{
|
||||||
size = [size, size];
|
size = [size, size];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setAttribute('size', size, log);
|
this._setAttribute('size', size, log);
|
||||||
@ -90,10 +93,10 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the orientation attribute.
|
* Setter for the orientation attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.VisualStim#setOri
|
* @name module:visual.VisualStim#setOri
|
||||||
* @public
|
* @public
|
||||||
* @param {number} ori - the orientation in degree with 0 as the vertical position, positive values rotate clockwise.
|
* @param {number} ori - the orientation in degree with 0 as the vertical position, positive values rotate clockwise.
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setOri(ori, log = false)
|
setOri(ori, log = false)
|
||||||
@ -111,7 +114,7 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the position attribute.
|
* Setter for the position attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.VisualStim#setPos
|
* @name module:visual.VisualStim#setPos
|
||||||
* @public
|
* @public
|
||||||
* @param {Array.<number>} pos - position of the center of the stimulus, in stimulus units
|
* @param {Array.<number>} pos - position of the center of the stimulus, in stimulus units
|
||||||
@ -124,13 +127,13 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
|||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the opacity attribute.
|
* Setter for the opacity attribute.
|
||||||
*
|
*
|
||||||
* @name module:visual.VisualStim#setOpacity
|
* @name module:visual.VisualStim#setOpacity
|
||||||
* @public
|
* @public
|
||||||
* @param {number} opacity - the opacity: 0 is completely transparent, 1 is fully opaque
|
* @param {number} opacity - the opacity: 0 is completely transparent, 1 is fully opaque
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setOpacity(opacity, log = false)
|
setOpacity(opacity, log = false)
|
||||||
|
Loading…
Reference in New Issue
Block a user