mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 02:30:53 +00:00
complete linting, saving of partial results, improved GUI, various bug fixes
This commit is contained in:
parent
8276bd790a
commit
1fc76d1461
@ -1,4 +1,4 @@
|
|||||||
Copyright 2019 Ilixa Ltd.
|
Copyright 2020 Ilixa Ltd.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
* Manager handling the keyboard and mouse/touch events.
|
* Manager handling the keyboard and mouse/touch events.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MonotonicClock, Clock } from '../util/Clock';
|
import {MonotonicClock, Clock} from '../util/Clock';
|
||||||
import { PsychoJS } from './PsychoJS';
|
import {PsychoJS} from './PsychoJS';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,14 +20,18 @@ import { PsychoJS } from './PsychoJS';
|
|||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
*/
|
*/
|
||||||
export class EventManager {
|
export class EventManager
|
||||||
|
{
|
||||||
|
|
||||||
constructor(psychoJS) {
|
constructor(psychoJS)
|
||||||
|
{
|
||||||
this._psychoJS = psychoJS;
|
this._psychoJS = psychoJS;
|
||||||
|
|
||||||
// populate the reverse pyglet map:
|
// populate the reverse pyglet map:
|
||||||
for (const keyName in EventManager._pygletMap)
|
for (const keyName in EventManager._pygletMap)
|
||||||
|
{
|
||||||
EventManager._reversePygletMap[EventManager._pygletMap[keyName]] = keyName;
|
EventManager._reversePygletMap[EventManager._pygletMap[keyName]] = keyName;
|
||||||
|
}
|
||||||
|
|
||||||
// add key listeners:
|
// add key listeners:
|
||||||
this._keyBuffer = [];
|
this._keyBuffer = [];
|
||||||
@ -67,34 +71,52 @@ export class EventManager {
|
|||||||
getKeys({
|
getKeys({
|
||||||
keyList = null,
|
keyList = null,
|
||||||
timeStamped = false
|
timeStamped = false
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
if (keyList != null)
|
if (keyList != null)
|
||||||
|
{
|
||||||
keyList = EventManager.pyglet2w3c(keyList);
|
keyList = EventManager.pyglet2w3c(keyList);
|
||||||
|
}
|
||||||
|
|
||||||
let newBuffer = [];
|
let newBuffer = [];
|
||||||
let keys = [];
|
let keys = [];
|
||||||
for (let i = 0; i < this._keyBuffer.length; ++i) {
|
for (let i = 0; i < this._keyBuffer.length; ++i)
|
||||||
|
{
|
||||||
const key = this._keyBuffer[i];
|
const key = this._keyBuffer[i];
|
||||||
let keyId = null;
|
let keyId = null;
|
||||||
|
|
||||||
if (keyList != null) {
|
if (keyList != null)
|
||||||
|
{
|
||||||
let index = keyList.indexOf(key.code);
|
let index = keyList.indexOf(key.code);
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
|
{
|
||||||
index = keyList.indexOf(EventManager._keycodeMap[key.keyCode]);
|
index = keyList.indexOf(EventManager._keycodeMap[key.keyCode]);
|
||||||
|
}
|
||||||
if (index >= 0)
|
if (index >= 0)
|
||||||
|
{
|
||||||
keyId = EventManager._reversePygletMap[keyList[index]];
|
keyId = EventManager._reversePygletMap[keyList[index]];
|
||||||
}
|
}
|
||||||
else
|
|
||||||
keyId = EventManager._reversePygletMap[key.code];
|
|
||||||
|
|
||||||
if (keyId != null) {
|
|
||||||
if (timeStamped)
|
|
||||||
keys.push([keyId, key.timestamp]);
|
|
||||||
else
|
|
||||||
keys.push(keyId);
|
|
||||||
}
|
}
|
||||||
else
|
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;
|
this._keyBuffer = newBuffer;
|
||||||
@ -108,6 +130,7 @@ export class EventManager {
|
|||||||
* @property {Array.Clock} clocks - the clocks associated to the mouse buttons, reset whenever the button is pressed
|
* @property {Array.Clock} clocks - the clocks associated to the mouse buttons, reset whenever the button is pressed
|
||||||
* @property {Array.number} times - the time elapsed since the last rest of the associated clock
|
* @property {Array.number} times - the time elapsed since the last rest of the associated clock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef EventManager.MouseInfo
|
* @typedef EventManager.MouseInfo
|
||||||
* @property {Array.number} pos - the position of the mouse [x, y]
|
* @property {Array.number} pos - the position of the mouse [x, y]
|
||||||
@ -123,7 +146,8 @@ export class EventManager {
|
|||||||
* @public
|
* @public
|
||||||
* @return {EventManager.MouseInfo} the mouse info.
|
* @return {EventManager.MouseInfo} the mouse info.
|
||||||
*/
|
*/
|
||||||
getMouseInfo() {
|
getMouseInfo()
|
||||||
|
{
|
||||||
return this._mouseInfo;
|
return this._mouseInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +161,8 @@ export class EventManager {
|
|||||||
*
|
*
|
||||||
* @todo handle the attribs argument
|
* @todo handle the attribs argument
|
||||||
*/
|
*/
|
||||||
clearEvents(attribs) {
|
clearEvents(attribs)
|
||||||
|
{
|
||||||
this.clearKeys();
|
this.clearKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +174,8 @@ export class EventManager {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
clearKeys() {
|
clearKeys()
|
||||||
|
{
|
||||||
this._keyBuffer = [];
|
this._keyBuffer = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +189,8 @@ export class EventManager {
|
|||||||
*
|
*
|
||||||
* @todo not implemented
|
* @todo not implemented
|
||||||
*/
|
*/
|
||||||
startMoveClock() {
|
startMoveClock()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -176,7 +203,8 @@ export class EventManager {
|
|||||||
*
|
*
|
||||||
* @todo not implemented
|
* @todo not implemented
|
||||||
*/
|
*/
|
||||||
stopMoveClock() {
|
stopMoveClock()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -189,7 +217,8 @@ export class EventManager {
|
|||||||
*
|
*
|
||||||
* @todo not implemented
|
* @todo not implemented
|
||||||
*/
|
*/
|
||||||
resetMoveClock() {
|
resetMoveClock()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -201,10 +230,12 @@ export class EventManager {
|
|||||||
* @public
|
* @public
|
||||||
* @param {PIXI.Renderer} renderer - The Pixi renderer
|
* @param {PIXI.Renderer} renderer - The Pixi renderer
|
||||||
*/
|
*/
|
||||||
addMouseListeners(renderer) {
|
addMouseListeners(renderer)
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
renderer.view.addEventListener("pointerdown", (event) => {
|
renderer.view.addEventListener("pointerdown", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.buttons.pressed[event.button] = 1;
|
self._mouseInfo.buttons.pressed[event.button] = 1;
|
||||||
@ -216,7 +247,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("touchstart", (event) => {
|
renderer.view.addEventListener("touchstart", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.buttons.pressed[0] = 1;
|
self._mouseInfo.buttons.pressed[0] = 1;
|
||||||
@ -230,7 +262,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("pointerup", (event) => {
|
renderer.view.addEventListener("pointerup", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.buttons.pressed[event.button] = 0;
|
self._mouseInfo.buttons.pressed[event.button] = 0;
|
||||||
@ -241,7 +274,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("touchend", (event) => {
|
renderer.view.addEventListener("touchend", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.buttons.pressed[0] = 0;
|
self._mouseInfo.buttons.pressed[0] = 0;
|
||||||
@ -255,7 +289,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("pointermove", (event) => {
|
renderer.view.addEventListener("pointermove", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.moveClock.reset();
|
self._mouseInfo.moveClock.reset();
|
||||||
@ -263,7 +298,8 @@ export class EventManager {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
renderer.view.addEventListener("touchmove", (event) => {
|
renderer.view.addEventListener("touchmove", (event) =>
|
||||||
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
self._mouseInfo.moveClock.reset();
|
self._mouseInfo.moveClock.reset();
|
||||||
@ -275,7 +311,8 @@ export class EventManager {
|
|||||||
|
|
||||||
|
|
||||||
// (*) wheel
|
// (*) wheel
|
||||||
renderer.view.addEventListener("wheel", event => {
|
renderer.view.addEventListener("wheel", event =>
|
||||||
|
{
|
||||||
self._mouseInfo.wheelRel[0] += event.deltaX;
|
self._mouseInfo.wheelRel[0] += event.deltaX;
|
||||||
self._mouseInfo.wheelRel[1] += event.deltaY;
|
self._mouseInfo.wheelRel[1] += event.deltaY;
|
||||||
|
|
||||||
@ -307,7 +344,9 @@ export class EventManager {
|
|||||||
|
|
||||||
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
||||||
if (typeof code === 'undefined')
|
if (typeof code === 'undefined')
|
||||||
|
{
|
||||||
code = EventManager.keycode2w3c(event.keyCode);
|
code = EventManager.keycode2w3c(event.keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
self._keyBuffer.push({
|
self._keyBuffer.push({
|
||||||
code,
|
code,
|
||||||
@ -324,7 +363,6 @@ export class EventManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a keylist that uses pyglet key names to one that uses W3C KeyboardEvent.code values.
|
* Convert a keylist that uses pyglet key names to one that uses W3C KeyboardEvent.code values.
|
||||||
* <p>This allows key lists that work in the builder environment to work in psychoJS web experiments.</p>
|
* <p>This allows key lists that work in the builder environment to work in psychoJS web experiments.</p>
|
||||||
@ -335,14 +373,20 @@ export class EventManager {
|
|||||||
* @param {Array.string} pygletKeyList - the array of pyglet key names
|
* @param {Array.string} pygletKeyList - the array of pyglet key names
|
||||||
* @return {Array.string} the w3c keyList
|
* @return {Array.string} the w3c keyList
|
||||||
*/
|
*/
|
||||||
static pyglet2w3c(pygletKeyList) {
|
static pyglet2w3c(pygletKeyList)
|
||||||
|
{
|
||||||
let w3cKeyList = [];
|
let w3cKeyList = [];
|
||||||
for (let i = 0; i < pygletKeyList.length; i++) {
|
for (let i = 0; i < pygletKeyList.length; i++)
|
||||||
|
{
|
||||||
if (typeof EventManager._pygletMap[pygletKeyList[i]] === 'undefined')
|
if (typeof EventManager._pygletMap[pygletKeyList[i]] === 'undefined')
|
||||||
|
{
|
||||||
w3cKeyList.push(pygletKeyList[i]);
|
w3cKeyList.push(pygletKeyList[i]);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
w3cKeyList.push(EventManager._pygletMap[pygletKeyList[i]]);
|
w3cKeyList.push(EventManager._pygletMap[pygletKeyList[i]]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return w3cKeyList;
|
return w3cKeyList;
|
||||||
}
|
}
|
||||||
@ -358,12 +402,17 @@ export class EventManager {
|
|||||||
* @param {string} code - W3C Key Code
|
* @param {string} code - W3C Key Code
|
||||||
* @returns {string} corresponding pyglet key
|
* @returns {string} corresponding pyglet key
|
||||||
*/
|
*/
|
||||||
static w3c2pyglet(code) {
|
static w3c2pyglet(code)
|
||||||
|
{
|
||||||
if (code in EventManager._reversePygletMap)
|
if (code in EventManager._reversePygletMap)
|
||||||
|
{
|
||||||
return EventManager._reversePygletMap[code];
|
return EventManager._reversePygletMap[code];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -377,7 +426,8 @@ export class EventManager {
|
|||||||
* @param {number} keycode - the keycode
|
* @param {number} keycode - the keycode
|
||||||
* @returns {string} corresponding W3C UI Event code
|
* @returns {string} corresponding W3C UI Event code
|
||||||
*/
|
*/
|
||||||
static keycode2w3c(keycode) {
|
static keycode2w3c(keycode)
|
||||||
|
{
|
||||||
return EventManager._keycodeMap[keycode];
|
return EventManager._keycodeMap[keycode];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -598,8 +648,10 @@ EventManager._reversePygletMap = {};
|
|||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
*/
|
*/
|
||||||
export class BuilderKeyResponse {
|
export class BuilderKeyResponse
|
||||||
constructor(psychoJS) {
|
{
|
||||||
|
constructor(psychoJS)
|
||||||
|
{
|
||||||
this._psychoJS = psychoJS;
|
this._psychoJS = psychoJS;
|
||||||
|
|
||||||
this.status = PsychoJS.Status.NOT_STARTED;
|
this.status = PsychoJS.Status.NOT_STARTED;
|
||||||
|
166
js/core/GUI.js
166
js/core/GUI.js
@ -2,7 +2,7 @@
|
|||||||
* Graphic User Interface
|
* Graphic User Interface
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -12,7 +12,7 @@ import {PsychoJS} from './PsychoJS';
|
|||||||
import {ServerManager} from './ServerManager';
|
import {ServerManager} from './ServerManager';
|
||||||
import {Scheduler} from '../util/Scheduler';
|
import {Scheduler} from '../util/Scheduler';
|
||||||
import {Clock} from '../util/Clock';
|
import {Clock} from '../util/Clock';
|
||||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -27,14 +27,18 @@ import * as util from '../util/Util';
|
|||||||
export class GUI
|
export class GUI
|
||||||
{
|
{
|
||||||
|
|
||||||
get dialogComponent() { return this._dialogComponent; }
|
get dialogComponent()
|
||||||
|
{
|
||||||
|
return this._dialogComponent;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(psychoJS)
|
constructor(psychoJS)
|
||||||
{
|
{
|
||||||
this._psychoJS = psychoJS;
|
this._psychoJS = psychoJS;
|
||||||
|
|
||||||
// gui listens to RESOURCE events from the server manager:
|
// gui listens to RESOURCE events from the server manager:
|
||||||
psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) => {
|
psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) =>
|
||||||
|
{
|
||||||
this._onResourceEvents(signal);
|
this._onResourceEvents(signal);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -116,7 +120,7 @@ export class GUI
|
|||||||
// logo:
|
// logo:
|
||||||
if (typeof logoUrl === 'string')
|
if (typeof logoUrl === 'string')
|
||||||
{
|
{
|
||||||
htmlCode += '<img id="dialog-logo" class="logo" src="' + logoUrl + '">';
|
htmlCode += '<img id="dialog-logo" class="logo" alt="logo" src="' + logoUrl + '">';
|
||||||
}
|
}
|
||||||
|
|
||||||
// information text:
|
// information text:
|
||||||
@ -128,17 +132,19 @@ export class GUI
|
|||||||
|
|
||||||
// add a combobox or text areas for each entry in the dictionary:
|
// add a combobox or text areas for each entry in the dictionary:
|
||||||
htmlCode += '<form>';
|
htmlCode += '<form>';
|
||||||
for (const key in dictionary) {
|
for (const key in dictionary)
|
||||||
|
{
|
||||||
const value = dictionary[key];
|
const value = dictionary[key];
|
||||||
const keyId = CSS.escape(key) + '_id';
|
const keyId = CSS.escape(key) + '_id';
|
||||||
|
|
||||||
// only create an input if the key is not in the URL:
|
// only create an input if the key is not in the URL:
|
||||||
let inUrl = false;
|
let inUrl = false;
|
||||||
const cleanedDictKey = key.trim().toLowerCase();
|
const cleanedDictKey = key.trim().toLowerCase();
|
||||||
infoFromUrl.forEach( (urlValue, urlKey) =>
|
infoFromUrl.forEach((urlValue, urlKey) =>
|
||||||
{
|
{
|
||||||
const cleanedUrlKey = urlKey.trim().toLowerCase();
|
const cleanedUrlKey = urlKey.trim().toLowerCase();
|
||||||
if (cleanedUrlKey === cleanedDictKey) {
|
if (cleanedUrlKey === cleanedDictKey)
|
||||||
|
{
|
||||||
inUrl = true;
|
inUrl = true;
|
||||||
// break;
|
// break;
|
||||||
}
|
}
|
||||||
@ -150,19 +156,26 @@ export class GUI
|
|||||||
|
|
||||||
// if the field is required:
|
// if the field is required:
|
||||||
if (key.slice(-1) === '*')
|
if (key.slice(-1) === '*')
|
||||||
|
{
|
||||||
self._requiredKeys.push(key);
|
self._requiredKeys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
// if value is an array, we create a select drop-down menu:
|
// if value is an array, we create a select drop-down menu:
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value))
|
||||||
|
{
|
||||||
htmlCode += '<select name="' + key + '" id="' + keyId + '" class="text ui-widget-content' +
|
htmlCode += '<select name="' + key + '" id="' + keyId + '" class="text ui-widget-content' +
|
||||||
' ui-corner-all">';
|
' ui-corner-all">';
|
||||||
|
|
||||||
// if the field is required, we add an empty option and select it:
|
// if the field is required, we add an empty option and select it:
|
||||||
if (key.slice(-1) === '*')
|
if (key.slice(-1) === '*')
|
||||||
|
{
|
||||||
htmlCode += '<option disabled selected>...</option>';
|
htmlCode += '<option disabled selected>...</option>';
|
||||||
|
}
|
||||||
|
|
||||||
for (const option of value)
|
for (const option of value)
|
||||||
|
{
|
||||||
htmlCode += '<option>' + option + '</option>';
|
htmlCode += '<option>' + option + '</option>';
|
||||||
|
}
|
||||||
|
|
||||||
htmlCode += '</select>';
|
htmlCode += '</select>';
|
||||||
$('#' + keyId).selectmenu({classes: {}});
|
$('#' + keyId).selectmenu({classes: {}});
|
||||||
@ -170,8 +183,10 @@ export class GUI
|
|||||||
|
|
||||||
// otherwise we use a single string input:
|
// otherwise we use a single string input:
|
||||||
else /*if (typeof value === 'string')*/
|
else /*if (typeof value === 'string')*/
|
||||||
htmlCode += '<input type="text" name="' + key + '" id="' + keyId + '" value="' + value + '" class="text ui-widget-content ui-corner-all">';
|
{
|
||||||
|
htmlCode += '<input type="text" name="' + key + '" id="' + keyId;
|
||||||
|
htmlCode += '" value="' + value + '" class="text ui-widget-content ui-corner-all">';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
htmlCode += '</form>';
|
htmlCode += '</form>';
|
||||||
@ -199,12 +214,15 @@ export class GUI
|
|||||||
|
|
||||||
|
|
||||||
// setup change event handlers for all required keys:
|
// setup change event handlers for all required keys:
|
||||||
for (const key of this._requiredKeys) {
|
for (const key of this._requiredKeys)
|
||||||
|
{
|
||||||
const keyId = CSS.escape(key) + '_id';
|
const keyId = CSS.escape(key) + '_id';
|
||||||
const input = document.getElementById(keyId);
|
const input = document.getElementById(keyId);
|
||||||
if (input)
|
if (input)
|
||||||
|
{
|
||||||
input.oninput = (event) => GUI._onKeyChange(self, event);
|
input.oninput = (event) => GUI._onKeyChange(self, event);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// init and open the dialog box:
|
// init and open the dialog box:
|
||||||
self._dialogComponent.button = 'Cancel';
|
self._dialogComponent.button = 'Cancel';
|
||||||
@ -223,17 +241,21 @@ export class GUI
|
|||||||
{
|
{
|
||||||
id: "buttonOk",
|
id: "buttonOk",
|
||||||
text: "Ok",
|
text: "Ok",
|
||||||
click: function () {
|
click: function ()
|
||||||
|
{
|
||||||
|
|
||||||
// update dictionary:
|
// update dictionary:
|
||||||
for (const key in dictionary) {
|
for (const key in dictionary)
|
||||||
|
{
|
||||||
const input = document.getElementById(CSS.escape(key) + "_id");
|
const input = document.getElementById(CSS.escape(key) + "_id");
|
||||||
if (input)
|
if (input)
|
||||||
|
{
|
||||||
dictionary[key] = input.value;
|
dictionary[key] = input.value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self._dialogComponent.button = 'OK';
|
self._dialogComponent.button = 'OK';
|
||||||
$("#expDialog").dialog("close");
|
$("#expDialog").dialog('close');
|
||||||
|
|
||||||
// switch to full screen if requested:
|
// switch to full screen if requested:
|
||||||
self._psychoJS.window.adjustScreenSize();
|
self._psychoJS.window.adjustScreenSize();
|
||||||
@ -242,9 +264,10 @@ export class GUI
|
|||||||
{
|
{
|
||||||
id: "buttonCancel",
|
id: "buttonCancel",
|
||||||
text: "Cancel",
|
text: "Cancel",
|
||||||
click: function () {
|
click: function ()
|
||||||
|
{
|
||||||
self._dialogComponent.button = 'Cancel';
|
self._dialogComponent.button = 'Cancel';
|
||||||
$("#expDialog").dialog("close");
|
$("#expDialog").dialog('close');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -253,7 +276,8 @@ export class GUI
|
|||||||
open: self._onDialogOpen('#expDialog'),
|
open: self._onDialogOpen('#expDialog'),
|
||||||
|
|
||||||
// close is called by both buttons and when the user clicks on the cross:
|
// close is called by both buttons and when the user clicks on the cross:
|
||||||
close: function () {
|
close: function ()
|
||||||
|
{
|
||||||
//$.unblockUI();
|
//$.unblockUI();
|
||||||
$(this).dialog('destroy').remove();
|
$(this).dialog('destroy').remove();
|
||||||
self._dialogComponent.status = PsychoJS.Status.FINISHED;
|
self._dialogComponent.status = PsychoJS.Status.FINISHED;
|
||||||
@ -282,9 +306,13 @@ export class GUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (self._dialogComponent.status === PsychoJS.Status.FINISHED)
|
if (self._dialogComponent.status === PsychoJS.Status.FINISHED)
|
||||||
|
{
|
||||||
return Scheduler.Event.NEXT;
|
return Scheduler.Event.NEXT;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return Scheduler.Event.FLIP_REPEAT;
|
return Scheduler.Event.FLIP_REPEAT;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,7 +341,20 @@ export class GUI
|
|||||||
error,
|
error,
|
||||||
showOK = true,
|
showOK = true,
|
||||||
onOK
|
onOK
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
|
|
||||||
|
// close the previously opened dialog box, if there is one:
|
||||||
|
const expDialog = $("#expDialog");
|
||||||
|
if (expDialog.length)
|
||||||
|
{
|
||||||
|
expDialog.dialog("destroy").remove();
|
||||||
|
}
|
||||||
|
const msgDialog = $("#msgDialog");
|
||||||
|
if (msgDialog.length)
|
||||||
|
{
|
||||||
|
msgDialog.dialog("destroy").remove();
|
||||||
|
}
|
||||||
|
|
||||||
let htmlCode;
|
let htmlCode;
|
||||||
let titleColour;
|
let titleColour;
|
||||||
@ -325,13 +366,16 @@ export class GUI
|
|||||||
|
|
||||||
// deal with null error:
|
// deal with null error:
|
||||||
if (!error)
|
if (!error)
|
||||||
|
{
|
||||||
error = 'Unspecified JavaScript error';
|
error = 'Unspecified JavaScript error';
|
||||||
|
}
|
||||||
|
|
||||||
let errorCode = null;
|
let errorCode = null;
|
||||||
|
|
||||||
// go through the error stack and look for errorCode if there is one:
|
// go through the error stack and look for errorCode if there is one:
|
||||||
let stackCode = '<ul>';
|
let stackCode = '<ul>';
|
||||||
while (true) {
|
while (true)
|
||||||
|
{
|
||||||
|
|
||||||
if (typeof error === 'object' && 'errorCode' in error)
|
if (typeof error === 'object' && 'errorCode' in error)
|
||||||
{
|
{
|
||||||
@ -371,7 +415,8 @@ export class GUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we are displaying a message:
|
// we are displaying a message:
|
||||||
else if (typeof message !== 'undefined') {
|
else if (typeof message !== 'undefined')
|
||||||
|
{
|
||||||
htmlCode = '<div id="msgDialog" title="Message">' +
|
htmlCode = '<div id="msgDialog" title="Message">' +
|
||||||
'<p class="validateTips">' + message + '</p>' +
|
'<p class="validateTips">' + message + '</p>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@ -379,7 +424,8 @@ export class GUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we are displaying a warning:
|
// we are displaying a warning:
|
||||||
else if (typeof warning !== 'undefined') {
|
else if (typeof warning !== 'undefined')
|
||||||
|
{
|
||||||
htmlCode = '<div id="msgDialog" title="Warning">' +
|
htmlCode = '<div id="msgDialog" title="Warning">' +
|
||||||
'<p class="validateTips">' + warning + '</p>' +
|
'<p class="validateTips">' + warning + '</p>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@ -395,7 +441,7 @@ export class GUI
|
|||||||
this._estimateDialogScalingFactor();
|
this._estimateDialogScalingFactor();
|
||||||
const dialogSize = this._getDialogSize();
|
const dialogSize = this._getDialogSize();
|
||||||
const self = this;
|
const self = this;
|
||||||
$('#msgDialog').dialog({
|
$("#msgDialog").dialog({
|
||||||
dialogClass: 'no-close',
|
dialogClass: 'no-close',
|
||||||
|
|
||||||
width: dialogSize[0],
|
width: dialogSize[0],
|
||||||
@ -405,16 +451,19 @@ export class GUI
|
|||||||
modal: true,
|
modal: true,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
|
|
||||||
buttons: (!showOK)?[]:[{
|
buttons: (!showOK) ? [] : [{
|
||||||
id: "buttonOk",
|
id: "buttonOk",
|
||||||
text: "Ok",
|
text: "Ok",
|
||||||
click: function() {
|
click: function ()
|
||||||
|
{
|
||||||
$(this).dialog("destroy").remove();
|
$(this).dialog("destroy").remove();
|
||||||
|
|
||||||
// execute callback function:
|
// execute callback function:
|
||||||
if (typeof onOK !== 'undefined')
|
if (typeof onOK !== 'undefined')
|
||||||
|
{
|
||||||
onOK();
|
onOK();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}],
|
}],
|
||||||
|
|
||||||
// open the dialog in the middle of the screen:
|
// open the dialog in the middle of the screen:
|
||||||
@ -471,10 +520,12 @@ export class GUI
|
|||||||
* @param {String} dialogId - the dialog ID
|
* @param {String} dialogId - the dialog ID
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_dialogResize(dialogId) {
|
_dialogResize(dialogId)
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
$(window).resize( function() {
|
$(window).resize(function ()
|
||||||
|
{
|
||||||
const parent = $(dialogId).parent();
|
const parent = $(dialogId).parent();
|
||||||
const windowSize = [$(window).width(), $(window).height()];
|
const windowSize = [$(window).width(), $(window).height()];
|
||||||
|
|
||||||
@ -486,7 +537,8 @@ export class GUI
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isDifferent = self._estimateDialogScalingFactor();
|
const isDifferent = self._estimateDialogScalingFactor();
|
||||||
if (!isDifferent) {
|
if (!isDifferent)
|
||||||
|
{
|
||||||
$(dialogId).css({
|
$(dialogId).css({
|
||||||
width: dialogSize[0] - self._contentDelta[0],
|
width: dialogSize[0] - self._contentDelta[0],
|
||||||
maxHeight: dialogSize[1] - self._contentDelta[1]
|
maxHeight: dialogSize[1] - self._contentDelta[1]
|
||||||
@ -499,7 +551,7 @@ export class GUI
|
|||||||
left: Math.max(0, (windowSize[0] - parent.outerWidth()) / 2.0),
|
left: Math.max(0, (windowSize[0] - parent.outerWidth()) / 2.0),
|
||||||
top: Math.max(0, (windowSize[1] - parent.outerHeight()) / 2.0),
|
top: Math.max(0, (windowSize[1] - parent.outerHeight()) / 2.0),
|
||||||
});
|
});
|
||||||
} );
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -511,11 +563,13 @@ export class GUI
|
|||||||
* @private
|
* @private
|
||||||
* @param {Object.<string, string|Symbol>} signal the signal
|
* @param {Object.<string, string|Symbol>} signal the signal
|
||||||
*/
|
*/
|
||||||
_onResourceEvents(signal) {
|
_onResourceEvents(signal)
|
||||||
|
{
|
||||||
this._psychoJS.logger.debug('signal: ' + util.toString(signal));
|
this._psychoJS.logger.debug('signal: ' + util.toString(signal));
|
||||||
|
|
||||||
// all resources have been registered:
|
// all resources have been registered:
|
||||||
if (signal.message === ServerManager.Event.RESOURCES_REGISTERED) {
|
if (signal.message === ServerManager.Event.RESOURCES_REGISTERED)
|
||||||
|
{
|
||||||
// for each resource, we have a 'downloading resource' and a 'resource downloaded' message:
|
// for each resource, we have a 'downloading resource' and a 'resource downloaded' message:
|
||||||
this._progressBarMax = signal.count * 2;
|
this._progressBarMax = signal.count * 2;
|
||||||
$("#progressbar").progressbar("option", "max", this._progressBarMax);
|
$("#progressbar").progressbar("option", "max", this._progressBarMax);
|
||||||
@ -525,7 +579,8 @@ export class GUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// all the resources have been downloaded: show the ok button
|
// all the resources have been downloaded: show the ok button
|
||||||
else if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED) {
|
else if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED)
|
||||||
|
{
|
||||||
this._allResourcesDownloaded = true;
|
this._allResourcesDownloaded = true;
|
||||||
$("#progressMsg").text('all resources downloaded.');
|
$("#progressMsg").text('all resources downloaded.');
|
||||||
this._updateOkButtonStatus();
|
this._updateOkButtonStatus();
|
||||||
@ -535,11 +590,15 @@ export class GUI
|
|||||||
else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
||||||
{
|
{
|
||||||
if (typeof this._progressBarCurrentIncrement === 'undefined')
|
if (typeof this._progressBarCurrentIncrement === 'undefined')
|
||||||
|
{
|
||||||
this._progressBarCurrentIncrement = 0;
|
this._progressBarCurrentIncrement = 0;
|
||||||
++ this._progressBarCurrentIncrement;
|
}
|
||||||
|
++this._progressBarCurrentIncrement;
|
||||||
|
|
||||||
if (signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
if (signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
|
||||||
$("#progressMsg").text('downloaded ' + this._progressBarCurrentIncrement/2 + ' / ' + this._progressBarMax/2);
|
{
|
||||||
|
$("#progressMsg").text('downloaded ' + this._progressBarCurrentIncrement / 2 + ' / ' + this._progressBarMax / 2);
|
||||||
|
}
|
||||||
// $("#progressMsg").text(signal.resource + ': downloaded.');
|
// $("#progressMsg").text(signal.resource + ': downloaded.');
|
||||||
// else
|
// else
|
||||||
// $("#progressMsg").text(signal.resource + ': downloading...');
|
// $("#progressMsg").text(signal.resource + ': downloading...');
|
||||||
@ -549,8 +608,10 @@ export class GUI
|
|||||||
|
|
||||||
// unknown message: we just display it
|
// unknown message: we just display it
|
||||||
else
|
else
|
||||||
|
{
|
||||||
$("#progressMsg").text(signal.message);
|
$("#progressMsg").text(signal.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -562,15 +623,21 @@ export class GUI
|
|||||||
*/
|
*/
|
||||||
_updateOkButtonStatus()
|
_updateOkButtonStatus()
|
||||||
{
|
{
|
||||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL || (this._allResourcesDownloaded && this._setRequiredKeys.size >= this._requiredKeys.length) )
|
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL || (this._allResourcesDownloaded && this._setRequiredKeys.size >= this._requiredKeys.length))
|
||||||
{
|
{
|
||||||
$("#buttonOk").button("option", "disabled", false);
|
$("#buttonOk").button("option", "disabled", false);
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
$("#buttonOk").button("option", "disabled", true);
|
$("#buttonOk").button("option", "disabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
// strangely, changing the disabled option sometimes fails to update the ui,
|
// strangely, changing the disabled option sometimes fails to update the ui,
|
||||||
// so we need to hide it and show it again:
|
// so we need to hide it and show it again:
|
||||||
$("#buttonOk").hide(0, () => { $("#buttonOk").show(); });
|
$("#buttonOk").hide(0, () =>
|
||||||
|
{
|
||||||
|
$("#buttonOk").show();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -582,21 +649,26 @@ export class GUI
|
|||||||
* @private
|
* @private
|
||||||
* @returns {boolean} whether or not the scaling factor is different from the previously estimated one
|
* @returns {boolean} whether or not the scaling factor is different from the previously estimated one
|
||||||
*/
|
*/
|
||||||
_estimateDialogScalingFactor() {
|
_estimateDialogScalingFactor()
|
||||||
|
{
|
||||||
const windowSize = [$(window).width(), $(window).height()];
|
const windowSize = [$(window).width(), $(window).height()];
|
||||||
|
|
||||||
// desktop:
|
// desktop:
|
||||||
let dialogScalingFactor = 1.0;
|
let dialogScalingFactor = 1.0;
|
||||||
|
|
||||||
// mobile or tablet:
|
// mobile or tablet:
|
||||||
if (windowSize[0] < 1080) {
|
if (windowSize[0] < 1080)
|
||||||
|
{
|
||||||
// landscape:
|
// landscape:
|
||||||
if (windowSize[0] > windowSize[1])
|
if (windowSize[0] > windowSize[1])
|
||||||
|
{
|
||||||
dialogScalingFactor = 1.5;
|
dialogScalingFactor = 1.5;
|
||||||
// portrait:
|
}// portrait:
|
||||||
else
|
else
|
||||||
|
{
|
||||||
dialogScalingFactor = 2.0;
|
dialogScalingFactor = 2.0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isDifferent = (dialogScalingFactor !== this._dialogScalingFactor);
|
const isDifferent = (dialogScalingFactor !== this._dialogScalingFactor);
|
||||||
this._dialogScalingFactor = dialogScalingFactor;
|
this._dialogScalingFactor = dialogScalingFactor;
|
||||||
@ -612,13 +684,14 @@ export class GUI
|
|||||||
* @private
|
* @private
|
||||||
* @returns {number[]} the size of the popup dialog window
|
* @returns {number[]} the size of the popup dialog window
|
||||||
*/
|
*/
|
||||||
_getDialogSize() {
|
_getDialogSize()
|
||||||
|
{
|
||||||
const windowSize = [$(window).width(), $(window).height()];
|
const windowSize = [$(window).width(), $(window).height()];
|
||||||
this._estimateDialogScalingFactor();
|
this._estimateDialogScalingFactor();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Math.min(GUI.dialogMaxSize[0], (windowSize[0]-GUI.dialogMargin[0]) / this._dialogScalingFactor),
|
Math.min(GUI.dialogMaxSize[0], (windowSize[0] - GUI.dialogMargin[0]) / this._dialogScalingFactor),
|
||||||
Math.min(GUI.dialogMaxSize[1], (windowSize[1]-GUI.dialogMargin[1]) / this._dialogScalingFactor)];
|
Math.min(GUI.dialogMaxSize[1], (windowSize[1] - GUI.dialogMargin[1]) / this._dialogScalingFactor)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -632,14 +705,19 @@ export class GUI
|
|||||||
* @param {module:core.GUI} gui - this GUI
|
* @param {module:core.GUI} gui - this GUI
|
||||||
* @param {Event} event - event
|
* @param {Event} event - event
|
||||||
*/
|
*/
|
||||||
static _onKeyChange(gui, event) {
|
static _onKeyChange(gui, event)
|
||||||
|
{
|
||||||
const element = event.target;
|
const element = event.target;
|
||||||
const value = element.value;
|
const value = element.value;
|
||||||
|
|
||||||
if (typeof value !== 'undefined' && value.length > 0)
|
if (typeof value !== 'undefined' && value.length > 0)
|
||||||
|
{
|
||||||
gui._setRequiredKeys.set(event.target, true);
|
gui._setRequiredKeys.set(event.target, true);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
gui._setRequiredKeys.delete(event.target);
|
gui._setRequiredKeys.delete(event.target);
|
||||||
|
}
|
||||||
|
|
||||||
gui._updateOkButtonStatus();
|
gui._updateOkButtonStatus();
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Manager handling the keyboard events.
|
* Manager handling the keyboard events.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -21,8 +21,10 @@ import {EventManager} from "./EventManager";
|
|||||||
* @param {number} tDown - time of key press (keydown event) relative to the global Monotonic Clock
|
* @param {number} tDown - time of key press (keydown event) relative to the global Monotonic Clock
|
||||||
* @param {string | undefined} name - pyglet key name
|
* @param {string | undefined} name - pyglet key name
|
||||||
*/
|
*/
|
||||||
export class KeyPress {
|
export class KeyPress
|
||||||
constructor(code, tDown, name) {
|
{
|
||||||
|
constructor(code, tDown, name)
|
||||||
|
{
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.tDown = tDown;
|
this.tDown = tDown;
|
||||||
this.name = (typeof name !== 'undefined') ? name : EventManager.w3c2pyglet(code);
|
this.name = (typeof name !== 'undefined') ? name : EventManager.w3c2pyglet(code);
|
||||||
@ -49,7 +51,8 @@ export class KeyPress {
|
|||||||
* @param {Clock} [options.clock= undefined] - an optional clock
|
* @param {Clock} [options.clock= undefined] - an optional clock
|
||||||
* @param {boolean} options.autoLog - whether or not to log
|
* @param {boolean} options.autoLog - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class Keyboard extends PsychObject {
|
export class Keyboard extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
@ -57,16 +60,19 @@ export class Keyboard extends PsychObject {
|
|||||||
waitForStart = false,
|
waitForStart = false,
|
||||||
clock,
|
clock,
|
||||||
autoLog = false,
|
autoLog = false,
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
|
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
if (typeof clock === 'undefined')
|
if (typeof clock === 'undefined')
|
||||||
clock = new Clock(); //this._psychoJS.monotonicClock;
|
{
|
||||||
|
clock = new Clock();
|
||||||
|
} //this._psychoJS.monotonicClock;
|
||||||
|
|
||||||
this._addAttributes(Keyboard, bufferSize, waitForStart, clock, autoLog);
|
this._addAttributes(Keyboard, bufferSize, waitForStart, clock, autoLog);
|
||||||
// start recording key events if need be:
|
// start recording key events if need be:
|
||||||
this._addAttribute('status', (waitForStart)?PsychoJS.Status.NOT_STARTED:PsychoJS.Status.STARTED);
|
this._addAttribute('status', (waitForStart) ? PsychoJS.Status.NOT_STARTED : PsychoJS.Status.STARTED);
|
||||||
|
|
||||||
// setup circular buffer:
|
// setup circular buffer:
|
||||||
this.clearEvents();
|
this.clearEvents();
|
||||||
@ -85,7 +91,8 @@ export class Keyboard extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
start() {
|
start()
|
||||||
|
{
|
||||||
this._status = PsychoJS.Status.STARTED;
|
this._status = PsychoJS.Status.STARTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +105,8 @@ export class Keyboard extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop()
|
||||||
|
{
|
||||||
this._status = PsychoJS.Status.STOPPED;
|
this._status = PsychoJS.Status.STOPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,20 +129,26 @@ export class Keyboard extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Keyboard.KeyEvent[]} the list of events still in the buffer
|
* @return {Keyboard.KeyEvent[]} the list of events still in the buffer
|
||||||
*/
|
*/
|
||||||
getEvents() {
|
getEvents()
|
||||||
|
{
|
||||||
if (this._bufferLength === 0)
|
if (this._bufferLength === 0)
|
||||||
|
{
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// iterate over the buffer, from start to end, and discard the null event:
|
// iterate over the buffer, from start to end, and discard the null event:
|
||||||
let filteredEvents = [];
|
let filteredEvents = [];
|
||||||
const bufferWrap = (this._bufferLength === this._bufferSize);
|
const bufferWrap = (this._bufferLength === this._bufferSize);
|
||||||
let i = bufferWrap ? this._bufferIndex : -1;
|
let i = bufferWrap ? this._bufferIndex : -1;
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
i = (i + 1) % this._bufferSize;
|
i = (i + 1) % this._bufferSize;
|
||||||
const keyEvent = this._circularBuffer[i];
|
const keyEvent = this._circularBuffer[i];
|
||||||
if (keyEvent)
|
if (keyEvent)
|
||||||
|
{
|
||||||
filteredEvents.push(keyEvent);
|
filteredEvents.push(keyEvent);
|
||||||
|
}
|
||||||
} while (i !== this._bufferIndex);
|
} while (i !== this._bufferIndex);
|
||||||
|
|
||||||
return filteredEvents;
|
return filteredEvents;
|
||||||
@ -160,29 +174,37 @@ export class Keyboard extends PsychObject {
|
|||||||
keyList = [],
|
keyList = [],
|
||||||
waitRelease = true,
|
waitRelease = true,
|
||||||
clear = true
|
clear = true
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
|
|
||||||
// if nothing in the buffer, return immediately:
|
// if nothing in the buffer, return immediately:
|
||||||
if (this._bufferLength === 0)
|
if (this._bufferLength === 0)
|
||||||
|
{
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
let keyPresses = [];
|
let keyPresses = [];
|
||||||
|
|
||||||
// iterate over the circular buffer, looking for keyup events:
|
// iterate over the circular buffer, looking for keyup events:
|
||||||
const bufferWrap = (this._bufferLength === this._bufferSize);
|
const bufferWrap = (this._bufferLength === this._bufferSize);
|
||||||
let i = bufferWrap ? this._bufferIndex : -1;
|
let i = bufferWrap ? this._bufferIndex : -1;
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
i = (i + 1) % this._bufferSize;
|
i = (i + 1) % this._bufferSize;
|
||||||
|
|
||||||
const keyEvent = this._circularBuffer[i];
|
const keyEvent = this._circularBuffer[i];
|
||||||
if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP) {
|
if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP)
|
||||||
|
{
|
||||||
// check that the key is in the keyList:
|
// check that the key is in the keyList:
|
||||||
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
|
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey))
|
||||||
|
{
|
||||||
// look for a corresponding, preceding keydown event:
|
// look for a corresponding, preceding keydown event:
|
||||||
const precedingKeydownIndex = keyEvent.keydownIndex;
|
const precedingKeydownIndex = keyEvent.keydownIndex;
|
||||||
if (typeof precedingKeydownIndex !== 'undefined') {
|
if (typeof precedingKeydownIndex !== 'undefined')
|
||||||
|
{
|
||||||
const precedingKeydownEvent = this._circularBuffer[precedingKeydownIndex];
|
const precedingKeydownEvent = this._circularBuffer[precedingKeydownIndex];
|
||||||
if (precedingKeydownEvent) {
|
if (precedingKeydownEvent)
|
||||||
|
{
|
||||||
// prepare KeyPress and add it to the array:
|
// prepare KeyPress and add it to the array:
|
||||||
const tDown = precedingKeydownEvent.timestamp;
|
const tDown = precedingKeydownEvent.timestamp;
|
||||||
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
||||||
@ -191,9 +213,11 @@ export class Keyboard extends PsychObject {
|
|||||||
keyPresses.push(keyPress);
|
keyPresses.push(keyPress);
|
||||||
|
|
||||||
if (clear)
|
if (clear)
|
||||||
|
{
|
||||||
this._circularBuffer[precedingKeydownIndex] = null;
|
this._circularBuffer[precedingKeydownIndex] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* old approach: the circular buffer contains independent keydown and keyup events:
|
/* old approach: the circular buffer contains independent keydown and keyup events:
|
||||||
let j = i - 1;
|
let j = i - 1;
|
||||||
@ -220,7 +244,9 @@ export class Keyboard extends PsychObject {
|
|||||||
} while ((bufferWrap && j !== i) || (j > -1));*/
|
} while ((bufferWrap && j !== i) || (j > -1));*/
|
||||||
|
|
||||||
if (clear)
|
if (clear)
|
||||||
|
{
|
||||||
this._circularBuffer[i] = null;
|
this._circularBuffer[i] = null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,18 +255,23 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
|
|
||||||
// if waitRelease = false, we iterate again over the map of unmatched keydown events:
|
// if waitRelease = false, we iterate again over the map of unmatched keydown events:
|
||||||
if (!waitRelease) {
|
if (!waitRelease)
|
||||||
for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values()) {
|
{
|
||||||
|
for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values())
|
||||||
|
{
|
||||||
const keyEvent = this._circularBuffer[unmatchedKeyDownIndex];
|
const keyEvent = this._circularBuffer[unmatchedKeyDownIndex];
|
||||||
if (keyEvent) {
|
if (keyEvent)
|
||||||
|
{
|
||||||
// check that the key is in the keyList:
|
// check that the key is in the keyList:
|
||||||
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
|
if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey))
|
||||||
|
{
|
||||||
const tDown = keyEvent.timestamp;
|
const tDown = keyEvent.timestamp;
|
||||||
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
|
||||||
keyPress.rt = tDown - this._clock.getLastResetTime();
|
keyPress.rt = tDown - this._clock.getLastResetTime();
|
||||||
keyPresses.push(keyPress);
|
keyPresses.push(keyPress);
|
||||||
|
|
||||||
if (clear) {
|
if (clear)
|
||||||
|
{
|
||||||
this._unmatchedKeydownMap.delete(keyEvent.code);
|
this._unmatchedKeydownMap.delete(keyEvent.code);
|
||||||
this._circularBuffer[unmatchedKeyDownIndex] = null;
|
this._circularBuffer[unmatchedKeyDownIndex] = null;
|
||||||
}
|
}
|
||||||
@ -272,14 +303,15 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
// if clear = true and the keyList is empty, we clear all the events:
|
// if clear = true and the keyList is empty, we clear all the events:
|
||||||
if (clear && keyList.length === 0)
|
if (clear && keyList.length === 0)
|
||||||
|
{
|
||||||
this.clearEvents();
|
this.clearEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return keyPresses;
|
return keyPresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all events and resets the circular buffers.
|
* Clear all events and resets the circular buffers.
|
||||||
*
|
*
|
||||||
@ -299,7 +331,6 @@ export class Keyboard extends PsychObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test whether a list of KeyPress's contains one with a particular name.
|
* Test whether a list of KeyPress's contains one with a particular name.
|
||||||
*
|
*
|
||||||
@ -317,12 +348,11 @@ export class Keyboard extends PsychObject {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = keypressList.find( (keypress) => keypress.name === keyName );
|
const value = keypressList.find((keypress) => keypress.name === keyName);
|
||||||
return (typeof value !== 'undefined');
|
return (typeof value !== 'undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add key listeners to the document.
|
* Add key listeners to the document.
|
||||||
*
|
*
|
||||||
@ -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
|
// only consider non-repeat events, i.e. only the first keydown event associated with a participant
|
||||||
// holding a key down:
|
// holding a key down:
|
||||||
if (event.repeat)
|
if (event.repeat)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
||||||
|
|
||||||
if (this._status !== PsychoJS.Status.STARTED)
|
if (this._status !== PsychoJS.Status.STARTED)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DEPRECATED: we now use event.repeat
|
* DEPRECATED: we now use event.repeat
|
||||||
@ -362,7 +396,9 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
||||||
if (typeof code === 'undefined')
|
if (typeof code === 'undefined')
|
||||||
|
{
|
||||||
code = EventManager.keycode2w3c(event.keyCode);
|
code = EventManager.keycode2w3c(event.keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
let pigletKey = EventManager.w3c2pyglet(code);
|
let pigletKey = EventManager.w3c2pyglet(code);
|
||||||
|
|
||||||
@ -392,7 +428,9 @@ export class Keyboard extends PsychObject {
|
|||||||
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
|
||||||
|
|
||||||
if (this._status !== PsychoJS.Status.STARTED)
|
if (this._status !== PsychoJS.Status.STARTED)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self._previousKeydownKey = undefined;
|
self._previousKeydownKey = undefined;
|
||||||
|
|
||||||
@ -400,7 +438,9 @@ export class Keyboard extends PsychObject {
|
|||||||
|
|
||||||
// take care of legacy Microsoft Edge:
|
// take care of legacy Microsoft Edge:
|
||||||
if (typeof code === 'undefined')
|
if (typeof code === 'undefined')
|
||||||
|
{
|
||||||
code = EventManager.keycode2w3c(event.keyCode);
|
code = EventManager.keycode2w3c(event.keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
let pigletKey = EventManager.w3c2pyglet(code);
|
let pigletKey = EventManager.w3c2pyglet(code);
|
||||||
|
|
||||||
@ -418,7 +458,8 @@ export class Keyboard extends PsychObject {
|
|||||||
// note: if more keys are down than there are slots in the circular buffer, there might
|
// note: if more keys are down than there are slots in the circular buffer, there might
|
||||||
// not be a corresponding keydown event
|
// not be a corresponding keydown event
|
||||||
const correspondingKeydownIndex = self._unmatchedKeydownMap.get(event.code);
|
const correspondingKeydownIndex = self._unmatchedKeydownMap.get(event.code);
|
||||||
if (typeof correspondingKeydownIndex !== 'undefined') {
|
if (typeof correspondingKeydownIndex !== 'undefined')
|
||||||
|
{
|
||||||
self._circularBuffer[self._bufferIndex].keydownIndex = correspondingKeydownIndex;
|
self._circularBuffer[self._bufferIndex].keydownIndex = correspondingKeydownIndex;
|
||||||
self._unmatchedKeydownMap.delete(event.code);
|
self._unmatchedKeydownMap.delete(event.code);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Logger
|
* Logger
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
import {MonotonicClock} from '../util/Clock';
|
import {MonotonicClock} from '../util/Clock';
|
||||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +23,8 @@ import { ExperimentHandler } from '../data/ExperimentHandler';
|
|||||||
* @class
|
* @class
|
||||||
* @param {*} threshold - the logging threshold, e.g. log4javascript.Level.ERROR
|
* @param {*} threshold - the logging threshold, e.g. log4javascript.Level.ERROR
|
||||||
*/
|
*/
|
||||||
export class Logger {
|
export class Logger
|
||||||
|
{
|
||||||
|
|
||||||
constructor(psychoJS, threshold)
|
constructor(psychoJS, threshold)
|
||||||
{
|
{
|
||||||
@ -80,7 +81,6 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a server message at the DATA level.
|
* Log a server message at the DATA level.
|
||||||
*
|
*
|
||||||
@ -96,7 +96,6 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a server message.
|
* Log a server message.
|
||||||
*
|
*
|
||||||
@ -110,7 +109,9 @@ export class Logger {
|
|||||||
log(msg, level, time, obj)
|
log(msg, level, time, obj)
|
||||||
{
|
{
|
||||||
if (typeof time === 'undefined')
|
if (typeof time === 'undefined')
|
||||||
|
{
|
||||||
time = MonotonicClock.getReferenceTime();
|
time = MonotonicClock.getReferenceTime();
|
||||||
|
}
|
||||||
|
|
||||||
this._serverLogs.push({
|
this._serverLogs.push({
|
||||||
msg,
|
msg,
|
||||||
@ -122,7 +123,6 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush all server logs to the server.
|
* Flush all server logs to the server.
|
||||||
*
|
*
|
||||||
@ -149,7 +149,9 @@ export class Logger {
|
|||||||
'\t' + Symbol.keyFor(log.level) +
|
'\t' + Symbol.keyFor(log.level) +
|
||||||
'\t' + log.msg;
|
'\t' + log.msg;
|
||||||
if (log.obj !== 'undefined')
|
if (log.obj !== 'undefined')
|
||||||
|
{
|
||||||
formattedLog += '\t' + log.obj;
|
formattedLog += '\t' + log.obj;
|
||||||
|
}
|
||||||
formattedLog += '\n';
|
formattedLog += '\n';
|
||||||
|
|
||||||
formattedLogs += formattedLog;
|
formattedLogs += formattedLog;
|
||||||
@ -157,7 +159,8 @@ export class Logger {
|
|||||||
|
|
||||||
// send logs to the server or display them in the console:
|
// send logs to the server or display them in the console:
|
||||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
||||||
this._psychoJS.config.experiment.status === 'RUNNING')
|
this._psychoJS.config.experiment.status === 'RUNNING' &&
|
||||||
|
!this._psychoJS._serverMsg.has('__pilotToken'))
|
||||||
{
|
{
|
||||||
// if the pako compression library is present, we compress the logs:
|
// if the pako compression library is present, we compress the logs:
|
||||||
if (typeof pako !== 'undefined')
|
if (typeof pako !== 'undefined')
|
||||||
@ -189,7 +192,6 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a custom console layout.
|
* Create a custom console layout.
|
||||||
*
|
*
|
||||||
@ -219,10 +221,13 @@ export class Logger {
|
|||||||
{
|
{
|
||||||
// look for entry immediately after those of log4javascript:
|
// look for entry immediately after those of log4javascript:
|
||||||
for (let entry of stackEntries)
|
for (let entry of stackEntries)
|
||||||
if (entry.indexOf('log4javascript.min.js') <= 0) {
|
{
|
||||||
|
if (entry.indexOf('log4javascript.min.js') <= 0)
|
||||||
|
{
|
||||||
relevantEntry = entry;
|
relevantEntry = entry;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const buf = relevantEntry.split(':');
|
const buf = relevantEntry.split(':');
|
||||||
const line = buf[buf.length - 2];
|
const line = buf[buf.length - 2];
|
||||||
@ -242,16 +247,18 @@ export class Logger {
|
|||||||
let buf = relevantEntry.split(' ');
|
let buf = relevantEntry.split(' ');
|
||||||
let fileLine = buf.pop();
|
let fileLine = buf.pop();
|
||||||
const method = buf.pop();
|
const method = buf.pop();
|
||||||
buf = fileLine.split(':'); buf.pop();
|
buf = fileLine.split(':');
|
||||||
|
buf.pop();
|
||||||
const line = buf.pop();
|
const line = buf.pop();
|
||||||
const file = buf.pop().split('/').pop();
|
const file = buf.pop().split('/').pop();
|
||||||
|
|
||||||
return method + ' ' + file + ' ' + line;
|
return method + ' ' + file + ' ' + line;
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return customLayout;
|
return customLayout;
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
* Base class for all stimuli.
|
* Base class for all stimuli.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import { PsychoJS } from './PsychoJS';
|
import {PsychoJS} from './PsychoJS';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,31 +55,41 @@ export class MinimalStim extends PsychObject
|
|||||||
*/
|
*/
|
||||||
setAutoDraw(autoDraw, log = false)
|
setAutoDraw(autoDraw, log = false)
|
||||||
{
|
{
|
||||||
let response = { origin : 'MinimalStim.setAutoDraw', context: 'when setting the autoDraw attribute of stimulus: ' + this._name };
|
let response = {
|
||||||
|
origin: 'MinimalStim.setAutoDraw',
|
||||||
|
context: 'when setting the autoDraw attribute of stimulus: ' + this._name
|
||||||
|
};
|
||||||
|
|
||||||
this._setAttribute('autoDraw', autoDraw, log);
|
this._setAttribute('autoDraw', autoDraw, log);
|
||||||
|
|
||||||
const index = this.win._drawList.indexOf(this);
|
const index = this.win._drawList.indexOf(this);
|
||||||
|
|
||||||
// autoDraw = true: add the stimulus to the draw list if it's not there already
|
// autoDraw = true: add the stimulus to the draw list if it's not there already
|
||||||
if (this._autoDraw) {
|
if (this._autoDraw)
|
||||||
if (this.win) {
|
{
|
||||||
|
if (this.win)
|
||||||
|
{
|
||||||
// if the stimilus is not already in the draw list:
|
// if the stimilus is not already in the draw list:
|
||||||
if (index < 0) {
|
if (index < 0)
|
||||||
|
{
|
||||||
// update the stimulus if need be before we add its PIXI representation to the window container:
|
// update the stimulus if need be before we add its PIXI representation to the window container:
|
||||||
this._updateIfNeeded();
|
this._updateIfNeeded();
|
||||||
if (typeof this._pixi === 'undefined')
|
if (typeof this._pixi === 'undefined')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('the Pixi.js representation of this stimulus is undefined.');
|
this.psychoJS.logger.warn('the Pixi.js representation of this stimulus is undefined.');
|
||||||
// throw Object.assign(response, { error: 'the PIXI representation of the stimulus is unavailable'});
|
}// throw Object.assign(response, { error: 'the PIXI representation of the stimulus is unavailable'});
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
this.win._rootContainer.addChild(this._pixi);
|
this.win._rootContainer.addChild(this._pixi);
|
||||||
this.win._drawList.push(this);
|
this.win._drawList.push(this);
|
||||||
}
|
}
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
// the stimulus is already in the list, if it needs to be updated, we remove it
|
// the stimulus is already in the list, if it needs to be updated, we remove it
|
||||||
// from the window container, update it, then put it back:
|
// from the window container, update it, then put it back:
|
||||||
if (this._needUpdate && typeof this._pixi !== 'undefined') {
|
if (this._needUpdate && typeof this._pixi !== 'undefined')
|
||||||
|
{
|
||||||
this.win._rootContainer.removeChild(this._pixi);
|
this.win._rootContainer.removeChild(this._pixi);
|
||||||
this._updateIfNeeded();
|
this._updateIfNeeded();
|
||||||
this.win._rootContainer.addChild(this._pixi);
|
this.win._rootContainer.addChild(this._pixi);
|
||||||
@ -91,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
|
// autoDraw = false: remove the stimulus from the draw list and window container if it's already there
|
||||||
else {
|
else
|
||||||
if (this.win) {
|
{
|
||||||
|
if (this.win)
|
||||||
|
{
|
||||||
// if the stimulus is in the draw list, remove it from the list and from the window container:
|
// if the stimulus is in the draw list, remove it from the list and from the window container:
|
||||||
if (index >= 0) {
|
if (index >= 0)
|
||||||
|
{
|
||||||
this.win._drawList.splice(index, 1);
|
this.win._drawList.splice(index, 1);
|
||||||
if (typeof this._pixi !== 'undefined')
|
if (typeof this._pixi !== 'undefined')
|
||||||
|
{
|
||||||
this.win._rootContainer.removeChild(this._pixi);
|
this.win._rootContainer.removeChild(this._pixi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.status = PsychoJS.Status.STOPPED;
|
this.status = PsychoJS.Status.STOPPED;
|
||||||
}
|
}
|
||||||
@ -137,7 +152,11 @@ export class MinimalStim extends PsychObject
|
|||||||
*/
|
*/
|
||||||
contains(object, units)
|
contains(object, units)
|
||||||
{
|
{
|
||||||
throw {origin: 'MinimalStim.contains', context: `when determining whether stimulus: ${this._name} contains object: ${util.toString(object)}`, error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'MinimalStim.contains',
|
||||||
|
context: `when determining whether stimulus: ${this._name} contains object: ${util.toString(object)}`,
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -153,6 +172,10 @@ export class MinimalStim extends PsychObject
|
|||||||
*/
|
*/
|
||||||
_updateIfNeeded()
|
_updateIfNeeded()
|
||||||
{
|
{
|
||||||
throw {origin: 'MinimalStim._updateIfNeeded', context: 'when updating stimulus: ' + this._name, error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'MinimalStim._updateIfNeeded',
|
||||||
|
context: 'when updating stimulus: ' + this._name,
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
* Manager responsible for the interactions between the experiment's stimuli and the mouse.
|
* Manager responsible for the interactions between the experiment's stimuli and the mouse.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PsychoJS } from './PsychoJS';
|
import {PsychoJS} from './PsychoJS';
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -26,13 +26,15 @@ import * as util from '../util/Util';
|
|||||||
*
|
*
|
||||||
* @todo visible is not handled at the moment (mouse is always visible)
|
* @todo visible is not handled at the moment (mouse is always visible)
|
||||||
*/
|
*/
|
||||||
export class Mouse extends PsychObject {
|
export class Mouse extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
autoLog = true
|
autoLog = true
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(win._psychoJS, name);
|
super(win._psychoJS, name);
|
||||||
|
|
||||||
// note: those are in window units:
|
// note: those are in window units:
|
||||||
@ -56,7 +58,8 @@ export class Mouse extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Array.number} the position of the mouse in mouse/Window units
|
* @return {Array.number} the position of the mouse in mouse/Window units
|
||||||
*/
|
*/
|
||||||
getPos() {
|
getPos()
|
||||||
|
{
|
||||||
// get mouse position in the canvas:
|
// get mouse position in the canvas:
|
||||||
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
||||||
let pos_px = mouseInfo.pos.slice();
|
let pos_px = mouseInfo.pos.slice();
|
||||||
@ -81,10 +84,14 @@ export class Mouse extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Array.number} the relation position of the mouse in mouse/Window units.
|
* @return {Array.number} the relation position of the mouse in mouse/Window units.
|
||||||
*/
|
*/
|
||||||
getRel() {
|
getRel()
|
||||||
|
{
|
||||||
if (typeof this._lastPos === 'undefined')
|
if (typeof this._lastPos === 'undefined')
|
||||||
|
{
|
||||||
return this.getPos();
|
return this.getPos();
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// note: (this.getPos()-lastPos) would not work here since getPos changes this._lastPos
|
// note: (this.getPos()-lastPos) would not work here since getPos changes this._lastPos
|
||||||
const lastPos = this._lastPos;
|
const lastPos = this._lastPos;
|
||||||
const pos = this.getPos();
|
const pos = this.getPos();
|
||||||
@ -128,11 +135,15 @@ export class Mouse extends PsychObject {
|
|||||||
* @param {boolean} [getTime= false] whether or not to also return timestamps
|
* @param {boolean} [getTime= false] whether or not to also return timestamps
|
||||||
* @return {Array.number | Array.<Array.number>} either an array of size 3 with the status (1 for pressed, 0 for released) of each mouse button [left, center, right], or a tuple with that array and another array of size 3 with the timestamps.
|
* @return {Array.number | Array.<Array.number>} either an array of size 3 with the status (1 for pressed, 0 for released) of each mouse button [left, center, right], or a tuple with that array and another array of size 3 with the timestamps.
|
||||||
*/
|
*/
|
||||||
getPressed(getTime = false) {
|
getPressed(getTime = false)
|
||||||
|
{
|
||||||
const buttonPressed = this.psychoJS.eventManager.getMouseInfo().buttons.pressed.slice();
|
const buttonPressed = this.psychoJS.eventManager.getMouseInfo().buttons.pressed.slice();
|
||||||
if (!getTime)
|
if (!getTime)
|
||||||
|
{
|
||||||
return buttonPressed;
|
return buttonPressed;
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
const buttonTimes = this.psychoJS.eventManager.getMouseInfo().buttons.times.slice();
|
const buttonTimes = this.psychoJS.eventManager.getMouseInfo().buttons.times.slice();
|
||||||
return [buttonPressed, buttonTimes];
|
return [buttonPressed, buttonTimes];
|
||||||
}
|
}
|
||||||
@ -164,66 +175,91 @@ export class Mouse extends PsychObject {
|
|||||||
* @param {boolean|String|Array.number} [reset= false] - see above for a full description
|
* @param {boolean|String|Array.number} [reset= false] - see above for a full description
|
||||||
* @return {boolean} see above for a full description
|
* @return {boolean} see above for a full description
|
||||||
*/
|
*/
|
||||||
mouseMoved(distance, reset = false) {
|
mouseMoved(distance, reset = false)
|
||||||
|
{
|
||||||
// make sure that _lastPos is defined:
|
// make sure that _lastPos is defined:
|
||||||
if (typeof this._lastPos === 'undefined')
|
if (typeof this._lastPos === 'undefined')
|
||||||
|
{
|
||||||
this.getPos();
|
this.getPos();
|
||||||
|
}
|
||||||
this._prevPos = this._lastPos.slice();
|
this._prevPos = this._lastPos.slice();
|
||||||
this.getPos();
|
this.getPos();
|
||||||
|
|
||||||
if (typeof reset === 'boolean' && reset == false) {
|
if (typeof reset === 'boolean' && reset == false)
|
||||||
|
{
|
||||||
if (typeof distance === 'undefined')
|
if (typeof distance === 'undefined')
|
||||||
|
{
|
||||||
return (this._prevPos[0] != this._lastPos[0]) || (this._prevPos[1] != this._lastPos[1]);
|
return (this._prevPos[0] != this._lastPos[0]) || (this._prevPos[1] != this._lastPos[1]);
|
||||||
else {
|
}
|
||||||
if (typeof distance === 'number') {
|
else
|
||||||
|
{
|
||||||
|
if (typeof distance === 'number')
|
||||||
|
{
|
||||||
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
|
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
|
||||||
return (this._movedistance > distance);
|
return (this._movedistance > distance);
|
||||||
}
|
}
|
||||||
if (this._prevPos[0] + distance[0] - this._lastPos[0] > 0.0)
|
if (this._prevPos[0] + distance[0] - this._lastPos[0] > 0.0)
|
||||||
return true; // moved on X-axis
|
{
|
||||||
|
return true;
|
||||||
|
} // moved on X-axis
|
||||||
if (this._prevPos[1] + distance[1] - this._lastPos[0] > 0.0)
|
if (this._prevPos[1] + distance[1] - this._lastPos[0] > 0.0)
|
||||||
return true; // moved on Y-axis
|
{
|
||||||
|
return true;
|
||||||
|
} // moved on Y-axis
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (typeof reset === 'boolean' && reset == true) {
|
else if (typeof reset === 'boolean' && reset == true)
|
||||||
|
{
|
||||||
// reset the moveClock:
|
// reset the moveClock:
|
||||||
this.psychoJS.eventManager.getMouseInfo().moveClock.reset();
|
this.psychoJS.eventManager.getMouseInfo().moveClock.reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (reset === 'here') {
|
else if (reset === 'here')
|
||||||
|
{
|
||||||
// set to wherever we are
|
// set to wherever we are
|
||||||
this._prevPos = this._lastPos.clone();
|
this._prevPos = this._lastPos.clone();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (reset instanceof Array) {
|
else if (reset instanceof Array)
|
||||||
|
{
|
||||||
// an (x,y) array
|
// an (x,y) array
|
||||||
// reset to (x,y) to check movement from there
|
// reset to (x,y) to check movement from there
|
||||||
this._prevPos = reset.slice();
|
this._prevPos = reset.slice();
|
||||||
if (!distance)
|
if (!distance)
|
||||||
return false; // just resetting prevPos, not checking distance
|
{
|
||||||
else {
|
return false;
|
||||||
|
}// just resetting prevPos, not checking distance
|
||||||
|
else
|
||||||
|
{
|
||||||
// checking distance of current pos to newly reset prevposition
|
// checking distance of current pos to newly reset prevposition
|
||||||
if (typeof distance === 'number') {
|
if (typeof distance === 'number')
|
||||||
|
{
|
||||||
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
|
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
|
||||||
return (this._movedistance > distance);
|
return (this._movedistance > distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.abs(this._lastPos[0] - this._prevPos[0]) > distance[0])
|
if (Math.abs(this._lastPos[0] - this._prevPos[0]) > distance[0])
|
||||||
return true; // moved on X-axis
|
{
|
||||||
|
return true;
|
||||||
|
} // moved on X-axis
|
||||||
if (Math.abs(this._lastPos[1] - this._prevPos[1]) > distance[1])
|
if (Math.abs(this._lastPos[1] - this._prevPos[1]) > distance[1])
|
||||||
return true; // moved on Y-axis
|
{
|
||||||
|
return true;
|
||||||
|
} // moved on Y-axis
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -234,7 +270,8 @@ export class Mouse extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {number} the time elapsed since the last mouse movement
|
* @return {number} the time elapsed since the last mouse movement
|
||||||
*/
|
*/
|
||||||
mouseMoveTime() {
|
mouseMoveTime()
|
||||||
|
{
|
||||||
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
|
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,9 +284,11 @@ export class Mouse extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {Array.number} [buttons= [0,1,2]] the buttons to reset (0: left, 1: center, 2: right)
|
* @param {Array.number} [buttons= [0,1,2]] the buttons to reset (0: left, 1: center, 2: right)
|
||||||
*/
|
*/
|
||||||
clickReset(buttons = [0, 1, 2]) {
|
clickReset(buttons = [0, 1, 2])
|
||||||
|
{
|
||||||
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
|
||||||
for (const b of buttons) {
|
for (const b of buttons)
|
||||||
|
{
|
||||||
mouseInfo.buttons.clocks[b].reset();
|
mouseInfo.buttons.clocks[b].reset();
|
||||||
mouseInfo.buttons.times[b] = 0.0;
|
mouseInfo.buttons.times[b] = 0.0;
|
||||||
}
|
}
|
||||||
|
@ -3,24 +3,23 @@
|
|||||||
* Main component of the PsychoJS library.
|
* Main component of the PsychoJS library.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { Scheduler } from '../util/Scheduler';
|
import {Scheduler} from '../util/Scheduler';
|
||||||
import { ServerManager } from './ServerManager';
|
import {ServerManager} from './ServerManager';
|
||||||
import { ExperimentHandler } from '../data/ExperimentHandler';
|
import {ExperimentHandler} from '../data/ExperimentHandler';
|
||||||
import { EventManager } from './EventManager';
|
import {EventManager} from './EventManager';
|
||||||
import { Window } from './Window';
|
import {Window} from './Window';
|
||||||
import { GUI } from './GUI';
|
import {GUI} from './GUI';
|
||||||
import { MonotonicClock } from '../util/Clock';
|
import {MonotonicClock} from '../util/Clock';
|
||||||
import { Logger } from './Logger';
|
import {Logger} from './Logger';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>PsychoJS manages the lifecycle of an experiment. It initialises the PsychoJS library and its various components (e.g. the {@link ServerManager}, the {@link EventManager}), and is used by the experiment to schedule the various tasks.</p>
|
* <p>PsychoJS manages the lifecycle of an experiment. It initialises the PsychoJS library and its various components (e.g. the {@link ServerManager}, the {@link EventManager}), and is used by the experiment to schedule the various tasks.</p>
|
||||||
*
|
*
|
||||||
@ -34,25 +33,81 @@ export class PsychoJS
|
|||||||
/**
|
/**
|
||||||
* Properties
|
* Properties
|
||||||
*/
|
*/
|
||||||
get status() { return this._status; }
|
get status()
|
||||||
set status(status) {
|
{
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
set status(status)
|
||||||
|
{
|
||||||
this._status = status;
|
this._status = status;
|
||||||
}
|
}
|
||||||
get config() { return this._config; }
|
|
||||||
get window() { return this._window; }
|
|
||||||
get serverManager() { return this._serverManager; }
|
|
||||||
get experiment() { return this._experiment; }
|
|
||||||
get scheduler() { return this._scheduler; }
|
|
||||||
get monotonicClock() { return this._monotonicClock; }
|
|
||||||
get logger() { return this._logger.consoleLogger; }
|
|
||||||
get experimentLogger() { return this._logger; }
|
|
||||||
get eventManager() { return this._eventManager; }
|
|
||||||
get gui() { return this._gui; }
|
|
||||||
get IP() { return this._IP; }
|
|
||||||
// this._serverMsg is a bi-directional message board for communications with the pavlovia.org server:
|
|
||||||
get serverMsg() { return this._serverMsg; }
|
|
||||||
get browser() { return this._browser; }
|
|
||||||
|
|
||||||
|
get config()
|
||||||
|
{
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
get window()
|
||||||
|
{
|
||||||
|
return this._window;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverManager()
|
||||||
|
{
|
||||||
|
return this._serverManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
get experiment()
|
||||||
|
{
|
||||||
|
return this._experiment;
|
||||||
|
}
|
||||||
|
|
||||||
|
get scheduler()
|
||||||
|
{
|
||||||
|
return this._scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
get monotonicClock()
|
||||||
|
{
|
||||||
|
return this._monotonicClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
get logger()
|
||||||
|
{
|
||||||
|
return this._logger.consoleLogger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get experimentLogger()
|
||||||
|
{
|
||||||
|
return this._logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get eventManager()
|
||||||
|
{
|
||||||
|
return this._eventManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
get gui()
|
||||||
|
{
|
||||||
|
return this._gui;
|
||||||
|
}
|
||||||
|
|
||||||
|
get IP()
|
||||||
|
{
|
||||||
|
return this._IP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this._serverMsg is a bi-directional message board for communications with the pavlovia.org server:
|
||||||
|
get serverMsg()
|
||||||
|
{
|
||||||
|
return this._serverMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
get browser()
|
||||||
|
{
|
||||||
|
return this._browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,11 +159,13 @@ export class PsychoJS
|
|||||||
// make the PsychoJS.Status accessible from the top level of the generated experiment script
|
// make the PsychoJS.Status accessible from the top level of the generated experiment script
|
||||||
// in order to accommodate PsychoPy's Code Components
|
// in order to accommodate PsychoPy's Code Components
|
||||||
if (topLevelStatus)
|
if (topLevelStatus)
|
||||||
|
{
|
||||||
this._makeStatusTopLevel();
|
this._makeStatusTopLevel();
|
||||||
|
|
||||||
this.logger.info('[PsychoJS] Initialised.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info('[PsychoJS] Initialised.');
|
||||||
|
this.logger.info('[PsychoJS] @version 2020.5');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,10 +176,13 @@ export class PsychoJS
|
|||||||
getEnvironment()
|
getEnvironment()
|
||||||
{
|
{
|
||||||
if (typeof this._config === 'undefined')
|
if (typeof this._config === 'undefined')
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
return this._config.environment;
|
return this._config.environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a PsychoJS Window.
|
* Open a PsychoJS Window.
|
||||||
*
|
*
|
||||||
@ -148,11 +208,18 @@ export class PsychoJS
|
|||||||
units,
|
units,
|
||||||
waitBlanking,
|
waitBlanking,
|
||||||
autoLog
|
autoLog
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
this.logger.info('[PsychoJS] Open Window.');
|
this.logger.info('[PsychoJS] Open Window.');
|
||||||
|
|
||||||
if (typeof this._window !== 'undefined')
|
if (typeof this._window !== 'undefined')
|
||||||
throw { origin : 'PsychoJS.openWindow', context : 'when opening a Window', error : 'A Window has already been opened.' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'PsychoJS.openWindow',
|
||||||
|
context: 'when opening a Window',
|
||||||
|
error: 'A Window has already been opened.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this._window = new Window({
|
this._window = new Window({
|
||||||
psychoJS: this,
|
psychoJS: this,
|
||||||
@ -172,7 +239,8 @@ export class PsychoJS
|
|||||||
* @param {string} completionUrl - the completion URL
|
* @param {string} completionUrl - the completion URL
|
||||||
* @param {string} cancellationUrl - the cancellation URL
|
* @param {string} cancellationUrl - the cancellation URL
|
||||||
*/
|
*/
|
||||||
setRedirectUrls(completionUrl, cancellationUrl) {
|
setRedirectUrls(completionUrl, cancellationUrl)
|
||||||
|
{
|
||||||
this._completionUrl = completionUrl;
|
this._completionUrl = completionUrl;
|
||||||
this._cancellationUrl = cancellationUrl;
|
this._cancellationUrl = cancellationUrl;
|
||||||
}
|
}
|
||||||
@ -185,7 +253,8 @@ export class PsychoJS
|
|||||||
* @param args - arguments for that task
|
* @param args - arguments for that task
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
schedule(task, args) {
|
schedule(task, args)
|
||||||
|
{
|
||||||
this.logger.debug('schedule task: ', task.toString().substring(0, 50), '...');
|
this.logger.debug('schedule task: ', task.toString().substring(0, 50), '...');
|
||||||
|
|
||||||
this._scheduler.add(task, args);
|
this._scheduler.add(task, args);
|
||||||
@ -204,7 +273,8 @@ export class PsychoJS
|
|||||||
* @param {Scheduler} elseScheduler scheduler to run if the condition is false
|
* @param {Scheduler} elseScheduler scheduler to run if the condition is false
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
scheduleCondition(condition, thenScheduler, elseScheduler) {
|
scheduleCondition(condition, thenScheduler, elseScheduler)
|
||||||
|
{
|
||||||
this.logger.debug('schedule condition: ', condition.toString().substring(0, 50), '...');
|
this.logger.debug('schedule condition: ', condition.toString().substring(0, 50), '...');
|
||||||
|
|
||||||
this._scheduler.addConditional(condition, thenScheduler, elseScheduler);
|
this._scheduler.addConditional(condition, thenScheduler, elseScheduler);
|
||||||
@ -224,27 +294,31 @@ export class PsychoJS
|
|||||||
*
|
*
|
||||||
* @todo: close session on window or tab close
|
* @todo: close session on window or tab close
|
||||||
*/
|
*/
|
||||||
async start({ configURL = 'config.json', expName = 'UNKNOWN', expInfo, resources = [] } = {})
|
async start({configURL = 'config.json', expName = 'UNKNOWN', expInfo, resources = []} = {})
|
||||||
{
|
{
|
||||||
this.logger.debug();
|
this.logger.debug();
|
||||||
|
|
||||||
const response = { origin: 'PsychoJS.start', context: 'when starting the experiment' };
|
const response = {origin: 'PsychoJS.start', context: 'when starting the experiment'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// configure the experiment:
|
// configure the experiment:
|
||||||
await this._configure(configURL, expName);
|
await this._configure(configURL, expName);
|
||||||
|
|
||||||
// get the participant IP:
|
// get the participant IP:
|
||||||
if (this._collectIP)
|
if (this._collectIP)
|
||||||
|
{
|
||||||
this._getParticipantIPInfo();
|
this._getParticipantIPInfo();
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this._IP = {
|
this._IP = {
|
||||||
IP: 'X',
|
IP: 'X',
|
||||||
hostname : 'X',
|
hostname: 'X',
|
||||||
city : 'X',
|
city: 'X',
|
||||||
region : 'X',
|
region: 'X',
|
||||||
country : 'X',
|
country: 'X',
|
||||||
location : 'X'
|
location: 'X'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,21 +338,42 @@ export class PsychoJS
|
|||||||
// open a session:
|
// open a session:
|
||||||
await this._serverManager.openSession();
|
await this._serverManager.openSession();
|
||||||
|
|
||||||
// attempt to close the session on beforeunload/unload (we use a synchronous request since
|
// warn the user when they attempt to close the tab or browser:
|
||||||
// the Beacon API only allows POST and we need DELETE ) and release the WebGL context:
|
this.beforeunloadCallback = (event) =>
|
||||||
const self = this;
|
{
|
||||||
window.onbeforeunload = () => {
|
// preventDefault should ensure that the user gets prompted:
|
||||||
self._serverManager.closeSession(false, true);
|
event.preventDefault();
|
||||||
|
|
||||||
if (typeof self._window !== 'undefined')
|
// Chrome requires returnValue to be set:
|
||||||
self._window.close();
|
event.returnValue = '';
|
||||||
};
|
};
|
||||||
window.addEventListener('unload', function(event) {
|
window.addEventListener('beforeunload', this.beforeunloadCallback);
|
||||||
|
|
||||||
|
|
||||||
|
// 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);
|
self._serverManager.closeSession(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof self._window !== 'undefined')
|
if (typeof self._window !== 'undefined')
|
||||||
|
{
|
||||||
self._window.close();
|
self._window.close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -289,9 +384,10 @@ export class PsychoJS
|
|||||||
this.logger.info('[PsychoJS] Start Experiment.');
|
this.logger.info('[PsychoJS] Start Experiment.');
|
||||||
this._scheduler.start();
|
this._scheduler.start();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
// this._gui.dialog({ error: { ...response, error } });
|
// this._gui.dialog({ error: { ...response, error } });
|
||||||
this._gui.dialog({ error: Object.assign(response, { error }) });
|
this._gui.dialog({error: Object.assign(response, {error})});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,13 +406,16 @@ export class PsychoJS
|
|||||||
* @async
|
* @async
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
async downloadResources(resources = []) {
|
async downloadResources(resources = [])
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
await this.serverManager.downloadResources(resources);
|
await this.serverManager.downloadResources(resources);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
// this._gui.dialog({ error: { ...response, error } });
|
// this._gui.dialog({ error: { ...response, error } });
|
||||||
this._gui.dialog({ error: Object.assign(response, { error }) });
|
this._gui.dialog({error: Object.assign(response, {error})});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,13 +427,17 @@ export class PsychoJS
|
|||||||
* @param {Object.<string, *>} obj the object whose attributes we will mirror
|
* @param {Object.<string, *>} obj the object whose attributes we will mirror
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
importAttributes(obj) {
|
importAttributes(obj)
|
||||||
|
{
|
||||||
this.logger.debug('import attributes from: ', util.toString(obj));
|
this.logger.debug('import attributes from: ', util.toString(obj));
|
||||||
|
|
||||||
if (typeof obj === 'undefined')
|
if (typeof obj === 'undefined')
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const attribute in obj) {
|
for (const attribute in obj)
|
||||||
|
{
|
||||||
// this[attribute] = obj[attribute];
|
// this[attribute] = obj[attribute];
|
||||||
window[attribute] = obj[attribute];
|
window[attribute] = obj[attribute];
|
||||||
}
|
}
|
||||||
@ -354,26 +457,38 @@ export class PsychoJS
|
|||||||
* @async
|
* @async
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
async quit({ message, isCompleted = false } = {}) {
|
async quit({message, isCompleted = false} = {})
|
||||||
|
{
|
||||||
this.logger.info('[PsychoJS] Quit.');
|
this.logger.info('[PsychoJS] Quit.');
|
||||||
|
|
||||||
this._experiment.experimentEnded = true;
|
this._experiment.experimentEnded = true;
|
||||||
this._status = PsychoJS.Status.FINISHED;
|
this._status = PsychoJS.Status.FINISHED;
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// stop the main scheduler:
|
// stop the main scheduler:
|
||||||
this._scheduler.stop();
|
this._scheduler.stop();
|
||||||
|
|
||||||
|
// remove the beforeunload listener:
|
||||||
|
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
|
||||||
|
{
|
||||||
|
window.removeEventListener('beforeunload', this.beforeunloadCallback);
|
||||||
|
}
|
||||||
|
|
||||||
// save the results and the logs of the experiment:
|
// save the results and the logs of the experiment:
|
||||||
this.gui.dialog({
|
this.gui.dialog({
|
||||||
warning: 'Closing the session. Please wait a few moments.',
|
warning: 'Closing the session. Please wait a few moments.',
|
||||||
showOK: false
|
showOK: false
|
||||||
});
|
});
|
||||||
|
if (isCompleted || this._config.experiment.saveIncompleteResults)
|
||||||
|
{
|
||||||
await this._experiment.save();
|
await this._experiment.save();
|
||||||
await this._logger.flush();
|
await this._logger.flush();
|
||||||
|
}
|
||||||
|
|
||||||
// close the session:
|
// close the session:
|
||||||
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER) {
|
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
|
||||||
|
{
|
||||||
await this._serverManager.closeSession(isCompleted);
|
await this._serverManager.closeSession(isCompleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,29 +498,37 @@ export class PsychoJS
|
|||||||
const self = this;
|
const self = this;
|
||||||
this._gui.dialog({
|
this._gui.dialog({
|
||||||
message: text,
|
message: text,
|
||||||
onOK: () => {
|
onOK: () =>
|
||||||
|
{
|
||||||
// close the window:
|
// close the window:
|
||||||
self._window.close();
|
self._window.close();
|
||||||
|
|
||||||
// remove everything from the browser window:
|
// remove everything from the browser window:
|
||||||
while (document.body.hasChildNodes())
|
while (document.body.hasChildNodes())
|
||||||
|
{
|
||||||
document.body.removeChild(document.body.lastChild);
|
document.body.removeChild(document.body.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
// return from fullscreen if we were there:
|
// return from fullscreen if we were there:
|
||||||
this._window.closeFullScreen();
|
this._window.closeFullScreen();
|
||||||
|
|
||||||
// redirect if redirection URLs have been provided:
|
// redirect if redirection URLs have been provided:
|
||||||
if (isCompleted && typeof self._completionUrl !== 'undefined')
|
if (isCompleted && typeof self._completionUrl !== 'undefined')
|
||||||
|
{
|
||||||
window.location = self._completionUrl;
|
window.location = self._completionUrl;
|
||||||
|
}
|
||||||
else if (!isCompleted && typeof self._cancellationUrl !== 'undefined')
|
else if (!isCompleted && typeof self._cancellationUrl !== 'undefined')
|
||||||
|
{
|
||||||
window.location = self._cancellationUrl;
|
window.location = self._cancellationUrl;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this._gui.dialog({ error });
|
this._gui.dialog({error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,21 +541,25 @@ export class PsychoJS
|
|||||||
* @param {string} configURL - the URL of the configuration file
|
* @param {string} configURL - the URL of the configuration file
|
||||||
* @param {string} name - the name of the experiment
|
* @param {string} name - the name of the experiment
|
||||||
*/
|
*/
|
||||||
async _configure(configURL, name) {
|
async _configure(configURL, name)
|
||||||
const response = { origin: 'PsychoJS.configure', context: 'when configuring PsychoJS for the experiment' };
|
{
|
||||||
|
const response = {origin: 'PsychoJS.configure', context: 'when configuring PsychoJS for the experiment'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.CONFIGURING;
|
this.status = PsychoJS.Status.CONFIGURING;
|
||||||
|
|
||||||
// if the experiment is running from the pavlovia.org server, we read the configuration file:
|
// if the experiment is running from the pavlovia.org server, we read the configuration file:
|
||||||
const experimentUrl = window.location.href;
|
const experimentUrl = window.location.href;
|
||||||
if (experimentUrl.indexOf('https://run.pavlovia.org/') === 0 || experimentUrl.indexOf('https://pavlovia.org/run/') === 0) {
|
if (experimentUrl.indexOf('https://run.pavlovia.org/') === 0 || experimentUrl.indexOf('https://pavlovia.org/run/') === 0)
|
||||||
|
{
|
||||||
const serverResponse = await this._serverManager.getConfiguration(configURL);
|
const serverResponse = await this._serverManager.getConfiguration(configURL);
|
||||||
this._config = serverResponse.config;
|
this._config = serverResponse.config;
|
||||||
|
|
||||||
// legacy experiments had a psychoJsManager block instead of a pavlovia block, and the URL
|
// legacy experiments had a psychoJsManager block instead of a pavlovia block, and the URL
|
||||||
// pointed to https://pavlovia.org/server
|
// pointed to https://pavlovia.org/server
|
||||||
if ('psychoJsManager' in this._config) {
|
if ('psychoJsManager' in this._config)
|
||||||
|
{
|
||||||
delete this._config.psychoJsManager;
|
delete this._config.psychoJsManager;
|
||||||
this._config.pavlovia = {
|
this._config.pavlovia = {
|
||||||
URL: 'https://pavlovia.org'
|
URL: 'https://pavlovia.org'
|
||||||
@ -441,41 +568,56 @@ export class PsychoJS
|
|||||||
|
|
||||||
// tests for the presence of essential blocks in the configuration:
|
// tests for the presence of essential blocks in the configuration:
|
||||||
if (!('experiment' in this._config))
|
if (!('experiment' in this._config))
|
||||||
|
{
|
||||||
throw 'missing experiment block in configuration';
|
throw 'missing experiment block in configuration';
|
||||||
|
}
|
||||||
if (!('name' in this._config.experiment))
|
if (!('name' in this._config.experiment))
|
||||||
|
{
|
||||||
throw 'missing name in experiment block in configuration';
|
throw 'missing name in experiment block in configuration';
|
||||||
|
}
|
||||||
if (!('fullpath' in this._config.experiment))
|
if (!('fullpath' in this._config.experiment))
|
||||||
|
{
|
||||||
throw 'missing fullpath in experiment block in configuration';
|
throw 'missing fullpath in experiment block in configuration';
|
||||||
|
}
|
||||||
if (!('pavlovia' in this._config))
|
if (!('pavlovia' in this._config))
|
||||||
|
{
|
||||||
throw 'missing pavlovia block in configuration';
|
throw 'missing pavlovia block in configuration';
|
||||||
|
}
|
||||||
if (!('URL' in this._config.pavlovia))
|
if (!('URL' in this._config.pavlovia))
|
||||||
|
{
|
||||||
throw 'missing URL in pavlovia block in configuration';
|
throw 'missing URL in pavlovia block in configuration';
|
||||||
|
}
|
||||||
|
|
||||||
this._config.environment = ExperimentHandler.Environment.SERVER;
|
this._config.environment = ExperimentHandler.Environment.SERVER;
|
||||||
|
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
// otherwise we create an ad-hoc configuration:
|
// otherwise we create an ad-hoc configuration:
|
||||||
{
|
{
|
||||||
this._config = {
|
this._config = {
|
||||||
environment: ExperimentHandler.Environment.LOCAL,
|
environment: ExperimentHandler.Environment.LOCAL,
|
||||||
experiment: { name, saveFormat: ExperimentHandler.SaveFormat.CSV }
|
experiment: {name, saveFormat: ExperimentHandler.SaveFormat.CSV}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the server parameters (those starting with a double underscore):
|
// get the server parameters (those starting with a double underscore):
|
||||||
this._serverMsg = new Map();
|
this._serverMsg = new Map();
|
||||||
util.getUrlParameters().forEach((value, key) => {
|
util.getUrlParameters().forEach((value, key) =>
|
||||||
|
{
|
||||||
if (key.indexOf('__') === 0)
|
if (key.indexOf('__') === 0)
|
||||||
|
{
|
||||||
this._serverMsg.set(key, value);
|
this._serverMsg.set(key, value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.status = PsychoJS.Status.CONFIGURED;
|
this.status = PsychoJS.Status.CONFIGURED;
|
||||||
this.logger.debug('configuration:', util.toString(this._config));
|
this.logger.debug('configuration:', util.toString(this._config));
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
// throw { ...response, error };
|
// throw { ...response, error };
|
||||||
throw Object.assign(response, { error });
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,13 +628,18 @@ export class PsychoJS
|
|||||||
* <p>Note: we use [http://www.geoplugin.net/json.gp]{@link http://www.geoplugin.net/json.gp}.</p>
|
* <p>Note: we use [http://www.geoplugin.net/json.gp]{@link http://www.geoplugin.net/json.gp}.</p>
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
async _getParticipantIPInfo() {
|
async _getParticipantIPInfo()
|
||||||
const response = { origin: 'PsychoJS._getParticipantIPInfo', context: 'when getting the IP information of the participant' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'PsychoJS._getParticipantIPInfo',
|
||||||
|
context: 'when getting the IP information of the participant'
|
||||||
|
};
|
||||||
|
|
||||||
this.logger.debug('getting the IP information of the participant');
|
this.logger.debug('getting the IP information of the participant');
|
||||||
|
|
||||||
this._IP = {};
|
this._IP = {};
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
const geoResponse = await $.get('http://www.geoplugin.net/json.gp');
|
const geoResponse = await $.get('http://www.geoplugin.net/json.gp');
|
||||||
const geoData = JSON.parse(geoResponse);
|
const geoData = JSON.parse(geoResponse);
|
||||||
this._IP = {
|
this._IP = {
|
||||||
@ -503,9 +650,10 @@ export class PsychoJS
|
|||||||
};
|
};
|
||||||
this.logger.debug('IP information of the participant: ' + util.toString(this._IP));
|
this.logger.debug('IP information of the participant: ' + util.toString(this._IP));
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
// throw { ...response, error };
|
// throw { ...response, error };
|
||||||
throw Object.assign(response, { error });
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,13 +663,15 @@ export class PsychoJS
|
|||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_captureErrors() {
|
_captureErrors()
|
||||||
|
{
|
||||||
this.logger.debug('capturing all errors using window.onerror');
|
this.logger.debug('capturing all errors using window.onerror');
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
window.onerror = function (message, source, lineno, colno, error) {
|
window.onerror = function (message, source, lineno, colno, error)
|
||||||
|
{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
self._gui.dialog({ "error": error });
|
self._gui.dialog({"error": error});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -539,8 +689,10 @@ export class PsychoJS
|
|||||||
* Make the various Status top level, in order to accommodate PsychoPy's Code Components.
|
* Make the various Status top level, in order to accommodate PsychoPy's Code Components.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_makeStatusTopLevel() {
|
_makeStatusTopLevel()
|
||||||
for (const status in PsychoJS.Status) {
|
{
|
||||||
|
for (const status in PsychoJS.Status)
|
||||||
|
{
|
||||||
window[status] = PsychoJS.Status[status];
|
window[status] = PsychoJS.Status[status];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,19 @@
|
|||||||
* Manager responsible for the communication between the experiment running in the participant's browser and the remote PsychoJS manager running on the remote https://pavlovia.org server.
|
* Manager responsible for the communication between the experiment running in the participant's browser and the remote PsychoJS manager running on the remote https://pavlovia.org server.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { PsychoJS } from './PsychoJS';
|
import {PsychoJS} from './PsychoJS';
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
import {ExperimentHandler} from "../data/ExperimentHandler";
|
import {ExperimentHandler} from "../data/ExperimentHandler";
|
||||||
import {MonotonicClock} from "../util/Clock";
|
import {MonotonicClock} from "../util/Clock";
|
||||||
// import { Howl } from 'howler';
|
|
||||||
|
|
||||||
|
// import { Howl } from 'howler';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,12 +29,14 @@ import {MonotonicClock} from "../util/Clock";
|
|||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class ServerManager extends PsychObject {
|
export class ServerManager extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
autoLog = false
|
autoLog = false
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
// session:
|
// session:
|
||||||
@ -66,15 +68,22 @@ export class ServerManager extends PsychObject {
|
|||||||
*
|
*
|
||||||
* @returns {Promise<ServerManager.GetConfigurationPromise>} the response
|
* @returns {Promise<ServerManager.GetConfigurationPromise>} the response
|
||||||
*/
|
*/
|
||||||
getConfiguration(configURL) {
|
getConfiguration(configURL)
|
||||||
const response = { origin: 'ServerManager.getConfiguration', context: 'when reading the configuration file: ' + configURL };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.getConfiguration',
|
||||||
|
context: 'when reading the configuration file: ' + configURL
|
||||||
|
};
|
||||||
|
|
||||||
this._psychoJS.logger.debug('reading the configuration file: ' + configURL);
|
this._psychoJS.logger.debug('reading the configuration file: ' + configURL);
|
||||||
return new Promise((resolve, reject) => {
|
const self = this;
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
{
|
||||||
$.get(configURL, 'json')
|
$.get(configURL, 'json')
|
||||||
.done((config, textStatus) => {
|
.done((config, textStatus) =>
|
||||||
|
{
|
||||||
// resolve({ ...response, config });
|
// resolve({ ...response, config });
|
||||||
resolve(Object.assign(response, { config }));
|
resolve(Object.assign(response, {config}));
|
||||||
})
|
})
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -83,7 +92,7 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -118,7 +127,9 @@ export class ServerManager extends PsychObject {
|
|||||||
// prepare POST query:
|
// prepare POST query:
|
||||||
let data = {};
|
let data = {};
|
||||||
if (this._psychoJS._serverMsg.has('__pilotToken'))
|
if (this._psychoJS._serverMsg.has('__pilotToken'))
|
||||||
|
{
|
||||||
data.pilotToken = this._psychoJS._serverMsg.get('__pilotToken');
|
data.pilotToken = this._psychoJS._serverMsg.get('__pilotToken');
|
||||||
|
}
|
||||||
|
|
||||||
// query pavlovia server:
|
// query pavlovia server:
|
||||||
const self = this;
|
const self = this;
|
||||||
@ -128,26 +139,32 @@ export class ServerManager extends PsychObject {
|
|||||||
$.post(url, data, null, 'json')
|
$.post(url, data, null, 'json')
|
||||||
.done((data, textStatus) =>
|
.done((data, textStatus) =>
|
||||||
{
|
{
|
||||||
if (!('token' in data)) {
|
if (!('token' in data))
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
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'});
|
// 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);
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
// reject({...response, error: 'unexpected answer from server: no experiment'});
|
// 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.status = data.experiment.status2;
|
||||||
self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
|
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.license = data.experiment.license;
|
||||||
self._psychoJS.config.experiment.runMode = data.experiment.runMode;
|
self._psychoJS.config.experiment.runMode = data.experiment.runMode;
|
||||||
|
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
// resolve({ ...response, token: data.token, status: data.status });
|
// 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) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -156,7 +173,7 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -178,8 +195,12 @@ export class ServerManager extends PsychObject {
|
|||||||
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
|
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
|
||||||
* @returns {Promise<ServerManager.CloseSessionPromise> | void} the response
|
* @returns {Promise<ServerManager.CloseSessionPromise> | void} the response
|
||||||
*/
|
*/
|
||||||
closeSession(isCompleted = false, sync = false) {
|
async closeSession(isCompleted = false, sync = false)
|
||||||
const response = { origin: 'ServerManager.closeSession', context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.closeSession',
|
||||||
|
context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath
|
||||||
|
};
|
||||||
|
|
||||||
this._psychoJS.logger.debug('closing the session for experiment: ' + this._psychoJS.config.experiment.name);
|
this._psychoJS.logger.debug('closing the session for experiment: ' + this._psychoJS.config.experiment.name);
|
||||||
|
|
||||||
@ -187,33 +208,49 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
// prepare DELETE query:
|
// prepare DELETE query:
|
||||||
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) + '/sessions/' + this._psychoJS.config.session.token;
|
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) + '/sessions/' + this._psychoJS.config.session.token;
|
||||||
const data = { isCompleted };
|
|
||||||
|
|
||||||
// synchronous query the pavlovia server:
|
// synchronous query the pavlovia server:
|
||||||
if (sync)
|
if (sync)
|
||||||
{
|
{
|
||||||
|
/* This is now deprecated in most browsers.
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
request.open("DELETE", url, false);
|
request.open("DELETE", url, false);
|
||||||
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
||||||
request.send(JSON.stringify(data));
|
request.send(JSON.stringify(data));
|
||||||
|
*/
|
||||||
return;
|
/* This does not work in Chrome before of a CORS bug
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
||||||
|
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:
|
// asynchronously query the pavlovia server:
|
||||||
|
else
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) =>
|
||||||
|
{
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url,
|
url,
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
data,
|
data: {isCompleted},
|
||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
})
|
})
|
||||||
.done((data, textStatus) => {
|
.done((data, textStatus) =>
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
|
self._psychoJS.config.session.status = 'CLOSED';
|
||||||
|
|
||||||
// resolve({ ...response, data });
|
// resolve({ ...response, data });
|
||||||
resolve(Object.assign(response, { data }));
|
resolve(Object.assign(response, {data}));
|
||||||
})
|
})
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -222,10 +259,11 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -238,13 +276,19 @@ export class ServerManager extends PsychObject {
|
|||||||
* @return {Object} value of the resource
|
* @return {Object} value of the resource
|
||||||
* @throws {Object.<string, *>} exception if no resource with that name has previously been registered
|
* @throws {Object.<string, *>} exception if no resource with that name has previously been registered
|
||||||
*/
|
*/
|
||||||
getResource(name) {
|
getResource(name)
|
||||||
const response = { origin: 'ServerManager.getResource', context: 'when getting the value of resource: ' + name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.getResource',
|
||||||
|
context: 'when getting the value of resource: ' + name
|
||||||
|
};
|
||||||
|
|
||||||
const path_data = this._resources.get(name);
|
const path_data = this._resources.get(name);
|
||||||
if (typeof path_data === 'undefined')
|
if (typeof path_data === 'undefined')
|
||||||
// throw { ...response, error: 'unknown resource' };
|
// throw { ...response, error: 'unknown resource' };
|
||||||
throw Object.assign(response, { error: 'unknown resource' });
|
{
|
||||||
|
throw Object.assign(response, {error: 'unknown resource'});
|
||||||
|
}
|
||||||
|
|
||||||
return path_data.data;
|
return path_data.data;
|
||||||
}
|
}
|
||||||
@ -257,17 +301,25 @@ export class ServerManager extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
setStatus(status) {
|
setStatus(status)
|
||||||
const response = { origin: 'ServerManager.setStatus', context: 'when changing the status of the server manager to: ' + util.toString(status) };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.setStatus',
|
||||||
|
context: 'when changing the status of the server manager to: ' + util.toString(status)
|
||||||
|
};
|
||||||
|
|
||||||
// check status:
|
// check status:
|
||||||
const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
|
const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
|
||||||
if (!statusKey)
|
if (!statusKey)
|
||||||
// throw { ...response, error: 'status must be a symbol' };
|
// throw { ...response, error: 'status must be a symbol' };
|
||||||
throw Object.assign(response, { error: 'status must be a symbol' });
|
{
|
||||||
|
throw Object.assign(response, {error: 'status must be a symbol'});
|
||||||
|
}
|
||||||
if (!ServerManager.Status.hasOwnProperty(statusKey))
|
if (!ServerManager.Status.hasOwnProperty(statusKey))
|
||||||
// throw { ...response, error: 'unknown status' };
|
// throw { ...response, error: 'unknown status' };
|
||||||
throw Object.assign(response, { error: 'unknown status' });
|
{
|
||||||
|
throw Object.assign(response, {error: 'unknown status'});
|
||||||
|
}
|
||||||
|
|
||||||
this._status = status;
|
this._status = status;
|
||||||
|
|
||||||
@ -286,7 +338,8 @@ export class ServerManager extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {ServerManager.Status.READY} the new status
|
* @return {ServerManager.Status.READY} the new status
|
||||||
*/
|
*/
|
||||||
resetStatus() {
|
resetStatus()
|
||||||
|
{
|
||||||
return this.setStatus(ServerManager.Status.READY);
|
return this.setStatus(ServerManager.Status.READY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,8 +360,12 @@ export class ServerManager extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
downloadResources(resources = []) {
|
downloadResources(resources = [])
|
||||||
const response = { origin: 'ServerManager.downloadResources', context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ServerManager.downloadResources',
|
||||||
|
context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name
|
||||||
|
};
|
||||||
|
|
||||||
this._psychoJS.logger.debug('downloading resources for experiment: ' + this._psychoJS.config.experiment.name);
|
this._psychoJS.logger.debug('downloading resources for experiment: ' + this._psychoJS.config.experiment.name);
|
||||||
|
|
||||||
@ -316,26 +373,37 @@ export class ServerManager extends PsychObject {
|
|||||||
// but we want to run the asynchronous _listResources and _downloadResources in sequence
|
// but we want to run the asynchronous _listResources and _downloadResources in sequence
|
||||||
const self = this;
|
const self = this;
|
||||||
const newResources = new Map();
|
const newResources = new Map();
|
||||||
let download = async () => {
|
let download = async () =>
|
||||||
try {
|
{
|
||||||
if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER) {
|
try
|
||||||
|
{
|
||||||
|
if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER)
|
||||||
|
{
|
||||||
// no resources specified, we register them all:
|
// no resources specified, we register them all:
|
||||||
if (resources.length === 0) {
|
if (resources.length === 0)
|
||||||
|
{
|
||||||
// list the resources from the resources directory of the experiment on the server:
|
// list the resources from the resources directory of the experiment on the server:
|
||||||
const serverResponse = await self._listResources();
|
const serverResponse = await self._listResources();
|
||||||
for (const name of serverResponse.resources)
|
for (const name of serverResponse.resources)
|
||||||
self._resources.set(name, { path: serverResponse.resourceDirectory + '/' + name });
|
{
|
||||||
|
self._resources.set(name, {path: serverResponse.resourceDirectory + '/' + name});
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// only registered the specified resources:
|
// only registered the specified resources:
|
||||||
for (const {name, path} of resources) {
|
for (const {name, path} of resources)
|
||||||
|
{
|
||||||
self._resources.set(name, {path});
|
self._resources.set(name, {path});
|
||||||
newResources.set(name, {path});
|
newResources.set(name, {path});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// register the specified resources:
|
// register the specified resources:
|
||||||
for (const {name, path} of resources) {
|
for (const {name, path} of resources)
|
||||||
|
{
|
||||||
self._resources.set(name, {path});
|
self._resources.set(name, {path});
|
||||||
newResources.set(name, {path});
|
newResources.set(name, {path});
|
||||||
}
|
}
|
||||||
@ -343,17 +411,23 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
self._nbResources = self._resources.size;
|
self._nbResources = self._resources.size;
|
||||||
for (const name of self._resources.keys())
|
for (const name of self._resources.keys())
|
||||||
|
{
|
||||||
this._psychoJS.logger.debug('resource:', name, self._resources.get(name).path);
|
this._psychoJS.logger.debug('resource:', name, self._resources.get(name).path);
|
||||||
|
}
|
||||||
|
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCES_REGISTERED, count: self._nbResources });
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.RESOURCES_REGISTERED,
|
||||||
|
count: self._nbResources
|
||||||
|
});
|
||||||
|
|
||||||
// download the registered resources:
|
// download the registered resources:
|
||||||
await self._downloadRegisteredResources(newResources);
|
await self._downloadRegisteredResources(newResources);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
|
{
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
// throw { ...response, error: error };
|
// throw { ...response, error: error };
|
||||||
throw Object.assign(response, { error });
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -375,10 +449,11 @@ export class ServerManager extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {string} key - the data key (e.g. the name of .csv file)
|
* @param {string} key - the data key (e.g. the name of .csv file)
|
||||||
* @param {string} value - the data value (e.g. a string containing the .csv header and records)
|
* @param {string} value - the data value (e.g. a string containing the .csv header and records)
|
||||||
|
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
|
||||||
*
|
*
|
||||||
* @returns {Promise<ServerManager.UploadDataPromise>} the response
|
* @returns {Promise<ServerManager.UploadDataPromise>} the response
|
||||||
*/
|
*/
|
||||||
uploadData(key, value)
|
uploadData(key, value, sync = false)
|
||||||
{
|
{
|
||||||
const response = {
|
const response = {
|
||||||
origin: 'ServerManager.uploadData',
|
origin: 'ServerManager.uploadData',
|
||||||
@ -388,26 +463,35 @@ export class ServerManager extends PsychObject {
|
|||||||
this._psychoJS.logger.debug('uploading data for experiment: ' + this._psychoJS.config.experiment.fullpath);
|
this._psychoJS.logger.debug('uploading data for experiment: ' + this._psychoJS.config.experiment.fullpath);
|
||||||
this.setStatus(ServerManager.Status.BUSY);
|
this.setStatus(ServerManager.Status.BUSY);
|
||||||
|
|
||||||
// prepare the POST query:
|
const url = this._psychoJS.config.pavlovia.URL +
|
||||||
|
'/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 = {
|
const data = {
|
||||||
key,
|
key,
|
||||||
value
|
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')
|
$.post(url, data, null, 'json')
|
||||||
.done((serverData, textStatus) =>
|
.done((serverData, textStatus) =>
|
||||||
{
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
resolve(Object.assign(response, { serverData }));
|
resolve(Object.assign(response, {serverData}));
|
||||||
})
|
})
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -416,11 +500,11 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -468,7 +552,7 @@ export class ServerManager extends PsychObject {
|
|||||||
.done((serverData, textStatus) =>
|
.done((serverData, textStatus) =>
|
||||||
{
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
resolve(Object.assign(response, { serverData }));
|
resolve(Object.assign(response, {serverData}));
|
||||||
})
|
})
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -477,14 +561,12 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List the resources available to the experiment.
|
* List the resources available to the experiment.
|
||||||
|
|
||||||
@ -524,18 +606,21 @@ export class ServerManager extends PsychObject {
|
|||||||
{
|
{
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
// reject({ ...response, error: 'unexpected answer from server: no resources' });
|
// reject({ ...response, error: 'unexpected answer from server: no resources' });
|
||||||
reject(Object.assign(response, { error: 'unexpected answer from server: no resources' }));
|
reject(Object.assign(response, {error: 'unexpected answer from server: no resources'}));
|
||||||
}
|
}
|
||||||
if (!('resourceDirectory' in data))
|
if (!('resourceDirectory' in data))
|
||||||
{
|
{
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
// reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
|
// reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
|
||||||
reject(Object.assign(response, { error: 'unexpected answer from server: no resourceDirectory' }));
|
reject(Object.assign(response, {error: 'unexpected answer from server: no resourceDirectory'}));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
// resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
|
// resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
|
||||||
resolve(Object.assign(response, { resources: data.resources, resourceDirectory: data.resourceDirectory }));
|
resolve(Object.assign(response, {
|
||||||
|
resources: data.resources,
|
||||||
|
resourceDirectory: data.resourceDirectory
|
||||||
|
}));
|
||||||
})
|
})
|
||||||
.fail((jqXHR, textStatus, errorThrown) =>
|
.fail((jqXHR, textStatus, errorThrown) =>
|
||||||
{
|
{
|
||||||
@ -544,14 +629,13 @@ export class ServerManager extends PsychObject {
|
|||||||
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
|
||||||
console.error('error:', errorMsg);
|
console.error('error:', errorMsg);
|
||||||
|
|
||||||
reject(Object.assign(response, { error: errorMsg }));
|
reject(Object.assign(response, {error: errorMsg}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the resources previously registered.
|
* Download the resources previously registered.
|
||||||
*
|
*
|
||||||
@ -563,7 +647,10 @@ export class ServerManager extends PsychObject {
|
|||||||
*/
|
*/
|
||||||
_downloadRegisteredResources(resources = new Map())
|
_downloadRegisteredResources(resources = new Map())
|
||||||
{
|
{
|
||||||
const response = { origin: 'ServerManager._downloadResources', context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name };
|
const response = {
|
||||||
|
origin: 'ServerManager._downloadResources',
|
||||||
|
context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name
|
||||||
|
};
|
||||||
|
|
||||||
this._psychoJS.logger.debug('downloading the registered resources for experiment: ' + this._psychoJS.config.experiment.name);
|
this._psychoJS.logger.debug('downloading the registered resources for experiment: ' + this._psychoJS.config.experiment.name);
|
||||||
|
|
||||||
@ -578,32 +665,43 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
const filesToDownload = resources.size ? resources : this._resources;
|
const filesToDownload = resources.size ? resources : this._resources;
|
||||||
|
|
||||||
this._resourceQueue.addEventListener("filestart", event => {
|
this._resourceQueue.addEventListener("filestart", event =>
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOADING_RESOURCE, resource: event.item.id });
|
{
|
||||||
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.DOWNLOADING_RESOURCE,
|
||||||
|
resource: event.item.id
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._resourceQueue.addEventListener("fileload", event => {
|
this._resourceQueue.addEventListener("fileload", event =>
|
||||||
|
{
|
||||||
++self._nbLoadedResources;
|
++self._nbLoadedResources;
|
||||||
let path_data = self._resources.get(event.item.id);
|
let path_data = self._resources.get(event.item.id);
|
||||||
path_data.data = event.result;
|
path_data.data = event.result;
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCE_DOWNLOADED, resource: event.item.id });
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.RESOURCE_DOWNLOADED,
|
||||||
|
resource: event.item.id
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// loading completed:
|
// loading completed:
|
||||||
this._resourceQueue.addEventListener("complete", event => {
|
this._resourceQueue.addEventListener("complete", event =>
|
||||||
|
{
|
||||||
self._resourceQueue.close();
|
self._resourceQueue.close();
|
||||||
if (self._nbLoadedResources === filesToDownload.size) {
|
if (self._nbLoadedResources === filesToDownload.size)
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
|
self.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// error: we throw an exception
|
// error: we throw an exception
|
||||||
this._resourceQueue.addEventListener("error", event => {
|
this._resourceQueue.addEventListener("error", event =>
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.ERROR);
|
self.setStatus(ServerManager.Status.ERROR);
|
||||||
const resourceId = (typeof event.data !== 'undefined')?event.data.id:'UNKNOWN RESOURCE';
|
const resourceId = (typeof event.data !== 'undefined') ? event.data.id : 'UNKNOWN RESOURCE';
|
||||||
// throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
|
// throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
|
||||||
throw Object.assign(response, { error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' });
|
throw Object.assign(response, {error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -623,9 +721,9 @@ export class ServerManager extends PsychObject {
|
|||||||
|
|
||||||
// preload.js with forced binary for xls and xlsx:
|
// preload.js with forced binary for xls and xlsx:
|
||||||
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(extension) > -1)
|
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(extension) > -1)
|
||||||
manifest.push({ id: name, src: path_data.path, type: createjs.Types.BINARY });
|
{
|
||||||
|
manifest.push({id: name, src: path_data.path, type: createjs.Types.BINARY});
|
||||||
/* ascii .csv are adequately handled in binary format
|
}/* ascii .csv are adequately handled in binary format
|
||||||
// forced text for .csv:
|
// forced text for .csv:
|
||||||
else if (['csv'].indexOf(resourceExtension) > -1)
|
else if (['csv'].indexOf(resourceExtension) > -1)
|
||||||
manifest.push({ id: resourceName, src: resourceName, type: createjs.Types.TEXT });
|
manifest.push({ id: resourceName, src: resourceName, type: createjs.Types.TEXT });
|
||||||
@ -637,29 +735,41 @@ export class ServerManager extends PsychObject {
|
|||||||
soundResources.push(name);
|
soundResources.push(name);
|
||||||
|
|
||||||
if (extension === 'wav')
|
if (extension === 'wav')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn(`wav files are not supported by all browsers. We recommend you convert "${name}" to another format, e.g. mp3`);
|
this.psychoJS.logger.warn(`wav files are not supported by all browsers. We recommend you convert "${name}" to another format, e.g. mp3`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// preload.js for the other extensions (download type decided by preload.js):
|
// preload.js for the other extensions (download type decided by preload.js):
|
||||||
else
|
else
|
||||||
manifest.push({ id: name, src: path_data.path });
|
{
|
||||||
|
manifest.push({id: name, src: path_data.path});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// (*) start loading non-sound resources:
|
// (*) start loading non-sound resources:
|
||||||
if (manifest.length > 0)
|
if (manifest.length > 0)
|
||||||
|
{
|
||||||
this._resourceQueue.loadManifest(manifest);
|
this._resourceQueue.loadManifest(manifest);
|
||||||
else {
|
}
|
||||||
if (this._nbLoadedResources === filesToDownload.size) {
|
else
|
||||||
|
{
|
||||||
|
if (this._nbLoadedResources === filesToDownload.size)
|
||||||
|
{
|
||||||
this.setStatus(ServerManager.Status.READY);
|
this.setStatus(ServerManager.Status.READY);
|
||||||
this.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
|
this.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// (*) prepare and start loading sound resources:
|
// (*) prepare and start loading sound resources:
|
||||||
for (const name of soundResources) {
|
for (const name of soundResources)
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOADING_RESOURCE, resource: name });
|
{
|
||||||
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.DOWNLOADING_RESOURCE,
|
||||||
|
resource: name
|
||||||
|
});
|
||||||
const path_data = self._resources.get(name);
|
const path_data = self._resources.get(name);
|
||||||
const howl = new Howl({
|
const howl = new Howl({
|
||||||
src: path_data.path,
|
src: path_data.path,
|
||||||
@ -667,20 +777,26 @@ export class ServerManager extends PsychObject {
|
|||||||
autoplay: false
|
autoplay: false
|
||||||
});
|
});
|
||||||
|
|
||||||
howl.on('load', (event) => {
|
howl.on('load', (event) =>
|
||||||
|
{
|
||||||
++self._nbLoadedResources;
|
++self._nbLoadedResources;
|
||||||
path_data.data = howl;
|
path_data.data = howl;
|
||||||
// self._resources.set(resource.name, howl);
|
// self._resources.set(resource.name, howl);
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCE_DOWNLOADED, resource: name });
|
self.emit(ServerManager.Event.RESOURCE, {
|
||||||
|
message: ServerManager.Event.RESOURCE_DOWNLOADED,
|
||||||
|
resource: name
|
||||||
|
});
|
||||||
|
|
||||||
if (self._nbLoadedResources === filesToDownload.size) {
|
if (self._nbLoadedResources === filesToDownload.size)
|
||||||
|
{
|
||||||
self.setStatus(ServerManager.Status.READY);
|
self.setStatus(ServerManager.Status.READY);
|
||||||
self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
|
self.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
howl.on('loaderror', (id, error) => {
|
howl.on('loaderror', (id, error) =>
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
|
// throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
|
||||||
throw Object.assign(response, { error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' });
|
throw Object.assign(response, {error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')'});
|
||||||
});
|
});
|
||||||
|
|
||||||
howl.load();
|
howl.load();
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
* Window responsible for displaying the experiment stimuli
|
* Window responsible for displaying the experiment stimuli
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import { MonotonicClock } from '../util/Clock';
|
import {MonotonicClock} from '../util/Clock';
|
||||||
import { Logger } from "./Logger";
|
import {Logger} from "./Logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Window displays the various stimuli of the experiment.</p>
|
* <p>Window displays the various stimuli of the experiment.</p>
|
||||||
@ -29,7 +29,8 @@ import { Logger } from "./Logger";
|
|||||||
* before flipping
|
* before flipping
|
||||||
* @param {boolean} [options.autoLog= true] whether or not to log
|
* @param {boolean} [options.autoLog= true] whether or not to log
|
||||||
*/
|
*/
|
||||||
export class Window extends PsychObject {
|
export class Window extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter for monitorFramePeriod.
|
* Getter for monitorFramePeriod.
|
||||||
@ -38,7 +39,10 @@ export class Window extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get monitorFramePeriod() { return this._monitorFramePeriod; }
|
get monitorFramePeriod()
|
||||||
|
{
|
||||||
|
return this._monitorFramePeriod;
|
||||||
|
}
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
@ -76,7 +80,8 @@ export class Window extends PsychObject {
|
|||||||
// fullscreen listener:
|
// fullscreen listener:
|
||||||
this._windowAlreadyInFullScreen = false;
|
this._windowAlreadyInFullScreen = false;
|
||||||
const self = this;
|
const self = this;
|
||||||
document.addEventListener('fullscreenchange', (event) => {
|
document.addEventListener('fullscreenchange', (event) =>
|
||||||
|
{
|
||||||
self._windowAlreadyInFullScreen = !!document.fullscreenElement;
|
self._windowAlreadyInFullScreen = !!document.fullscreenElement;
|
||||||
|
|
||||||
console.log('windowAlreadyInFullScreen:', self._windowAlreadyInFullScreen);
|
console.log('windowAlreadyInFullScreen:', self._windowAlreadyInFullScreen);
|
||||||
@ -84,13 +89,17 @@ export class Window extends PsychObject {
|
|||||||
// the Window and all of the stimuli need to be updated:
|
// the Window and all of the stimuli need to be updated:
|
||||||
self._needUpdate = true;
|
self._needUpdate = true;
|
||||||
for (const stimulus of self._drawList)
|
for (const stimulus of self._drawList)
|
||||||
|
{
|
||||||
stimulus._needUpdate = true;
|
stimulus._needUpdate = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,10 +114,14 @@ export class Window extends PsychObject {
|
|||||||
close()
|
close()
|
||||||
{
|
{
|
||||||
if (!this._renderer)
|
if (!this._renderer)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (document.body.contains(this._renderer.view))
|
if (document.body.contains(this._renderer.view))
|
||||||
|
{
|
||||||
document.body.removeChild(this._renderer.view);
|
document.body.removeChild(this._renderer.view);
|
||||||
|
}
|
||||||
|
|
||||||
// destroy the renderer and the WebGL context:
|
// destroy the renderer and the WebGL context:
|
||||||
if (typeof this._renderer.gl !== 'undefined')
|
if (typeof this._renderer.gl !== 'undefined')
|
||||||
@ -173,17 +186,22 @@ export class Window extends PsychObject {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (typeof document.documentElement.mozRequestFullScreen === 'function')
|
else if (typeof document.documentElement.mozRequestFullScreen === 'function')
|
||||||
|
{
|
||||||
document.documentElement.mozRequestFullScreen();
|
document.documentElement.mozRequestFullScreen();
|
||||||
|
}
|
||||||
else if (typeof document.documentElement.webkitRequestFullscreen === 'function')
|
else if (typeof document.documentElement.webkitRequestFullscreen === 'function')
|
||||||
|
{
|
||||||
document.documentElement.webkitRequestFullscreen();
|
document.documentElement.webkitRequestFullscreen();
|
||||||
|
}
|
||||||
else if (typeof document.documentElement.msRequestFullscreen === 'function')
|
else if (typeof document.documentElement.msRequestFullscreen === 'function')
|
||||||
|
{
|
||||||
document.documentElement.msRequestFullscreen();
|
document.documentElement.msRequestFullscreen();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('Unable to go fullscreen.');
|
this.psychoJS.logger.warn('Unable to go fullscreen.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,17 +228,22 @@ export class Window extends PsychObject {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (typeof document.mozCancelFullScreen === 'function')
|
else if (typeof document.mozCancelFullScreen === 'function')
|
||||||
|
{
|
||||||
document.mozCancelFullScreen();
|
document.mozCancelFullScreen();
|
||||||
|
}
|
||||||
else if (typeof document.webkitExitFullscreen === 'function')
|
else if (typeof document.webkitExitFullscreen === 'function')
|
||||||
|
{
|
||||||
document.webkitExitFullscreen();
|
document.webkitExitFullscreen();
|
||||||
|
}
|
||||||
else if (typeof document.msExitFullscreen === 'function')
|
else if (typeof document.msExitFullscreen === 'function')
|
||||||
|
{
|
||||||
document.msExitFullscreen();
|
document.msExitFullscreen();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('Unable to close fullscreen.');
|
this.psychoJS.logger.warn('Unable to close fullscreen.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,9 +264,10 @@ export class Window extends PsychObject {
|
|||||||
logOnFlip({
|
logOnFlip({
|
||||||
msg,
|
msg,
|
||||||
level = Logger.ServerLevel.EXP,
|
level = Logger.ServerLevel.EXP,
|
||||||
obj} = {})
|
obj
|
||||||
|
} = {})
|
||||||
{
|
{
|
||||||
this._msgToBeLogged.push({ msg, level, obj });
|
this._msgToBeLogged.push({msg, level, obj});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -281,7 +305,9 @@ export class Window extends PsychObject {
|
|||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
if (!this._renderer)
|
if (!this._renderer)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
this._frameCount++;
|
this._frameCount++;
|
||||||
@ -297,12 +323,16 @@ export class Window extends PsychObject {
|
|||||||
|
|
||||||
// blocks execution until the rendering is fully done:
|
// blocks execution until the rendering is fully done:
|
||||||
if (this._waitBlanking)
|
if (this._waitBlanking)
|
||||||
|
{
|
||||||
this._renderer.gl.finish();
|
this._renderer.gl.finish();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// call the callOnFlip functions and remove them:
|
// call the callOnFlip functions and remove them:
|
||||||
for (let callback of this._flipCallbacks)
|
for (let callback of this._flipCallbacks)
|
||||||
|
{
|
||||||
callback['function'](...callback['arguments']);
|
callback['function'](...callback['arguments']);
|
||||||
|
}
|
||||||
this._flipCallbacks = [];
|
this._flipCallbacks = [];
|
||||||
|
|
||||||
// log:
|
// log:
|
||||||
@ -325,7 +355,9 @@ export class Window extends PsychObject {
|
|||||||
if (this._needUpdate)
|
if (this._needUpdate)
|
||||||
{
|
{
|
||||||
if (this._renderer)
|
if (this._renderer)
|
||||||
|
{
|
||||||
this._renderer.backgroundColor = this._color.int;
|
this._renderer.backgroundColor = this._color.int;
|
||||||
|
}
|
||||||
|
|
||||||
// we also change the background color of the body since the dialog popup may be longer than the window's height:
|
// we also change the background color of the body since the dialog popup may be longer than the window's height:
|
||||||
document.body.style.backgroundColor = this._color.hex;
|
document.body.style.backgroundColor = this._color.hex;
|
||||||
@ -348,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
|
// if a stimuli needs to be updated, we remove it from the window container, update it, then put it back
|
||||||
for (const stimulus of this._drawList)
|
for (const stimulus of this._drawList)
|
||||||
if (stimulus._needUpdate && typeof stimulus._pixi !== 'undefined') {
|
{
|
||||||
|
if (stimulus._needUpdate && typeof stimulus._pixi !== 'undefined')
|
||||||
|
{
|
||||||
this._rootContainer.removeChild(stimulus._pixi);
|
this._rootContainer.removeChild(stimulus._pixi);
|
||||||
stimulus._updateIfNeeded();
|
stimulus._updateIfNeeded();
|
||||||
this._rootContainer.addChild(stimulus._pixi);
|
this._rootContainer.addChild(stimulus._pixi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -368,7 +403,9 @@ export class Window extends PsychObject {
|
|||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
|
|
||||||
for (const stimulus of this._drawList)
|
for (const stimulus of this._drawList)
|
||||||
|
{
|
||||||
stimulus.refresh();
|
stimulus.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
this._refresh();
|
this._refresh();
|
||||||
}
|
}
|
||||||
@ -413,7 +450,8 @@ export class Window extends PsychObject {
|
|||||||
this.psychoJS.eventManager.addMouseListeners(this._renderer);
|
this.psychoJS.eventManager.addMouseListeners(this._renderer);
|
||||||
|
|
||||||
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
|
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
|
||||||
this._resizeCallback = (e) => {
|
this._resizeCallback = (e) =>
|
||||||
|
{
|
||||||
Window._resizePixiRenderer(this, e);
|
Window._resizePixiRenderer(this, e);
|
||||||
this._fullRefresh();
|
this._fullRefresh();
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Mixin implementing various unit-handling measurement methods.
|
* Mixin implementing various unit-handling measurement methods.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -19,8 +19,10 @@
|
|||||||
* @mixin
|
* @mixin
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export let WindowMixin = (superclass) => class extends superclass {
|
export let WindowMixin = (superclass) => class extends superclass
|
||||||
constructor(args) {
|
{
|
||||||
|
constructor(args)
|
||||||
|
{
|
||||||
super(args);
|
super(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +36,8 @@ export let WindowMixin = (superclass) => class extends superclass {
|
|||||||
* @param {String} [units= this.win.units] - the units
|
* @param {String} [units= this.win.units] - the units
|
||||||
* @param {boolean} [log= false] - whether or not to log
|
* @param {boolean} [log= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
setUnits(units = this.win.units, log = false) {
|
setUnits(units = this.win.units, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('units', units, log);
|
this._setAttribute('units', units, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,23 +51,31 @@ export let WindowMixin = (superclass) => class extends superclass {
|
|||||||
* @param {number} length - the length in stimulus units
|
* @param {number} length - the length in stimulus units
|
||||||
* @return {number} - the length in pixel units
|
* @return {number} - the length in pixel units
|
||||||
*/
|
*/
|
||||||
_getLengthPix(length) {
|
_getLengthPix(length)
|
||||||
let response = { origin: 'WindowMixin._getLengthPix', context: 'when converting a length from stimulus unit to pixel units' };
|
{
|
||||||
|
let response = {
|
||||||
|
origin: 'WindowMixin._getLengthPix',
|
||||||
|
context: 'when converting a length from stimulus unit to pixel units'
|
||||||
|
};
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix')
|
||||||
|
{
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||||
|
{
|
||||||
var winSize = this.win.size;
|
var winSize = this.win.size;
|
||||||
return length * winSize[1] / 2; // TODO: how do we handle norm when width != height?
|
return length * winSize[1] / 2; // TODO: how do we handle norm when width != height?
|
||||||
}
|
}
|
||||||
else if (this._units === 'height') {
|
else if (this._units === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||||
return length * minSize;
|
return length * minSize;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,23 +89,31 @@ export let WindowMixin = (superclass) => class extends superclass {
|
|||||||
* @param {number} length_px - the length in pixel units
|
* @param {number} length_px - the length in pixel units
|
||||||
* @return {number} - the length in stimulus units
|
* @return {number} - the length in stimulus units
|
||||||
*/
|
*/
|
||||||
_getLengthUnits(length_px) {
|
_getLengthUnits(length_px)
|
||||||
let response = { origin: 'WindowMixin._getLengthUnits', context: 'when converting a length from pixel unit to stimulus units' };
|
{
|
||||||
|
let response = {
|
||||||
|
origin: 'WindowMixin._getLengthUnits',
|
||||||
|
context: 'when converting a length from pixel unit to stimulus units'
|
||||||
|
};
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix')
|
||||||
|
{
|
||||||
return length_px;
|
return length_px;
|
||||||
}
|
}
|
||||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||||
|
{
|
||||||
const winSize = this.win.size;
|
const winSize = this.win.size;
|
||||||
return length_px / (winSize[1] / 2); // TODO: how do we handle norm when width != height?
|
return length_px / (winSize[1] / 2); // TODO: how do we handle norm when width != height?
|
||||||
}
|
}
|
||||||
else if (this._units === 'height') {
|
else if (this._units === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||||
return length_px / minSize;
|
return length_px / minSize;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,23 +127,31 @@ export let WindowMixin = (superclass) => class extends superclass {
|
|||||||
* @param {number} length_px - the length in pixel units
|
* @param {number} length_px - the length in pixel units
|
||||||
* @return {number} - the length in stimulus units
|
* @return {number} - the length in stimulus units
|
||||||
*/
|
*/
|
||||||
_getHorLengthPix(length) {
|
_getHorLengthPix(length)
|
||||||
let response = { origin: 'WindowMixin._getHorLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
{
|
||||||
|
let response = {
|
||||||
|
origin: 'WindowMixin._getHorLengthPix',
|
||||||
|
context: 'when converting a length from pixel unit to stimulus units'
|
||||||
|
};
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix')
|
||||||
|
{
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||||
|
{
|
||||||
var winSize = this.win.size;
|
var winSize = this.win.size;
|
||||||
return length * winSize[0] / 2;
|
return length * winSize[0] / 2;
|
||||||
}
|
}
|
||||||
else if (this._units === 'height') {
|
else if (this._units === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||||
return length * minSize;
|
return length * minSize;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,23 +164,31 @@ export let WindowMixin = (superclass) => class extends superclass {
|
|||||||
* @param {number} length_px - the length in pixel units
|
* @param {number} length_px - the length in pixel units
|
||||||
* @return {number} - the length in stimulus units
|
* @return {number} - the length in stimulus units
|
||||||
*/
|
*/
|
||||||
_getVerLengthPix(length) {
|
_getVerLengthPix(length)
|
||||||
let response = { origin: 'WindowMixin._getVerLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
|
{
|
||||||
|
let response = {
|
||||||
|
origin: 'WindowMixin._getVerLengthPix',
|
||||||
|
context: 'when converting a length from pixel unit to stimulus units'
|
||||||
|
};
|
||||||
|
|
||||||
if (this._units === 'pix') {
|
if (this._units === 'pix')
|
||||||
|
{
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
else if (typeof this._units === 'undefined' || this._units === 'norm') {
|
else if (typeof this._units === 'undefined' || this._units === 'norm')
|
||||||
|
{
|
||||||
var winSize = this.win.size;
|
var winSize = this.win.size;
|
||||||
return length * winSize[1] / 2;
|
return length * winSize[1] / 2;
|
||||||
}
|
}
|
||||||
else if (this._units === 'height') {
|
else if (this._units === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
const minSize = Math.min(this.win.size[0], this.win.size[1]);
|
||||||
return length * minSize;
|
return length * minSize;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
|
||||||
throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
|
throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
* Experiment Handler
|
* Experiment Handler
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import { MonotonicClock } from '../util/Clock';
|
import {MonotonicClock} from '../util/Clock';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +26,8 @@ import * as util from '../util/Util';
|
|||||||
* @param {string} options.name - name of the experiment
|
* @param {string} options.name - name of the experiment
|
||||||
* @param {Object} options.extraInfo - additional information, such as session name, participant name, etc.
|
* @param {Object} options.extraInfo - additional information, such as session name, participant name, etc.
|
||||||
*/
|
*/
|
||||||
export class ExperimentHandler extends PsychObject {
|
export class ExperimentHandler extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter for experimentEnded.
|
* Getter for experimentEnded.
|
||||||
@ -35,7 +36,10 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get experimentEnded() { return this._experimentEnded; }
|
get experimentEnded()
|
||||||
|
{
|
||||||
|
return this._experimentEnded;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for experimentEnded.
|
* Setter for experimentEnded.
|
||||||
@ -44,21 +48,32 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
set experimentEnded(ended) { this._experimentEnded = ended; }
|
set experimentEnded(ended)
|
||||||
|
{
|
||||||
|
this._experimentEnded = ended;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy experiment getters.
|
* Legacy experiment getters.
|
||||||
*/
|
*/
|
||||||
get _thisEntry() { return this._currentTrialData; }
|
get _thisEntry()
|
||||||
get _entries() { return this._trialsData; }
|
{
|
||||||
|
return this._currentTrialData;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _entries()
|
||||||
|
{
|
||||||
|
return this._trialsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
name,
|
name,
|
||||||
extraInfo
|
extraInfo
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(psychoJS, name);
|
super(psychoJS, name);
|
||||||
|
|
||||||
this._addAttributes(ExperimentHandler, extraInfo);
|
this._addAttributes(ExperimentHandler, extraInfo);
|
||||||
@ -85,10 +100,13 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @returns {boolean} whether or not the current entry is empty
|
* @returns {boolean} whether or not the current entry is empty
|
||||||
*/
|
*/
|
||||||
isEntryEmpty() {
|
isEntryEmpty()
|
||||||
|
{
|
||||||
return (Object.keys(this._currentTrialData).length > 0);
|
return (Object.keys(this._currentTrialData).length > 0);
|
||||||
}
|
}
|
||||||
isEntryEmtpy() {
|
|
||||||
|
isEntryEmtpy()
|
||||||
|
{
|
||||||
return (Object.keys(this._currentTrialData).length > 0);
|
return (Object.keys(this._currentTrialData).length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +122,8 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
||||||
*/
|
*/
|
||||||
addLoop(loop) {
|
addLoop(loop)
|
||||||
|
{
|
||||||
this._loops.push(loop);
|
this._loops.push(loop);
|
||||||
this._unfinishedLoops.push(loop);
|
this._unfinishedLoops.push(loop);
|
||||||
loop.experimentHandler = this;
|
loop.experimentHandler = this;
|
||||||
@ -119,11 +138,14 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
|
||||||
*/
|
*/
|
||||||
removeLoop(loop) {
|
removeLoop(loop)
|
||||||
|
{
|
||||||
const index = this._unfinishedLoops.indexOf(loop);
|
const index = this._unfinishedLoops.indexOf(loop);
|
||||||
if (index !== -1)
|
if (index !== -1)
|
||||||
|
{
|
||||||
this._unfinishedLoops.splice(index, 1);
|
this._unfinishedLoops.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -138,14 +160,18 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @param {Object} key - the key
|
* @param {Object} key - the key
|
||||||
* @param {Object} value - the value
|
* @param {Object} value - the value
|
||||||
*/
|
*/
|
||||||
addData(key, value) {
|
addData(key, value)
|
||||||
if (this._trialsKeys.indexOf(key) === -1) {
|
{
|
||||||
|
if (this._trialsKeys.indexOf(key) === -1)
|
||||||
|
{
|
||||||
this._trialsKeys.push(key);
|
this._trialsKeys.push(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// turn arrays into their json equivalent:
|
// turn arrays into their json equivalent:
|
||||||
if (Array.isArray(value))
|
if (Array.isArray(value))
|
||||||
|
{
|
||||||
value = JSON.stringify(value);
|
value = JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
this._currentTrialData[key] = value;
|
this._currentTrialData[key] = value;
|
||||||
}
|
}
|
||||||
@ -166,14 +192,21 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
{
|
{
|
||||||
// turn single snapshot into a one-element array:
|
// turn single snapshot into a one-element array:
|
||||||
if (!Array.isArray(snapshots))
|
if (!Array.isArray(snapshots))
|
||||||
|
{
|
||||||
snapshots = [snapshots];
|
snapshots = [snapshots];
|
||||||
|
}
|
||||||
|
|
||||||
for (const snapshot of snapshots) {
|
for (const snapshot of snapshots)
|
||||||
|
{
|
||||||
const attributes = ExperimentHandler._getLoopAttributes(snapshot);
|
const attributes = ExperimentHandler._getLoopAttributes(snapshot);
|
||||||
for (let a in attributes)
|
for (let a in attributes)
|
||||||
|
{
|
||||||
if (attributes.hasOwnProperty(a))
|
if (attributes.hasOwnProperty(a))
|
||||||
|
{
|
||||||
this._currentTrialData[a] = attributes[a];
|
this._currentTrialData[a] = attributes[a];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// this is to support legacy generated JavaScript code and does not properly handle
|
// 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);
|
const attributes = ExperimentHandler._getLoopAttributes(loop);
|
||||||
for (const a in attributes)
|
for (const a in attributes)
|
||||||
|
{
|
||||||
if (attributes.hasOwnProperty(a))
|
if (attributes.hasOwnProperty(a))
|
||||||
|
{
|
||||||
this._currentTrialData[a] = attributes[a];
|
this._currentTrialData[a] = attributes[a];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// add the extraInfo dict to the data:
|
// add the extraInfo dict to the data:
|
||||||
for (let a in this.extraInfo)
|
for (let a in this.extraInfo)
|
||||||
|
{
|
||||||
if (this.extraInfo.hasOwnProperty(a))
|
if (this.extraInfo.hasOwnProperty(a))
|
||||||
|
{
|
||||||
this._currentTrialData[a] = this.extraInfo[a];
|
this._currentTrialData[a] = this.extraInfo[a];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._trialsData.push(this._currentTrialData);
|
this._trialsData.push(this._currentTrialData);
|
||||||
|
|
||||||
@ -214,28 +255,40 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {Array.<Object>} [options.attributes] - the attributes to be saved
|
* @param {Array.<Object>} [options.attributes] - the attributes to be saved
|
||||||
|
* @param {Array.<Object>} [options.sync] - whether or not to communicate with the server in a synchronous manner
|
||||||
*/
|
*/
|
||||||
async save({
|
async save({
|
||||||
attributes = []
|
attributes = [],
|
||||||
} = {}) {
|
sync = false
|
||||||
|
} = {})
|
||||||
|
{
|
||||||
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
|
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
|
||||||
|
|
||||||
// (*) get attributes:
|
// (*) get attributes:
|
||||||
if (attributes.length === 0) {
|
if (attributes.length === 0)
|
||||||
|
{
|
||||||
attributes = this._trialsKeys.slice();
|
attributes = this._trialsKeys.slice();
|
||||||
for (let l = 0; l < this._loops.length; l++) {
|
for (let l = 0; l < this._loops.length; l++)
|
||||||
|
{
|
||||||
const loop = this._loops[l];
|
const loop = this._loops[l];
|
||||||
|
|
||||||
const loopAttributes = ExperimentHandler._getLoopAttributes(loop);
|
const loopAttributes = ExperimentHandler._getLoopAttributes(loop);
|
||||||
for (let a in loopAttributes)
|
for (let a in loopAttributes)
|
||||||
|
{
|
||||||
if (loopAttributes.hasOwnProperty(a))
|
if (loopAttributes.hasOwnProperty(a))
|
||||||
|
{
|
||||||
attributes.push(a);
|
attributes.push(a);
|
||||||
}
|
}
|
||||||
for (let a in this.extraInfo) {
|
}
|
||||||
|
}
|
||||||
|
for (let a in this.extraInfo)
|
||||||
|
{
|
||||||
if (this.extraInfo.hasOwnProperty(a))
|
if (this.extraInfo.hasOwnProperty(a))
|
||||||
|
{
|
||||||
attributes.push(a);
|
attributes.push(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// (*) get various experiment info:
|
// (*) get various experiment info:
|
||||||
@ -249,41 +302,26 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
|
|
||||||
|
|
||||||
// (*) save to a .csv file:
|
// (*) save to a .csv file:
|
||||||
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV) {
|
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV)
|
||||||
/*
|
{
|
||||||
// a. manual approach
|
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
|
||||||
let csv = "";
|
// newlines, etc.
|
||||||
|
|
||||||
// build the csv header:
|
|
||||||
for (let h = 0; h < attributes.length; h++) {
|
|
||||||
if (h > 0)
|
|
||||||
csv = csv + ', ';
|
|
||||||
csv = csv + attributes[h];
|
|
||||||
}
|
|
||||||
csv = csv + '\n';
|
|
||||||
|
|
||||||
// build the records:
|
|
||||||
for (let r = 0; r < this._trialsData.length; r++) {
|
|
||||||
for (let h = 0; h < attributes.length; h++) {
|
|
||||||
if (h > 0)
|
|
||||||
csv = csv + ', ';
|
|
||||||
csv = csv + this._trialsData[r][attributes[h]];
|
|
||||||
}
|
|
||||||
csv = csv + '\n';
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// b. XLSX approach (automatically deal with header, takes care of quotes, newlines, etc.)
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
|
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
|
||||||
const csv = XLSX.utils.sheet_to_csv(worksheet);
|
const csv = XLSX.utils.sheet_to_csv(worksheet);
|
||||||
|
|
||||||
// upload data to the pavlovia server or offer them for download:
|
// upload data to the pavlovia server or offer them for download:
|
||||||
const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
|
const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
|
||||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER && this._psychoJS.config.experiment.status === 'RUNNING')
|
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
||||||
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv);
|
this._psychoJS.config.experiment.status === 'RUNNING' &&
|
||||||
|
!this._psychoJS._serverMsg.has('__pilotToken'))
|
||||||
|
{
|
||||||
|
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv, sync);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
util.offerDataForDownload(key, csv, 'text/csv');
|
util.offerDataForDownload(key, csv, 'text/csv');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// (*) save in the database on the remote server:
|
// (*) save in the database on the remote server:
|
||||||
@ -291,20 +329,29 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
{
|
{
|
||||||
let documents = [];
|
let documents = [];
|
||||||
|
|
||||||
for (let r = 0; r < this._trialsData.length; r++) {
|
for (let r = 0; r < this._trialsData.length; r++)
|
||||||
|
{
|
||||||
let doc = {__projectId, __experimentName, __participant, __session, __datetime};
|
let doc = {__projectId, __experimentName, __participant, __session, __datetime};
|
||||||
for (let h = 0; h < attributes.length; h++)
|
for (let h = 0; h < attributes.length; h++)
|
||||||
|
{
|
||||||
doc[attributes[h]] = this._trialsData[r][attributes[h]];
|
doc[attributes[h]] = this._trialsData[r][attributes[h]];
|
||||||
|
}
|
||||||
|
|
||||||
documents.push(doc);
|
documents.push(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload data to the pavlovia server or offer them for download:
|
// upload data to the pavlovia server or offer them for download:
|
||||||
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) {
|
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
|
||||||
|
this._psychoJS.config.experiment.status === 'RUNNING' &&
|
||||||
|
!this._psychoJS._serverMsg.has('__pilotToken'))
|
||||||
|
{
|
||||||
const key = 'results'; // name of the mongoDB collection
|
const key = 'results'; // name of the mongoDB collection
|
||||||
return await this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents));
|
return /*await*/ this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents), sync);
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
util.offerDataForDownload('results.json', JSON.stringify(documents), 'application/json');
|
util.offerDataForDownload('results.json', JSON.stringify(documents), 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -320,23 +367,30 @@ export class ExperimentHandler extends PsychObject {
|
|||||||
* @protected
|
* @protected
|
||||||
* @param {Object} loop - the loop
|
* @param {Object} loop - the loop
|
||||||
*/
|
*/
|
||||||
static _getLoopAttributes(loop) {
|
static _getLoopAttributes(loop)
|
||||||
|
{
|
||||||
// standard trial attributes:
|
// standard trial attributes:
|
||||||
const properties = ['thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'stepSizeCurrent', 'ran', 'order'];
|
const properties = ['thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'stepSizeCurrent', 'ran', 'order'];
|
||||||
let attributes = {};
|
let attributes = {};
|
||||||
const loopName = loop.name;
|
const loopName = loop.name;
|
||||||
for (const loopProperty in loop)
|
for (const loopProperty in loop)
|
||||||
if (properties.includes(loopProperty)) {
|
{
|
||||||
const key = (loopProperty === 'stepSizeCurrent')? loopName + '.stepSize' : loopName + '.' + loopProperty;
|
if (properties.includes(loopProperty))
|
||||||
|
{
|
||||||
|
const key = (loopProperty === 'stepSizeCurrent') ? loopName + '.stepSize' : loopName + '.' + loopProperty;
|
||||||
attributes[key] = loop[loopProperty];
|
attributes[key] = loop[loopProperty];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// specific trial attributes:
|
// specific trial attributes:
|
||||||
if (typeof loop.getCurrentTrial === 'function') {
|
if (typeof loop.getCurrentTrial === 'function')
|
||||||
|
{
|
||||||
const currentTrial = loop.getCurrentTrial();
|
const currentTrial = loop.getCurrentTrial();
|
||||||
for (const trialProperty in currentTrial)
|
for (const trialProperty in currentTrial)
|
||||||
|
{
|
||||||
attributes[trialProperty] = currentTrial[trialProperty];
|
attributes[trialProperty] = currentTrial[trialProperty];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* TODO
|
/* TODO
|
||||||
// method of constants
|
// method of constants
|
||||||
|
@ -3,13 +3,13 @@
|
|||||||
* Trial Handler
|
* Trial Handler
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -27,7 +27,8 @@ import * as util from '../util/Util';
|
|||||||
* @param {number} options.seed - seed for the random number generator
|
* @param {number} options.seed - seed for the random number generator
|
||||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
export class TrialHandler extends PsychObject {
|
export class TrialHandler extends PsychObject
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter for experimentHandler.
|
* Getter for experimentHandler.
|
||||||
@ -36,7 +37,10 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get experimentHandler() { return this._experimentHandler; }
|
get experimentHandler()
|
||||||
|
{
|
||||||
|
return this._experimentHandler;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for experimentHandler.
|
* Setter for experimentHandler.
|
||||||
@ -45,7 +49,8 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
set experimentHandler(exp) {
|
set experimentHandler(exp)
|
||||||
|
{
|
||||||
this._experimentHandler = exp;
|
this._experimentHandler = exp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +70,8 @@ export class TrialHandler extends PsychObject {
|
|||||||
seed,
|
seed,
|
||||||
name,
|
name,
|
||||||
autoLog = true
|
autoLog = true
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
this._addAttributes(TrialHandler, trialList, nReps, method, extraInfo, seed, name, autoLog);
|
this._addAttributes(TrialHandler, trialList, nReps, method, extraInfo, seed, name, autoLog);
|
||||||
@ -117,13 +123,15 @@ export class TrialHandler extends PsychObject {
|
|||||||
[Symbol.iterator]()
|
[Symbol.iterator]()
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
next: () => {
|
next: () =>
|
||||||
|
{
|
||||||
this.thisTrialN++;
|
this.thisTrialN++;
|
||||||
this.thisN++;
|
this.thisN++;
|
||||||
this.nRemaining--;
|
this.nRemaining--;
|
||||||
|
|
||||||
// check for the last trial:
|
// check for the last trial:
|
||||||
if (this.nRemaining === 0) {
|
if (this.nRemaining === 0)
|
||||||
|
{
|
||||||
this.finished = true;
|
this.finished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,9 +143,10 @@ export class TrialHandler extends PsychObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if we have completed the sequence:
|
// check if we have completed the sequence:
|
||||||
if (this.thisRepN >= this.nReps) {
|
if (this.thisRepN >= this.nReps)
|
||||||
|
{
|
||||||
this.thisTrial = null;
|
this.thisTrial = null;
|
||||||
return { done: true };
|
return {done: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.thisIndex = this._trialSequence[this.thisRepN][this.thisTrialN];
|
this.thisIndex = this._trialSequence[this.thisRepN][this.thisTrialN];
|
||||||
@ -150,7 +159,7 @@ export class TrialHandler extends PsychObject {
|
|||||||
vals = (self.thisRepN, self.thisTrialN, self.thisTrial)
|
vals = (self.thisRepN, self.thisTrialN, self.thisTrial)
|
||||||
logging.exp(msg % vals, obj=self.thisTrial)*/
|
logging.exp(msg % vals, obj=self.thisTrial)*/
|
||||||
|
|
||||||
return { value: this.thisTrial, done: false };
|
return {value: this.thisTrial, done: false};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -165,11 +174,13 @@ export class TrialHandler extends PsychObject {
|
|||||||
{
|
{
|
||||||
const trialIterator = this[Symbol.iterator]();
|
const trialIterator = this[Symbol.iterator]();
|
||||||
|
|
||||||
while(true)
|
while (true)
|
||||||
{
|
{
|
||||||
const result = trialIterator.next();
|
const result = trialIterator.next();
|
||||||
if (result.done)
|
if (result.done)
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
callback(result.value);
|
callback(result.value);
|
||||||
}
|
}
|
||||||
@ -229,7 +240,8 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {number} the current trial index
|
* @return {number} the current trial index
|
||||||
*/
|
*/
|
||||||
getTrialIndex() {
|
getTrialIndex()
|
||||||
|
{
|
||||||
return this.thisIndex;
|
return this.thisIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +251,8 @@ export class TrialHandler extends PsychObject {
|
|||||||
*
|
*
|
||||||
* @param {number} index - the new trial index
|
* @param {number} index - the new trial index
|
||||||
*/
|
*/
|
||||||
setTrialIndex(index) {
|
setTrialIndex(index)
|
||||||
|
{
|
||||||
this.thisIndex = index;
|
this.thisIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,13 +266,18 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Array.string} the attributes
|
* @return {Array.string} the attributes
|
||||||
*/
|
*/
|
||||||
getAttributes() {
|
getAttributes()
|
||||||
|
{
|
||||||
if (!Array.isArray(this.trialList) || this.nStim === 0)
|
if (!Array.isArray(this.trialList) || this.nStim === 0)
|
||||||
|
{
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const firstTrial = this.trialList[0];
|
const firstTrial = this.trialList[0];
|
||||||
if (!firstTrial)
|
if (!firstTrial)
|
||||||
|
{
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(this.trialList[0]);
|
return Object.keys(this.trialList[0]);
|
||||||
}
|
}
|
||||||
@ -271,7 +289,8 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Object} the current trial
|
* @return {Object} the current trial
|
||||||
*/
|
*/
|
||||||
getCurrentTrial() {
|
getCurrentTrial()
|
||||||
|
{
|
||||||
return this.trialList[this.thisIndex];
|
return this.trialList[this.thisIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,9 +301,12 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @param {number} index - the trial index
|
* @param {number} index - the trial index
|
||||||
* @return {Object|undefined} the requested trial or undefined if attempting to go beyond the last trial.
|
* @return {Object|undefined} the requested trial or undefined if attempting to go beyond the last trial.
|
||||||
*/
|
*/
|
||||||
getTrial(index = 0) {
|
getTrial(index = 0)
|
||||||
|
{
|
||||||
if (index < 0 || index > this.nTotal)
|
if (index < 0 || index > this.nTotal)
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return this.trialList[index];
|
return this.trialList[index];
|
||||||
}
|
}
|
||||||
@ -298,11 +320,14 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @return {Object|undefined} the future trial (if n is positive) or past trial (if n is negative)
|
* @return {Object|undefined} the future trial (if n is positive) or past trial (if n is negative)
|
||||||
* or undefined if attempting to go beyond the last trial.
|
* or undefined if attempting to go beyond the last trial.
|
||||||
*/
|
*/
|
||||||
getFutureTrial(n = 1) {
|
getFutureTrial(n = 1)
|
||||||
if (this.thisIndex+n < 0 || n > this.nRemaining)
|
{
|
||||||
|
if (this.thisIndex + n < 0 || n > this.nRemaining)
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return this.trialList[this.thisIndex+n];
|
return this.trialList[this.thisIndex + n];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -314,7 +339,8 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @param {number} [n = -1] - increment
|
* @param {number} [n = -1] - increment
|
||||||
* @return {Object|undefined} the past trial or undefined if attempting to go prior to the first trial.
|
* @return {Object|undefined} the past trial or undefined if attempting to go prior to the first trial.
|
||||||
*/
|
*/
|
||||||
getEarlierTrial(n = -1) {
|
getEarlierTrial(n = -1)
|
||||||
|
{
|
||||||
return getFutureTrial(-abs(n));
|
return getFutureTrial(-abs(n));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,10 +352,13 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @param {Object} key - the key
|
* @param {Object} key - the key
|
||||||
* @param {Object} value - the value
|
* @param {Object} value - the value
|
||||||
*/
|
*/
|
||||||
addData(key, value) {
|
addData(key, value)
|
||||||
|
{
|
||||||
if (this._experimentHandler)
|
if (this._experimentHandler)
|
||||||
|
{
|
||||||
this._experimentHandler.addData(key, value);
|
this._experimentHandler.addData(key, value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -367,23 +396,28 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @return {Object} the parsed conditions as an array of 'object as map'
|
* @return {Object} the parsed conditions as an array of 'object as map'
|
||||||
* @throws {Object} Throws an exception if importing the conditions failed.
|
* @throws {Object} Throws an exception if importing the conditions failed.
|
||||||
*/
|
*/
|
||||||
static importConditions(serverManager, resourceName, selection = null) {
|
static importConditions(serverManager, resourceName, selection = null)
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
let resourceExtension = resourceName.split('.').pop();
|
let resourceExtension = resourceName.split('.').pop();
|
||||||
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(resourceExtension) > -1) {
|
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(resourceExtension) > -1)
|
||||||
|
{
|
||||||
// (*) read conditions from resource:
|
// (*) read conditions from resource:
|
||||||
const resourceValue = serverManager.getResource(resourceName);
|
const resourceValue = serverManager.getResource(resourceName);
|
||||||
const workbook = XLSX.read(new Uint8Array(resourceValue), { type: "array" });
|
const workbook = XLSX.read(new Uint8Array(resourceValue), {type: "array"});
|
||||||
// const workbook = XLSX.read(resourceValue, { type: "buffer" }); // would work for ascii .csv
|
// const workbook = XLSX.read(resourceValue, { type: "buffer" }); // would work for ascii .csv
|
||||||
|
|
||||||
// we consider only the first worksheet:
|
// we consider only the first worksheet:
|
||||||
if (workbook.SheetNames.length === 0)
|
if (workbook.SheetNames.length === 0)
|
||||||
|
{
|
||||||
throw 'workbook should contain at least one worksheet';
|
throw 'workbook should contain at least one worksheet';
|
||||||
|
}
|
||||||
const sheetName = workbook.SheetNames[0];
|
const sheetName = workbook.SheetNames[0];
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
|
||||||
// worksheet to array of arrays (the first array contains the fields):
|
// worksheet to array of arrays (the first array contains the fields):
|
||||||
const sheet = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false });
|
const sheet = XLSX.utils.sheet_to_json(worksheet, {header: 1, blankrows: false});
|
||||||
const fields = sheet.shift();
|
const fields = sheet.shift();
|
||||||
|
|
||||||
// (*) select conditions:
|
// (*) select conditions:
|
||||||
@ -396,14 +430,17 @@ export class TrialHandler extends PsychObject {
|
|||||||
// ...
|
// ...
|
||||||
// ]
|
// ]
|
||||||
let trialList = new Array(selectedRows.length - 1);
|
let trialList = new Array(selectedRows.length - 1);
|
||||||
for (let r = 0; r < selectedRows.length; ++r) {
|
for (let r = 0; r < selectedRows.length; ++r)
|
||||||
|
{
|
||||||
let row = selectedRows[r];
|
let row = selectedRows[r];
|
||||||
let trial = {};
|
let trial = {};
|
||||||
for (let l = 0; l < fields.length; ++l) {
|
for (let l = 0; l < fields.length; ++l)
|
||||||
|
{
|
||||||
let value = row[l];
|
let value = row[l];
|
||||||
|
|
||||||
// if value is a numerical string, convert it to a number:
|
// if value is a numerical string, convert it to a number:
|
||||||
if (typeof value === 'string' && !isNaN(value)) {
|
if (typeof value === 'string' && !isNaN(value))
|
||||||
|
{
|
||||||
value = Number.parseFloat(value);
|
value = Number.parseFloat(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,12 +452,18 @@ export class TrialHandler extends PsychObject {
|
|||||||
return trialList;
|
return trialList;
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
throw 'extension: ' + resourceExtension + ' currently not supported.';
|
throw 'extension: ' + resourceExtension + ' currently not supported.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw { origin: 'TrialHandler.importConditions', context: `when importing condition: ${resourceName}`, error};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'TrialHandler.importConditions',
|
||||||
|
context: `when importing condition: ${resourceName}`,
|
||||||
|
error
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,27 +474,35 @@ export class TrialHandler extends PsychObject {
|
|||||||
* @protected
|
* @protected
|
||||||
* @param {Array.<Object> | String} trialList - a list of trials, or the name of a condition resource
|
* @param {Array.<Object> | String} trialList - a list of trials, or the name of a condition resource
|
||||||
*/
|
*/
|
||||||
_prepareTrialList(trialList) {
|
_prepareTrialList(trialList)
|
||||||
const response = { origin : 'TrialHandler._prepareTrialList', context : 'when preparing the trial list' };
|
{
|
||||||
|
const response = {origin: 'TrialHandler._prepareTrialList', context: 'when preparing the trial list'};
|
||||||
|
|
||||||
// we treat undefined trialList as a list with a single empty entry:
|
// we treat undefined trialList as a list with a single empty entry:
|
||||||
if (typeof trialList === 'undefined')
|
if (typeof trialList === 'undefined')
|
||||||
|
{
|
||||||
this.trialList = [undefined];
|
this.trialList = [undefined];
|
||||||
|
}// if trialList is an array, we make sure it is not empty:
|
||||||
// if trialList is an array, we make sure it is not empty:
|
else if (Array.isArray(trialList))
|
||||||
else if (Array.isArray(trialList)) {
|
{
|
||||||
if (trialList.length === 0)
|
if (trialList.length === 0)
|
||||||
|
{
|
||||||
this.trialList = [undefined];
|
this.trialList = [undefined];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if trialList is a string, we treat it as the name of the condition resource:
|
// if trialList is a string, we treat it as the name of the condition resource:
|
||||||
else if (typeof trialList === 'string')
|
else if (typeof trialList === 'string')
|
||||||
|
{
|
||||||
this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, trialList);
|
this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, trialList);
|
||||||
|
}// unknown type:
|
||||||
// unknown type:
|
|
||||||
else
|
else
|
||||||
throw Object.assign(response, { error: 'unable to prepare trial list:' +
|
{
|
||||||
' unknown type: ' + (typeof trialList) });
|
throw Object.assign(response, {
|
||||||
|
error: 'unable to prepare trial list:' +
|
||||||
|
' unknown type: ' + (typeof trialList)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -481,35 +532,50 @@ export class TrialHandler extends PsychObject {
|
|||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_prepareSequence() {
|
_prepareSequence()
|
||||||
const response = { origin : 'TrialHandler._prepareSequence', context : 'when preparing a sequence of trials' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'TrialHandler._prepareSequence',
|
||||||
|
context: 'when preparing a sequence of trials'
|
||||||
|
};
|
||||||
|
|
||||||
// get an array of the indices of the elements of trialList :
|
// get an array of the indices of the elements of trialList :
|
||||||
const indices = Array.from(this.trialList.keys());
|
const indices = Array.from(this.trialList.keys());
|
||||||
|
|
||||||
// seed the random number generator:
|
// seed the random number generator:
|
||||||
if (typeof (this.seed) !== 'undefined')
|
if (typeof (this.seed) !== 'undefined')
|
||||||
|
{
|
||||||
Math.seedrandom(this.seed);
|
Math.seedrandom(this.seed);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
Math.seedrandom();
|
Math.seedrandom();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.method === TrialHandler.Method.SEQUENTIAL) {
|
if (this.method === TrialHandler.Method.SEQUENTIAL)
|
||||||
|
{
|
||||||
this._trialSequence = Array(this.nReps).fill(indices);
|
this._trialSequence = Array(this.nReps).fill(indices);
|
||||||
// transposed version:
|
// transposed version:
|
||||||
//this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] );
|
//this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] );
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (this.method === TrialHandler.Method.RANDOM) {
|
else if (this.method === TrialHandler.Method.RANDOM)
|
||||||
|
{
|
||||||
this._trialSequence = [];
|
this._trialSequence = [];
|
||||||
for (let i = 0; i < this.nReps; ++i)
|
for (let i = 0; i < this.nReps; ++i)
|
||||||
|
{
|
||||||
this._trialSequence.push(util.shuffle(indices.slice()));
|
this._trialSequence.push(util.shuffle(indices.slice()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else if (this.method === TrialHandler.Method.FULL_RANDOM) {
|
else if (this.method === TrialHandler.Method.FULL_RANDOM)
|
||||||
|
{
|
||||||
// create a flat sequence with nReps repeats of indices:
|
// create a flat sequence with nReps repeats of indices:
|
||||||
let flatSequence = [];
|
let flatSequence = [];
|
||||||
for (let i = 0; i < this.nReps; ++i)
|
for (let i = 0; i < this.nReps; ++i)
|
||||||
|
{
|
||||||
flatSequence.push.apply(flatSequence, indices);
|
flatSequence.push.apply(flatSequence, indices);
|
||||||
|
}
|
||||||
|
|
||||||
// shuffle the sequence:
|
// shuffle the sequence:
|
||||||
util.shuffle(flatSequence);
|
util.shuffle(flatSequence);
|
||||||
@ -517,10 +583,13 @@ export class TrialHandler extends PsychObject {
|
|||||||
// reshape it into the trialSequence:
|
// reshape it into the trialSequence:
|
||||||
this._trialSequence = [];
|
this._trialSequence = [];
|
||||||
for (let i = 0; i < this.nReps; i++)
|
for (let i = 0; i < this.nReps; i++)
|
||||||
|
{
|
||||||
this._trialSequence.push(flatSequence.slice(i * this.nStim, (i + 1) * this.nStim));
|
this._trialSequence.push(flatSequence.slice(i * this.nStim, (i + 1) * this.nStim));
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
throw Object.assign(response, { error: 'unknown method' });
|
else
|
||||||
|
{
|
||||||
|
throw Object.assign(response, {error: 'unknown method'});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._trialSequence;
|
return this._trialSequence;
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
* Sound stimulus.
|
* Sound stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PsychoJS } from '../core/PsychoJS';
|
import {PsychoJS} from '../core/PsychoJS';
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
import { TonePlayer } from './TonePlayer';
|
import {TonePlayer} from './TonePlayer';
|
||||||
import { TrackPlayer } from './TrackPlayer';
|
import {TrackPlayer} from './TrackPlayer';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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.volume= 1.0] - volume of the sound (must be between 0 and 1.0)
|
||||||
* @param {number} [options.loops= 0] - how many times to repeat the track or tone after it has played once. If loops == -1, the track or tone will repeat indefinitely until stopped.
|
* @param {number} [options.loops= 0] - how many times to repeat the track or tone after it has played once. If loops == -1, the track or tone will repeat indefinitely until stopped.
|
||||||
* @param {boolean} [options.autoLog= true] whether or not to log
|
* @param {boolean} [options.autoLog= true] whether or not to log
|
||||||
*/
|
*/
|
||||||
export class Sound extends PsychObject {
|
export class Sound extends PsychObject
|
||||||
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
@ -64,7 +65,8 @@ export class Sound extends PsychObject {
|
|||||||
loops = 0,
|
loops = 0,
|
||||||
//hamming = true,
|
//hamming = true,
|
||||||
autoLog = true
|
autoLog = true
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(win._psychoJS, name);
|
super(win._psychoJS, name);
|
||||||
|
|
||||||
// the SoundPlayer, e.g. TonePlayer:
|
// the SoundPlayer, e.g. TonePlayer:
|
||||||
@ -118,7 +120,8 @@ export class Sound extends PsychObject {
|
|||||||
* @public
|
* @public
|
||||||
* @return {number} the duration of the sound, in seconds
|
* @return {number} the duration of the sound, in seconds
|
||||||
*/
|
*/
|
||||||
getDuration() {
|
getDuration()
|
||||||
|
{
|
||||||
return this._player.getDuration();
|
return this._player.getDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,8 +139,10 @@ export class Sound extends PsychObject {
|
|||||||
this._setAttribute('volume', volume, log);
|
this._setAttribute('volume', volume, log);
|
||||||
|
|
||||||
if (typeof this._player !== 'undefined')
|
if (typeof this._player !== 'undefined')
|
||||||
|
{
|
||||||
this._player.setVolume(volume, mute);
|
this._player.setVolume(volume, mute);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,9 +157,10 @@ export class Sound extends PsychObject {
|
|||||||
this._setAttribute('loops', loops, log);
|
this._setAttribute('loops', loops, log);
|
||||||
|
|
||||||
if (typeof this._player !== 'undefined')
|
if (typeof this._player !== 'undefined')
|
||||||
|
{
|
||||||
this._player.setLoops(loops);
|
this._player.setLoops(loops);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -169,9 +175,10 @@ export class Sound extends PsychObject {
|
|||||||
this._setAttribute('secs', secs, log);
|
this._setAttribute('secs', secs, log);
|
||||||
|
|
||||||
if (typeof this._player !== 'undefined')
|
if (typeof this._player !== 'undefined')
|
||||||
|
{
|
||||||
this._player.setDuration(secs);
|
this._player.setDuration(secs);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -181,19 +188,27 @@ export class Sound extends PsychObject {
|
|||||||
* @return {SoundPlayer} the appropriate SoundPlayer
|
* @return {SoundPlayer} the appropriate SoundPlayer
|
||||||
* @throws {Object.<string, *>} exception if no appropriate SoundPlayer could be found for the sound
|
* @throws {Object.<string, *>} exception if no appropriate SoundPlayer could be found for the sound
|
||||||
*/
|
*/
|
||||||
_getPlayer() {
|
_getPlayer()
|
||||||
|
{
|
||||||
const acceptFns = [
|
const acceptFns = [
|
||||||
sound => TonePlayer.accept(sound),
|
sound => TonePlayer.accept(sound),
|
||||||
sound => TrackPlayer.accept(sound)
|
sound => TrackPlayer.accept(sound)
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const acceptFn of acceptFns) {
|
for (const acceptFn of acceptFns)
|
||||||
|
{
|
||||||
this._player = acceptFn(this);
|
this._player = acceptFn(this);
|
||||||
if (typeof this._player !== 'undefined')
|
if (typeof this._player !== 'undefined')
|
||||||
|
{
|
||||||
return this._player;
|
return this._player;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw { origin: 'SoundPlayer._getPlayer', context: 'when finding a player for the sound', error: 'could not find an appropriate player.' };
|
throw {
|
||||||
|
origin: 'SoundPlayer._getPlayer',
|
||||||
|
context: 'when finding a player for the sound',
|
||||||
|
error: 'could not find an appropriate player.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
* Sound player interface
|
* Sound player interface
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PsychObject } from '../util/PsychObject';
|
import {PsychObject} from '../util/PsychObject';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +39,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
static accept(sound)
|
static accept(sound)
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.accept', context: 'when evaluating whether this player can play a given sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.accept',
|
||||||
|
context: 'when evaluating whether this player can play a given sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -54,7 +58,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
play(loops)
|
play(loops)
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.play', context: 'when starting the playback of a sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.play',
|
||||||
|
context: 'when starting the playback of a sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -68,7 +76,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
stop()
|
stop()
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.stop', context: 'when stopping the playback of a sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.stop',
|
||||||
|
context: 'when stopping the playback of a sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -82,7 +94,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
getDuration()
|
getDuration()
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.getDuration', context: 'when getting the duration of the sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.getDuration',
|
||||||
|
context: 'when getting the duration of the sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -96,11 +112,14 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
setDuration(duration_s)
|
setDuration(duration_s)
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.setDuration', context: 'when setting the duration of the sound', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.setDuration',
|
||||||
|
context: 'when setting the duration of the sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the number of loops.
|
* Set the number of loops.
|
||||||
*
|
*
|
||||||
@ -112,7 +131,11 @@ export class SoundPlayer extends PsychObject
|
|||||||
*/
|
*/
|
||||||
setLoops(loops)
|
setLoops(loops)
|
||||||
{
|
{
|
||||||
throw {origin: 'SoundPlayer.setLoops', context: 'when setting the number of loops', error: 'this method is abstract and should not be called.'};
|
throw {
|
||||||
|
origin: 'SoundPlayer.setLoops',
|
||||||
|
context: 'when setting the number of loops',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -126,8 +149,13 @@ export class SoundPlayer extends PsychObject
|
|||||||
* @param {Integer} volume - the volume of the tone
|
* @param {Integer} volume - the volume of the tone
|
||||||
* @param {boolean} [mute= false] - whether or not to mute the tone
|
* @param {boolean} [mute= false] - whether or not to mute the tone
|
||||||
*/
|
*/
|
||||||
setVolume(volume, mute = false) {
|
setVolume(volume, mute = false)
|
||||||
throw {origin: 'SoundPlayer.setVolume', context: 'when setting the volume of the sound', error: 'this method is abstract and should not be called.'};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'SoundPlayer.setVolume',
|
||||||
|
context: 'when setting the volume of the sound',
|
||||||
|
error: 'this method is abstract and should not be called.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
* Tone Player.
|
* Tone Player.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SoundPlayer } from './SoundPlayer';
|
import {SoundPlayer} from './SoundPlayer';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,7 +84,8 @@ export class TonePlayer extends SoundPlayer
|
|||||||
{
|
{
|
||||||
// mapping between the PsychoPY notes and the standard ones:
|
// mapping between the PsychoPY notes and the standard ones:
|
||||||
let psychopyToToneMap = new Map();
|
let psychopyToToneMap = new Map();
|
||||||
for (const note of ['A', 'B', 'C', 'D', 'E', 'F', 'G']) {
|
for (const note of ['A', 'B', 'C', 'D', 'E', 'F', 'G'])
|
||||||
|
{
|
||||||
psychopyToToneMap.set(note, note);
|
psychopyToToneMap.set(note, note);
|
||||||
psychopyToToneMap.set(note + 'fl', note + 'b');
|
psychopyToToneMap.set(note + 'fl', note + 'b');
|
||||||
psychopyToToneMap.set(note + 'sh', note + '#');
|
psychopyToToneMap.set(note + 'sh', note + '#');
|
||||||
@ -123,7 +124,6 @@ export class TonePlayer extends SoundPlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the duration of the tone.
|
* Set the duration of the tone.
|
||||||
*
|
*
|
||||||
@ -138,7 +138,6 @@ export class TonePlayer extends SoundPlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the number of loops.
|
* Set the number of loops.
|
||||||
*
|
*
|
||||||
@ -197,7 +196,9 @@ export class TonePlayer extends SoundPlayer
|
|||||||
play(loops)
|
play(loops)
|
||||||
{
|
{
|
||||||
if (typeof loops !== 'undefined')
|
if (typeof loops !== 'undefined')
|
||||||
|
{
|
||||||
this._loops = loops;
|
this._loops = loops;
|
||||||
|
}
|
||||||
|
|
||||||
// if duration_s == -1, the sound should play indefinitely, therefore we use an arbitrarily long playing time
|
// if duration_s == -1, the sound should play indefinitely, therefore we use an arbitrarily long playing time
|
||||||
const actualDuration_s = (this._duration_s === -1) ? 1000000 : this._duration_s;
|
const actualDuration_s = (this._duration_s === -1) ? 1000000 : this._duration_s;
|
||||||
@ -250,7 +251,7 @@ export class TonePlayer extends SoundPlayer
|
|||||||
this.duration_s * (this._loops + 1)
|
this.duration_s * (this._loops + 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -269,8 +270,10 @@ export class TonePlayer extends SoundPlayer
|
|||||||
|
|
||||||
// clear the repeat event if need be:
|
// clear the repeat event if need be:
|
||||||
if (this._toneId)
|
if (this._toneId)
|
||||||
|
{
|
||||||
Tone.Transport.clear(this._toneId);
|
Tone.Transport.clear(this._toneId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
const contextCurrentTime = this._audioContext.currentTime;
|
const contextCurrentTime = this._audioContext.currentTime;
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
* Track Player.
|
* Track Player.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SoundPlayer } from './SoundPlayer';
|
import {SoundPlayer} from './SoundPlayer';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,7 +27,8 @@ import { SoundPlayer } from './SoundPlayer';
|
|||||||
* @todo stopTime is currently not implemented (tracks will play from startTime to finish)
|
* @todo stopTime is currently not implemented (tracks will play from startTime to finish)
|
||||||
* @todo stereo is currently not implemented
|
* @todo stereo is currently not implemented
|
||||||
*/
|
*/
|
||||||
export class TrackPlayer extends SoundPlayer {
|
export class TrackPlayer extends SoundPlayer
|
||||||
|
{
|
||||||
constructor({
|
constructor({
|
||||||
psychoJS,
|
psychoJS,
|
||||||
howl,
|
howl,
|
||||||
@ -36,7 +37,8 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
stereo = true,
|
stereo = true,
|
||||||
volume = 0,
|
volume = 0,
|
||||||
loops = 0
|
loops = 0
|
||||||
} = {}) {
|
} = {})
|
||||||
|
{
|
||||||
super(psychoJS);
|
super(psychoJS);
|
||||||
|
|
||||||
this._addAttributes(TrackPlayer, howl, startTime, stopTime, stereo, loops, volume);
|
this._addAttributes(TrackPlayer, howl, startTime, stopTime, stereo, loops, volume);
|
||||||
@ -55,11 +57,14 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
* @param {module:sound.Sound} - the sound
|
* @param {module:sound.Sound} - the sound
|
||||||
* @return {Object|undefined} an instance of TrackPlayer that can play the given sound or undefined otherwise
|
* @return {Object|undefined} an instance of TrackPlayer that can play the given sound or undefined otherwise
|
||||||
*/
|
*/
|
||||||
static accept(sound) {
|
static accept(sound)
|
||||||
|
{
|
||||||
// if the sound's value is a string, we check whether it is the name of a resource:
|
// if the sound's value is a string, we check whether it is the name of a resource:
|
||||||
if (typeof sound.value === 'string') {
|
if (typeof sound.value === 'string')
|
||||||
|
{
|
||||||
const howl = sound.psychoJS.serverManager.getResource(sound.value);
|
const howl = sound.psychoJS.serverManager.getResource(sound.value);
|
||||||
if (typeof howl !== 'undefined') {
|
if (typeof howl !== 'undefined')
|
||||||
|
{
|
||||||
// build the player:
|
// build the player:
|
||||||
const player = new TrackPlayer({
|
const player = new TrackPlayer({
|
||||||
psychoJS: sound.psychoJS,
|
psychoJS: sound.psychoJS,
|
||||||
@ -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 {Integer} volume - the volume of the track (must be between 0 and 1.0)
|
||||||
* @param {boolean} [mute= false] - whether or not to mute the track
|
* @param {boolean} [mute= false] - whether or not to mute the track
|
||||||
*/
|
*/
|
||||||
setVolume(volume, mute = false) {
|
setVolume(volume, mute = false)
|
||||||
|
{
|
||||||
this._volume = volume;
|
this._volume = volume;
|
||||||
|
|
||||||
this._howl.volume(volume);
|
this._howl.volume(volume);
|
||||||
@ -124,10 +130,14 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
this._currentLoopIndex = -1;
|
this._currentLoopIndex = -1;
|
||||||
|
|
||||||
if (loops === 0)
|
if (loops === 0)
|
||||||
|
{
|
||||||
this._howl.loop(false);
|
this._howl.loop(false);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
this._howl.loop(true);
|
this._howl.loop(true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -138,18 +148,26 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
* @public
|
* @public
|
||||||
* @param {number} loops - how many times to repeat the track after it has played once. If loops == -1, the track will repeat indefinitely until stopped.
|
* @param {number} loops - how many times to repeat the track after it has played once. If loops == -1, the track will repeat indefinitely until stopped.
|
||||||
*/
|
*/
|
||||||
play(loops) {
|
play(loops)
|
||||||
|
{
|
||||||
if (typeof loops !== 'undefined')
|
if (typeof loops !== 'undefined')
|
||||||
|
{
|
||||||
this.setLoops(loops);
|
this.setLoops(loops);
|
||||||
|
}
|
||||||
|
|
||||||
// handle repeats:
|
// handle repeats:
|
||||||
if (loops > 0) {
|
if (loops > 0)
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
this._howl.on('end', (event) => {
|
this._howl.on('end', (event) =>
|
||||||
|
{
|
||||||
++this._currentLoopIndex;
|
++this._currentLoopIndex;
|
||||||
if (self._currentLoopIndex > self._loops)
|
if (self._currentLoopIndex > self._loops)
|
||||||
|
{
|
||||||
self.stop();
|
self.stop();
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
self._howl.seek(self._startTime);
|
self._howl.seek(self._startTime);
|
||||||
self._howl.play();
|
self._howl.play();
|
||||||
}
|
}
|
||||||
@ -168,7 +186,8 @@ export class TrackPlayer extends SoundPlayer {
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop()
|
||||||
|
{
|
||||||
this._howl.stop();
|
this._howl.stop();
|
||||||
this._howl.off('end');
|
this._howl.off('end');
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Clock component.
|
* Clock component.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -15,8 +15,10 @@
|
|||||||
* @class
|
* @class
|
||||||
* @param {number} [startTime= <time elapsed since the reference point, i.e. the time when the module was loaded>] - the clock's start time (in ms)
|
* @param {number} [startTime= <time elapsed since the reference point, i.e. the time when the module was loaded>] - the clock's start time (in ms)
|
||||||
*/
|
*/
|
||||||
export class MonotonicClock {
|
export class MonotonicClock
|
||||||
constructor(startTime = MonotonicClock.getReferenceTime()) {
|
{
|
||||||
|
constructor(startTime = MonotonicClock.getReferenceTime())
|
||||||
|
{
|
||||||
this._timeAtLastReset = startTime;
|
this._timeAtLastReset = startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +31,8 @@ export class MonotonicClock {
|
|||||||
* @public
|
* @public
|
||||||
* @return {number} the current time (in seconds)
|
* @return {number} the current time (in seconds)
|
||||||
*/
|
*/
|
||||||
getTime() {
|
getTime()
|
||||||
|
{
|
||||||
return MonotonicClock.getReferenceTime() - this._timeAtLastReset;
|
return MonotonicClock.getReferenceTime() - this._timeAtLastReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +45,8 @@ export class MonotonicClock {
|
|||||||
* @public
|
* @public
|
||||||
* @return {number} the offset (in seconds)
|
* @return {number} the offset (in seconds)
|
||||||
*/
|
*/
|
||||||
getLastResetTime() {
|
getLastResetTime()
|
||||||
|
{
|
||||||
return this._timeAtLastReset;
|
return this._timeAtLastReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +94,7 @@ export class MonotonicClock {
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
MonotonicClock._referenceTime = performance.now() / 1000.0;
|
MonotonicClock._referenceTime = performance.now() / 1000.0;
|
||||||
|
|
||||||
// MonotonicClock._referenceTime = new Date().getTime() / 1000.0;
|
// MonotonicClock._referenceTime = new Date().getTime() / 1000.0;
|
||||||
|
|
||||||
|
|
||||||
@ -100,8 +105,10 @@ MonotonicClock._referenceTime = performance.now() / 1000.0;
|
|||||||
* @class
|
* @class
|
||||||
* @extends MonotonicClock
|
* @extends MonotonicClock
|
||||||
*/
|
*/
|
||||||
export class Clock extends MonotonicClock {
|
export class Clock extends MonotonicClock
|
||||||
constructor() {
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +121,8 @@ export class Clock extends MonotonicClock {
|
|||||||
* @public
|
* @public
|
||||||
* @param {number} [newTime= 0] the new time on the clock.
|
* @param {number} [newTime= 0] the new time on the clock.
|
||||||
*/
|
*/
|
||||||
reset(newTime = 0) {
|
reset(newTime = 0)
|
||||||
|
{
|
||||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +138,8 @@ export class Clock extends MonotonicClock {
|
|||||||
* @public
|
* @public
|
||||||
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||||
*/
|
*/
|
||||||
add(deltaTime) {
|
add(deltaTime)
|
||||||
|
{
|
||||||
this._timeAtLastReset += deltaTime;
|
this._timeAtLastReset += deltaTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,13 +153,16 @@ export class Clock extends MonotonicClock {
|
|||||||
* @extends Clock
|
* @extends Clock
|
||||||
* @param {number} [startTime= 0] - the start time of the countdown
|
* @param {number} [startTime= 0] - the start time of the countdown
|
||||||
*/
|
*/
|
||||||
export class CountdownTimer extends Clock {
|
export class CountdownTimer extends Clock
|
||||||
constructor(startTime = 0) {
|
{
|
||||||
|
constructor(startTime = 0)
|
||||||
|
{
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._timeAtLastReset = MonotonicClock.getReferenceTime();
|
this._timeAtLastReset = MonotonicClock.getReferenceTime();
|
||||||
this._countdown_duration = startTime;
|
this._countdown_duration = startTime;
|
||||||
if (startTime) {
|
if (startTime)
|
||||||
|
{
|
||||||
this.add(startTime);
|
this.add(startTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,7 +179,8 @@ export class CountdownTimer extends Clock {
|
|||||||
* @public
|
* @public
|
||||||
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
* @param {number} [deltaTime] the time to be added to the clock's start time (t0)
|
||||||
*/
|
*/
|
||||||
add(deltaTime) {
|
add(deltaTime)
|
||||||
|
{
|
||||||
this._timeAtLastReset += deltaTime;
|
this._timeAtLastReset += deltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
* @param {number} [newTime] - if newTime is undefined, the countdown time is reset to zero, otherwise we set it
|
||||||
* to newTime
|
* to newTime
|
||||||
*/
|
*/
|
||||||
reset(newTime = undefined) {
|
reset(newTime = undefined)
|
||||||
if (typeof newTime == 'undefined') {
|
{
|
||||||
|
if (typeof newTime == 'undefined')
|
||||||
|
{
|
||||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + this._countdown_duration;
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + this._countdown_duration;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
this._countdown_duration = newTime;
|
this._countdown_duration = newTime;
|
||||||
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
|
||||||
}
|
}
|
||||||
@ -200,7 +216,8 @@ export class CountdownTimer extends Clock {
|
|||||||
* @public
|
* @public
|
||||||
* @return {number} the time left on the countdown (in seconds)
|
* @return {number} the time left on the countdown (in seconds)
|
||||||
*/
|
*/
|
||||||
getTime() {
|
getTime()
|
||||||
|
{
|
||||||
return this._timeAtLastReset - MonotonicClock.getReferenceTime();
|
return this._timeAtLastReset - MonotonicClock.getReferenceTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
171
js/util/Color.js
171
js/util/Color.js
@ -2,7 +2,7 @@
|
|||||||
* Color management.
|
* Color management.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -30,27 +30,38 @@
|
|||||||
*
|
*
|
||||||
* @todo implement HSV, DKL, and LMS colorspaces
|
* @todo implement HSV, DKL, and LMS colorspaces
|
||||||
*/
|
*/
|
||||||
export class Color {
|
export class Color
|
||||||
|
{
|
||||||
|
|
||||||
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB) {
|
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB)
|
||||||
const response = { origin: 'Color', context: 'when defining a color' };
|
{
|
||||||
|
const response = {origin: 'Color', context: 'when defining a color'};
|
||||||
|
|
||||||
// named color (e.g. 'seagreen') or string hexadecimal representation (e.g. '#FF0000'):
|
// named color (e.g. 'seagreen') or string hexadecimal representation (e.g. '#FF0000'):
|
||||||
// note: we expect the color space to be RGB
|
// note: we expect the color space to be RGB
|
||||||
if (typeof obj == 'string') {
|
if (typeof obj == 'string')
|
||||||
|
{
|
||||||
if (colorspace !== Color.COLOR_SPACE.RGB)
|
if (colorspace !== Color.COLOR_SPACE.RGB)
|
||||||
throw Object.assign(response, { error: 'the colorspace must be RGB for' +
|
{
|
||||||
|
throw Object.assign(response, {
|
||||||
|
error: 'the colorspace must be RGB for' +
|
||||||
' a' +
|
' a' +
|
||||||
' named color' });
|
' named color'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// hexademical representation:
|
// hexademical representation:
|
||||||
if (obj[0] === '#') {
|
if (obj[0] === '#')
|
||||||
|
{
|
||||||
this._hex = obj;
|
this._hex = obj;
|
||||||
}
|
}
|
||||||
// named color:
|
// named color:
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
if (!(obj.toLowerCase() in Color.NAMED_COLORS))
|
if (!(obj.toLowerCase() in Color.NAMED_COLORS))
|
||||||
throw Object.assign(response, { error: 'unknown named color: ' + obj });
|
{
|
||||||
|
throw Object.assign(response, {error: 'unknown named color: ' + obj});
|
||||||
|
}
|
||||||
|
|
||||||
this._hex = Color.NAMED_COLORS[obj.toLowerCase()];
|
this._hex = Color.NAMED_COLORS[obj.toLowerCase()];
|
||||||
}
|
}
|
||||||
@ -60,22 +71,29 @@ export class Color {
|
|||||||
|
|
||||||
// hexadecimal number representation (e.g. 0xFF0000)
|
// hexadecimal number representation (e.g. 0xFF0000)
|
||||||
// note: we expect the color space to be RGB
|
// note: we expect the color space to be RGB
|
||||||
else if (typeof obj == 'number') {
|
else if (typeof obj == 'number')
|
||||||
|
{
|
||||||
if (colorspace !== Color.COLOR_SPACE.RGB)
|
if (colorspace !== Color.COLOR_SPACE.RGB)
|
||||||
throw Object.assign(response, { error: 'the colorspace must be RGB for' +
|
{
|
||||||
|
throw Object.assign(response, {
|
||||||
|
error: 'the colorspace must be RGB for' +
|
||||||
' a' +
|
' a' +
|
||||||
' named color' });
|
' named color'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this._rgb = Color._intToRgb(obj);
|
this._rgb = Color._intToRgb(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// array of numbers:
|
// array of numbers:
|
||||||
else if (Array.isArray(obj)) {
|
else if (Array.isArray(obj))
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(obj);
|
Color._checkTypeAndRange(obj);
|
||||||
let [a, b, c] = obj;
|
let [a, b, c] = obj;
|
||||||
|
|
||||||
// check range and convert to [0,1]:
|
// check range and convert to [0,1]:
|
||||||
if (colorspace !== Color.COLOR_SPACE.RGB255) {
|
if (colorspace !== Color.COLOR_SPACE.RGB255)
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(obj, [-1, 1]);
|
Color._checkTypeAndRange(obj, [-1, 1]);
|
||||||
|
|
||||||
a = (a + 1.0) / 2.0;
|
a = (a + 1.0) / 2.0;
|
||||||
@ -84,7 +102,8 @@ export class Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get RGB components:
|
// get RGB components:
|
||||||
switch (colorspace) {
|
switch (colorspace)
|
||||||
|
{
|
||||||
case Color.COLOR_SPACE.RGB255:
|
case Color.COLOR_SPACE.RGB255:
|
||||||
Color._checkTypeAndRange(obj, [0, 255]);
|
Color._checkTypeAndRange(obj, [0, 255]);
|
||||||
this._rgb = [a / 255.0, b / 255.0, c / 255.0];
|
this._rgb = [a / 255.0, b / 255.0, c / 255.0];
|
||||||
@ -104,7 +123,7 @@ export class Color {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unknown colorspace: ' + colorspace });
|
throw Object.assign(response, {error: 'unknown colorspace: ' + colorspace});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -119,7 +138,10 @@ export class Color {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
||||||
*/
|
*/
|
||||||
get rgb() { return this._rgb; }
|
get rgb()
|
||||||
|
{
|
||||||
|
return this._rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -130,7 +152,10 @@ export class Color {
|
|||||||
* @public
|
* @public
|
||||||
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
||||||
*/
|
*/
|
||||||
get rgb255() { return [Math.round(this._rgb[0] * 255.0), Math.round(this._rgb[1] * 255.0), Math.round(this._rgb[2] * 255.0)]; }
|
get rgb255()
|
||||||
|
{
|
||||||
|
return [Math.round(this._rgb[0] * 255.0), Math.round(this._rgb[1] * 255.0), Math.round(this._rgb[2] * 255.0)];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -141,9 +166,12 @@ export class Color {
|
|||||||
* @public
|
* @public
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
get hex() {
|
get hex()
|
||||||
|
{
|
||||||
if (typeof this._hex === 'undefined')
|
if (typeof this._hex === 'undefined')
|
||||||
|
{
|
||||||
this._hex = Color._rgbToHex(this._rgb);
|
this._hex = Color._rgbToHex(this._rgb);
|
||||||
|
}
|
||||||
return this._hex;
|
return this._hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,9 +183,12 @@ export class Color {
|
|||||||
* @public
|
* @public
|
||||||
* @return {number} the integer code equivalent
|
* @return {number} the integer code equivalent
|
||||||
*/
|
*/
|
||||||
get int() {
|
get int()
|
||||||
|
{
|
||||||
if (typeof this._int === 'undefined')
|
if (typeof this._int === 'undefined')
|
||||||
|
{
|
||||||
this._int = Color._rgbToInt(this._rgb);
|
this._int = Color._rgbToInt(this._rgb);
|
||||||
|
}
|
||||||
return this._int;
|
return this._int;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,10 +236,17 @@ export class Color {
|
|||||||
* @param {string} hex - the hexadecimal color code
|
* @param {string} hex - the hexadecimal color code
|
||||||
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
* @return {Array.<number>} the [0,255] RGB triplet equivalent
|
||||||
*/
|
*/
|
||||||
static hexToRgb255(hex) {
|
static hexToRgb255(hex)
|
||||||
|
{
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
throw { origin: 'Color.hexToRgb255', context: 'when converting an hexadecimal color code to its 255- or [0,1]-based RGB color representation', error: 'unable to parse the argument: wrong type or wrong code' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'Color.hexToRgb255',
|
||||||
|
context: 'when converting an hexadecimal color code to its 255- or [0,1]-based RGB color representation',
|
||||||
|
error: 'unable to parse the argument: wrong type or wrong code'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
|
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
|
||||||
}
|
}
|
||||||
@ -224,7 +262,8 @@ export class Color {
|
|||||||
* @param {string} hex - the hexadecimal color code
|
* @param {string} hex - the hexadecimal color code
|
||||||
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
* @return {Array.<number>} the [0,1] RGB triplet equivalent
|
||||||
*/
|
*/
|
||||||
static hexToRgb(hex) {
|
static hexToRgb(hex)
|
||||||
|
{
|
||||||
const [r255, g255, b255] = Color.hexToRgb255(hex);
|
const [r255, g255, b255] = Color.hexToRgb255(hex);
|
||||||
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
||||||
}
|
}
|
||||||
@ -240,15 +279,21 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
static rgb255ToHex(rgb255) {
|
static rgb255ToHex(rgb255)
|
||||||
const response = { origin : 'Color.rgb255ToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'Color.rgb255ToHex',
|
||||||
|
context: 'when converting an rgb triplet to its hexadecimal color representation'
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(rgb255, [0, 255]);
|
Color._checkTypeAndRange(rgb255, [0, 255]);
|
||||||
return Color._rgb255ToHex(rgb255);
|
return Color._rgb255ToHex(rgb255);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,15 +308,21 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
static rgbToHex(rgb) {
|
static rgbToHex(rgb)
|
||||||
const response = { origin : 'Color.rgbToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'Color.rgbToHex',
|
||||||
|
context: 'when converting an rgb triplet to its hexadecimal color representation'
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(rgb, [0, 1]);
|
Color._checkTypeAndRange(rgb, [0, 1]);
|
||||||
return Color._rgbToHex(rgb);
|
return Color._rgbToHex(rgb);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,15 +337,21 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
* @return {number} the integer equivalent
|
* @return {number} the integer equivalent
|
||||||
*/
|
*/
|
||||||
static rgbToInt(rgb) {
|
static rgbToInt(rgb)
|
||||||
const response = { origin : 'Color.rgbToInt', context: 'when converting an rgb triplet to its integer representation' };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'Color.rgbToInt',
|
||||||
|
context: 'when converting an rgb triplet to its integer representation'
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(rgb, [0, 1]);
|
Color._checkTypeAndRange(rgb, [0, 1]);
|
||||||
return Color._rgbToInt(rgb);
|
return Color._rgbToInt(rgb);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,14 +366,20 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
* @return {number} the integer equivalent
|
* @return {number} the integer equivalent
|
||||||
*/
|
*/
|
||||||
static rgb255ToInt(rgb255) {
|
static rgb255ToInt(rgb255)
|
||||||
const response = { origin : 'Color.rgb255ToInt', context: 'when converting an rgb triplet to its integer representation' };
|
{
|
||||||
try {
|
const response = {
|
||||||
|
origin: 'Color.rgb255ToInt',
|
||||||
|
context: 'when converting an rgb triplet to its integer representation'
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
Color._checkTypeAndRange(rgb255, [0, 255]);
|
Color._checkTypeAndRange(rgb255, [0, 255]);
|
||||||
return Color._rgb255ToInt(rgb255);
|
return Color._rgb255ToInt(rgb255);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,7 +396,8 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
static _rgb255ToHex(rgb255) {
|
static _rgb255ToHex(rgb255)
|
||||||
|
{
|
||||||
return "#" + ((1 << 24) + (rgb255[0] << 16) + (rgb255[1] << 8) + rgb255[2]).toString(16).slice(1);
|
return "#" + ((1 << 24) + (rgb255[0] << 16) + (rgb255[1] << 8) + rgb255[2]).toString(16).slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,7 +414,8 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
* @return {string} the hexadecimal color code equivalent
|
* @return {string} the hexadecimal color code equivalent
|
||||||
*/
|
*/
|
||||||
static _rgbToHex(rgb) {
|
static _rgbToHex(rgb)
|
||||||
|
{
|
||||||
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
||||||
return Color._rgb255ToHex(rgb255);
|
return Color._rgb255ToHex(rgb255);
|
||||||
}
|
}
|
||||||
@ -368,7 +433,8 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
|
||||||
* @return {number} the integer equivalent
|
* @return {number} the integer equivalent
|
||||||
*/
|
*/
|
||||||
static _rgbToInt(rgb) {
|
static _rgbToInt(rgb)
|
||||||
|
{
|
||||||
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
|
||||||
return Color._rgb255ToInt(rgb255);
|
return Color._rgb255ToInt(rgb255);
|
||||||
}
|
}
|
||||||
@ -386,7 +452,8 @@ export class Color {
|
|||||||
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
|
||||||
* @return {number} the integer equivalent
|
* @return {number} the integer equivalent
|
||||||
*/
|
*/
|
||||||
static _rgb255ToInt(rgb255) {
|
static _rgb255ToInt(rgb255)
|
||||||
|
{
|
||||||
return rgb255[0] * 0x10000 + rgb255[1] * 0x100 + rgb255[2];
|
return rgb255[0] * 0x10000 + rgb255[1] * 0x100 + rgb255[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,7 +470,8 @@ export class Color {
|
|||||||
* @param {number} hex - the integer color code
|
* @param {number} hex - the integer color code
|
||||||
* @return {Array.<number>} the [0, 255] RGB equivalent
|
* @return {Array.<number>} the [0, 255] RGB equivalent
|
||||||
*/
|
*/
|
||||||
static _intToRgb255(hex) {
|
static _intToRgb255(hex)
|
||||||
|
{
|
||||||
const r255 = hex >>> 0x10;
|
const r255 = hex >>> 0x10;
|
||||||
const g255 = (hex & 0xFF00) / 0x100;
|
const g255 = (hex & 0xFF00) / 0x100;
|
||||||
const b255 = hex & 0xFF;
|
const b255 = hex & 0xFF;
|
||||||
@ -424,7 +492,8 @@ export class Color {
|
|||||||
* @param {number} hex - the integer color code
|
* @param {number} hex - the integer color code
|
||||||
* @return {Array.<number>} the [0, 1] RGB equivalent
|
* @return {Array.<number>} the [0, 1] RGB equivalent
|
||||||
*/
|
*/
|
||||||
static _intToRgb(hex) {
|
static _intToRgb(hex)
|
||||||
|
{
|
||||||
const [r255, g255, b255] = Color._intToRgb255(hex);
|
const [r255, g255, b255] = Color._intToRgb255(hex);
|
||||||
|
|
||||||
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
|
||||||
@ -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]))
|
if (typeof range !== 'undefined' && (arg[0] < range[0] || arg[0] > range[1] || arg[1] < range[0] || arg[1] > range[1] || arg[2] < range[0] || arg[2] > range[1]))
|
||||||
|
{
|
||||||
throw 'the color components should all belong to [' + range[0] + ', ' + range[1] + ']';
|
throw 'the color components should all belong to [' + range[0] + ', ' + range[1] + ']';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
* Color Mixin.
|
* Color Mixin.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { Color } from './Color';
|
import {Color} from './Color';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,8 +17,10 @@ import { Color } from './Color';
|
|||||||
* @name module:util.ColorMixin
|
* @name module:util.ColorMixin
|
||||||
* @mixin
|
* @mixin
|
||||||
*/
|
*/
|
||||||
export let ColorMixin = (superclass) => class extends superclass {
|
export let ColorMixin = (superclass) => class extends superclass
|
||||||
constructor(args) {
|
{
|
||||||
|
constructor(args)
|
||||||
|
{
|
||||||
super(args);
|
super(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +34,8 @@ export let ColorMixin = (superclass) => class extends superclass {
|
|||||||
* @param {Color} color - the new color
|
* @param {Color} color - the new color
|
||||||
* @param {boolean} [log= false] - whether or not to log
|
* @param {boolean} [log= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
setColor(color, log) {
|
setColor(color, log)
|
||||||
|
{
|
||||||
this._setAttribute('color', color, log);
|
this._setAttribute('color', color, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -48,7 +51,8 @@ export let ColorMixin = (superclass) => class extends superclass {
|
|||||||
* @param {number} contrast - the new contrast (must be between 0 and 1)
|
* @param {number} contrast - the new contrast (must be between 0 and 1)
|
||||||
* @param {boolean} [log= false] - whether or not to log
|
* @param {boolean} [log= false] - whether or not to log
|
||||||
*/
|
*/
|
||||||
setContrast(contrast, log) {
|
setContrast(contrast, log)
|
||||||
|
{
|
||||||
this._setAttribute('contrast', contrast, log);
|
this._setAttribute('contrast', contrast, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -64,7 +68,8 @@ export let ColorMixin = (superclass) => class extends superclass {
|
|||||||
* @param {string|number|Array.<number>} color - the color
|
* @param {string|number|Array.<number>} color - the color
|
||||||
* @param {number} contrast - the contrast (must be between 0 and 1)
|
* @param {number} contrast - the contrast (must be between 0 and 1)
|
||||||
*/
|
*/
|
||||||
getContrastedColor(color, contrast) {
|
getContrastedColor(color, contrast)
|
||||||
|
{
|
||||||
const rgb = color.rgb.map(c => (c * 2.0 - 1.0) * contrast);
|
const rgb = color.rgb.map(c => (c * 2.0 - 1.0) * contrast);
|
||||||
return new Color(rgb, Color.COLOR_SPACE.RGB);
|
return new Color(rgb, Color.COLOR_SPACE.RGB);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Event Emitter.
|
* Event Emitter.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -57,14 +57,18 @@ export class EventEmitter
|
|||||||
{
|
{
|
||||||
// check that the listener is a function:
|
// check that the listener is a function:
|
||||||
if (typeof listener !== 'function')
|
if (typeof listener !== 'function')
|
||||||
|
{
|
||||||
throw new TypeError('listener must be a function');
|
throw new TypeError('listener must be a function');
|
||||||
|
}
|
||||||
|
|
||||||
// generate a new uuid:
|
// generate a new uuid:
|
||||||
let uuid = util.makeUuid();
|
let uuid = util.makeUuid();
|
||||||
|
|
||||||
// add the listener to the event map:
|
// add the listener to the event map:
|
||||||
if (!this._listeners.has(name))
|
if (!this._listeners.has(name))
|
||||||
|
{
|
||||||
this._listeners.set(name, []);
|
this._listeners.set(name, []);
|
||||||
|
}
|
||||||
this._listeners.get(name).push({uuid, listener});
|
this._listeners.get(name).push({uuid, listener});
|
||||||
|
|
||||||
return uuid;
|
return uuid;
|
||||||
@ -86,7 +90,9 @@ export class EventEmitter
|
|||||||
let uuid = this.on(name, listener);
|
let uuid = this.on(name, listener);
|
||||||
|
|
||||||
if (!this._onceUuids.has(name))
|
if (!this._onceUuids.has(name))
|
||||||
|
{
|
||||||
this._onceUuids.set(name, []);
|
this._onceUuids.set(name, []);
|
||||||
|
}
|
||||||
this._onceUuids.get(name).push(uuid);
|
this._onceUuids.get(name).push(uuid);
|
||||||
|
|
||||||
return uuid;
|
return uuid;
|
||||||
@ -106,8 +112,9 @@ export class EventEmitter
|
|||||||
{
|
{
|
||||||
let relevantUuidListeners = this._listeners.get(name);
|
let relevantUuidListeners = this._listeners.get(name);
|
||||||
|
|
||||||
if (relevantUuidListeners && relevantUuidListeners.length) {
|
if (relevantUuidListeners && relevantUuidListeners.length)
|
||||||
this._listeners.set(name, relevantUuidListeners.filter( uuidlistener => (uuidlistener.uuid != uuid) ) );
|
{
|
||||||
|
this._listeners.set(name, relevantUuidListeners.filter(uuidlistener => (uuidlistener.uuid != uuid)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -131,11 +138,14 @@ export class EventEmitter
|
|||||||
{
|
{
|
||||||
let onceUuids = this._onceUuids.get(name);
|
let onceUuids = this._onceUuids.get(name);
|
||||||
let self = this;
|
let self = this;
|
||||||
relevantUuidListeners.forEach( ({uuid, listener}) => {
|
relevantUuidListeners.forEach(({uuid, listener}) =>
|
||||||
|
{
|
||||||
listener(data);
|
listener(data);
|
||||||
|
|
||||||
if (typeof onceUuids !== 'undefined' && onceUuids.includes(uuid))
|
if (typeof onceUuids !== 'undefined' && onceUuids.includes(uuid))
|
||||||
|
{
|
||||||
self.off(name, uuid);
|
self.off(name, uuid);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,13 @@
|
|||||||
* Core Object.
|
* Core Object.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { EventEmitter } from './EventEmitter';
|
import {EventEmitter} from './EventEmitter';
|
||||||
import * as util from './Util';
|
import * as util from './Util';
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +33,9 @@ export class PsychObject extends EventEmitter
|
|||||||
|
|
||||||
// name:
|
// name:
|
||||||
if (typeof name === 'undefined')
|
if (typeof name === 'undefined')
|
||||||
|
{
|
||||||
name = this.constructor.name;
|
name = this.constructor.name;
|
||||||
|
}
|
||||||
this._addAttribute('name', name);
|
this._addAttribute('name', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,18 +79,24 @@ export class PsychObject extends EventEmitter
|
|||||||
for (const attribute of this._userAttributes)
|
for (const attribute of this._userAttributes)
|
||||||
{
|
{
|
||||||
if (addComma)
|
if (addComma)
|
||||||
|
{
|
||||||
representation += ', ';
|
representation += ', ';
|
||||||
|
}
|
||||||
addComma = true;
|
addComma = true;
|
||||||
|
|
||||||
let value = util.toString(this['_'+attribute]);
|
let value = util.toString(this['_' + attribute]);
|
||||||
const l = value.length;
|
const l = value.length;
|
||||||
if (l > 50)
|
if (l > 50)
|
||||||
{
|
{
|
||||||
if (value[l-1] === ')')
|
if (value[l - 1] === ')')
|
||||||
|
{
|
||||||
value = value.substring(0, 50) + '~)';
|
value = value.substring(0, 50) + '~)';
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
value = value.substring(0, 50) + '~';
|
value = value.substring(0, 50) + '~';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
representation += attribute + '=' + value;
|
representation += attribute + '=' + value;
|
||||||
}
|
}
|
||||||
@ -98,7 +106,6 @@ export class PsychObject extends EventEmitter
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value of an attribute.
|
* Set the value of an attribute.
|
||||||
*
|
*
|
||||||
@ -113,32 +120,46 @@ export class PsychObject extends EventEmitter
|
|||||||
*/
|
*/
|
||||||
_setAttribute(attributeName, attributeValue, log = false, operation = undefined, stealth = false)
|
_setAttribute(attributeName, attributeValue, log = false, operation = undefined, stealth = false)
|
||||||
{
|
{
|
||||||
const response = { origin: 'PsychObject.setAttribute', context: 'when setting the attribute of an object' };
|
const response = {origin: 'PsychObject.setAttribute', context: 'when setting the attribute of an object'};
|
||||||
|
|
||||||
if (typeof attributeName == 'undefined')
|
if (typeof attributeName == 'undefined')
|
||||||
throw Object.assign(response, { error: 'the attribute name cannot be' +
|
{
|
||||||
' undefined' });
|
throw Object.assign(response, {
|
||||||
if (typeof attributeValue == 'undefined') {
|
error: 'the attribute name cannot be' +
|
||||||
|
' undefined'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (typeof attributeValue == 'undefined')
|
||||||
|
{
|
||||||
this._psychoJS.logger.warn('setting the value of attribute: ' + attributeName + ' in PsychObject: ' + this._name + ' as: undefined');
|
this._psychoJS.logger.warn('setting the value of attribute: ' + attributeName + ' in PsychObject: ' + this._name + ' as: undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
// (*) apply operation to old and new values:
|
// (*) apply operation to old and new values:
|
||||||
if (typeof operation !== 'undefined' && this.hasOwnProperty('_' + attributeName)) {
|
if (typeof operation !== 'undefined' && this.hasOwnProperty('_' + attributeName))
|
||||||
|
{
|
||||||
let oldValue = this['_' + attributeName];
|
let oldValue = this['_' + attributeName];
|
||||||
|
|
||||||
// operations can only be applied to numbers and array of numbers (which can be empty):
|
// operations can only be applied to numbers and array of numbers (which can be empty):
|
||||||
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == 'number'))) {
|
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == 'number')))
|
||||||
|
{
|
||||||
|
|
||||||
// value is an array:
|
// value is an array:
|
||||||
if (Array.isArray(attributeValue)) {
|
if (Array.isArray(attributeValue))
|
||||||
|
{
|
||||||
// old value is also an array
|
// old value is also an array
|
||||||
if (Array.isArray(oldValue)) {
|
if (Array.isArray(oldValue))
|
||||||
|
{
|
||||||
if (attributeValue.length !== oldValue.length)
|
if (attributeValue.length !== oldValue.length)
|
||||||
throw Object.assign(response, { error: 'old and new' +
|
{
|
||||||
|
throw Object.assign(response, {
|
||||||
|
error: 'old and new' +
|
||||||
' value should have' +
|
' value should have' +
|
||||||
' the same size when they are both arrays' });
|
' the same size when they are both arrays'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
switch (operation) {
|
switch (operation)
|
||||||
|
{
|
||||||
case '':
|
case '':
|
||||||
// no change to value;
|
// no change to value;
|
||||||
break;
|
break;
|
||||||
@ -161,14 +182,18 @@ export class PsychObject extends EventEmitter
|
|||||||
attributeValue = attributeValue.map((v, i) => oldValue[i] % v);
|
attributeValue = attributeValue.map((v, i) => oldValue[i] % v);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unsupported' +
|
throw Object.assign(response, {
|
||||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
error: 'unsupported' +
|
||||||
|
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
// old value is a scalar
|
// old value is a scalar
|
||||||
{
|
{
|
||||||
switch (operation) {
|
switch (operation)
|
||||||
|
{
|
||||||
case '':
|
case '':
|
||||||
// no change to value;
|
// no change to value;
|
||||||
break;
|
break;
|
||||||
@ -191,17 +216,22 @@ export class PsychObject extends EventEmitter
|
|||||||
attributeValue = attributeValue.map(v => oldValue % v);
|
attributeValue = attributeValue.map(v => oldValue % v);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unsupported' +
|
throw Object.assign(response, {
|
||||||
|
error: 'unsupported' +
|
||||||
' value: ' + JSON.stringify(attributeValue) + ' for' +
|
' 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
|
// value is a scalar
|
||||||
{
|
{
|
||||||
// old value is an array
|
// old value is an array
|
||||||
if (Array.isArray(oldValue)) {
|
if (Array.isArray(oldValue))
|
||||||
switch (operation) {
|
{
|
||||||
|
switch (operation)
|
||||||
|
{
|
||||||
case '':
|
case '':
|
||||||
attributeValue = oldValue.map(v => attributeValue);
|
attributeValue = oldValue.map(v => attributeValue);
|
||||||
break;
|
break;
|
||||||
@ -224,14 +254,18 @@ export class PsychObject extends EventEmitter
|
|||||||
attributeValue = oldValue.map(v => v % attributeValue);
|
attributeValue = oldValue.map(v => v % attributeValue);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unsupported' +
|
throw Object.assign(response, {
|
||||||
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
error: 'unsupported' +
|
||||||
|
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
// old value is a scalar
|
// old value is a scalar
|
||||||
{
|
{
|
||||||
switch (operation) {
|
switch (operation)
|
||||||
|
{
|
||||||
case '':
|
case '':
|
||||||
// no change to value;
|
// no change to value;
|
||||||
break;
|
break;
|
||||||
@ -254,14 +288,19 @@ export class PsychObject extends EventEmitter
|
|||||||
attributeValue = oldValue % attributeValue;
|
attributeValue = oldValue % attributeValue;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Object.assign(response, { error: 'unsupported' +
|
throw Object.assign(response, {
|
||||||
' value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name });
|
error: 'unsupported' +
|
||||||
|
' value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
}
|
||||||
throw Object.assign(response, { error: 'operation: ' + operation + ' is invalid for old value: ' + JSON.stringify(oldValue) + ' and new value: ' + JSON.stringify(attributeValue) });
|
else
|
||||||
|
{
|
||||||
|
throw Object.assign(response, {error: 'operation: ' + operation + ' is invalid for old value: ' + JSON.stringify(oldValue) + ' and new value: ' + JSON.stringify(attributeValue)});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -313,12 +352,16 @@ export class PsychObject extends EventEmitter
|
|||||||
// (*) add (argument name, argument value) pairs to the attribute map:
|
// (*) add (argument name, argument value) pairs to the attribute map:
|
||||||
let attributeMap = new Map();
|
let attributeMap = new Map();
|
||||||
for (let i = 1; i < callArgs.length; ++i)
|
for (let i = 1; i < callArgs.length; ++i)
|
||||||
|
{
|
||||||
attributeMap.set(callArgs[i], args[i - 1]);
|
attributeMap.set(callArgs[i], args[i - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
// (*) set the value, define the get/set<attributeName> properties and define the getter and setter:
|
// (*) set the value, define the get/set<attributeName> properties and define the getter and setter:
|
||||||
for (let [name, value] of attributeMap.entries())
|
for (let [name, value] of attributeMap.entries())
|
||||||
|
{
|
||||||
this._addAttribute(name, value);
|
this._addAttribute(name, value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -332,18 +375,29 @@ export class PsychObject extends EventEmitter
|
|||||||
{
|
{
|
||||||
const getPropertyName = 'get' + name[0].toUpperCase() + name.substr(1);
|
const getPropertyName = 'get' + name[0].toUpperCase() + name.substr(1);
|
||||||
if (typeof this[getPropertyName] === 'undefined')
|
if (typeof this[getPropertyName] === 'undefined')
|
||||||
|
{
|
||||||
this[getPropertyName] = () => this['_' + name];
|
this[getPropertyName] = () => this['_' + name];
|
||||||
|
}
|
||||||
|
|
||||||
const setPropertyName = 'set' + name[0].toUpperCase() + name.substr(1);
|
const setPropertyName = 'set' + name[0].toUpperCase() + name.substr(1);
|
||||||
if (typeof this[setPropertyName] === 'undefined')
|
if (typeof this[setPropertyName] === 'undefined')
|
||||||
this[setPropertyName] = (value, log = false) => {
|
{
|
||||||
|
this[setPropertyName] = (value, log = false) =>
|
||||||
|
{
|
||||||
this._setAttribute(name, value, log);
|
this._setAttribute(name, value, log);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Object.defineProperty(this, name, {
|
Object.defineProperty(this, name, {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() { return this[getPropertyName](); /* return this['_' + name];*/ },
|
get()
|
||||||
set(value) { this[setPropertyName](value); }
|
{
|
||||||
|
return this[getPropertyName](); /* return this['_' + name];*/
|
||||||
|
},
|
||||||
|
set(value)
|
||||||
|
{
|
||||||
|
this[setPropertyName](value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// note: we use this[name] instead of this['_' + name] since a this.set<Name> method may available
|
// note: we use this[name] instead of this['_' + name] since a this.set<Name> method may available
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Scheduler.
|
* Scheduler.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -61,7 +61,10 @@ export class Scheduler
|
|||||||
* @public
|
* @public
|
||||||
* @returns {module:util.Scheduler#Status} the status of the scheduler
|
* @returns {module:util.Scheduler#Status} the status of the scheduler
|
||||||
*/
|
*/
|
||||||
get status() { return this._status; }
|
get status()
|
||||||
|
{
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,7 +81,8 @@ export class Scheduler
|
|||||||
* @param {module:util.Scheduler~Task | module:util.Scheduler} task - the task to be scheduled
|
* @param {module:util.Scheduler~Task | module:util.Scheduler} task - the task to be scheduled
|
||||||
* @param {...*} args - arguments for that task
|
* @param {...*} args - arguments for that task
|
||||||
*/
|
*/
|
||||||
add(task, ...args) {
|
add(task, ...args)
|
||||||
|
{
|
||||||
this._taskList.push(task);
|
this._taskList.push(task);
|
||||||
this._argsList.push(args);
|
this._argsList.push(args);
|
||||||
}
|
}
|
||||||
@ -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} thenScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is satisfied
|
||||||
* @param {module:util.Scheduler} elseScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is not satisfied
|
* @param {module:util.Scheduler} elseScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is not satisfied
|
||||||
*/
|
*/
|
||||||
addConditional(condition, thenScheduler, elseScheduler) {
|
addConditional(condition, thenScheduler, elseScheduler)
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
let task = function () {
|
let task = function ()
|
||||||
|
{
|
||||||
if (condition())
|
if (condition())
|
||||||
|
{
|
||||||
self.add(thenScheduler);
|
self.add(thenScheduler);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
self.add(elseScheduler);
|
self.add(elseScheduler);
|
||||||
|
}
|
||||||
|
|
||||||
return Scheduler.Event.NEXT;
|
return Scheduler.Event.NEXT;
|
||||||
};
|
};
|
||||||
@ -124,11 +134,14 @@ export class Scheduler
|
|||||||
* @name module:util.Scheduler#start
|
* @name module:util.Scheduler#start
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
start() {
|
start()
|
||||||
|
{
|
||||||
const self = this;
|
const self = this;
|
||||||
let update = () => {
|
let update = () =>
|
||||||
|
{
|
||||||
// stop the animation if need be:
|
// stop the animation if need be:
|
||||||
if (self._stopAtNextUpdate) {
|
if (self._stopAtNextUpdate)
|
||||||
|
{
|
||||||
self._status = Scheduler.Status.STOPPED;
|
self._status = Scheduler.Status.STOPPED;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -137,7 +150,8 @@ export class Scheduler
|
|||||||
|
|
||||||
// run the next scheduled tasks until a scene render is requested:
|
// run the next scheduled tasks until a scene render is requested:
|
||||||
const state = self._runNextTasks();
|
const state = self._runNextTasks();
|
||||||
if (state === Scheduler.Event.QUIT) {
|
if (state === Scheduler.Event.QUIT)
|
||||||
|
{
|
||||||
self._status = Scheduler.Status.STOPPED;
|
self._status = Scheduler.Status.STOPPED;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -160,7 +174,8 @@ export class Scheduler
|
|||||||
* @name module:util.Scheduler#stop
|
* @name module:util.Scheduler#stop
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop()
|
||||||
|
{
|
||||||
this._status = Scheduler.Status.STOPPED;
|
this._status = Scheduler.Status.STOPPED;
|
||||||
this._stopAtNextTask = true;
|
this._stopAtNextTask = true;
|
||||||
this._stopAtNextUpdate = true;
|
this._stopAtNextUpdate = true;
|
||||||
@ -174,52 +189,67 @@ export class Scheduler
|
|||||||
* @private
|
* @private
|
||||||
* @return {module:util.Scheduler#Event} the state of the scheduler after the last task ran
|
* @return {module:util.Scheduler#Event} the state of the scheduler after the last task ran
|
||||||
*/
|
*/
|
||||||
_runNextTasks() {
|
_runNextTasks()
|
||||||
|
{
|
||||||
this._status = Scheduler.Status.RUNNING;
|
this._status = Scheduler.Status.RUNNING;
|
||||||
|
|
||||||
let state = Scheduler.Event.NEXT;
|
let state = Scheduler.Event.NEXT;
|
||||||
while (state === Scheduler.Event.NEXT) {
|
while (state === Scheduler.Event.NEXT)
|
||||||
|
{
|
||||||
// check if we need to quit:
|
// check if we need to quit:
|
||||||
if (this._stopAtNextTask)
|
if (this._stopAtNextTask)
|
||||||
|
{
|
||||||
return Scheduler.Event.QUIT;
|
return Scheduler.Event.QUIT;
|
||||||
|
}
|
||||||
|
|
||||||
// if there is no current task, we look for the next one in the list or quit if there is none:
|
// if there is no current task, we look for the next one in the list or quit if there is none:
|
||||||
if (typeof this._currentTask == 'undefined') {
|
if (typeof this._currentTask == 'undefined')
|
||||||
|
{
|
||||||
|
|
||||||
// a task is available in the taskList:
|
// a task is available in the taskList:
|
||||||
if (this._taskList.length > 0) {
|
if (this._taskList.length > 0)
|
||||||
|
{
|
||||||
this._currentTask = this._taskList.shift();
|
this._currentTask = this._taskList.shift();
|
||||||
this._currentArgs = this._argsList.shift();
|
this._currentArgs = this._argsList.shift();
|
||||||
}
|
}
|
||||||
// the taskList is empty: we quit
|
// the taskList is empty: we quit
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
this._currentTask = undefined;
|
this._currentTask = undefined;
|
||||||
this._currentArgs = undefined;
|
this._currentArgs = undefined;
|
||||||
return Scheduler.Event.QUIT;
|
return Scheduler.Event.QUIT;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// we are repeating a task
|
// we are repeating a task
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the current task is a function, we run it:
|
// if the current task is a function, we run it:
|
||||||
if (this._currentTask instanceof Function) {
|
if (this._currentTask instanceof Function)
|
||||||
|
{
|
||||||
state = this._currentTask(...this._currentArgs);
|
state = this._currentTask(...this._currentArgs);
|
||||||
}
|
}
|
||||||
// otherwise, we assume that the current task is a scheduler and we run its tasks until a rendering
|
// otherwise, we assume that the current task is a scheduler and we run its tasks until a rendering
|
||||||
// of the scene is required.
|
// of the scene is required.
|
||||||
// note: "if (this._currentTask instanceof Scheduler)" does not work because of CORS...
|
// note: "if (this._currentTask instanceof Scheduler)" does not work because of CORS...
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
state = this._currentTask._runNextTasks();
|
state = this._currentTask._runNextTasks();
|
||||||
if (state === Scheduler.Event.QUIT) {
|
if (state === Scheduler.Event.QUIT)
|
||||||
|
{
|
||||||
// if the experiment has not ended, we move onto the next task:
|
// if the experiment has not ended, we move onto the next task:
|
||||||
if (!this._psychoJS.experiment.experimentEnded)
|
if (!this._psychoJS.experiment.experimentEnded)
|
||||||
|
{
|
||||||
state = Scheduler.Event.NEXT;
|
state = Scheduler.Event.NEXT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if the current task's return status is FLIP_REPEAT, we will re-run it, otherwise
|
// if the current task's return status is FLIP_REPEAT, we will re-run it, otherwise
|
||||||
// we move onto the next task:
|
// we move onto the next task:
|
||||||
if (state !== Scheduler.Event.FLIP_REPEAT) {
|
if (state !== Scheduler.Event.FLIP_REPEAT)
|
||||||
|
{
|
||||||
this._currentTask = undefined;
|
this._currentTask = undefined;
|
||||||
this._currentArgs = undefined;
|
this._currentArgs = undefined;
|
||||||
}
|
}
|
||||||
|
323
js/util/Util.js
323
js/util/Util.js
@ -2,7 +2,7 @@
|
|||||||
* Various utilities.
|
* Various utilities.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -24,8 +24,11 @@
|
|||||||
* class NewClass extends mix(BaseClass).with(Mixin1, Mixin2) { ... }
|
* class NewClass extends mix(BaseClass).with(Mixin1, Mixin2) { ... }
|
||||||
*/
|
*/
|
||||||
export let mix = (superclass) => new MixinBuilder(superclass);
|
export let mix = (superclass) => new MixinBuilder(superclass);
|
||||||
class MixinBuilder {
|
|
||||||
constructor(superclass) {
|
class MixinBuilder
|
||||||
|
{
|
||||||
|
constructor(superclass)
|
||||||
|
{
|
||||||
this.superclass = superclass;
|
this.superclass = superclass;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +37,8 @@ class MixinBuilder {
|
|||||||
* @param mixins
|
* @param mixins
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
with(...mixins) {
|
with(...mixins)
|
||||||
|
{
|
||||||
return mixins.reduce((c, mixin) => mixin(c), this.superclass);
|
return mixins.reduce((c, mixin) => mixin(c), this.superclass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,7 +54,8 @@ class MixinBuilder {
|
|||||||
* @return {Object[]} the resulting value in the format [error, return data]
|
* @return {Object[]} the resulting value in the format [error, return data]
|
||||||
* where error is null if there was no error
|
* where error is null if there was no error
|
||||||
*/
|
*/
|
||||||
export function promiseToTupple(promise) {
|
export function promiseToTupple(promise)
|
||||||
|
{
|
||||||
return promise
|
return promise
|
||||||
.then(data => [null, data])
|
.then(data => [null, data])
|
||||||
.catch(error => [error, null]);
|
.catch(error => [error, null]);
|
||||||
@ -68,7 +73,8 @@ export function promiseToTupple(promise) {
|
|||||||
*/
|
*/
|
||||||
export function makeUuid()
|
export function makeUuid()
|
||||||
{
|
{
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c)
|
||||||
|
{
|
||||||
const r = Math.random() * 16 | 0, v = (c === 'x') ? r : (r & 0x3 | 0x8);
|
const r = Math.random() * 16 | 0, v = (c === 'x') ? r : (r & 0x3 | 0x8);
|
||||||
return v.toString(16);
|
return v.toString(16);
|
||||||
});
|
});
|
||||||
@ -85,9 +91,12 @@ export function makeUuid()
|
|||||||
*/
|
*/
|
||||||
export function getErrorStack()
|
export function getErrorStack()
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
throw Error('');
|
throw Error('');
|
||||||
} catch(error) {
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
// we need to remove the second line since it references getErrorStack:
|
// we need to remove the second line since it references getErrorStack:
|
||||||
let stack = error.stack.split("\n");
|
let stack = error.stack.split("\n");
|
||||||
stack.splice(1, 1);
|
stack.splice(1, 1);
|
||||||
@ -108,10 +117,22 @@ export function getErrorStack()
|
|||||||
*/
|
*/
|
||||||
export function isEmpty(x)
|
export function isEmpty(x)
|
||||||
{
|
{
|
||||||
if (typeof x === 'undefined') return true;
|
if (typeof x === 'undefined')
|
||||||
if (!Array.isArray(x)) return false;
|
{
|
||||||
if (x.length === 0) return true;
|
return true;
|
||||||
if (x.length === 1 && typeof x[0] === 'undefined') return true;
|
}
|
||||||
|
if (!Array.isArray(x))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (x.length === 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (x.length === 1 && typeof x[0] === 'undefined')
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -133,38 +154,65 @@ export function detectBrowser()
|
|||||||
{
|
{
|
||||||
// Opera 8.0+
|
// Opera 8.0+
|
||||||
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
||||||
if (isOpera) return 'Opera';
|
if (isOpera)
|
||||||
|
{
|
||||||
|
return 'Opera';
|
||||||
|
}
|
||||||
|
|
||||||
// Firefox 1.0+
|
// Firefox 1.0+
|
||||||
const isFirefox = (typeof InstallTrigger !== 'undefined');
|
const isFirefox = (typeof InstallTrigger !== 'undefined');
|
||||||
if (isFirefox) return 'Firefox';
|
if (isFirefox)
|
||||||
|
{
|
||||||
|
return 'Firefox';
|
||||||
|
}
|
||||||
|
|
||||||
// Safari 3.0+ "[object HTMLElementConstructor]"
|
// Safari 3.0+ "[object HTMLElementConstructor]"
|
||||||
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification));
|
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p)
|
||||||
if (isSafari) return 'Safari';
|
{
|
||||||
|
return p.toString() === "[object SafariRemoteNotification]";
|
||||||
|
})(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification));
|
||||||
|
if (isSafari)
|
||||||
|
{
|
||||||
|
return 'Safari';
|
||||||
|
}
|
||||||
|
|
||||||
// Internet Explorer 6-11
|
// Internet Explorer 6-11
|
||||||
// const isIE6 = !window.XMLHttpRequest;
|
// const isIE6 = !window.XMLHttpRequest;
|
||||||
// const isIE7 = document.all && window.XMLHttpRequest && !XDomainRequest && !window.opera;
|
// const isIE7 = document.all && window.XMLHttpRequest && !XDomainRequest && !window.opera;
|
||||||
// const isIE8 = document.documentMode==8;
|
// const isIE8 = document.documentMode==8;
|
||||||
const isIE = /*@cc_on!@*/false || !!document.documentMode;
|
const isIE = /*@cc_on!@*/false || !!document.documentMode;
|
||||||
if (isIE) return 'IE';
|
if (isIE)
|
||||||
|
{
|
||||||
|
return 'IE';
|
||||||
|
}
|
||||||
|
|
||||||
// Edge 20+
|
// Edge 20+
|
||||||
const isEdge = !isIE && !!window.StyleMedia;
|
const isEdge = !isIE && !!window.StyleMedia;
|
||||||
if (isEdge) return 'Edge';
|
if (isEdge)
|
||||||
|
{
|
||||||
|
return 'Edge';
|
||||||
|
}
|
||||||
|
|
||||||
// Chrome 1+
|
// Chrome 1+
|
||||||
const isChrome = window.chrome;
|
const isChrome = window.chrome;
|
||||||
if (isChrome) return 'Chrome';
|
if (isChrome)
|
||||||
|
{
|
||||||
|
return 'Chrome';
|
||||||
|
}
|
||||||
|
|
||||||
// Chromium-based Edge:
|
// Chromium-based Edge:
|
||||||
const isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") !== -1);
|
const isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") !== -1);
|
||||||
if (isEdgeChromium) return 'EdgeChromium';
|
if (isEdgeChromium)
|
||||||
|
{
|
||||||
|
return 'EdgeChromium';
|
||||||
|
}
|
||||||
|
|
||||||
// Blink engine detection
|
// Blink engine detection
|
||||||
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
||||||
if (isBlink) return 'Blink';
|
if (isBlink)
|
||||||
|
{
|
||||||
|
return 'Blink';
|
||||||
|
}
|
||||||
|
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
@ -188,20 +236,30 @@ export function detectBrowser()
|
|||||||
*/
|
*/
|
||||||
export function toNumerical(obj)
|
export function toNumerical(obj)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.toNumerical', context: 'when converting an object to its numerical form' };
|
const response = {origin: 'util.toNumerical', context: 'when converting an object to its numerical form'};
|
||||||
|
|
||||||
if (typeof obj === 'number')
|
if (typeof obj === 'number')
|
||||||
|
{
|
||||||
return obj;
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof obj === 'string')
|
if (typeof obj === 'string')
|
||||||
|
{
|
||||||
obj = [obj];
|
obj = [obj];
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj))
|
||||||
return obj.map( e => {
|
{
|
||||||
|
return obj.map(e =>
|
||||||
|
{
|
||||||
let n = Number.parseFloat(e);
|
let n = Number.parseFloat(e);
|
||||||
if (Number.isNaN(n))
|
if (Number.isNaN(n))
|
||||||
Object.assign(response, { error: 'unable to convert: ' + e + ' to a' +
|
{
|
||||||
' number.'});
|
Object.assign(response, {
|
||||||
|
error: 'unable to convert: ' + e + ' to a' +
|
||||||
|
' number.'
|
||||||
|
});
|
||||||
|
}
|
||||||
return n;
|
return n;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -231,7 +289,10 @@ export function IsPointInsidePolygon(point, vertices)
|
|||||||
const xi = vertices[i][0], yi = vertices[i][1];
|
const xi = vertices[i][0], yi = vertices[i][1];
|
||||||
const xj = vertices[j][0], yj = vertices[j][1];
|
const xj = vertices[j][0], yj = vertices[j][1];
|
||||||
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||||
if (intersect) isInside = !isInside;
|
if (intersect)
|
||||||
|
{
|
||||||
|
isInside = !isInside;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isInside;
|
return isInside;
|
||||||
@ -271,16 +332,20 @@ export function shuffle(array)
|
|||||||
*/
|
*/
|
||||||
export function getPositionFromObject(object, units)
|
export function getPositionFromObject(object, units)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.getPositionFromObject', context: 'when getting the position of an object' };
|
const response = {origin: 'util.getPositionFromObject', context: 'when getting the position of an object'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
if (typeof object === 'undefined')
|
if (typeof object === 'undefined')
|
||||||
|
{
|
||||||
throw 'cannot get the position of an undefined object';
|
throw 'cannot get the position of an undefined object';
|
||||||
|
}
|
||||||
|
|
||||||
let objectWin = undefined;
|
let objectWin = undefined;
|
||||||
|
|
||||||
// object has a getPos function:
|
// object has a getPos function:
|
||||||
if (typeof object.getPos === 'function') {
|
if (typeof object.getPos === 'function')
|
||||||
|
{
|
||||||
units = object.units;
|
units = object.units;
|
||||||
objectWin = object.win;
|
objectWin = object.win;
|
||||||
object = object.getPos();
|
object = object.getPos();
|
||||||
@ -289,8 +354,9 @@ export function getPositionFromObject(object, units)
|
|||||||
// convert object to pixel units:
|
// convert object to pixel units:
|
||||||
return to_px(object, units, objectWin);
|
return to_px(object, units, objectWin);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,18 +374,25 @@ export function getPositionFromObject(object, units)
|
|||||||
*/
|
*/
|
||||||
export function to_px(pos, posUnit, win)
|
export function to_px(pos, posUnit, win)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_px', context: 'when converting a position to pixel units' };
|
const response = {origin: 'util.to_px', context: 'when converting a position to pixel units'};
|
||||||
|
|
||||||
if (posUnit === 'pix')
|
if (posUnit === 'pix')
|
||||||
|
{
|
||||||
return pos;
|
return pos;
|
||||||
|
}
|
||||||
else if (posUnit === 'norm')
|
else if (posUnit === 'norm')
|
||||||
return [pos[0] * win.size[0]/2.0, pos[1] * win.size[1]/2.0];
|
{
|
||||||
else if (posUnit === 'height') {
|
return [pos[0] * win.size[0] / 2.0, pos[1] * win.size[1] / 2.0];
|
||||||
|
}
|
||||||
|
else if (posUnit === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(win.size[0], win.size[1]);
|
const minSize = Math.min(win.size[0], win.size[1]);
|
||||||
return [pos[0] * minSize, pos[1] * minSize];
|
return [pos[0] * minSize, pos[1] * minSize];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
{
|
||||||
|
throw Object.assign(response, {error: `unknown position units: ${posUnit}`});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -336,18 +409,23 @@ export function to_px(pos, posUnit, win)
|
|||||||
*/
|
*/
|
||||||
export function to_norm(pos, posUnit, win)
|
export function to_norm(pos, posUnit, win)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_norm', context: 'when converting a position to norm units' };
|
const response = {origin: 'util.to_norm', context: 'when converting a position to norm units'};
|
||||||
|
|
||||||
if (posUnit === 'norm')
|
if (posUnit === 'norm')
|
||||||
|
{
|
||||||
return pos;
|
return pos;
|
||||||
|
}
|
||||||
if (posUnit === 'pix')
|
if (posUnit === 'pix')
|
||||||
return [pos[0] / (win.size[0]/2.0), pos[1] / (win.size[1]/2.0)];
|
{
|
||||||
if (posUnit === 'height') {
|
return [pos[0] / (win.size[0] / 2.0), pos[1] / (win.size[1] / 2.0)];
|
||||||
|
}
|
||||||
|
if (posUnit === 'height')
|
||||||
|
{
|
||||||
const minSize = Math.min(win.size[0], win.size[1]);
|
const minSize = Math.min(win.size[0], win.size[1]);
|
||||||
return [pos[0] * minSize / (win.size[0]/2.0), pos[1] * minSize / (win.size[1]/2.0)];
|
return [pos[0] * minSize / (win.size[0] / 2.0), pos[1] * minSize / (win.size[1] / 2.0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
throw Object.assign(response, {error: `unknown position units: ${posUnit}`});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -364,20 +442,24 @@ export function to_norm(pos, posUnit, win)
|
|||||||
*/
|
*/
|
||||||
export function to_height(pos, posUnit, win)
|
export function to_height(pos, posUnit, win)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_height', context: 'when converting a position to height units' };
|
const response = {origin: 'util.to_height', context: 'when converting a position to height units'};
|
||||||
|
|
||||||
if (posUnit === 'height')
|
if (posUnit === 'height')
|
||||||
|
{
|
||||||
return pos;
|
return pos;
|
||||||
if (posUnit === 'pix') {
|
}
|
||||||
|
if (posUnit === 'pix')
|
||||||
|
{
|
||||||
const minSize = Math.min(win.size[0], win.size[1]);
|
const minSize = Math.min(win.size[0], win.size[1]);
|
||||||
return [pos[0] / minSize, pos[1] / minSize];
|
return [pos[0] / minSize, pos[1] / minSize];
|
||||||
}
|
}
|
||||||
if (posUnit === 'norm') {
|
if (posUnit === 'norm')
|
||||||
|
{
|
||||||
const minSize = Math.min(win.size[0], win.size[1]);
|
const minSize = Math.min(win.size[0], win.size[1]);
|
||||||
return [pos[0] * win.size[0]/2.0 / minSize, pos[1] * win.size[1]/2.0 / minSize];
|
return [pos[0] * win.size[0] / 2.0 / minSize, pos[1] * win.size[1] / 2.0 / minSize];
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
|
throw Object.assign(response, {error: `unknown position units: ${posUnit}`});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -394,19 +476,28 @@ export function to_height(pos, posUnit, win)
|
|||||||
*/
|
*/
|
||||||
export function to_win(pos, posUnit, win)
|
export function to_win(pos, posUnit, win)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_win', context: 'when converting a position to window units' };
|
const response = {origin: 'util.to_win', context: 'when converting a position to window units'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
if (win._units === 'pix')
|
if (win._units === 'pix')
|
||||||
|
{
|
||||||
return to_px(pos, posUnit, win);
|
return to_px(pos, posUnit, win);
|
||||||
|
}
|
||||||
if (win._units === 'norm')
|
if (win._units === 'norm')
|
||||||
|
{
|
||||||
return to_norm(pos, posUnit, win);
|
return to_norm(pos, posUnit, win);
|
||||||
|
}
|
||||||
if (win._units === 'height')
|
if (win._units === 'height')
|
||||||
|
{
|
||||||
return to_height(pos, posUnit, win);
|
return to_height(pos, posUnit, win);
|
||||||
|
}
|
||||||
|
|
||||||
throw `unknown window units: ${win._units}`;
|
throw `unknown window units: ${win._units}`;
|
||||||
} catch (error) {
|
}
|
||||||
throw Object.assign(response, { response, error });
|
catch (error)
|
||||||
|
{
|
||||||
|
throw Object.assign(response, {response, error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,19 +516,28 @@ export function to_win(pos, posUnit, win)
|
|||||||
*/
|
*/
|
||||||
export function to_unit(pos, posUnit, win, targetUnit)
|
export function to_unit(pos, posUnit, win, targetUnit)
|
||||||
{
|
{
|
||||||
const response = { origin: 'util.to_unit', context: 'when converting a position to different units' };
|
const response = {origin: 'util.to_unit', context: 'when converting a position to different units'};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
if (targetUnit === 'pix')
|
if (targetUnit === 'pix')
|
||||||
|
{
|
||||||
return to_px(pos, posUnit, win);
|
return to_px(pos, posUnit, win);
|
||||||
|
}
|
||||||
if (targetUnit === 'norm')
|
if (targetUnit === 'norm')
|
||||||
|
{
|
||||||
return to_norm(pos, posUnit, win);
|
return to_norm(pos, posUnit, win);
|
||||||
|
}
|
||||||
if (targetUnit === 'height')
|
if (targetUnit === 'height')
|
||||||
|
{
|
||||||
return to_height(pos, posUnit, win);
|
return to_height(pos, posUnit, win);
|
||||||
|
}
|
||||||
|
|
||||||
throw `unknown target units: ${targetUnit}`;
|
throw `unknown target units: ${targetUnit}`;
|
||||||
} catch (error) {
|
}
|
||||||
throw Object.assign(response, { error });
|
catch (error)
|
||||||
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,26 +574,39 @@ export function to_pixiPoint(pos, posUnit, win)
|
|||||||
export function toString(object)
|
export function toString(object)
|
||||||
{
|
{
|
||||||
if (typeof object === 'undefined')
|
if (typeof object === 'undefined')
|
||||||
|
{
|
||||||
return 'undefined';
|
return 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
if (!object)
|
if (!object)
|
||||||
|
{
|
||||||
return 'null';
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof object === 'string')
|
if (typeof object === 'string')
|
||||||
|
{
|
||||||
return object;
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
// if the object is a class and has a toString method:
|
// if the object is a class and has a toString method:
|
||||||
if (object.constructor.toString().substring(0, 5) === 'class' && typeof object.toString === 'function')
|
if (object.constructor.toString().substring(0, 5) === 'class' && typeof object.toString === 'function')
|
||||||
|
{
|
||||||
return object.toString();
|
return object.toString();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try
|
||||||
const symbolReplacer = (key, value) => {
|
{
|
||||||
|
const symbolReplacer = (key, value) =>
|
||||||
|
{
|
||||||
if (typeof value === 'symbol')
|
if (typeof value === 'symbol')
|
||||||
|
{
|
||||||
value = Symbol.keyFor(value);
|
value = Symbol.keyFor(value);
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
return JSON.stringify(object, symbolReplacer);
|
return JSON.stringify(object, symbolReplacer);
|
||||||
} catch (e)
|
}
|
||||||
|
catch (e)
|
||||||
{
|
{
|
||||||
return 'Object (circular)';
|
return 'Object (circular)';
|
||||||
}
|
}
|
||||||
@ -502,15 +615,15 @@ export function toString(object)
|
|||||||
|
|
||||||
if (!String.prototype.format)
|
if (!String.prototype.format)
|
||||||
{
|
{
|
||||||
String.prototype.format = function()
|
String.prototype.format = function ()
|
||||||
{
|
{
|
||||||
var args = arguments;
|
var args = arguments;
|
||||||
return this
|
return this
|
||||||
.replace(/{(\d+)}/g, function(match, number)
|
.replace(/{(\d+)}/g, function (match, number)
|
||||||
{
|
{
|
||||||
return typeof args[number] != 'undefined' ? args[number] : match;
|
return typeof args[number] != 'undefined' ? args[number] : match;
|
||||||
})
|
})
|
||||||
.replace(/{([$_a-zA-Z][$_a-zA-Z0-9]*)}/g, function(match, name)
|
.replace(/{([$_a-zA-Z][$_a-zA-Z0-9]*)}/g, function (match, name)
|
||||||
{
|
{
|
||||||
//console.log("n=" + name + " args[0][name]=" + args[0][name]);
|
//console.log("n=" + name + " args[0][name]=" + args[0][name]);
|
||||||
return args.length > 0 && args[0][name] !== undefined ? args[0][name] : match;
|
return args.length > 0 && args[0][name] !== undefined ? args[0][name] : match;
|
||||||
@ -534,19 +647,22 @@ export function getRequestError(jqXHR, textStatus, errorThrown)
|
|||||||
let errorMsg = 'unknown error';
|
let errorMsg = 'unknown error';
|
||||||
|
|
||||||
if (typeof jqXHR.responseJSON !== 'undefined')
|
if (typeof jqXHR.responseJSON !== 'undefined')
|
||||||
|
{
|
||||||
errorMsg = jqXHR.responseJSON;
|
errorMsg = jqXHR.responseJSON;
|
||||||
|
}
|
||||||
else if (typeof jqXHR.responseText !== 'undefined')
|
else if (typeof jqXHR.responseText !== 'undefined')
|
||||||
|
{
|
||||||
errorMsg = jqXHR.responseText;
|
errorMsg = jqXHR.responseText;
|
||||||
|
}
|
||||||
else if (typeof errorThrown !== 'undefined')
|
else if (typeof errorThrown !== 'undefined')
|
||||||
|
{
|
||||||
errorMsg = errorThrown;
|
errorMsg = errorThrown;
|
||||||
|
}
|
||||||
|
|
||||||
return errorMsg;
|
return errorMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test whether an object is either an integer or the string representation of an integer.
|
* Test whether an object is either an integer or the string representation of an integer.
|
||||||
* <p>This is adapted from: https://stackoverflow.com/a/14794066</p>
|
* <p>This is adapted from: https://stackoverflow.com/a/14794066</p>
|
||||||
@ -557,9 +673,12 @@ export function getRequestError(jqXHR, textStatus, errorThrown)
|
|||||||
* @param {Object} obj - the input object
|
* @param {Object} obj - the input object
|
||||||
* @returns {boolean} whether or not the object is an integer or the string representation of an integer
|
* @returns {boolean} whether or not the object is an integer or the string representation of an integer
|
||||||
*/
|
*/
|
||||||
export function isInt(obj) {
|
export function isInt(obj)
|
||||||
|
{
|
||||||
if (isNaN(obj))
|
if (isNaN(obj))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const x = parseFloat(obj);
|
const x = parseFloat(obj);
|
||||||
return (x | 0) === x;
|
return (x | 0) === x;
|
||||||
@ -611,10 +730,12 @@ export function addInfoFromUrl(info)
|
|||||||
// note: parameters starting with a double underscore are reserved for client/server communication,
|
// note: parameters starting with a double underscore are reserved for client/server communication,
|
||||||
// we do not add them to info
|
// we do not add them to info
|
||||||
// for (const [key, value] of infoFromUrl)
|
// for (const [key, value] of infoFromUrl)
|
||||||
infoFromUrl.forEach( (value, key) =>
|
infoFromUrl.forEach((value, key) =>
|
||||||
{
|
{
|
||||||
if (key.indexOf('__') !== 0)
|
if (key.indexOf('__') !== 0)
|
||||||
|
{
|
||||||
info[key] = value;
|
info[key] = value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
@ -641,34 +762,48 @@ export function addInfoFromUrl(info)
|
|||||||
* @param {number | Array.<number> | string} selection - the selection
|
* @param {number | Array.<number> | string} selection - the selection
|
||||||
* @returns {Object | Array.<Object>} the array of selected items
|
* @returns {Object | Array.<Object>} the array of selected items
|
||||||
*/
|
*/
|
||||||
export function selectFromArray(array, selection) {
|
export function selectFromArray(array, selection)
|
||||||
|
{
|
||||||
|
|
||||||
// if selection is an integer, or a string representing an integer, we treat it as an index in the array
|
// if selection is an integer, or a string representing an integer, we treat it as an index in the array
|
||||||
// and return that entry:
|
// and return that entry:
|
||||||
if (isInt(selection))
|
if (isInt(selection))
|
||||||
|
{
|
||||||
return array[parseInt(selection)];
|
return array[parseInt(selection)];
|
||||||
|
}// if selection is an array, we treat it as a list of indices
|
||||||
// if selection is an array, we treat it as a list of indices
|
|
||||||
// and return an array with the entries corresponding to those indices:
|
// and return an array with the entries corresponding to those indices:
|
||||||
else if (Array.isArray(selection))
|
else if (Array.isArray(selection))
|
||||||
return array.filter( (e,i) => (selection.includes(i)) );
|
{
|
||||||
|
return array.filter((e, i) => (selection.includes(i)));
|
||||||
// if selection is a string, we decode it:
|
}// if selection is a string, we decode it:
|
||||||
else if (typeof selection === 'string') {
|
else if (typeof selection === 'string')
|
||||||
|
{
|
||||||
if (selection.indexOf(',') > -1)
|
if (selection.indexOf(',') > -1)
|
||||||
|
{
|
||||||
return selection.split(',').map(a => selectFromArray(array, a));
|
return selection.split(',').map(a => selectFromArray(array, a));
|
||||||
// return flattenArray( selection.split(',').map(a => selectFromArray(array, a)) );
|
}// return flattenArray( selection.split(',').map(a => selectFromArray(array, a)) );
|
||||||
else if (selection.indexOf(':') > -1) {
|
else if (selection.indexOf(':') > -1)
|
||||||
|
{
|
||||||
let sliceParams = selection.split(':').map(a => parseInt(a));
|
let sliceParams = selection.split(':').map(a => parseInt(a));
|
||||||
if (sliceParams.length === 3)
|
if (sliceParams.length === 3)
|
||||||
|
{
|
||||||
return sliceArray(array, sliceParams[0], sliceParams[2], sliceParams[1]);
|
return sliceArray(array, sliceParams[0], sliceParams[2], sliceParams[1]);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return sliceArray(array, ...sliceParams);
|
return sliceArray(array, ...sliceParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
throw { origin: 'selectFromArray', context: 'when selecting entries from an array', error: 'unknown selection type: ' + (typeof selection)};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'selectFromArray',
|
||||||
|
context: 'when selecting entries from an array',
|
||||||
|
error: 'unknown selection type: ' + (typeof selection)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -681,10 +816,12 @@ export function selectFromArray(array, selection) {
|
|||||||
* @param {Array.<Object>} array - the input array of arrays
|
* @param {Array.<Object>} array - the input array of arrays
|
||||||
* @returns {Array.<Object>} the flatten array
|
* @returns {Array.<Object>} the flatten array
|
||||||
*/
|
*/
|
||||||
export function flattenArray(array) {
|
export function flattenArray(array)
|
||||||
|
{
|
||||||
return array.reduce(
|
return array.reduce(
|
||||||
(flat, next) => {
|
(flat, next) =>
|
||||||
flat.push( (Array.isArray(next) && Array.isArray(next[0])) ? flattenArray(next) : next );
|
{
|
||||||
|
flat.push((Array.isArray(next) && Array.isArray(next[0])) ? flattenArray(next) : next);
|
||||||
return flat;
|
return flat;
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@ -706,22 +843,36 @@ export function flattenArray(array) {
|
|||||||
*/
|
*/
|
||||||
export function sliceArray(array, from = NaN, to = NaN, step = NaN)
|
export function sliceArray(array, from = NaN, to = NaN, step = NaN)
|
||||||
{
|
{
|
||||||
if (isNaN(from)) from = 0;
|
if (isNaN(from))
|
||||||
if (isNaN(to)) to = array.length;
|
{
|
||||||
|
from = 0;
|
||||||
|
}
|
||||||
|
if (isNaN(to))
|
||||||
|
{
|
||||||
|
to = array.length;
|
||||||
|
}
|
||||||
|
|
||||||
let arraySlice = array.slice(from, to);
|
let arraySlice = array.slice(from, to);
|
||||||
|
|
||||||
if (isNaN(step))
|
if (isNaN(step))
|
||||||
|
{
|
||||||
return arraySlice;
|
return arraySlice;
|
||||||
|
}
|
||||||
|
|
||||||
if (step < 0)
|
if (step < 0)
|
||||||
|
{
|
||||||
arraySlice.reverse();
|
arraySlice.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
step = Math.abs(step);
|
step = Math.abs(step);
|
||||||
if (step == 1)
|
if (step == 1)
|
||||||
|
{
|
||||||
return arraySlice;
|
return arraySlice;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
return arraySlice.filter( (e,i) => (i % step == 0) );
|
{
|
||||||
|
return arraySlice.filter((e, i) => (i % step == 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -735,11 +886,15 @@ export function sliceArray(array, from = NaN, to = NaN, step = NaN)
|
|||||||
* @param {*} data - the data
|
* @param {*} data - the data
|
||||||
* @param {string} type - the MIME type of the data, e.g. 'text/csv' or 'application/json'
|
* @param {string} type - the MIME type of the data, e.g. 'text/csv' or 'application/json'
|
||||||
*/
|
*/
|
||||||
export function offerDataForDownload(filename, data, type) {
|
export function offerDataForDownload(filename, data, type)
|
||||||
const blob = new Blob([data], { type });
|
{
|
||||||
|
const blob = new Blob([data], {type});
|
||||||
if (window.navigator.msSaveOrOpenBlob)
|
if (window.navigator.msSaveOrOpenBlob)
|
||||||
|
{
|
||||||
window.navigator.msSaveBlob(blob, filename);
|
window.navigator.msSaveBlob(blob, filename);
|
||||||
else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
let elem = window.document.createElement('a');
|
let elem = window.document.createElement('a');
|
||||||
elem.href = window.URL.createObjectURL(blob);
|
elem.href = window.URL.createObjectURL(blob);
|
||||||
elem.download = filename;
|
elem.download = filename;
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
* Image Stimulus.
|
* Image Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { VisualStim } from './VisualStim';
|
import {VisualStim} from './VisualStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { ColorMixin } from '../util/ColorMixin';
|
import {ColorMixin} from '../util/ColorMixin';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -63,16 +63,19 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
flipVert = false,
|
flipVert = false,
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {}) {
|
} = {})
|
||||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
{
|
||||||
|
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||||
|
|
||||||
this.psychoJS.logger.debug('create a new ImageStim with name: ', name);
|
this.psychoJS.logger.debug('create a new ImageStim with name: ', name);
|
||||||
|
|
||||||
this._addAttributes(ImageStim, image, mask, color, contrast, texRes, interpolate, depth, flipHoriz, flipVert);
|
this._addAttributes(ImageStim, image, mask, color, contrast, texRes, interpolate, depth, flipHoriz, flipVert);
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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 {HTMLImageElement | string} image - the name of the image resource or HTMLImageElement corresponding to the image
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setImage(image, log = false) {
|
setImage(image, log = false)
|
||||||
const response = { origin: 'ImageStim.setImage', context: 'when setting the image of ImageStim: ' + this._name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ImageStim.setImage',
|
||||||
|
context: 'when setting the image of ImageStim: ' + this._name
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// image is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
|
// image is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
|
||||||
if (typeof image === 'undefined') {
|
if (typeof image === 'undefined')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('setting the image of ImageStim: ' + this._name + ' with argument: undefined.');
|
this.psychoJS.logger.warn('setting the image of ImageStim: ' + this._name + ' with argument: undefined.');
|
||||||
this.psychoJS.logger.debug('set the image of ImageStim: ' + this._name + ' as: undefined');
|
this.psychoJS.logger.debug('set the image of ImageStim: ' + this._name + ' as: undefined');
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// image is a string: it should be the name of a resource, which we load
|
// image is a string: it should be the name of a resource, which we load
|
||||||
if (typeof image === 'string')
|
if (typeof image === 'string')
|
||||||
|
{
|
||||||
image = this.psychoJS.serverManager.getResource(image);
|
image = this.psychoJS.serverManager.getResource(image);
|
||||||
|
}
|
||||||
|
|
||||||
// image should now be an actual HTMLImageElement: we raise an error if it is not
|
// image should now be an actual HTMLImageElement: we raise an error if it is not
|
||||||
if (!(image instanceof HTMLImageElement)) {
|
if (!(image instanceof HTMLImageElement))
|
||||||
|
{
|
||||||
throw 'the argument: ' + image.toString() + ' is not an image" }';
|
throw 'the argument: ' + image.toString() + ' is not an image" }';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,8 +122,9 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setMask(mask, log = false) {
|
setMask(mask, log = false)
|
||||||
const response = { origin: 'ImageStim.setMask', context: 'when setting the mask of ImageStim: ' + this._name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'ImageStim.setMask',
|
||||||
|
context: 'when setting the mask of ImageStim: ' + this._name
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// mask is undefined: that's fine but we raise a warning in case this is a sympton of an actual problem
|
// mask is undefined: that's fine but we raise a warning in case this is a sympton of an actual problem
|
||||||
if (typeof mask === 'undefined') {
|
if (typeof mask === 'undefined')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('setting the mask of ImageStim: ' + this._name + ' with argument: undefined.');
|
this.psychoJS.logger.warn('setting the mask of ImageStim: ' + this._name + ' with argument: undefined.');
|
||||||
this.psychoJS.logger.debug('set the mask of ImageStim: ' + this._name + ' as: undefined');
|
this.psychoJS.logger.debug('set the mask of ImageStim: ' + this._name + ' as: undefined');
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// mask is a string: it should be the name of a resource, which we load
|
// mask is a string: it should be the name of a resource, which we load
|
||||||
if (typeof mask === 'string')
|
if (typeof mask === 'string')
|
||||||
|
{
|
||||||
mask = this.psychoJS.serverManager.getResource(mask);
|
mask = this.psychoJS.serverManager.getResource(mask);
|
||||||
|
}
|
||||||
|
|
||||||
// mask should now be an actual HTMLImageElement: we raise an error if it is not
|
// mask should now be an actual HTMLImageElement: we raise an error if it is not
|
||||||
if (!(mask instanceof HTMLImageElement)) {
|
if (!(mask instanceof HTMLImageElement))
|
||||||
|
{
|
||||||
throw 'the argument: ' + mask.toString() + ' is not an image" }';
|
throw 'the argument: ' + mask.toString() + ' is not an image" }';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,8 +173,9 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +188,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} flipVert - whether or not to flip vertically
|
* @param {boolean} flipVert - whether or not to flip vertically
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipVert(flipVert, log = false) {
|
setFlipVert(flipVert, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('flipVert', flipVert, log);
|
this._setAttribute('flipVert', flipVert, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -178,7 +204,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipHoriz(flipHoriz, log = false) {
|
setFlipHoriz(flipHoriz, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -197,12 +224,20 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
contains(object, units)
|
contains(object, units)
|
||||||
{
|
{
|
||||||
if (typeof this._image === 'undefined')
|
if (typeof this._image === 'undefined')
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// get position of object:
|
// get position of object:
|
||||||
let objectPos_px = util.getPositionFromObject(object, units);
|
let objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined')
|
if (typeof objectPos_px === 'undefined')
|
||||||
throw { origin : 'ImageStim.contains', context : 'when determining whether ImageStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'ImageStim.contains',
|
||||||
|
context: 'when determining whether ImageStim: ' + this._name + ' contains object: ' + util.toString(object),
|
||||||
|
error: 'unable to determine the position of the object'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// test for inclusion:
|
// test for inclusion:
|
||||||
// note: since _pixi.anchor is [0.5, 0.5] the image is actually centered on pos
|
// note: since _pixi.anchor is [0.5, 0.5] the image is actually centered on pos
|
||||||
@ -225,16 +260,21 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.ImageStim#_updateIfNeeded
|
* @name module:visual.ImageStim#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded() {
|
_updateIfNeeded()
|
||||||
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._pixi = undefined;
|
this._pixi = undefined;
|
||||||
|
|
||||||
// no image to draw: return immediately
|
// no image to draw: return immediately
|
||||||
if (typeof this._image === 'undefined')
|
if (typeof this._image === 'undefined')
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// prepare the image:
|
// prepare the image:
|
||||||
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image));
|
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image));
|
||||||
@ -243,7 +283,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._pixi.zOrder = this.depth;
|
this._pixi.zOrder = this.depth;
|
||||||
|
|
||||||
// add a mask if need be:
|
// add a mask if need be:
|
||||||
if (typeof this._mask !== 'undefined') {
|
if (typeof this._mask !== 'undefined')
|
||||||
|
{
|
||||||
this._maskTexture = new PIXI.Texture(new PIXI.BaseTexture(this._mask));
|
this._maskTexture = new PIXI.Texture(new PIXI.BaseTexture(this._mask));
|
||||||
this._pixi.mask = new PIXI.Sprite(this._maskTexture); //PIXI.Sprite.fromImage(this._mask);
|
this._pixi.mask = new PIXI.Sprite(this._maskTexture); //PIXI.Sprite.fromImage(this._mask);
|
||||||
|
|
||||||
@ -255,7 +296,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// since _texture.width may not be immediately available but the rest of the code needs its value
|
// since _texture.width may not be immediately available but the rest of the code needs its value
|
||||||
// we arrange for repeated calls to _updateIfNeeded until we have a width:
|
// we arrange for repeated calls to _updateIfNeeded until we have a width:
|
||||||
if (this._texture.width === 0) {
|
if (this._texture.width === 0)
|
||||||
|
{
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,17 @@
|
|||||||
* Movie Stimulus.
|
* Movie Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { VisualStim } from './VisualStim';
|
import {VisualStim} from './VisualStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { ColorMixin } from '../util/ColorMixin';
|
import {ColorMixin} from '../util/ColorMixin';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
import { PsychoJS } from "../core/PsychoJS";
|
import {PsychoJS} from "../core/PsychoJS";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,7 +45,8 @@ import { PsychoJS } from "../core/PsychoJS";
|
|||||||
*
|
*
|
||||||
* @todo autoPlay does not work for the moment.
|
* @todo autoPlay does not work for the moment.
|
||||||
*/
|
*/
|
||||||
export class MovieStim extends VisualStim {
|
export class MovieStim extends VisualStim
|
||||||
|
{
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
win,
|
win,
|
||||||
@ -66,8 +67,9 @@ export class MovieStim extends VisualStim {
|
|||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {}) {
|
} = {})
|
||||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
{
|
||||||
|
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||||
|
|
||||||
this.psychoJS.logger.debug('create a new MovieStim with name: ', name);
|
this.psychoJS.logger.debug('create a new MovieStim with name: ', name);
|
||||||
|
|
||||||
@ -78,8 +80,10 @@ export class MovieStim extends VisualStim {
|
|||||||
this._hasFastSeek = (typeof videoElement.fastSeek === 'function');
|
this._hasFastSeek = (typeof videoElement.fastSeek === 'function');
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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 {string | HTMLVideoElement} movie - the name of the movie resource or the HTMLVideoElement corresponding to the movie
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setMovie(movie, log = false) {
|
setMovie(movie, log = false)
|
||||||
const response = { origin: 'MovieStim.setMovie', context: 'when setting the movie of MovieStim: ' + this._name };
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'MovieStim.setMovie',
|
||||||
|
context: 'when setting the movie of MovieStim: ' + this._name
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// movie is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
|
// movie is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
|
||||||
if (typeof movie === 'undefined') {
|
if (typeof movie === 'undefined')
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('setting the movie of MovieStim: ' + this._name + ' with argument: undefined.');
|
this.psychoJS.logger.warn('setting the movie of MovieStim: ' + this._name + ' with argument: undefined.');
|
||||||
this.psychoJS.logger.debug('set the movie of MovieStim: ' + this._name + ' as: undefined');
|
this.psychoJS.logger.debug('set the movie of MovieStim: ' + this._name + ' as: undefined');
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// movie is a string: it should be the name of a resource, which we load
|
// movie is a string: it should be the name of a resource, which we load
|
||||||
if (typeof movie === 'string')
|
if (typeof movie === 'string')
|
||||||
|
{
|
||||||
movie = this.psychoJS.serverManager.getResource(movie);
|
movie = this.psychoJS.serverManager.getResource(movie);
|
||||||
|
}
|
||||||
|
|
||||||
// movie should now be an actual HTMLVideoElement: we raise an error if it is not
|
// movie should now be an actual HTMLVideoElement: we raise an error if it is not
|
||||||
if (!(movie instanceof HTMLVideoElement))
|
if (!(movie instanceof HTMLVideoElement))
|
||||||
|
{
|
||||||
throw 'the argument: ' + movie.toString() + ' is not a video" }';
|
throw 'the argument: ' + movie.toString() + ' is not a video" }';
|
||||||
|
}
|
||||||
|
|
||||||
this.psychoJS.logger.debug(`set the movie of MovieStim: ${this._name} as: src= ${movie.src}, size= ${movie.videoWidth}x${movie.videoHeight}, duration= ${movie.duration}s`);
|
this.psychoJS.logger.debug(`set the movie of MovieStim: ${this._name} as: src= ${movie.src}, size= ${movie.videoWidth}x${movie.videoHeight}, duration= ${movie.duration}s`);
|
||||||
}
|
}
|
||||||
@ -114,12 +129,16 @@ export class MovieStim extends VisualStim {
|
|||||||
this._setAttribute('movie', movie, log);
|
this._setAttribute('movie', movie, log);
|
||||||
|
|
||||||
// change status of stimulus when movie finish playing:
|
// change status of stimulus when movie finish playing:
|
||||||
this._movie.onended = () => { this.status = PsychoJS.Status.FINISHED; };
|
this._movie.onended = () =>
|
||||||
|
{
|
||||||
|
this.status = PsychoJS.Status.FINISHED;
|
||||||
|
};
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error)
|
||||||
throw Object.assign(response, { error });
|
{
|
||||||
|
throw Object.assign(response, {error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,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 {number} volume - the volume of the audio track (must be between 0.0 and 1.0)
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setVolume(volume, log = false) {
|
setVolume(volume, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('volume', volume, log);
|
this._setAttribute('volume', volume, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -142,7 +162,8 @@ export class MovieStim extends VisualStim {
|
|||||||
* @param {boolean} noAudio - whether or not to mute the audio
|
* @param {boolean} noAudio - whether or not to mute the audio
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setNoAudio(noAudio, log = false) {
|
setNoAudio(noAudio, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('noAudio', noAudio, log);
|
this._setAttribute('noAudio', noAudio, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -156,7 +177,8 @@ export class MovieStim extends VisualStim {
|
|||||||
* @param {boolean} flipVert - whether or not to flip vertically
|
* @param {boolean} flipVert - whether or not to flip vertically
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipVert(flipVert, log = false) {
|
setFlipVert(flipVert, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('flipVert', flipVert, log);
|
this._setAttribute('flipVert', flipVert, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -171,7 +193,8 @@ export class MovieStim extends VisualStim {
|
|||||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipHoriz(flipHoriz, log = false) {
|
setFlipHoriz(flipHoriz, log = false)
|
||||||
|
{
|
||||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -183,10 +206,14 @@ export class MovieStim extends VisualStim {
|
|||||||
*
|
*
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
reset(log = false) {
|
reset(log = false)
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.NOT_STARTED;
|
this.status = PsychoJS.Status.NOT_STARTED;
|
||||||
this._movie.pause();
|
this._movie.pause();
|
||||||
if (this._hasFastSeek) this._movie.fastSeek(0);
|
if (this._hasFastSeek)
|
||||||
|
{
|
||||||
|
this._movie.fastSeek(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -195,7 +222,8 @@ export class MovieStim extends VisualStim {
|
|||||||
*
|
*
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
play(log = false) {
|
play(log = false)
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.STARTED;
|
this.status = PsychoJS.Status.STARTED;
|
||||||
this._movie.play();
|
this._movie.play();
|
||||||
}
|
}
|
||||||
@ -206,7 +234,8 @@ export class MovieStim extends VisualStim {
|
|||||||
*
|
*
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
pause(log = false) {
|
pause(log = false)
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.STOPPED;
|
this.status = PsychoJS.Status.STOPPED;
|
||||||
this._movie.pause();
|
this._movie.pause();
|
||||||
}
|
}
|
||||||
@ -217,12 +246,15 @@ export class MovieStim extends VisualStim {
|
|||||||
*
|
*
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
stop(log = false) {
|
stop(log = false)
|
||||||
|
{
|
||||||
this.status = PsychoJS.Status.STOPPED;
|
this.status = PsychoJS.Status.STOPPED;
|
||||||
this._movie.pause();
|
this._movie.pause();
|
||||||
if (this._hasFastSeek) this._movie.fastSeek(0);
|
if (this._hasFastSeek)
|
||||||
|
{
|
||||||
|
this._movie.fastSeek(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -233,8 +265,10 @@ export class MovieStim extends VisualStim {
|
|||||||
* @param {number} timePoint - the timepoint to which to jump (in second)
|
* @param {number} timePoint - the timepoint to which to jump (in second)
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
seek(timePoint, log = false) {
|
seek(timePoint, log = false)
|
||||||
if (timePoint < 0 || timePoint > this._movie.duration) {
|
{
|
||||||
|
if (timePoint < 0 || timePoint > this._movie.duration)
|
||||||
|
{
|
||||||
throw {
|
throw {
|
||||||
origin: 'MovieStim.seek',
|
origin: 'MovieStim.seek',
|
||||||
context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`,
|
context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`,
|
||||||
@ -243,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
|
* @param {string} units - the units
|
||||||
* @return {boolean} whether or not the image contains the object
|
* @return {boolean} whether or not the image contains the object
|
||||||
*/
|
*/
|
||||||
contains(object, units) {
|
contains(object, units)
|
||||||
|
{
|
||||||
// get position of object:
|
// get position of object:
|
||||||
let objectPos_px = util.getPositionFromObject(object, units);
|
let objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined') {
|
if (typeof objectPos_px === 'undefined')
|
||||||
|
{
|
||||||
throw {
|
throw {
|
||||||
origin: 'MovieStim.contains',
|
origin: 'MovieStim.contains',
|
||||||
context: `when determining whether MovieStim: ${this._name} contains object: ${util.toString(object)}`,
|
context: `when determining whether MovieStim: ${this._name} contains object: ${util.toString(object)}`,
|
||||||
@ -287,16 +326,21 @@ export class MovieStim extends VisualStim {
|
|||||||
* @name module:visual.MovieStim#_updateIfNeeded
|
* @name module:visual.MovieStim#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded() {
|
_updateIfNeeded()
|
||||||
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._pixi = undefined;
|
this._pixi = undefined;
|
||||||
|
|
||||||
// no movie to draw: return immediately
|
// no movie to draw: return immediately
|
||||||
if (typeof this._movie === 'undefined')
|
if (typeof this._movie === 'undefined')
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// create a PixiJS video sprite:
|
// create a PixiJS video sprite:
|
||||||
this._texture = PIXI.Texture.fromVideo(this._movie);
|
this._texture = PIXI.Texture.fromVideo(this._movie);
|
||||||
@ -305,7 +349,8 @@ export class MovieStim extends VisualStim {
|
|||||||
|
|
||||||
// since _texture.width may not be immedialy available but the rest of the code needs its value
|
// since _texture.width may not be immedialy available but the rest of the code needs its value
|
||||||
// we arrange for repeated calls to _updateIfNeeded until we have a width:
|
// we arrange for repeated calls to _updateIfNeeded until we have a width:
|
||||||
if (this._texture.width === 0) {
|
if (this._texture.width === 0)
|
||||||
|
{
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -325,7 +370,8 @@ export class MovieStim extends VisualStim {
|
|||||||
// stimulus size:
|
// stimulus size:
|
||||||
// note: we use the size of the texture if MovieStim has no specified size:
|
// note: we use the size of the texture if MovieStim has no specified size:
|
||||||
let stimSize = this.size;
|
let stimSize = this.size;
|
||||||
if (typeof stimSize === 'undefined') {
|
if (typeof stimSize === 'undefined')
|
||||||
|
{
|
||||||
const textureSize = [this._texture.width, this._texture.height];
|
const textureSize = [this._texture.width, this._texture.height];
|
||||||
stimSize = util.to_unit(textureSize, 'pix', this.win, this.units);
|
stimSize = util.to_unit(textureSize, 'pix', this.win, this.units);
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
* Polygonal Stimulus.
|
* Polygonal Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { ShapeStim } from './ShapeStim';
|
import {ShapeStim} from './ShapeStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,7 +59,23 @@ export class Polygon extends ShapeStim
|
|||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({ name, win, lineWidth, lineColor, fillColor, opacity, pos, ori, size, units, contrast, depth, interpolate, autoDraw, autoLog });
|
super({
|
||||||
|
name,
|
||||||
|
win,
|
||||||
|
lineWidth,
|
||||||
|
lineColor,
|
||||||
|
fillColor,
|
||||||
|
opacity,
|
||||||
|
pos,
|
||||||
|
ori,
|
||||||
|
size,
|
||||||
|
units,
|
||||||
|
contrast,
|
||||||
|
depth,
|
||||||
|
interpolate,
|
||||||
|
autoDraw,
|
||||||
|
autoLog
|
||||||
|
});
|
||||||
|
|
||||||
this._psychoJS.logger.debug('create a new Polygon with name: ', name);
|
this._psychoJS.logger.debug('create a new Polygon with name: ', name);
|
||||||
|
|
||||||
@ -68,9 +84,10 @@ export class Polygon extends ShapeStim
|
|||||||
this._updateVertices();
|
this._updateVertices();
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,7 +107,6 @@ export class Polygon extends ShapeStim
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the edges attribute.
|
* Setter for the edges attribute.
|
||||||
*
|
*
|
||||||
@ -121,7 +137,9 @@ export class Polygon extends ShapeStim
|
|||||||
const angle = 2.0 * Math.PI / this._edges;
|
const angle = 2.0 * Math.PI / this._edges;
|
||||||
const vertices = [];
|
const vertices = [];
|
||||||
for (let v = 0; v < this._edges; ++v)
|
for (let v = 0; v < this._edges; ++v)
|
||||||
vertices.push([ Math.sin(v * angle) * this._radius, Math.cos(v * angle) * this._radius ]);
|
{
|
||||||
|
vertices.push([Math.sin(v * angle) * this._radius, Math.cos(v * angle) * this._radius]);
|
||||||
|
}
|
||||||
this.setVertices(vertices);
|
this.setVertices(vertices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
* Rectangular Stimulus.
|
* Rectangular Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { ShapeStim } from './ShapeStim';
|
import {ShapeStim} from './ShapeStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,7 +59,23 @@ export class Rect extends ShapeStim
|
|||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({ name, win, lineWidth, lineColor, fillColor, opacity, pos, ori, size, units, contrast, depth, interpolate, autoDraw, autoLog });
|
super({
|
||||||
|
name,
|
||||||
|
win,
|
||||||
|
lineWidth,
|
||||||
|
lineColor,
|
||||||
|
fillColor,
|
||||||
|
opacity,
|
||||||
|
pos,
|
||||||
|
ori,
|
||||||
|
size,
|
||||||
|
units,
|
||||||
|
contrast,
|
||||||
|
depth,
|
||||||
|
interpolate,
|
||||||
|
autoDraw,
|
||||||
|
autoLog
|
||||||
|
});
|
||||||
|
|
||||||
this._psychoJS.logger.debug('create a new Rect with name: ', name);
|
this._psychoJS.logger.debug('create a new Rect with name: ', name);
|
||||||
|
|
||||||
@ -68,9 +84,10 @@ export class Rect extends ShapeStim
|
|||||||
this._updateVertices();
|
this._updateVertices();
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,7 +107,6 @@ export class Rect extends ShapeStim
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the height attribute.
|
* Setter for the height attribute.
|
||||||
*
|
*
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
* Basic Shape Stimulus.
|
* Basic Shape Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { VisualStim } from './VisualStim';
|
import {VisualStim} from './VisualStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { ColorMixin } from '../util/ColorMixin';
|
import {ColorMixin} from '../util/ColorMixin';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||||
|
|
||||||
// the PIXI polygon corresponding to the vertices, in pixel units:
|
// the PIXI polygon corresponding to the vertices, in pixel units:
|
||||||
this._pixiPolygon_px = undefined;
|
this._pixiPolygon_px = undefined;
|
||||||
@ -76,7 +76,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force a refresh of the stimulus.
|
* Force a refresh of the stimulus.
|
||||||
*
|
*
|
||||||
@ -91,7 +90,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the size attribute.
|
* Setter for the size attribute.
|
||||||
*
|
*
|
||||||
@ -108,7 +106,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setter for the line width attribute.
|
* Setter for the line width attribute.
|
||||||
*
|
*
|
||||||
@ -180,10 +177,14 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
if (typeof vertices === 'string')
|
if (typeof vertices === 'string')
|
||||||
{
|
{
|
||||||
if (vertices in ShapeStim.KnownShapes)
|
if (vertices in ShapeStim.KnownShapes)
|
||||||
|
{
|
||||||
vertices = ShapeStim.KnownShapes[vertices];
|
vertices = ShapeStim.KnownShapes[vertices];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
throw 'unknown shape';
|
throw 'unknown shape';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._setAttribute('vertices', vertices, log);
|
this._setAttribute('vertices', vertices, log);
|
||||||
// this._setAttribute({
|
// this._setAttribute({
|
||||||
@ -197,7 +198,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
throw Object.assign(response, { error: error });
|
throw Object.assign(response, {error: error});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,13 +212,20 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {string} units - the units
|
* @param {string} units - the units
|
||||||
* @return {boolean} whether or not the stimulus contains the object
|
* @return {boolean} whether or not the stimulus contains the object
|
||||||
*/
|
*/
|
||||||
contains(object, units) {
|
contains(object, units)
|
||||||
|
{
|
||||||
this._psychoJS.logger.debug('test whether BaseShameStim:', this.name, 'contains object: ', ('name' in object) ? object.name : object);
|
this._psychoJS.logger.debug('test whether BaseShameStim:', this.name, 'contains object: ', ('name' in object) ? object.name : object);
|
||||||
|
|
||||||
// get position of object:
|
// get position of object:
|
||||||
const objectPos_px = util.getPositionFromObject(object, units);
|
const objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined')
|
if (typeof objectPos_px === 'undefined')
|
||||||
throw { origin : 'ShapeStim.contains', context : 'when determining whether BaseShameStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'ShapeStim.contains',
|
||||||
|
context: 'when determining whether BaseShameStim: ' + this._name + ' contains object: ' + util.toString(object),
|
||||||
|
error: 'unable to determine the position of the object'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// test for inclusion
|
// test for inclusion
|
||||||
// note: the vertices are centered around (0, 0) so we need to add to them the stimulus' position
|
// note: the vertices are centered around (0, 0) so we need to add to them the stimulus' position
|
||||||
@ -234,9 +242,12 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.ShapeStim#_updateIfNeeded
|
* @name module:visual.ShapeStim#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded() {
|
_updateIfNeeded()
|
||||||
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._getPolygon(/*true*/); // this also updates _vertices_px
|
this._getPolygon(/*true*/); // this also updates _vertices_px
|
||||||
@ -245,16 +256,22 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// no polygon to draw: return immediately
|
// no polygon to draw: return immediately
|
||||||
if (typeof this._pixiPolygon_px === 'undefined')
|
if (typeof this._pixiPolygon_px === 'undefined')
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// prepare the polygon in the given color and opacity:
|
// prepare the polygon in the given color and opacity:
|
||||||
this._pixi = new PIXI.Graphics();
|
this._pixi = new PIXI.Graphics();
|
||||||
this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5);
|
this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5);
|
||||||
if (typeof this._fillColor !== 'undefined')
|
if (typeof this._fillColor !== 'undefined')
|
||||||
|
{
|
||||||
this._pixi.beginFill(this._fillColor.int, this._opacity);
|
this._pixi.beginFill(this._fillColor.int, this._opacity);
|
||||||
|
}
|
||||||
this._pixi.drawPolygon(this._pixiPolygon_px);
|
this._pixi.drawPolygon(this._pixiPolygon_px);
|
||||||
if (typeof this._fillColor !== 'undefined')
|
if (typeof this._fillColor !== 'undefined')
|
||||||
|
{
|
||||||
this._pixi.endFill();
|
this._pixi.endFill();
|
||||||
|
}
|
||||||
|
|
||||||
// set polygon position and rotation:
|
// set polygon position and rotation:
|
||||||
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
|
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
|
||||||
@ -262,7 +279,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the PIXI polygon (in pixel units) corresponding to the vertices.
|
* Get the PIXI polygon (in pixel units) corresponding to the vertices.
|
||||||
*
|
*
|
||||||
@ -270,9 +286,12 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @private
|
* @private
|
||||||
* @return {Object} the PIXI polygon corresponding to this stimulus vertices.
|
* @return {Object} the PIXI polygon corresponding to this stimulus vertices.
|
||||||
*/
|
*/
|
||||||
_getPolygon(/*force = false*/) {
|
_getPolygon(/*force = false*/)
|
||||||
|
{
|
||||||
if (!this._needVertexUpdate)
|
if (!this._needVertexUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needVertexUpdate = false;
|
this._needVertexUpdate = false;
|
||||||
|
|
||||||
console.log('>>>>>>>>> CREATING PIXI POLYGON!!!!');
|
console.log('>>>>>>>>> CREATING PIXI POLYGON!!!!');
|
||||||
@ -285,13 +304,17 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._getVertices_px(/*force*/);
|
this._getVertices_px(/*force*/);
|
||||||
let coords_px = [];
|
let coords_px = [];
|
||||||
for (const vertex_px of this._vertices_px)
|
for (const vertex_px of this._vertices_px)
|
||||||
|
{
|
||||||
coords_px.push.apply(coords_px, vertex_px);
|
coords_px.push.apply(coords_px, vertex_px);
|
||||||
|
}
|
||||||
|
|
||||||
// close the polygon if need be:
|
// close the polygon if need be:
|
||||||
if (coords_px.length >= 6 && this._closeShape) {
|
if (coords_px.length >= 6 && this._closeShape)
|
||||||
|
{
|
||||||
// note: we first check whether the vertices already define a closed polygon:
|
// note: we first check whether the vertices already define a closed polygon:
|
||||||
const n = coords_px.length;
|
const n = coords_px.length;
|
||||||
if (coords_px[0] !== coords_px[n - 2] || coords_px[1] !== coords_px[n - 1]) {
|
if (coords_px[0] !== coords_px[n - 2] || coords_px[1] !== coords_px[n - 1])
|
||||||
|
{
|
||||||
coords_px.push(coords_px[0]);
|
coords_px.push(coords_px[0]);
|
||||||
coords_px.push(coords_px[1]);
|
coords_px.push(coords_px[1]);
|
||||||
}
|
}
|
||||||
@ -303,7 +326,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the vertices in pixel units.
|
* Get the vertices in pixel units.
|
||||||
*
|
*
|
||||||
@ -319,12 +341,16 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
// handle flipping:
|
// handle flipping:
|
||||||
let flip = [1.0, 1.0];
|
let flip = [1.0, 1.0];
|
||||||
if ('_flipHoriz' in this && this._flipHoriz)
|
if ('_flipHoriz' in this && this._flipHoriz)
|
||||||
|
{
|
||||||
flip[0] = -1.0;
|
flip[0] = -1.0;
|
||||||
|
}
|
||||||
if ('_flipVert' in this && this._flipVert)
|
if ('_flipVert' in this && this._flipVert)
|
||||||
|
{
|
||||||
flip[1] = -1.0;
|
flip[1] = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
// handle size, flipping, and convert to pixel units:
|
// handle size, flipping, and convert to pixel units:
|
||||||
this._vertices_px = this._vertices.map( v => util.to_px([v[0] * this._size[0] * flip[0], v[1] * this._size[1] * flip[1]], this._units, this._win) );
|
this._vertices_px = this._vertices.map(v => util.to_px([v[0] * this._size[0] * flip[0], v[1] * this._size[1] * flip[1]], this._units, this._win));
|
||||||
|
|
||||||
return this._vertices_px;
|
return this._vertices_px;
|
||||||
}
|
}
|
||||||
@ -355,20 +381,20 @@ ShapeStim.KnownShapes = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
star7: [
|
star7: [
|
||||||
[0.0,0.5],
|
[0.0, 0.5],
|
||||||
[0.09,0.18],
|
[0.09, 0.18],
|
||||||
[0.39,0.31],
|
[0.39, 0.31],
|
||||||
[0.19,0.04],
|
[0.19, 0.04],
|
||||||
[0.49,-0.11],
|
[0.49, -0.11],
|
||||||
[0.16,-0.12],
|
[0.16, -0.12],
|
||||||
[0.22,-0.45],
|
[0.22, -0.45],
|
||||||
[0.0,-0.2],
|
[0.0, -0.2],
|
||||||
[-0.22,-0.45],
|
[-0.22, -0.45],
|
||||||
[-0.16,-0.12],
|
[-0.16, -0.12],
|
||||||
[-0.49,-0.11],
|
[-0.49, -0.11],
|
||||||
[-0.19,0.04],
|
[-0.19, 0.04],
|
||||||
[-0.39,0.31],
|
[-0.39, 0.31],
|
||||||
[-0.09,0.18]
|
[-0.09, 0.18]
|
||||||
]
|
]
|
||||||
|
|
||||||
};
|
};
|
@ -2,7 +2,7 @@
|
|||||||
* Slider Stimulus.
|
* Slider Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -74,7 +74,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
opacity,
|
opacity,
|
||||||
|
|
||||||
style = [Slider.Style.RATING],
|
style = [Slider.Style.RATING],
|
||||||
ticks = [1,2,3,4,5],
|
ticks = [1, 2, 3, 4, 5],
|
||||||
labels = [],
|
labels = [],
|
||||||
labelHeight,
|
labelHeight,
|
||||||
granularity = 0,
|
granularity = 0,
|
||||||
@ -88,8 +88,9 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
autoDraw,
|
autoDraw,
|
||||||
autoLog
|
autoLog
|
||||||
} = {}) {
|
} = {})
|
||||||
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
|
{
|
||||||
|
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
|
||||||
|
|
||||||
this._needMarkerUpdate = false;
|
this._needMarkerUpdate = false;
|
||||||
|
|
||||||
@ -113,8 +114,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._isCategorical = (this._ticks.length === 0);
|
this._isCategorical = (this._ticks.length === 0);
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,12 +131,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
*
|
*
|
||||||
* @todo this is currently not implemented and always returns false
|
* @todo this is currently not implemented and always returns false
|
||||||
*/
|
*/
|
||||||
contains(object, units) {
|
contains(object, units)
|
||||||
|
{
|
||||||
// get position of object:
|
// get position of object:
|
||||||
let objectPos_px = util.getPositionFromObject(object, units);
|
let objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined')
|
if (typeof objectPos_px === 'undefined')
|
||||||
throw { origin : 'Slider.contains', context : `when determining whether Slider: ${this._name} contains
|
{
|
||||||
object: ${util.toString(object)}`, error : 'unable to determine the position of the object' };
|
throw {
|
||||||
|
origin: 'Slider.contains', context: `when determining whether Slider: ${this._name} contains
|
||||||
|
object: ${util.toString(object)}`, error: 'unable to determine the position of the object'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -145,7 +153,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#reset
|
* @name module:visual.Slider#reset
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset()
|
||||||
|
{
|
||||||
this.psychoJS.logger.debug('reset Slider: ', this._name);
|
this.psychoJS.logger.debug('reset Slider: ', this._name);
|
||||||
|
|
||||||
this._markerPos = undefined;
|
this._markerPos = undefined;
|
||||||
@ -159,8 +168,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// the marker should be invisible when markerPos is undefined:
|
// the marker should be invisible when markerPos is undefined:
|
||||||
if (typeof this._marker !== 'undefined')
|
if (typeof this._marker !== 'undefined')
|
||||||
|
{
|
||||||
this._marker.alpha = 0;
|
this._marker.alpha = 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,13 +181,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @public
|
* @public
|
||||||
* @returns {number | undefined} the rating or undefined if there is none
|
* @returns {number | undefined} the rating or undefined if there is none
|
||||||
*/
|
*/
|
||||||
getRating() {
|
getRating()
|
||||||
|
{
|
||||||
const historyLength = this._history.length;
|
const historyLength = this._history.length;
|
||||||
if (historyLength > 0)
|
if (historyLength > 0)
|
||||||
return this._history[historyLength-1]['rating'];
|
{
|
||||||
|
return this._history[historyLength - 1]['rating'];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,13 +202,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @public
|
* @public
|
||||||
* @returns {number | undefined} the response time (in second) or undefined if there is none
|
* @returns {number | undefined} the response time (in second) or undefined if there is none
|
||||||
*/
|
*/
|
||||||
getRT() {
|
getRT()
|
||||||
|
{
|
||||||
const historyLength = this._history.length;
|
const historyLength = this._history.length;
|
||||||
if (historyLength > 0)
|
if (historyLength > 0)
|
||||||
return this._history[historyLength-1]['responseTime'];
|
{
|
||||||
|
return this._history[historyLength - 1]['responseTime'];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -205,14 +226,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} [fontSize] - the font size
|
* @param {number} [fontSize] - the font size
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFontSize(fontSize, log = false) {
|
setFontSize(fontSize, log = false)
|
||||||
if (typeof fontSize === 'undefined') {
|
{
|
||||||
|
if (typeof fontSize === 'undefined')
|
||||||
|
{
|
||||||
fontSize = (this._units === 'pix') ? 14 : 0.03;
|
fontSize = (this._units === 'pix') ? 14 : 0.03;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanged = this._setAttribute('fontSize', fontSize, log);
|
const hasChanged = this._setAttribute('fontSize', fontSize, log);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged)
|
||||||
|
{
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
this._needVertexUpdate = true;
|
this._needVertexUpdate = true;
|
||||||
}
|
}
|
||||||
@ -226,11 +250,13 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} [bold= true] - whether or not the font of the labels is bold
|
* @param {boolean} [bold= true] - whether or not the font of the labels is bold
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setBold(bold = true, log = false) {
|
setBold(bold = true, log = false)
|
||||||
|
{
|
||||||
const hasChanged = this._setAttribute('bold', bold, log);
|
const hasChanged = this._setAttribute('bold', bold, log);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged)
|
||||||
this._fontWeight = (bold)?'bold':'normal';
|
{
|
||||||
|
this._fontWeight = (bold) ? 'bold' : 'normal';
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
this._needVertexUpdate = true;
|
this._needVertexUpdate = true;
|
||||||
}
|
}
|
||||||
@ -245,11 +271,13 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} [italic= false] - whether or not the font of the labels is italic
|
* @param {boolean} [italic= false] - whether or not the font of the labels is italic
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setItalic(italic = false, log = false) {
|
setItalic(italic = false, log = false)
|
||||||
|
{
|
||||||
const hasChanged = this._setAttribute('italic', italic, log);
|
const hasChanged = this._setAttribute('italic', italic, log);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged)
|
||||||
this._fontStyle = (italic)?'italic':'normal';
|
{
|
||||||
|
this._fontStyle = (italic) ? 'italic' : 'normal';
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
this._needVertexUpdate = true;
|
this._needVertexUpdate = true;
|
||||||
}
|
}
|
||||||
@ -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} [readOnly= true] - whether or not the slider is read-only
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setReadOnly(readOnly = true, log = false) {
|
setReadOnly(readOnly = true, log = false)
|
||||||
|
{
|
||||||
const hasChanged = this._setAttribute('readOnly', readOnly, log);
|
const hasChanged = this._setAttribute('readOnly', readOnly, log);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged)
|
||||||
|
{
|
||||||
// halve the opacity:
|
// halve the opacity:
|
||||||
if (readOnly)
|
if (readOnly)
|
||||||
|
{
|
||||||
this._opacity /= 2.0;
|
this._opacity /= 2.0;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
this._opacity *= 2.0;
|
this._opacity *= 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
@ -294,12 +328,14 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} displayedRating - the displayed rating
|
* @param {number} displayedRating - the displayed rating
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setMarkerPos(displayedRating, log = false) {
|
setMarkerPos(displayedRating, log = false)
|
||||||
|
{
|
||||||
const previousMarkerPos = this._markerPos;
|
const previousMarkerPos = this._markerPos;
|
||||||
this._markerPos = this._granularise(displayedRating);
|
this._markerPos = this._granularise(displayedRating);
|
||||||
|
|
||||||
// if the displayed rating has changed, we need to update the pixi representation:
|
// if the displayed rating has changed, we need to update the pixi representation:
|
||||||
if (previousMarkerPos !== this._markerPos) {
|
if (previousMarkerPos !== this._markerPos)
|
||||||
|
{
|
||||||
this._needMarkerUpdate = true;
|
this._needMarkerUpdate = true;
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
}
|
}
|
||||||
@ -316,11 +352,14 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} rating - the rating
|
* @param {number} rating - the rating
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setRating(rating, log = false) {
|
setRating(rating, log = false)
|
||||||
|
{
|
||||||
rating = this._granularise(rating);
|
rating = this._granularise(rating);
|
||||||
this._markerPos = rating;
|
this._markerPos = rating;
|
||||||
if (this._isCategorical)
|
if (this._isCategorical)
|
||||||
|
{
|
||||||
rating = this._labels[Math.round(rating)];
|
rating = this._labels[Math.round(rating)];
|
||||||
|
}
|
||||||
|
|
||||||
this._setAttribute('rating', rating, log);
|
this._setAttribute('rating', rating, log);
|
||||||
}
|
}
|
||||||
@ -337,10 +376,13 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} [responseTime] - the reaction time
|
* @param {number} [responseTime] - the reaction time
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
_recordRating(rating, responseTime = undefined, log = false) {
|
_recordRating(rating, responseTime = undefined, log = false)
|
||||||
|
{
|
||||||
// get response time:
|
// get response time:
|
||||||
if (typeof responseTime === 'undefined')
|
if (typeof responseTime === 'undefined')
|
||||||
|
{
|
||||||
responseTime = this._responseClock.getTime();
|
responseTime = this._responseClock.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
// set rating:
|
// set rating:
|
||||||
// rating = this._granularise(rating);
|
// rating = this._granularise(rating);
|
||||||
@ -363,9 +405,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#_updateIfNeeded
|
* @name module:visual.Slider#_updateIfNeeded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateIfNeeded() {
|
_updateIfNeeded()
|
||||||
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._buildSlider();
|
this._buildSlider();
|
||||||
@ -387,21 +432,28 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#_updateMarker
|
* @name module:visual.Slider#_updateMarker
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateMarker() {
|
_updateMarker()
|
||||||
|
{
|
||||||
if (!this._needMarkerUpdate)
|
if (!this._needMarkerUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needMarkerUpdate = false;
|
this._needMarkerUpdate = false;
|
||||||
|
|
||||||
if (typeof this._marker !== 'undefined') {
|
if (typeof this._marker !== 'undefined')
|
||||||
if (typeof this._markerPos !== 'undefined') {
|
{
|
||||||
|
if (typeof this._markerPos !== 'undefined')
|
||||||
|
{
|
||||||
const visibleMarkerPos = this._ratingToPos([this._markerPos]);
|
const visibleMarkerPos = this._ratingToPos([this._markerPos]);
|
||||||
this._marker.position = util.to_pixiPoint(visibleMarkerPos[0], this.units, this.win);
|
this._marker.position = util.to_pixiPoint(visibleMarkerPos[0], this.units, this.win);
|
||||||
this._marker.alpha = 1;
|
this._marker.alpha = 1;
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this._marker.alpha = 0;
|
this._marker.alpha = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -412,9 +464,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#_buildSlider
|
* @name module:visual.Slider#_buildSlider
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_buildSlider() {
|
_buildSlider()
|
||||||
|
{
|
||||||
if (!this._needVertexUpdate)
|
if (!this._needVertexUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needVertexUpdate = false;
|
this._needVertexUpdate = false;
|
||||||
|
|
||||||
this._applyStyle();
|
this._applyStyle();
|
||||||
@ -430,30 +485,39 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// (*) central bar:
|
// (*) central bar:
|
||||||
const barSize_px = util.to_px(this._barSize, this._units, this._win).map(v => Math.max(1, v));
|
const barSize_px = util.to_px(this._barSize, this._units, this._win).map(v => Math.max(1, v));
|
||||||
if (this._barLineWidth_px > 0) {
|
if (this._barLineWidth_px > 0)
|
||||||
|
{
|
||||||
this._body.lineStyle(this._barLineWidth_px, this._barLineColor.int, this._opacity, 0.5);
|
this._body.lineStyle(this._barLineWidth_px, this._barLineColor.int, this._opacity, 0.5);
|
||||||
if (typeof this._barFillColor !== 'undefined')
|
if (typeof this._barFillColor !== 'undefined')
|
||||||
|
{
|
||||||
this._body.beginFill(this._barFillColor.int, this._opacity);
|
this._body.beginFill(this._barFillColor.int, this._opacity);
|
||||||
|
}
|
||||||
this._body.drawRect(-barSize_px[0] / 2, -barSize_px[1] / 2, barSize_px[0], barSize_px[1]);
|
this._body.drawRect(-barSize_px[0] / 2, -barSize_px[1] / 2, barSize_px[0], barSize_px[1]);
|
||||||
if (typeof this._barFillColor !== 'undefined')
|
if (typeof this._barFillColor !== 'undefined')
|
||||||
|
{
|
||||||
this._body.endFill();
|
this._body.endFill();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// (*) ticks:
|
// (*) ticks:
|
||||||
if (this._isCategorical) {
|
if (this._isCategorical)
|
||||||
|
{
|
||||||
this._ticks = [...Array(this._labels.length)].map((_, i) => i);
|
this._ticks = [...Array(this._labels.length)].map((_, i) => i);
|
||||||
this._granularity = 1.0;
|
this._granularity = 1.0;
|
||||||
}
|
}
|
||||||
const tickPositions = this._ratingToPos(this._ticks);
|
const tickPositions = this._ratingToPos(this._ticks);
|
||||||
const tickPositions_px = tickPositions.map( p => util.to_px(p, this._units, this._win));
|
const tickPositions_px = tickPositions.map(p => util.to_px(p, this._units, this._win));
|
||||||
this._body.lineStyle(this._barLineWidth_px*2, this._tickColor.int, this._opacity, 0.5);
|
this._body.lineStyle(this._barLineWidth_px * 2, this._tickColor.int, this._opacity, 0.5);
|
||||||
const tickSize_px = util.to_px(this._tickSize, this._units, this._win);
|
const tickSize_px = util.to_px(this._tickSize, this._units, this._win);
|
||||||
for (let tickPosition_px of tickPositions_px) {
|
for (let tickPosition_px of tickPositions_px)
|
||||||
if (this._tickType === Slider.Shape.LINE) {
|
{
|
||||||
this._body.moveTo(tickPosition_px[0] - tickSize_px[0]/2, tickPosition_px[1] - tickSize_px[1]/2);
|
if (this._tickType === Slider.Shape.LINE)
|
||||||
this._body.lineTo(tickPosition_px[0] + tickSize_px[0]/2, tickPosition_px[1] + tickSize_px[1]/2);
|
{
|
||||||
|
this._body.moveTo(tickPosition_px[0] - tickSize_px[0] / 2, tickPosition_px[1] - tickSize_px[1] / 2);
|
||||||
|
this._body.lineTo(tickPosition_px[0] + tickSize_px[0] / 2, tickPosition_px[1] + tickSize_px[1] / 2);
|
||||||
}
|
}
|
||||||
else if (this._tickType === Slider.Shape.DISC) {
|
else if (this._tickType === Slider.Shape.DISC)
|
||||||
|
{
|
||||||
this._body.beginFill(this._tickColor.int, this._opacity);
|
this._body.beginFill(this._tickColor.int, this._opacity);
|
||||||
this._body.drawCircle(tickPosition_px[0], tickPosition_px[1], Math.max(tickSize_px[0], tickSize_px[1]));
|
this._body.drawCircle(tickPosition_px[0], tickPosition_px[1], Math.max(tickSize_px[0], tickSize_px[1]));
|
||||||
this._body.endFill();
|
this._body.endFill();
|
||||||
@ -465,7 +529,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
// outside of marker and labels:
|
// outside of marker and labels:
|
||||||
const eventCaptureRectangle = new PIXI.Graphics();
|
const eventCaptureRectangle = new PIXI.Graphics();
|
||||||
eventCaptureRectangle.beginFill(0, 0);
|
eventCaptureRectangle.beginFill(0, 0);
|
||||||
eventCaptureRectangle.drawRect(-barSize_px[0]/2 - tickSize_px[0]/2, -barSize_px[1]/2 - tickSize_px[1]/2,
|
eventCaptureRectangle.drawRect(-barSize_px[0] / 2 - tickSize_px[0] / 2, -barSize_px[1] / 2 - tickSize_px[1] / 2,
|
||||||
barSize_px[0] + tickSize_px[0], barSize_px[1] + tickSize_px[1]);
|
barSize_px[0] + tickSize_px[0], barSize_px[1] + tickSize_px[1]);
|
||||||
eventCaptureRectangle.endFill();
|
eventCaptureRectangle.endFill();
|
||||||
this._pixi.addChild(eventCaptureRectangle);
|
this._pixi.addChild(eventCaptureRectangle);
|
||||||
@ -473,17 +537,19 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
// (*) labels:
|
// (*) labels:
|
||||||
const labelPositions_px = [...Array(this._labels.length)].map(
|
const labelPositions_px = [...Array(this._labels.length)].map(
|
||||||
(_, i) => tickPositions_px[Math.round(i / (this._labels.length-1) * (this._ticks.length-1))]);
|
(_, i) => tickPositions_px[Math.round(i / (this._labels.length - 1) * (this._ticks.length - 1))]);
|
||||||
|
|
||||||
const fontSize_px = util.to_px([this._fontSize, this._fontSize], this._units, this._win);
|
const fontSize_px = util.to_px([this._fontSize, this._fontSize], this._units, this._win);
|
||||||
for (let l = 0; l < labelPositions_px.length; ++l) {
|
for (let l = 0; l < labelPositions_px.length; ++l)
|
||||||
|
{
|
||||||
const labelText = new PIXI.Text(this._labels[l], {
|
const labelText = new PIXI.Text(this._labels[l], {
|
||||||
fontFamily : this._fontFamily,
|
fontFamily: this._fontFamily,
|
||||||
fontWeight: this._fontWeight,
|
fontWeight: this._fontWeight,
|
||||||
fontStyle: this._fontStyle,
|
fontStyle: this._fontStyle,
|
||||||
fontSize: Math.round(fontSize_px[0]),
|
fontSize: Math.round(fontSize_px[0]),
|
||||||
fill: this._labelColor.hex,
|
fill: this._labelColor.hex,
|
||||||
align: this._labelAlign});
|
align: this._labelAlign
|
||||||
|
});
|
||||||
|
|
||||||
const labelBounds = labelText.getBounds(true);
|
const labelBounds = labelText.getBounds(true);
|
||||||
labelText.position.x = labelPositions_px[l][0];
|
labelText.position.x = labelPositions_px[l][0];
|
||||||
@ -491,20 +557,32 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
labelText.anchor.x = this._labelAnchor.x;
|
labelText.anchor.x = this._labelAnchor.x;
|
||||||
labelText.anchor.y = this._labelAnchor.y;
|
labelText.anchor.y = this._labelAnchor.y;
|
||||||
|
|
||||||
if (this._isHorizontal()) {
|
if (this._isHorizontal())
|
||||||
|
{
|
||||||
if (this._flip)
|
if (this._flip)
|
||||||
|
{
|
||||||
labelText.position.y -= labelBounds.height + tickSize_px[1];
|
labelText.position.y -= labelBounds.height + tickSize_px[1];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
labelText.position.y += tickSize_px[1];
|
labelText.position.y += tickSize_px[1];
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (this._flip)
|
if (this._flip)
|
||||||
|
{
|
||||||
labelText.position.x += tickSize_px[0];
|
labelText.position.x += tickSize_px[0];
|
||||||
else
|
}
|
||||||
if (this._labelOri === 0)
|
else if (this._labelOri === 0)
|
||||||
|
{
|
||||||
labelText.position.x -= labelBounds.width + tickSize_px[0];
|
labelText.position.x -= labelBounds.width + tickSize_px[0];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
labelText.position.x -= tickSize_px[0];
|
labelText.position.x -= tickSize_px[0];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
labelText.rotation = (this._ori + this._labelOri) * Math.PI / 180;
|
labelText.rotation = (this._ori + this._labelOri) * Math.PI / 180;
|
||||||
labelText.alpha = this._opacity;
|
labelText.alpha = this._opacity;
|
||||||
@ -519,31 +597,42 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._marker.interactive = true;
|
this._marker.interactive = true;
|
||||||
this._pixi.addChild(this._marker);
|
this._pixi.addChild(this._marker);
|
||||||
|
|
||||||
if (this._markerType === Slider.Shape.DISC) {
|
if (this._markerType === Slider.Shape.DISC)
|
||||||
|
{
|
||||||
this._marker.lineStyle(1, this._markerColor.int, this._opacity, 0.5);
|
this._marker.lineStyle(1, this._markerColor.int, this._opacity, 0.5);
|
||||||
this._marker.beginFill(this._markerColor.int, this._opacity);
|
this._marker.beginFill(this._markerColor.int, this._opacity);
|
||||||
this._marker.drawCircle(0, 0, markerSize_px/2);
|
this._marker.drawCircle(0, 0, markerSize_px / 2);
|
||||||
this._marker.endFill();
|
this._marker.endFill();
|
||||||
}
|
}
|
||||||
else if (this._markerType === Slider.Shape.TRIANGLE) {
|
else if (this._markerType === Slider.Shape.TRIANGLE)
|
||||||
|
{
|
||||||
this._marker.lineStyle(1, this._markerColor.int, this._opacity, 0.5);
|
this._marker.lineStyle(1, this._markerColor.int, this._opacity, 0.5);
|
||||||
this._marker.beginFill(this._markerColor.int, this._opacity);
|
this._marker.beginFill(this._markerColor.int, this._opacity);
|
||||||
this._marker.moveTo(0, 0);
|
this._marker.moveTo(0, 0);
|
||||||
if (this._isHorizontal()) {
|
if (this._isHorizontal())
|
||||||
if (this._flip) {
|
{
|
||||||
this._marker.lineTo(markerSize_px/2, markerSize_px/2);
|
if (this._flip)
|
||||||
this._marker.lineTo(-markerSize_px/2, markerSize_px/2);
|
{
|
||||||
} else {
|
this._marker.lineTo(markerSize_px / 2, markerSize_px / 2);
|
||||||
this._marker.lineTo(markerSize_px/2, -markerSize_px/2);
|
this._marker.lineTo(-markerSize_px / 2, markerSize_px / 2);
|
||||||
this._marker.lineTo(-markerSize_px/2, -markerSize_px/2);
|
|
||||||
}
|
}
|
||||||
} else {
|
else
|
||||||
if (this._flip) {
|
{
|
||||||
this._marker.lineTo(-markerSize_px/2, markerSize_px/2);
|
this._marker.lineTo(markerSize_px / 2, -markerSize_px / 2);
|
||||||
this._marker.lineTo(-markerSize_px/2, -markerSize_px/2);
|
this._marker.lineTo(-markerSize_px / 2, -markerSize_px / 2);
|
||||||
} else {
|
}
|
||||||
this._marker.lineTo(markerSize_px/2, markerSize_px/2);
|
}
|
||||||
this._marker.lineTo(markerSize_px/2, -markerSize_px/2);
|
else
|
||||||
|
{
|
||||||
|
if (this._flip)
|
||||||
|
{
|
||||||
|
this._marker.lineTo(-markerSize_px / 2, markerSize_px / 2);
|
||||||
|
this._marker.lineTo(-markerSize_px / 2, -markerSize_px / 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._marker.lineTo(markerSize_px / 2, markerSize_px / 2);
|
||||||
|
this._marker.lineTo(markerSize_px / 2, -markerSize_px / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._marker.endFill();
|
this._marker.endFill();
|
||||||
@ -554,10 +643,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
const self = this;
|
const self = this;
|
||||||
self._markerDragging = false;
|
self._markerDragging = false;
|
||||||
|
|
||||||
this._marker.pointerdown = this._marker.mousedown = this._marker.touchstart = (event) => {
|
this._marker.pointerdown = this._marker.mousedown = this._marker.touchstart = (event) =>
|
||||||
if (event.data.button === 0) {
|
{
|
||||||
|
if (event.data.button === 0)
|
||||||
|
{
|
||||||
self._markerDragging = true;
|
self._markerDragging = true;
|
||||||
/* not quite right, just yet (as of May 2020)
|
/* not quite right, just yet (as of May 2020)
|
||||||
// set markerPos, but not rating:
|
// set markerPos, but not rating:
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||||
@ -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
|
// pointer was released inside the marker: if we were dragging, we record the rating
|
||||||
this._marker.pointerup = this._marker.mouseup = this._marker.touchend = (event) => {
|
this._marker.pointerup = this._marker.mouseup = this._marker.touchend = (event) =>
|
||||||
if (self._markerDragging) {
|
{
|
||||||
|
if (self._markerDragging)
|
||||||
|
{
|
||||||
self._markerDragging = false;
|
self._markerDragging = false;
|
||||||
|
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||||
@ -584,8 +677,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
};
|
};
|
||||||
|
|
||||||
// pointer was released outside of the marker: cancel the dragging
|
// pointer was released outside of the marker: cancel the dragging
|
||||||
this._marker.pointerupoutside = this._marker.mouseupoutside = this._marker.touchendoutside = (event) => {
|
this._marker.pointerupoutside = this._marker.mouseupoutside = this._marker.touchendoutside = (event) =>
|
||||||
if (self._markerDragging) {
|
{
|
||||||
|
if (self._markerDragging)
|
||||||
|
{
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||||
self._recordRating(rating);
|
self._recordRating(rating);
|
||||||
@ -597,8 +692,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
};
|
};
|
||||||
|
|
||||||
// pointer is moving: if we are dragging, we move the marker position
|
// pointer is moving: if we are dragging, we move the marker position
|
||||||
this._marker.pointermove = (event) => {
|
this._marker.pointermove = (event) =>
|
||||||
if (self._markerDragging) {
|
{
|
||||||
|
if (self._markerDragging)
|
||||||
|
{
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._pixi);
|
||||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||||
self.setMarkerPos(rating);
|
self.setMarkerPos(rating);
|
||||||
@ -631,7 +728,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
};
|
};
|
||||||
*/
|
*/
|
||||||
|
|
||||||
this._pixi.pointerup = this._pixi.mouseup = this._pixi.touchend = (event) => {
|
this._pixi.pointerup = this._pixi.mouseup = this._pixi.touchend = (event) =>
|
||||||
|
{
|
||||||
const mouseLocalPos_px = event.data.getLocalPosition(self._body);
|
const mouseLocalPos_px = event.data.getLocalPosition(self._body);
|
||||||
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
const rating = self._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]);
|
||||||
self._recordRating(rating);
|
self._recordRating(rating);
|
||||||
@ -650,14 +748,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @name module:visual.Slider#_applyStyle
|
* @name module:visual.Slider#_applyStyle
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_applyStyle() {
|
_applyStyle()
|
||||||
|
{
|
||||||
|
|
||||||
// default style:
|
// default style:
|
||||||
if (this._isHorizontal()) {
|
if (this._isHorizontal())
|
||||||
|
{
|
||||||
this._barSize = [this._size[0], 0];
|
this._barSize = [this._size[0], 0];
|
||||||
this._tickSize = [0, this._size[1]];
|
this._tickSize = [0, this._size[1]];
|
||||||
this._labelAnchor = new PIXI.Point(0.5, 0);
|
this._labelAnchor = new PIXI.Point(0.5, 0);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this._barSize = [0, this._size[1]];
|
this._barSize = [0, this._size[1]];
|
||||||
this._tickSize = [this._size[0], 0];
|
this._tickSize = [this._size[0], 0];
|
||||||
this._labelAnchor = new PIXI.Point(0, 0.5);
|
this._labelAnchor = new PIXI.Point(0, 0.5);
|
||||||
@ -681,24 +783,28 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
|
|
||||||
|
|
||||||
// rating:
|
// rating:
|
||||||
if (this._style.indexOf(Slider.Style.RATING) > -1) {
|
if (this._style.indexOf(Slider.Style.RATING) > -1)
|
||||||
|
{
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
// triangleMarker:
|
// triangleMarker:
|
||||||
if (this._style.indexOf(Slider.Style.TRIANGLE_MARKER) > -1) {
|
if (this._style.indexOf(Slider.Style.TRIANGLE_MARKER) > -1)
|
||||||
|
{
|
||||||
this._markerType = Slider.Shape.TRIANGLE;
|
this._markerType = Slider.Shape.TRIANGLE;
|
||||||
this._markerSize = this._markerSize.map( s => s*2 );
|
this._markerSize = this._markerSize.map(s => s * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// slider:
|
// slider:
|
||||||
if (this._style.indexOf(Slider.Style.SLIDER) > -1) {
|
if (this._style.indexOf(Slider.Style.SLIDER) > -1)
|
||||||
|
{
|
||||||
this.psychoJS.logger.warn('"slider" style not implemented!');
|
this.psychoJS.logger.warn('"slider" style not implemented!');
|
||||||
//TODO
|
//TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
// whiteOnBlack:
|
// whiteOnBlack:
|
||||||
if (this._style.indexOf(Slider.Style.WHITE_ON_BLACK) > -1) {
|
if (this._style.indexOf(Slider.Style.WHITE_ON_BLACK) > -1)
|
||||||
|
{
|
||||||
this._barLineColor = new Color('black');
|
this._barLineColor = new Color('black');
|
||||||
// this._barFillColor = new Color('black');
|
// this._barFillColor = new Color('black');
|
||||||
this._tickColor = new Color('black');
|
this._tickColor = new Color('black');
|
||||||
@ -707,11 +813,15 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// labels45:
|
// labels45:
|
||||||
if (this._style.indexOf(Slider.Style.LABELS45) > -1) {
|
if (this._style.indexOf(Slider.Style.LABELS45) > -1)
|
||||||
if (this._flip) {
|
{
|
||||||
|
if (this._flip)
|
||||||
|
{
|
||||||
this._labelAnchor = new PIXI.Point(0, 0.5);
|
this._labelAnchor = new PIXI.Point(0, 0.5);
|
||||||
this._labelAlign = 'left';
|
this._labelAlign = 'left';
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this._labelAnchor = new PIXI.Point(1, 0.5);
|
this._labelAnchor = new PIXI.Point(1, 0.5);
|
||||||
this._labelAlign = 'right';
|
this._labelAlign = 'right';
|
||||||
}
|
}
|
||||||
@ -719,7 +829,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// radio:
|
// radio:
|
||||||
if (this._style.indexOf(Slider.Style.RADIO) > -1) {
|
if (this._style.indexOf(Slider.Style.RADIO) > -1)
|
||||||
|
{
|
||||||
this._barLineWidth_px = 0;
|
this._barLineWidth_px = 0;
|
||||||
this._tickType = Slider.Shape.DISC;
|
this._tickType = Slider.Shape.DISC;
|
||||||
|
|
||||||
@ -739,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,
|
* @returns {Array.<Array.<number>>} the positions corresponding to the ratings (in Slider units,
|
||||||
* with 0 at the center of the Slider)
|
* with 0 at the center of the Slider)
|
||||||
*/
|
*/
|
||||||
_ratingToPos(ratings) {
|
_ratingToPos(ratings)
|
||||||
const range = this._ticks[this._ticks.length-1] - this._ticks[0];
|
{
|
||||||
|
const range = this._ticks[this._ticks.length - 1] - this._ticks[0];
|
||||||
if (this._isHorizontal())
|
if (this._isHorizontal())
|
||||||
return ratings.map( v => [((v-this._ticks[0])/range-0.5) * this._size[0], 0]);
|
{
|
||||||
|
return ratings.map(v => [((v - this._ticks[0]) / range - 0.5) * this._size[0], 0]);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
return ratings.map( v => [0, (1.0 - (v-this._ticks[0])/range - 0.5) * this._size[1]]);
|
{
|
||||||
|
return ratings.map(v => [0, (1.0 - (v - this._ticks[0]) / range - 0.5) * this._size[1]]);
|
||||||
|
}
|
||||||
// return ratings.map( v => [0, ((v-this._ticks[0])/range-0.5) * this._size[1]]);
|
// return ratings.map( v => [0, ((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.
|
* @param {number[]} pos_px - the [x,y] position, in pixel units, relative to the slider.
|
||||||
* @returns {number} the corresponding rating.
|
* @returns {number} the corresponding rating.
|
||||||
*/
|
*/
|
||||||
_posToRating(pos_px) {
|
_posToRating(pos_px)
|
||||||
const range = this._ticks[this._ticks.length-1] - this._ticks[0];
|
{
|
||||||
|
const range = this._ticks[this._ticks.length - 1] - this._ticks[0];
|
||||||
const size_px = util.to_px(this._size, this._units, this._win);
|
const size_px = util.to_px(this._size, this._units, this._win);
|
||||||
if (this._isHorizontal())
|
if (this._isHorizontal())
|
||||||
|
{
|
||||||
return (pos_px[0] / size_px[0] + 0.5) * range + this._ticks[0];
|
return (pos_px[0] / size_px[0] + 0.5) * range + this._ticks[0];
|
||||||
// return ((pos_px[0]-this._pixi.position.x) / size_px[0] + 0.5) * range + this._ticks[0];
|
}// return ((pos_px[0]-this._pixi.position.x) / size_px[0] + 0.5) * range + this._ticks[0];
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return (1.0 - (pos_px[1] / size_px[1] + 0.5)) * range + this._ticks[0];
|
return (1.0 - (pos_px[1] / size_px[1] + 0.5)) * range + this._ticks[0];
|
||||||
|
}
|
||||||
// return (pos_px[1] / size_px[1] + 0.5) * range + this._ticks[0];
|
// return (pos_px[1] / size_px[1] + 0.5) * range + this._ticks[0];
|
||||||
// return ((pos_px[1]-this._pixi.position.y) / size_px[1] + 0.5) * range + this._ticks[0];
|
// return ((pos_px[1]-this._pixi.position.y) / size_px[1] + 0.5) * range + this._ticks[0];
|
||||||
}
|
}
|
||||||
@ -779,7 +899,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @private
|
* @private
|
||||||
* @returns {boolean} whether or not the slider is horizontal
|
* @returns {boolean} whether or not the slider is horizontal
|
||||||
*/
|
*/
|
||||||
_isHorizontal() {
|
_isHorizontal()
|
||||||
|
{
|
||||||
return (this._size[0] > this._size[1]);
|
return (this._size[0] > this._size[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -792,13 +913,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} rating - the input rating
|
* @param {number} rating - the input rating
|
||||||
* @returns {number} the new rating with granularity applied
|
* @returns {number} the new rating with granularity applied
|
||||||
*/
|
*/
|
||||||
_granularise(rating) {
|
_granularise(rating)
|
||||||
|
{
|
||||||
if (typeof rating === 'undefined')
|
if (typeof rating === 'undefined')
|
||||||
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._granularity > 0)
|
if (this._granularity > 0)
|
||||||
rating = Math.round( rating / this._granularity ) * this._granularity;
|
{
|
||||||
rating = Math.min( Math.max(this._ticks[0], rating), this._ticks[this._ticks.length-1]);
|
rating = Math.round(rating / this._granularity) * this._granularity;
|
||||||
|
}
|
||||||
|
rating = Math.min(Math.max(this._ticks[0], rating), this._ticks[this._ticks.length - 1]);
|
||||||
|
|
||||||
return rating;
|
return rating;
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
* Text Stimulus.
|
* Text Stimulus.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { VisualStim } from './VisualStim';
|
import {VisualStim} from './VisualStim';
|
||||||
import { Color } from '../util/Color';
|
import {Color} from '../util/Color';
|
||||||
import { ColorMixin } from '../util/ColorMixin';
|
import {ColorMixin} from '../util/ColorMixin';
|
||||||
import * as util from '../util/Util';
|
import * as util from '../util/Util';
|
||||||
|
|
||||||
|
|
||||||
@ -68,13 +68,15 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
autoLog
|
autoLog
|
||||||
} = {})
|
} = {})
|
||||||
{
|
{
|
||||||
super({ name, win, units, ori, opacity, pos, autoDraw, autoLog });
|
super({name, win, units, ori, opacity, pos, autoDraw, autoLog});
|
||||||
|
|
||||||
this._addAttributes(TextStim, text, font, color, contrast, height, bold, italic, alignHoriz, alignVert, wrapWidth, flipHoriz, flipVert);
|
this._addAttributes(TextStim, text, font, color, contrast, height, bold, italic, alignHoriz, alignVert, wrapWidth, flipHoriz, flipVert);
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,7 +104,8 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {string} alignHoriz - the text horizontal alignment, e.g. 'center'
|
* @param {string} alignHoriz - the text horizontal alignment, e.g. 'center'
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setAlignHoriz(alignHoriz, log) {
|
setAlignHoriz(alignHoriz, log)
|
||||||
|
{
|
||||||
this._setAttribute('alignHoriz', alignHoriz, log);
|
this._setAttribute('alignHoriz', alignHoriz, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -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} wrapWidth - whether or not to wrap the text at the given width
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setWrapWidth(wrapWidth, log) {
|
setWrapWidth(wrapWidth, log)
|
||||||
if (typeof wrapWidth === 'undefined') {
|
{
|
||||||
|
if (typeof wrapWidth === 'undefined')
|
||||||
|
{
|
||||||
if (!TextStim._defaultWrapWidthMap.has(this._units))
|
if (!TextStim._defaultWrapWidthMap.has(this._units))
|
||||||
throw { origin : 'TextStim.setWrapWidth', context : 'when setting the wrap width of TextStim: ' + this._name, error : 'no default wrap width for unit: ' + this._units};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'TextStim.setWrapWidth',
|
||||||
|
context: 'when setting the wrap width of TextStim: ' + this._name,
|
||||||
|
error: 'no default wrap width for unit: ' + this._units
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
wrapWidth = TextStim._defaultWrapWidthMap.get(this._units);
|
wrapWidth = TextStim._defaultWrapWidthMap.get(this._units);
|
||||||
}
|
}
|
||||||
@ -141,10 +152,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {number} height - text height
|
* @param {number} height - text height
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setHeight(height, log) {
|
setHeight(height, log)
|
||||||
if (typeof height === 'undefined') {
|
{
|
||||||
|
if (typeof height === 'undefined')
|
||||||
|
{
|
||||||
if (!TextStim._defaultLetterHeightMap.has(this._units))
|
if (!TextStim._defaultLetterHeightMap.has(this._units))
|
||||||
throw { origin : 'TextStim.setHeight', context : 'when setting the height of TextStim: ' + this._name, error : 'no default letter height for unit: ' + this._units};
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'TextStim.setHeight',
|
||||||
|
context: 'when setting the height of TextStim: ' + this._name,
|
||||||
|
error: 'no default letter height for unit: ' + this._units
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
height = TextStim._defaultLetterHeightMap.get(this._units);
|
height = TextStim._defaultLetterHeightMap.get(this._units);
|
||||||
}
|
}
|
||||||
@ -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} italic - whether or not the text is italic
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setItalic(italic, log) {
|
setItalic(italic, log)
|
||||||
|
{
|
||||||
this._setAttribute('italic', italic, log);
|
this._setAttribute('italic', italic, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -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} bold - whether or not the text is bold
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setBold(bold, log) {
|
setBold(bold, log)
|
||||||
|
{
|
||||||
this._setAttribute('bold', bold, log);
|
this._setAttribute('bold', bold, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -196,7 +217,8 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} flipVert - whether or not to flip vertically
|
* @param {boolean} flipVert - whether or not to flip vertically
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipVert(flipVert, log) {
|
setFlipVert(flipVert, log)
|
||||||
|
{
|
||||||
this._setAttribute('flipVert', flipVert, log);
|
this._setAttribute('flipVert', flipVert, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -212,7 +234,8 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
* @param {boolean} flipHoriz - whether or not to flip horizontally
|
||||||
* @param {boolean} [log= false] - whether of not to log
|
* @param {boolean} [log= false] - whether of not to log
|
||||||
*/
|
*/
|
||||||
setFlipHoriz(flipHoriz, log) {
|
setFlipHoriz(flipHoriz, log)
|
||||||
|
{
|
||||||
this._setAttribute('flipHoriz', flipHoriz, log);
|
this._setAttribute('flipHoriz', flipHoriz, log);
|
||||||
|
|
||||||
this._needUpdate = true;
|
this._needUpdate = true;
|
||||||
@ -231,11 +254,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
*
|
*
|
||||||
* @todo this is currently NOT implemented
|
* @todo this is currently NOT implemented
|
||||||
*/
|
*/
|
||||||
contains(object, units) {
|
contains(object, units)
|
||||||
|
{
|
||||||
// get position of object:
|
// get position of object:
|
||||||
let objectPos_px = util.getPositionFromObject(object, units);
|
let objectPos_px = util.getPositionFromObject(object, units);
|
||||||
if (typeof objectPos_px === 'undefined')
|
if (typeof objectPos_px === 'undefined')
|
||||||
throw { origin : 'TextStim.contains', context : 'when determining whether TextStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
|
{
|
||||||
|
throw {
|
||||||
|
origin: 'TextStim.contains',
|
||||||
|
context: 'when determining whether TextStim: ' + this._name + ' contains object: ' + util.toString(object),
|
||||||
|
error: 'unable to determine the position of the object'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// test for inclusion:
|
// test for inclusion:
|
||||||
// TODO
|
// TODO
|
||||||
@ -254,7 +284,9 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
_updateIfNeeded()
|
_updateIfNeeded()
|
||||||
{
|
{
|
||||||
if (!this._needUpdate)
|
if (!this._needUpdate)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
this._needUpdate = false;
|
this._needUpdate = false;
|
||||||
|
|
||||||
this._heightPix = this._getLengthPix(this._height);
|
this._heightPix = this._getLengthPix(this._height);
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Base class for all visual stimuli.
|
* Base class for all visual stimuli.
|
||||||
*
|
*
|
||||||
* @author Alain Pitiot
|
* @author Alain Pitiot
|
||||||
* @version 2020.1
|
* @version 2020.5
|
||||||
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
|
||||||
* @license Distributed under the terms of the MIT License
|
* @license Distributed under the terms of the MIT License
|
||||||
*/
|
*/
|
||||||
@ -76,11 +76,14 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
|
|||||||
setSize(size, log = false)
|
setSize(size, log = false)
|
||||||
{
|
{
|
||||||
// size is either undefined or a tuple of numbers:
|
// size is either undefined or a tuple of numbers:
|
||||||
if (typeof size !== 'undefined') {
|
if (typeof size !== 'undefined')
|
||||||
|
{
|
||||||
size = util.toNumerical(size);
|
size = util.toNumerical(size);
|
||||||
if (!Array.isArray(size))
|
if (!Array.isArray(size))
|
||||||
|
{
|
||||||
size = [size, size];
|
size = [size, size];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._setAttribute('size', size, log);
|
this._setAttribute('size', size, log);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user