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:
|
||||
|
||||
|
@ -2,13 +2,13 @@
|
||||
* 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';
|
||||
|
||||
|
||||
/**
|
||||
@ -20,14 +20,18 @@ import { PsychoJS } from './PsychoJS';
|
||||
* @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 = [];
|
||||
@ -67,34 +71,52 @@ export class EventManager {
|
||||
getKeys({
|
||||
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 (timeStamped)
|
||||
keys.push([keyId, key.timestamp]);
|
||||
else
|
||||
keys.push(keyId);
|
||||
}
|
||||
else
|
||||
newBuffer.push(key); // keep key press in buffer
|
||||
{
|
||||
keyId = EventManager._reversePygletMap[key.code];
|
||||
}
|
||||
|
||||
if (keyId != null)
|
||||
{
|
||||
if (timeStamped)
|
||||
{
|
||||
keys.push([keyId, key.timestamp]);
|
||||
}
|
||||
else
|
||||
{
|
||||
keys.push(keyId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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]
|
||||
@ -123,7 +146,8 @@ export class EventManager {
|
||||
* @public
|
||||
* @return {EventManager.MouseInfo} the mouse info.
|
||||
*/
|
||||
getMouseInfo() {
|
||||
getMouseInfo()
|
||||
{
|
||||
return this._mouseInfo;
|
||||
}
|
||||
|
||||
@ -137,7 +161,8 @@ export class EventManager {
|
||||
*
|
||||
* @todo handle the attribs argument
|
||||
*/
|
||||
clearEvents(attribs) {
|
||||
clearEvents(attribs)
|
||||
{
|
||||
this.clearKeys();
|
||||
}
|
||||
|
||||
@ -149,7 +174,8 @@ export class EventManager {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
clearKeys() {
|
||||
clearKeys()
|
||||
{
|
||||
this._keyBuffer = [];
|
||||
}
|
||||
|
||||
@ -163,7 +189,8 @@ export class EventManager {
|
||||
*
|
||||
* @todo not implemented
|
||||
*/
|
||||
startMoveClock() {
|
||||
startMoveClock()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@ -176,7 +203,8 @@ export class EventManager {
|
||||
*
|
||||
* @todo not implemented
|
||||
*/
|
||||
stopMoveClock() {
|
||||
stopMoveClock()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@ -189,7 +217,8 @@ export class EventManager {
|
||||
*
|
||||
* @todo not implemented
|
||||
*/
|
||||
resetMoveClock() {
|
||||
resetMoveClock()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@ -201,10 +230,12 @@ export class EventManager {
|
||||
* @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,7 +298,8 @@ export class EventManager {
|
||||
}, false);
|
||||
|
||||
|
||||
renderer.view.addEventListener("touchmove", (event) => {
|
||||
renderer.view.addEventListener("touchmove", (event) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
self._mouseInfo.moveClock.reset();
|
||||
@ -275,7 +311,8 @@ 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;
|
||||
|
||||
@ -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,7 +363,6 @@ 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>
|
||||
@ -335,14 +373,20 @@ export class EventManager {
|
||||
* @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,12 +402,17 @@ 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];
|
||||
}
|
||||
}
|
||||
@ -598,8 +648,10 @@ EventManager._reversePygletMap = {};
|
||||
* @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;
|
||||
|
166
js/core/GUI.js
166
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);
|
||||
});
|
||||
|
||||
@ -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,12 +214,15 @@ 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:
|
||||
self._dialogComponent.button = 'Cancel';
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -313,7 +341,20 @@ export class GUI
|
||||
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)
|
||||
{
|
||||
@ -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,16 +451,19 @@ 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();
|
||||
}
|
||||
}
|
||||
}],
|
||||
|
||||
// open the dialog in the middle of the screen:
|
||||
@ -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,11 +590,15 @@ 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...');
|
||||
@ -549,8 +608,10 @@ export class GUI
|
||||
|
||||
// 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,21 +649,26 @@ 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);
|
||||
this._dialogScalingFactor = 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,9 +213,11 @@ export class Keyboard extends PsychObject {
|
||||
keyPresses.push(keyPress);
|
||||
|
||||
if (clear)
|
||||
{
|
||||
this._circularBuffer[precedingKeydownIndex] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* old approach: the circular buffer contains independent keydown and keyup events:
|
||||
let j = i - 1;
|
||||
@ -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.
|
||||
*
|
||||
@ -343,12 +373,16 @@ export class Keyboard extends PsychObject {
|
||||
// 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);
|
||||
|
||||
@ -392,7 +428,9 @@ export class Keyboard extends PsychObject {
|
||||
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);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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,7 +10,7 @@
|
||||
|
||||
import * as util from '../util/Util';
|
||||
import {MonotonicClock} from '../util/Clock';
|
||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
||||
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||
|
||||
|
||||
/**
|
||||
@ -23,7 +23,8 @@ import { ExperimentHandler } from '../data/ExperimentHandler';
|
||||
* @class
|
||||
* @param {*} threshold - the logging threshold, e.g. log4javascript.Level.ERROR
|
||||
*/
|
||||
export class Logger {
|
||||
export class Logger
|
||||
{
|
||||
|
||||
constructor(psychoJS, threshold)
|
||||
{
|
||||
@ -80,7 +81,6 @@ export class Logger {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Log a server message at the DATA level.
|
||||
*
|
||||
@ -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,7 +192,6 @@ export class Logger {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a custom console 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,16 +247,18 @@ 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';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return customLayout;
|
||||
|
@ -2,13 +2,13 @@
|
||||
* 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';
|
||||
|
||||
|
||||
/**
|
||||
@ -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,15 +101,20 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.status = PsychoJS.Status.STOPPED;
|
||||
}
|
||||
@ -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.'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@
|
||||
* 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';
|
||||
|
||||
|
||||
@ -26,13 +26,15 @@ import * as util from '../util/Util';
|
||||
*
|
||||
* @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
|
||||
} = {}) {
|
||||
} = {})
|
||||
{
|
||||
super(win._psychoJS, name);
|
||||
|
||||
// note: those are in window units:
|
||||
@ -56,7 +58,8 @@ export class Mouse extends PsychObject {
|
||||
* @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();
|
||||
@ -81,10 +84,14 @@ export class Mouse extends PsychObject {
|
||||
* @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();
|
||||
@ -128,11 +135,15 @@ export class Mouse extends PsychObject {
|
||||
* @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];
|
||||
}
|
||||
@ -164,66 +175,91 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -234,7 +270,8 @@ export class Mouse extends PsychObject {
|
||||
* @public
|
||||
* @return {number} the time elapsed since the last mouse movement
|
||||
*/
|
||||
mouseMoveTime() {
|
||||
mouseMoveTime()
|
||||
{
|
||||
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
|
||||
}
|
||||
|
||||
@ -247,9 +284,11 @@ export class Mouse extends PsychObject {
|
||||
* @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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -104,11 +159,13 @@ 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] Initialised.');
|
||||
this.logger.info('[PsychoJS] @version 2020.5');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -119,10 +176,13 @@ export class PsychoJS
|
||||
getEnvironment()
|
||||
{
|
||||
if (typeof this._config === 'undefined')
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
return this._config.environment;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Open a PsychoJS Window.
|
||||
*
|
||||
@ -148,11 +208,18 @@ export class PsychoJS
|
||||
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) {
|
||||
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
|
||||
});
|
||||
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
|
||||
} = {}) {
|
||||
} = {})
|
||||
{
|
||||
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;
|
||||
@ -128,26 +139,32 @@ export class ServerManager extends PsychObject {
|
||||
$.post(url, data, null, 'json')
|
||||
.done((data, textStatus) =>
|
||||
{
|
||||
if (!('token' in data)) {
|
||||
if (!('token' in data))
|
||||
{
|
||||
self.setStatus(ServerManager.Status.ERROR);
|
||||
reject(Object.assign(response, { error: 'unexpected answer from server: no token'}));
|
||||
reject(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)) {
|
||||
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'}));
|
||||
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 }));
|
||||
resolve(Object.assign(response, {token: data.token, status: data.status}));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) =>
|
||||
{
|
||||
@ -156,7 +173,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}));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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,33 +208,49 @@ 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;
|
||||
*/
|
||||
/* 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) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
$.ajax({
|
||||
url,
|
||||
type: 'delete',
|
||||
data,
|
||||
data: {isCompleted},
|
||||
dataType: 'json'
|
||||
})
|
||||
.done((data, textStatus) => {
|
||||
.done((data, textStatus) =>
|
||||
{
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
self._psychoJS.config.session.status = 'CLOSED';
|
||||
|
||||
// resolve({ ...response, data });
|
||||
resolve(Object.assign(response, { data }));
|
||||
resolve(Object.assign(response, {data}));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) =>
|
||||
{
|
||||
@ -222,10 +259,11 @@ 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}));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -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 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 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 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,26 +463,35 @@ 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 url = this._psychoJS.config.pavlovia.URL +
|
||||
'/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) +
|
||||
'/sessions/' + this._psychoJS.config.session.token +
|
||||
'/results';
|
||||
|
||||
// synchronous query the pavlovia server:
|
||||
if (sync)
|
||||
{
|
||||
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) =>
|
||||
{
|
||||
const data = {
|
||||
key,
|
||||
value
|
||||
};
|
||||
|
||||
// query the pavlovia server:
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
const url = self._psychoJS.config.pavlovia.URL +
|
||||
'/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) +
|
||||
'/sessions/' + self._psychoJS.config.session.token +
|
||||
'/results';
|
||||
|
||||
$.post(url, data, null, 'json')
|
||||
.done((serverData, textStatus) =>
|
||||
{
|
||||
self.setStatus(ServerManager.Status.READY);
|
||||
resolve(Object.assign(response, { serverData }));
|
||||
resolve(Object.assign(response, {serverData}));
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) =>
|
||||
{
|
||||
@ -416,11 +500,11 @@ 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}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -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,7 +39,10 @@ export class Window extends PsychObject {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
get monitorFramePeriod() { return this._monitorFramePeriod; }
|
||||
get monitorFramePeriod()
|
||||
{
|
||||
return this._monitorFramePeriod;
|
||||
}
|
||||
|
||||
constructor({
|
||||
psychoJS,
|
||||
@ -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,13 +89,17 @@ 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,17 +186,22 @@ 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,17 +228,22 @@ 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,12 +380,15 @@ 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();
|
||||
};
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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
|
||||
*/
|
||||
@ -19,8 +19,10 @@
|
||||
* @mixin
|
||||
*
|
||||
*/
|
||||
export let WindowMixin = (superclass) => class extends superclass {
|
||||
constructor(args) {
|
||||
export let WindowMixin = (superclass) => class extends superclass
|
||||
{
|
||||
constructor(args)
|
||||
{
|
||||
super(args);
|
||||
}
|
||||
|
||||
@ -34,7 +36,8 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @param {String} [units= this.win.units] - the units
|
||||
* @param {boolean} [log= false] - whether or not to log
|
||||
*/
|
||||
setUnits(units = this.win.units, log = false) {
|
||||
setUnits(units = this.win.units, log = false)
|
||||
{
|
||||
this._setAttribute('units', units, log);
|
||||
}
|
||||
|
||||
@ -48,23 +51,31 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,23 +89,31 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @param {number} length_px - the length in pixel units
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getLengthUnits(length_px) {
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,23 +127,31 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @param {number} length_px - the length in pixel units
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getHorLengthPix(length) {
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,23 +164,31 @@ export let WindowMixin = (superclass) => class extends superclass {
|
||||
* @param {number} length_px - the length in pixel units
|
||||
* @return {number} - the length in stimulus units
|
||||
*/
|
||||
_getVerLengthPix(length) {
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,14 @@
|
||||
* 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';
|
||||
|
||||
|
||||
@ -26,7 +26,8 @@ import * as util from '../util/Util';
|
||||
* @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.
|
||||
@ -35,7 +36,10 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
get experimentEnded() { return this._experimentEnded; }
|
||||
get experimentEnded()
|
||||
{
|
||||
return this._experimentEnded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for experimentEnded.
|
||||
@ -44,21 +48,32 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @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
|
||||
} = {}) {
|
||||
} = {})
|
||||
{
|
||||
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,11 +138,14 @@ 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,14 +192,21 @@ 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// this is to support legacy generated JavaScript code and does not properly handle
|
||||
@ -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,28 +255,40 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (*) get various experiment info:
|
||||
@ -249,41 +302,26 @@ 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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (*) save in the database on the remote server:
|
||||
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -320,23 +367,30 @@ export class ExperimentHandler extends PsychObject {
|
||||
* @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
|
||||
// method of constants
|
||||
|
@ -3,13 +3,13 @@
|
||||
* 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';
|
||||
|
||||
|
||||
@ -27,7 +27,8 @@ 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.
|
||||
@ -36,7 +37,10 @@ export class TrialHandler extends PsychObject {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
get experimentHandler() { return this._experimentHandler; }
|
||||
get experimentHandler()
|
||||
{
|
||||
return this._experimentHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for experimentHandler.
|
||||
@ -45,7 +49,8 @@ export class TrialHandler extends PsychObject {
|
||||
* @function
|
||||
* @public
|
||||
*/
|
||||
set experimentHandler(exp) {
|
||||
set experimentHandler(exp)
|
||||
{
|
||||
this._experimentHandler = exp;
|
||||
}
|
||||
|
||||
@ -65,7 +70,8 @@ export class TrialHandler extends PsychObject {
|
||||
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};
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -165,11 +174,13 @@ export class TrialHandler extends PsychObject {
|
||||
{
|
||||
const trialIterator = this[Symbol.iterator]();
|
||||
|
||||
while(true)
|
||||
while (true)
|
||||
{
|
||||
const result = trialIterator.next();
|
||||
if (result.done)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
callback(result.value);
|
||||
}
|
||||
@ -229,7 +240,8 @@ export class TrialHandler extends PsychObject {
|
||||
* @public
|
||||
* @return {number} the current trial index
|
||||
*/
|
||||
getTrialIndex() {
|
||||
getTrialIndex()
|
||||
{
|
||||
return this.thisIndex;
|
||||
}
|
||||
|
||||
@ -239,7 +251,8 @@ export class TrialHandler extends PsychObject {
|
||||
*
|
||||
* @param {number} index - the new trial index
|
||||
*/
|
||||
setTrialIndex(index) {
|
||||
setTrialIndex(index)
|
||||
{
|
||||
this.thisIndex = index;
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
@ -271,7 +289,8 @@ export class TrialHandler extends PsychObject {
|
||||
* @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,10 +352,13 @@ 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;
|
||||
|
@ -3,15 +3,15 @@
|
||||
* 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';
|
||||
|
||||
|
||||
/**
|
||||
@ -49,8 +49,9 @@ 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,
|
||||
@ -64,7 +65,8 @@ export class Sound extends PsychObject {
|
||||
loops = 0,
|
||||
//hamming = true,
|
||||
autoLog = true
|
||||
} = {}) {
|
||||
} = {})
|
||||
{
|
||||
super(win._psychoJS, name);
|
||||
|
||||
// the SoundPlayer, e.g. TonePlayer:
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -136,8 +139,10 @@ export class Sound extends PsychObject {
|
||||
this._setAttribute('volume', volume, log);
|
||||
|
||||
if (typeof this._player !== 'undefined')
|
||||
{
|
||||
this._player.setVolume(volume, mute);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -152,9 +157,10 @@ export class Sound extends PsychObject {
|
||||
this._setAttribute('loops', loops, log);
|
||||
|
||||
if (typeof this._player !== 'undefined')
|
||||
{
|
||||
this._player.setLoops(loops);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -169,9 +175,10 @@ export class Sound extends PsychObject {
|
||||
this._setAttribute('secs', secs, log);
|
||||
|
||||
if (typeof this._player !== 'undefined')
|
||||
{
|
||||
this._player.setDuration(secs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -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.'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,12 +2,12 @@
|
||||
* 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,7 +131,11 @@ 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.'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,12 +2,12 @@
|
||||
* 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';
|
||||
|
||||
|
||||
/**
|
||||
@ -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.
|
||||
*
|
||||
@ -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,8 +270,10 @@ export class TonePlayer extends SoundPlayer
|
||||
|
||||
// clear the repeat event if need be:
|
||||
if (this._toneId)
|
||||
{
|
||||
Tone.Transport.clear(this._toneId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const contextCurrentTime = this._audioContext.currentTime;
|
||||
|
@ -2,12 +2,12 @@
|
||||
* 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';
|
||||
|
||||
|
||||
/**
|
||||
@ -27,7 +27,8 @@ import { SoundPlayer } from './SoundPlayer';
|
||||
* @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,
|
||||
@ -36,7 +37,8 @@ export class TrackPlayer extends SoundPlayer {
|
||||
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,
|
||||
@ -102,7 +107,8 @@ export class TrackPlayer extends SoundPlayer {
|
||||
* @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);
|
||||
@ -124,10 +130,14 @@ 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');
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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
|
||||
*/
|
||||
@ -15,8 +15,10 @@
|
||||
* @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;
|
||||
}
|
||||
|
||||
@ -29,7 +31,8 @@ export class MonotonicClock {
|
||||
* @public
|
||||
* @return {number} the current time (in seconds)
|
||||
*/
|
||||
getTime() {
|
||||
getTime()
|
||||
{
|
||||
return MonotonicClock.getReferenceTime() - this._timeAtLastReset;
|
||||
}
|
||||
|
||||
@ -42,7 +45,8 @@ export class MonotonicClock {
|
||||
* @public
|
||||
* @return {number} the offset (in seconds)
|
||||
*/
|
||||
getLastResetTime() {
|
||||
getLastResetTime()
|
||||
{
|
||||
return this._timeAtLastReset;
|
||||
}
|
||||
|
||||
@ -90,6 +94,7 @@ export class MonotonicClock {
|
||||
* @type {number}
|
||||
*/
|
||||
MonotonicClock._referenceTime = performance.now() / 1000.0;
|
||||
|
||||
// MonotonicClock._referenceTime = new Date().getTime() / 1000.0;
|
||||
|
||||
|
||||
@ -100,8 +105,10 @@ MonotonicClock._referenceTime = performance.now() / 1000.0;
|
||||
* @class
|
||||
* @extends MonotonicClock
|
||||
*/
|
||||
export class Clock extends MonotonicClock {
|
||||
constructor() {
|
||||
export class Clock extends MonotonicClock
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
@ -114,7 +121,8 @@ export class Clock extends MonotonicClock {
|
||||
* @public
|
||||
* @param {number} [newTime= 0] the new time on the clock.
|
||||
*/
|
||||
reset(newTime = 0) {
|
||||
reset(newTime = 0)
|
||||
{
|
||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
||||
}
|
||||
|
||||
@ -130,7 +138,8 @@ export class Clock extends MonotonicClock {
|
||||
* @public
|
||||
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||
*/
|
||||
add(deltaTime) {
|
||||
add(deltaTime)
|
||||
{
|
||||
this._timeAtLastReset += deltaTime;
|
||||
}
|
||||
}
|
||||
@ -144,13 +153,16 @@ export class Clock extends MonotonicClock {
|
||||
* @extends Clock
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
@ -167,7 +179,8 @@ export class CountdownTimer extends Clock {
|
||||
* @public
|
||||
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||
*/
|
||||
add(deltaTime) {
|
||||
add(deltaTime)
|
||||
{
|
||||
this._timeAtLastReset += deltaTime;
|
||||
}
|
||||
|
||||
@ -181,11 +194,14 @@ export class CountdownTimer extends Clock {
|
||||
* @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;
|
||||
}
|
||||
@ -200,7 +216,8 @@ export class CountdownTimer extends Clock {
|
||||
* @public
|
||||
* @return {number} the time left on the countdown (in seconds)
|
||||
*/
|
||||
getTime() {
|
||||
getTime()
|
||||
{
|
||||
return this._timeAtLastReset - MonotonicClock.getReferenceTime();
|
||||
}
|
||||
}
|
||||
|
171
js/util/Color.js
171
js/util/Color.js
@ -2,7 +2,7 @@
|
||||
* 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
|
||||
*/
|
||||
@ -30,27 +30,38 @@
|
||||
*
|
||||
* @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});
|
||||
}
|
||||
|
||||
}
|
||||
@ -119,7 +138,10 @@ export class Color {
|
||||
* @public
|
||||
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
||||
*/
|
||||
get rgb() { return this._rgb; }
|
||||
get rgb()
|
||||
{
|
||||
return this._rgb;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -130,7 +152,10 @@ export class Color {
|
||||
* @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)];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -141,9 +166,12 @@ export class Color {
|
||||
* @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;
|
||||
}
|
||||
|
||||
@ -155,9 +183,12 @@ export class Color {
|
||||
* @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;
|
||||
}
|
||||
|
||||
@ -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)];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -240,15 +279,21 @@ 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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,15 +308,21 @@ 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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,15 +337,21 @@ 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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -309,14 +366,20 @@ 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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,7 +396,8 @@ 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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -386,7 +452,8 @@ 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];
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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];
|
||||
@ -450,8 +519,10 @@ export class Color {
|
||||
}
|
||||
|
||||
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] + ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,13 +2,13 @@
|
||||
* 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';
|
||||
|
||||
|
||||
/**
|
||||
@ -17,8 +17,10 @@ import { Color } from './Color';
|
||||
* @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);
|
||||
}
|
||||
|
||||
@ -32,7 +34,8 @@ export let ColorMixin = (superclass) => class extends superclass {
|
||||
* @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;
|
||||
@ -48,7 +51,8 @@ export let ColorMixin = (superclass) => class extends superclass {
|
||||
* @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;
|
||||
@ -64,7 +68,8 @@ export let ColorMixin = (superclass) => class extends superclass {
|
||||
* @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);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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
|
||||
*/
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
|
@ -3,13 +3,13 @@
|
||||
* 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';
|
||||
|
||||
|
||||
@ -33,7 +33,9 @@ export class PsychObject extends EventEmitter
|
||||
|
||||
// name:
|
||||
if (typeof name === 'undefined')
|
||||
{
|
||||
name = this.constructor.name;
|
||||
}
|
||||
this._addAttribute('name', name);
|
||||
}
|
||||
|
||||
@ -77,18 +79,24 @@ 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,7 +106,6 @@ export class PsychObject extends EventEmitter
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set the value of an attribute.
|
||||
*
|
||||
@ -113,32 +120,46 @@ export class PsychObject extends EventEmitter
|
||||
*/
|
||||
_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' +
|
||||
throw Object.assign(response, {
|
||||
error: 'unsupported' +
|
||||
' value: ' + JSON.stringify(attributeValue) + ' for' +
|
||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
||||
' 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)});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -313,12 +352,16 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -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
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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
|
||||
*/
|
||||
@ -61,7 +61,10 @@ export class Scheduler
|
||||
* @public
|
||||
* @returns {module:util.Scheduler#Status} the status of the scheduler
|
||||
*/
|
||||
get status() { return this._status; }
|
||||
get status()
|
||||
{
|
||||
return this._status;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -78,7 +81,8 @@ export class Scheduler
|
||||
* @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);
|
||||
}
|
||||
@ -101,13 +105,19 @@ export class Scheduler
|
||||
* @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;
|
||||
}
|
||||
@ -160,7 +174,8 @@ export class 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;
|
||||
}
|
||||
|
323
js/util/Util.js
323
js/util/Util.js
@ -2,7 +2,7 @@
|
||||
* 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,8 +24,11 @@
|
||||
* class NewClass extends mix(BaseClass).with(Mixin1, Mixin2) { ... }
|
||||
*/
|
||||
export let mix = (superclass) => new MixinBuilder(superclass);
|
||||
class MixinBuilder {
|
||||
constructor(superclass) {
|
||||
|
||||
class MixinBuilder
|
||||
{
|
||||
constructor(superclass)
|
||||
{
|
||||
this.superclass = superclass;
|
||||
}
|
||||
|
||||
@ -34,7 +37,8 @@ class MixinBuilder {
|
||||
* @param mixins
|
||||
* @returns {*}
|
||||
*/
|
||||
with(...mixins) {
|
||||
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,9 +91,12 @@ 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);
|
||||
@ -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';
|
||||
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;
|
||||
});
|
||||
}
|
||||
@ -231,7 +289,10 @@ export function IsPointInsidePolygon(point, vertices)
|
||||
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;
|
||||
if (intersect)
|
||||
{
|
||||
isInside = !isInside;
|
||||
}
|
||||
}
|
||||
|
||||
return isInside;
|
||||
@ -271,16 +332,20 @@ export function shuffle(array)
|
||||
*/
|
||||
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,15 +615,15 @@ 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)
|
||||
.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)
|
||||
.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;
|
||||
}
|
||||
|
||||
const x = parseFloat(obj);
|
||||
return (x | 0) === x;
|
||||
@ -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;
|
||||
|
@ -2,15 +2,15 @@
|
||||
* 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';
|
||||
|
||||
|
||||
@ -63,16 +63,19 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
flipVert = false,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {}) {
|
||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
||||
} = {})
|
||||
{
|
||||
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||
|
||||
this.psychoJS.logger.debug('create a new ImageStim with name: ', name);
|
||||
|
||||
this._addAttributes(ImageStim, image, mask, color, contrast, texRes, interpolate, depth, flipHoriz, flipVert);
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -83,22 +86,32 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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,8 +122,9 @@ 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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,22 +137,32 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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,8 +173,9 @@ 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});
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +188,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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;
|
||||
@ -178,7 +204,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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;
|
||||
@ -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
|
||||
@ -225,16 +260,21 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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;
|
||||
}
|
||||
|
@ -2,17 +2,17 @@
|
||||
* 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,7 +45,8 @@ 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,
|
||||
@ -66,8 +67,9 @@ export class MovieStim extends VisualStim {
|
||||
autoPlay = true,
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {}) {
|
||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
||||
} = {})
|
||||
{
|
||||
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||
|
||||
this.psychoJS.logger.debug('create a new MovieStim with name: ', name);
|
||||
|
||||
@ -78,8 +80,10 @@ export class MovieStim extends VisualStim {
|
||||
this._hasFastSeek = (typeof videoElement.fastSeek === 'function');
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -90,23 +94,34 @@ export class MovieStim extends VisualStim {
|
||||
* @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,7 +149,8 @@ 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;
|
||||
@ -142,7 +162,8 @@ export class MovieStim extends VisualStim {
|
||||
* @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;
|
||||
@ -156,7 +177,8 @@ export class MovieStim extends VisualStim {
|
||||
* @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;
|
||||
@ -171,7 +193,8 @@ export class MovieStim extends VisualStim {
|
||||
* @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;
|
||||
@ -183,10 +206,14 @@ export class MovieStim extends VisualStim {
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -195,7 +222,8 @@ export class MovieStim extends VisualStim {
|
||||
*
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
play(log = false) {
|
||||
play(log = false)
|
||||
{
|
||||
this.status = PsychoJS.Status.STARTED;
|
||||
this._movie.play();
|
||||
}
|
||||
@ -206,7 +234,8 @@ export class MovieStim extends VisualStim {
|
||||
*
|
||||
* @param {boolean} [log= false] - whether of not to log
|
||||
*/
|
||||
pause(log = false) {
|
||||
pause(log = false)
|
||||
{
|
||||
this.status = PsychoJS.Status.STOPPED;
|
||||
this._movie.pause();
|
||||
}
|
||||
@ -217,12 +246,15 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@ -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,7 +277,10 @@ export class MovieStim extends VisualStim {
|
||||
}
|
||||
|
||||
|
||||
if (this._hasFastSeek) this._movie.fastSeek(timePoint);
|
||||
if (this._hasFastSeek)
|
||||
{
|
||||
this._movie.fastSeek(timePoint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -256,10 +293,12 @@ export class MovieStim extends VisualStim {
|
||||
* @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)}`,
|
||||
@ -287,16 +326,21 @@ export class MovieStim extends VisualStim {
|
||||
* @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,9 +84,10 @@ export class Polygon extends ShapeStim
|
||||
this._updateVertices();
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -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,14 +2,14 @@
|
||||
* 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';
|
||||
|
||||
|
||||
/**
|
||||
@ -59,7 +59,23 @@ export class Rect 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 Rect with name: ', name);
|
||||
|
||||
@ -68,9 +84,10 @@ export class Rect extends ShapeStim
|
||||
this._updateVertices();
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -90,7 +107,6 @@ export class Rect extends ShapeStim
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the height attribute.
|
||||
*
|
||||
|
@ -3,15 +3,15 @@
|
||||
* 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';
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
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,7 +106,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Setter for the line width attribute.
|
||||
*
|
||||
@ -180,10 +177,14 @@ 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);
|
||||
// this._setAttribute({
|
||||
@ -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
|
||||
*/
|
||||
@ -74,7 +74,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
opacity,
|
||||
|
||||
style = [Slider.Style.RATING],
|
||||
ticks = [1,2,3,4,5],
|
||||
ticks = [1, 2, 3, 4, 5],
|
||||
labels = [],
|
||||
labelHeight,
|
||||
granularity = 0,
|
||||
@ -88,8 +88,9 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
|
||||
autoDraw,
|
||||
autoLog
|
||||
} = {}) {
|
||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
||||
} = {})
|
||||
{
|
||||
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||
|
||||
this._needMarkerUpdate = false;
|
||||
|
||||
@ -113,8 +114,10 @@ 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,8 +168,10 @@ 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,13 +181,18 @@ 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,13 +202,18 @@ 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;
|
||||
}
|
||||
@ -267,15 +295,21 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @param {boolean} [readOnly= true] - whether or not the slider is read-only
|
||||
* @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;
|
||||
}
|
||||
@ -316,11 +352,14 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @param {number} rating - the rating
|
||||
* @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,21 +432,28 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -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,20 +557,32 @@ 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)
|
||||
}
|
||||
else if (this._labelOri === 0)
|
||||
{
|
||||
labelText.position.x -= labelBounds.width + tickSize_px[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
labelText.position.x -= tickSize_px[0];
|
||||
}
|
||||
}
|
||||
|
||||
labelText.rotation = (this._ori + this._labelOri) * Math.PI / 180;
|
||||
labelText.alpha = this._opacity;
|
||||
@ -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,10 +643,12 @@ 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)
|
||||
/* 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]);
|
||||
@ -571,8 +662,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
};
|
||||
|
||||
// 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);
|
||||
@ -681,24 +783,28 @@ 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');
|
||||
@ -707,11 +813,15 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
||||
}
|
||||
|
||||
// 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,12 +850,17 @@ 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, (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,14 +873,18 @@ 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];
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
@ -2,15 +2,15 @@
|
||||
* 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';
|
||||
|
||||
|
||||
@ -68,13 +68,15 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
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()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -102,7 +104,8 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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;
|
||||
@ -118,10 +121,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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);
|
||||
}
|
||||
@ -141,10 +152,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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);
|
||||
}
|
||||
@ -164,7 +183,8 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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;
|
||||
@ -180,7 +200,8 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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;
|
||||
@ -196,7 +217,8 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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;
|
||||
@ -212,7 +234,8 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @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;
|
||||
@ -231,11 +254,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
*
|
||||
* @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
|
||||
@ -254,7 +284,9 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
_updateIfNeeded()
|
||||
{
|
||||
if (!this._needUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._needUpdate = false;
|
||||
|
||||
this._heightPix = this._getLengthPix(this._height);
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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
|
||||
*/
|
||||
@ -76,11 +76,14 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
||||
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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user