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:
|
||||
|
||||
|
@ -1,33 +1,37 @@
|
||||
/**
|
||||
* Manager handling the keyboard and mouse/touch events.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
import { MonotonicClock, Clock } from '../util/Clock';
|
||||
import { PsychoJS } from './PsychoJS';
|
||||
import {MonotonicClock, Clock} from '../util/Clock';
|
||||
import {PsychoJS} from './PsychoJS';
|
||||
|
||||
|
||||
/**
|
||||
* @class
|
||||
* <p>This manager handles all participant interactions with the experiment, i.e. keyboard, mouse and touch events.</p>
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager
|
||||
* @class
|
||||
* @param {Object} options
|
||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||
*/
|
||||
export class EventManager {
|
||||
export class EventManager
|
||||
{
|
||||
|
||||
constructor(psychoJS) {
|
||||
constructor(psychoJS)
|
||||
{
|
||||
this._psychoJS = psychoJS;
|
||||
|
||||
// populate the reverse pyglet map:
|
||||
for (const keyName in EventManager._pygletMap)
|
||||
{
|
||||
EventManager._reversePygletMap[EventManager._pygletMap[keyName]] = keyName;
|
||||
}
|
||||
|
||||
// add key listeners:
|
||||
this._keyBuffer = [];
|
||||
@ -53,9 +57,9 @@ export class EventManager {
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#getKeys
|
||||
* @function
|
||||
* @public
|
||||
@ -65,36 +69,54 @@ export class EventManager {
|
||||
* @return {string[]} the list of keys that were pressed.
|
||||
*/
|
||||
getKeys({
|
||||
keyList = null,
|
||||
timeStamped = false
|
||||
} = {}) {
|
||||
keyList = null,
|
||||
timeStamped = false
|
||||
} = {})
|
||||
{
|
||||
if (keyList != null)
|
||||
{
|
||||
keyList = EventManager.pyglet2w3c(keyList);
|
||||
}
|
||||
|
||||
let newBuffer = [];
|
||||
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];
|
||||
let keyId = null;
|
||||
|
||||
if (keyList != null) {
|
||||
if (keyList != null)
|
||||
{
|
||||
let index = keyList.indexOf(key.code);
|
||||
if (index < 0)
|
||||
{
|
||||
index = keyList.indexOf(EventManager._keycodeMap[key.keyCode]);
|
||||
}
|
||||
if (index >= 0)
|
||||
{
|
||||
keyId = EventManager._reversePygletMap[keyList[index]];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
keyId = EventManager._reversePygletMap[key.code];
|
||||
}
|
||||
|
||||
if (keyId != null) {
|
||||
if (keyId != null)
|
||||
{
|
||||
if (timeStamped)
|
||||
{
|
||||
keys.push([keyId, key.timestamp]);
|
||||
}
|
||||
else
|
||||
{
|
||||
keys.push(keyId);
|
||||
}
|
||||
}
|
||||
else
|
||||
newBuffer.push(key); // keep key press in buffer
|
||||
{
|
||||
newBuffer.push(key);
|
||||
} // keep key press in buffer
|
||||
}
|
||||
|
||||
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.number} times - the time elapsed since the last rest of the associated clock
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef EventManager.MouseInfo
|
||||
* @property {Array.number} pos - the position of the mouse [x, y]
|
||||
@ -117,94 +140,102 @@ export class EventManager {
|
||||
*/
|
||||
/**
|
||||
* Get the mouse info.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#getMouseInfo
|
||||
* @function
|
||||
* @public
|
||||
* @return {EventManager.MouseInfo} the mouse info.
|
||||
*/
|
||||
getMouseInfo() {
|
||||
getMouseInfo()
|
||||
{
|
||||
return this._mouseInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Clear all events from the event buffer.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#clearEvents
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
*
|
||||
* @todo handle the attribs argument
|
||||
*/
|
||||
clearEvents(attribs) {
|
||||
clearEvents(attribs)
|
||||
{
|
||||
this.clearKeys();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear all keys from the key buffer.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#clearKeys
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
clearKeys() {
|
||||
clearKeys()
|
||||
{
|
||||
this._keyBuffer = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the move clock.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#startMoveClock
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
*
|
||||
* @todo not implemented
|
||||
*/
|
||||
startMoveClock() {
|
||||
startMoveClock()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stop the move clock.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#stopMoveClock
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
*
|
||||
* @todo not implemented
|
||||
*/
|
||||
stopMoveClock() {
|
||||
stopMoveClock()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the move clock.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#resetMoveClock
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
*
|
||||
* @todo not implemented
|
||||
*/
|
||||
resetMoveClock() {
|
||||
resetMoveClock()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add various mouse listeners to the Pixi renderer of the {@link Window}.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#addMouseListeners
|
||||
* @function
|
||||
* @public
|
||||
* @param {PIXI.Renderer} renderer - The Pixi renderer
|
||||
*/
|
||||
addMouseListeners(renderer) {
|
||||
addMouseListeners(renderer)
|
||||
{
|
||||
const self = this;
|
||||
|
||||
renderer.view.addEventListener("pointerdown", (event) => {
|
||||
renderer.view.addEventListener("pointerdown", (event) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
self._mouseInfo.buttons.pressed[event.button] = 1;
|
||||
@ -216,7 +247,8 @@ export class EventManager {
|
||||
}, false);
|
||||
|
||||
|
||||
renderer.view.addEventListener("touchstart", (event) => {
|
||||
renderer.view.addEventListener("touchstart", (event) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
self._mouseInfo.buttons.pressed[0] = 1;
|
||||
@ -230,7 +262,8 @@ export class EventManager {
|
||||
}, false);
|
||||
|
||||
|
||||
renderer.view.addEventListener("pointerup", (event) => {
|
||||
renderer.view.addEventListener("pointerup", (event) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
self._mouseInfo.buttons.pressed[event.button] = 0;
|
||||
@ -241,7 +274,8 @@ export class EventManager {
|
||||
}, false);
|
||||
|
||||
|
||||
renderer.view.addEventListener("touchend", (event) => {
|
||||
renderer.view.addEventListener("touchend", (event) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
self._mouseInfo.buttons.pressed[0] = 0;
|
||||
@ -255,7 +289,8 @@ export class EventManager {
|
||||
}, false);
|
||||
|
||||
|
||||
renderer.view.addEventListener("pointermove", (event) => {
|
||||
renderer.view.addEventListener("pointermove", (event) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
self._mouseInfo.moveClock.reset();
|
||||
@ -263,11 +298,12 @@ export class EventManager {
|
||||
}, false);
|
||||
|
||||
|
||||
renderer.view.addEventListener("touchmove", (event) => {
|
||||
renderer.view.addEventListener("touchmove", (event) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
self._mouseInfo.moveClock.reset();
|
||||
|
||||
|
||||
// we use the first touch, discarding all others:
|
||||
const touches = event.changedTouches;
|
||||
self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY];
|
||||
@ -275,19 +311,20 @@ export class EventManager {
|
||||
|
||||
|
||||
// (*) wheel
|
||||
renderer.view.addEventListener("wheel", event => {
|
||||
renderer.view.addEventListener("wheel", event =>
|
||||
{
|
||||
self._mouseInfo.wheelRel[0] += event.deltaX;
|
||||
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] + ")");
|
||||
}, false);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add key listeners to the document.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#_addKeyListeners
|
||||
* @function
|
||||
* @private
|
||||
@ -307,7 +344,9 @@ export class EventManager {
|
||||
|
||||
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
||||
if (typeof code === 'undefined')
|
||||
{
|
||||
code = EventManager.keycode2w3c(event.keyCode);
|
||||
}
|
||||
|
||||
self._keyBuffer.push({
|
||||
code,
|
||||
@ -324,24 +363,29 @@ export class EventManager {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Convert a keylist that uses pyglet key names to one that uses W3C KeyboardEvent.code values.
|
||||
* <p>This allows key lists that work in the builder environment to work in psychoJS web experiments.</p>
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#pyglet2w3c
|
||||
* @function
|
||||
* @public
|
||||
* @param {Array.string} pygletKeyList - the array of pyglet key names
|
||||
* @return {Array.string} the w3c keyList
|
||||
*/
|
||||
static pyglet2w3c(pygletKeyList) {
|
||||
static pyglet2w3c(pygletKeyList)
|
||||
{
|
||||
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')
|
||||
{
|
||||
w3cKeyList.push(pygletKeyList[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
w3cKeyList.push(EventManager._pygletMap[pygletKeyList[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
return w3cKeyList;
|
||||
@ -358,11 +402,16 @@ export class EventManager {
|
||||
* @param {string} code - W3C Key Code
|
||||
* @returns {string} corresponding pyglet key
|
||||
*/
|
||||
static w3c2pyglet(code) {
|
||||
static w3c2pyglet(code)
|
||||
{
|
||||
if (code in EventManager._reversePygletMap)
|
||||
{
|
||||
return EventManager._reversePygletMap[code];
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'N/A';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -377,7 +426,8 @@ export class EventManager {
|
||||
* @param {number} keycode - the keycode
|
||||
* @returns {string} corresponding W3C UI Event code
|
||||
*/
|
||||
static keycode2w3c(keycode) {
|
||||
static keycode2w3c(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
|
||||
* Right, or between Enter and Numpad Enter. Use at your own risk (or upgrade your browser...).</p>
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#_keycodeMap
|
||||
* @readonly
|
||||
* @private
|
||||
@ -479,7 +529,7 @@ EventManager._keycodeMap = {
|
||||
/**
|
||||
* This map associates pyglet key names to the corresponding W3C KeyboardEvent codes values.
|
||||
* <p>More information can be found [here]{@link https://www.w3.org/TR/uievents-code}</p>
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#_pygletMap
|
||||
* @readonly
|
||||
* @private
|
||||
@ -581,10 +631,10 @@ EventManager._pygletMap = {
|
||||
|
||||
/**
|
||||
* <p>This map associates W3C KeyboardEvent.codes to the corresponding pyglet key names.
|
||||
*
|
||||
*
|
||||
* @name module:core.EventManager#_reversePygletMap
|
||||
* @readonly
|
||||
* @private
|
||||
* @private
|
||||
* @type {Object.<String,String>}
|
||||
*/
|
||||
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)
|
||||
*
|
||||
*
|
||||
* @name module:core.BuilderKeyResponse
|
||||
* @class
|
||||
* @param {Object} options
|
||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||
*/
|
||||
export class BuilderKeyResponse {
|
||||
constructor(psychoJS) {
|
||||
export class BuilderKeyResponse
|
||||
{
|
||||
constructor(psychoJS)
|
||||
{
|
||||
this._psychoJS = psychoJS;
|
||||
|
||||
this.status = PsychoJS.Status.NOT_STARTED;
|
||||
|
196
js/core/GUI.js
196
js/core/GUI.js
@ -2,7 +2,7 @@
|
||||
* Graphic User Interface
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -12,7 +12,7 @@ import {PsychoJS} from './PsychoJS';
|
||||
import {ServerManager} from './ServerManager';
|
||||
import {Scheduler} from '../util/Scheduler';
|
||||
import {Clock} from '../util/Clock';
|
||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
||||
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||
import * as util from '../util/Util';
|
||||
|
||||
|
||||
@ -27,14 +27,18 @@ import * as util from '../util/Util';
|
||||
export class GUI
|
||||
{
|
||||
|
||||
get dialogComponent() { return this._dialogComponent; }
|
||||
get dialogComponent()
|
||||
{
|
||||
return this._dialogComponent;
|
||||
}
|
||||
|
||||
constructor(psychoJS)
|
||||
{
|
||||
this._psychoJS = psychoJS;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
@ -66,11 +70,11 @@ export class GUI
|
||||
* @param {String} options.title - name of the project
|
||||
*/
|
||||
DlgFromDict({
|
||||
logoUrl,
|
||||
text,
|
||||
dictionary,
|
||||
title
|
||||
})
|
||||
logoUrl,
|
||||
text,
|
||||
dictionary,
|
||||
title
|
||||
})
|
||||
{
|
||||
// get info from URL:
|
||||
const infoFromUrl = util.getUrlParameters();
|
||||
@ -116,7 +120,7 @@ export class GUI
|
||||
// logo:
|
||||
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:
|
||||
@ -128,17 +132,19 @@ export class GUI
|
||||
|
||||
// add a combobox or text areas for each entry in the dictionary:
|
||||
htmlCode += '<form>';
|
||||
for (const key in dictionary) {
|
||||
for (const key in dictionary)
|
||||
{
|
||||
const value = dictionary[key];
|
||||
const keyId = CSS.escape(key) + '_id';
|
||||
|
||||
// only create an input if the key is not in the URL:
|
||||
let inUrl = false;
|
||||
const cleanedDictKey = key.trim().toLowerCase();
|
||||
infoFromUrl.forEach( (urlValue, urlKey) =>
|
||||
infoFromUrl.forEach((urlValue, urlKey) =>
|
||||
{
|
||||
const cleanedUrlKey = urlKey.trim().toLowerCase();
|
||||
if (cleanedUrlKey === cleanedDictKey) {
|
||||
if (cleanedUrlKey === cleanedDictKey)
|
||||
{
|
||||
inUrl = true;
|
||||
// break;
|
||||
}
|
||||
@ -150,19 +156,26 @@ export class GUI
|
||||
|
||||
// if the field is required:
|
||||
if (key.slice(-1) === '*')
|
||||
{
|
||||
self._requiredKeys.push(key);
|
||||
}
|
||||
|
||||
// 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' +
|
||||
' ui-corner-all">';
|
||||
|
||||
// if the field is required, we add an empty option and select it:
|
||||
if (key.slice(-1) === '*')
|
||||
{
|
||||
htmlCode += '<option disabled selected>...</option>';
|
||||
}
|
||||
|
||||
for (const option of value)
|
||||
{
|
||||
htmlCode += '<option>' + option + '</option>';
|
||||
}
|
||||
|
||||
htmlCode += '</select>';
|
||||
$('#' + keyId).selectmenu({classes: {}});
|
||||
@ -170,8 +183,10 @@ export class GUI
|
||||
|
||||
// otherwise we use a single string input:
|
||||
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>';
|
||||
@ -199,11 +214,14 @@ export class GUI
|
||||
|
||||
|
||||
// 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 input = document.getElementById(keyId);
|
||||
if (input)
|
||||
{
|
||||
input.oninput = (event) => GUI._onKeyChange(self, event);
|
||||
}
|
||||
}
|
||||
|
||||
// init and open the dialog box:
|
||||
@ -223,17 +241,21 @@ export class GUI
|
||||
{
|
||||
id: "buttonOk",
|
||||
text: "Ok",
|
||||
click: function () {
|
||||
click: function ()
|
||||
{
|
||||
|
||||
// update dictionary:
|
||||
for (const key in dictionary) {
|
||||
for (const key in dictionary)
|
||||
{
|
||||
const input = document.getElementById(CSS.escape(key) + "_id");
|
||||
if (input)
|
||||
{
|
||||
dictionary[key] = input.value;
|
||||
}
|
||||
}
|
||||
|
||||
self._dialogComponent.button = 'OK';
|
||||
$("#expDialog").dialog("close");
|
||||
$("#expDialog").dialog('close');
|
||||
|
||||
// switch to full screen if requested:
|
||||
self._psychoJS.window.adjustScreenSize();
|
||||
@ -242,9 +264,10 @@ export class GUI
|
||||
{
|
||||
id: "buttonCancel",
|
||||
text: "Cancel",
|
||||
click: function () {
|
||||
click: function ()
|
||||
{
|
||||
self._dialogComponent.button = 'Cancel';
|
||||
$("#expDialog").dialog("close");
|
||||
$("#expDialog").dialog('close');
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -253,7 +276,8 @@ export class GUI
|
||||
open: self._onDialogOpen('#expDialog'),
|
||||
|
||||
// close is called by both buttons and when the user clicks on the cross:
|
||||
close: function () {
|
||||
close: function ()
|
||||
{
|
||||
//$.unblockUI();
|
||||
$(this).dialog('destroy').remove();
|
||||
self._dialogComponent.status = PsychoJS.Status.FINISHED;
|
||||
@ -282,9 +306,13 @@ export class GUI
|
||||
}
|
||||
|
||||
if (self._dialogComponent.status === PsychoJS.Status.FINISHED)
|
||||
{
|
||||
return Scheduler.Event.NEXT;
|
||||
}
|
||||
else
|
||||
{
|
||||
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
|
||||
*/
|
||||
dialog({
|
||||
message,
|
||||
warning,
|
||||
error,
|
||||
showOK = true,
|
||||
onOK
|
||||
} = {}) {
|
||||
message,
|
||||
warning,
|
||||
error,
|
||||
showOK = true,
|
||||
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 titleColour;
|
||||
@ -325,13 +366,16 @@ export class GUI
|
||||
|
||||
// deal with null error:
|
||||
if (!error)
|
||||
{
|
||||
error = 'Unspecified JavaScript error';
|
||||
}
|
||||
|
||||
let errorCode = null;
|
||||
|
||||
// go through the error stack and look for errorCode if there is one:
|
||||
let stackCode = '<ul>';
|
||||
while (true) {
|
||||
while (true)
|
||||
{
|
||||
|
||||
if (typeof error === 'object' && 'errorCode' in error)
|
||||
{
|
||||
@ -340,12 +384,12 @@ export class GUI
|
||||
|
||||
if (typeof error === 'object' && 'context' in error)
|
||||
{
|
||||
stackCode += '<li>' + error.context + '</li>';
|
||||
error = error.error;
|
||||
stackCode += '<li>' + error.context + '</li>';
|
||||
error = error.error;
|
||||
}
|
||||
else
|
||||
{
|
||||
stackCode += '<li><b>' + error + '</b></li>';
|
||||
stackCode += '<li><b>' + error + '</b></li>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -371,7 +415,8 @@ export class GUI
|
||||
}
|
||||
|
||||
// we are displaying a message:
|
||||
else if (typeof message !== 'undefined') {
|
||||
else if (typeof message !== 'undefined')
|
||||
{
|
||||
htmlCode = '<div id="msgDialog" title="Message">' +
|
||||
'<p class="validateTips">' + message + '</p>' +
|
||||
'</div>';
|
||||
@ -379,7 +424,8 @@ export class GUI
|
||||
}
|
||||
|
||||
// we are displaying a warning:
|
||||
else if (typeof warning !== 'undefined') {
|
||||
else if (typeof warning !== 'undefined')
|
||||
{
|
||||
htmlCode = '<div id="msgDialog" title="Warning">' +
|
||||
'<p class="validateTips">' + warning + '</p>' +
|
||||
'</div>';
|
||||
@ -395,7 +441,7 @@ export class GUI
|
||||
this._estimateDialogScalingFactor();
|
||||
const dialogSize = this._getDialogSize();
|
||||
const self = this;
|
||||
$('#msgDialog').dialog({
|
||||
$("#msgDialog").dialog({
|
||||
dialogClass: 'no-close',
|
||||
|
||||
width: dialogSize[0],
|
||||
@ -405,15 +451,18 @@ export class GUI
|
||||
modal: true,
|
||||
closeOnEscape: false,
|
||||
|
||||
buttons: (!showOK)?[]:[{
|
||||
buttons: (!showOK) ? [] : [{
|
||||
id: "buttonOk",
|
||||
text: "Ok",
|
||||
click: function() {
|
||||
click: function ()
|
||||
{
|
||||
$(this).dialog("destroy").remove();
|
||||
|
||||
// execute callback function:
|
||||
if (typeof onOK !== 'undefined')
|
||||
{
|
||||
onOK();
|
||||
}
|
||||
}
|
||||
}],
|
||||
|
||||
@ -422,7 +471,7 @@ export class GUI
|
||||
|
||||
})
|
||||
// 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:
|
||||
@ -471,10 +520,12 @@ export class GUI
|
||||
* @param {String} dialogId - the dialog ID
|
||||
* @private
|
||||
*/
|
||||
_dialogResize(dialogId) {
|
||||
_dialogResize(dialogId)
|
||||
{
|
||||
const self = this;
|
||||
|
||||
$(window).resize( function() {
|
||||
$(window).resize(function ()
|
||||
{
|
||||
const parent = $(dialogId).parent();
|
||||
const windowSize = [$(window).width(), $(window).height()];
|
||||
|
||||
@ -486,7 +537,8 @@ export class GUI
|
||||
});
|
||||
|
||||
const isDifferent = self._estimateDialogScalingFactor();
|
||||
if (!isDifferent) {
|
||||
if (!isDifferent)
|
||||
{
|
||||
$(dialogId).css({
|
||||
width: dialogSize[0] - self._contentDelta[0],
|
||||
maxHeight: dialogSize[1] - self._contentDelta[1]
|
||||
@ -499,7 +551,7 @@ export class GUI
|
||||
left: Math.max(0, (windowSize[0] - parent.outerWidth()) / 2.0),
|
||||
top: Math.max(0, (windowSize[1] - parent.outerHeight()) / 2.0),
|
||||
});
|
||||
} );
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -511,11 +563,13 @@ export class GUI
|
||||
* @private
|
||||
* @param {Object.<string, string|Symbol>} signal the signal
|
||||
*/
|
||||
_onResourceEvents(signal) {
|
||||
_onResourceEvents(signal)
|
||||
{
|
||||
this._psychoJS.logger.debug('signal: ' + util.toString(signal));
|
||||
|
||||
// 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:
|
||||
this._progressBarMax = signal.count * 2;
|
||||
$("#progressbar").progressbar("option", "max", this._progressBarMax);
|
||||
@ -525,7 +579,8 @@ export class GUI
|
||||
}
|
||||
|
||||
// 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;
|
||||
$("#progressMsg").text('all resources downloaded.');
|
||||
this._updateOkButtonStatus();
|
||||
@ -535,21 +590,27 @@ export class GUI
|
||||
else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
||||
{
|
||||
if (typeof this._progressBarCurrentIncrement === 'undefined')
|
||||
{
|
||||
this._progressBarCurrentIncrement = 0;
|
||||
++ this._progressBarCurrentIncrement;
|
||||
}
|
||||
++this._progressBarCurrentIncrement;
|
||||
|
||||
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.');
|
||||
// else
|
||||
// $("#progressMsg").text(signal.resource + ': downloading...');
|
||||
// $("#progressMsg").text(signal.resource + ': downloading...');
|
||||
|
||||
$("#progressbar").progressbar("option", "value", this._progressBarCurrentIncrement);
|
||||
}
|
||||
|
||||
// unknown message: we just display it
|
||||
else
|
||||
{
|
||||
$("#progressMsg").text(signal.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -562,15 +623,21 @@ export class GUI
|
||||
*/
|
||||
_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);
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#buttonOk").button("option", "disabled", true);
|
||||
}
|
||||
|
||||
// strangely, changing the disabled option sometimes fails to update the ui,
|
||||
// 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
|
||||
* @returns {boolean} whether or not the scaling factor is different from the previously estimated one
|
||||
*/
|
||||
_estimateDialogScalingFactor() {
|
||||
_estimateDialogScalingFactor()
|
||||
{
|
||||
const windowSize = [$(window).width(), $(window).height()];
|
||||
|
||||
// desktop:
|
||||
let dialogScalingFactor = 1.0;
|
||||
|
||||
// mobile or tablet:
|
||||
if (windowSize[0] < 1080) {
|
||||
if (windowSize[0] < 1080)
|
||||
{
|
||||
// landscape:
|
||||
if (windowSize[0] > windowSize[1])
|
||||
{
|
||||
dialogScalingFactor = 1.5;
|
||||
// portrait:
|
||||
}// portrait:
|
||||
else
|
||||
{
|
||||
dialogScalingFactor = 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
const isDifferent = (dialogScalingFactor !== this._dialogScalingFactor);
|
||||
@ -612,13 +684,14 @@ export class GUI
|
||||
* @private
|
||||
* @returns {number[]} the size of the popup dialog window
|
||||
*/
|
||||
_getDialogSize() {
|
||||
_getDialogSize()
|
||||
{
|
||||
const windowSize = [$(window).width(), $(window).height()];
|
||||
this._estimateDialogScalingFactor();
|
||||
|
||||
return [
|
||||
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[0], (windowSize[0] - GUI.dialogMargin[0]) / 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 {Event} event - event
|
||||
*/
|
||||
static _onKeyChange(gui, event) {
|
||||
static _onKeyChange(gui, event)
|
||||
{
|
||||
const element = event.target;
|
||||
const value = element.value;
|
||||
|
||||
if (typeof value !== 'undefined' && value.length > 0)
|
||||
{
|
||||
gui._setRequiredKeys.set(event.target, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
gui._setRequiredKeys.delete(event.target);
|
||||
}
|
||||
|
||||
gui._updateOkButtonStatus();
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Manager handling the keyboard events.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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 {string | undefined} name - pyglet key name
|
||||
*/
|
||||
export class KeyPress {
|
||||
constructor(code, tDown, name) {
|
||||
export class KeyPress
|
||||
{
|
||||
constructor(code, tDown, name)
|
||||
{
|
||||
this.code = code;
|
||||
this.tDown = tDown;
|
||||
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 {boolean} options.autoLog - whether or not to log
|
||||
*/
|
||||
export class Keyboard extends PsychObject {
|
||||
export class Keyboard extends PsychObject
|
||||
{
|
||||
|
||||
constructor({
|
||||
psychoJS,
|
||||
@ -57,16 +60,19 @@ export class Keyboard extends PsychObject {
|
||||
waitForStart = false,
|
||||
clock,
|
||||
autoLog = false,
|
||||
} = {}) {
|
||||
} = {})
|
||||
{
|
||||
|
||||
super(psychoJS);
|
||||
|
||||
if (typeof clock === 'undefined')
|
||||
clock = new Clock(); //this._psychoJS.monotonicClock;
|
||||
{
|
||||
clock = new Clock();
|
||||
} //this._psychoJS.monotonicClock;
|
||||
|
||||
this._addAttributes(Keyboard, bufferSize, waitForStart, clock, autoLog);
|
||||
// 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:
|
||||
this.clearEvents();
|
||||
@ -85,7 +91,8 @@ export class Keyboard extends PsychObject {
|
||||
* @public
|
||||
*
|
||||
*/
|
||||
start() {
|
||||
start()
|
||||
{
|
||||
this._status = PsychoJS.Status.STARTED;
|
||||
}
|
||||
|
||||
@ -98,7 +105,8 @@ export class Keyboard extends PsychObject {
|
||||
* @public
|
||||
*
|
||||
*/
|
||||
stop() {
|
||||
stop()
|
||||
{
|
||||
this._status = PsychoJS.Status.STOPPED;
|
||||
}
|
||||
|
||||
@ -121,20 +129,26 @@ export class Keyboard extends PsychObject {
|
||||
* @public
|
||||
* @return {Keyboard.KeyEvent[]} the list of events still in the buffer
|
||||
*/
|
||||
getEvents() {
|
||||
getEvents()
|
||||
{
|
||||
if (this._bufferLength === 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// iterate over the buffer, from start to end, and discard the null event:
|
||||
let filteredEvents = [];
|
||||
const bufferWrap = (this._bufferLength === this._bufferSize);
|
||||
let i = bufferWrap ? this._bufferIndex : -1;
|
||||
do {
|
||||
do
|
||||
{
|
||||
i = (i + 1) % this._bufferSize;
|
||||
const keyEvent = this._circularBuffer[i];
|
||||
if (keyEvent)
|
||||
{
|
||||
filteredEvents.push(keyEvent);
|
||||
}
|
||||
} while (i !== this._bufferIndex);
|
||||
|
||||
return filteredEvents;
|
||||
@ -160,29 +174,37 @@ export class Keyboard extends PsychObject {
|
||||
keyList = [],
|
||||
waitRelease = true,
|
||||
clear = true
|
||||
} = {}) {
|
||||
} = {})
|
||||
{
|
||||
|
||||
// if nothing in the buffer, return immediately:
|
||||
if (this._bufferLength === 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
let keyPresses = [];
|
||||
|
||||
// iterate over the circular buffer, looking for keyup events:
|
||||
const bufferWrap = (this._bufferLength === this._bufferSize);
|
||||
let i = bufferWrap ? this._bufferIndex : -1;
|
||||
do {
|
||||
do
|
||||
{
|
||||
i = (i + 1) % this._bufferSize;
|
||||
|
||||
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:
|
||||
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
|
||||
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey))
|
||||
{
|
||||
// look for a corresponding, preceding keydown event:
|
||||
const precedingKeydownIndex = keyEvent.keydownIndex;
|
||||
if (typeof precedingKeydownIndex !== 'undefined') {
|
||||
if (typeof precedingKeydownIndex !== 'undefined')
|
||||
{
|
||||
const precedingKeydownEvent = this._circularBuffer[precedingKeydownIndex];
|
||||
if (precedingKeydownEvent) {
|
||||
if (precedingKeydownEvent)
|
||||
{
|
||||
// prepare KeyPress and add it to the array:
|
||||
const tDown = precedingKeydownEvent.timestamp;
|
||||
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
||||
@ -191,7 +213,9 @@ export class Keyboard extends PsychObject {
|
||||
keyPresses.push(keyPress);
|
||||
|
||||
if (clear)
|
||||
{
|
||||
this._circularBuffer[precedingKeydownIndex] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +244,9 @@ export class Keyboard extends PsychObject {
|
||||
} while ((bufferWrap && j !== i) || (j > -1));*/
|
||||
|
||||
if (clear)
|
||||
{
|
||||
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) {
|
||||
for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values()) {
|
||||
if (!waitRelease)
|
||||
{
|
||||
for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values())
|
||||
{
|
||||
const keyEvent = this._circularBuffer[unmatchedKeyDownIndex];
|
||||
if (keyEvent) {
|
||||
if (keyEvent)
|
||||
{
|
||||
// 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 keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
||||
keyPress.rt = tDown - this._clock.getLastResetTime();
|
||||
keyPresses.push(keyPress);
|
||||
|
||||
if (clear) {
|
||||
if (clear)
|
||||
{
|
||||
this._unmatchedKeydownMap.delete(keyEvent.code);
|
||||
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 && keyList.length === 0)
|
||||
{
|
||||
this.clearEvents();
|
||||
}
|
||||
|
||||
|
||||
return keyPresses;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@ -317,12 +348,11 @@ export class Keyboard extends PsychObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
const value = keypressList.find( (keypress) => keypress.name === keyName );
|
||||
const value = keypressList.find((keypress) => keypress.name === keyName);
|
||||
return (typeof value !== 'undefined');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Add key listeners to the document.
|
||||
*
|
||||
@ -338,17 +368,21 @@ export class Keyboard extends PsychObject {
|
||||
|
||||
// add a keydown listener:
|
||||
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
|
||||
// holding a key down:
|
||||
if (event.repeat)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
||||
|
||||
if (this._status !== PsychoJS.Status.STARTED)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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):
|
||||
if (typeof code === 'undefined')
|
||||
{
|
||||
code = EventManager.keycode2w3c(event.keyCode);
|
||||
}
|
||||
|
||||
let pigletKey = EventManager.w3c2pyglet(code);
|
||||
|
||||
@ -387,12 +423,14 @@ export class Keyboard extends PsychObject {
|
||||
|
||||
// add a keyup listener:
|
||||
window.addEventListener("keyup", (event) =>
|
||||
// document.addEventListener("keyup", (event) =>
|
||||
// document.addEventListener("keyup", (event) =>
|
||||
{
|
||||
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
||||
|
||||
if (this._status !== PsychoJS.Status.STARTED)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self._previousKeydownKey = undefined;
|
||||
|
||||
@ -400,7 +438,9 @@ export class Keyboard extends PsychObject {
|
||||
|
||||
// take care of legacy Microsoft Edge:
|
||||
if (typeof code === 'undefined')
|
||||
{
|
||||
code = EventManager.keycode2w3c(event.keyCode);
|
||||
}
|
||||
|
||||
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
|
||||
// not be a corresponding keydown event
|
||||
const correspondingKeydownIndex = self._unmatchedKeydownMap.get(event.code);
|
||||
if (typeof correspondingKeydownIndex !== 'undefined') {
|
||||
if (typeof correspondingKeydownIndex !== 'undefined')
|
||||
{
|
||||
self._circularBuffer[self._bufferIndex].keydownIndex = correspondingKeydownIndex;
|
||||
self._unmatchedKeydownMap.delete(event.code);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Logger
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -10,20 +10,21 @@
|
||||
|
||||
import * as util from '../util/Util';
|
||||
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),
|
||||
* a remote one, etc.</p>
|
||||
*
|
||||
*
|
||||
* <p>Note: we use log4javascript for the console logger, and our own for the server logger.</p>
|
||||
*
|
||||
*
|
||||
* @name module:core.Logger
|
||||
* @class
|
||||
* @param {*} threshold - the logging threshold, e.g. log4javascript.Level.ERROR
|
||||
*/
|
||||
export class Logger {
|
||||
export class Logger
|
||||
{
|
||||
|
||||
constructor(psychoJS, threshold)
|
||||
{
|
||||
@ -80,11 +81,10 @@ export class Logger {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Log a server message at the DATA level.
|
||||
*
|
||||
* @name module:core.Logger#data
|
||||
* @name module:core.Logger#data
|
||||
* @public
|
||||
* @param {string} msg - the message to be logged.
|
||||
* @param {number} [time] - the logging time
|
||||
@ -96,7 +96,6 @@ export class Logger {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Log a server message.
|
||||
*
|
||||
@ -110,7 +109,9 @@ export class Logger {
|
||||
log(msg, level, time, obj)
|
||||
{
|
||||
if (typeof time === 'undefined')
|
||||
{
|
||||
time = MonotonicClock.getReferenceTime();
|
||||
}
|
||||
|
||||
this._serverLogs.push({
|
||||
msg,
|
||||
@ -122,7 +123,6 @@ export class Logger {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Flush all server logs to the server.
|
||||
*
|
||||
@ -149,7 +149,9 @@ export class Logger {
|
||||
'\t' + Symbol.keyFor(log.level) +
|
||||
'\t' + log.msg;
|
||||
if (log.obj !== 'undefined')
|
||||
{
|
||||
formattedLog += '\t' + log.obj;
|
||||
}
|
||||
formattedLog += '\n';
|
||||
|
||||
formattedLogs += formattedLog;
|
||||
@ -157,7 +159,8 @@ export class Logger {
|
||||
|
||||
// send logs to the server or display them in the console:
|
||||
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 (typeof pako !== 'undefined')
|
||||
@ -189,10 +192,9 @@ export class Logger {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a custom console layout.
|
||||
*
|
||||
*
|
||||
* @name module:core.Logger#_customConsoleLayout
|
||||
* @private
|
||||
* @return {*} the custom layout
|
||||
@ -219,10 +221,13 @@ export class Logger {
|
||||
{
|
||||
// look for entry immediately after those of log4javascript:
|
||||
for (let entry of stackEntries)
|
||||
if (entry.indexOf('log4javascript.min.js') <= 0) {
|
||||
{
|
||||
if (entry.indexOf('log4javascript.min.js') <= 0)
|
||||
{
|
||||
relevantEntry = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const buf = relevantEntry.split(':');
|
||||
const line = buf[buf.length - 2];
|
||||
@ -242,15 +247,17 @@ export class Logger {
|
||||
let buf = relevantEntry.split(' ');
|
||||
let fileLine = buf.pop();
|
||||
const method = buf.pop();
|
||||
buf = fileLine.split(':'); buf.pop();
|
||||
buf = fileLine.split(':');
|
||||
buf.pop();
|
||||
const line = buf.pop();
|
||||
const file = buf.pop().split('/').pop();
|
||||
|
||||
return method + ' ' + file + ' ' + line;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
/**
|
||||
* Base class for all stimuli.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
import { PsychObject } from '../util/PsychObject';
|
||||
import { PsychoJS } from './PsychoJS';
|
||||
import {PsychObject} from '../util/PsychObject';
|
||||
import {PsychoJS} from './PsychoJS';
|
||||
|
||||
|
||||
/**
|
||||
* <p>MinimalStim is the base class for all stimuli.</p>
|
||||
*
|
||||
*
|
||||
* @name module:core.MinimalStim
|
||||
* @class
|
||||
* @extends PsychObject
|
||||
@ -26,11 +26,11 @@ import { PsychoJS } from './PsychoJS';
|
||||
export class MinimalStim extends PsychObject
|
||||
{
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
autoDraw = false,
|
||||
autoLog = win.autoLog
|
||||
} = {})
|
||||
name,
|
||||
win,
|
||||
autoDraw = false,
|
||||
autoLog = win.autoLog
|
||||
} = {})
|
||||
{
|
||||
super(win._psychoJS, name);
|
||||
|
||||
@ -55,31 +55,41 @@ export class MinimalStim extends PsychObject
|
||||
*/
|
||||
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);
|
||||
|
||||
const index = this.win._drawList.indexOf(this);
|
||||
|
||||
// autoDraw = true: add the stimulus to the draw list if it's not there already
|
||||
if (this._autoDraw) {
|
||||
if (this.win) {
|
||||
if (this._autoDraw)
|
||||
{
|
||||
if (this.win)
|
||||
{
|
||||
// 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:
|
||||
this._updateIfNeeded();
|
||||
if (typeof this._pixi === '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'});
|
||||
else {
|
||||
}// throw Object.assign(response, { error: 'the PIXI representation of the stimulus is unavailable'});
|
||||
else
|
||||
{
|
||||
this.win._rootContainer.addChild(this._pixi);
|
||||
this.win._drawList.push(this);
|
||||
}
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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:
|
||||
if (this._needUpdate && typeof this._pixi !== 'undefined') {
|
||||
if (this._needUpdate && typeof this._pixi !== 'undefined')
|
||||
{
|
||||
this.win._rootContainer.removeChild(this._pixi);
|
||||
this._updateIfNeeded();
|
||||
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
|
||||
else {
|
||||
if (this.win) {
|
||||
else
|
||||
{
|
||||
if (this.win)
|
||||
{
|
||||
// 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);
|
||||
if (typeof this._pixi !== 'undefined')
|
||||
{
|
||||
this.win._rootContainer.removeChild(this._pixi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +123,7 @@ export class MinimalStim extends PsychObject
|
||||
|
||||
/**
|
||||
* Draw this stimulus on the next frame draw.
|
||||
*
|
||||
*
|
||||
* @name module:core.MinimalStim#draw
|
||||
* @function
|
||||
* @public
|
||||
@ -127,7 +142,7 @@ export class MinimalStim extends PsychObject
|
||||
|
||||
/**
|
||||
* Determine whether an object is inside this stimulus.
|
||||
*
|
||||
*
|
||||
* @name module:core.MinimalStim#contains
|
||||
* @function
|
||||
* @abstract
|
||||
@ -137,7 +152,11 @@ export class MinimalStim extends PsychObject
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* Note: this is an abstract function, which should not be called.
|
||||
*
|
||||
*
|
||||
* @name module:core.MinimalStim#_updateIfNeeded
|
||||
* @function
|
||||
* @abstract
|
||||
@ -153,6 +172,10 @@ export class MinimalStim extends PsychObject
|
||||
*/
|
||||
_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.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
import { PsychoJS } from './PsychoJS';
|
||||
import { PsychObject } from '../util/PsychObject';
|
||||
import {PsychoJS} from './PsychoJS';
|
||||
import {PsychObject} from '../util/PsychObject';
|
||||
import * as util from '../util/Util';
|
||||
|
||||
|
||||
/**
|
||||
* <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>
|
||||
*
|
||||
*
|
||||
* @name module:core.Mouse
|
||||
* @class
|
||||
* @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 {Window} options.win - the associated Window
|
||||
* @param {boolean} [options.autoLog= true] - whether or not to log
|
||||
*
|
||||
*
|
||||
* @todo visible is not handled at the moment (mouse is always visible)
|
||||
*/
|
||||
export class Mouse extends PsychObject {
|
||||
export class Mouse extends PsychObject
|
||||
{
|
||||
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
autoLog = true
|
||||
} = {}) {
|
||||
name,
|
||||
win,
|
||||
autoLog = true
|
||||
} = {})
|
||||
{
|
||||
super(win._psychoJS, name);
|
||||
|
||||
// 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.
|
||||
*
|
||||
*
|
||||
* @name module:core.Mouse#getPos
|
||||
* @function
|
||||
* @public
|
||||
* @return {Array.number} the position of the mouse in mouse/Window units
|
||||
*/
|
||||
getPos() {
|
||||
getPos()
|
||||
{
|
||||
// get mouse position in the canvas:
|
||||
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
||||
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
|
||||
* or getPos, in mouse/Window units.
|
||||
*
|
||||
*
|
||||
* @name module:core.Mouse#getRel
|
||||
* @function
|
||||
* @public
|
||||
* @return {Array.number} the relation position of the mouse in mouse/Window units.
|
||||
*/
|
||||
getRel() {
|
||||
getRel()
|
||||
{
|
||||
if (typeof this._lastPos === 'undefined')
|
||||
{
|
||||
return this.getPos();
|
||||
else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// note: (this.getPos()-lastPos) would not work here since getPos changes this._lastPos
|
||||
const lastPos = this._lastPos;
|
||||
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.
|
||||
*
|
||||
*
|
||||
* <p>Note: Even though this method returns a [x, y] array, for most wheels/systems y is the only
|
||||
* value that varies.</p>
|
||||
*
|
||||
*
|
||||
* @name module:core.Mouse#getWheelRel
|
||||
* @function
|
||||
* @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.
|
||||
*
|
||||
*
|
||||
* <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
|
||||
* @function
|
||||
* @public
|
||||
* @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.
|
||||
*/
|
||||
getPressed(getTime = false) {
|
||||
getPressed(getTime = false)
|
||||
{
|
||||
const buttonPressed = this.psychoJS.eventManager.getMouseInfo().buttons.pressed.slice();
|
||||
if (!getTime)
|
||||
{
|
||||
return buttonPressed;
|
||||
else {
|
||||
}
|
||||
else
|
||||
{
|
||||
const buttonTimes = this.psychoJS.eventManager.getMouseInfo().buttons.times.slice();
|
||||
return [buttonPressed, buttonTimes];
|
||||
}
|
||||
@ -141,7 +152,7 @@ export class Mouse extends PsychObject {
|
||||
|
||||
/**
|
||||
* Determine whether the mouse has moved beyond a certain distance.
|
||||
*
|
||||
*
|
||||
* <p><b>distance</b>
|
||||
* <ul>
|
||||
* <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,number], false): determine whether the mouse has travelled horizontally or vertically further then the given horizontal and vertical distances</li>
|
||||
* </ul></p>
|
||||
*
|
||||
*
|
||||
* <p><b>reset</b>
|
||||
* <ul>
|
||||
* <li>mouseMoved(distance, true): reset the mouse move clock, 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>
|
||||
* </ul></p>
|
||||
*
|
||||
*
|
||||
* @name module:core.Mouse#mouseMoved
|
||||
* @function
|
||||
* @public
|
||||
@ -164,92 +175,120 @@ export class Mouse extends PsychObject {
|
||||
* @param {boolean|String|Array.number} [reset= false] - 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:
|
||||
if (typeof this._lastPos === 'undefined')
|
||||
{
|
||||
this.getPos();
|
||||
}
|
||||
this._prevPos = this._lastPos.slice();
|
||||
this.getPos();
|
||||
|
||||
if (typeof reset === 'boolean' && reset == false) {
|
||||
if (typeof reset === 'boolean' && reset == false)
|
||||
{
|
||||
if (typeof distance === 'undefined')
|
||||
{
|
||||
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]));
|
||||
return (this._movedistance > distance);
|
||||
}
|
||||
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)
|
||||
return true; // moved on Y-axis
|
||||
{
|
||||
return true;
|
||||
} // moved on Y-axis
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
else if (typeof reset === 'boolean' && reset == true) {
|
||||
else if (typeof reset === 'boolean' && reset == true)
|
||||
{
|
||||
// reset the moveClock:
|
||||
this.psychoJS.eventManager.getMouseInfo().moveClock.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
else if (reset === 'here') {
|
||||
else if (reset === 'here')
|
||||
{
|
||||
// set to wherever we are
|
||||
this._prevPos = this._lastPos.clone();
|
||||
return false;
|
||||
}
|
||||
|
||||
else if (reset instanceof Array) {
|
||||
else if (reset instanceof Array)
|
||||
{
|
||||
// an (x,y) array
|
||||
// reset to (x,y) to check movement from there
|
||||
this._prevPos = reset.slice();
|
||||
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
|
||||
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]));
|
||||
return (this._movedistance > distance);
|
||||
}
|
||||
|
||||
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])
|
||||
return true; // moved on Y-axis
|
||||
{
|
||||
return true;
|
||||
} // moved on Y-axis
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the amount of time elapsed since the last mouse movement.
|
||||
*
|
||||
*
|
||||
* @name module:core.Mouse#mouseMoveTime
|
||||
* @function
|
||||
* @public
|
||||
* @return {number} the time elapsed since the last mouse movement
|
||||
*/
|
||||
mouseMoveTime() {
|
||||
mouseMoveTime()
|
||||
{
|
||||
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the clocks associated to the given mouse buttons.
|
||||
*
|
||||
*
|
||||
* @name module:core.Mouse#clickReset
|
||||
* @function
|
||||
* @public
|
||||
* @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();
|
||||
for (const b of buttons) {
|
||||
for (const b of buttons)
|
||||
{
|
||||
mouseInfo.buttons.clocks[b].reset();
|
||||
mouseInfo.buttons.times[b] = 0.0;
|
||||
}
|
||||
|
@ -3,24 +3,23 @@
|
||||
* Main component of the PsychoJS library.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { Scheduler } from '../util/Scheduler';
|
||||
import { ServerManager } from './ServerManager';
|
||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
||||
import { EventManager } from './EventManager';
|
||||
import { Window } from './Window';
|
||||
import { GUI } from './GUI';
|
||||
import { MonotonicClock } from '../util/Clock';
|
||||
import { Logger } from './Logger';
|
||||
import {Scheduler} from '../util/Scheduler';
|
||||
import {ServerManager} from './ServerManager';
|
||||
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||
import {EventManager} from './EventManager';
|
||||
import {Window} from './Window';
|
||||
import {GUI} from './GUI';
|
||||
import {MonotonicClock} from '../util/Clock';
|
||||
import {Logger} from './Logger';
|
||||
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>
|
||||
*
|
||||
@ -34,25 +33,81 @@ export class PsychoJS
|
||||
/**
|
||||
* Properties
|
||||
*/
|
||||
get status() { return this._status; }
|
||||
set status(status) {
|
||||
get status()
|
||||
{
|
||||
return this._status;
|
||||
}
|
||||
|
||||
set 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,
|
||||
collectIP = false,
|
||||
topLevelStatus = true
|
||||
} = {})
|
||||
} = {})
|
||||
{
|
||||
// logging:
|
||||
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
|
||||
// in order to accommodate PsychoPy's Code Components
|
||||
if (topLevelStatus)
|
||||
{
|
||||
this._makeStatusTopLevel();
|
||||
}
|
||||
|
||||
this.logger.info('[PsychoJS] Initialised.');
|
||||
this.logger.info('[PsychoJS] @version 2020.5');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the experiment's environment.
|
||||
*
|
||||
@ -119,10 +176,13 @@ export class PsychoJS
|
||||
getEnvironment()
|
||||
{
|
||||
if (typeof this._config === 'undefined')
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
return this._config.environment;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Open a PsychoJS Window.
|
||||
*
|
||||
@ -142,17 +202,24 @@ export class PsychoJS
|
||||
* @public
|
||||
*/
|
||||
openWindow({
|
||||
name,
|
||||
fullscr,
|
||||
color,
|
||||
units,
|
||||
waitBlanking,
|
||||
autoLog
|
||||
} = {}) {
|
||||
name,
|
||||
fullscr,
|
||||
color,
|
||||
units,
|
||||
waitBlanking,
|
||||
autoLog
|
||||
} = {})
|
||||
{
|
||||
this.logger.info('[PsychoJS] Open Window.');
|
||||
|
||||
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({
|
||||
psychoJS: this,
|
||||
@ -172,7 +239,8 @@ export class PsychoJS
|
||||
* @param {string} completionUrl - the completion URL
|
||||
* @param {string} cancellationUrl - the cancellation URL
|
||||
*/
|
||||
setRedirectUrls(completionUrl, cancellationUrl) {
|
||||
setRedirectUrls(completionUrl, cancellationUrl)
|
||||
{
|
||||
this._completionUrl = completionUrl;
|
||||
this._cancellationUrl = cancellationUrl;
|
||||
}
|
||||
@ -185,7 +253,8 @@ export class PsychoJS
|
||||
* @param args - arguments for that task
|
||||
* @public
|
||||
*/
|
||||
schedule(task, args) {
|
||||
schedule(task, args)
|
||||
{
|
||||
this.logger.debug('schedule task: ', task.toString().substring(0, 50), '...');
|
||||
|
||||
this._scheduler.add(task, args);
|
||||
@ -204,7 +273,8 @@ export class PsychoJS
|
||||
* @param {Scheduler} elseScheduler scheduler to run if the condition is false
|
||||
* @public
|
||||
*/
|
||||
scheduleCondition(condition, thenScheduler, elseScheduler) {
|
||||
scheduleCondition(condition, thenScheduler, elseScheduler)
|
||||
{
|
||||
this.logger.debug('schedule condition: ', condition.toString().substring(0, 50), '...');
|
||||
|
||||
this._scheduler.addConditional(condition, thenScheduler, elseScheduler);
|
||||
@ -224,27 +294,31 @@ export class PsychoJS
|
||||
*
|
||||
* @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();
|
||||
|
||||
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:
|
||||
await this._configure(configURL, expName);
|
||||
|
||||
// get the participant IP:
|
||||
if (this._collectIP)
|
||||
{
|
||||
this._getParticipantIPInfo();
|
||||
else {
|
||||
}
|
||||
else
|
||||
{
|
||||
this._IP = {
|
||||
IP: 'X',
|
||||
hostname : 'X',
|
||||
city : 'X',
|
||||
region : 'X',
|
||||
country : 'X',
|
||||
location : 'X'
|
||||
hostname: 'X',
|
||||
city: 'X',
|
||||
region: 'X',
|
||||
country: 'X',
|
||||
location: 'X'
|
||||
};
|
||||
}
|
||||
|
||||
@ -264,21 +338,42 @@ export class PsychoJS
|
||||
// open a session:
|
||||
await this._serverManager.openSession();
|
||||
|
||||
// attempt to close the session on beforeunload/unload (we use a synchronous request since
|
||||
// the Beacon API only allows POST and we need DELETE ) and release the WebGL context:
|
||||
const self = this;
|
||||
window.onbeforeunload = () => {
|
||||
self._serverManager.closeSession(false, true);
|
||||
// warn the user when they attempt to close the tab or browser:
|
||||
this.beforeunloadCallback = (event) =>
|
||||
{
|
||||
// preventDefault should ensure that the user gets prompted:
|
||||
event.preventDefault();
|
||||
|
||||
if (typeof self._window !== 'undefined')
|
||||
self._window.close();
|
||||
// Chrome requires returnValue to be set:
|
||||
event.returnValue = '';
|
||||
};
|
||||
window.addEventListener('unload', function(event) {
|
||||
self._serverManager.closeSession(false, true);
|
||||
window.addEventListener('beforeunload', this.beforeunloadCallback);
|
||||
|
||||
|
||||
// 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')
|
||||
{
|
||||
self._window.close();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -289,9 +384,10 @@ export class PsychoJS
|
||||
this.logger.info('[PsychoJS] Start Experiment.');
|
||||
this._scheduler.start();
|
||||
}
|
||||
catch (error) {
|
||||
catch (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
|
||||
* @public
|
||||
*/
|
||||
async downloadResources(resources = []) {
|
||||
try {
|
||||
async downloadResources(resources = [])
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.serverManager.downloadResources(resources);
|
||||
}
|
||||
catch (error) {
|
||||
catch (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
|
||||
* @public
|
||||
*/
|
||||
importAttributes(obj) {
|
||||
importAttributes(obj)
|
||||
{
|
||||
this.logger.debug('import attributes from: ', util.toString(obj));
|
||||
|
||||
if (typeof obj === 'undefined')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const attribute in obj) {
|
||||
for (const attribute in obj)
|
||||
{
|
||||
// this[attribute] = obj[attribute];
|
||||
window[attribute] = obj[attribute];
|
||||
}
|
||||
@ -354,26 +457,38 @@ export class PsychoJS
|
||||
* @async
|
||||
* @public
|
||||
*/
|
||||
async quit({ message, isCompleted = false } = {}) {
|
||||
async quit({message, isCompleted = false} = {})
|
||||
{
|
||||
this.logger.info('[PsychoJS] Quit.');
|
||||
|
||||
this._experiment.experimentEnded = true;
|
||||
this._status = PsychoJS.Status.FINISHED;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
// stop the main scheduler:
|
||||
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:
|
||||
this.gui.dialog({
|
||||
warning: 'Closing the session. Please wait a few moments.',
|
||||
showOK: false
|
||||
});
|
||||
await this._experiment.save();
|
||||
await this._logger.flush();
|
||||
if (isCompleted || this._config.experiment.saveIncompleteResults)
|
||||
{
|
||||
await this._experiment.save();
|
||||
await this._logger.flush();
|
||||
}
|
||||
|
||||
// close the session:
|
||||
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER) {
|
||||
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
|
||||
{
|
||||
await this._serverManager.closeSession(isCompleted);
|
||||
}
|
||||
|
||||
@ -383,29 +498,37 @@ export class PsychoJS
|
||||
const self = this;
|
||||
this._gui.dialog({
|
||||
message: text,
|
||||
onOK: () => {
|
||||
onOK: () =>
|
||||
{
|
||||
// close the window:
|
||||
self._window.close();
|
||||
|
||||
// remove everything from the browser window:
|
||||
while (document.body.hasChildNodes())
|
||||
{
|
||||
document.body.removeChild(document.body.lastChild);
|
||||
}
|
||||
|
||||
// return from fullscreen if we were there:
|
||||
this._window.closeFullScreen();
|
||||
|
||||
// redirect if redirection URLs have been provided:
|
||||
if (isCompleted && typeof self._completionUrl !== 'undefined')
|
||||
{
|
||||
window.location = self._completionUrl;
|
||||
}
|
||||
else if (!isCompleted && typeof self._cancellationUrl !== 'undefined')
|
||||
{
|
||||
window.location = self._cancellationUrl;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
catch (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} name - the name of the experiment
|
||||
*/
|
||||
async _configure(configURL, name) {
|
||||
const response = { origin: 'PsychoJS.configure', context: 'when configuring PsychoJS for the experiment' };
|
||||
async _configure(configURL, name)
|
||||
{
|
||||
const response = {origin: 'PsychoJS.configure', context: 'when configuring PsychoJS for the experiment'};
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
this.status = PsychoJS.Status.CONFIGURING;
|
||||
|
||||
// if the experiment is running from the pavlovia.org server, we read the configuration file:
|
||||
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);
|
||||
this._config = serverResponse.config;
|
||||
|
||||
// legacy experiments had a psychoJsManager block instead of a pavlovia block, and the URL
|
||||
// pointed to https://pavlovia.org/server
|
||||
if ('psychoJsManager' in this._config) {
|
||||
if ('psychoJsManager' in this._config)
|
||||
{
|
||||
delete this._config.psychoJsManager;
|
||||
this._config.pavlovia = {
|
||||
URL: 'https://pavlovia.org'
|
||||
@ -441,41 +568,56 @@ export class PsychoJS
|
||||
|
||||
// tests for the presence of essential blocks in the configuration:
|
||||
if (!('experiment' in this._config))
|
||||
{
|
||||
throw 'missing experiment block in configuration';
|
||||
}
|
||||
if (!('name' in this._config.experiment))
|
||||
{
|
||||
throw 'missing name in experiment block in configuration';
|
||||
}
|
||||
if (!('fullpath' in this._config.experiment))
|
||||
{
|
||||
throw 'missing fullpath in experiment block in configuration';
|
||||
}
|
||||
if (!('pavlovia' in this._config))
|
||||
{
|
||||
throw 'missing pavlovia block in configuration';
|
||||
}
|
||||
if (!('URL' in this._config.pavlovia))
|
||||
{
|
||||
throw 'missing URL in pavlovia block in configuration';
|
||||
}
|
||||
|
||||
this._config.environment = ExperimentHandler.Environment.SERVER;
|
||||
|
||||
} else
|
||||
}
|
||||
else
|
||||
// otherwise we create an ad-hoc configuration:
|
||||
{
|
||||
this._config = {
|
||||
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):
|
||||
this._serverMsg = new Map();
|
||||
util.getUrlParameters().forEach((value, key) => {
|
||||
util.getUrlParameters().forEach((value, key) =>
|
||||
{
|
||||
if (key.indexOf('__') === 0)
|
||||
{
|
||||
this._serverMsg.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.status = PsychoJS.Status.CONFIGURED;
|
||||
this.logger.debug('configuration:', util.toString(this._config));
|
||||
}
|
||||
catch (error) {
|
||||
catch (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>
|
||||
* @protected
|
||||
*/
|
||||
async _getParticipantIPInfo() {
|
||||
const response = { origin: 'PsychoJS._getParticipantIPInfo', context: 'when getting the IP information of the participant' };
|
||||
async _getParticipantIPInfo()
|
||||
{
|
||||
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._IP = {};
|
||||
try {
|
||||
try
|
||||
{
|
||||
const geoResponse = await $.get('http://www.geoplugin.net/json.gp');
|
||||
const geoData = JSON.parse(geoResponse);
|
||||
this._IP = {
|
||||
@ -503,9 +650,10 @@ export class PsychoJS
|
||||
};
|
||||
this.logger.debug('IP information of the participant: ' + util.toString(this._IP));
|
||||
}
|
||||
catch (error) {
|
||||
catch (error)
|
||||
{
|
||||
// throw { ...response, error };
|
||||
throw Object.assign(response, { error });
|
||||
throw Object.assign(response, {error});
|
||||
}
|
||||
}
|
||||
|
||||
@ -515,13 +663,15 @@ export class PsychoJS
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_captureErrors() {
|
||||
_captureErrors()
|
||||
{
|
||||
this.logger.debug('capturing all errors using window.onerror');
|
||||
|
||||
const self = this;
|
||||
window.onerror = function (message, source, lineno, colno, error) {
|
||||
window.onerror = function (message, source, lineno, colno, error)
|
||||
{
|
||||
console.error(error);
|
||||
self._gui.dialog({ "error": error });
|
||||
self._gui.dialog({"error": error});
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -539,8 +689,10 @@ export class PsychoJS
|
||||
* Make the various Status top level, in order to accommodate PsychoPy's Code Components.
|
||||
* @private
|
||||
*/
|
||||
_makeStatusTopLevel() {
|
||||
for (const status in PsychoJS.Status) {
|
||||
_makeStatusTopLevel()
|
||||
{
|
||||
for (const status in PsychoJS.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.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { PsychoJS } from './PsychoJS';
|
||||
import { PsychObject } from '../util/PsychObject';
|
||||
import {PsychoJS} from './PsychoJS';
|
||||
import {PsychObject} from '../util/PsychObject';
|
||||
import * as util from '../util/Util';
|
||||
import {ExperimentHandler} from "../data/ExperimentHandler";
|
||||
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 {boolean} [options.autoLog= false] - whether or not to log
|
||||
*/
|
||||
export class ServerManager extends PsychObject {
|
||||
export class ServerManager extends PsychObject
|
||||
{
|
||||
|
||||
constructor({
|
||||
psychoJS,
|
||||
autoLog = false
|
||||
} = {}) {
|
||||
psychoJS,
|
||||
autoLog = false
|
||||
} = {})
|
||||
{
|
||||
super(psychoJS);
|
||||
|
||||
// session:
|
||||
@ -66,15 +68,22 @@ export class ServerManager extends PsychObject {
|
||||
*
|
||||
* @returns {Promise<ServerManager.GetConfigurationPromise>} the response
|
||||
*/
|
||||
getConfiguration(configURL) {
|
||||
const response = { origin: 'ServerManager.getConfiguration', context: 'when reading the configuration file: ' + configURL };
|
||||
getConfiguration(configURL)
|
||||
{
|
||||
const response = {
|
||||
origin: 'ServerManager.getConfiguration',
|
||||
context: 'when 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')
|
||||
.done((config, textStatus) => {
|
||||
.done((config, textStatus) =>
|
||||
{
|
||||
// resolve({ ...response, config });
|
||||
resolve(Object.assign(response, { config }));
|
||||
resolve(Object.assign(response, {config}));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) =>
|
||||
{
|
||||
@ -83,7 +92,7 @@ export class ServerManager extends PsychObject {
|
||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||
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:
|
||||
let data = {};
|
||||
if (this._psychoJS._serverMsg.has('__pilotToken'))
|
||||
{
|
||||
data.pilotToken = this._psychoJS._serverMsg.get('__pilotToken');
|
||||
}
|
||||
|
||||
// query pavlovia server:
|
||||
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';
|
||||
$.post(url, data, null, 'json')
|
||||
.done((data, textStatus) =>
|
||||
{
|
||||
if (!('token' in data)) {
|
||||
.done((data, textStatus) =>
|
||||
{
|
||||
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);
|
||||
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)) {
|
||||
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.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;
|
||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||
console.error('error:', errorMsg);
|
||||
|
||||
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);
|
||||
|
||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||
console.error('error:', errorMsg);
|
||||
|
||||
reject(Object.assign(response, { 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
|
||||
* @returns {Promise<ServerManager.CloseSessionPromise> | void} the response
|
||||
*/
|
||||
closeSession(isCompleted = false, sync = false) {
|
||||
const response = { origin: 'ServerManager.closeSession', context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath };
|
||||
async closeSession(isCompleted = false, sync = false)
|
||||
{
|
||||
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);
|
||||
|
||||
@ -187,44 +208,61 @@ export class ServerManager extends PsychObject {
|
||||
|
||||
// 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 data = { isCompleted };
|
||||
|
||||
// synchronous query the pavlovia server:
|
||||
if (sync)
|
||||
{
|
||||
/* This is now deprecated in most browsers.
|
||||
const request = new XMLHttpRequest();
|
||||
request.open("DELETE", url, false);
|
||||
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
||||
request.send(JSON.stringify(data));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// asynchronously query the pavlovia server:
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
$.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 }));
|
||||
*/
|
||||
/* This does not work in Chrome before of a CORS bug
|
||||
await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
||||
body: JSON.stringify(data),
|
||||
// keepalive makes it possible for the request to outlive the page (e.g. when the participant closes the tab)
|
||||
keepalive: true
|
||||
});
|
||||
});
|
||||
*/
|
||||
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
|
||||
* @throws {Object.<string, *>} exception if no resource with that name has previously been registered
|
||||
*/
|
||||
getResource(name) {
|
||||
const response = { origin: 'ServerManager.getResource', context: 'when getting the value of resource: ' + name };
|
||||
getResource(name)
|
||||
{
|
||||
const response = {
|
||||
origin: 'ServerManager.getResource',
|
||||
context: 'when getting the value of resource: ' + name
|
||||
};
|
||||
|
||||
const path_data = this._resources.get(name);
|
||||
if (typeof path_data === 'undefined')
|
||||
// throw { ...response, error: 'unknown resource' };
|
||||
throw Object.assign(response, { error: 'unknown resource' });
|
||||
// throw { ...response, error: 'unknown resource' };
|
||||
{
|
||||
throw Object.assign(response, {error: 'unknown resource'});
|
||||
}
|
||||
|
||||
return path_data.data;
|
||||
}
|
||||
@ -257,17 +301,25 @@ export class ServerManager extends PsychObject {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
setStatus(status) {
|
||||
const response = { origin: 'ServerManager.setStatus', context: 'when changing the status of the server manager to: ' + util.toString(status) };
|
||||
setStatus(status)
|
||||
{
|
||||
const response = {
|
||||
origin: 'ServerManager.setStatus',
|
||||
context: 'when changing the status of the server manager to: ' + util.toString(status)
|
||||
};
|
||||
|
||||
// check status:
|
||||
const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
|
||||
if (!statusKey)
|
||||
// throw { ...response, error: 'status must be a symbol' };
|
||||
throw Object.assign(response, { error: 'status must be a symbol' });
|
||||
// throw { ...response, error: 'status must be a symbol' };
|
||||
{
|
||||
throw Object.assign(response, {error: 'status must be a symbol'});
|
||||
}
|
||||
if (!ServerManager.Status.hasOwnProperty(statusKey))
|
||||
// throw { ...response, error: 'unknown status' };
|
||||
throw Object.assign(response, { error: 'unknown status' });
|
||||
// throw { ...response, error: 'unknown status' };
|
||||
{
|
||||
throw Object.assign(response, {error: 'unknown status'});
|
||||
}
|
||||
|
||||
this._status = status;
|
||||
|
||||
@ -286,7 +338,8 @@ export class ServerManager extends PsychObject {
|
||||
* @public
|
||||
* @return {ServerManager.Status.READY} the new status
|
||||
*/
|
||||
resetStatus() {
|
||||
resetStatus()
|
||||
{
|
||||
return this.setStatus(ServerManager.Status.READY);
|
||||
}
|
||||
|
||||
@ -307,8 +360,12 @@ export class ServerManager extends PsychObject {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
downloadResources(resources = []) {
|
||||
const response = { origin: 'ServerManager.downloadResources', context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name };
|
||||
downloadResources(resources = [])
|
||||
{
|
||||
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);
|
||||
|
||||
@ -316,26 +373,37 @@ export class ServerManager extends PsychObject {
|
||||
// but we want to run the asynchronous _listResources and _downloadResources in sequence
|
||||
const self = this;
|
||||
const newResources = new Map();
|
||||
let download = async () => {
|
||||
try {
|
||||
if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER) {
|
||||
let download = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER)
|
||||
{
|
||||
// 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:
|
||||
const serverResponse = await self._listResources();
|
||||
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:
|
||||
for (const {name, path} of resources) {
|
||||
for (const {name, path} of resources)
|
||||
{
|
||||
self._resources.set(name, {path});
|
||||
newResources.set(name, {path});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// register the specified resources:
|
||||
for (const {name, path} of resources) {
|
||||
for (const {name, path} of resources)
|
||||
{
|
||||
self._resources.set(name, {path});
|
||||
newResources.set(name, {path});
|
||||
}
|
||||
@ -343,17 +411,23 @@ export class ServerManager extends PsychObject {
|
||||
|
||||
self._nbResources = self._resources.size;
|
||||
for (const name of self._resources.keys())
|
||||
{
|
||||
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:
|
||||
await self._downloadRegisteredResources(newResources);
|
||||
}
|
||||
catch (error) {
|
||||
catch (error)
|
||||
{
|
||||
console.log('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
|
||||
* @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 {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
|
||||
*
|
||||
* @returns {Promise<ServerManager.UploadDataPromise>} the response
|
||||
*/
|
||||
uploadData(key, value)
|
||||
uploadData(key, value, sync = false)
|
||||
{
|
||||
const response = {
|
||||
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.setStatus(ServerManager.Status.BUSY);
|
||||
|
||||
// prepare the POST query:
|
||||
const data = {
|
||||
key,
|
||||
value
|
||||
};
|
||||
const url = this._psychoJS.config.pavlovia.URL +
|
||||
'/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) +
|
||||
'/sessions/' + this._psychoJS.config.session.token +
|
||||
'/results';
|
||||
|
||||
// query the pavlovia server:
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) =>
|
||||
// synchronous query the pavlovia server:
|
||||
if (sync)
|
||||
{
|
||||
const url = self._psychoJS.config.pavlovia.URL +
|
||||
'/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) +
|
||||
'/sessions/' + self._psychoJS.config.session.token +
|
||||
'/results';
|
||||
|
||||
$.post(url, data, null, 'json')
|
||||
.done((serverData, textStatus) =>
|
||||
const formData = new FormData();
|
||||
formData.append('key', key);
|
||||
formData.append('value', value);
|
||||
navigator.sendBeacon(url, formData);
|
||||
}
|
||||
// asynchronously query the pavlovia server:
|
||||
else
|
||||
{
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
resolve(Object.assign(response, { serverData }));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) =>
|
||||
{
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
const data = {
|
||||
key,
|
||||
value
|
||||
};
|
||||
|
||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||
console.error('error:', errorMsg);
|
||||
$.post(url, data, null, 'json')
|
||||
.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.
|
||||
*
|
||||
@ -468,7 +552,7 @@ export class ServerManager extends PsychObject {
|
||||
.done((serverData, textStatus) =>
|
||||
{
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
resolve(Object.assign(response, { serverData }));
|
||||
resolve(Object.assign(response, {serverData}));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) =>
|
||||
{
|
||||
@ -477,14 +561,12 @@ export class ServerManager extends PsychObject {
|
||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||
console.error('error:', errorMsg);
|
||||
|
||||
reject(Object.assign(response, { error: errorMsg }));
|
||||
reject(Object.assign(response, {error: errorMsg}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* List the resources available to the experiment.
|
||||
|
||||
@ -524,18 +606,21 @@ export class ServerManager extends PsychObject {
|
||||
{
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
// 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))
|
||||
{
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
// 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);
|
||||
// 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) =>
|
||||
{
|
||||
@ -544,14 +629,13 @@ export class ServerManager extends PsychObject {
|
||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||
console.error('error:', errorMsg);
|
||||
|
||||
reject(Object.assign(response, { error: errorMsg }));
|
||||
reject(Object.assign(response, {error: errorMsg}));
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Download the resources previously registered.
|
||||
*
|
||||
@ -563,7 +647,10 @@ export class ServerManager extends PsychObject {
|
||||
*/
|
||||
_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);
|
||||
|
||||
@ -578,32 +665,43 @@ export class ServerManager extends PsychObject {
|
||||
|
||||
const filesToDownload = resources.size ? resources : this._resources;
|
||||
|
||||
this._resourceQueue.addEventListener("filestart", event => {
|
||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOADING_RESOURCE, resource: event.item.id });
|
||||
this._resourceQueue.addEventListener("filestart", event =>
|
||||
{
|
||||
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;
|
||||
let path_data = self._resources.get(event.item.id);
|
||||
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:
|
||||
this._resourceQueue.addEventListener("complete", event => {
|
||||
this._resourceQueue.addEventListener("complete", event =>
|
||||
{
|
||||
self._resourceQueue.close();
|
||||
if (self._nbLoadedResources === filesToDownload.size) {
|
||||
if (self._nbLoadedResources === filesToDownload.size)
|
||||
{
|
||||
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
|
||||
this._resourceQueue.addEventListener("error", event => {
|
||||
this._resourceQueue.addEventListener("error", event =>
|
||||
{
|
||||
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 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:
|
||||
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(extension) > -1)
|
||||
manifest.push({ id: name, src: path_data.path, type: createjs.Types.BINARY });
|
||||
|
||||
/* ascii .csv are adequately handled in binary format
|
||||
{
|
||||
manifest.push({id: name, src: path_data.path, type: createjs.Types.BINARY});
|
||||
}/* ascii .csv are adequately handled in binary format
|
||||
// forced text for .csv:
|
||||
else if (['csv'].indexOf(resourceExtension) > -1)
|
||||
manifest.push({ id: resourceName, src: resourceName, type: createjs.Types.TEXT });
|
||||
@ -637,29 +735,41 @@ export class ServerManager extends PsychObject {
|
||||
soundResources.push(name);
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
// preload.js for the other extensions (download type decided by preload.js):
|
||||
else
|
||||
manifest.push({ id: name, src: path_data.path });
|
||||
{
|
||||
manifest.push({id: name, src: path_data.path});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (*) start loading non-sound resources:
|
||||
if (manifest.length > 0)
|
||||
{
|
||||
this._resourceQueue.loadManifest(manifest);
|
||||
else {
|
||||
if (this._nbLoadedResources === filesToDownload.size) {
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this._nbLoadedResources === filesToDownload.size)
|
||||
{
|
||||
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:
|
||||
for (const name of soundResources) {
|
||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOADING_RESOURCE, resource: name });
|
||||
for (const name of soundResources)
|
||||
{
|
||||
self.emit(ServerManager.Event.RESOURCE, {
|
||||
message: ServerManager.Event.DOWNLOADING_RESOURCE,
|
||||
resource: name
|
||||
});
|
||||
const path_data = self._resources.get(name);
|
||||
const howl = new Howl({
|
||||
src: path_data.path,
|
||||
@ -667,20 +777,26 @@ export class ServerManager extends PsychObject {
|
||||
autoplay: false
|
||||
});
|
||||
|
||||
howl.on('load', (event) => {
|
||||
howl.on('load', (event) =>
|
||||
{
|
||||
++self._nbLoadedResources;
|
||||
path_data.data = 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.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 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();
|
||||
|
@ -2,15 +2,15 @@
|
||||
* Window responsible for displaying the experiment stimuli
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
import { Color } from '../util/Color';
|
||||
import { PsychObject } from '../util/PsychObject';
|
||||
import { MonotonicClock } from '../util/Clock';
|
||||
import { Logger } from "./Logger";
|
||||
import {Color} from '../util/Color';
|
||||
import {PsychObject} from '../util/PsychObject';
|
||||
import {MonotonicClock} from '../util/Clock';
|
||||
import {Logger} from "./Logger";
|
||||
|
||||
/**
|
||||
* <p>Window displays the various stimuli of the experiment.</p>
|
||||
@ -29,7 +29,8 @@ import { Logger } from "./Logger";
|
||||
* before flipping
|
||||
* @param {boolean} [options.autoLog= true] whether or not to log
|
||||
*/
|
||||
export class Window extends PsychObject {
|
||||
export class Window extends PsychObject
|
||||
{
|
||||
|
||||
/**
|
||||
* Getter for monitorFramePeriod.
|
||||
@ -38,17 +39,20 @@ export class Window extends PsychObject {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
get monitorFramePeriod() { return this._monitorFramePeriod; }
|
||||
get monitorFramePeriod()
|
||||
{
|
||||
return this._monitorFramePeriod;
|
||||
}
|
||||
|
||||
constructor({
|
||||
psychoJS,
|
||||
name,
|
||||
fullscr = false,
|
||||
color = new Color('black'),
|
||||
units = 'pix',
|
||||
waitBlanking = false,
|
||||
autoLog = true
|
||||
} = {})
|
||||
psychoJS,
|
||||
name,
|
||||
fullscr = false,
|
||||
color = new Color('black'),
|
||||
units = 'pix',
|
||||
waitBlanking = false,
|
||||
autoLog = true
|
||||
} = {})
|
||||
{
|
||||
super(psychoJS, name);
|
||||
|
||||
@ -76,7 +80,8 @@ export class Window extends PsychObject {
|
||||
// fullscreen listener:
|
||||
this._windowAlreadyInFullScreen = false;
|
||||
const self = this;
|
||||
document.addEventListener('fullscreenchange', (event) => {
|
||||
document.addEventListener('fullscreenchange', (event) =>
|
||||
{
|
||||
self._windowAlreadyInFullScreen = !!document.fullscreenElement;
|
||||
|
||||
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:
|
||||
self._needUpdate = true;
|
||||
for (const stimulus of self._drawList)
|
||||
{
|
||||
stimulus._needUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -105,10 +114,14 @@ export class Window extends PsychObject {
|
||||
close()
|
||||
{
|
||||
if (!this._renderer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.body.contains(this._renderer.view))
|
||||
{
|
||||
document.body.removeChild(this._renderer.view);
|
||||
}
|
||||
|
||||
// destroy the renderer and the WebGL context:
|
||||
if (typeof this._renderer.gl !== 'undefined')
|
||||
@ -173,16 +186,21 @@ export class Window extends PsychObject {
|
||||
});
|
||||
}
|
||||
else if (typeof document.documentElement.mozRequestFullScreen === 'function')
|
||||
{
|
||||
document.documentElement.mozRequestFullScreen();
|
||||
|
||||
}
|
||||
else if (typeof document.documentElement.webkitRequestFullscreen === 'function')
|
||||
{
|
||||
document.documentElement.webkitRequestFullscreen();
|
||||
|
||||
}
|
||||
else if (typeof document.documentElement.msRequestFullscreen === 'function')
|
||||
{
|
||||
document.documentElement.msRequestFullscreen();
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.psychoJS.logger.warn('Unable to go fullscreen.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -210,16 +228,21 @@ export class Window extends PsychObject {
|
||||
});
|
||||
}
|
||||
else if (typeof document.mozCancelFullScreen === 'function')
|
||||
{
|
||||
document.mozCancelFullScreen();
|
||||
|
||||
}
|
||||
else if (typeof document.webkitExitFullscreen === 'function')
|
||||
{
|
||||
document.webkitExitFullscreen();
|
||||
|
||||
}
|
||||
else if (typeof document.msExitFullscreen === 'function')
|
||||
{
|
||||
document.msExitFullscreen();
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.psychoJS.logger.warn('Unable to close fullscreen.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -241,9 +264,10 @@ export class Window extends PsychObject {
|
||||
logOnFlip({
|
||||
msg,
|
||||
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()
|
||||
{
|
||||
if (!this._renderer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this._frameCount++;
|
||||
@ -297,12 +323,16 @@ export class Window extends PsychObject {
|
||||
|
||||
// blocks execution until the rendering is fully done:
|
||||
if (this._waitBlanking)
|
||||
{
|
||||
this._renderer.gl.finish();
|
||||
}
|
||||
}
|
||||
|
||||
// call the callOnFlip functions and remove them:
|
||||
for (let callback of this._flipCallbacks)
|
||||
{
|
||||
callback['function'](...callback['arguments']);
|
||||
}
|
||||
this._flipCallbacks = [];
|
||||
|
||||
// log:
|
||||
@ -325,7 +355,9 @@ export class Window extends PsychObject {
|
||||
if (this._needUpdate)
|
||||
{
|
||||
if (this._renderer)
|
||||
{
|
||||
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:
|
||||
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
|
||||
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);
|
||||
stimulus._updateIfNeeded();
|
||||
this._rootContainer.addChild(stimulus._pixi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -368,7 +403,9 @@ export class Window extends PsychObject {
|
||||
this._needUpdate = true;
|
||||
|
||||
for (const stimulus of this._drawList)
|
||||
{
|
||||
stimulus.refresh();
|
||||
}
|
||||
|
||||
this._refresh();
|
||||
}
|
||||
@ -413,7 +450,8 @@ export class Window extends PsychObject {
|
||||
this.psychoJS.eventManager.addMouseListeners(this._renderer);
|
||||
|
||||
// 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);
|
||||
this._fullRefresh();
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Mixin implementing various unit-handling measurement methods.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -10,150 +10,185 @@
|
||||
|
||||
/**
|
||||
* <p>This mixin implements various unit-handling measurement methods.</p>
|
||||
*
|
||||
*
|
||||
* <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.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @name module:core.WindowMixin
|
||||
* @mixin
|
||||
*
|
||||
*
|
||||
*/
|
||||
export let WindowMixin = (superclass) => class extends superclass {
|
||||
constructor(args) {
|
||||
export let WindowMixin = (superclass) => class extends superclass
|
||||
{
|
||||
constructor(args)
|
||||
{
|
||||
super(args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for units attribute.
|
||||
*
|
||||
*
|
||||
* @name module:core.WindowMixin#setUnits
|
||||
* @function
|
||||
* @public
|
||||
* @param {String} [units= this.win.units] - the units
|
||||
* @param {boolean} [log= false] - whether or not to log
|
||||
*/
|
||||
setUnits(units = this.win.units, log = false) {
|
||||
setUnits(units = this.win.units, log = false)
|
||||
{
|
||||
this._setAttribute('units', units, log);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert the given length from stimulus unit to pixel units.
|
||||
*
|
||||
*
|
||||
* @name module:core.WindowMixin#_getLengthPix
|
||||
* @function
|
||||
* @protected
|
||||
* @param {number} length - the length in stimulus units
|
||||
* @return {number} - the length in pixel units
|
||||
*/
|
||||
_getLengthPix(length) {
|
||||
let response = { origin: 'WindowMixin._getLengthPix', context: 'when converting a length from stimulus unit to pixel units' };
|
||||
_getLengthPix(length)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
||||
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||
{
|
||||
var winSize = this.win.size;
|
||||
return length * winSize[1] / 2; // TODO: how do we handle norm when width != height?
|
||||
}
|
||||
else if (this._units === 'height') {
|
||||
else if (this._units === 'height')
|
||||
{
|
||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||
return length * minSize;
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
// 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
|
||||
*
|
||||
*
|
||||
* @name module:core.WindowMixin#_getLengthUnits
|
||||
* @function
|
||||
* @protected
|
||||
* @param {number} length_px - the length in pixel units
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getLengthUnits(length_px) {
|
||||
let response = { origin: 'WindowMixin._getLengthUnits', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
_getLengthUnits(length_px)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
||||
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||
{
|
||||
const winSize = this.win.size;
|
||||
return length_px / (winSize[1] / 2); // TODO: how do we handle norm when width != height?
|
||||
}
|
||||
else if (this._units === 'height') {
|
||||
else if (this._units === 'height')
|
||||
{
|
||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||
return length_px / minSize;
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
// 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
|
||||
*
|
||||
*
|
||||
* @name module:core.WindowMixin#_getHorLengthPix
|
||||
* @function
|
||||
* @protected
|
||||
* @param {number} length_px - the length in pixel units
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getHorLengthPix(length) {
|
||||
let response = { origin: 'WindowMixin._getHorLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
_getHorLengthPix(length)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
||||
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||
{
|
||||
var winSize = this.win.size;
|
||||
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]);
|
||||
return length * minSize;
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
// 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
|
||||
*
|
||||
*
|
||||
* @name module:core.WindowMixin#_getVerLengthPix
|
||||
* @function
|
||||
* @protected
|
||||
* @param {number} length_px - the length in pixel units
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getVerLengthPix(length) {
|
||||
let response = { origin: 'WindowMixin._getVerLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
||||
_getVerLengthPix(length)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
||||
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||
{
|
||||
var winSize = this.win.size;
|
||||
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]);
|
||||
return length * minSize;
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
// 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
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { PsychObject } from '../util/PsychObject';
|
||||
import { MonotonicClock } from '../util/Clock';
|
||||
import {PsychObject} from '../util/PsychObject';
|
||||
import {MonotonicClock} from '../util/Clock';
|
||||
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
|
||||
* for generating a single data file from an experiment with many different loops (e.g. interleaved
|
||||
* staircases or loops within loops.</p>
|
||||
*
|
||||
*
|
||||
* @name module:data.ExperimentHandler
|
||||
* @class
|
||||
* @class
|
||||
* @extends PsychObject
|
||||
* @param {Object} options
|
||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||
* @param {string} options.name - name of the experiment
|
||||
* @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.
|
||||
*
|
||||
*
|
||||
* @name module:core.Window#experimentEnded
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
get experimentEnded() { return this._experimentEnded; }
|
||||
get experimentEnded()
|
||||
{
|
||||
return this._experimentEnded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for experimentEnded.
|
||||
*
|
||||
*
|
||||
* @name module:core.Window#experimentEnded
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
set experimentEnded(ended) { this._experimentEnded = ended; }
|
||||
set experimentEnded(ended)
|
||||
{
|
||||
this._experimentEnded = ended;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Legacy experiment getters.
|
||||
*/
|
||||
get _thisEntry() { return this._currentTrialData; }
|
||||
get _entries() { return this._trialsData; }
|
||||
get _thisEntry()
|
||||
{
|
||||
return this._currentTrialData;
|
||||
}
|
||||
|
||||
get _entries()
|
||||
{
|
||||
return this._trialsData;
|
||||
}
|
||||
|
||||
|
||||
constructor({
|
||||
psychoJS,
|
||||
name,
|
||||
extraInfo
|
||||
} = {}) {
|
||||
psychoJS,
|
||||
name,
|
||||
extraInfo
|
||||
} = {})
|
||||
{
|
||||
super(psychoJS, name);
|
||||
|
||||
this._addAttributes(ExperimentHandler, extraInfo);
|
||||
@ -85,10 +100,13 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @public
|
||||
* @returns {boolean} whether or not the current entry is empty
|
||||
*/
|
||||
isEntryEmpty() {
|
||||
isEntryEmpty()
|
||||
{
|
||||
return (Object.keys(this._currentTrialData).length > 0);
|
||||
}
|
||||
isEntryEmtpy() {
|
||||
|
||||
isEntryEmtpy()
|
||||
{
|
||||
return (Object.keys(this._currentTrialData).length > 0);
|
||||
}
|
||||
|
||||
@ -104,7 +122,8 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @public
|
||||
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
||||
*/
|
||||
addLoop(loop) {
|
||||
addLoop(loop)
|
||||
{
|
||||
this._loops.push(loop);
|
||||
this._unfinishedLoops.push(loop);
|
||||
loop.experimentHandler = this;
|
||||
@ -119,10 +138,13 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @public
|
||||
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
||||
*/
|
||||
removeLoop(loop) {
|
||||
removeLoop(loop)
|
||||
{
|
||||
const index = this._unfinishedLoops.indexOf(loop);
|
||||
if (index !== -1)
|
||||
{
|
||||
this._unfinishedLoops.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -138,14 +160,18 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @param {Object} key - the key
|
||||
* @param {Object} value - the value
|
||||
*/
|
||||
addData(key, value) {
|
||||
if (this._trialsKeys.indexOf(key) === -1) {
|
||||
addData(key, value)
|
||||
{
|
||||
if (this._trialsKeys.indexOf(key) === -1)
|
||||
{
|
||||
this._trialsKeys.push(key);
|
||||
}
|
||||
|
||||
// turn arrays into their json equivalent:
|
||||
if (Array.isArray(value))
|
||||
{
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
|
||||
this._currentTrialData[key] = value;
|
||||
}
|
||||
@ -166,13 +192,20 @@ export class ExperimentHandler extends PsychObject {
|
||||
{
|
||||
// turn single snapshot into a one-element array:
|
||||
if (!Array.isArray(snapshots))
|
||||
{
|
||||
snapshots = [snapshots];
|
||||
}
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
for (const snapshot of snapshots)
|
||||
{
|
||||
const attributes = ExperimentHandler._getLoopAttributes(snapshot);
|
||||
for (let a in attributes)
|
||||
{
|
||||
if (attributes.hasOwnProperty(a))
|
||||
{
|
||||
this._currentTrialData[a] = attributes[a];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -184,15 +217,23 @@ export class ExperimentHandler extends PsychObject {
|
||||
{
|
||||
const attributes = ExperimentHandler._getLoopAttributes(loop);
|
||||
for (const a in attributes)
|
||||
{
|
||||
if (attributes.hasOwnProperty(a))
|
||||
{
|
||||
this._currentTrialData[a] = attributes[a];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add the extraInfo dict to the data:
|
||||
for (let a in this.extraInfo)
|
||||
{
|
||||
if (this.extraInfo.hasOwnProperty(a))
|
||||
{
|
||||
this._currentTrialData[a] = this.extraInfo[a];
|
||||
}
|
||||
}
|
||||
|
||||
this._trialsData.push(this._currentTrialData);
|
||||
|
||||
@ -214,26 +255,38 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @public
|
||||
* @param {Object} options
|
||||
* @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({
|
||||
attributes = []
|
||||
} = {}) {
|
||||
attributes = [],
|
||||
sync = false
|
||||
} = {})
|
||||
{
|
||||
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
|
||||
|
||||
// (*) get attributes:
|
||||
if (attributes.length === 0) {
|
||||
if (attributes.length === 0)
|
||||
{
|
||||
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 loopAttributes = ExperimentHandler._getLoopAttributes(loop);
|
||||
for (let a in loopAttributes)
|
||||
{
|
||||
if (loopAttributes.hasOwnProperty(a))
|
||||
{
|
||||
attributes.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let a in this.extraInfo) {
|
||||
for (let a in this.extraInfo)
|
||||
{
|
||||
if (this.extraInfo.hasOwnProperty(a))
|
||||
{
|
||||
attributes.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,40 +302,25 @@ export class ExperimentHandler extends PsychObject {
|
||||
|
||||
|
||||
// (*) save to a .csv file:
|
||||
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV) {
|
||||
/*
|
||||
// a. manual approach
|
||||
let csv = "";
|
||||
|
||||
// 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.)
|
||||
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV)
|
||||
{
|
||||
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
|
||||
// newlines, etc.
|
||||
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
|
||||
const csv = XLSX.utils.sheet_to_csv(worksheet);
|
||||
|
||||
// upload data to the pavlovia server or offer them for download:
|
||||
const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
|
||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER && this._psychoJS.config.experiment.status === 'RUNNING')
|
||||
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv);
|
||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
||||
this._psychoJS.config.experiment.status === 'RUNNING' &&
|
||||
!this._psychoJS._serverMsg.has('__pilotToken'))
|
||||
{
|
||||
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv, sync);
|
||||
}
|
||||
else
|
||||
{
|
||||
util.offerDataForDownload(key, csv, 'text/csv');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -291,20 +329,29 @@ export class ExperimentHandler extends PsychObject {
|
||||
{
|
||||
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};
|
||||
for (let h = 0; h < attributes.length; h++)
|
||||
{
|
||||
doc[attributes[h]] = this._trialsData[r][attributes[h]];
|
||||
}
|
||||
|
||||
documents.push(doc);
|
||||
}
|
||||
|
||||
// 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
|
||||
return await this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents));
|
||||
} else
|
||||
return /*await*/ this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents), sync);
|
||||
}
|
||||
else
|
||||
{
|
||||
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.
|
||||
* <p> Only only info relating to the trial execution are returned.</p>
|
||||
*
|
||||
*
|
||||
* @name module:data.ExperimentHandler#_getLoopAttributes
|
||||
* @function
|
||||
* @static
|
||||
* @protected
|
||||
* @param {Object} loop - the loop
|
||||
*/
|
||||
static _getLoopAttributes(loop) {
|
||||
static _getLoopAttributes(loop)
|
||||
{
|
||||
// standard trial attributes:
|
||||
const properties = ['thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'stepSizeCurrent', 'ran', 'order'];
|
||||
let attributes = {};
|
||||
const loopName = loop.name;
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
// specific trial attributes:
|
||||
if (typeof loop.getCurrentTrial === 'function') {
|
||||
if (typeof loop.getCurrentTrial === 'function')
|
||||
{
|
||||
const currentTrial = loop.getCurrentTrial();
|
||||
for (const trialProperty in currentTrial)
|
||||
{
|
||||
attributes[trialProperty] = currentTrial[trialProperty];
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO
|
||||
@ -370,7 +424,7 @@ export class ExperimentHandler extends PsychObject {
|
||||
|
||||
/**
|
||||
* Experiment result format
|
||||
*
|
||||
*
|
||||
* @name module:core.ServerManager#SaveFormat
|
||||
* @enum {Symbol}
|
||||
* @readonly
|
||||
|
@ -1,21 +1,21 @@
|
||||
/** @module data */
|
||||
/**
|
||||
* Trial Handler
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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';
|
||||
|
||||
|
||||
/**
|
||||
* <p>A Trial Handler handles the importing and sequencing of conditions.</p>
|
||||
*
|
||||
*
|
||||
* @class
|
||||
* @extends PsychObject
|
||||
* @param {Object} options
|
||||
@ -27,25 +27,30 @@ import * as util from '../util/Util';
|
||||
* @param {number} options.seed - seed for the random number generator
|
||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||
*/
|
||||
export class TrialHandler extends PsychObject {
|
||||
export class TrialHandler extends PsychObject
|
||||
{
|
||||
|
||||
/**
|
||||
* Getter for experimentHandler.
|
||||
*
|
||||
*
|
||||
* @name module:core.Window#experimentHandler
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
get experimentHandler() { return this._experimentHandler; }
|
||||
get experimentHandler()
|
||||
{
|
||||
return this._experimentHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for experimentHandler.
|
||||
*
|
||||
*
|
||||
* @name module:core.Window#experimentHandler
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
set experimentHandler(exp) {
|
||||
set 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
|
||||
*/
|
||||
constructor({
|
||||
psychoJS,
|
||||
trialList = [undefined],
|
||||
nReps,
|
||||
method = TrialHandler.Method.RANDOM,
|
||||
extraInfo = [],
|
||||
seed,
|
||||
name,
|
||||
autoLog = true
|
||||
} = {}) {
|
||||
psychoJS,
|
||||
trialList = [undefined],
|
||||
nReps,
|
||||
method = TrialHandler.Method.RANDOM,
|
||||
extraInfo = [],
|
||||
seed,
|
||||
name,
|
||||
autoLog = true
|
||||
} = {})
|
||||
{
|
||||
super(psychoJS);
|
||||
|
||||
this._addAttributes(TrialHandler, trialList, nReps, method, extraInfo, seed, name, autoLog);
|
||||
@ -117,13 +123,15 @@ export class TrialHandler extends PsychObject {
|
||||
[Symbol.iterator]()
|
||||
{
|
||||
return {
|
||||
next: () => {
|
||||
next: () =>
|
||||
{
|
||||
this.thisTrialN++;
|
||||
this.thisN++;
|
||||
this.nRemaining--;
|
||||
|
||||
// check for the last trial:
|
||||
if (this.nRemaining === 0) {
|
||||
if (this.nRemaining === 0)
|
||||
{
|
||||
this.finished = true;
|
||||
}
|
||||
|
||||
@ -135,9 +143,10 @@ export class TrialHandler extends PsychObject {
|
||||
}
|
||||
|
||||
// check if we have completed the sequence:
|
||||
if (this.thisRepN >= this.nReps) {
|
||||
if (this.thisRepN >= this.nReps)
|
||||
{
|
||||
this.thisTrial = null;
|
||||
return { done: true };
|
||||
return {done: true};
|
||||
}
|
||||
|
||||
this.thisIndex = this._trialSequence[this.thisRepN][this.thisTrialN];
|
||||
@ -150,7 +159,7 @@ export class TrialHandler extends PsychObject {
|
||||
vals = (self.thisRepN, self.thisTrialN, 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.
|
||||
*
|
||||
*
|
||||
* @param callback
|
||||
*/
|
||||
forEach(callback)
|
||||
{
|
||||
const trialIterator = this[Symbol.iterator]();
|
||||
|
||||
while(true)
|
||||
while (true)
|
||||
{
|
||||
const result = trialIterator.next();
|
||||
if (result.done)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
callback(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} Snapshot
|
||||
* @property {string} name - the trialHandler name
|
||||
@ -225,25 +236,27 @@ export class TrialHandler extends PsychObject {
|
||||
|
||||
/**
|
||||
* Get the trial index.
|
||||
*
|
||||
*
|
||||
* @public
|
||||
* @return {number} the current trial index
|
||||
*/
|
||||
getTrialIndex() {
|
||||
getTrialIndex()
|
||||
{
|
||||
return this.thisIndex;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the trial index.
|
||||
*
|
||||
*
|
||||
* @param {number} index - the new trial index
|
||||
*/
|
||||
setTrialIndex(index) {
|
||||
setTrialIndex(index)
|
||||
{
|
||||
this.thisIndex = index;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the attributes of the trials.
|
||||
*
|
||||
@ -253,13 +266,18 @@ export class TrialHandler extends PsychObject {
|
||||
* @public
|
||||
* @return {Array.string} the attributes
|
||||
*/
|
||||
getAttributes() {
|
||||
getAttributes()
|
||||
{
|
||||
if (!Array.isArray(this.trialList) || this.nStim === 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
const firstTrial = this.trialList[0];
|
||||
if (!firstTrial)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(this.trialList[0]);
|
||||
}
|
||||
@ -267,11 +285,12 @@ export class TrialHandler extends PsychObject {
|
||||
|
||||
/**
|
||||
* Get the current trial.
|
||||
*
|
||||
*
|
||||
* @public
|
||||
* @return {Object} the current trial
|
||||
*/
|
||||
getCurrentTrial() {
|
||||
getCurrentTrial()
|
||||
{
|
||||
return this.trialList[this.thisIndex];
|
||||
}
|
||||
|
||||
@ -282,9 +301,12 @@ export class TrialHandler extends PsychObject {
|
||||
* @param {number} index - the trial index
|
||||
* @return {Object|undefined} the requested trial or undefined if attempting to go beyond the last trial.
|
||||
*/
|
||||
getTrial(index = 0) {
|
||||
getTrial(index = 0)
|
||||
{
|
||||
if (index < 0 || index > this.nTotal)
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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)
|
||||
* or undefined if attempting to go beyond the last trial.
|
||||
*/
|
||||
getFutureTrial(n = 1) {
|
||||
if (this.thisIndex+n < 0 || n > this.nRemaining)
|
||||
getFutureTrial(n = 1)
|
||||
{
|
||||
if (this.thisIndex + n < 0 || n > this.nRemaining)
|
||||
{
|
||||
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
|
||||
* @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));
|
||||
}
|
||||
|
||||
@ -326,9 +352,12 @@ export class TrialHandler extends PsychObject {
|
||||
* @param {Object} key - the key
|
||||
* @param {Object} value - the value
|
||||
*/
|
||||
addData(key, value) {
|
||||
addData(key, value)
|
||||
{
|
||||
if (this._experimentHandler)
|
||||
{
|
||||
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'
|
||||
* @throws {Object} Throws an exception if importing the conditions failed.
|
||||
*/
|
||||
static importConditions(serverManager, resourceName, selection = null) {
|
||||
try {
|
||||
static importConditions(serverManager, resourceName, selection = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
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:
|
||||
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
|
||||
|
||||
// we consider only the first worksheet:
|
||||
if (workbook.SheetNames.length === 0)
|
||||
{
|
||||
throw 'workbook should contain at least one worksheet';
|
||||
}
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
|
||||
// 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();
|
||||
|
||||
// (*) select conditions:
|
||||
@ -396,14 +430,17 @@ export class TrialHandler extends PsychObject {
|
||||
// ...
|
||||
// ]
|
||||
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 trial = {};
|
||||
for (let l = 0; l < fields.length; ++l) {
|
||||
for (let l = 0; l < fields.length; ++l)
|
||||
{
|
||||
let value = row[l];
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -415,12 +452,18 @@ export class TrialHandler extends PsychObject {
|
||||
return trialList;
|
||||
}
|
||||
|
||||
else {
|
||||
else
|
||||
{
|
||||
throw 'extension: ' + resourceExtension + ' currently not supported.';
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
throw { origin: 'TrialHandler.importConditions', context: `when importing condition: ${resourceName}`, error};
|
||||
catch (error)
|
||||
{
|
||||
throw {
|
||||
origin: 'TrialHandler.importConditions',
|
||||
context: `when importing condition: ${resourceName}`,
|
||||
error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,27 +474,35 @@ export class TrialHandler extends PsychObject {
|
||||
* @protected
|
||||
* @param {Array.<Object> | String} trialList - a list of trials, or the name of a condition resource
|
||||
*/
|
||||
_prepareTrialList(trialList) {
|
||||
const response = { origin : 'TrialHandler._prepareTrialList', context : 'when preparing the trial list' };
|
||||
_prepareTrialList(trialList)
|
||||
{
|
||||
const response = {origin: 'TrialHandler._prepareTrialList', context: 'when preparing the trial list'};
|
||||
|
||||
// we treat undefined trialList as a list with a single empty entry:
|
||||
if (typeof trialList === 'undefined')
|
||||
{
|
||||
this.trialList = [undefined];
|
||||
|
||||
// if trialList is an array, we make sure it is not empty:
|
||||
else if (Array.isArray(trialList)) {
|
||||
}// if trialList is an array, we make sure it is not empty:
|
||||
else if (Array.isArray(trialList))
|
||||
{
|
||||
if (trialList.length === 0)
|
||||
{
|
||||
this.trialList = [undefined];
|
||||
}
|
||||
}
|
||||
|
||||
// if trialList is a string, we treat it as the name of the condition resource:
|
||||
else if (typeof trialList === 'string')
|
||||
{
|
||||
this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, trialList);
|
||||
|
||||
// unknown type:
|
||||
}// unknown type:
|
||||
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
|
||||
*/
|
||||
_prepareSequence() {
|
||||
const response = { origin : 'TrialHandler._prepareSequence', context : 'when preparing a sequence of trials' };
|
||||
_prepareSequence()
|
||||
{
|
||||
const response = {
|
||||
origin: 'TrialHandler._prepareSequence',
|
||||
context: 'when preparing a sequence of trials'
|
||||
};
|
||||
|
||||
// get an array of the indices of the elements of trialList :
|
||||
const indices = Array.from(this.trialList.keys());
|
||||
|
||||
// seed the random number generator:
|
||||
if (typeof (this.seed) !== 'undefined')
|
||||
{
|
||||
Math.seedrandom(this.seed);
|
||||
}
|
||||
else
|
||||
{
|
||||
Math.seedrandom();
|
||||
}
|
||||
|
||||
if (this.method === TrialHandler.Method.SEQUENTIAL) {
|
||||
if (this.method === TrialHandler.Method.SEQUENTIAL)
|
||||
{
|
||||
this._trialSequence = Array(this.nReps).fill(indices);
|
||||
// transposed version:
|
||||
//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 = [];
|
||||
for (let i = 0; i < this.nReps; ++i)
|
||||
{
|
||||
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:
|
||||
let flatSequence = [];
|
||||
for (let i = 0; i < this.nReps; ++i)
|
||||
{
|
||||
flatSequence.push.apply(flatSequence, indices);
|
||||
}
|
||||
|
||||
// shuffle the sequence:
|
||||
util.shuffle(flatSequence);
|
||||
@ -517,10 +583,13 @@ export class TrialHandler extends PsychObject {
|
||||
// reshape it into the trialSequence:
|
||||
this._trialSequence = [];
|
||||
for (let i = 0; i < this.nReps; i++)
|
||||
{
|
||||
this._trialSequence.push(flatSequence.slice(i * this.nStim, (i + 1) * this.nStim));
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw Object.assign(response, { error: 'unknown method' });
|
||||
else
|
||||
{
|
||||
throw Object.assign(response, {error: 'unknown method'});
|
||||
}
|
||||
|
||||
return this._trialSequence;
|
||||
|
@ -1,22 +1,22 @@
|
||||
/** @module sound */
|
||||
/**
|
||||
* Sound stimulus.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
import { PsychoJS } from '../core/PsychoJS';
|
||||
import { PsychObject } from '../util/PsychObject';
|
||||
import { TonePlayer } from './TonePlayer';
|
||||
import { TrackPlayer } from './TrackPlayer';
|
||||
import {PsychoJS} from '../core/PsychoJS';
|
||||
import {PsychObject} from '../util/PsychObject';
|
||||
import {TonePlayer} from './TonePlayer';
|
||||
import {TrackPlayer} from './TrackPlayer';
|
||||
|
||||
|
||||
/**
|
||||
* <p>This class handles sound playing (tones and tracks)</p>
|
||||
*
|
||||
*
|
||||
* <ul>
|
||||
* <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>
|
||||
@ -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
|
||||
* Tone.js</p>
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* [...]
|
||||
* 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.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
|
||||
*/
|
||||
export class Sound extends PsychObject {
|
||||
*/
|
||||
export class Sound extends PsychObject
|
||||
{
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
value = 'C',
|
||||
octave = 4,
|
||||
secs = 0.5,
|
||||
startTime = 0,
|
||||
stopTime = -1,
|
||||
stereo = true,
|
||||
volume = 1.0,
|
||||
loops = 0,
|
||||
//hamming = true,
|
||||
autoLog = true
|
||||
} = {}) {
|
||||
name,
|
||||
win,
|
||||
value = 'C',
|
||||
octave = 4,
|
||||
secs = 0.5,
|
||||
startTime = 0,
|
||||
stopTime = -1,
|
||||
stereo = true,
|
||||
volume = 1.0,
|
||||
loops = 0,
|
||||
//hamming = true,
|
||||
autoLog = true
|
||||
} = {})
|
||||
{
|
||||
super(win._psychoJS, name);
|
||||
|
||||
// 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.
|
||||
* Repeat calls to play may results in the sounds being played on top of each other.</p>
|
||||
*
|
||||
*
|
||||
* @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 {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
|
||||
*/
|
||||
stop({
|
||||
log = true
|
||||
} = {})
|
||||
log = true
|
||||
} = {})
|
||||
{
|
||||
this._player.stop();
|
||||
this.status = PsychoJS.Status.STOPPED;
|
||||
@ -118,7 +120,8 @@ export class Sound extends PsychObject {
|
||||
* @public
|
||||
* @return {number} the duration of the sound, in seconds
|
||||
*/
|
||||
getDuration() {
|
||||
getDuration()
|
||||
{
|
||||
return this._player.getDuration();
|
||||
}
|
||||
|
||||
@ -126,7 +129,7 @@ export class Sound extends PsychObject {
|
||||
/**
|
||||
* Set the playing volume of the sound.
|
||||
*
|
||||
* @public
|
||||
* @public
|
||||
* @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} [log= true] - whether of not to log
|
||||
@ -136,14 +139,16 @@ export class Sound extends PsychObject {
|
||||
this._setAttribute('volume', volume, log);
|
||||
|
||||
if (typeof this._player !== 'undefined')
|
||||
{
|
||||
this._player.setVolume(volume, mute);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 {boolean} [log=true] - whether of not to log
|
||||
*/
|
||||
@ -152,11 +157,12 @@ export class Sound extends PsychObject {
|
||||
this._setAttribute('loops', loops, log);
|
||||
|
||||
if (typeof this._player !== 'undefined')
|
||||
{
|
||||
this._player.setLoops(loops);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set the duration (in seconds)
|
||||
*
|
||||
@ -169,11 +175,12 @@ export class Sound extends PsychObject {
|
||||
this._setAttribute('secs', secs, log);
|
||||
|
||||
if (typeof this._player !== 'undefined')
|
||||
{
|
||||
this._player.setDuration(secs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Identify the appropriate player for the sound.
|
||||
*
|
||||
@ -181,19 +188,27 @@ export class Sound extends PsychObject {
|
||||
* @return {SoundPlayer} the appropriate SoundPlayer
|
||||
* @throws {Object.<string, *>} exception if no appropriate SoundPlayer could be found for the sound
|
||||
*/
|
||||
_getPlayer() {
|
||||
_getPlayer()
|
||||
{
|
||||
const acceptFns = [
|
||||
sound => TonePlayer.accept(sound),
|
||||
sound => TrackPlayer.accept(sound)
|
||||
];
|
||||
|
||||
for (const acceptFn of acceptFns) {
|
||||
for (const acceptFn of acceptFns)
|
||||
{
|
||||
this._player = acceptFn(this);
|
||||
if (typeof this._player !== 'undefined')
|
||||
{
|
||||
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
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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.
|
||||
*
|
||||
@ -112,13 +131,17 @@ export class SoundPlayer extends PsychObject
|
||||
*/
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:sound.SoundPlayer#setVolume
|
||||
* @function
|
||||
* @public
|
||||
@ -126,8 +149,13 @@ export class SoundPlayer extends PsychObject
|
||||
* @param {Integer} volume - the volume of the tone
|
||||
* @param {boolean} [mute= false] - whether or not to mute the tone
|
||||
*/
|
||||
setVolume(volume, mute = false) {
|
||||
throw {origin: 'SoundPlayer.setVolume', context: 'when setting the volume of the sound', error: 'this method is abstract and should not be called.'};
|
||||
setVolume(volume, mute = false)
|
||||
{
|
||||
throw {
|
||||
origin: 'SoundPlayer.setVolume',
|
||||
context: 'when setting the volume of the sound',
|
||||
error: 'this method is abstract and should not be called.'
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,17 +2,17 @@
|
||||
* Tone Player.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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>
|
||||
*
|
||||
*
|
||||
* @name module:sound.TonePlayer
|
||||
* @class
|
||||
* @extends SoundPlayer
|
||||
@ -26,14 +26,14 @@ import { SoundPlayer } from './SoundPlayer';
|
||||
export class TonePlayer extends SoundPlayer
|
||||
{
|
||||
constructor({
|
||||
psychoJS,
|
||||
note = 'C4',
|
||||
duration_s = 0.5,
|
||||
volume = 1.0,
|
||||
loops = 0,
|
||||
soundLibrary = TonePlayer.SoundLibrary.TONE_JS,
|
||||
autoLog = true
|
||||
} = {})
|
||||
psychoJS,
|
||||
note = 'C4',
|
||||
duration_s = 0.5,
|
||||
volume = 1.0,
|
||||
loops = 0,
|
||||
soundLibrary = TonePlayer.SoundLibrary.TONE_JS,
|
||||
autoLog = true
|
||||
} = {})
|
||||
{
|
||||
super(psychoJS);
|
||||
|
||||
@ -84,7 +84,8 @@ export class TonePlayer extends SoundPlayer
|
||||
{
|
||||
// mapping between the PsychoPY notes and the standard ones:
|
||||
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 + 'fl', note + 'b');
|
||||
psychopyToToneMap.set(note + 'sh', note + '#');
|
||||
@ -123,7 +124,6 @@ export class TonePlayer extends SoundPlayer
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set the duration of the tone.
|
||||
*
|
||||
@ -138,7 +138,6 @@ export class TonePlayer extends SoundPlayer
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set the number of loops.
|
||||
*
|
||||
@ -155,7 +154,7 @@ export class TonePlayer extends SoundPlayer
|
||||
|
||||
/**
|
||||
* Set the volume of the tone.
|
||||
*
|
||||
*
|
||||
* @name module:sound.TonePlayer#setVolume
|
||||
* @function
|
||||
* @public
|
||||
@ -197,7 +196,9 @@ export class TonePlayer extends SoundPlayer
|
||||
play(loops)
|
||||
{
|
||||
if (typeof loops !== 'undefined')
|
||||
{
|
||||
this._loops = loops;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@ -250,7 +251,7 @@ export class TonePlayer extends SoundPlayer
|
||||
this.duration_s * (this._loops + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -269,7 +270,9 @@ export class TonePlayer extends SoundPlayer
|
||||
|
||||
// clear the repeat event if need be:
|
||||
if (this._toneId)
|
||||
{
|
||||
Tone.Transport.clear(this._toneId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -369,4 +372,4 @@ export class TonePlayer extends SoundPlayer
|
||||
TonePlayer.SoundLibrary = {
|
||||
AUDIO_CONTEXT: Symbol.for('AUDIO_CONTEXT'),
|
||||
TONE_JS: Symbol.for('TONE_JS')
|
||||
};
|
||||
};
|
||||
|
@ -2,17 +2,17 @@
|
||||
* Track Player.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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>
|
||||
*
|
||||
*
|
||||
* @name module:sound.TrackPlayer
|
||||
* @class
|
||||
* @extends SoundPlayer
|
||||
@ -23,20 +23,22 @@ import { SoundPlayer } from './SoundPlayer';
|
||||
* @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 {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 stereo is currently not implemented
|
||||
*/
|
||||
export class TrackPlayer extends SoundPlayer {
|
||||
export class TrackPlayer extends SoundPlayer
|
||||
{
|
||||
constructor({
|
||||
psychoJS,
|
||||
howl,
|
||||
startTime = 0,
|
||||
stopTime = -1,
|
||||
stereo = true,
|
||||
volume = 0,
|
||||
loops = 0
|
||||
} = {}) {
|
||||
psychoJS,
|
||||
howl,
|
||||
startTime = 0,
|
||||
stopTime = -1,
|
||||
stereo = true,
|
||||
volume = 0,
|
||||
loops = 0
|
||||
} = {})
|
||||
{
|
||||
super(psychoJS);
|
||||
|
||||
this._addAttributes(TrackPlayer, howl, startTime, stopTime, stereo, loops, volume);
|
||||
@ -55,11 +57,14 @@ export class TrackPlayer extends SoundPlayer {
|
||||
* @param {module:sound.Sound} - the sound
|
||||
* @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 (typeof sound.value === 'string') {
|
||||
if (typeof sound.value === 'string')
|
||||
{
|
||||
const howl = sound.psychoJS.serverManager.getResource(sound.value);
|
||||
if (typeof howl !== 'undefined') {
|
||||
if (typeof howl !== 'undefined')
|
||||
{
|
||||
// build the player:
|
||||
const player = new TrackPlayer({
|
||||
psychoJS: sound.psychoJS,
|
||||
@ -95,16 +100,17 @@ export class TrackPlayer extends SoundPlayer {
|
||||
|
||||
/**
|
||||
* Set the volume of the tone.
|
||||
*
|
||||
*
|
||||
* @name module:sound.TrackPlayer#setVolume
|
||||
* @function
|
||||
* @public
|
||||
* @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
|
||||
*/
|
||||
setVolume(volume, mute = false) {
|
||||
setVolume(volume, mute = false)
|
||||
{
|
||||
this._volume = volume;
|
||||
|
||||
|
||||
this._howl.volume(volume);
|
||||
this._howl.mute(mute);
|
||||
}
|
||||
@ -124,9 +130,13 @@ export class TrackPlayer extends SoundPlayer {
|
||||
this._currentLoopIndex = -1;
|
||||
|
||||
if (loops === 0)
|
||||
{
|
||||
this._howl.loop(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._howl.loop(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -138,18 +148,26 @@ export class TrackPlayer extends SoundPlayer {
|
||||
* @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.
|
||||
*/
|
||||
play(loops) {
|
||||
play(loops)
|
||||
{
|
||||
if (typeof loops !== 'undefined')
|
||||
{
|
||||
this.setLoops(loops);
|
||||
}
|
||||
|
||||
// handle repeats:
|
||||
if (loops > 0) {
|
||||
if (loops > 0)
|
||||
{
|
||||
const self = this;
|
||||
this._howl.on('end', (event) => {
|
||||
this._howl.on('end', (event) =>
|
||||
{
|
||||
++this._currentLoopIndex;
|
||||
if (self._currentLoopIndex > self._loops)
|
||||
{
|
||||
self.stop();
|
||||
else {
|
||||
}
|
||||
else
|
||||
{
|
||||
self._howl.seek(self._startTime);
|
||||
self._howl.play();
|
||||
}
|
||||
@ -168,7 +186,8 @@ export class TrackPlayer extends SoundPlayer {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
stop() {
|
||||
stop()
|
||||
{
|
||||
this._howl.stop();
|
||||
this._howl.off('end');
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Clock component.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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>
|
||||
*
|
||||
*
|
||||
* @name module:util.MonotonicClock
|
||||
* @class
|
||||
* @param {number} [startTime= <time elapsed since the reference point, i.e. the time when the module was loaded>] - the clock's start time (in ms)
|
||||
*/
|
||||
export class MonotonicClock {
|
||||
constructor(startTime = MonotonicClock.getReferenceTime()) {
|
||||
export class MonotonicClock
|
||||
{
|
||||
constructor(startTime = MonotonicClock.getReferenceTime())
|
||||
{
|
||||
this._timeAtLastReset = startTime;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current time on this clock.
|
||||
*
|
||||
*
|
||||
* @name module:util.MonotonicClock#getTime
|
||||
* @function
|
||||
* @public
|
||||
* @return {number} the current time (in seconds)
|
||||
*/
|
||||
getTime() {
|
||||
getTime()
|
||||
{
|
||||
return MonotonicClock.getReferenceTime() - this._timeAtLastReset;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current offset being applied to the high resolution timebase used by this Clock.
|
||||
*
|
||||
*
|
||||
* @name module:util.MonotonicClock#getLastResetTime
|
||||
* @function
|
||||
* @public
|
||||
* @return {number} the offset (in seconds)
|
||||
*/
|
||||
getLastResetTime() {
|
||||
getLastResetTime()
|
||||
{
|
||||
return this._timeAtLastReset;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the time elapsed since the reference point.
|
||||
*
|
||||
*
|
||||
* @name module:util.MonotonicClock#getReferenceTime
|
||||
* @function
|
||||
* @public
|
||||
@ -64,9 +68,9 @@ export class MonotonicClock {
|
||||
|
||||
/**
|
||||
* Get the clock's current time as a formatted string.
|
||||
*
|
||||
*
|
||||
* <p>Note: this is mostly used as an appendix to the name of the keys save to the server.</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.MonotonicClock.getDateStr
|
||||
* @function
|
||||
* @public
|
||||
@ -83,54 +87,59 @@ export class MonotonicClock {
|
||||
|
||||
/**
|
||||
* The clock's referenceTime is the time when the module was loaded (in seconds).
|
||||
*
|
||||
*
|
||||
* @name module:util.MonotonicClock._referenceTime
|
||||
* @readonly
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
MonotonicClock._referenceTime = performance.now() / 1000.0;
|
||||
|
||||
// MonotonicClock._referenceTime = new Date().getTime() / 1000.0;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Clock is a MonotonicClock that also offers the possibility of being reset.</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Clock
|
||||
* @class
|
||||
* @extends MonotonicClock
|
||||
*/
|
||||
export class Clock extends MonotonicClock {
|
||||
constructor() {
|
||||
export class Clock extends MonotonicClock
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the time on the clock.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @name module:util.Clock#reset
|
||||
* @function
|
||||
* @public
|
||||
* @param {number} [newTime= 0] the new time on the clock.
|
||||
*/
|
||||
reset(newTime = 0) {
|
||||
reset(newTime = 0)
|
||||
{
|
||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add more time to the clock's 'start' time (t0).
|
||||
*
|
||||
*
|
||||
* <p>Note: by adding time to t0, the current time is pushed forward (it becomes
|
||||
* smaller). As a consequence, getTime() may return a negative number.</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Clock#add
|
||||
* @function
|
||||
* @public
|
||||
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||
*/
|
||||
add(deltaTime) {
|
||||
add(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.
|
||||
*
|
||||
*
|
||||
* @name module:util.CountdownTimer
|
||||
* @class
|
||||
* @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 {
|
||||
constructor(startTime = 0) {
|
||||
export class CountdownTimer extends Clock
|
||||
{
|
||||
constructor(startTime = 0)
|
||||
{
|
||||
super();
|
||||
|
||||
this._timeAtLastReset = MonotonicClock.getReferenceTime();
|
||||
this._countdown_duration = startTime;
|
||||
if (startTime) {
|
||||
if (startTime)
|
||||
{
|
||||
this.add(startTime);
|
||||
}
|
||||
}
|
||||
@ -158,34 +170,38 @@ export class CountdownTimer extends Clock {
|
||||
|
||||
/**
|
||||
* Add more time to the clock's 'start' time (t0).
|
||||
*
|
||||
*
|
||||
* <p>Note: by adding time to t0, you push the current time forward (make it
|
||||
* smaller). As a consequence, getTime() may return a negative number.</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.CountdownTimer#add
|
||||
* @function
|
||||
* @public
|
||||
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||
*/
|
||||
add(deltaTime) {
|
||||
add(deltaTime)
|
||||
{
|
||||
this._timeAtLastReset += deltaTime;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the time on the countdown.
|
||||
*
|
||||
*
|
||||
* @name module:util.CountdownTimer#reset
|
||||
* @function
|
||||
* @public
|
||||
* @param {number} [newTime] - if newTime is undefined, the countdown time is reset to zero, otherwise we set it
|
||||
* to newTime
|
||||
*/
|
||||
reset(newTime = undefined) {
|
||||
if (typeof newTime == 'undefined') {
|
||||
reset(newTime = undefined)
|
||||
{
|
||||
if (typeof newTime == 'undefined')
|
||||
{
|
||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + this._countdown_duration;
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
this._countdown_duration = newTime;
|
||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
||||
}
|
||||
@ -194,13 +210,14 @@ export class CountdownTimer extends Clock {
|
||||
|
||||
/**
|
||||
* Get the time currently left on the countdown.
|
||||
*
|
||||
*
|
||||
* @name module:util.CountdownTimer#getTime
|
||||
* @function
|
||||
* @public
|
||||
* @return {number} the time left on the countdown (in seconds)
|
||||
*/
|
||||
getTime() {
|
||||
getTime()
|
||||
{
|
||||
return this._timeAtLastReset - MonotonicClock.getReferenceTime();
|
||||
}
|
||||
}
|
||||
|
237
js/util/Color.js
237
js/util/Color.js
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Color management.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -11,7 +11,7 @@
|
||||
/**
|
||||
* <p>This class handles multiple color spaces, and offers various
|
||||
* static methods for converting colors from one space to another.</p>
|
||||
*
|
||||
*
|
||||
* <p>The constructor accepts the following color representations:
|
||||
* <ul>
|
||||
* <li>a named color, e.g. 'aliceblue' (the colorspace must be RGB)</li>
|
||||
@ -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>
|
||||
* </ul>
|
||||
* </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
|
||||
* @class
|
||||
* @param {string|number|Array.<number>|undefined} [obj= 'black'] - an object representing a color
|
||||
* @param {module:util.Color#COLOR_SPACE|undefined} [colorspace=Color.COLOR_SPACE.RGB] - the colorspace of that color
|
||||
*
|
||||
*
|
||||
* @todo implement HSV, DKL, and LMS colorspaces
|
||||
*/
|
||||
export class Color {
|
||||
export class Color
|
||||
{
|
||||
|
||||
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB) {
|
||||
const response = { origin: 'Color', context: 'when defining a color' };
|
||||
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB)
|
||||
{
|
||||
const response = {origin: 'Color', context: 'when defining a color'};
|
||||
|
||||
// named color (e.g. 'seagreen') or string hexadecimal representation (e.g. '#FF0000'):
|
||||
// note: we expect the color space to be RGB
|
||||
if (typeof obj == 'string') {
|
||||
if (typeof obj == 'string')
|
||||
{
|
||||
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' +
|
||||
' named color' });
|
||||
' named color'
|
||||
});
|
||||
}
|
||||
|
||||
// hexademical representation:
|
||||
if (obj[0] === '#') {
|
||||
if (obj[0] === '#')
|
||||
{
|
||||
this._hex = obj;
|
||||
}
|
||||
// named color:
|
||||
else {
|
||||
else
|
||||
{
|
||||
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()];
|
||||
}
|
||||
@ -60,22 +71,29 @@ export class Color {
|
||||
|
||||
// hexadecimal number representation (e.g. 0xFF0000)
|
||||
// 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)
|
||||
throw Object.assign(response, { error: 'the colorspace must be RGB for' +
|
||||
{
|
||||
throw Object.assign(response, {
|
||||
error: 'the colorspace must be RGB for' +
|
||||
' a' +
|
||||
' named color' });
|
||||
' named color'
|
||||
});
|
||||
}
|
||||
|
||||
this._rgb = Color._intToRgb(obj);
|
||||
}
|
||||
|
||||
// array of numbers:
|
||||
else if (Array.isArray(obj)) {
|
||||
else if (Array.isArray(obj))
|
||||
{
|
||||
Color._checkTypeAndRange(obj);
|
||||
let [a, b, c] = obj;
|
||||
|
||||
// check range and convert to [0,1]:
|
||||
if (colorspace !== Color.COLOR_SPACE.RGB255) {
|
||||
if (colorspace !== Color.COLOR_SPACE.RGB255)
|
||||
{
|
||||
Color._checkTypeAndRange(obj, [-1, 1]);
|
||||
|
||||
a = (a + 1.0) / 2.0;
|
||||
@ -84,7 +102,8 @@ export class Color {
|
||||
}
|
||||
|
||||
// get RGB components:
|
||||
switch (colorspace) {
|
||||
switch (colorspace)
|
||||
{
|
||||
case Color.COLOR_SPACE.RGB255:
|
||||
Color._checkTypeAndRange(obj, [0, 255]);
|
||||
this._rgb = [a / 255.0, b / 255.0, c / 255.0];
|
||||
@ -104,7 +123,7 @@ export class Color {
|
||||
break;
|
||||
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.rgb
|
||||
* @function
|
||||
* @public
|
||||
* @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.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.rgb255
|
||||
* @function
|
||||
* @public
|
||||
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
||||
*/
|
||||
get rgb255() { return [Math.round(this._rgb[0] * 255.0), Math.round(this._rgb[1] * 255.0), Math.round(this._rgb[2] * 255.0)]; }
|
||||
get rgb255()
|
||||
{
|
||||
return [Math.round(this._rgb[0] * 255.0), Math.round(this._rgb[1] * 255.0), Math.round(this._rgb[2] * 255.0)];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the hexadecimal color code equivalent of this Color.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.hex
|
||||
* @function
|
||||
* @public
|
||||
* @return {string} the hexadecimal color code equivalent
|
||||
*/
|
||||
get hex() {
|
||||
*/
|
||||
get hex()
|
||||
{
|
||||
if (typeof this._hex === 'undefined')
|
||||
{
|
||||
this._hex = Color._rgbToHex(this._rgb);
|
||||
}
|
||||
return this._hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the integer code equivalent of this Color.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.int
|
||||
* @function
|
||||
* @public
|
||||
* @return {number} the integer code equivalent
|
||||
*/
|
||||
get int() {
|
||||
*/
|
||||
get int()
|
||||
{
|
||||
if (typeof this._int === 'undefined')
|
||||
{
|
||||
this._int = Color._rgbToInt(this._rgb);
|
||||
}
|
||||
return this._int;
|
||||
}
|
||||
|
||||
@ -197,7 +228,7 @@ export class Color {
|
||||
|
||||
/**
|
||||
* Get the [0,255] RGB triplet equivalent of the hexadecimal color code.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.hexToRgb255
|
||||
* @function
|
||||
* @static
|
||||
@ -205,10 +236,17 @@ export class Color {
|
||||
* @param {string} hex - the hexadecimal color code
|
||||
* @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);
|
||||
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)];
|
||||
}
|
||||
@ -216,7 +254,7 @@ export class Color {
|
||||
|
||||
/**
|
||||
* Get the [0,1] RGB triplet equivalent of the hexadecimal color code.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.hexToRgb
|
||||
* @function
|
||||
* @static
|
||||
@ -224,7 +262,8 @@ export class Color {
|
||||
* @param {string} hex - the hexadecimal color code
|
||||
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
||||
*/
|
||||
static hexToRgb(hex) {
|
||||
static hexToRgb(hex)
|
||||
{
|
||||
const [r255, g255, b255] = Color.hexToRgb255(hex);
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.rgb255ToHex
|
||||
* @function
|
||||
* @static
|
||||
@ -240,22 +279,28 @@ export class Color {
|
||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||
* @return {string} the hexadecimal color code equivalent
|
||||
*/
|
||||
static rgb255ToHex(rgb255) {
|
||||
const response = { origin : 'Color.rgb255ToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
|
||||
static rgb255ToHex(rgb255)
|
||||
{
|
||||
const response = {
|
||||
origin: 'Color.rgb255ToHex',
|
||||
context: 'when converting an rgb triplet to its hexadecimal color representation'
|
||||
};
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
Color._checkTypeAndRange(rgb255, [0, 255]);
|
||||
return Color._rgb255ToHex(rgb255);
|
||||
}
|
||||
catch (error) {
|
||||
throw Object.assign(response, { error });
|
||||
catch (error)
|
||||
{
|
||||
throw Object.assign(response, {error});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.rgbToHex
|
||||
* @function
|
||||
* @static
|
||||
@ -263,22 +308,28 @@ export class Color {
|
||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||
* @return {string} the hexadecimal color code equivalent
|
||||
*/
|
||||
static rgbToHex(rgb) {
|
||||
const response = { origin : 'Color.rgbToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
|
||||
static rgbToHex(rgb)
|
||||
{
|
||||
const response = {
|
||||
origin: 'Color.rgbToHex',
|
||||
context: 'when converting an rgb triplet to its hexadecimal color representation'
|
||||
};
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
Color._checkTypeAndRange(rgb, [0, 1]);
|
||||
return Color._rgbToHex(rgb);
|
||||
}
|
||||
catch (error) {
|
||||
throw Object.assign(response, { error });
|
||||
catch (error)
|
||||
{
|
||||
throw Object.assign(response, {error});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the integer equivalent of the [0, 1] RGB triplet.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.rgbToInt
|
||||
* @function
|
||||
* @static
|
||||
@ -286,22 +337,28 @@ export class Color {
|
||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||
* @return {number} the integer equivalent
|
||||
*/
|
||||
static rgbToInt(rgb) {
|
||||
const response = { origin : 'Color.rgbToInt', context: 'when converting an rgb triplet to its integer representation' };
|
||||
static rgbToInt(rgb)
|
||||
{
|
||||
const response = {
|
||||
origin: 'Color.rgbToInt',
|
||||
context: 'when converting an rgb triplet to its integer representation'
|
||||
};
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
Color._checkTypeAndRange(rgb, [0, 1]);
|
||||
return Color._rgbToInt(rgb);
|
||||
}
|
||||
catch (error) {
|
||||
throw Object.assign(response, { error });
|
||||
catch (error)
|
||||
{
|
||||
throw Object.assign(response, {error});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the integer equivalent of the [0, 255] RGB triplet.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color.rgb255ToInt
|
||||
* @function
|
||||
* @static
|
||||
@ -309,23 +366,29 @@ export class Color {
|
||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||
* @return {number} the integer equivalent
|
||||
*/
|
||||
static rgb255ToInt(rgb255) {
|
||||
const response = { origin : 'Color.rgb255ToInt', context: 'when converting an rgb triplet to its integer representation' };
|
||||
try {
|
||||
static rgb255ToInt(rgb255)
|
||||
{
|
||||
const response = {
|
||||
origin: 'Color.rgb255ToInt',
|
||||
context: 'when converting an rgb triplet to its integer representation'
|
||||
};
|
||||
try
|
||||
{
|
||||
Color._checkTypeAndRange(rgb255, [0, 255]);
|
||||
return Color._rgb255ToInt(rgb255);
|
||||
}
|
||||
catch (error) {
|
||||
throw Object.assign(response, { error });
|
||||
catch (error)
|
||||
{
|
||||
throw Object.assign(response, {error});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
|
||||
*
|
||||
*
|
||||
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Color._rgb255ToHex
|
||||
* @function
|
||||
* @static
|
||||
@ -333,16 +396,17 @@ export class Color {
|
||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
|
||||
*
|
||||
*
|
||||
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Color._rgbToHex
|
||||
* @function
|
||||
* @static
|
||||
@ -350,7 +414,8 @@ export class Color {
|
||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||
* @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)];
|
||||
return Color._rgb255ToHex(rgb255);
|
||||
}
|
||||
@ -358,9 +423,9 @@ export class Color {
|
||||
|
||||
/**
|
||||
* Get the integer equivalent of the [0, 1] RGB triplet.
|
||||
*
|
||||
*
|
||||
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Color._rgbToInt
|
||||
* @function
|
||||
* @static
|
||||
@ -368,7 +433,8 @@ export class Color {
|
||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||
* @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)];
|
||||
return Color._rgb255ToInt(rgb255);
|
||||
}
|
||||
@ -376,9 +442,9 @@ export class Color {
|
||||
|
||||
/**
|
||||
* Get the integer equivalent of the [0, 255] RGB triplet.
|
||||
*
|
||||
*
|
||||
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Color._rgb255ToInt
|
||||
* @function
|
||||
* @static
|
||||
@ -386,16 +452,17 @@ export class Color {
|
||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||
* @return {number} the integer equivalent
|
||||
*/
|
||||
static _rgb255ToInt(rgb255) {
|
||||
static _rgb255ToInt(rgb255)
|
||||
{
|
||||
return rgb255[0] * 0x10000 + rgb255[1] * 0x100 + rgb255[2];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the [0, 255] based RGB triplet equivalent of the integer color code.
|
||||
*
|
||||
*
|
||||
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Color._intToRgb255
|
||||
* @function
|
||||
* @static
|
||||
@ -403,7 +470,8 @@ export class Color {
|
||||
* @param {number} hex - the integer color code
|
||||
* @return {Array.<number>} the [0, 255] RGB equivalent
|
||||
*/
|
||||
static _intToRgb255(hex) {
|
||||
static _intToRgb255(hex)
|
||||
{
|
||||
const r255 = hex >>> 0x10;
|
||||
const g255 = (hex & 0xFF00) / 0x100;
|
||||
const b255 = hex & 0xFF;
|
||||
@ -414,9 +482,9 @@ export class Color {
|
||||
|
||||
/**
|
||||
* Get the [0, 1] based RGB triplet equivalent of the integer color code.
|
||||
*
|
||||
*
|
||||
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Color._intToRgb
|
||||
* @function
|
||||
* @static
|
||||
@ -424,7 +492,8 @@ export class Color {
|
||||
* @param {number} hex - the integer color code
|
||||
* @return {Array.<number>} the [0, 1] RGB equivalent
|
||||
*/
|
||||
static _intToRgb(hex) {
|
||||
static _intToRgb(hex)
|
||||
{
|
||||
const [r255, g255, b255] = Color._intToRgb255(hex);
|
||||
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color._checkTypeAndRange
|
||||
* @function
|
||||
* @static
|
||||
@ -443,21 +512,23 @@ export class Color {
|
||||
*/
|
||||
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')
|
||||
{
|
||||
throw 'the argument should be an array of numbers of length 3';
|
||||
}
|
||||
|
||||
if (typeof range !== 'undefined' && (arg[0] < range[0] || arg[0] > range[1] || arg[1] < range[0] || arg[1] > range[1] || arg[2] < range[0] || arg[2] > range[1]))
|
||||
{
|
||||
throw 'the color components should all belong to [' + range[0] + ', ' + range[1] + ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Color spaces.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color#COLOR_SPACE
|
||||
* @enum {Symbol}
|
||||
* @readonly
|
||||
@ -484,7 +555,7 @@ Color.COLOR_SPACE = {
|
||||
|
||||
/**
|
||||
* Named colors.
|
||||
*
|
||||
*
|
||||
* @name module:util.Color#NAMED_COLORS
|
||||
* @enum {Symbol}
|
||||
* @readonly
|
||||
|
@ -2,37 +2,40 @@
|
||||
* Color Mixin.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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>
|
||||
*
|
||||
*
|
||||
* @name module:util.ColorMixin
|
||||
* @mixin
|
||||
*/
|
||||
export let ColorMixin = (superclass) => class extends superclass {
|
||||
constructor(args) {
|
||||
export let ColorMixin = (superclass) => class extends superclass
|
||||
{
|
||||
constructor(args)
|
||||
{
|
||||
super(args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for Color attribute.
|
||||
*
|
||||
*
|
||||
* @name module:util.ColorMixin#setColor
|
||||
* @function
|
||||
* @public
|
||||
* @param {Color} color - the new color
|
||||
* @param {boolean} [log= false] - whether or not to log
|
||||
*/
|
||||
setColor(color, log) {
|
||||
setColor(color, log)
|
||||
{
|
||||
this._setAttribute('color', color, log);
|
||||
|
||||
this._needUpdate = true;
|
||||
@ -41,14 +44,15 @@ export let ColorMixin = (superclass) => class extends superclass {
|
||||
|
||||
/**
|
||||
* Setter for Contrast attribute.
|
||||
*
|
||||
*
|
||||
* @name module:util.ColorMixin#setContrast
|
||||
* @function
|
||||
* @public
|
||||
* @param {number} contrast - the new contrast (must be between 0 and 1)
|
||||
* @param {boolean} [log= false] - whether or not to log
|
||||
*/
|
||||
setContrast(contrast, log) {
|
||||
setContrast(contrast, log)
|
||||
{
|
||||
this._setAttribute('contrast', contrast, log);
|
||||
|
||||
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
|
||||
*
|
||||
*
|
||||
* @name module:util.ColorMixin#getContrastedColor
|
||||
* @function
|
||||
* @public
|
||||
* @param {string|number|Array.<number>} color - the color
|
||||
* @param {number} contrast - the contrast (must be between 0 and 1)
|
||||
*/
|
||||
getContrastedColor(color, contrast) {
|
||||
getContrastedColor(color, contrast)
|
||||
{
|
||||
const rgb = color.rgb.map(c => (c * 2.0 - 1.0) * contrast);
|
||||
return new Color(rgb, Color.COLOR_SPACE.RGB);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Event Emitter.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -18,7 +18,7 @@ import * as util from './Util';
|
||||
*
|
||||
* @name module:util.EventEmitter
|
||||
* @class
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* let observable = new EventEmitter();
|
||||
* let uuid1 = observable.on('change', data => { console.log(data); });
|
||||
@ -28,7 +28,7 @@ import * as util from './Util';
|
||||
*/
|
||||
export class EventEmitter
|
||||
{
|
||||
constructor()
|
||||
constructor()
|
||||
{
|
||||
this._listeners = 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.
|
||||
*
|
||||
*
|
||||
* @callback module:util.EventEmitter~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.
|
||||
*
|
||||
*
|
||||
* @name module:util.EventEmitter#on
|
||||
* @function
|
||||
* @public
|
||||
@ -57,14 +57,18 @@ export class EventEmitter
|
||||
{
|
||||
// check that the listener is a function:
|
||||
if (typeof listener !== 'function')
|
||||
{
|
||||
throw new TypeError('listener must be a function');
|
||||
}
|
||||
|
||||
// generate a new uuid:
|
||||
let uuid = util.makeUuid();
|
||||
|
||||
// add the listener to the event map:
|
||||
if (!this._listeners.has(name))
|
||||
{
|
||||
this._listeners.set(name, []);
|
||||
}
|
||||
this._listeners.get(name).push({uuid, listener});
|
||||
|
||||
return uuid;
|
||||
@ -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.
|
||||
*
|
||||
*
|
||||
* @name module:util.EventEmitter#once
|
||||
* @function
|
||||
* @public
|
||||
@ -86,7 +90,9 @@ export class EventEmitter
|
||||
let uuid = this.on(name, listener);
|
||||
|
||||
if (!this._onceUuids.has(name))
|
||||
{
|
||||
this._onceUuids.set(name, []);
|
||||
}
|
||||
this._onceUuids.get(name).push(uuid);
|
||||
|
||||
return uuid;
|
||||
@ -95,7 +101,7 @@ export class EventEmitter
|
||||
|
||||
/**
|
||||
* Remove the listener with the given uuid associated to the given event name.
|
||||
*
|
||||
*
|
||||
* @name module:util.EventEmitter#off
|
||||
* @function
|
||||
* @public
|
||||
@ -106,8 +112,9 @@ export class EventEmitter
|
||||
{
|
||||
let relevantUuidListeners = this._listeners.get(name);
|
||||
|
||||
if (relevantUuidListeners && relevantUuidListeners.length) {
|
||||
this._listeners.set(name, relevantUuidListeners.filter( uuidlistener => (uuidlistener.uuid != uuid) ) );
|
||||
if (relevantUuidListeners && relevantUuidListeners.length)
|
||||
{
|
||||
this._listeners.set(name, relevantUuidListeners.filter(uuidlistener => (uuidlistener.uuid != uuid)));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -131,11 +138,14 @@ export class EventEmitter
|
||||
{
|
||||
let onceUuids = this._onceUuids.get(name);
|
||||
let self = this;
|
||||
relevantUuidListeners.forEach( ({uuid, listener}) => {
|
||||
relevantUuidListeners.forEach(({uuid, listener}) =>
|
||||
{
|
||||
listener(data);
|
||||
|
||||
if (typeof onceUuids !== 'undefined' && onceUuids.includes(uuid))
|
||||
{
|
||||
self.off(name, uuid);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@ -144,4 +154,4 @@ export class EventEmitter
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -3,20 +3,20 @@
|
||||
* Core Object.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import {EventEmitter} from './EventEmitter';
|
||||
import * as util from './Util';
|
||||
|
||||
|
||||
/**
|
||||
* <p>PsychoObject is the base class for all PsychoJS objects.
|
||||
* It is responsible for handling attributes.</p>
|
||||
*
|
||||
*
|
||||
* @class
|
||||
* @extends EventEmitter
|
||||
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
||||
@ -33,14 +33,16 @@ export class PsychObject extends EventEmitter
|
||||
|
||||
// name:
|
||||
if (typeof name === 'undefined')
|
||||
{
|
||||
name = this.constructor.name;
|
||||
}
|
||||
this._addAttribute('name', name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the PsychoJS instance.
|
||||
*
|
||||
*
|
||||
* @public
|
||||
* @return {PsychoJS} the PsychoJS instance
|
||||
*/
|
||||
@ -52,7 +54,7 @@ export class PsychObject extends EventEmitter
|
||||
|
||||
/**
|
||||
* Setter for the PsychoJS attribute.
|
||||
*
|
||||
*
|
||||
* @public
|
||||
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
||||
*/
|
||||
@ -77,17 +79,23 @@ export class PsychObject extends EventEmitter
|
||||
for (const attribute of this._userAttributes)
|
||||
{
|
||||
if (addComma)
|
||||
{
|
||||
representation += ', ';
|
||||
}
|
||||
addComma = true;
|
||||
|
||||
let value = util.toString(this['_'+attribute]);
|
||||
let value = util.toString(this['_' + attribute]);
|
||||
const l = value.length;
|
||||
if (l > 50)
|
||||
{
|
||||
if (value[l-1] === ')')
|
||||
if (value[l - 1] === ')')
|
||||
{
|
||||
value = value.substring(0, 50) + '~)';
|
||||
}
|
||||
else
|
||||
{
|
||||
value = value.substring(0, 50) + '~';
|
||||
}
|
||||
}
|
||||
|
||||
representation += attribute + '=' + value;
|
||||
@ -98,47 +106,60 @@ export class PsychObject extends EventEmitter
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set the value of an attribute.
|
||||
*
|
||||
*
|
||||
* @private
|
||||
* @param {string} attributeName - the name of the attribute
|
||||
* @param {object} attributeValue - the value of the attribute
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
* @param {string} [operation] - the binary operation such that the new value of the attribute is the result of the application of the operation to the current value of the attribute and attributeValue
|
||||
* @param {boolean} [stealth= false] - whether or not to call the potential attribute setters when setting the value of this attribute
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
* @param {string} [operation] - the binary operation such that the new value of the attribute is the result of the application of the operation to the current value of the attribute and attributeValue
|
||||
* @param {boolean} [stealth= false] - whether or not to call the potential attribute setters when setting the value of this attribute
|
||||
* @return {boolean} whether or not the value of that attribute has changed (false if the attribute
|
||||
* was not previously set)
|
||||
*/
|
||||
_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')
|
||||
throw Object.assign(response, { error: 'the attribute name cannot be' +
|
||||
' undefined' });
|
||||
if (typeof attributeValue == 'undefined') {
|
||||
{
|
||||
throw Object.assign(response, {
|
||||
error: 'the attribute name cannot be' +
|
||||
' undefined'
|
||||
});
|
||||
}
|
||||
if (typeof attributeValue == 'undefined')
|
||||
{
|
||||
this._psychoJS.logger.warn('setting the value of attribute: ' + attributeName + ' in PsychObject: ' + this._name + ' as: undefined');
|
||||
}
|
||||
|
||||
// (*) 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];
|
||||
|
||||
// operations can only be applied to numbers and array of numbers (which can be empty):
|
||||
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == 'number'))) {
|
||||
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == 'number')))
|
||||
{
|
||||
|
||||
// value is an array:
|
||||
if (Array.isArray(attributeValue)) {
|
||||
if (Array.isArray(attributeValue))
|
||||
{
|
||||
// old value is also an array
|
||||
if (Array.isArray(oldValue)) {
|
||||
if (Array.isArray(oldValue))
|
||||
{
|
||||
if (attributeValue.length !== oldValue.length)
|
||||
throw Object.assign(response, { error: 'old and new' +
|
||||
{
|
||||
throw Object.assign(response, {
|
||||
error: 'old and new' +
|
||||
' value should have' +
|
||||
' the same size when they are both arrays' });
|
||||
' the same size when they are both arrays'
|
||||
});
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
switch (operation)
|
||||
{
|
||||
case '':
|
||||
// no change to value;
|
||||
break;
|
||||
@ -161,14 +182,18 @@ export class PsychObject extends EventEmitter
|
||||
attributeValue = attributeValue.map((v, i) => oldValue[i] % v);
|
||||
break;
|
||||
default:
|
||||
throw Object.assign(response, { error: 'unsupported' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
throw Object.assign(response, {
|
||||
error: 'unsupported' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||
});
|
||||
}
|
||||
|
||||
} else
|
||||
}
|
||||
else
|
||||
// old value is a scalar
|
||||
{
|
||||
switch (operation) {
|
||||
switch (operation)
|
||||
{
|
||||
case '':
|
||||
// no change to value;
|
||||
break;
|
||||
@ -191,17 +216,22 @@ export class PsychObject extends EventEmitter
|
||||
attributeValue = attributeValue.map(v => oldValue % v);
|
||||
break;
|
||||
default:
|
||||
throw Object.assign(response, { error: 'unsupported' +
|
||||
' value: ' + JSON.stringify(attributeValue) + ' for' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
throw Object.assign(response, {
|
||||
error: 'unsupported' +
|
||||
' value: ' + JSON.stringify(attributeValue) + ' for' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||
});
|
||||
}
|
||||
}
|
||||
} else
|
||||
}
|
||||
else
|
||||
// value is a scalar
|
||||
{
|
||||
// old value is an array
|
||||
if (Array.isArray(oldValue)) {
|
||||
switch (operation) {
|
||||
if (Array.isArray(oldValue))
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case '':
|
||||
attributeValue = oldValue.map(v => attributeValue);
|
||||
break;
|
||||
@ -224,14 +254,18 @@ export class PsychObject extends EventEmitter
|
||||
attributeValue = oldValue.map(v => v % attributeValue);
|
||||
break;
|
||||
default:
|
||||
throw Object.assign(response, { error: 'unsupported' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
throw Object.assign(response, {
|
||||
error: 'unsupported' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||
});
|
||||
}
|
||||
|
||||
} else
|
||||
}
|
||||
else
|
||||
// old value is a scalar
|
||||
{
|
||||
switch (operation) {
|
||||
switch (operation)
|
||||
{
|
||||
case '':
|
||||
// no change to value;
|
||||
break;
|
||||
@ -254,14 +288,19 @@ export class PsychObject extends EventEmitter
|
||||
attributeValue = oldValue % attributeValue;
|
||||
break;
|
||||
default:
|
||||
throw Object.assign(response, { error: 'unsupported' +
|
||||
' value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
throw Object.assign(response, {
|
||||
error: 'unsupported' +
|
||||
' value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else
|
||||
throw 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.
|
||||
*
|
||||
*
|
||||
* <p>Notes:
|
||||
* <ul>
|
||||
* <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>
|
||||
* </ul></p>
|
||||
*
|
||||
* @protected
|
||||
* @protected
|
||||
* @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)
|
||||
*
|
||||
@ -313,18 +352,22 @@ export class PsychObject extends EventEmitter
|
||||
// (*) add (argument name, argument value) pairs to the attribute map:
|
||||
let attributeMap = new Map();
|
||||
for (let i = 1; i < callArgs.length; ++i)
|
||||
{
|
||||
attributeMap.set(callArgs[i], args[i - 1]);
|
||||
}
|
||||
|
||||
// (*) set the value, define the get/set<attributeName> properties and define the getter and setter:
|
||||
for (let [name, value] of attributeMap.entries())
|
||||
{
|
||||
this._addAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an attribute to this instance (e.g. define setters and getters) and affect a value to it.
|
||||
*
|
||||
* @protected
|
||||
*
|
||||
* @protected
|
||||
* @param {string} name - the name 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);
|
||||
if (typeof this[getPropertyName] === 'undefined')
|
||||
{
|
||||
this[getPropertyName] = () => this['_' + name];
|
||||
}
|
||||
|
||||
const setPropertyName = 'set' + name[0].toUpperCase() + name.substr(1);
|
||||
if (typeof this[setPropertyName] === 'undefined')
|
||||
this[setPropertyName] = (value, log = false) => {
|
||||
{
|
||||
this[setPropertyName] = (value, log = false) =>
|
||||
{
|
||||
this._setAttribute(name, value, log);
|
||||
};
|
||||
}
|
||||
|
||||
Object.defineProperty(this, name, {
|
||||
configurable: true,
|
||||
get() { return this[getPropertyName](); /* return this['_' + name];*/ },
|
||||
set(value) { this[setPropertyName](value); }
|
||||
get()
|
||||
{
|
||||
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
|
||||
@ -354,4 +408,4 @@ export class PsychObject extends EventEmitter
|
||||
this._userAttributes.add(name);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Scheduler.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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,
|
||||
* called tasks, after each frame is displayed.</p>
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* Tasks are either another [Scheduler]{@link module:util.Scheduler}, or a
|
||||
* JavaScript functions returning one of the following codes:
|
||||
@ -22,19 +22,19 @@
|
||||
* <li>Scheduler.Event.QUIT: Quit the scheduler.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* <p> It is possible to create sub-schedulers, e.g. to handle loops.
|
||||
* Sub-schedulers are added to a parent scheduler as a normal
|
||||
* task would be by calling [scheduler.add(subScheduler)]{@link module:util.Scheduler#add}.</p>
|
||||
*
|
||||
*
|
||||
* <p> Conditional branching is also available:
|
||||
* [scheduler.addConditionalBranches]{@link module:util.Scheduler#addConditional}</p>
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @name module:util.Scheduler
|
||||
* @class
|
||||
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
|
||||
*
|
||||
*
|
||||
*/
|
||||
export class Scheduler
|
||||
{
|
||||
@ -61,24 +61,28 @@ export class Scheduler
|
||||
* @public
|
||||
* @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.
|
||||
*
|
||||
* @callback module:util.Scheduler~Task
|
||||
/**
|
||||
* Task to be run by the scheduler.
|
||||
*
|
||||
* @callback module:util.Scheduler~Task
|
||||
* @param {*} [args] optional arguments
|
||||
*/
|
||||
*/
|
||||
/**
|
||||
* Schedule a new task.
|
||||
*
|
||||
*
|
||||
* @name module:util.Scheduler#add
|
||||
* @public
|
||||
* @param {module:util.Scheduler~Task | module:util.Scheduler} task - the task to be scheduled
|
||||
* @param {...*} args - arguments for that task
|
||||
*/
|
||||
add(task, ...args) {
|
||||
add(task, ...args)
|
||||
{
|
||||
this._taskList.push(task);
|
||||
this._argsList.push(args);
|
||||
}
|
||||
@ -86,28 +90,34 @@ export class Scheduler
|
||||
|
||||
/**
|
||||
* Condition evaluated when the task is run.
|
||||
*
|
||||
*
|
||||
* @callback module:util.Scheduler~Condition
|
||||
* @return {boolean}
|
||||
* @return {boolean}
|
||||
*/
|
||||
/**
|
||||
* Schedule a series of task or another, based on a condition.
|
||||
*
|
||||
*
|
||||
* <p>Note: the tasks are [sub-schedulers]{@link module:util.Scheduler}.</p>
|
||||
*
|
||||
*
|
||||
* @name module:util.Scheduler#addConditional
|
||||
* @public
|
||||
* @param {module:util.Scheduler~Condition} condition - the condition
|
||||
* @param {module:util.Scheduler} thenScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is satisfied
|
||||
* @param {module:util.Scheduler} elseScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is not satisfied
|
||||
*/
|
||||
addConditional(condition, thenScheduler, elseScheduler) {
|
||||
addConditional(condition, thenScheduler, elseScheduler)
|
||||
{
|
||||
const self = this;
|
||||
let task = function () {
|
||||
let task = function ()
|
||||
{
|
||||
if (condition())
|
||||
{
|
||||
self.add(thenScheduler);
|
||||
}
|
||||
else
|
||||
{
|
||||
self.add(elseScheduler);
|
||||
}
|
||||
|
||||
return Scheduler.Event.NEXT;
|
||||
};
|
||||
@ -124,11 +134,14 @@ export class Scheduler
|
||||
* @name module:util.Scheduler#start
|
||||
* @public
|
||||
*/
|
||||
start() {
|
||||
start()
|
||||
{
|
||||
const self = this;
|
||||
let update = () => {
|
||||
let update = () =>
|
||||
{
|
||||
// stop the animation if need be:
|
||||
if (self._stopAtNextUpdate) {
|
||||
if (self._stopAtNextUpdate)
|
||||
{
|
||||
self._status = Scheduler.Status.STOPPED;
|
||||
return;
|
||||
}
|
||||
@ -137,7 +150,8 @@ export class Scheduler
|
||||
|
||||
// run the next scheduled tasks until a scene render is requested:
|
||||
const state = self._runNextTasks();
|
||||
if (state === Scheduler.Event.QUIT) {
|
||||
if (state === Scheduler.Event.QUIT)
|
||||
{
|
||||
self._status = Scheduler.Status.STOPPED;
|
||||
return;
|
||||
}
|
||||
@ -156,11 +170,12 @@ export class Scheduler
|
||||
|
||||
/**
|
||||
* Stop this scheduler.
|
||||
*
|
||||
*
|
||||
* @name module:util.Scheduler#stop
|
||||
* @public
|
||||
*/
|
||||
stop() {
|
||||
stop()
|
||||
{
|
||||
this._status = Scheduler.Status.STOPPED;
|
||||
this._stopAtNextTask = true;
|
||||
this._stopAtNextUpdate = true;
|
||||
@ -174,52 +189,67 @@ export class Scheduler
|
||||
* @private
|
||||
* @return {module:util.Scheduler#Event} the state of the scheduler after the last task ran
|
||||
*/
|
||||
_runNextTasks() {
|
||||
_runNextTasks()
|
||||
{
|
||||
this._status = Scheduler.Status.RUNNING;
|
||||
|
||||
let state = Scheduler.Event.NEXT;
|
||||
while (state === Scheduler.Event.NEXT) {
|
||||
while (state === Scheduler.Event.NEXT)
|
||||
{
|
||||
// check if we need to quit:
|
||||
if (this._stopAtNextTask)
|
||||
{
|
||||
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 (typeof this._currentTask == 'undefined') {
|
||||
if (typeof this._currentTask == 'undefined')
|
||||
{
|
||||
|
||||
// a task is available in the taskList:
|
||||
if (this._taskList.length > 0) {
|
||||
if (this._taskList.length > 0)
|
||||
{
|
||||
this._currentTask = this._taskList.shift();
|
||||
this._currentArgs = this._argsList.shift();
|
||||
}
|
||||
// the taskList is empty: we quit
|
||||
else {
|
||||
else
|
||||
{
|
||||
this._currentTask = undefined;
|
||||
this._currentArgs = undefined;
|
||||
return Scheduler.Event.QUIT;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// we are repeating a task
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// otherwise, we assume that the current task is a scheduler and we run its tasks until a rendering
|
||||
// of the scene is required.
|
||||
// note: "if (this._currentTask instanceof Scheduler)" does not work because of CORS...
|
||||
else {
|
||||
else
|
||||
{
|
||||
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 (!this._psychoJS.experiment.experimentEnded)
|
||||
{
|
||||
state = Scheduler.Event.NEXT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the current task's return status is FLIP_REPEAT, we will re-run it, otherwise
|
||||
// we move onto the next task:
|
||||
if (state !== Scheduler.Event.FLIP_REPEAT) {
|
||||
if (state !== Scheduler.Event.FLIP_REPEAT)
|
||||
{
|
||||
this._currentTask = undefined;
|
||||
this._currentArgs = undefined;
|
||||
}
|
||||
@ -234,7 +264,7 @@ export class Scheduler
|
||||
|
||||
/**
|
||||
* Events.
|
||||
*
|
||||
*
|
||||
* @name module:util.Scheduler#Event
|
||||
* @enum {Symbol}
|
||||
* @readonly
|
||||
@ -248,7 +278,7 @@ Scheduler.Event = {
|
||||
|
||||
/**
|
||||
* Render the scene and repeat the task.
|
||||
*/
|
||||
*/
|
||||
FLIP_REPEAT: Symbol.for('FLIP_REPEAT'),
|
||||
|
||||
/**
|
||||
@ -281,4 +311,4 @@ Scheduler.Status = {
|
||||
* The Scheduler is stopped.
|
||||
*/
|
||||
STOPPED: Symbol.for('STOPPED')
|
||||
};
|
||||
};
|
||||
|
383
js/util/Util.js
383
js/util/Util.js
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Various utilities.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
@ -24,19 +24,23 @@
|
||||
* class NewClass extends mix(BaseClass).with(Mixin1, Mixin2) { ... }
|
||||
*/
|
||||
export let mix = (superclass) => new MixinBuilder(superclass);
|
||||
class MixinBuilder {
|
||||
constructor(superclass) {
|
||||
this.superclass = superclass;
|
||||
}
|
||||
|
||||
class MixinBuilder
|
||||
{
|
||||
constructor(superclass)
|
||||
{
|
||||
this.superclass = superclass;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mixins
|
||||
* @returns {*}
|
||||
*/
|
||||
with(...mixins) {
|
||||
return mixins.reduce((c, mixin) => mixin(c), this.superclass);
|
||||
}
|
||||
with(...mixins)
|
||||
{
|
||||
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]
|
||||
* where error is null if there was no error
|
||||
*/
|
||||
export function promiseToTupple(promise) {
|
||||
export function promiseToTupple(promise)
|
||||
{
|
||||
return promise
|
||||
.then(data => [null, data])
|
||||
.catch(error => [error, null]);
|
||||
@ -68,7 +73,8 @@ export function promiseToTupple(promise) {
|
||||
*/
|
||||
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);
|
||||
return v.toString(16);
|
||||
});
|
||||
@ -85,13 +91,16 @@ export function makeUuid()
|
||||
*/
|
||||
export function getErrorStack()
|
||||
{
|
||||
try {
|
||||
try
|
||||
{
|
||||
throw Error('');
|
||||
} catch(error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
// we need to remove the second line since it references getErrorStack:
|
||||
let stack = error.stack.split("\n");
|
||||
stack.splice(1, 1);
|
||||
|
||||
|
||||
return JSON.stringify(stack.join('\n'));
|
||||
}
|
||||
}
|
||||
@ -108,10 +117,22 @@ export function getErrorStack()
|
||||
*/
|
||||
export function isEmpty(x)
|
||||
{
|
||||
if (typeof x === '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;
|
||||
if (typeof x === '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;
|
||||
}
|
||||
@ -133,38 +154,65 @@ export function detectBrowser()
|
||||
{
|
||||
// Opera 8.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+
|
||||
const isFirefox = (typeof InstallTrigger !== 'undefined');
|
||||
if (isFirefox) return 'Firefox';
|
||||
if (isFirefox)
|
||||
{
|
||||
return 'Firefox';
|
||||
}
|
||||
|
||||
// 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));
|
||||
if (isSafari) return 'Safari';
|
||||
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p)
|
||||
{
|
||||
return p.toString() === "[object SafariRemoteNotification]";
|
||||
})(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification));
|
||||
if (isSafari)
|
||||
{
|
||||
return 'Safari';
|
||||
}
|
||||
|
||||
// Internet Explorer 6-11
|
||||
// const isIE6 = !window.XMLHttpRequest;
|
||||
// const isIE7 = document.all && window.XMLHttpRequest && !XDomainRequest && !window.opera;
|
||||
// const isIE8 = document.documentMode==8;
|
||||
const isIE = /*@cc_on!@*/false || !!document.documentMode;
|
||||
if (isIE) return 'IE';
|
||||
if (isIE)
|
||||
{
|
||||
return 'IE';
|
||||
}
|
||||
|
||||
// Edge 20+
|
||||
const isEdge = !isIE && !!window.StyleMedia;
|
||||
if (isEdge) return 'Edge';
|
||||
if (isEdge)
|
||||
{
|
||||
return 'Edge';
|
||||
}
|
||||
|
||||
// Chrome 1+
|
||||
const isChrome = window.chrome;
|
||||
if (isChrome) return 'Chrome';
|
||||
if (isChrome)
|
||||
{
|
||||
return 'Chrome';
|
||||
}
|
||||
|
||||
// Chromium-based Edge:
|
||||
const isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") !== -1);
|
||||
if (isEdgeChromium) return 'EdgeChromium';
|
||||
if (isEdgeChromium)
|
||||
{
|
||||
return 'EdgeChromium';
|
||||
}
|
||||
|
||||
// Blink engine detection
|
||||
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
||||
if (isBlink) return 'Blink';
|
||||
// Blink engine detection
|
||||
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
||||
if (isBlink)
|
||||
{
|
||||
return 'Blink';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
@ -188,20 +236,30 @@ export function detectBrowser()
|
||||
*/
|
||||
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')
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj === 'string')
|
||||
{
|
||||
obj = [obj];
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map( e => {
|
||||
if (Array.isArray(obj))
|
||||
{
|
||||
return obj.map(e =>
|
||||
{
|
||||
let n = Number.parseFloat(e);
|
||||
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;
|
||||
});
|
||||
}
|
||||
@ -224,17 +282,20 @@ export function IsPointInsidePolygon(point, vertices)
|
||||
{
|
||||
const x = point[0];
|
||||
const y = point[1];
|
||||
|
||||
|
||||
let isInside = false;
|
||||
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++)
|
||||
{
|
||||
const xi = vertices[i][0], yi = vertices[i][1];
|
||||
const xj = vertices[j][0], yj = vertices[j][1];
|
||||
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||
if (intersect) isInside = !isInside;
|
||||
}
|
||||
|
||||
return isInside;
|
||||
const xi = vertices[i][0], yi = vertices[i][1];
|
||||
const xj = vertices[j][0], yj = vertices[j][1];
|
||||
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||
if (intersect)
|
||||
{
|
||||
isInside = !isInside;
|
||||
}
|
||||
}
|
||||
|
||||
return isInside;
|
||||
}
|
||||
|
||||
|
||||
@ -269,18 +330,22 @@ export function shuffle(array)
|
||||
* @param {string} units - the 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')
|
||||
{
|
||||
throw 'cannot get the position of an undefined object';
|
||||
}
|
||||
|
||||
let objectWin = undefined;
|
||||
|
||||
// object has a getPos function:
|
||||
if (typeof object.getPos === 'function') {
|
||||
if (typeof object.getPos === 'function')
|
||||
{
|
||||
units = object.units;
|
||||
objectWin = object.win;
|
||||
object = object.getPos();
|
||||
@ -289,8 +354,9 @@ export function getPositionFromObject(object, units)
|
||||
// convert object to pixel units:
|
||||
return to_px(object, units, objectWin);
|
||||
}
|
||||
catch (error) {
|
||||
throw Object.assign(response, { error });
|
||||
catch (error)
|
||||
{
|
||||
throw Object.assign(response, {error});
|
||||
}
|
||||
}
|
||||
|
||||
@ -308,18 +374,25 @@ export function getPositionFromObject(object, units)
|
||||
*/
|
||||
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')
|
||||
{
|
||||
return pos;
|
||||
}
|
||||
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]);
|
||||
return [pos[0] * minSize, pos[1] * minSize];
|
||||
}
|
||||
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)
|
||||
{
|
||||
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')
|
||||
{
|
||||
return pos;
|
||||
}
|
||||
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]);
|
||||
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)
|
||||
{
|
||||
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')
|
||||
{
|
||||
return pos;
|
||||
if (posUnit === 'pix') {
|
||||
}
|
||||
if (posUnit === 'pix')
|
||||
{
|
||||
const minSize = Math.min(win.size[0], win.size[1]);
|
||||
return [pos[0] / minSize, pos[1] / minSize];
|
||||
}
|
||||
if (posUnit === 'norm') {
|
||||
if (posUnit === 'norm')
|
||||
{
|
||||
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)
|
||||
{
|
||||
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')
|
||||
{
|
||||
return to_px(pos, posUnit, win);
|
||||
}
|
||||
if (win._units === 'norm')
|
||||
{
|
||||
return to_norm(pos, posUnit, win);
|
||||
}
|
||||
if (win._units === 'height')
|
||||
{
|
||||
return to_height(pos, posUnit, win);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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')
|
||||
{
|
||||
return to_px(pos, posUnit, win);
|
||||
}
|
||||
if (targetUnit === 'norm')
|
||||
{
|
||||
return to_norm(pos, posUnit, win);
|
||||
}
|
||||
if (targetUnit === 'height')
|
||||
{
|
||||
return to_height(pos, posUnit, win);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (typeof object === 'undefined')
|
||||
{
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
if (!object)
|
||||
{
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (typeof object === 'string')
|
||||
{
|
||||
return object;
|
||||
}
|
||||
|
||||
// if the object is a class and has a toString method:
|
||||
if (object.constructor.toString().substring(0, 5) === 'class' && typeof object.toString === 'function')
|
||||
{
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
try {
|
||||
const symbolReplacer = (key, value) => {
|
||||
try
|
||||
{
|
||||
const symbolReplacer = (key, value) =>
|
||||
{
|
||||
if (typeof value === 'symbol')
|
||||
{
|
||||
value = Symbol.keyFor(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
return JSON.stringify(object, symbolReplacer);
|
||||
} catch (e)
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
return 'Object (circular)';
|
||||
}
|
||||
@ -502,20 +615,20 @@ export function toString(object)
|
||||
|
||||
if (!String.prototype.format)
|
||||
{
|
||||
String.prototype.format = function()
|
||||
String.prototype.format = function ()
|
||||
{
|
||||
var args = arguments;
|
||||
return this
|
||||
.replace(/{(\d+)}/g, function(match, number)
|
||||
{
|
||||
return typeof args[number] != 'undefined' ? args[number] : match;
|
||||
})
|
||||
.replace(/{([$_a-zA-Z][$_a-zA-Z0-9]*)}/g, function(match, name)
|
||||
{
|
||||
//console.log("n=" + name + " args[0][name]=" + args[0][name]);
|
||||
return args.length > 0 && args[0][name] !== undefined ? args[0][name] : match;
|
||||
});
|
||||
};
|
||||
var args = arguments;
|
||||
return this
|
||||
.replace(/{(\d+)}/g, function (match, number)
|
||||
{
|
||||
return typeof args[number] != 'undefined' ? args[number] : match;
|
||||
})
|
||||
.replace(/{([$_a-zA-Z][$_a-zA-Z0-9]*)}/g, function (match, name)
|
||||
{
|
||||
//console.log("n=" + name + " args[0][name]=" + args[0][name]);
|
||||
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';
|
||||
|
||||
if (typeof jqXHR.responseJSON !== 'undefined')
|
||||
{
|
||||
errorMsg = jqXHR.responseJSON;
|
||||
|
||||
}
|
||||
else if (typeof jqXHR.responseText !== 'undefined')
|
||||
{
|
||||
errorMsg = jqXHR.responseText;
|
||||
|
||||
}
|
||||
else if (typeof errorThrown !== 'undefined')
|
||||
{
|
||||
errorMsg = errorThrown;
|
||||
}
|
||||
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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>
|
||||
@ -557,9 +673,12 @@ export function getRequestError(jqXHR, textStatus, errorThrown)
|
||||
* @param {Object} obj - the input object
|
||||
* @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))
|
||||
return false;
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const x = parseFloat(obj);
|
||||
return (x | 0) === x;
|
||||
@ -573,12 +692,12 @@ export function isInt(obj) {
|
||||
* @function
|
||||
* @public
|
||||
* @returns {URLSearchParams} the iterable URLSearchParams
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const urlParameters = util.getUrlParameters();
|
||||
* for (const [key, value] of urlParameters)
|
||||
* console.log(key + ' = ' + value);
|
||||
*
|
||||
*
|
||||
*/
|
||||
export function getUrlParameters()
|
||||
{
|
||||
@ -611,10 +730,12 @@ export function addInfoFromUrl(info)
|
||||
// note: parameters starting with a double underscore are reserved for client/server communication,
|
||||
// we do not add them to info
|
||||
// for (const [key, value] of infoFromUrl)
|
||||
infoFromUrl.forEach( (value, key) =>
|
||||
infoFromUrl.forEach((value, key) =>
|
||||
{
|
||||
if (key.indexOf('__') !== 0)
|
||||
{
|
||||
info[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return info;
|
||||
@ -641,34 +762,48 @@ export function addInfoFromUrl(info)
|
||||
* @param {number | Array.<number> | string} selection - the selection
|
||||
* @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
|
||||
// and return that entry:
|
||||
if (isInt(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:
|
||||
else if (Array.isArray(selection))
|
||||
return array.filter( (e,i) => (selection.includes(i)) );
|
||||
|
||||
// if selection is a string, we decode it:
|
||||
else if (typeof selection === 'string') {
|
||||
{
|
||||
return array.filter((e, i) => (selection.includes(i)));
|
||||
}// if selection is a string, we decode it:
|
||||
else if (typeof selection === 'string')
|
||||
{
|
||||
if (selection.indexOf(',') > -1)
|
||||
{
|
||||
return selection.split(',').map(a => selectFromArray(array, a));
|
||||
// return flattenArray( selection.split(',').map(a => selectFromArray(array, a)) );
|
||||
else if (selection.indexOf(':') > -1) {
|
||||
}// return flattenArray( selection.split(',').map(a => selectFromArray(array, a)) );
|
||||
else if (selection.indexOf(':') > -1)
|
||||
{
|
||||
let sliceParams = selection.split(':').map(a => parseInt(a));
|
||||
if (sliceParams.length === 3)
|
||||
{
|
||||
return sliceArray(array, sliceParams[0], sliceParams[2], sliceParams[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return sliceArray(array, ...sliceParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
* @returns {Array.<Object>} the flatten array
|
||||
*/
|
||||
export function flattenArray(array) {
|
||||
export function flattenArray(array)
|
||||
{
|
||||
return array.reduce(
|
||||
(flat, next) => {
|
||||
flat.push( (Array.isArray(next) && Array.isArray(next[0])) ? flattenArray(next) : next );
|
||||
(flat, next) =>
|
||||
{
|
||||
flat.push((Array.isArray(next) && Array.isArray(next[0])) ? flattenArray(next) : next);
|
||||
return flat;
|
||||
},
|
||||
[]
|
||||
@ -706,22 +843,36 @@ export function flattenArray(array) {
|
||||
*/
|
||||
export function sliceArray(array, from = NaN, to = NaN, step = NaN)
|
||||
{
|
||||
if (isNaN(from)) from = 0;
|
||||
if (isNaN(to)) to = array.length;
|
||||
if (isNaN(from))
|
||||
{
|
||||
from = 0;
|
||||
}
|
||||
if (isNaN(to))
|
||||
{
|
||||
to = array.length;
|
||||
}
|
||||
|
||||
let arraySlice = array.slice(from, to);
|
||||
|
||||
if (isNaN(step))
|
||||
{
|
||||
return arraySlice;
|
||||
}
|
||||
|
||||
if (step < 0)
|
||||
{
|
||||
arraySlice.reverse();
|
||||
}
|
||||
|
||||
step = Math.abs(step);
|
||||
if (step == 1)
|
||||
{
|
||||
return arraySlice;
|
||||
}
|
||||
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 {string} type - the MIME type of the data, e.g. 'text/csv' or 'application/json'
|
||||
*/
|
||||
export function offerDataForDownload(filename, data, type) {
|
||||
const blob = new Blob([data], { type });
|
||||
export function offerDataForDownload(filename, data, type)
|
||||
{
|
||||
const blob = new Blob([data], {type});
|
||||
if (window.navigator.msSaveOrOpenBlob)
|
||||
{
|
||||
window.navigator.msSaveBlob(blob, filename);
|
||||
else {
|
||||
}
|
||||
else
|
||||
{
|
||||
let elem = window.document.createElement('a');
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
elem.download = filename;
|
||||
|
@ -1,22 +1,22 @@
|
||||
/**
|
||||
* Image Stimulus.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { VisualStim } from './VisualStim';
|
||||
import { Color } from '../util/Color';
|
||||
import { ColorMixin } from '../util/ColorMixin';
|
||||
import {VisualStim} from './VisualStim';
|
||||
import {Color} from '../util/Color';
|
||||
import {ColorMixin} from '../util/ColorMixin';
|
||||
import * as util from '../util/Util';
|
||||
|
||||
|
||||
/**
|
||||
* Image Stimulus.
|
||||
*
|
||||
*
|
||||
* @name module:visual.ImageStim
|
||||
* @class
|
||||
* @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 {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.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 {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
|
||||
@ -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.flipHoriz= false] - whether or not to flip horizontally
|
||||
* @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
|
||||
*/
|
||||
export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
{
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
image,
|
||||
mask,
|
||||
pos,
|
||||
units,
|
||||
ori,
|
||||
size,
|
||||
color = new Color('white'),
|
||||
opacity = 1.0,
|
||||
contrast = 1.0,
|
||||
texRes = 128,
|
||||
depth = 0,
|
||||
interpolate = false,
|
||||
flipHoriz = false,
|
||||
flipVert = false,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {}) {
|
||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
||||
name,
|
||||
win,
|
||||
image,
|
||||
mask,
|
||||
pos,
|
||||
units,
|
||||
ori,
|
||||
size,
|
||||
color = new Color('white'),
|
||||
opacity = 1.0,
|
||||
contrast = 1.0,
|
||||
texRes = 128,
|
||||
depth = 0,
|
||||
interpolate = false,
|
||||
flipHoriz = false,
|
||||
flipVert = false,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {})
|
||||
{
|
||||
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||
|
||||
this.psychoJS.logger.debug('create a new ImageStim with name: ', name);
|
||||
|
||||
this._addAttributes(ImageStim, image, mask, color, contrast, texRes, interpolate, depth, flipHoriz, flipVert);
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the image attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.ImageStim#setImage
|
||||
* @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
|
||||
*/
|
||||
setImage(image, log = false) {
|
||||
const response = { origin: 'ImageStim.setImage', context: 'when setting the image of ImageStim: ' + this._name };
|
||||
setImage(image, log = false)
|
||||
{
|
||||
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
|
||||
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.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
|
||||
if (typeof image === 'string')
|
||||
{
|
||||
image = this.psychoJS.serverManager.getResource(image);
|
||||
}
|
||||
|
||||
// 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" }';
|
||||
}
|
||||
|
||||
@ -109,36 +122,47 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
this._needUpdate = true;
|
||||
}
|
||||
catch (error) {
|
||||
throw Object.assign(response, { error });
|
||||
catch (error)
|
||||
{
|
||||
throw Object.assign(response, {error});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the mask attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.ImageStim#setImage
|
||||
* @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
|
||||
*/
|
||||
setMask(mask, log = false) {
|
||||
const response = { origin: 'ImageStim.setMask', context: 'when setting the mask of ImageStim: ' + this._name };
|
||||
setMask(mask, log = false)
|
||||
{
|
||||
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
|
||||
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.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
|
||||
if (typeof mask === 'string')
|
||||
{
|
||||
mask = this.psychoJS.serverManager.getResource(mask);
|
||||
}
|
||||
|
||||
// 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" }';
|
||||
}
|
||||
|
||||
@ -149,21 +173,23 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
this._needUpdate = true;
|
||||
}
|
||||
catch (error) {
|
||||
throw Object.assign(response, { error });
|
||||
catch (error)
|
||||
{
|
||||
throw Object.assign(response, {error});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the flipVert attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.ImageStim#setFlipVert
|
||||
* @public
|
||||
* @param {boolean} flipVert - whether or not to flip vertically
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setFlipVert(flipVert, log = false) {
|
||||
setFlipVert(flipVert, log = false)
|
||||
{
|
||||
this._setAttribute('flipVert', flipVert, log);
|
||||
|
||||
this._needUpdate = true;
|
||||
@ -172,13 +198,14 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Setter for the flipHoriz attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.ImageStim#setFlipHoriz
|
||||
* @public
|
||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setFlipHoriz(flipHoriz, log = false) {
|
||||
setFlipHoriz(flipHoriz, log = false)
|
||||
{
|
||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:visual.ImageStim#contains
|
||||
* @public
|
||||
* @param {Object} object - the object
|
||||
@ -197,12 +224,20 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
contains(object, units)
|
||||
{
|
||||
if (typeof this._image === 'undefined')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// get position of object:
|
||||
let objectPos_px = util.getPositionFromObject(object, units);
|
||||
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:
|
||||
// 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.
|
||||
*
|
||||
*
|
||||
* @name module:visual.ImageStim#_updateIfNeeded
|
||||
* @private
|
||||
*/
|
||||
_updateIfNeeded() {
|
||||
_updateIfNeeded()
|
||||
{
|
||||
if (!this._needUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needUpdate = false;
|
||||
|
||||
this._pixi = undefined;
|
||||
|
||||
// no image to draw: return immediately
|
||||
if (typeof this._image === 'undefined')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// prepare the 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;
|
||||
|
||||
// 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._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
|
||||
// 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;
|
||||
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.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.rotation = this.ori * Math.PI / 180;
|
||||
this._pixi.anchor.x = 0.5;
|
||||
|
@ -1,18 +1,18 @@
|
||||
/**
|
||||
* Movie Stimulus.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { VisualStim } from './VisualStim';
|
||||
import { Color } from '../util/Color';
|
||||
import { ColorMixin } from '../util/ColorMixin';
|
||||
import {VisualStim} from './VisualStim';
|
||||
import {Color} from '../util/Color';
|
||||
import {ColorMixin} from '../util/ColorMixin';
|
||||
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.
|
||||
*/
|
||||
export class MovieStim extends VisualStim {
|
||||
export class MovieStim extends VisualStim
|
||||
{
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
movie,
|
||||
pos,
|
||||
units,
|
||||
ori,
|
||||
size,
|
||||
color = new Color('white'),
|
||||
opacity = 1.0,
|
||||
contrast = 1.0,
|
||||
interpolate = false,
|
||||
flipHoriz = false,
|
||||
flipVert = false,
|
||||
loop = false,
|
||||
volume = 1.0,
|
||||
noAudio = false,
|
||||
autoPlay = true,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {}) {
|
||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
||||
name,
|
||||
win,
|
||||
movie,
|
||||
pos,
|
||||
units,
|
||||
ori,
|
||||
size,
|
||||
color = new Color('white'),
|
||||
opacity = 1.0,
|
||||
contrast = 1.0,
|
||||
interpolate = false,
|
||||
flipHoriz = false,
|
||||
flipVert = false,
|
||||
loop = false,
|
||||
volume = 1.0,
|
||||
noAudio = false,
|
||||
autoPlay = true,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {})
|
||||
{
|
||||
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||
|
||||
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');
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the movie attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.MovieStim#setMovie
|
||||
* @public
|
||||
* @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
|
||||
*/
|
||||
setMovie(movie, log = false) {
|
||||
const response = { origin: 'MovieStim.setMovie', context: 'when setting the movie of MovieStim: ' + this._name };
|
||||
setMovie(movie, log = false)
|
||||
{
|
||||
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
|
||||
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.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
|
||||
if (typeof movie === 'string')
|
||||
{
|
||||
movie = this.psychoJS.serverManager.getResource(movie);
|
||||
}
|
||||
|
||||
// movie should now be an actual HTMLVideoElement: we raise an error if it is not
|
||||
if (!(movie instanceof HTMLVideoElement))
|
||||
{
|
||||
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`);
|
||||
}
|
||||
@ -114,12 +129,16 @@ export class MovieStim extends VisualStim {
|
||||
this._setAttribute('movie', movie, log);
|
||||
|
||||
// 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;
|
||||
}
|
||||
catch (error) {
|
||||
throw Object.assign(response, { error });
|
||||
catch (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 {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setVolume(volume, log = false) {
|
||||
setVolume(volume, log = false)
|
||||
{
|
||||
this._setAttribute('volume', volume, log);
|
||||
|
||||
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} [log= false] - whether of not to log
|
||||
*/
|
||||
setNoAudio(noAudio, log = false) {
|
||||
setNoAudio(noAudio, log = false)
|
||||
{
|
||||
this._setAttribute('noAudio', noAudio, log);
|
||||
|
||||
this._needUpdate = true;
|
||||
@ -150,13 +171,14 @@ export class MovieStim extends VisualStim {
|
||||
|
||||
/**
|
||||
* Setter for the flipVert attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.MovieStim#setFlipVert
|
||||
* @public
|
||||
* @param {boolean} flipVert - whether or not to flip vertically
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setFlipVert(flipVert, log = false) {
|
||||
setFlipVert(flipVert, log = false)
|
||||
{
|
||||
this._setAttribute('flipVert', flipVert, log);
|
||||
|
||||
this._needUpdate = true;
|
||||
@ -165,13 +187,14 @@ export class MovieStim extends VisualStim {
|
||||
|
||||
/**
|
||||
* Setter for the flipHoriz attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.MovieStim#setFlipHoriz
|
||||
* @public
|
||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setFlipHoriz(flipHoriz, log = false) {
|
||||
setFlipHoriz(flipHoriz, log = false)
|
||||
{
|
||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||
|
||||
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
|
||||
*/
|
||||
reset(log = false) {
|
||||
reset(log = false)
|
||||
{
|
||||
this.status = PsychoJS.Status.NOT_STARTED;
|
||||
this._movie.pause();
|
||||
if (this._hasFastSeek) this._movie.fastSeek(0);
|
||||
if (this._hasFastSeek)
|
||||
{
|
||||
this._movie.fastSeek(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start playing the movie.
|
||||
*
|
||||
*
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
play(log = false) {
|
||||
play(log = false)
|
||||
{
|
||||
this.status = PsychoJS.Status.STARTED;
|
||||
this._movie.play();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pause the movie.
|
||||
*
|
||||
* Pause the movie.
|
||||
*
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
pause(log = false) {
|
||||
pause(log = false)
|
||||
{
|
||||
this.status = PsychoJS.Status.STOPPED;
|
||||
this._movie.pause();
|
||||
}
|
||||
@ -217,14 +246,17 @@ export class MovieStim extends VisualStim {
|
||||
*
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
stop(log = false) {
|
||||
stop(log = false)
|
||||
{
|
||||
this.status = PsychoJS.Status.STOPPED;
|
||||
this._movie.pause();
|
||||
if (this._hasFastSeek) this._movie.fastSeek(0);
|
||||
if (this._hasFastSeek)
|
||||
{
|
||||
this._movie.fastSeek(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
seek(timePoint, log = false) {
|
||||
if (timePoint < 0 || timePoint > this._movie.duration) {
|
||||
seek(timePoint, log = false)
|
||||
{
|
||||
if (timePoint < 0 || timePoint > this._movie.duration)
|
||||
{
|
||||
throw {
|
||||
origin: 'MovieStim.seek',
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:visual.MovieStim#contains
|
||||
* @public
|
||||
* @param {Object} object - the object
|
||||
* @param {string} units - the units
|
||||
* @return {boolean} whether or not the image contains the object
|
||||
*/
|
||||
contains(object, units) {
|
||||
contains(object, units)
|
||||
{
|
||||
// get position of object:
|
||||
let objectPos_px = util.getPositionFromObject(object, units);
|
||||
if (typeof objectPos_px === 'undefined') {
|
||||
if (typeof objectPos_px === 'undefined')
|
||||
{
|
||||
throw {
|
||||
origin: 'MovieStim.contains',
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:visual.MovieStim#_updateIfNeeded
|
||||
* @private
|
||||
*/
|
||||
_updateIfNeeded() {
|
||||
_updateIfNeeded()
|
||||
{
|
||||
if (!this._needUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needUpdate = false;
|
||||
|
||||
this._pixi = undefined;
|
||||
|
||||
// no movie to draw: return immediately
|
||||
if (typeof this._movie === 'undefined')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// create a PixiJS video sprite:
|
||||
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
|
||||
// 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;
|
||||
return;
|
||||
}
|
||||
@ -325,7 +370,8 @@ export class MovieStim extends VisualStim {
|
||||
// stimulus size:
|
||||
// note: we use the size of the texture if MovieStim has no specified size:
|
||||
let stimSize = this.size;
|
||||
if (typeof stimSize === 'undefined') {
|
||||
if (typeof stimSize === 'undefined')
|
||||
{
|
||||
const textureSize = [this._texture.width, this._texture.height];
|
||||
stimSize = util.to_unit(textureSize, 'pix', this.win, this.units);
|
||||
}
|
||||
|
@ -2,14 +2,14 @@
|
||||
* Polygonal Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { ShapeStim } from './ShapeStim';
|
||||
import { Color } from '../util/Color';
|
||||
import {ShapeStim} from './ShapeStim';
|
||||
import {Color} from '../util/Color';
|
||||
|
||||
|
||||
/**
|
||||
@ -59,7 +59,23 @@ export class Polygon extends ShapeStim
|
||||
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);
|
||||
|
||||
@ -68,11 +84,12 @@ export class Polygon extends ShapeStim
|
||||
this._updateVertices();
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the radius attribute.
|
||||
*
|
||||
@ -90,7 +107,6 @@ export class Polygon extends ShapeStim
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the edges attribute.
|
||||
*
|
||||
@ -121,7 +137,9 @@ export class Polygon extends ShapeStim
|
||||
const angle = 2.0 * Math.PI / this._edges;
|
||||
const vertices = [];
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -2,19 +2,19 @@
|
||||
* Rectangular Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { ShapeStim } from './ShapeStim';
|
||||
import { Color } from '../util/Color';
|
||||
import {ShapeStim} from './ShapeStim';
|
||||
import {Color} from '../util/Color';
|
||||
|
||||
|
||||
/**
|
||||
* <p>Rectangular visual stimulus.</p>
|
||||
*
|
||||
*
|
||||
* @name module:visual.Rect
|
||||
* @class
|
||||
* @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 {Window} options.win - the associated Window
|
||||
* @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 {number} [options.opacity= 1.0] - the opacity
|
||||
* @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.depth= 0] - the depth
|
||||
* @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
|
||||
*/
|
||||
export class Rect extends ShapeStim
|
||||
{
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
lineWidth = 1.5,
|
||||
lineColor = new Color('white'),
|
||||
fillColor,
|
||||
opacity = 1.0,
|
||||
width = 0.5,
|
||||
height = 0.5,
|
||||
pos = [0, 0],
|
||||
size = 1.0,
|
||||
ori = 0.0,
|
||||
units,
|
||||
contrast = 1.0,
|
||||
depth = 0,
|
||||
interpolate = true,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {})
|
||||
name,
|
||||
win,
|
||||
lineWidth = 1.5,
|
||||
lineColor = new Color('white'),
|
||||
fillColor,
|
||||
opacity = 1.0,
|
||||
width = 0.5,
|
||||
height = 0.5,
|
||||
pos = [0, 0],
|
||||
size = 1.0,
|
||||
ori = 0.0,
|
||||
units,
|
||||
contrast = 1.0,
|
||||
depth = 0,
|
||||
interpolate = true,
|
||||
autoDraw,
|
||||
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);
|
||||
|
||||
@ -68,16 +84,17 @@ export class Rect extends ShapeStim
|
||||
this._updateVertices();
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the width attribute.
|
||||
*
|
||||
* @name module:visual.Rect#setWidth
|
||||
* @public
|
||||
* @public
|
||||
* @param {number} width - the rectange width
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
@ -90,12 +107,11 @@ export class Rect extends ShapeStim
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the height attribute.
|
||||
*
|
||||
* @name module:visual.Rect#setHeight
|
||||
* @public
|
||||
* @public
|
||||
* @param {number} height - the rectange height
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
@ -110,9 +126,9 @@ export class Rect extends ShapeStim
|
||||
|
||||
/**
|
||||
* Update the vertices.
|
||||
*
|
||||
*
|
||||
* @name module:visual.Rect#_updateVertices
|
||||
* @private
|
||||
* @private
|
||||
*/
|
||||
_updateVertices()
|
||||
{
|
||||
|
@ -3,21 +3,21 @@
|
||||
* Basic Shape Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { VisualStim } from './VisualStim';
|
||||
import { Color } from '../util/Color';
|
||||
import { ColorMixin } from '../util/ColorMixin';
|
||||
import {VisualStim} from './VisualStim';
|
||||
import {Color} from '../util/Color';
|
||||
import {ColorMixin} from '../util/ColorMixin';
|
||||
import * as util from '../util/Util';
|
||||
|
||||
|
||||
/**
|
||||
* <p>This class provides the basic functionalities of shape stimuli.</p>
|
||||
*
|
||||
*
|
||||
* @class
|
||||
* @extends VisualStim
|
||||
* @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 {Window} options.win - the associated Window
|
||||
* @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 {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
|
||||
@ -37,32 +37,32 @@ import * as util from '../util/Util';
|
||||
* @param {number} [options.contrast= 1.0] - the contrast
|
||||
* @param {number} [options.depth= 0] - the depth
|
||||
* @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
|
||||
*/
|
||||
export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
{
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
lineWidth = 1.5,
|
||||
lineColor = new Color('white'),
|
||||
fillColor,
|
||||
opacity = 1.0,
|
||||
vertices = [[-0.5, 0], [0, 0.5], [0.5, 0]],
|
||||
closeShape = true,
|
||||
pos = [0, 0],
|
||||
size = 1.0,
|
||||
ori = 0.0,
|
||||
units,
|
||||
contrast = 1.0,
|
||||
depth = 0,
|
||||
interpolate = true,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {})
|
||||
name,
|
||||
win,
|
||||
lineWidth = 1.5,
|
||||
lineColor = new Color('white'),
|
||||
fillColor,
|
||||
opacity = 1.0,
|
||||
vertices = [[-0.5, 0], [0, 0.5], [0.5, 0]],
|
||||
closeShape = true,
|
||||
pos = [0, 0],
|
||||
size = 1.0,
|
||||
ori = 0.0,
|
||||
units,
|
||||
contrast = 1.0,
|
||||
depth = 0,
|
||||
interpolate = true,
|
||||
autoDraw,
|
||||
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:
|
||||
this._pixiPolygon_px = undefined;
|
||||
@ -76,7 +76,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Force a refresh of the stimulus.
|
||||
*
|
||||
@ -91,7 +90,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the size attribute.
|
||||
*
|
||||
@ -108,12 +106,11 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the line width attribute.
|
||||
*
|
||||
* @name module:visual.ShapeStim#setLineWidth
|
||||
* @public
|
||||
* @public
|
||||
* @param {number} lineWidth - the line width
|
||||
* @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 (vertices in ShapeStim.KnownShapes)
|
||||
{
|
||||
vertices = ShapeStim.KnownShapes[vertices];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw 'unknown shape';
|
||||
}
|
||||
}
|
||||
|
||||
this._setAttribute('vertices', vertices, log);
|
||||
@ -197,7 +198,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
}
|
||||
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
|
||||
* @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);
|
||||
|
||||
// get position of object:
|
||||
const objectPos_px = util.getPositionFromObject(object, units);
|
||||
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
|
||||
// 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
|
||||
* @private
|
||||
*/
|
||||
_updateIfNeeded() {
|
||||
_updateIfNeeded()
|
||||
{
|
||||
if (!this._needUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needUpdate = false;
|
||||
|
||||
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
|
||||
if (typeof this._pixiPolygon_px === 'undefined')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// prepare the polygon in the given color and opacity:
|
||||
this._pixi = new PIXI.Graphics();
|
||||
this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5);
|
||||
if (typeof this._fillColor !== 'undefined')
|
||||
{
|
||||
this._pixi.beginFill(this._fillColor.int, this._opacity);
|
||||
}
|
||||
this._pixi.drawPolygon(this._pixiPolygon_px);
|
||||
if (typeof this._fillColor !== 'undefined')
|
||||
{
|
||||
this._pixi.endFill();
|
||||
}
|
||||
|
||||
// set polygon position and rotation:
|
||||
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.
|
||||
*
|
||||
@ -270,9 +286,12 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @private
|
||||
* @return {Object} the PIXI polygon corresponding to this stimulus vertices.
|
||||
*/
|
||||
_getPolygon(/*force = false*/) {
|
||||
_getPolygon(/*force = false*/)
|
||||
{
|
||||
if (!this._needVertexUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needVertexUpdate = false;
|
||||
|
||||
console.log('>>>>>>>>> CREATING PIXI POLYGON!!!!');
|
||||
@ -285,13 +304,17 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
this._getVertices_px(/*force*/);
|
||||
let coords_px = [];
|
||||
for (const vertex_px of this._vertices_px)
|
||||
{
|
||||
coords_px.push.apply(coords_px, vertex_px);
|
||||
}
|
||||
|
||||
// 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:
|
||||
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[1]);
|
||||
}
|
||||
@ -303,7 +326,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the vertices in pixel units.
|
||||
*
|
||||
@ -319,12 +341,16 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
// handle flipping:
|
||||
let flip = [1.0, 1.0];
|
||||
if ('_flipHoriz' in this && this._flipHoriz)
|
||||
{
|
||||
flip[0] = -1.0;
|
||||
}
|
||||
if ('_flipVert' in this && this._flipVert)
|
||||
{
|
||||
flip[1] = -1.0;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@ -355,20 +381,20 @@ ShapeStim.KnownShapes = {
|
||||
],
|
||||
|
||||
star7: [
|
||||
[0.0,0.5],
|
||||
[0.09,0.18],
|
||||
[0.39,0.31],
|
||||
[0.19,0.04],
|
||||
[0.49,-0.11],
|
||||
[0.16,-0.12],
|
||||
[0.22,-0.45],
|
||||
[0.0,-0.2],
|
||||
[-0.22,-0.45],
|
||||
[-0.16,-0.12],
|
||||
[-0.49,-0.11],
|
||||
[-0.19,0.04],
|
||||
[-0.39,0.31],
|
||||
[-0.09,0.18]
|
||||
[0.0, 0.5],
|
||||
[0.09, 0.18],
|
||||
[0.39, 0.31],
|
||||
[0.19, 0.04],
|
||||
[0.49, -0.11],
|
||||
[0.16, -0.12],
|
||||
[0.22, -0.45],
|
||||
[0.0, -0.2],
|
||||
[-0.22, -0.45],
|
||||
[-0.16, -0.12],
|
||||
[-0.49, -0.11],
|
||||
[-0.19, 0.04],
|
||||
[-0.39, 0.31],
|
||||
[-0.09, 0.18]
|
||||
]
|
||||
|
||||
};
|
||||
};
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Slider Stimulus.
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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)
|
||||
{
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
pos,
|
||||
size,
|
||||
ori,
|
||||
units = 'height',
|
||||
name,
|
||||
win,
|
||||
pos,
|
||||
size,
|
||||
ori,
|
||||
units = 'height',
|
||||
|
||||
color = new Color('LightGray'),
|
||||
contrast = 1.0,
|
||||
opacity,
|
||||
color = new Color('LightGray'),
|
||||
contrast = 1.0,
|
||||
opacity,
|
||||
|
||||
style = [Slider.Style.RATING],
|
||||
ticks = [1,2,3,4,5],
|
||||
labels = [],
|
||||
labelHeight,
|
||||
granularity = 0,
|
||||
flip = false,
|
||||
readOnly = false,
|
||||
style = [Slider.Style.RATING],
|
||||
ticks = [1, 2, 3, 4, 5],
|
||||
labels = [],
|
||||
labelHeight,
|
||||
granularity = 0,
|
||||
flip = false,
|
||||
readOnly = false,
|
||||
|
||||
fontFamily = 'Helvetica',
|
||||
bold = true,
|
||||
italic = false,
|
||||
fontSize,
|
||||
fontFamily = 'Helvetica',
|
||||
bold = true,
|
||||
italic = false,
|
||||
fontSize,
|
||||
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {}) {
|
||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {})
|
||||
{
|
||||
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||
|
||||
this._needMarkerUpdate = false;
|
||||
|
||||
@ -113,7 +114,9 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
this._isCategorical = (this._ticks.length === 0);
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
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
|
||||
*/
|
||||
contains(object, units) {
|
||||
contains(object, units)
|
||||
{
|
||||
// get position of object:
|
||||
let objectPos_px = util.getPositionFromObject(object, units);
|
||||
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;
|
||||
}
|
||||
@ -145,7 +153,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @name module:visual.Slider#reset
|
||||
* @public
|
||||
*/
|
||||
reset() {
|
||||
reset()
|
||||
{
|
||||
this.psychoJS.logger.debug('reset Slider: ', this._name);
|
||||
|
||||
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:
|
||||
if (typeof this._marker !== 'undefined')
|
||||
{
|
||||
this._marker.alpha = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -170,12 +181,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @public
|
||||
* @returns {number | undefined} the rating or undefined if there is none
|
||||
*/
|
||||
getRating() {
|
||||
getRating()
|
||||
{
|
||||
const historyLength = this._history.length;
|
||||
if (historyLength > 0)
|
||||
return this._history[historyLength-1]['rating'];
|
||||
{
|
||||
return this._history[historyLength - 1]['rating'];
|
||||
}
|
||||
else
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -186,12 +202,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @public
|
||||
* @returns {number | undefined} the response time (in second) or undefined if there is none
|
||||
*/
|
||||
getRT() {
|
||||
getRT()
|
||||
{
|
||||
const historyLength = this._history.length;
|
||||
if (historyLength > 0)
|
||||
return this._history[historyLength-1]['responseTime'];
|
||||
{
|
||||
return this._history[historyLength - 1]['responseTime'];
|
||||
}
|
||||
else
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -205,14 +226,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @param {number} [fontSize] - the font size
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setFontSize(fontSize, log = false) {
|
||||
if (typeof fontSize === 'undefined') {
|
||||
setFontSize(fontSize, log = false)
|
||||
{
|
||||
if (typeof fontSize === 'undefined')
|
||||
{
|
||||
fontSize = (this._units === 'pix') ? 14 : 0.03;
|
||||
}
|
||||
|
||||
const hasChanged = this._setAttribute('fontSize', fontSize, log);
|
||||
|
||||
if (hasChanged) {
|
||||
if (hasChanged)
|
||||
{
|
||||
this._needUpdate = 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} [log= false] - whether of not to log
|
||||
*/
|
||||
setBold(bold = true, log = false) {
|
||||
setBold(bold = true, log = false)
|
||||
{
|
||||
const hasChanged = this._setAttribute('bold', bold, log);
|
||||
|
||||
if (hasChanged) {
|
||||
this._fontWeight = (bold)?'bold':'normal';
|
||||
if (hasChanged)
|
||||
{
|
||||
this._fontWeight = (bold) ? 'bold' : 'normal';
|
||||
this._needUpdate = 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} [log= false] - whether of not to log
|
||||
*/
|
||||
setItalic(italic = false, log = false) {
|
||||
setItalic(italic = false, log = false)
|
||||
{
|
||||
const hasChanged = this._setAttribute('italic', italic, log);
|
||||
|
||||
if (hasChanged) {
|
||||
this._fontStyle = (italic)?'italic':'normal';
|
||||
if (hasChanged)
|
||||
{
|
||||
this._fontStyle = (italic) ? 'italic' : 'normal';
|
||||
this._needUpdate = true;
|
||||
this._needVertexUpdate = true;
|
||||
}
|
||||
@ -265,17 +293,23 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @name module:visual.Slider#setReadOnly
|
||||
* @public
|
||||
* @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);
|
||||
|
||||
if (hasChanged) {
|
||||
if (hasChanged)
|
||||
{
|
||||
// halve the opacity:
|
||||
if (readOnly)
|
||||
{
|
||||
this._opacity /= 2.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._opacity *= 2.0;
|
||||
}
|
||||
|
||||
this._needUpdate = true;
|
||||
}
|
||||
@ -294,12 +328,14 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @param {number} displayedRating - the displayed rating
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setMarkerPos(displayedRating, log = false) {
|
||||
setMarkerPos(displayedRating, log = false)
|
||||
{
|
||||
const previousMarkerPos = this._markerPos;
|
||||
this._markerPos = this._granularise(displayedRating);
|
||||
|
||||
// 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._needUpdate = true;
|
||||
}
|
||||
@ -314,13 +350,16 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @name module:visual.Slider#setRating
|
||||
* @public
|
||||
* @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);
|
||||
this._markerPos = rating;
|
||||
if (this._isCategorical)
|
||||
{
|
||||
rating = this._labels[Math.round(rating)];
|
||||
}
|
||||
|
||||
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 {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
_recordRating(rating, responseTime = undefined, log = false) {
|
||||
_recordRating(rating, responseTime = undefined, log = false)
|
||||
{
|
||||
// get response time:
|
||||
if (typeof responseTime === 'undefined')
|
||||
{
|
||||
responseTime = this._responseClock.getTime();
|
||||
}
|
||||
|
||||
// set rating:
|
||||
// rating = this._granularise(rating);
|
||||
@ -363,9 +405,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @name module:visual.Slider#_updateIfNeeded
|
||||
* @private
|
||||
*/
|
||||
_updateIfNeeded() {
|
||||
_updateIfNeeded()
|
||||
{
|
||||
if (!this._needUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needUpdate = false;
|
||||
|
||||
this._buildSlider();
|
||||
@ -387,23 +432,30 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @name module:visual.Slider#_updateMarker
|
||||
* @private
|
||||
*/
|
||||
_updateMarker() {
|
||||
_updateMarker()
|
||||
{
|
||||
if (!this._needMarkerUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needMarkerUpdate = false;
|
||||
|
||||
if (typeof this._marker !== 'undefined') {
|
||||
if (typeof this._markerPos !== 'undefined') {
|
||||
if (typeof this._marker !== 'undefined')
|
||||
{
|
||||
if (typeof this._markerPos !== 'undefined')
|
||||
{
|
||||
const visibleMarkerPos = this._ratingToPos([this._markerPos]);
|
||||
this._marker.position = util.to_pixiPoint(visibleMarkerPos[0], this.units, this.win);
|
||||
this._marker.alpha = 1;
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
this._marker.alpha = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @private
|
||||
*/
|
||||
_buildSlider() {
|
||||
_buildSlider()
|
||||
{
|
||||
if (!this._needVertexUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needVertexUpdate = false;
|
||||
|
||||
this._applyStyle();
|
||||
@ -430,30 +485,39 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
// (*) central bar:
|
||||
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);
|
||||
if (typeof this._barFillColor !== 'undefined')
|
||||
{
|
||||
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]);
|
||||
if (typeof this._barFillColor !== 'undefined')
|
||||
{
|
||||
this._body.endFill();
|
||||
}
|
||||
}
|
||||
|
||||
// (*) ticks:
|
||||
if (this._isCategorical) {
|
||||
if (this._isCategorical)
|
||||
{
|
||||
this._ticks = [...Array(this._labels.length)].map((_, i) => i);
|
||||
this._granularity = 1.0;
|
||||
}
|
||||
const tickPositions = this._ratingToPos(this._ticks);
|
||||
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);
|
||||
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);
|
||||
const tickSize_px = util.to_px(this._tickSize, this._units, this._win);
|
||||
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);
|
||||
this._body.lineTo(tickPosition_px[0] + tickSize_px[0]/2, tickPosition_px[1] + tickSize_px[1]/2);
|
||||
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);
|
||||
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.drawCircle(tickPosition_px[0], tickPosition_px[1], Math.max(tickSize_px[0], tickSize_px[1]));
|
||||
this._body.endFill();
|
||||
@ -465,7 +529,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
// outside of marker and labels:
|
||||
const eventCaptureRectangle = new PIXI.Graphics();
|
||||
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]);
|
||||
eventCaptureRectangle.endFill();
|
||||
this._pixi.addChild(eventCaptureRectangle);
|
||||
@ -473,17 +537,19 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
// (*) labels:
|
||||
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);
|
||||
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], {
|
||||
fontFamily : this._fontFamily,
|
||||
fontFamily: this._fontFamily,
|
||||
fontWeight: this._fontWeight,
|
||||
fontStyle: this._fontStyle,
|
||||
fontSize: Math.round(fontSize_px[0]),
|
||||
fill: this._labelColor.hex,
|
||||
align: this._labelAlign});
|
||||
align: this._labelAlign
|
||||
});
|
||||
|
||||
const labelBounds = labelText.getBounds(true);
|
||||
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.y = this._labelAnchor.y;
|
||||
|
||||
if (this._isHorizontal()) {
|
||||
if (this._isHorizontal())
|
||||
{
|
||||
if (this._flip)
|
||||
{
|
||||
labelText.position.y -= labelBounds.height + tickSize_px[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
labelText.position.y += tickSize_px[1];
|
||||
} else {
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this._flip)
|
||||
{
|
||||
labelText.position.x += tickSize_px[0];
|
||||
}
|
||||
else if (this._labelOri === 0)
|
||||
{
|
||||
labelText.position.x -= labelBounds.width + tickSize_px[0];
|
||||
}
|
||||
else
|
||||
if (this._labelOri === 0)
|
||||
labelText.position.x -= labelBounds.width + tickSize_px[0];
|
||||
else
|
||||
labelText.position.x -= tickSize_px[0];
|
||||
{
|
||||
labelText.position.x -= tickSize_px[0];
|
||||
}
|
||||
}
|
||||
|
||||
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._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.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();
|
||||
}
|
||||
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.beginFill(this._markerColor.int, this._opacity);
|
||||
this._marker.moveTo(0, 0);
|
||||
if (this._isHorizontal()) {
|
||||
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);
|
||||
if (this._isHorizontal())
|
||||
{
|
||||
if (this._flip)
|
||||
{
|
||||
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);
|
||||
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();
|
||||
@ -554,25 +643,29 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
const self = this;
|
||||
self._markerDragging = false;
|
||||
|
||||
this._marker.pointerdown = this._marker.mousedown = this._marker.touchstart = (event) => {
|
||||
if (event.data.button === 0) {
|
||||
this._marker.pointerdown = this._marker.mousedown = this._marker.touchstart = (event) =>
|
||||
{
|
||||
if (event.data.button === 0)
|
||||
{
|
||||
self._markerDragging = true;
|
||||
/* not quite right, just yet (as of May 2020)
|
||||
// set markerPos, but not rating:
|
||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||
self._markerPos = self._granularise(rating);
|
||||
/* not quite right, just yet (as of May 2020)
|
||||
// set markerPos, but not rating:
|
||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||
self._markerPos = self._granularise(rating);
|
||||
|
||||
self._needMarkerUpdate = true;
|
||||
*/
|
||||
self._needMarkerUpdate = true;
|
||||
*/
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
// pointer was released inside the marker: if we were dragging, we record the rating
|
||||
this._marker.pointerup = this._marker.mouseup = this._marker.touchend = (event) => {
|
||||
if (self._markerDragging) {
|
||||
this._marker.pointerup = this._marker.mouseup = this._marker.touchend = (event) =>
|
||||
{
|
||||
if (self._markerDragging)
|
||||
{
|
||||
self._markerDragging = false;
|
||||
|
||||
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
|
||||
this._marker.pointerupoutside = this._marker.mouseupoutside = this._marker.touchendoutside = (event) => {
|
||||
if (self._markerDragging) {
|
||||
this._marker.pointerupoutside = this._marker.mouseupoutside = this._marker.touchendoutside = (event) =>
|
||||
{
|
||||
if (self._markerDragging)
|
||||
{
|
||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||
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
|
||||
this._marker.pointermove = (event) => {
|
||||
if (self._markerDragging) {
|
||||
this._marker.pointermove = (event) =>
|
||||
{
|
||||
if (self._markerDragging)
|
||||
{
|
||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||
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 rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||
self._recordRating(rating);
|
||||
@ -650,14 +748,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @name module:visual.Slider#_applyStyle
|
||||
* @private
|
||||
*/
|
||||
_applyStyle() {
|
||||
_applyStyle()
|
||||
{
|
||||
|
||||
// default style:
|
||||
if (this._isHorizontal()) {
|
||||
if (this._isHorizontal())
|
||||
{
|
||||
this._barSize = [this._size[0], 0];
|
||||
this._tickSize = [0, this._size[1]];
|
||||
this._labelAnchor = new PIXI.Point(0.5, 0);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
this._barSize = [0, this._size[1]];
|
||||
this._tickSize = [this._size[0], 0];
|
||||
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._tickType = Slider.Shape.LINE;
|
||||
this._tickColor = this._color;
|
||||
this._tickColor = this._color;
|
||||
|
||||
this._markerColor = new Color('red');
|
||||
this._markerType = Slider.Shape.DISC;
|
||||
@ -681,37 +783,45 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
|
||||
// rating:
|
||||
if (this._style.indexOf(Slider.Style.RATING) > -1) {
|
||||
if (this._style.indexOf(Slider.Style.RATING) > -1)
|
||||
{
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
// 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._markerSize = this._markerSize.map( s => s*2 );
|
||||
this._markerSize = this._markerSize.map(s => s * 2);
|
||||
}
|
||||
|
||||
// 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!');
|
||||
//TODO
|
||||
}
|
||||
|
||||
// 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._barFillColor = new Color('black');
|
||||
this._tickColor = new Color('black');
|
||||
this._tickColor = new Color('black');
|
||||
this._markerColor = new Color('white');
|
||||
this._labelColor = new Color('black');
|
||||
}
|
||||
|
||||
// labels45:
|
||||
if (this._style.indexOf(Slider.Style.LABELS45) > -1) {
|
||||
if (this._flip) {
|
||||
if (this._style.indexOf(Slider.Style.LABELS45) > -1)
|
||||
{
|
||||
if (this._flip)
|
||||
{
|
||||
this._labelAnchor = new PIXI.Point(0, 0.5);
|
||||
this._labelAlign = 'left';
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
this._labelAnchor = new PIXI.Point(1, 0.5);
|
||||
this._labelAlign = 'right';
|
||||
}
|
||||
@ -719,7 +829,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
}
|
||||
|
||||
// radio:
|
||||
if (this._style.indexOf(Slider.Style.RADIO) > -1) {
|
||||
if (this._style.indexOf(Slider.Style.RADIO) > -1)
|
||||
{
|
||||
this._barLineWidth_px = 0;
|
||||
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,
|
||||
* with 0 at the center of the Slider)
|
||||
*/
|
||||
_ratingToPos(ratings) {
|
||||
const range = this._ticks[this._ticks.length-1] - this._ticks[0];
|
||||
_ratingToPos(ratings)
|
||||
{
|
||||
const range = this._ticks[this._ticks.length - 1] - this._ticks[0];
|
||||
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
|
||||
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.
|
||||
* @returns {number} the corresponding rating.
|
||||
*/
|
||||
_posToRating(pos_px) {
|
||||
const range = this._ticks[this._ticks.length-1] - this._ticks[0];
|
||||
_posToRating(pos_px)
|
||||
{
|
||||
const range = this._ticks[this._ticks.length - 1] - this._ticks[0];
|
||||
const size_px = util.to_px(this._size, this._units, this._win);
|
||||
if (this._isHorizontal())
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
* @returns {boolean} whether or not the slider is horizontal
|
||||
*/
|
||||
_isHorizontal() {
|
||||
_isHorizontal()
|
||||
{
|
||||
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
|
||||
* @returns {number} the new rating with granularity applied
|
||||
*/
|
||||
_granularise(rating) {
|
||||
_granularise(rating)
|
||||
{
|
||||
if (typeof rating === 'undefined')
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Text Stimulus.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @license Distributed under the terms of the MIT License
|
||||
*/
|
||||
|
||||
|
||||
import { VisualStim } from './VisualStim';
|
||||
import { Color } from '../util/Color';
|
||||
import { ColorMixin } from '../util/ColorMixin';
|
||||
import {VisualStim} from './VisualStim';
|
||||
import {Color} from '../util/Color';
|
||||
import {ColorMixin} from '../util/ColorMixin';
|
||||
import * as util from '../util/Util';
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ import * as util from '../util/Util';
|
||||
* @param {Object} options
|
||||
* @param {String} options.name - the name used when logging messages from this stimulus
|
||||
* @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 {Array.<number>} [options.pos= [0, 0]] - the position of the center of the text
|
||||
* @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} [flipHoriz= false] - whether or not to flip the text horizontally
|
||||
* @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
|
||||
*
|
||||
*
|
||||
* @todo vertical alignment, and orientation are currently NOT implemented
|
||||
*/
|
||||
export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
{
|
||||
constructor({
|
||||
name,
|
||||
win,
|
||||
text = 'Hello World',
|
||||
font = 'Arial',
|
||||
pos,
|
||||
color = new Color('white'),
|
||||
opacity,
|
||||
contrast = 1.0,
|
||||
units,
|
||||
ori,
|
||||
height = 0.1,
|
||||
bold = false,
|
||||
italic = false,
|
||||
alignHoriz = 'left',
|
||||
alignVert = 'center',
|
||||
wrapWidth,
|
||||
flipHoriz = false,
|
||||
flipVert = false,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {})
|
||||
name,
|
||||
win,
|
||||
text = 'Hello World',
|
||||
font = 'Arial',
|
||||
pos,
|
||||
color = new Color('white'),
|
||||
opacity,
|
||||
contrast = 1.0,
|
||||
units,
|
||||
ori,
|
||||
height = 0.1,
|
||||
bold = false,
|
||||
italic = false,
|
||||
alignHoriz = 'left',
|
||||
alignVert = 'center',
|
||||
wrapWidth,
|
||||
flipHoriz = false,
|
||||
flipVert = false,
|
||||
autoDraw,
|
||||
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);
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the text attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#setText
|
||||
* @public
|
||||
* @param {string} text - the text
|
||||
@ -96,13 +98,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Setter for the alignHoriz attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#setAlignHoriz
|
||||
* @public
|
||||
* @param {string} alignHoriz - the text horizontal alignment, e.g. 'center'
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setAlignHoriz(alignHoriz, log) {
|
||||
setAlignHoriz(alignHoriz, log)
|
||||
{
|
||||
this._setAttribute('alignHoriz', alignHoriz, log);
|
||||
|
||||
this._needUpdate = true;
|
||||
@ -112,16 +115,24 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Setter for the wrapWidth attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#setWrapWidth
|
||||
* @public
|
||||
* @param {boolean} wrapWidth - whether or not to wrap the text at the given width
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setWrapWidth(wrapWidth, log) {
|
||||
if (typeof wrapWidth === 'undefined') {
|
||||
setWrapWidth(wrapWidth, log)
|
||||
{
|
||||
if (typeof wrapWidth === 'undefined')
|
||||
{
|
||||
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);
|
||||
}
|
||||
@ -135,16 +146,24 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Setter for the height attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#setHeight
|
||||
* @public
|
||||
* @param {number} height - text height
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setHeight(height, log) {
|
||||
if (typeof height === 'undefined') {
|
||||
setHeight(height, log)
|
||||
{
|
||||
if (typeof height === 'undefined')
|
||||
{
|
||||
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);
|
||||
}
|
||||
@ -158,13 +177,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Setter for the italic attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#setItalic
|
||||
* @public
|
||||
* @param {boolean} italic - whether or not the text is italic
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setItalic(italic, log) {
|
||||
setItalic(italic, log)
|
||||
{
|
||||
this._setAttribute('italic', italic, log);
|
||||
|
||||
this._needUpdate = true;
|
||||
@ -174,13 +194,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Setter for the bold attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#setBold
|
||||
* @public
|
||||
* @param {boolean} bold - whether or not the text is bold
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setBold(bold, log) {
|
||||
setBold(bold, log)
|
||||
{
|
||||
this._setAttribute('bold', bold, log);
|
||||
|
||||
this._needUpdate = true;
|
||||
@ -190,13 +211,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Setter for the flipVert attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#setFlipVert
|
||||
* @public
|
||||
* @param {boolean} flipVert - whether or not to flip vertically
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setFlipVert(flipVert, log) {
|
||||
setFlipVert(flipVert, log)
|
||||
{
|
||||
this._setAttribute('flipVert', flipVert, log);
|
||||
|
||||
this._needUpdate = true;
|
||||
@ -206,13 +228,14 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Setter for the flipHoriz attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#setFlipHoriz
|
||||
* @public
|
||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setFlipHoriz(flipHoriz, log) {
|
||||
setFlipHoriz(flipHoriz, log)
|
||||
{
|
||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#contains
|
||||
* @public
|
||||
* @param {Object} object - the object
|
||||
* @param {string} units - the units
|
||||
* @return {boolean} whether or not the object is inside the bounding box of the text
|
||||
*
|
||||
*
|
||||
* @todo this is currently NOT implemented
|
||||
*/
|
||||
contains(object, units) {
|
||||
contains(object, units)
|
||||
{
|
||||
// get position of object:
|
||||
let objectPos_px = util.getPositionFromObject(object, units);
|
||||
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:
|
||||
// TODO
|
||||
@ -245,16 +275,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
/**
|
||||
* Update the stimulus, if necessary.
|
||||
*
|
||||
*
|
||||
* @name module:visual.TextStim#_updateIfNeeded
|
||||
* @private
|
||||
*
|
||||
*
|
||||
* @todo take size into account
|
||||
*/
|
||||
_updateIfNeeded()
|
||||
{
|
||||
if (!this._needUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needUpdate = false;
|
||||
|
||||
this._heightPix = this._getLengthPix(this._height);
|
||||
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Base class for all visual stimuli.
|
||||
*
|
||||
*
|
||||
* @author Alain Pitiot
|
||||
* @version 2020.1
|
||||
* @version 2020.5
|
||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||
* @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.
|
||||
*
|
||||
*
|
||||
* @name module:visual.VisualStim
|
||||
* @class
|
||||
* @extends MinimalStim
|
||||
@ -28,7 +28,7 @@ import * as util from '../util/Util';
|
||||
* @param {number} [options.opacity= 1.0] - the opacity
|
||||
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
|
||||
* @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
|
||||
*/
|
||||
export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
||||
@ -43,7 +43,7 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
||||
size,
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @name module:visual.VisualStim#setSize
|
||||
* @public
|
||||
* @public
|
||||
* @param {number | number[]} size - the stimulus size
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
setSize(size, log = false)
|
||||
{
|
||||
// size is either undefined or a tuple of numbers:
|
||||
if (typeof size !== 'undefined') {
|
||||
if (typeof size !== 'undefined')
|
||||
{
|
||||
size = util.toNumerical(size);
|
||||
if (!Array.isArray(size))
|
||||
{
|
||||
size = [size, size];
|
||||
}
|
||||
}
|
||||
|
||||
this._setAttribute('size', size, log);
|
||||
@ -90,10 +93,10 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
||||
|
||||
/**
|
||||
* Setter for the orientation attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.VisualStim#setOri
|
||||
* @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
|
||||
*/
|
||||
setOri(ori, log = false)
|
||||
@ -111,7 +114,7 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
||||
|
||||
/**
|
||||
* Setter for the position attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.VisualStim#setPos
|
||||
* @public
|
||||
* @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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the opacity attribute.
|
||||
*
|
||||
*
|
||||
* @name module:visual.VisualStim#setOpacity
|
||||
* @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
|
||||
*/
|
||||
setOpacity(opacity, log = false)
|
||||
|
Loading…
Reference in New Issue
Block a user