mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 10:40:54 +00:00
commit
36e6f8131a
1002
package-lock.json
generated
1002
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "psychojs",
|
||||
"version": "2022.3.1",
|
||||
"version": "2023.2.0",
|
||||
"private": true,
|
||||
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
|
||||
"license": "MIT",
|
||||
@ -34,6 +34,7 @@
|
||||
"howler": "^2.2.1",
|
||||
"log4javascript": "github:Ritzlgrmft/log4javascript",
|
||||
"pako": "^1.0.10",
|
||||
"pixi-filters": "^5.0.0",
|
||||
"pixi.js-legacy": "^6.0.4",
|
||||
"seedrandom": "^3.0.5",
|
||||
"tone": "^14.7.77",
|
||||
|
@ -302,7 +302,13 @@ export class EventManager
|
||||
{
|
||||
const timestamp = MonotonicClock.getReferenceTime();
|
||||
|
||||
let code = event.code;
|
||||
// Note: we are using event.key since we are interested in the input character rather than
|
||||
// the physical key position on the keyboard, i.e. we need to take into account the keyboard
|
||||
// layout
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding
|
||||
// event.code's lack of suitability
|
||||
let code = EventManager._pygletMap[event.key];
|
||||
// let code = event.code;
|
||||
|
||||
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
||||
if (typeof code === "undefined")
|
||||
|
@ -50,6 +50,9 @@ export class GUI
|
||||
{
|
||||
this._psychoJS = psychoJS;
|
||||
|
||||
// info fields excluded from the GUI:
|
||||
this._excludedInfo = {};
|
||||
|
||||
// gui listens to RESOURCE events from the server manager:
|
||||
psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) =>
|
||||
{
|
||||
@ -87,9 +90,6 @@ export class GUI
|
||||
requireParticipantClick = GUI.DEFAULT_SETTINGS.DlgFromDict.requireParticipantClick
|
||||
})
|
||||
{
|
||||
// get info from URL:
|
||||
const infoFromUrl = util.getUrlParameters();
|
||||
|
||||
this._progressBarMax = 0;
|
||||
this._allResourcesDownloaded = false;
|
||||
this._requiredKeys = [];
|
||||
@ -113,6 +113,19 @@ export class GUI
|
||||
self._dialogComponent.tStart = t;
|
||||
self._dialogComponent.status = PsychoJS.Status.STARTED;
|
||||
|
||||
// prepare the info fields excluded from the GUI, including those from the URL:
|
||||
const excludedInfo = {};
|
||||
for (let key in self._excludedInfo)
|
||||
{
|
||||
excludedInfo[key.trim().toLowerCase()] = self._excludedInfo[key];
|
||||
}
|
||||
const infoFromUrl = util.getUrlParameters();
|
||||
infoFromUrl.forEach((value, key) =>
|
||||
{
|
||||
excludedInfo[key.trim().toLowerCase()] = value;
|
||||
});
|
||||
|
||||
|
||||
// if the experiment is licensed, and running on the license rather than on credit,
|
||||
// we use the license logo:
|
||||
if (self._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER
|
||||
@ -130,7 +143,13 @@ export class GUI
|
||||
markup += "<div class='dialog-content'>";
|
||||
|
||||
// alert title and close button:
|
||||
markup += `<div id='experiment-dialog-title' class='dialog-title'><p>${title}</p><button id='dialogClose' class='dialog-close' data-a11y-dialog-hide aria-label='Cancel Experiment'>×</button></div>`;
|
||||
markup += "<div id='experiment-dialog-title' class='dialog-title'>";
|
||||
markup += `<p>${title}</p>`;
|
||||
markup += "<button id='dialogClose' class='dialog-close' data-a11y-dialog-hide aria-label='Cancel Experiment'>×</button>";
|
||||
markup += "</div>";
|
||||
|
||||
// everything above the buttons is in a scrollable container:
|
||||
markup += "<div class='scrollable-container'>";
|
||||
|
||||
// logo, if need be:
|
||||
if (typeof logoUrl === "string")
|
||||
@ -139,14 +158,16 @@ export class GUI
|
||||
}
|
||||
|
||||
// add a combobox or text areas for each entry in the dictionary:
|
||||
let atLeastOneIncludedKey = false;
|
||||
Object.keys(dictionary).forEach((key, keyIdx) =>
|
||||
{
|
||||
const value = dictionary[key];
|
||||
const keyId = "form-input-" + keyIdx;
|
||||
|
||||
// only create an input if the key is not in the URL:
|
||||
let inUrl = false;
|
||||
const cleanedDictKey = key.trim().toLowerCase();
|
||||
const isIncluded = !(cleanedDictKey in excludedInfo);
|
||||
/*let inUrl = false;
|
||||
infoFromUrl.forEach((urlValue, urlKey) =>
|
||||
{
|
||||
const cleanedUrlKey = urlKey.trim().toLowerCase();
|
||||
@ -155,10 +176,13 @@ export class GUI
|
||||
inUrl = true;
|
||||
// break;
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
if (!inUrl)
|
||||
if (isIncluded)
|
||||
// if (!inUrl)
|
||||
{
|
||||
atLeastOneIncludedKey = true;
|
||||
|
||||
markup += `<label for='${keyId}'> ${key} </label>`;
|
||||
|
||||
// if the field is required:
|
||||
@ -199,17 +223,27 @@ export class GUI
|
||||
markup += "<p class='validateTips'>Fields marked with an asterisk (*) are required.</p>";
|
||||
}
|
||||
|
||||
markup += "</div>"; // scrollable-container
|
||||
|
||||
// separator, if need be:
|
||||
if (atLeastOneIncludedKey)
|
||||
{
|
||||
markup += "<hr>";
|
||||
}
|
||||
|
||||
// progress bar:
|
||||
markup += `<hr><div id='progressMsg' class='progress-msg'>${self._progressMessage}</div>`;
|
||||
markup += `<div id='progressMsg' class='progress-msg'>${self._progressMessage}</div>`;
|
||||
markup += "<div class='progress-container'><div id='progressBar' class='progress-bar'></div></div>";
|
||||
|
||||
// buttons:
|
||||
markup += "<hr>";
|
||||
markup += "<div class='dialog-button-group'>";
|
||||
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Cancel Experiment'>Cancel</button>";
|
||||
if (self._requireParticipantClick)
|
||||
{
|
||||
markup += "<button id='dialogOK' class='dialog-button disabled' aria-label='Start Experiment'>Ok</button>";
|
||||
}
|
||||
markup += "</div>"; // button-group
|
||||
|
||||
markup += "</div></div>";
|
||||
|
||||
@ -346,14 +380,18 @@ export class GUI
|
||||
{
|
||||
const error = this._userFriendlyError(errorCode);
|
||||
markup += `<div id='experiment-dialog-title' class='dialog-title ${error.class}'><p>${error.title}</p></div>`;
|
||||
markup += "<div class='scrollable-container'>";
|
||||
markup += `<p>${error.text}</p>`;
|
||||
markup += "</div>";
|
||||
}
|
||||
else
|
||||
{
|
||||
markup += `<div id='experiment-dialog-title' class='dialog-title dialog-error'><p>Error</p></div>`;
|
||||
markup += "<div class='scrollable-container'>";
|
||||
markup += `<p>Unfortunately we encountered the following error:</p>`;
|
||||
markup += stackCode;
|
||||
markup += "<p>Try to run the experiment again. If the error persists, contact the experiment designer.</p>";
|
||||
markup += "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,20 +399,27 @@ export class GUI
|
||||
else if (typeof warning !== "undefined")
|
||||
{
|
||||
markup += `<div id='experiment-dialog-title' class='dialog-title dialog-warning'><p>Warning</p></div>`;
|
||||
markup += "<div class='scrollable-container'>";
|
||||
markup += `<p>${warning}</p>`;
|
||||
markup += "</div>";
|
||||
}
|
||||
|
||||
// we are displaying a message:
|
||||
else if (typeof message !== "undefined")
|
||||
{
|
||||
markup += `<div id='experiment-dialog-title' class='dialog-title'><p>Message</p></div>`;
|
||||
markup += "<div id='experiment-dialog-title' class='dialog-title'><p>Message</p></div>";
|
||||
markup += "<div class='scrollable-container'>";
|
||||
markup += `<p>${message}</p>`;
|
||||
markup += "</div>";
|
||||
}
|
||||
|
||||
if (showOK || showCancel)
|
||||
{
|
||||
markup += "<hr>";
|
||||
}
|
||||
if (showCancel || showOK)
|
||||
{
|
||||
markup += "<div class='button-group'>";
|
||||
if (showCancel)
|
||||
{
|
||||
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Close dialog'>Cancel</button>";
|
||||
@ -383,6 +428,8 @@ export class GUI
|
||||
{
|
||||
markup += "<button id='dialogOK' class='dialog-button' aria-label='Close dialog'>Ok</button>";
|
||||
}
|
||||
markup += "</div>"; // button-group
|
||||
}
|
||||
markup += "</div></div>";
|
||||
|
||||
// replace root by the markup code:
|
||||
|
@ -354,7 +354,13 @@ export class Keyboard extends PsychObject
|
||||
*/
|
||||
self._previousKeydownKey = event.key;
|
||||
|
||||
let code = event.code;
|
||||
// Note: we are using event.key since we are interested in the input character rather than
|
||||
// the physical key position on the keyboard, i.e. we need to take into account the keyboard
|
||||
// layout
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding
|
||||
// event.code's lack of suitability
|
||||
let code = EventManager._pygletMap[event.key];
|
||||
// let code = event.code;
|
||||
|
||||
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
|
||||
if (typeof code === "undefined")
|
||||
@ -394,7 +400,9 @@ export class Keyboard extends PsychObject
|
||||
|
||||
self._previousKeydownKey = undefined;
|
||||
|
||||
let code = event.code;
|
||||
// Note: see above for explanation regarding the use of event.key in lieu of event.code
|
||||
let code = EventManager._pygletMap[event.key];
|
||||
// let code = event.code;
|
||||
|
||||
// take care of legacy Microsoft Edge:
|
||||
if (typeof code === "undefined")
|
||||
|
@ -530,6 +530,7 @@ export class PsychoJS
|
||||
const response = { origin: "PsychoJS.quit", context: "when terminating the experiment" };
|
||||
|
||||
this._experiment.experimentEnded = true;
|
||||
this._experiment.isCompleted = isCompleted;
|
||||
this.status = PsychoJS.Status.STOPPED;
|
||||
const isServerEnv = (this.getEnvironment() === ExperimentHandler.Environment.SERVER);
|
||||
|
||||
@ -601,7 +602,7 @@ export class PsychoJS
|
||||
|
||||
if (showOK)
|
||||
{
|
||||
let text = "Thank you for your patience.<br/><br/>";
|
||||
let text = "Thank you for your patience.";
|
||||
text += (typeof message !== "undefined") ? message : "Goodbye!";
|
||||
this._gui.dialog({
|
||||
message: text,
|
||||
|
@ -1293,7 +1293,7 @@ export class ServerManager extends PsychObject
|
||||
}
|
||||
|
||||
// font files:
|
||||
else if (["ttf", "otf", "woff", "woff2"].indexOf(pathExtension) > -1)
|
||||
else if (["ttf", "otf", "woff", "woff2","eot"].indexOf(pathExtension) > -1)
|
||||
{
|
||||
fontResources.push(name);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { MonotonicClock } from "../util/Clock.js";
|
||||
import { Color } from "../util/Color.js";
|
||||
import { PsychObject } from "../util/PsychObject.js";
|
||||
import { Logger } from "./Logger.js";
|
||||
import { hasTouchScreen } from "../util/Util.js";
|
||||
|
||||
/**
|
||||
* <p>Window displays the various stimuli of the experiment.</p>
|
||||
@ -181,7 +182,7 @@ export class Window extends PsychObject
|
||||
{
|
||||
// gets updated frame by frame
|
||||
const lastDelta = this.psychoJS.scheduler._lastDelta;
|
||||
const fps = lastDelta === 0 ? 60.0 : 1000 / lastDelta;
|
||||
const fps = (lastDelta === 0) ? 60.0 : (1000.0 / lastDelta);
|
||||
|
||||
return fps;
|
||||
}
|
||||
@ -493,6 +494,17 @@ export class Window extends PsychObject
|
||||
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
|
||||
this._resizeCallback = (e) =>
|
||||
{
|
||||
// if the user device is a mobile phone or tablet (we use the presence of a touch screen as a
|
||||
// proxy), we need to detect whether the change in size is due to the appearance of a virtual keyboard
|
||||
// in which case we do not want to resize the canvas. This is rather tricky and so we resort to
|
||||
// the below trick. It would be better to use the VirtualKeyboard API, but it is not widely
|
||||
// available just yet, as of 2023-06.
|
||||
const keyboardHeight = 300;
|
||||
if (hasTouchScreen() && (window.screen.height - window.visualViewport.height) > keyboardHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Window._resizePixiRenderer(this, e);
|
||||
this._backgroundSprite.width = this._size[0];
|
||||
this._backgroundSprite.height = this._size[1];
|
||||
|
@ -276,6 +276,7 @@ export class ExperimentHandler extends PsychObject
|
||||
}
|
||||
|
||||
let data = this._trialsData;
|
||||
|
||||
// if the experiment data have to be cleared, we first make a copy of them:
|
||||
if (clear)
|
||||
{
|
||||
@ -351,6 +352,19 @@ export class ExperimentHandler extends PsychObject
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the results of the experiment as a .csv string, ready to be uploaded or stored.
|
||||
*
|
||||
* @return {string} a .csv representation of the experiment results.
|
||||
*/
|
||||
getResultAsCsv()
|
||||
{
|
||||
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
|
||||
// newlines, etc.
|
||||
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
|
||||
return "\ufeff" + XLSX.utils.sheet_to_csv(worksheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attribute names and values for the current trial of a given loop.
|
||||
* <p> Only info relating to the trial execution are returned.</p>
|
||||
|
@ -26,7 +26,6 @@ body {
|
||||
|
||||
|
||||
/* Project and resource dialogs */
|
||||
|
||||
.dialog-container label,
|
||||
.dialog-container input,
|
||||
.dialog-container select {
|
||||
@ -40,6 +39,13 @@ body {
|
||||
margin-bottom: 1em;
|
||||
padding: 0.5em;
|
||||
width: 100%;
|
||||
|
||||
height: 34px;
|
||||
border: 1px solid #767676;
|
||||
border-radius: 2px;
|
||||
background: #ffffff;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dialog-container fieldset {
|
||||
@ -71,12 +77,19 @@ body {
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0;
|
||||
|
||||
margin: auto;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
|
||||
width: 500px;
|
||||
max-width: 88vw;
|
||||
/*max-height: 90vh;*/
|
||||
max-height: 93%;
|
||||
|
||||
padding: 0.5em;
|
||||
border-radius: 2px;
|
||||
|
||||
@ -88,11 +101,24 @@ body {
|
||||
box-shadow: 1px 1px 3px #555555;
|
||||
}
|
||||
|
||||
.dialog-content .scrollable-container {
|
||||
height: 100%;
|
||||
padding: 0 0.5em;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dialog-content hr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
padding: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
background-color: #009900;
|
||||
background-color: #00dd00;
|
||||
/*background-color: #009900;*/
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@ -111,6 +137,11 @@ body {
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1.1em;
|
||||
|
||||
position: absolute;
|
||||
top: 0.7em;
|
||||
right: 0.7em;
|
||||
@ -153,7 +184,7 @@ body {
|
||||
|
||||
.dialog-button {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
margin: 0.5em 0.5em 0.5em 0;
|
||||
/*margin: 0.5em 0.5em 0.5em 0;*/
|
||||
border: 1px solid #555555;
|
||||
border-radius: 2px;
|
||||
|
||||
@ -176,6 +207,14 @@ body {
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.dialog-button-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
column-gap: 0.5em;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
border: 1px solid #AAAAAA;
|
||||
color: #AAAAAA;
|
||||
@ -186,10 +225,15 @@ body {
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
|
||||
/*display: block;
|
||||
margin: 0 auto 1em;
|
||||
max-height: 20vh;
|
||||
max-width: 100%;
|
||||
max-width: 100%;*/
|
||||
}
|
||||
|
||||
a,
|
||||
|
@ -117,9 +117,12 @@ export class Scheduler
|
||||
* Start this scheduler.
|
||||
*
|
||||
* <p>Note: tasks are run after each animation frame.</p>
|
||||
*
|
||||
* @return {Promise<void>} a promise resolved when the scheduler stops, e.g. when the experiments finishes
|
||||
*/
|
||||
start()
|
||||
{
|
||||
let shedulerResolve;
|
||||
const self = this;
|
||||
const update = async (timestamp) =>
|
||||
{
|
||||
@ -127,6 +130,7 @@ export class Scheduler
|
||||
if (self._stopAtNextUpdate)
|
||||
{
|
||||
self._status = Scheduler.Status.STOPPED;
|
||||
shedulerResolve();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -137,6 +141,7 @@ export class Scheduler
|
||||
if (state === Scheduler.Event.QUIT)
|
||||
{
|
||||
self._status = Scheduler.Status.STOPPED;
|
||||
shedulerResolve();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -155,6 +160,12 @@ export class Scheduler
|
||||
|
||||
// start the animation:
|
||||
requestAnimationFrame(update);
|
||||
|
||||
// return a promise resolved when the scheduler is stopped:
|
||||
return new Promise((resolve, _) =>
|
||||
{
|
||||
shedulerResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -647,6 +647,11 @@ export function toString(object)
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
if (typeof object === "function")
|
||||
{
|
||||
return `<function ${object.name}>`;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const symbolReplacer = (key, value) =>
|
||||
@ -1473,6 +1478,47 @@ export function loadCss(cssId, cssPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user device has a touchscreen, e.g. it is a mobile phone or tablet.
|
||||
*
|
||||
* @return {boolean} true if the user device has a touchscreen.
|
||||
* @note the code below is directly adapted from MDN
|
||||
*/
|
||||
export function hasTouchScreen()
|
||||
{
|
||||
let hasTouchScreen = false;
|
||||
|
||||
if ("maxTouchPoints" in navigator)
|
||||
{
|
||||
hasTouchScreen = navigator.maxTouchPoints > 0;
|
||||
}
|
||||
else if ("msMaxTouchPoints" in navigator)
|
||||
{
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
const mQ = matchMedia?.("(pointer:coarse)");
|
||||
if (mQ?.media === "(pointer:coarse)")
|
||||
{
|
||||
hasTouchScreen = !!mQ.matches;
|
||||
}
|
||||
else if ("orientation" in window)
|
||||
{
|
||||
hasTouchScreen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
const UA = navigator.userAgent;
|
||||
hasTouchScreen =
|
||||
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
|
||||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
|
||||
}
|
||||
}
|
||||
|
||||
return hasTouchScreen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum that stores possible text directions.
|
||||
* Note that Arabic is the same as RTL but added here to support PsychoPy's
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
import { Mouse } from "../core/Mouse.js";
|
||||
import { TextBox } from "./TextBox.js";
|
||||
import * as util from "../util/Util";
|
||||
|
||||
/**
|
||||
* <p>ButtonStim visual stimulus.</p>
|
||||
@ -32,6 +33,7 @@ export class ButtonStim extends TextBox
|
||||
* @param {Color} [options.borderColor= Color("white")] the border color
|
||||
* @param {Color} [options.borderWidth= 0] the border width
|
||||
* @param {number} [options.opacity= 1.0] - the opacity
|
||||
* @param {number} [options.depth= 0] - the depth (i.e. the z order)
|
||||
* @param {number} [options.letterHeight= undefined] - the height of the text
|
||||
* @param {boolean} [options.bold= true] - whether or not the text is bold
|
||||
* @param {boolean} [options.italic= false] - whether or not the text is italic
|
||||
@ -54,11 +56,14 @@ export class ButtonStim extends TextBox
|
||||
borderColor,
|
||||
borderWidth = 0,
|
||||
opacity,
|
||||
depth,
|
||||
letterHeight,
|
||||
bold = true,
|
||||
italic,
|
||||
autoDraw,
|
||||
autoLog,
|
||||
boxFn,
|
||||
multiline
|
||||
} = {},
|
||||
)
|
||||
{
|
||||
@ -77,12 +82,15 @@ export class ButtonStim extends TextBox
|
||||
borderColor,
|
||||
borderWidth,
|
||||
opacity,
|
||||
depth,
|
||||
letterHeight,
|
||||
multiline,
|
||||
bold,
|
||||
italic,
|
||||
alignment: "center",
|
||||
autoDraw,
|
||||
autoLog,
|
||||
boxFn
|
||||
});
|
||||
|
||||
this.psychoJS.logger.debug("create a new Button with name: ", name);
|
||||
@ -112,7 +120,7 @@ export class ButtonStim extends TextBox
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
||||
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||
*/
|
||||
constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {})
|
||||
constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, aspectRatio, autoDraw, autoLog } = {})
|
||||
{
|
||||
super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog });
|
||||
|
||||
@ -94,6 +94,12 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
false,
|
||||
this._onChange(false, false),
|
||||
);
|
||||
this._addAttribute(
|
||||
"aspectRatio",
|
||||
aspectRatio,
|
||||
ImageStim.AspectRatioStrategy.VARIABLE,
|
||||
this._onChange(true, true),
|
||||
);
|
||||
|
||||
// estimate the bounding box:
|
||||
this._estimateBoundingBox();
|
||||
@ -309,7 +315,18 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image, texOpts));
|
||||
}
|
||||
|
||||
if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING)
|
||||
{
|
||||
const [width_px, _] = util.to_px([this.size[0], 0], this.units, this.win);
|
||||
this._pixi = PIXI.TilingSprite.from(this._texture, 1, 1);
|
||||
this._pixi.width = width_px;
|
||||
this._pixi.height = this._texture.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._pixi = PIXI.Sprite.from(this._texture);
|
||||
}
|
||||
|
||||
|
||||
// add a mask if need be:
|
||||
if (typeof this._mask !== "undefined")
|
||||
@ -349,8 +366,24 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
// set the scale:
|
||||
const displaySize = this._getDisplaySize();
|
||||
const size_px = util.to_px(displaySize, this.units, this.win);
|
||||
const scaleX = size_px[0] / this._texture.width;
|
||||
const scaleY = size_px[1] / this._texture.height;
|
||||
let scaleX = size_px[0] / this._texture.width;
|
||||
let scaleY = size_px[1] / this._texture.height;
|
||||
if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH)
|
||||
{
|
||||
scaleY = scaleX;
|
||||
}
|
||||
else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT)
|
||||
{
|
||||
scaleX = scaleY;
|
||||
}
|
||||
else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING)
|
||||
{
|
||||
scaleX = 1.0;
|
||||
scaleY = 1.0;
|
||||
}
|
||||
|
||||
// note: this calls VisualStim.setAnchor, which properly sets the PixiJS anchor
|
||||
// from the PsychoPy text format
|
||||
this.anchor = this._anchor;
|
||||
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
|
||||
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
|
||||
@ -383,7 +416,47 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
displaySize = util.to_unit(textureSize, "pix", this.win, this.units);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH)
|
||||
{
|
||||
// use the size of the texture, if we have access to it:
|
||||
if (typeof this._texture !== "undefined" && this._texture.width > 0)
|
||||
{
|
||||
displaySize = [displaySize[0], displaySize[0] * this._texture.height / this._texture.width];
|
||||
}
|
||||
}
|
||||
else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT)
|
||||
{
|
||||
// use the size of the texture, if we have access to it:
|
||||
if (typeof this._texture !== "undefined" && this._texture.width > 0)
|
||||
{
|
||||
displaySize = [displaySize[1] * this._texture.width / this._texture.height, displaySize[1]];
|
||||
}
|
||||
}
|
||||
else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING)
|
||||
{
|
||||
// use the size of the texture, if we have access to it:
|
||||
if (typeof this._texture !== "undefined" && this._texture.width > 0)
|
||||
{
|
||||
displaySize = [displaySize[0], this._texture.height];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return displaySize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ImageStim Aspect Ratio Strategy.
|
||||
*
|
||||
* @enum {Symbol}
|
||||
* @readonly
|
||||
*/
|
||||
ImageStim.AspectRatioStrategy = {
|
||||
FIT_TO_WIDTH: Symbol.for("FIT_TO_WIDTH"),
|
||||
HORIZONTAL_TILING: Symbol.for("HORIZONTAL_TILING"),
|
||||
FIT_TO_HEIGHT: Symbol.for("FIT_TO_HEIGHT"),
|
||||
VARIABLE: Symbol.for("VARIABLE"),
|
||||
};
|
||||
|
@ -86,7 +86,8 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
|
||||
clipMask,
|
||||
autoDraw,
|
||||
autoLog,
|
||||
fitToContent
|
||||
fitToContent,
|
||||
boxFn
|
||||
} = {},
|
||||
)
|
||||
{
|
||||
@ -202,12 +203,14 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
|
||||
// and setSize called from super class would not have a proper effect
|
||||
this.setSize(size);
|
||||
|
||||
this._addAttribute("boxFn", boxFn, null);
|
||||
|
||||
// estimate the bounding box:
|
||||
this._estimateBoundingBox();
|
||||
|
||||
if (this._autoLog)
|
||||
{
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
|
||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,6 +484,26 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
|
||||
alignmentStyles = ["center", "center"];
|
||||
}
|
||||
|
||||
let box;
|
||||
if (this._boxFn !== null)
|
||||
{
|
||||
box = this._boxFn;
|
||||
}
|
||||
else
|
||||
{
|
||||
// note: box style properties eventually become PIXI.Graphics settings, so same syntax applies
|
||||
box = {
|
||||
fill: new Color(this._fillColor).int,
|
||||
alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1,
|
||||
rounded: 5,
|
||||
stroke: {
|
||||
color: new Color(this._borderColor).int,
|
||||
width: borderWidth_px,
|
||||
alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
// input style properties eventually become CSS, so same syntax applies
|
||||
input: {
|
||||
@ -504,41 +527,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
|
||||
overflow: "hidden",
|
||||
pointerEvents: "none"
|
||||
},
|
||||
// box style properties eventually become PIXI.Graphics settings, so same syntax applies
|
||||
box: {
|
||||
fill: new Color(this._fillColor).int,
|
||||
alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1,
|
||||
rounded: 5,
|
||||
stroke: {
|
||||
color: new Color(this._borderColor).int,
|
||||
width: borderWidth_px,
|
||||
alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1
|
||||
},
|
||||
/*default: {
|
||||
fill: new Color(this._fillColor).int,
|
||||
rounded: 5,
|
||||
stroke: {
|
||||
color: new Color(this._borderColor).int,
|
||||
width: borderWidth_px
|
||||
}
|
||||
},
|
||||
focused: {
|
||||
fill: new Color(this._fillColor).int,
|
||||
rounded: 5,
|
||||
stroke: {
|
||||
color: new Color(this._borderColor).int,
|
||||
width: borderWidth_px
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
fill: new Color(this._fillColor).int,
|
||||
rounded: 5,
|
||||
stroke: {
|
||||
color: new Color(this._borderColor).int,
|
||||
width: borderWidth_px
|
||||
}
|
||||
}*/
|
||||
},
|
||||
box
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -95,18 +95,11 @@ class MaxDiffMatrix
|
||||
question.setCssRoot(rootClass);
|
||||
question.cssClasses.mainRoot = rootClass;
|
||||
}
|
||||
let html;
|
||||
let headerCells = "";
|
||||
let subHeaderCells = "";
|
||||
let bodyCells = "";
|
||||
let bodyHTML = "";
|
||||
let cellGenerator;
|
||||
let i, j;
|
||||
|
||||
// Relying on a fact that there's always 2 columns.
|
||||
// This is correct according current Qualtrics design for MaxDiff matrices.
|
||||
// Header generation
|
||||
headerCells =
|
||||
let headerCells =
|
||||
`<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[0].text}</th>
|
||||
<td></td>
|
||||
<td></td>
|
||||
@ -114,9 +107,10 @@ class MaxDiffMatrix
|
||||
<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[1].text}</th>`;
|
||||
|
||||
// Body generation
|
||||
for (i = 0; i < question.rows.length; i++)
|
||||
let bodyHTML = "";
|
||||
for (let i = 0; i < question.rows.length; i++)
|
||||
{
|
||||
bodyCells =
|
||||
const bodyCells =
|
||||
`<td class="${CSS_CLASSES.TABLE_CELL}">
|
||||
<label class="${CSS_CLASSES.LABEL}">
|
||||
<input type="radio" class="${CSS_CLASSES.ITEM_VALUE}" name="${question.rows[i].value}" data-column=${question.columns[0].value}>
|
||||
@ -135,7 +129,7 @@ class MaxDiffMatrix
|
||||
bodyHTML += `<tr class="${CSS_CLASSES.TABLE_ROW}">${bodyCells}</tr>`;
|
||||
}
|
||||
|
||||
html = `<table class="${CSS_CLASSES.TABLE}">
|
||||
let html = `<table class="${CSS_CLASSES.TABLE}">
|
||||
<thead>
|
||||
<tr>${headerCells}</tr>
|
||||
</thead>
|
||||
@ -147,14 +141,15 @@ class MaxDiffMatrix
|
||||
|
||||
let inputDOMS = el.querySelectorAll("input");
|
||||
|
||||
for (i = 0; i < inputDOMS.length; i++)
|
||||
for (let i = 0; i < inputDOMS.length; i++)
|
||||
{
|
||||
inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function init (Survey) {
|
||||
export default function init (Survey)
|
||||
{
|
||||
var widget = {
|
||||
//the widget name. It should be unique and written in lowcase.
|
||||
name: "maxdiffmatrix",
|
||||
|
Loading…
Reference in New Issue
Block a user