From a0eb577d1964f91c60ae8354bb4a9030dea798d2 Mon Sep 17 00:00:00 2001
From: lgtst
Date: Tue, 18 Jan 2022 00:57:25 +0300
Subject: [PATCH 01/92] Started Implementing GratingStim;
---
src/visual/GratingStim.js | 258 ++++++++++++++++++++++++++++++++++++++
1 file changed, 258 insertions(+)
create mode 100644 src/visual/GratingStim.js
diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js
new file mode 100644
index 00000000..3c3e310b
--- /dev/null
+++ b/src/visual/GratingStim.js
@@ -0,0 +1,258 @@
+/**
+ * Grating Stimulus.
+ *
+ * @author Alain Pitiot
+ * @version 2021.2.0
+ * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2021 Open Science Tools Ltd. (https://opensciencetools.org)
+ * @license Distributed under the terms of the MIT License
+ */
+
+
+
+/**
+ * Grating Stimulus.
+ *
+ * @name module:visual.GratingStim
+ * @class
+ * @extends VisualStim
+ * @mixes ColorMixin
+ * @param {Object} options
+ * @param {String} options.name - the name used when logging messages from this stimulus
+ * @param {Window} options.win - the associated Window
+ * @param {string | HTMLImageElement} options.image - the name of the image resource or the HTMLImageElement corresponding to the image
+ * @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask
+ * @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
+ * @param {Array.} [options.pos= [0, 0]] - the position of the center of the stimulus
+ * @param {string} [options.units= 'norm'] - the units of the stimulus vertices, size and position
+ * @param {number} [options.ori= 0.0] - the orientation (in degrees)
+ * @param {number} [options.size] - the size of the rendered image (the size of the image will be used if size is not specified)
+ * @param {Color} [options.color= 'white'] the background color
+ * @param {number} [options.opacity= 1.0] - the opacity
+ * @param {number} [options.contrast= 1.0] - the contrast
+ * @param {number} [options.depth= 0] - the depth (i.e. the z order)
+ * @param {number} [options.texRes= 128] - the resolution of the text
+ * @param {boolean} [options.interpolate= false] - whether or not the image is interpolated
+ * @param {boolean} [options.flipHoriz= false] - whether or not to flip horizontally
+ * @param {boolean} [options.flipVert= false] - whether or not to flip vertically
+ * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
+ * @param {boolean} [options.autoLog= false] - whether or not to log
+ */
+
+ // win,
+ // tex="sin",
+ // mask="none",
+ // units="",
+ // pos=(0.0, 0.0),
+ // size=None,
+ // sf=None,
+ // ori=0.0,
+ // phase=(0.0, 0.0),
+ // texRes=128,
+ // rgb=None,
+ // dkl=None,
+ // lms=None,
+ // color=(1.0, 1.0, 1.0),
+ // colorSpace='rgb',
+ // contrast=1.0,
+ // opacity=None,
+ // depth=0,
+ // rgbPedestal=(0.0, 0.0, 0.0),
+ // interpolate=False,
+ // blendmode='avg',
+ // name=None,
+ // autoLog=None,
+ // autoDraw=False,
+ // maskParams=None)
+export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
+{
+ constructor({
+ name,
+ tex,
+ win,
+ mask,
+ pos,
+ units,
+ sf,
+ ori,
+ phase,
+ size,
+ rgb,
+ dkl,
+ lms,
+ color,
+ colorSpace,
+ opacity,
+ contrast,
+ texRes,
+ depth,
+ rgbPedestal,
+ interpolate,
+ blendmode,
+ autoDraw,
+ autoLog,
+ maskParams
+ } = {})
+ {
+ super({ name, win, units, ori, opacity, depth, pos, size, autoDraw, autoLog });
+
+ this._addAttribute(
+ "tex",
+ tex,
+ );
+
+ this._addAttribute(
+ "mask",
+ mask,
+ );
+ this._addAttribute(
+ "color",
+ color,
+ "white",
+ this._onChange(true, false),
+ );
+ this._addAttribute(
+ "contrast",
+ contrast,
+ 1.0,
+ this._onChange(true, false),
+ );
+ this._addAttribute(
+ "texRes",
+ texRes,
+ 128,
+ this._onChange(true, false),
+ );
+ this._addAttribute(
+ "interpolate",
+ interpolate,
+ false,
+ this._onChange(true, false),
+ );
+ this._addAttribute(
+ "flipHoriz",
+ flipHoriz,
+ false,
+ this._onChange(false, false),
+ );
+ this._addAttribute(
+ "flipVert",
+ flipVert,
+ false,
+ this._onChange(false, false),
+ );
+
+ // estimate the bounding box:
+ this._estimateBoundingBox();
+
+ if (this._autoLog)
+ {
+ this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
+ }
+ }
+
+ /**
+ * Setter for the image attribute.
+ *
+ * @name module:visual.GratingStim#setImage
+ * @public
+ * @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
+ */
+ setTex(tex, log = false)
+ {
+ const response = {
+ origin: "GratingStim.setTex",
+ context: "when setting the texture of GratingStim: " + this._name,
+ };
+
+ try
+ {
+ // tex is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
+ if (typeof tex === "undefined")
+ {
+ this.psychoJS.logger.warn("setting the tex of GratingStim: " + this._name + " with argument: undefined.");
+ this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: undefined");
+ }
+ else
+ {
+ // tex is a string: it should be the name of a resource, which we load
+ if (typeof tex === "string")
+ {
+ tex = this.psychoJS.serverManager.getResource(tex);
+ }
+
+ // tex should now be an actual HTMLImageElement: we raise an error if it is not
+ if (!(tex instanceof HTMLImageElement))
+ {
+ throw "the argument: " + tex.toString() + ' is not an image" }';
+ }
+
+ this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: src= " + tex.src + ", size= " + tex.width + "x" + tex.height);
+ }
+
+ const existingImage = this.getImage();
+ const hasChanged = existingImage ? existingImage.src !== tex.src : true;
+
+ this._setAttribute("tex", tex, log);
+
+ if (hasChanged)
+ {
+ this._onChange(true, true)();
+ }
+ }
+ catch (error)
+ {
+ throw Object.assign(response, { error });
+ }
+ }
+
+ /**
+ * Setter for the mask attribute.
+ *
+ * @name module:visual.GratingStim#setMask
+ * @public
+ * @param {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask
+ * @param {boolean} [log= false] - whether of not to log
+ */
+ setMask(mask, log = false)
+ {
+ const response = {
+ origin: "GratingStim.setMask",
+ context: "when setting the mask of GratingStim: " + this._name,
+ };
+
+ try
+ {
+ // mask is undefined: that's fine but we raise a warning in case this is a sympton of an actual problem
+ if (typeof mask === "undefined")
+ {
+ this.psychoJS.logger.warn("setting the mask of GratingStim: " + this._name + " with argument: undefined.");
+ this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: undefined");
+ }
+ else
+ {
+ // mask is a string: it should be the name of a resource, which we load
+ if (typeof mask === "string")
+ {
+ mask = this.psychoJS.serverManager.getResource(mask);
+ }
+
+ // mask should now be an actual HTMLImageElement: we raise an error if it is not
+ if (!(mask instanceof HTMLImageElement))
+ {
+ throw "the argument: " + mask.toString() + ' is not an image" }';
+ }
+
+ this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: src= " + mask.src + ", size= " + mask.width + "x" + mask.height);
+ }
+
+ this._setAttribute("mask", mask, log);
+
+ this._onChange(true, false)();
+ }
+ catch (error)
+ {
+ throw Object.assign(response, { error });
+ }
+ }
+}
From e050eaf906349e12cf5dfeadc2295e54630458af Mon Sep 17 00:00:00 2001
From: lgtst
Date: Wed, 26 Jan 2022 00:31:03 +0300
Subject: [PATCH 02/92] GratingStim implementation first take;
---
src/visual/GratingStim.js | 297 +++++++++++++++++++++++-----
src/visual/index.js | 1 +
src/visual/shaders/defaultQuad.vert | 15 ++
src/visual/shaders/gaussShader.frag | 17 ++
src/visual/shaders/sinShader.frag | 15 ++
5 files changed, 300 insertions(+), 45 deletions(-)
create mode 100644 src/visual/shaders/defaultQuad.vert
create mode 100644 src/visual/shaders/gaussShader.frag
create mode 100644 src/visual/shaders/sinShader.frag
diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js
index 3c3e310b..f5f67181 100644
--- a/src/visual/GratingStim.js
+++ b/src/visual/GratingStim.js
@@ -7,7 +7,29 @@
* @license Distributed under the terms of the MIT License
*/
+import * as PIXI from "pixi.js-legacy";
+import { Color } from "../util/Color.js";
+import { ColorMixin } from "../util/ColorMixin.js";
+import { to_pixiPoint } from "../util/Pixi.js";
+import * as util from "../util/Util.js";
+import { VisualStim } from "./VisualStim.js";
+import defaultQuadVert from './shaders/defaultQuad.vert';
+import sinShader from './shaders/sinShader.frag';
+import gaussShader from './shaders/gaussShader.frag';
+const DEFINED_FUNCTIONS = {
+ sin: sinShader,
+ sqr: undefined,
+ saw: undefined,
+ tri: undefined,
+ sinXsin: undefined,
+ sqrXsqr: undefined,
+ circle: undefined,
+ gauss: gaussShader,
+ cross: undefined,
+ radRamp: undefined,
+ raisedCos: undefined
+};
/**
* Grating Stimulus.
@@ -32,39 +54,37 @@
* @param {number} [options.depth= 0] - the depth (i.e. the z order)
* @param {number} [options.texRes= 128] - the resolution of the text
* @param {boolean} [options.interpolate= false] - whether or not the image is interpolated
- * @param {boolean} [options.flipHoriz= false] - whether or not to flip horizontally
- * @param {boolean} [options.flipVert= false] - whether or not to flip vertically
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
- // win,
- // tex="sin",
- // mask="none",
- // units="",
- // pos=(0.0, 0.0),
- // size=None,
- // sf=None,
- // ori=0.0,
- // phase=(0.0, 0.0),
- // texRes=128,
- // rgb=None,
- // dkl=None,
- // lms=None,
- // color=(1.0, 1.0, 1.0),
- // colorSpace='rgb',
- // contrast=1.0,
- // opacity=None,
- // depth=0,
- // rgbPedestal=(0.0, 0.0, 0.0),
- // interpolate=False,
- // blendmode='avg',
- // name=None,
- // autoLog=None,
- // autoDraw=False,
- // maskParams=None)
export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
{
+ // win,
+ // tex="sin",
+ // mask="none",
+ // units="",
+ // pos=(0.0, 0.0),
+ // size=None,
+ // sf=None,
+ // ori=0.0,
+ // phase=(0.0, 0.0),
+ // texRes=128,
+ // rgb=None,
+ // dkl=None,
+ // lms=None,
+ // color=(1.0, 1.0, 1.0),
+ // colorSpace='rgb',
+ // contrast=1.0,
+ // opacity=None,
+ // depth=0,
+ // rgbPedestal=(0.0, 0.0, 0.0),
+ // interpolate=False,
+ // blendmode='avg',
+ // name=None,
+ // autoLog=None,
+ // autoDraw=False,
+ // maskParams=None)
constructor({
name,
tex,
@@ -72,7 +92,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
mask,
pos,
units,
- sf,
+ spatialFrequency = 10.,
ori,
phase,
size,
@@ -99,11 +119,22 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
"tex",
tex,
);
-
this._addAttribute(
"mask",
mask,
);
+ this._addAttribute(
+ "spatialFrequency",
+ spatialFrequency,
+ 10.,
+ this._onChange(true, false)
+ );
+ this._addAttribute(
+ "phase",
+ phase,
+ 0.,
+ this._onChange(true, false)
+ );
this._addAttribute(
"color",
color,
@@ -128,18 +159,6 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
false,
this._onChange(true, false),
);
- this._addAttribute(
- "flipHoriz",
- flipHoriz,
- false,
- this._onChange(false, false),
- );
- this._addAttribute(
- "flipVert",
- flipVert,
- false,
- this._onChange(false, false),
- );
// estimate the bounding box:
this._estimateBoundingBox();
@@ -162,17 +181,26 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
{
const response = {
origin: "GratingStim.setTex",
- context: "when setting the texture of GratingStim: " + this._name,
+ context: "when setting the tex of GratingStim: " + this._name,
};
try
{
+ let hasChanged = false;
+
// tex is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
if (typeof tex === "undefined")
{
this.psychoJS.logger.warn("setting the tex of GratingStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: undefined");
}
+ else if (DEFINED_FUNCTIONS[tex] !== undefined)
+ {
+ // tex is a string and it is one of predefined functions available in shaders
+ this.psychoJS.logger.debug("the tex is one of predefined functions. Set the tex of GratingStim: " + this._name + " as: " + tex);
+ const existingImage = this.getTex();
+ hasChanged = existingImage ? existingImage !== tex : true;
+ }
else
{
// tex is a string: it should be the name of a resource, which we load
@@ -188,11 +216,10 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
}
this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: src= " + tex.src + ", size= " + tex.width + "x" + tex.height);
+ const existingImage = this.getTex();
+ hasChanged = existingImage ? existingImage.src !== tex.src : true;
}
- const existingImage = this.getImage();
- const hasChanged = existingImage ? existingImage.src !== tex.src : true;
-
this._setAttribute("tex", tex, log);
if (hasChanged)
@@ -229,6 +256,11 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
this.psychoJS.logger.warn("setting the mask of GratingStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: undefined");
}
+ else if (DEFINED_FUNCTIONS[mask] !== undefined)
+ {
+ // mask is a string and it is one of predefined functions available in shaders
+ this.psychoJS.logger.debug("the mask is one of predefined functions. Set the mask of GratingStim: " + this._name + " as: " + mask);
+ }
else
{
// mask is a string: it should be the name of a resource, which we load
@@ -255,4 +287,179 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
throw Object.assign(response, { error });
}
}
+
+ /**
+ * Get the size of the display image, which is either that of the ImageStim or that of the image
+ * it contains.
+ *
+ * @name module:visual.ImageStim#_getDisplaySize
+ * @private
+ * @return {number[]} the size of the displayed image
+ */
+ _getDisplaySize()
+ {
+ let displaySize = this.size;
+
+ if (typeof displaySize === "undefined")
+ {
+ // use the size of the pixi element, if we have access to it:
+ if (typeof this._pixi !== "undefined" && this._pixi.width > 0)
+ {
+ const pixiContainerSize = [this._pixi.width, this._pixi.height];
+ displaySize = util.to_unit(pixiContainerSize, "pix", this.win, this.units);
+ }
+ }
+
+ return displaySize;
+ }
+
+ /**
+ * Estimate the bounding box.
+ *
+ * @name module:visual.ImageStim#_estimateBoundingBox
+ * @function
+ * @override
+ * @protected
+ */
+ _estimateBoundingBox()
+ {
+ const size = this._getDisplaySize();
+ if (typeof size !== "undefined")
+ {
+ this._boundingBox = new PIXI.Rectangle(
+ this._pos[0] - size[0] / 2,
+ this._pos[1] - size[1] / 2,
+ size[0],
+ size[1],
+ );
+ }
+
+ // TODO take the orientation into account
+ }
+
+ _getPixiMeshFromPredefinedShaders (funcName = '', uniforms = {}) {
+ const geometry = new PIXI.Geometry();
+ geometry.addAttribute(
+ 'aVertexPosition',
+ [
+ 0, 0,
+ 256, 0,
+ 256, 256,
+ 0, 256
+ ],
+ 2
+ );
+ geometry.addAttribute(
+ 'aUvs',
+ [0, 0, 1, 0, 1, 1, 0, 1],
+ 2
+ );
+ geometry.addIndex([0, 1, 2, 0, 2, 3]);
+ const vertexSrc = defaultQuadVert;
+ const fragmentSrc = DEFINED_FUNCTIONS[funcName];
+ const uniformsFinal = Object.assign(uniforms, {
+ // for future default uniforms
+ });
+ const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, uniformsFinal);
+ return new PIXI.Mesh(geometry, shader);
+ }
+
+ /**
+ * Update the stimulus, if necessary.
+ *
+ * @name module:visual.ImageStim#_updateIfNeeded
+ * @private
+ */
+ _updateIfNeeded()
+ {
+ if (!this._needUpdate)
+ {
+ return;
+ }
+ this._needUpdate = false;
+
+ // update the PIXI representation, if need be:
+ if (this._needPixiUpdate)
+ {
+ this._needPixiUpdate = false;
+ if (typeof this._pixi !== "undefined")
+ {
+ this._pixi.destroy(true);
+ }
+ this._pixi = undefined;
+
+ // no image to draw: return immediately
+ if (typeof this._tex === "undefined")
+ {
+ return;
+ }
+
+ if (this._tex instanceof HTMLImageElement)
+ {
+ this._pixi = PIXI.Sprite.from(this._tex);
+ }
+ else
+ {
+ this._pixi = this._getPixiMeshFromPredefinedShaders(this._tex, {
+ uFreq: this.spatialFrequency,
+ uPhase: this.phase
+ });
+ }
+ this._pixi.pivot.set(this._pixi.width * .5, this._pixi.width * .5);
+
+ // add a mask if need be:
+ if (typeof this._mask !== "undefined")
+ {
+ if (this._mask instanceof HTMLImageElement)
+ {
+ this._pixi.mask = PIXI.Sprite.from(this._mask);
+ this._pixi.addChild(this._pixi.mask);
+ }
+ else
+ {
+ // for some reason setting PIXI.Mesh as .mask doesn't do anything,
+ // rendering mask to texture for further use.
+ const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask);
+ const rt = PIXI.RenderTexture.create({
+ width: 256,
+ height: 256
+ });
+ this.win._renderer.render(maskMesh, {
+ renderTexture: rt
+ });
+ const maskSprite = new PIXI.Sprite.from(rt);
+ this._pixi.mask = maskSprite;
+ this._pixi.addChild(maskSprite);
+ }
+ }
+
+ // since _pixi.width may not be immediately available but the rest of the code needs its value
+ // we arrange for repeated calls to _updateIfNeeded until we have a width:
+ if (this._pixi.width === 0)
+ {
+ this._needUpdate = true;
+ this._needPixiUpdate = true;
+ return;
+ }
+ }
+
+ this._pixi.zIndex = this._depth;
+ this._pixi.alpha = this.opacity;
+
+ // set the scale:
+ const displaySize = this._getDisplaySize();
+ const size_px = util.to_px(displaySize, this.units, this.win);
+ const scaleX = size_px[0] / this._pixi.width;
+ const scaleY = size_px[1] / this._pixi.height;
+ this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
+ this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
+
+ // set the position, rotation, and anchor (image centered on pos):
+ let pos = to_pixiPoint(this.pos, this.units, this.win);
+ this._pixi.position.set(pos.x, pos.y);
+ this._pixi.rotation = this.ori * Math.PI / 180;
+
+ // re-estimate the bounding box, as the texture's width may now be available:
+ this._estimateBoundingBox();
+ }
}
diff --git a/src/visual/index.js b/src/visual/index.js
index 2794a47f..445e7548 100644
--- a/src/visual/index.js
+++ b/src/visual/index.js
@@ -1,6 +1,7 @@
export * from "./ButtonStim.js";
export * from "./Form.js";
export * from "./ImageStim.js";
+export * from "./GratingStim.js";
export * from "./MovieStim.js";
export * from "./Polygon.js";
export * from "./Rect.js";
diff --git a/src/visual/shaders/defaultQuad.vert b/src/visual/shaders/defaultQuad.vert
new file mode 100644
index 00000000..1f2ea67d
--- /dev/null
+++ b/src/visual/shaders/defaultQuad.vert
@@ -0,0 +1,15 @@
+#version 300 es
+
+precision mediump float;
+
+in vec2 aVertexPosition;
+in vec2 aUvs;
+out vec2 vUvs;
+
+uniform mat3 translationMatrix;
+uniform mat3 projectionMatrix;
+
+void main() {
+ vUvs = aUvs;
+ gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
+}
diff --git a/src/visual/shaders/gaussShader.frag b/src/visual/shaders/gaussShader.frag
new file mode 100644
index 00000000..17543f62
--- /dev/null
+++ b/src/visual/shaders/gaussShader.frag
@@ -0,0 +1,17 @@
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+
+float gauss(float x) {
+ return exp(-(x * x) * 20.);
+}
+
+void main() {
+ vec2 uv = vUvs;
+ float g = gauss(uv.x - .5) * gauss(uv.y - .5);
+ shaderOut = vec4(vec3(g), 1.);
+}
diff --git a/src/visual/shaders/sinShader.frag b/src/visual/shaders/sinShader.frag
new file mode 100644
index 00000000..9775d64f
--- /dev/null
+++ b/src/visual/shaders/sinShader.frag
@@ -0,0 +1,15 @@
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+uniform float uFreq;
+uniform float uPhase;
+
+void main() {
+ vec2 uv = vUvs;
+ float s = sin(uFreq * uv.x * 2. * M_PI + uPhase);
+ shaderOut = vec4(vec3(s), 1.0);
+}
From e4eb6f04c897dbb067846890c7bd0f696ab0c2ad Mon Sep 17 00:00:00 2001
From: lgtst
Date: Fri, 28 Jan 2022 16:13:25 +0300
Subject: [PATCH 03/92] sin range chnged to [0, 1] for proper gabor patch;
---
src/visual/shaders/sinShader.frag | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/visual/shaders/sinShader.frag b/src/visual/shaders/sinShader.frag
index 9775d64f..e4a78139 100644
--- a/src/visual/shaders/sinShader.frag
+++ b/src/visual/shaders/sinShader.frag
@@ -7,9 +7,10 @@ out vec4 shaderOut;
#define M_PI 3.14159265358979
uniform float uFreq;
uniform float uPhase;
+uniform sampler2D uMaskTex;
void main() {
vec2 uv = vUvs;
float s = sin(uFreq * uv.x * 2. * M_PI + uPhase);
- shaderOut = vec4(vec3(s), 1.0);
+ shaderOut = vec4(.5 + .5 * vec3(s), 1.0);
}
From 120060cbab1d30b4197678790715fd5d31252fec Mon Sep 17 00:00:00 2001
From: lgtst
Date: Fri, 28 Jan 2022 18:06:28 +0300
Subject: [PATCH 04/92] removed unused uniform uMaskTex;
---
src/visual/shaders/sinShader.frag | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/visual/shaders/sinShader.frag b/src/visual/shaders/sinShader.frag
index e4a78139..abdd5299 100644
--- a/src/visual/shaders/sinShader.frag
+++ b/src/visual/shaders/sinShader.frag
@@ -7,7 +7,6 @@ out vec4 shaderOut;
#define M_PI 3.14159265358979
uniform float uFreq;
uniform float uPhase;
-uniform sampler2D uMaskTex;
void main() {
vec2 uv = vUvs;
From f7e4b2ec53df3bb94d125e785973bf1bb9209df5 Mon Sep 17 00:00:00 2001
From: lgtst
Date: Fri, 28 Jan 2022 19:33:05 +0300
Subject: [PATCH 05/92] gamma correction introduced;
---
package.json | 1 +
src/core/PsychoJS.js | 2 ++
src/core/Window.js | 7 +++++++
3 files changed, 10 insertions(+)
diff --git a/package.json b/package.json
index 8f9f2d82..6006e5ae 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"start": "npm run build"
},
"dependencies": {
+ "@pixi/filter-adjustment": "^4.1.3",
"howler": "^2.2.1",
"log4javascript": "github:Ritzlgrmft/log4javascript",
"pako": "^1.0.10",
diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js
index 922ff0f8..4aaa176b 100644
--- a/src/core/PsychoJS.js
+++ b/src/core/PsychoJS.js
@@ -218,6 +218,7 @@ export class PsychoJS
name,
fullscr,
color,
+ gamma,
units,
waitBlanking,
autoLog,
@@ -239,6 +240,7 @@ export class PsychoJS
name,
fullscr,
color,
+ gamma,
units,
waitBlanking,
autoLog,
diff --git a/src/core/Window.js b/src/core/Window.js
index 7b11cc6d..a0f0c9d3 100644
--- a/src/core/Window.js
+++ b/src/core/Window.js
@@ -8,6 +8,7 @@
*/
import * as PIXI from "pixi.js-legacy";
+import {AdjustmentFilter} from "@pixi/filter-adjustment";
import { MonotonicClock } from "../util/Clock.js";
import { Color } from "../util/Color.js";
import { PsychObject } from "../util/PsychObject.js";
@@ -25,6 +26,7 @@ import { Logger } from "./Logger.js";
* @param {string} [options.name] the name of the window
* @param {boolean} [options.fullscr= false] whether or not to go fullscreen
* @param {Color} [options.color= Color('black')] the background color of the window
+ * @param {number} [options.gamma= 1] sets the delimiter for gamma correction. In other words gamma correction is calculated as pow(rgb, 1/gamma)
* @param {string} [options.units= 'pix'] the units of the window
* @param {boolean} [options.waitBlanking= false] whether or not to wait for all rendering operations to be done
* before flipping
@@ -49,6 +51,7 @@ export class Window extends PsychObject
name,
fullscr = false,
color = new Color("black"),
+ gamma = 1,
units = "pix",
waitBlanking = false,
autoLog = true,
@@ -64,6 +67,7 @@ export class Window extends PsychObject
this._addAttribute("fullscr", fullscr);
this._addAttribute("color", color);
+ this._addAttribute("gamma", gamma);
this._addAttribute("units", units);
this._addAttribute("waitBlanking", waitBlanking);
this._addAttribute("autoLog", autoLog);
@@ -428,6 +432,9 @@ export class Window extends PsychObject
// create a top-level PIXI container:
this._rootContainer = new PIXI.Container();
this._rootContainer.interactive = true;
+ this._rootContainer.filters = [new AdjustmentFilter({
+ gamma: this.gamma
+ })];
// set the initial size of the PIXI renderer and the position of the root container:
Window._resizePixiRenderer(this);
From 3594405efe5c133807fb983599bbc0f4c93b1cea Mon Sep 17 00:00:00 2001
From: lgtst
Date: Fri, 28 Jan 2022 23:51:47 +0300
Subject: [PATCH 06/92] added onChange() callback to properly reflect changes
of the _gamma value in adjustmentFilter;
---
src/core/Window.js | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/core/Window.js b/src/core/Window.js
index a0f0c9d3..cf27c937 100644
--- a/src/core/Window.js
+++ b/src/core/Window.js
@@ -62,12 +62,19 @@ export class Window extends PsychObject
// messages to be logged at the next "flip":
this._msgToBeLogged = [];
+ // storing AdjustmentFilter instance to access later;
+ this._adjustmentFilter = new AdjustmentFilter({
+ gamma
+ });
+
// list of all elements, in the order they are currently drawn:
this._drawList = [];
this._addAttribute("fullscr", fullscr);
this._addAttribute("color", color);
- this._addAttribute("gamma", gamma);
+ this._addAttribute("gamma", gamma, 1, () => {
+ this._adjustmentFilter.gamma = this._gamma;
+ });
this._addAttribute("units", units);
this._addAttribute("waitBlanking", waitBlanking);
this._addAttribute("autoLog", autoLog);
@@ -432,9 +439,7 @@ export class Window extends PsychObject
// create a top-level PIXI container:
this._rootContainer = new PIXI.Container();
this._rootContainer.interactive = true;
- this._rootContainer.filters = [new AdjustmentFilter({
- gamma: this.gamma
- })];
+ this._rootContainer.filters = [this._adjustmentFilter];
// set the initial size of the PIXI renderer and the position of the root container:
Window._resizePixiRenderer(this);
From 88b7345535c66e89c35be2dffac39035fc47a23d Mon Sep 17 00:00:00 2001
From: lgtst
Date: Thu, 10 Feb 2022 19:54:52 +0300
Subject: [PATCH 07/92] added spatial frequency and phase support for image
based grating stims;
---
src/visual/GratingStim.js | 73 +++++++++++++++++++++++++++------------
1 file changed, 51 insertions(+), 22 deletions(-)
diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js
index f5f67181..b1f93ff9 100644
--- a/src/visual/GratingStim.js
+++ b/src/visual/GratingStim.js
@@ -31,6 +31,8 @@ const DEFINED_FUNCTIONS = {
raisedCos: undefined
};
+const DEFAULT_STIM_SIZE = [256, 256]; // in pixels
+
/**
* Grating Stimulus.
*
@@ -45,7 +47,6 @@ const DEFINED_FUNCTIONS = {
* @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask
* @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
* @param {Array.} [options.pos= [0, 0]] - the position of the center of the stimulus
- * @param {string} [options.units= 'norm'] - the units of the stimulus vertices, size and position
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
* @param {number} [options.size] - the size of the rendered image (the size of the image will be used if size is not specified)
* @param {Color} [options.color= 'white'] the background color
@@ -126,14 +127,12 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
this._addAttribute(
"spatialFrequency",
spatialFrequency,
- 10.,
- this._onChange(true, false)
+ 10.
);
this._addAttribute(
"phase",
phase,
- 0.,
- this._onChange(true, false)
+ 0.
);
this._addAttribute(
"color",
@@ -167,6 +166,11 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
{
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
}
+
+ if (!Array.isArray(this.size) || this.size.length === 0) {
+ this.size = util.to_unit(DEFAULT_STIM_SIZE, "pix", this.win, this.units);
+ }
+ this._sizeInPixels = util.to_px(this.size, this.units, this.win);
}
/**
@@ -198,8 +202,8 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
{
// tex is a string and it is one of predefined functions available in shaders
this.psychoJS.logger.debug("the tex is one of predefined functions. Set the tex of GratingStim: " + this._name + " as: " + tex);
- const existingImage = this.getTex();
- hasChanged = existingImage ? existingImage !== tex : true;
+ const curFuncName = this.getTex();
+ hasChanged = curFuncName ? curFuncName !== tex : true;
}
else
{
@@ -289,10 +293,10 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
}
/**
- * Get the size of the display image, which is either that of the ImageStim or that of the image
+ * Get the size of the display image, which is either that of the GratingStim or that of the image
* it contains.
*
- * @name module:visual.ImageStim#_getDisplaySize
+ * @name module:visual.GratingStim#_getDisplaySize
* @private
* @return {number[]} the size of the displayed image
*/
@@ -316,7 +320,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
/**
* Estimate the bounding box.
*
- * @name module:visual.ImageStim#_estimateBoundingBox
+ * @name module:visual.GratingStim#_estimateBoundingBox
* @function
* @override
* @protected
@@ -343,9 +347,9 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
'aVertexPosition',
[
0, 0,
- 256, 0,
- 256, 256,
- 0, 256
+ this._sizeInPixels[0], 0,
+ this._sizeInPixels[0], this._sizeInPixels[1],
+ 0, this._sizeInPixels[1]
],
2
);
@@ -364,10 +368,30 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
return new PIXI.Mesh(geometry, shader);
}
+ setPhase (phase, log = false) {
+ this._setAttribute("phase", phase, log);
+ if (this._pixi instanceof PIXI.Mesh) {
+ this._pixi.shader.uniforms.uPhase = phase;
+ } else if (this._pixi instanceof PIXI.TilingSprite) {
+ this._pixi.tilePosition.x = -phase * (this._sizeInPixels[0] * this._pixi.tileScale.x) / (2 * Math.PI)
+ }
+ }
+
+ setSpatialFrequency (sf, log = false) {
+ this._setAttribute("spatialFrequency", sf, log);
+ if (this._pixi instanceof PIXI.Mesh) {
+ this._pixi.shader.uniforms.uFreq = sf;
+ } else if (this._pixi instanceof PIXI.TilingSprite) {
+ // tileScale units are pixels, so converting function frequency to pixels
+ // and also taking into account possible size difference between used texture and requested stim size
+ this._pixi.tileScale.x = (1 / sf) * (this._pixi.width / this._pixi.texture.width);
+ }
+ }
+
/**
* Update the stimulus, if necessary.
*
- * @name module:visual.ImageStim#_updateIfNeeded
+ * @name module:visual.GratingStim#_updateIfNeeded
* @private
*/
_updateIfNeeded()
@@ -396,13 +420,18 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
if (this._tex instanceof HTMLImageElement)
{
- this._pixi = PIXI.Sprite.from(this._tex);
+ this._pixi = PIXI.TilingSprite.from(this._tex, {
+ width: this._sizeInPixels[0],
+ height: this._sizeInPixels[1]
+ });
+ this.setPhase(this._phase);
+ this.setSpatialFrequency(this._spatialFrequency);
}
else
{
this._pixi = this._getPixiMeshFromPredefinedShaders(this._tex, {
- uFreq: this.spatialFrequency,
- uPhase: this.phase
+ uFreq: this._spatialFrequency,
+ uPhase: this._phase
});
}
this._pixi.pivot.set(this._pixi.width * .5, this._pixi.width * .5);
@@ -421,8 +450,8 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
// rendering mask to texture for further use.
const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask);
const rt = PIXI.RenderTexture.create({
- width: 256,
- height: 256
+ width: this._sizeInPixels[0],
+ height: this._sizeInPixels[1]
});
this.win._renderer.render(maskMesh, {
renderTexture: rt
@@ -448,9 +477,9 @@ export class GratingStim 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._pixi.width;
- const scaleY = size_px[1] / this._pixi.height;
+ this._sizeInPixels = util.to_px(displaySize, this.units, this.win);
+ const scaleX = this._sizeInPixels[0] / this._pixi.width;
+ const scaleY = this._sizeInPixels[1] / this._pixi.height;
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
From ff13338563c1e82f6b7c81bc5dfecec86fd8f758 Mon Sep 17 00:00:00 2001
From: lgtst
Date: Wed, 2 Mar 2022 20:45:12 +0300
Subject: [PATCH 08/92] Full set of signals for grating stim; Documentation
test;
---
docs/visual_GratingStim.js.html | 644 ++++++++++++++++++++++++
src/visual/GratingStim.js | 195 +++++--
src/visual/shaders/circleShader.frag | 13 +
src/visual/shaders/crossShader.frag | 16 +
src/visual/shaders/gaussShader.frag | 17 +-
src/visual/shaders/radRampShader.frag | 17 +
src/visual/shaders/raisedCosShader.frag | 29 ++
src/visual/shaders/sawShader.frag | 21 +
src/visual/shaders/sinXsinShader.frag | 17 +
src/visual/shaders/sqrShader.frag | 20 +
src/visual/shaders/sqrXsqrShader.frag | 17 +
src/visual/shaders/triShader.frag | 22 +
12 files changed, 975 insertions(+), 53 deletions(-)
create mode 100644 docs/visual_GratingStim.js.html
create mode 100644 src/visual/shaders/circleShader.frag
create mode 100644 src/visual/shaders/crossShader.frag
create mode 100644 src/visual/shaders/radRampShader.frag
create mode 100644 src/visual/shaders/raisedCosShader.frag
create mode 100644 src/visual/shaders/sawShader.frag
create mode 100644 src/visual/shaders/sinXsinShader.frag
create mode 100644 src/visual/shaders/sqrShader.frag
create mode 100644 src/visual/shaders/sqrXsqrShader.frag
create mode 100644 src/visual/shaders/triShader.frag
diff --git a/docs/visual_GratingStim.js.html b/docs/visual_GratingStim.js.html
new file mode 100644
index 00000000..e6804ffb
--- /dev/null
+++ b/docs/visual_GratingStim.js.html
@@ -0,0 +1,644 @@
+
+
+
+
+ JSDoc: Source: visual/GratingStim.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: visual/GratingStim.js
+
+
+
+
+
+
+
+
+
/**
+ * Grating Stimulus.
+ *
+ * @author Alain Pitiot
+ * @version 2021.2.0
+ * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2021 Open Science Tools Ltd. (https://opensciencetools.org)
+ * @license Distributed under the terms of the MIT License
+ */
+
+import * as PIXI from "pixi.js-legacy";
+import { Color } from "../util/Color.js";
+import { ColorMixin } from "../util/ColorMixin.js";
+import { to_pixiPoint } from "../util/Pixi.js";
+import * as util from "../util/Util.js";
+import { VisualStim } from "./VisualStim.js";
+import defaultQuadVert from "./shaders/defaultQuad.vert";
+import sinShader from "./shaders/sinShader.frag";
+import sqrShader from "./shaders/sqrShader.frag";
+import sawShader from "./shaders/sawShader.frag";
+import triShader from "./shaders/triShader.frag";
+import sinXsinShader from "./shaders/sinXsinShader.frag";
+import sqrXsqrShader from "./shaders/sqrXsqrShader.frag";
+import circleShader from "./shaders/circleShader.frag";
+import gaussShader from "./shaders/gaussShader.frag";
+import crossShader from "./shaders/crossShader.frag";
+import radRampShader from "./shaders/radRampShader.frag";
+import raisedCosShader from "./shaders/raisedCosShader.frag";
+
+
+/**
+ * Grating Stimulus.
+ *
+ * @name module:visual.GratingStim
+ * @class
+ * @extends VisualStim
+ * @mixes ColorMixin
+ * @param {Object} options
+ * @param {String} options.name - the name used when logging messages from this stimulus
+ * @param {Window} options.win - the associated Window
+ * @param {string | HTMLImageElement} options.image - the name of the image resource or the HTMLImageElement corresponding to the image
+ * @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask
+ * @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
+ * @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
+ * @param {number} [options.ori= 0.0] - the orientation (in degrees)
+ * @param {number} [options.size] - the size of the rendered image (the size of the image will be used if size is not specified)
+ * @param {Color} [options.color= "white"] the background color
+ * @param {number} [options.opacity= 1.0] - the opacity
+ * @param {number} [options.contrast= 1.0] - the contrast
+ * @param {number} [options.depth= 0] - the depth (i.e. the z order)
+ * @param {number} [options.texRes= 128] - the resolution of the text
+ * @param {boolean} [options.interpolate= false] - whether or not the image is interpolated
+ * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
+ * @param {boolean} [options.autoLog= false] - whether or not to log
+ */
+
+export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
+{
+ // win,
+ // tex="sin",
+ // mask="none",
+ // units="",
+ // pos=(0.0, 0.0),
+ // size=None,
+ // sf=None,
+ // ori=0.0,
+ // phase=(0.0, 0.0),
+ // texRes=128,
+ // rgb=None,
+ // dkl=None,
+ // lms=None,
+ // color=(1.0, 1.0, 1.0),
+ // colorSpace='rgb',
+ // contrast=1.0,
+ // opacity=None,
+ // depth=0,
+ // rgbPedestal=(0.0, 0.0, 0.0),
+ // interpolate=False,
+ // blendmode='avg',
+ // name=None,
+ // autoLog=None,
+ // autoDraw=False,
+ // maskParams=None)
+
+ static #DEFINED_FUNCTIONS = {
+ sin: {
+ shader: sinShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0
+ }
+ },
+ sqr: {
+ shader: sqrShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0
+ }
+ },
+ saw: {
+ shader: sawShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0
+ }
+ },
+ tri: {
+ shader: triShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0,
+ uPeriod: 1.0
+ }
+ },
+ sinXsin: {
+ shader: sinXsinShader,
+ uniforms: {
+
+ }
+ },
+ sqrXsqr: {
+ shader: sqrXsqrShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0
+ }
+ },
+ circle: {
+ shader: circleShader,
+ uniforms: {
+
+ }
+ },
+ gauss: {
+ shader: gaussShader,
+ uniforms: {
+ uA: 1.0,
+ uB: 0.0,
+ uC: 0.16
+ }
+ },
+ cross: {
+ shader: crossShader,
+ uniforms: {
+ uThickness: 0.1
+ }
+ },
+ radRamp: {
+ shader: radRampShader,
+ uniforms: {
+
+ }
+ },
+ raisedCos: {
+ shader: raisedCosShader,
+ uniforms: {
+ uBeta: 0.25,
+ uPeriod: 1.0
+ }
+ }
+ };
+
+ static #DEFAULT_STIM_SIZE_PX = [256, 256]; // in pixels
+
+ constructor({
+ name,
+ tex = "sin",
+ win,
+ mask,
+ pos,
+ units,
+ spatialFrequency = 1.,
+ ori,
+ phase,
+ size,
+ rgb,
+ dkl,
+ lms,
+ color,
+ colorSpace,
+ opacity,
+ contrast,
+ texRes,
+ depth,
+ rgbPedestal,
+ interpolate,
+ blendmode,
+ autoDraw,
+ autoLog,
+ maskParams
+ } = {})
+ {
+ super({ name, win, units, ori, opacity, depth, pos, size, autoDraw, autoLog });
+
+ this._addAttribute(
+ "tex",
+ tex,
+ );
+ this._addAttribute(
+ "mask",
+ mask,
+ );
+ this._addAttribute(
+ "spatialFrequency",
+ spatialFrequency,
+ GratingStim.#DEFINED_FUNCTIONS[tex].uniforms.uFreq || 1.0
+ );
+ this._addAttribute(
+ "phase",
+ phase,
+ GratingStim.#DEFINED_FUNCTIONS[tex].uniforms.uPhase || 0.0
+ );
+ this._addAttribute(
+ "color",
+ color,
+ "white",
+ this._onChange(true, false),
+ );
+ this._addAttribute(
+ "contrast",
+ contrast,
+ 1.0,
+ this._onChange(true, false),
+ );
+ this._addAttribute(
+ "texRes",
+ texRes,
+ 128,
+ this._onChange(true, false),
+ );
+ this._addAttribute(
+ "interpolate",
+ interpolate,
+ false,
+ this._onChange(true, false),
+ );
+
+ // estimate the bounding box:
+ this._estimateBoundingBox();
+
+ if (this._autoLog)
+ {
+ this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
+ }
+
+ if (!Array.isArray(this.size) || this.size.length === 0) {
+ this.size = util.to_unit(GratingStim.#DEFAULT_STIM_SIZE_PX, "pix", this.win, this.units);
+ }
+ this._size_px = util.to_px(this.size, this.units, this.win);
+ }
+
+ /**
+ * Setter for the image attribute.
+ *
+ * @name module:visual.GratingStim#setImage
+ * @public
+ * @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
+ */
+ setTex(tex, log = false)
+ {
+ const response = {
+ origin: "GratingStim.setTex",
+ context: "when setting the tex of GratingStim: " + this._name,
+ };
+
+ try
+ {
+ let hasChanged = false;
+
+ // tex is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
+ if (typeof tex === "undefined")
+ {
+ this.psychoJS.logger.warn("setting the tex of GratingStim: " + this._name + " with argument: undefined.");
+ this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: undefined");
+ }
+ else if (GratingStim.#DEFINED_FUNCTIONS[tex] !== undefined)
+ {
+ // tex is a string and it is one of predefined functions available in shaders
+ this.psychoJS.logger.debug("the tex is one of predefined functions. Set the tex of GratingStim: " + this._name + " as: " + tex);
+ const curFuncName = this.getTex();
+ hasChanged = curFuncName ? curFuncName !== tex : true;
+ }
+ else
+ {
+ // tex is a string: it should be the name of a resource, which we load
+ if (typeof tex === "string")
+ {
+ tex = this.psychoJS.serverManager.getResource(tex);
+ }
+
+ // tex should now be an actual HTMLImageElement: we raise an error if it is not
+ if (!(tex instanceof HTMLImageElement))
+ {
+ throw "the argument: " + tex.toString() + " is not an image\" }";
+ }
+
+ this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: src= " + tex.src + ", size= " + tex.width + "x" + tex.height);
+ const existingImage = this.getTex();
+ hasChanged = existingImage ? existingImage.src !== tex.src : true;
+ }
+
+ this._setAttribute("tex", tex, log);
+
+ if (hasChanged)
+ {
+ this._onChange(true, true)();
+ }
+ }
+ catch (error)
+ {
+ throw Object.assign(response, { error });
+ }
+ }
+
+ /**
+ * Setter for the mask attribute.
+ *
+ * @name module:visual.GratingStim#setMask
+ * @public
+ * @param {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask
+ * @param {boolean} [log= false] - whether of not to log
+ */
+ setMask(mask, log = false)
+ {
+ const response = {
+ origin: "GratingStim.setMask",
+ context: "when setting the mask of GratingStim: " + this._name,
+ };
+
+ try
+ {
+ // mask is undefined: that's fine but we raise a warning in case this is a sympton of an actual problem
+ if (typeof mask === "undefined")
+ {
+ this.psychoJS.logger.warn("setting the mask of GratingStim: " + this._name + " with argument: undefined.");
+ this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: undefined");
+ }
+ else if (GratingStim.#DEFINED_FUNCTIONS[mask] !== undefined)
+ {
+ // mask is a string and it is one of predefined functions available in shaders
+ this.psychoJS.logger.debug("the mask is one of predefined functions. Set the mask of GratingStim: " + this._name + " as: " + mask);
+ }
+ else
+ {
+ // mask is a string: it should be the name of a resource, which we load
+ if (typeof mask === "string")
+ {
+ mask = this.psychoJS.serverManager.getResource(mask);
+ }
+
+ // mask should now be an actual HTMLImageElement: we raise an error if it is not
+ if (!(mask instanceof HTMLImageElement))
+ {
+ throw "the argument: " + mask.toString() + " is not an image\" }";
+ }
+
+ this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: src= " + mask.src + ", size= " + mask.width + "x" + mask.height);
+ }
+
+ this._setAttribute("mask", mask, log);
+
+ this._onChange(true, false)();
+ }
+ catch (error)
+ {
+ throw Object.assign(response, { error });
+ }
+ }
+
+ /**
+ * Get the size of the display image, which is either that of the GratingStim or that of the image
+ * it contains.
+ *
+ * @name module:visual.GratingStim#_getDisplaySize
+ * @private
+ * @return {number[]} the size of the displayed image
+ */
+ _getDisplaySize()
+ {
+ let displaySize = this.size;
+
+ if (typeof displaySize === "undefined")
+ {
+ // use the size of the pixi element, if we have access to it:
+ if (typeof this._pixi !== "undefined" && this._pixi.width > 0)
+ {
+ const pixiContainerSize = [this._pixi.width, this._pixi.height];
+ displaySize = util.to_unit(pixiContainerSize, "pix", this.win, this.units);
+ }
+ }
+
+ return displaySize;
+ }
+
+ /**
+ * Estimate the bounding box.
+ *
+ * @name module:visual.GratingStim#_estimateBoundingBox
+ * @function
+ * @override
+ * @protected
+ */
+ _estimateBoundingBox()
+ {
+ const size = this._getDisplaySize();
+ if (typeof size !== "undefined")
+ {
+ this._boundingBox = new PIXI.Rectangle(
+ this._pos[0] - size[0] / 2,
+ this._pos[1] - size[1] / 2,
+ size[0],
+ size[1],
+ );
+ }
+
+ // TODO take the orientation into account
+ }
+
+ /**
+ * Generate PIXI.Mesh object based on provided shader function name and uniforms.
+ *
+ * @name module:visual.GratingStim#_getPixiMeshFromPredefinedShaders
+ * @function
+ * @private
+ * @param {String} funcName - name of the shader function. Must be one of the DEFINED_FUNCTIONS
+ * @param {Object} uniforms - a set of uniforms to supply to the shader. Mixed together with default uniform values.
+ * @return {Pixi.Mesh} Pixi.Mesh object that represents shader and later added to the scene.
+ */
+ _getPixiMeshFromPredefinedShaders (funcName = "", uniforms = {}) {
+ const geometry = new PIXI.Geometry();
+ geometry.addAttribute(
+ "aVertexPosition",
+ [
+ 0, 0,
+ this._size_px[0], 0,
+ this._size_px[0], this._size_px[1],
+ 0, this._size_px[1]
+ ],
+ 2
+ );
+ geometry.addAttribute(
+ "aUvs",
+ [0, 0, 1, 0, 1, 1, 0, 1],
+ 2
+ );
+ geometry.addIndex([0, 1, 2, 0, 2, 3]);
+ const vertexSrc = defaultQuadVert;
+ const fragmentSrc = GratingStim.#DEFINED_FUNCTIONS[funcName].shader;
+ const uniformsFinal = Object.assign({}, GratingStim.#DEFINED_FUNCTIONS[funcName].uniforms, uniforms);
+ const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, uniformsFinal);
+ return new PIXI.Mesh(geometry, shader);
+ }
+
+ /**
+ * Set phase value for the function.
+ *
+ * @name module:visual.GratingStim#setPhase
+ * @public
+ * @param {number} phase - phase value
+ * @param {boolean} [log= false] - whether of not to log
+ */
+ setPhase (phase, log = false) {
+ this._setAttribute("phase", phase, log);
+ if (this._pixi instanceof PIXI.Mesh) {
+ this._pixi.shader.uniforms.uPhase = phase;
+ } else if (this._pixi instanceof PIXI.TilingSprite) {
+ this._pixi.tilePosition.x = -phase * (this._size_px[0] * this._pixi.tileScale.x) / (2 * Math.PI)
+ }
+ }
+
+ /**
+ * Set spatial frequency value for the function.
+ *
+ * @name module:visual.GratingStim#setPhase
+ * @public
+ * @param {number} sf - spatial frequency value
+ * @param {boolean} [log= false] - whether of not to log
+ */
+ setSpatialFrequency (sf, log = false) {
+ this._setAttribute("spatialFrequency", sf, log);
+ if (this._pixi instanceof PIXI.Mesh) {
+ this._pixi.shader.uniforms.uFreq = sf;
+ } else if (this._pixi instanceof PIXI.TilingSprite) {
+ // tileScale units are pixels, so converting function frequency to pixels
+ // and also taking into account possible size difference between used texture and requested stim size
+ this._pixi.tileScale.x = (1 / sf) * (this._pixi.width / this._pixi.texture.width);
+ }
+ }
+
+ /**
+ * Update the stimulus, if necessary.
+ *
+ * @name module:visual.GratingStim#_updateIfNeeded
+ * @private
+ */
+ _updateIfNeeded()
+ {
+ if (!this._needUpdate)
+ {
+ return;
+ }
+ this._needUpdate = false;
+
+ // update the PIXI representation, if need be:
+ if (this._needPixiUpdate)
+ {
+ this._needPixiUpdate = false;
+ if (typeof this._pixi !== "undefined")
+ {
+ this._pixi.destroy(true);
+ }
+ this._pixi = undefined;
+
+ // no image to draw: return immediately
+ if (typeof this._tex === "undefined")
+ {
+ return;
+ }
+
+ if (this._tex instanceof HTMLImageElement)
+ {
+ this._pixi = PIXI.TilingSprite.from(this._tex, {
+ width: this._size_px[0],
+ height: this._size_px[1]
+ });
+ this.setPhase(this._phase);
+ this.setSpatialFrequency(this._spatialFrequency);
+ }
+ else
+ {
+ this._pixi = this._getPixiMeshFromPredefinedShaders(this._tex, {
+ uFreq: this._spatialFrequency,
+ uPhase: this._phase
+ });
+ }
+ this._pixi.pivot.set(this._pixi.width * .5, this._pixi.width * .5);
+
+ // add a mask if need be:
+ if (typeof this._mask !== "undefined")
+ {
+ if (this._mask instanceof HTMLImageElement)
+ {
+ this._pixi.mask = PIXI.Sprite.from(this._mask);
+ this._pixi.addChild(this._pixi.mask);
+ }
+ else
+ {
+ // for some reason setting PIXI.Mesh as .mask doesn't do anything,
+ // rendering mask to texture for further use.
+ const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask);
+ const rt = PIXI.RenderTexture.create({
+ width: this._size_px[0],
+ height: this._size_px[1]
+ });
+ this.win._renderer.render(maskMesh, {
+ renderTexture: rt
+ });
+ const maskSprite = new PIXI.Sprite.from(rt);
+ this._pixi.mask = maskSprite;
+ this._pixi.addChild(maskSprite);
+ }
+ }
+
+ // since _pixi.width may not be immediately available but the rest of the code needs its value
+ // we arrange for repeated calls to _updateIfNeeded until we have a width:
+ if (this._pixi.width === 0)
+ {
+ this._needUpdate = true;
+ this._needPixiUpdate = true;
+ return;
+ }
+ }
+
+ this._pixi.zIndex = this._depth;
+ this._pixi.alpha = this.opacity;
+
+ // set the scale:
+ const displaySize = this._getDisplaySize();
+ this._size_px = util.to_px(displaySize, this.units, this.win);
+ const scaleX = this._size_px[0] / this._pixi.width;
+ const scaleY = this._size_px[1] / this._pixi.height;
+ this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
+ this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
+
+ // set the position, rotation, and anchor (image centered on pos):
+ let pos = to_pixiPoint(this.pos, this.units, this.win);
+ this._pixi.position.set(pos.x, pos.y);
+ this._pixi.rotation = this.ori * Math.PI / 180;
+
+ // re-estimate the bounding box, as the texture's width may now be available:
+ this._estimateBoundingBox();
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js
index b1f93ff9..c06df13c 100644
--- a/src/visual/GratingStim.js
+++ b/src/visual/GratingStim.js
@@ -13,25 +13,19 @@ import { ColorMixin } from "../util/ColorMixin.js";
import { to_pixiPoint } from "../util/Pixi.js";
import * as util from "../util/Util.js";
import { VisualStim } from "./VisualStim.js";
-import defaultQuadVert from './shaders/defaultQuad.vert';
-import sinShader from './shaders/sinShader.frag';
-import gaussShader from './shaders/gaussShader.frag';
-
-const DEFINED_FUNCTIONS = {
- sin: sinShader,
- sqr: undefined,
- saw: undefined,
- tri: undefined,
- sinXsin: undefined,
- sqrXsqr: undefined,
- circle: undefined,
- gauss: gaussShader,
- cross: undefined,
- radRamp: undefined,
- raisedCos: undefined
-};
-
-const DEFAULT_STIM_SIZE = [256, 256]; // in pixels
+import defaultQuadVert from "./shaders/defaultQuad.vert";
+import sinShader from "./shaders/sinShader.frag";
+import sqrShader from "./shaders/sqrShader.frag";
+import sawShader from "./shaders/sawShader.frag";
+import triShader from "./shaders/triShader.frag";
+import sinXsinShader from "./shaders/sinXsinShader.frag";
+import sqrXsqrShader from "./shaders/sqrXsqrShader.frag";
+import circleShader from "./shaders/circleShader.frag";
+import gaussShader from "./shaders/gaussShader.frag";
+import crossShader from "./shaders/crossShader.frag";
+import radRampShader from "./shaders/radRampShader.frag";
+import raisedCosShader from "./shaders/raisedCosShader.frag";
+
/**
* Grating Stimulus.
@@ -49,7 +43,7 @@ const DEFAULT_STIM_SIZE = [256, 256]; // in pixels
* @param {Array.} [options.pos= [0, 0]] - the position of the center of the stimulus
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
* @param {number} [options.size] - the size of the rendered image (the size of the image will be used if size is not specified)
- * @param {Color} [options.color= 'white'] the background color
+ * @param {Color} [options.color= "white"] the background color
* @param {number} [options.opacity= 1.0] - the opacity
* @param {number} [options.contrast= 1.0] - the contrast
* @param {number} [options.depth= 0] - the depth (i.e. the z order)
@@ -86,14 +80,95 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
// autoLog=None,
// autoDraw=False,
// maskParams=None)
+
+ static #DEFINED_FUNCTIONS = {
+ sin: {
+ shader: sinShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0
+ }
+ },
+ sqr: {
+ shader: sqrShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0
+ }
+ },
+ saw: {
+ shader: sawShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0
+ }
+ },
+ tri: {
+ shader: triShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0,
+ uPeriod: 1.0
+ }
+ },
+ sinXsin: {
+ shader: sinXsinShader,
+ uniforms: {
+
+ }
+ },
+ sqrXsqr: {
+ shader: sqrXsqrShader,
+ uniforms: {
+ uFreq: 1.0,
+ uPhase: 0.0
+ }
+ },
+ circle: {
+ shader: circleShader,
+ uniforms: {
+
+ }
+ },
+ gauss: {
+ shader: gaussShader,
+ uniforms: {
+ uA: 1.0,
+ uB: 0.0,
+ uC: 0.16
+ }
+ },
+ cross: {
+ shader: crossShader,
+ uniforms: {
+ uThickness: 0.1
+ }
+ },
+ radRamp: {
+ shader: radRampShader,
+ uniforms: {
+
+ }
+ },
+ raisedCos: {
+ shader: raisedCosShader,
+ uniforms: {
+ uBeta: 0.25,
+ uPeriod: 1.0
+ }
+ }
+ };
+
+ static #DEFAULT_STIM_SIZE_PX = [256, 256]; // in pixels
+
constructor({
name,
- tex,
+ tex = "sin",
win,
mask,
pos,
units,
- spatialFrequency = 10.,
+ spatialFrequency = 1.,
ori,
phase,
size,
@@ -127,12 +202,12 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
this._addAttribute(
"spatialFrequency",
spatialFrequency,
- 10.
+ GratingStim.#DEFINED_FUNCTIONS[tex].uniforms.uFreq || 1.0
);
this._addAttribute(
"phase",
phase,
- 0.
+ GratingStim.#DEFINED_FUNCTIONS[tex].uniforms.uPhase || 0.0
);
this._addAttribute(
"color",
@@ -168,9 +243,9 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
}
if (!Array.isArray(this.size) || this.size.length === 0) {
- this.size = util.to_unit(DEFAULT_STIM_SIZE, "pix", this.win, this.units);
+ this.size = util.to_unit(GratingStim.#DEFAULT_STIM_SIZE_PX, "pix", this.win, this.units);
}
- this._sizeInPixels = util.to_px(this.size, this.units, this.win);
+ this._size_px = util.to_px(this.size, this.units, this.win);
}
/**
@@ -198,7 +273,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
this.psychoJS.logger.warn("setting the tex of GratingStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: undefined");
}
- else if (DEFINED_FUNCTIONS[tex] !== undefined)
+ else if (GratingStim.#DEFINED_FUNCTIONS[tex] !== undefined)
{
// tex is a string and it is one of predefined functions available in shaders
this.psychoJS.logger.debug("the tex is one of predefined functions. Set the tex of GratingStim: " + this._name + " as: " + tex);
@@ -216,7 +291,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
// tex should now be an actual HTMLImageElement: we raise an error if it is not
if (!(tex instanceof HTMLImageElement))
{
- throw "the argument: " + tex.toString() + ' is not an image" }';
+ throw "the argument: " + tex.toString() + " is not an image\" }";
}
this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: src= " + tex.src + ", size= " + tex.width + "x" + tex.height);
@@ -260,7 +335,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
this.psychoJS.logger.warn("setting the mask of GratingStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: undefined");
}
- else if (DEFINED_FUNCTIONS[mask] !== undefined)
+ else if (GratingStim.#DEFINED_FUNCTIONS[mask] !== undefined)
{
// mask is a string and it is one of predefined functions available in shaders
this.psychoJS.logger.debug("the mask is one of predefined functions. Set the mask of GratingStim: " + this._name + " as: " + mask);
@@ -276,7 +351,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
// mask should now be an actual HTMLImageElement: we raise an error if it is not
if (!(mask instanceof HTMLImageElement))
{
- throw "the argument: " + mask.toString() + ' is not an image" }';
+ throw "the argument: " + mask.toString() + " is not an image\" }";
}
this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: src= " + mask.src + ", size= " + mask.width + "x" + mask.height);
@@ -341,42 +416,66 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
// TODO take the orientation into account
}
- _getPixiMeshFromPredefinedShaders (funcName = '', uniforms = {}) {
+ /**
+ * Generate PIXI.Mesh object based on provided shader function name and uniforms.
+ *
+ * @name module:visual.GratingStim#_getPixiMeshFromPredefinedShaders
+ * @function
+ * @private
+ * @param {String} funcName - name of the shader function. Must be one of the DEFINED_FUNCTIONS
+ * @param {Object} uniforms - a set of uniforms to supply to the shader. Mixed together with default uniform values.
+ * @return {Pixi.Mesh} Pixi.Mesh object that represents shader and later added to the scene.
+ */
+ _getPixiMeshFromPredefinedShaders (funcName = "", uniforms = {}) {
const geometry = new PIXI.Geometry();
geometry.addAttribute(
- 'aVertexPosition',
+ "aVertexPosition",
[
0, 0,
- this._sizeInPixels[0], 0,
- this._sizeInPixels[0], this._sizeInPixels[1],
- 0, this._sizeInPixels[1]
+ this._size_px[0], 0,
+ this._size_px[0], this._size_px[1],
+ 0, this._size_px[1]
],
2
);
geometry.addAttribute(
- 'aUvs',
+ "aUvs",
[0, 0, 1, 0, 1, 1, 0, 1],
2
);
geometry.addIndex([0, 1, 2, 0, 2, 3]);
const vertexSrc = defaultQuadVert;
- const fragmentSrc = DEFINED_FUNCTIONS[funcName];
- const uniformsFinal = Object.assign(uniforms, {
- // for future default uniforms
- });
+ const fragmentSrc = GratingStim.#DEFINED_FUNCTIONS[funcName].shader;
+ const uniformsFinal = Object.assign({}, GratingStim.#DEFINED_FUNCTIONS[funcName].uniforms, uniforms);
const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, uniformsFinal);
return new PIXI.Mesh(geometry, shader);
}
+ /**
+ * Set phase value for the function.
+ *
+ * @name module:visual.GratingStim#setPhase
+ * @public
+ * @param {number} phase - phase value
+ * @param {boolean} [log= false] - whether of not to log
+ */
setPhase (phase, log = false) {
this._setAttribute("phase", phase, log);
if (this._pixi instanceof PIXI.Mesh) {
this._pixi.shader.uniforms.uPhase = phase;
} else if (this._pixi instanceof PIXI.TilingSprite) {
- this._pixi.tilePosition.x = -phase * (this._sizeInPixels[0] * this._pixi.tileScale.x) / (2 * Math.PI)
+ this._pixi.tilePosition.x = -phase * (this._size_px[0] * this._pixi.tileScale.x) / (2 * Math.PI)
}
}
+ /**
+ * Set spatial frequency value for the function.
+ *
+ * @name module:visual.GratingStim#setPhase
+ * @public
+ * @param {number} sf - spatial frequency value
+ * @param {boolean} [log= false] - whether of not to log
+ */
setSpatialFrequency (sf, log = false) {
this._setAttribute("spatialFrequency", sf, log);
if (this._pixi instanceof PIXI.Mesh) {
@@ -421,8 +520,8 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
if (this._tex instanceof HTMLImageElement)
{
this._pixi = PIXI.TilingSprite.from(this._tex, {
- width: this._sizeInPixels[0],
- height: this._sizeInPixels[1]
+ width: this._size_px[0],
+ height: this._size_px[1]
});
this.setPhase(this._phase);
this.setSpatialFrequency(this._spatialFrequency);
@@ -450,8 +549,8 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
// rendering mask to texture for further use.
const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask);
const rt = PIXI.RenderTexture.create({
- width: this._sizeInPixels[0],
- height: this._sizeInPixels[1]
+ width: this._size_px[0],
+ height: this._size_px[1]
});
this.win._renderer.render(maskMesh, {
renderTexture: rt
@@ -477,9 +576,9 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
// set the scale:
const displaySize = this._getDisplaySize();
- this._sizeInPixels = util.to_px(displaySize, this.units, this.win);
- const scaleX = this._sizeInPixels[0] / this._pixi.width;
- const scaleY = this._sizeInPixels[1] / this._pixi.height;
+ this._size_px = util.to_px(displaySize, this.units, this.win);
+ const scaleX = this._size_px[0] / this._pixi.width;
+ const scaleY = this._size_px[1] / this._pixi.height;
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
diff --git a/src/visual/shaders/circleShader.frag b/src/visual/shaders/circleShader.frag
new file mode 100644
index 00000000..3211825a
--- /dev/null
+++ b/src/visual/shaders/circleShader.frag
@@ -0,0 +1,13 @@
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+
+void main() {
+ vec2 uv = vUvs;
+ float s = 1. - step(.5, length(uv - .5));
+ shaderOut = vec4(vec3(s), 1.0);
+}
diff --git a/src/visual/shaders/crossShader.frag b/src/visual/shaders/crossShader.frag
new file mode 100644
index 00000000..dbff4c10
--- /dev/null
+++ b/src/visual/shaders/crossShader.frag
@@ -0,0 +1,16 @@
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+uniform float uThickness;
+
+void main() {
+ vec2 uv = vUvs;
+ float sx = step(uThickness, length(uv.x - .5));
+ float sy = step(uThickness, length(uv.y - .5));
+ float s = 1. - sx * sy;
+ shaderOut = vec4(vec3(s), 1.0);
+}
diff --git a/src/visual/shaders/gaussShader.frag b/src/visual/shaders/gaussShader.frag
index 17543f62..ed7fbecd 100644
--- a/src/visual/shaders/gaussShader.frag
+++ b/src/visual/shaders/gaussShader.frag
@@ -1,17 +1,24 @@
+//
+// Gaussian Function:
+// https://en.wikipedia.org/wiki/Gaussian_function
+//
+
#version 300 es
precision mediump float;
in vec2 vUvs;
out vec4 shaderOut;
-#define M_PI 3.14159265358979
+uniform float uA;
+uniform float uB;
+uniform float uC;
-float gauss(float x) {
- return exp(-(x * x) * 20.);
-}
+#define M_PI 3.14159265358979
void main() {
vec2 uv = vUvs;
- float g = gauss(uv.x - .5) * gauss(uv.y - .5);
+ float c2 = uC * uC;
+ float x = length(uv - .5);
+ float g = uA * exp(-pow(x - uB, 2.) / c2 * .5);
shaderOut = vec4(vec3(g), 1.);
}
diff --git a/src/visual/shaders/radRampShader.frag b/src/visual/shaders/radRampShader.frag
new file mode 100644
index 00000000..bbbc5867
--- /dev/null
+++ b/src/visual/shaders/radRampShader.frag
@@ -0,0 +1,17 @@
+//
+// Radial ramp function
+//
+
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+
+void main() {
+ vec2 uv = vUvs;
+ float s = 1. - length(uv * 2. - 1.);
+ shaderOut = vec4(vec3(s), 1.0);
+}
diff --git a/src/visual/shaders/raisedCosShader.frag b/src/visual/shaders/raisedCosShader.frag
new file mode 100644
index 00000000..605fbfed
--- /dev/null
+++ b/src/visual/shaders/raisedCosShader.frag
@@ -0,0 +1,29 @@
+//
+// Raised-cosine function:
+// https://en.wikipedia.org/wiki/Raised-cosine_filter
+//
+
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+uniform float uBeta;
+uniform float uPeriod;
+
+void main() {
+ vec2 uv = vUvs;
+ float absX = length(uv * 2. - 1.);
+ float edgeArgument1 = (1. - uBeta) / (2. * uPeriod);
+ float edgeArgument2 = (1. + uBeta) / (2. * uPeriod);
+ float frequencyFactor = (M_PI * uPeriod) / uBeta;
+ float s = .5 * (1. + cos(frequencyFactor * (absX - edgeArgument1)));
+ if (absX <= edgeArgument1) {
+ s = 1.;
+ } else if (absX > edgeArgument2) {
+ s = 0.;
+ }
+ shaderOut = vec4(vec3(s), 1.0);
+}
diff --git a/src/visual/shaders/sawShader.frag b/src/visual/shaders/sawShader.frag
new file mode 100644
index 00000000..ed55ceb8
--- /dev/null
+++ b/src/visual/shaders/sawShader.frag
@@ -0,0 +1,21 @@
+//
+// Sawtooth wave:
+// https://en.wikipedia.org/wiki/Sawtooth_wave
+//
+
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+uniform float uFreq;
+uniform float uPhase;
+
+void main() {
+ vec2 uv = vUvs;
+ float s = uFreq * uv.x + uPhase;
+ s = mod(s, 1.);
+ shaderOut = vec4(vec3(s), 1.0);
+}
diff --git a/src/visual/shaders/sinXsinShader.frag b/src/visual/shaders/sinXsinShader.frag
new file mode 100644
index 00000000..15435ac3
--- /dev/null
+++ b/src/visual/shaders/sinXsinShader.frag
@@ -0,0 +1,17 @@
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+uniform float uFreq;
+uniform float uPhase;
+
+void main() {
+ vec2 uv = vUvs;
+ float sx = sin(uFreq * uv.x * 2. * M_PI + uPhase);
+ float sy = sin(uFreq * uv.y * 2. * M_PI + uPhase);
+ float s = sx * sy * .5 + .5;
+ shaderOut = vec4(vec3(s), 1.0);
+}
diff --git a/src/visual/shaders/sqrShader.frag b/src/visual/shaders/sqrShader.frag
new file mode 100644
index 00000000..dc9bd747
--- /dev/null
+++ b/src/visual/shaders/sqrShader.frag
@@ -0,0 +1,20 @@
+//
+// Square wave:
+// https://en.wikipedia.org/wiki/Square_wave
+//
+
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+uniform float uFreq;
+uniform float uPhase;
+
+void main() {
+ vec2 uv = vUvs;
+ float s = sign(sin(uFreq * uv.x * 2. * M_PI + uPhase));
+ shaderOut = vec4(.5 + .5 * vec3(s), 1.0);
+}
diff --git a/src/visual/shaders/sqrXsqrShader.frag b/src/visual/shaders/sqrXsqrShader.frag
new file mode 100644
index 00000000..4eb6cfa2
--- /dev/null
+++ b/src/visual/shaders/sqrXsqrShader.frag
@@ -0,0 +1,17 @@
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+uniform float uFreq;
+uniform float uPhase;
+
+void main() {
+ vec2 uv = vUvs;
+ float sx = sign(sin(uFreq * uv.x * 2. * M_PI + uPhase));
+ float sy = sign(sin(uFreq * uv.y * 2. * M_PI + uPhase));
+ float s = sx * sy * .5 + .5;
+ shaderOut = vec4(vec3(s), 1.0);
+}
diff --git a/src/visual/shaders/triShader.frag b/src/visual/shaders/triShader.frag
new file mode 100644
index 00000000..5b45ce08
--- /dev/null
+++ b/src/visual/shaders/triShader.frag
@@ -0,0 +1,22 @@
+//
+// Triangle wave:
+// https://en.wikipedia.org/wiki/Triangle_wave
+//
+
+#version 300 es
+precision mediump float;
+
+in vec2 vUvs;
+out vec4 shaderOut;
+
+#define M_PI 3.14159265358979
+uniform float uFreq;
+uniform float uPhase;
+uniform float uPeriod;
+
+void main() {
+ vec2 uv = vUvs;
+ float s = uFreq * uv.x + uPhase;
+ s = 2. * abs(s / uPeriod - floor(s / uPeriod + .5));
+ shaderOut = vec4(vec3(s), 1.0);
+}
From 3ebd9c4943403f330b994013925ba637e9483475 Mon Sep 17 00:00:00 2001
From: lgtst
Date: Mon, 21 Mar 2022 23:28:54 +0300
Subject: [PATCH 09/92] new documentation for GratingStim, variables and
methods renamed to match psychoPy
---
docs/visual_GratingStim.js.html | 205 +++++++++++++++---------
package.json | 1 +
scripts/build.js.cjs | 26 ++-
src/visual/GratingStim.js | 203 ++++++++++++++---------
src/visual/shaders/circleShader.frag | 13 +-
src/visual/shaders/crossShader.frag | 14 +-
src/visual/shaders/gaussShader.frag | 14 +-
src/visual/shaders/radRampShader.frag | 15 +-
src/visual/shaders/raisedCosShader.frag | 14 +-
src/visual/shaders/sawShader.frag | 14 +-
src/visual/shaders/sinShader.frag | 11 ++
src/visual/shaders/sinXsinShader.frag | 11 ++
src/visual/shaders/sqrShader.frag | 14 +-
src/visual/shaders/sqrXsqrShader.frag | 11 ++
src/visual/shaders/triShader.frag | 14 +-
15 files changed, 403 insertions(+), 177 deletions(-)
diff --git a/docs/visual_GratingStim.js.html b/docs/visual_GratingStim.js.html
index e6804ffb..80cfac39 100644
--- a/docs/visual_GratingStim.js.html
+++ b/docs/visual_GratingStim.js.html
@@ -29,9 +29,9 @@
Source: visual/GratingStim.js
/**
* Grating Stimulus.
*
- * @author Alain Pitiot
+ * @author Alain Pitiot, Nikita Agafonov
* @version 2021.2.0
- * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2021 Open Science Tools Ltd. (https://opensciencetools.org)
+ * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org)
* @license Distributed under the terms of the MIT License
*/
@@ -54,7 +54,6 @@
Source: visual/GratingStim.js
import radRampShader from "./shaders/radRampShader.frag";
import raisedCosShader from "./shaders/raisedCosShader.frag";
-
/**
* Grating Stimulus.
*
@@ -65,51 +64,108 @@
Source: visual/GratingStim.js
* @param {Object} options
* @param {String} options.name - the name used when logging messages from this stimulus
* @param {Window} options.win - the associated Window
- * @param {string | HTMLImageElement} options.image - the name of the image resource or the HTMLImageElement corresponding to the image
- * @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask
- * @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
+ * @param {String | HTMLImageElement} [options.tex="sin"] - the name of the predefined grating texture or image resource or the HTMLImageElement corresponding to the texture
+ * @param {String | HTMLImageElement} [options.mask] - the name of the mask resource or HTMLImageElement corresponding to the mask
+ * @param {String} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
+ * @param {number} [options.sf=1.0] - spatial frequency of the function used in grating stimulus
+ * @param {number} [options.phase=1.0] - phase of the function used in grating stimulus
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
- * @param {number} [options.size] - the size of the rendered image (the size of the image will be used if size is not specified)
+ * @param {number} [options.size] - the size of the rendered image (DEFAULT_STIM_SIZE_PX will be used if size is not specified)
* @param {Color} [options.color= "white"] the background color
* @param {number} [options.opacity= 1.0] - the opacity
* @param {number} [options.contrast= 1.0] - the contrast
* @param {number} [options.depth= 0] - the depth (i.e. the z order)
- * @param {number} [options.texRes= 128] - the resolution of the text
- * @param {boolean} [options.interpolate= false] - whether or not the image is interpolated
+ * @param {boolean} [options.interpolate= false] - whether or not the image is interpolated. NOT IMPLEMENTED YET.
+ * @param {String} [options.blendmode= 'avg'] - blend mode of the stimulus, determines how the stimulus is blended with the background. NOT IMPLEMENTED YET.
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
export class GratingStim extends util.mix(VisualStim).with(ColorMixin)
{
- // win,
- // tex="sin",
- // mask="none",
- // units="",
- // pos=(0.0, 0.0),
- // size=None,
- // sf=None,
- // ori=0.0,
- // phase=(0.0, 0.0),
- // texRes=128,
- // rgb=None,
- // dkl=None,
- // lms=None,
- // color=(1.0, 1.0, 1.0),
- // colorSpace='rgb',
- // contrast=1.0,
- // opacity=None,
- // depth=0,
- // rgbPedestal=(0.0, 0.0, 0.0),
- // interpolate=False,
- // blendmode='avg',
- // name=None,
- // autoLog=None,
- // autoDraw=False,
- // maskParams=None)
-
- static #DEFINED_FUNCTIONS = {
+ /**
+ * An object that keeps shaders source code and default uniform values for them.
+ * Shader source code is later used for construction of shader programs to create respective visual stimuli.
+ * @name module:visual.GratingStim.#SHADERS
+ * @type {Object}
+ * @property {Object} sin - Creates 2d sine wave image as if 1d sine graph was extended across Z axis and observed from above.
+ * {@link https://en.wikipedia.org/wiki/Sine_wave}
+ * @property {String} sin.shader - shader source code for the sine wave stimuli
+ * @property {Object} sin.uniforms - default uniforms for sine wave shader
+ * @property {float} sin.uniforms.uFreq=1.0 - frequency of sine wave.
+ * @property {float} sin.uniforms.uPhase=0.0 - phase of sine wave.
+ *
+ * @property {Object} sqr - Creates 2d square wave image as if 1d square graph was extended across Z axis and observed from above.
+ * {@link https://en.wikipedia.org/wiki/Square_wave}
+ * @property {String} sqr.shader - shader source code for the square wave stimuli
+ * @property {Object} sqr.uniforms - default uniforms for square wave shader
+ * @property {float} sqr.uniforms.uFreq=1.0 - frequency of square wave.
+ * @property {float} sqr.uniforms.uPhase=0.0 - phase of square wave.
+ *
+ * @property {Object} saw - Creates 2d sawtooth wave image as if 1d sawtooth graph was extended across Z axis and observed from above.
+ * {@link https://en.wikipedia.org/wiki/Sawtooth_wave}
+ * @property {String} saw.shader - shader source code for the sawtooth wave stimuli
+ * @property {Object} saw.uniforms - default uniforms for sawtooth wave shader
+ * @property {float} saw.uniforms.uFreq=1.0 - frequency of sawtooth wave.
+ * @property {float} saw.uniforms.uPhase=0.0 - phase of sawtooth wave.
+ *
+ * @property {Object} tri - Creates 2d triangle wave image as if 1d triangle graph was extended across Z axis and observed from above.
+ * {@link https://en.wikipedia.org/wiki/Triangle_wave}
+ * @property {String} tri.shader - shader source code for the triangle wave stimuli
+ * @property {Object} tri.uniforms - default uniforms for triangle wave shader
+ * @property {float} tri.uniforms.uFreq=1.0 - frequency of triangle wave.
+ * @property {float} tri.uniforms.uPhase=0.0 - phase of triangle wave.
+ * @property {float} tri.uniforms.uPeriod=1.0 - period of triangle wave.
+ *
+ * @property {Object} sinXsin - Creates an image of two 2d sine waves multiplied with each other.
+ * {@link https://en.wikipedia.org/wiki/Sine_wave}
+ * @property {String} sinXsin.shader - shader source code for the two multiplied sine waves stimuli
+ * @property {Object} sinXsin.uniforms - default uniforms for shader
+ * @property {float} sinXsin.uniforms.uFreq=1.0 - frequency of sine wave (both of them).
+ * @property {float} sinXsin.uniforms.uPhase=0.0 - phase of sine wave (both of them).
+ *
+ * @property {Object} sqrXsqr - Creates an image of two 2d square waves multiplied with each other.
+ * {@link https://en.wikipedia.org/wiki/Square_wave}
+ * @property {String} sqrXsqr.shader - shader source code for the two multiplied sine waves stimuli
+ * @property {Object} sqrXsqr.uniforms - default uniforms for shader
+ * @property {float} sqrXsqr.uniforms.uFreq=1.0 - frequency of sine wave (both of them).
+ * @property {float} sqrXsqr.uniforms.uPhase=0.0 - phase of sine wave (both of them).
+ *
+ * @property {Object} circle - Creates a filled circle shape with sharp edges.
+ * @property {String} circle.shader - shader source code for filled circle.
+ * @property {Object} circle.uniforms - default uniforms for shader.
+ * @property {float} circle.uniforms.uRadius=1.0 - Radius of the circle. Ranges [0.0, 1.0], where 0.0 is circle so tiny it results in empty stim
+ * and 1.0 is circle that spans from edge to edge of the stim.
+ *
+ * @property {Object} gauss - Creates a 2d Gaussian image as if 1d Gaussian graph was rotated arount Y axis and observed from above.
+ * {@link https://en.wikipedia.org/wiki/Gaussian_function}
+ * @property {String} gauss.shader - shader source code for Gaussian shader
+ * @property {Object} gauss.uniforms - default uniforms for shader
+ * @property {float} gauss.uniforms.uA=1.0 - A constant for gaussian formula (see link).
+ * @property {float} gauss.uniforms.uB=0.0 - B constant for gaussian formula (see link).
+ * @property {float} gauss.uniforms.uC=0.16 - C constant for gaussian formula (see link).
+ *
+ * @property {Object} cross - Creates a filled cross shape with sharp edges.
+ * @property {String} cross.shader - shader source code for cross shader
+ * @property {Object} cross.uniforms - default uniforms for shader
+ * @property {float} cross.uniforms.uThickness=0.2 - Thickness of the cross. Ranges [0.0, 1.0], where 0.0 thickness makes a cross so thin it becomes
+ * invisible and results in an empty stim and 1.0 makes it so thick it fills the entire stim.
+ *
+ * @property {Object} radRamp - Creates 2d radial ramp image.
+ * @property {String} radRamp.shader - shader source code for radial ramp shader
+ * @property {Object} radRamp.uniforms - default uniforms for shader
+ * @property {float} radRamp.uniforms.uSqueeze=1.0 - coefficient that helps to modify size of the ramp. Ranges [0.0, Infinity], where 0.0 results in ramp being so large
+ * it fills the entire stim and Infinity makes it so tiny it's invisible.
+ *
+ * @property {Object} raisedCos - Creates 2d raised-cosine image as if 1d raised-cosine graph was rotated around Y axis and observed from above.
+ * {@link https://en.wikipedia.org/wiki/Raised-cosine_filter}
+ * @property {String} raisedCos.shader - shader source code for raised-cosine shader
+ * @property {Object} raisedCos.uniforms - default uniforms for shader
+ * @property {float} raisedCos.uniforms.uBeta=0.25 - roll-off factor (see link).
+ * @property {float} raisedCos.uniforms.uPeriod=0.625 - reciprocal of the symbol-rate (see link).
+ */
+ static #SHADERS = {
sin: {
shader: sinShader,
uniforms: {
@@ -142,7 +198,8 @@
}
/**
- * Setter for the image attribute.
+ * Setter for the tex attribute.
*
- * @name module:visual.GratingStim#setImage
+ * @name module:visual.GratingStim#setTex
* @public
- * @param {HTMLImageElement | string} image - the name of the image resource or HTMLImageElement corresponding to the image
+ * @param {HTMLImageElement | string} tex - the name of built in shader function or name of the image resource or HTMLImageElement corresponding to the image
* @param {boolean} [log= false] - whether of not to log
*/
setTex(tex, log = false)
@@ -301,7 +353,7 @@
Source: visual/GratingStim.js
this.psychoJS.logger.warn("setting the tex of GratingStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: undefined");
}
- else if (GratingStim.#DEFINED_FUNCTIONS[tex] !== undefined)
+ else if (GratingStim.#SHADERS[tex] !== undefined)
{
// tex is a string and it is one of predefined functions available in shaders
this.psychoJS.logger.debug("the tex is one of predefined functions. Set the tex of GratingStim: " + this._name + " as: " + tex);
@@ -363,7 +415,7 @@
Source: visual/GratingStim.js
this.psychoJS.logger.warn("setting the mask of GratingStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: undefined");
}
- else if (GratingStim.#DEFINED_FUNCTIONS[mask] !== undefined)
+ else if (GratingStim.#SHADERS[mask] !== undefined)
{
// mask is a string and it is one of predefined functions available in shaders
this.psychoJS.logger.debug("the mask is one of predefined functions. Set the mask of GratingStim: " + this._name + " as: " + mask);
@@ -449,8 +501,8 @@
Source: visual/GratingStim.js
*
* @name module:visual.GratingStim#_getPixiMeshFromPredefinedShaders
* @function
- * @private
- * @param {String} funcName - name of the shader function. Must be one of the DEFINED_FUNCTIONS
+ * @protected
+ * @param {String} funcName - name of the shader function. Must be one of the SHADERS
* @param {Object} uniforms - a set of uniforms to supply to the shader. Mixed together with default uniform values.
* @return {Pixi.Mesh} Pixi.Mesh object that represents shader and later added to the scene.
*/
@@ -473,8 +525,8 @@
/**
* Set spatial frequency value for the function.
*
- * @name module:visual.GratingStim#setPhase
+ * @name module:visual.GratingStim#setSF
* @public
* @param {number} sf - spatial frequency value
- * @param {boolean} [log= false] - whether of not to log
+ * @param {boolean} [log=false] - whether or not to log
*/
- setSpatialFrequency (sf, log = false) {
- this._setAttribute("spatialFrequency", sf, log);
+ setSF (sf, log = false) {
+ this._setAttribute("SF", sf, log);
if (this._pixi instanceof PIXI.Mesh) {
this._pixi.shader.uniforms.uFreq = sf;
} else if (this._pixi instanceof PIXI.TilingSprite) {
// tileScale units are pixels, so converting function frequency to pixels
// and also taking into account possible size difference between used texture and requested stim size
this._pixi.tileScale.x = (1 / sf) * (this._pixi.width / this._pixi.texture.width);
+ // since most functions defined in SHADERS assume spatial frequency change along X axis
+ // we assume desired effect for image based stims to be the same so tileScale.y is not affected by spatialFrequency
+ this._pixi.tileScale.y = this._pixi.height / this._pixi.texture.height;
}
}
@@ -552,16 +607,16 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/core_EventManager.js.html b/docs/core_EventManager.js.html
index 4d915f91..891ef9b7 100644
--- a/docs/core_EventManager.js.html
+++ b/docs/core_EventManager.js.html
@@ -35,9 +35,8 @@
Source: core/EventManager.js
* @license Distributed under the terms of the MIT License
*/
-import {MonotonicClock, Clock} from '../util/Clock';
-import {PsychoJS} from './PsychoJS';
-
+import { Clock, MonotonicClock } from "../util/Clock.js";
+import { PsychoJS } from "./PsychoJS.js";
/**
* @class
@@ -50,7 +49,6 @@
pressed: [0, 0, 0],
clocks: [new Clock(), new Clock(), new Clock()],
// time elapsed from last reset of the button.Clocks:
- times: [0.0, 0.0, 0.0]
+ times: [0.0, 0.0, 0.0],
},
// clock reset when mouse is moved:
- moveClock: new Clock()
+ moveClock: new Clock(),
};
}
-
/**
* Get the list of keys pressed by the participant.
*
@@ -97,9 +94,9 @@
Source: core/EventManager.js
* @return {string[]} the list of keys that were pressed.
*/
getKeys({
- keyList = null,
- timeStamped = false
- } = {})
+ keyList = null,
+ timeStamped = false,
+ } = {})
{
if (keyList != null)
{
@@ -151,7 +148,6 @@
Source: core/EventManager.js
return keys;
}
-
/**
* @typedef EventManager.ButtonInfo
* @property {Array.number} pressed - the status of each mouse button [left, center, right]: 1 for pressed, 0 for released
@@ -179,7 +175,6 @@
Source: core/EventManager.js
return this._mouseInfo;
}
-
/**
* Clear all events from the event buffer.
*
@@ -194,7 +189,6 @@
Source: core/EventManager.js
this.clearKeys();
}
-
/**
* Clear all keys from the key buffer.
*
@@ -207,7 +201,6 @@
// add a keydown listener
// note: IE11 is not happy with document.addEventListener
window.addEventListener("keydown", (event) =>
-// document.addEventListener("keydown", (event) =>
+ // document.addEventListener("keydown", (event) =>
{
const timestamp = MonotonicClock.getReferenceTime();
let code = event.code;
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
- if (typeof code === 'undefined')
+ if (typeof code === "undefined")
{
code = EventManager.keycode2w3c(event.keyCode);
}
@@ -380,17 +374,15 @@
Source: core/EventManager.js
code,
key: event.key,
keyCode: event.keyCode,
- timestamp
+ timestamp,
});
- self._psychoJS.logger.trace('keydown: ', event.key);
- self._psychoJS.experimentLogger.data('Keydown: ' + event.key);
+ self._psychoJS.logger.trace("keydown: ", event.key);
+ self._psychoJS.experimentLogger.data("Keydown: " + event.key);
event.stopPropagation();
});
-
}
-
/**
* 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>
@@ -406,7 +398,7 @@
Source: core/EventManager.js
let w3cKeyList = [];
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]);
}
@@ -419,7 +411,6 @@
Source: core/EventManager.js
return w3cKeyList;
}
-
/**
* Convert a W3C Key Code into a pyglet key.
*
@@ -438,11 +429,10 @@
Source: core/EventManager.js
}
else
{
- return 'N/A';
+ return "N/A";
}
}
-
/**
* Convert a keycode to a W3C UI Event code.
* <p>This is for legacy browsers.</p>
@@ -460,7 +450,6 @@
Source: core/EventManager.js
}
}
-
/**
* <p>This map provides support for browsers that have not yet
* adopted the W3C KeyboardEvent.code standard for detecting key presses.
@@ -550,10 +539,9 @@
Source: core/EventManager.js
39: "ArrowRight",
40: "ArrowDown",
27: "Escape",
- 32: "Space"
+ 32: "Space",
};
-
/**
* This map associates pyglet key names to the corresponding W3C KeyboardEvent codes values.
* <p>More information can be found [here]{@link https://www.w3.org/TR/uievents-code}</p>
@@ -653,10 +641,9 @@
*/
EventManager._reversePygletMap = {};
-
/**
* Utility class used by the experiment scripts to keep track of a clock and of the current status (whether or not we are currently checking the keyboard)
*
@@ -684,8 +670,8 @@
Source: core/EventManager.js
this.status = PsychoJS.Status.NOT_STARTED;
this.keys = []; // the key(s) pressed
- this.corr = 0; // was the resp correct this trial? (0=no, 1=yes)
- this.rt = []; // response time(s)
+ this.corr = 0; // was the resp correct this trial? (0=no, 1=yes)
+ this.rt = []; // response time(s)
this.clock = new Clock(); // we'll use this to measure the rt
}
}
@@ -699,13 +685,13 @@
* @license Distributed under the terms of the MIT License
*/
-import * as Tone from 'tone';
-import {PsychoJS} from './PsychoJS';
-import {ServerManager} from './ServerManager';
-import {Scheduler} from '../util/Scheduler';
-import {Clock} from '../util/Clock';
-import {ExperimentHandler} from '../data/ExperimentHandler';
-import * as util from '../util/Util';
-
+import * as Tone from "tone";
+import { ExperimentHandler } from "../data/ExperimentHandler.js";
+import { Clock } from "../util/Clock.js";
+import { Scheduler } from "../util/Scheduler.js";
+import * as util from "../util/Util.js";
+import { PsychoJS } from "./PsychoJS.js";
+import { ServerManager } from "./ServerManager.js";
/**
* @class
@@ -55,7 +54,6 @@
Source: core/GUI.js
*/
export class GUI
{
-
get dialogComponent()
{
return this._dialogComponent;
@@ -74,7 +72,6 @@
Source: core/GUI.js
this._dialogScalingFactor = 0;
}
-
/**
* <p>Create a dialog box that (a) enables the participant to set some
* experimental values (e.g. the session name), (b) shows progress of resource
@@ -99,22 +96,21 @@
Source: core/GUI.js
* @param {String} options.title - name of the project
*/
DlgFromDict({
- logoUrl,
- text,
- dictionary,
- title
- })
+ logoUrl,
+ text,
+ dictionary,
+ title,
+ })
{
// get info from URL:
const infoFromUrl = util.getUrlParameters();
- this._progressMsg = ' ';
+ this._progressMsg = " ";
this._progressBarMax = 0;
this._allResourcesDownloaded = false;
this._requiredKeys = [];
this._setRequiredKeys = new Map();
-
// prepare PsychoJS component:
this._dialogComponent = {};
this._dialogComponent.status = PsychoJS.Status.NOT_STARTED;
@@ -132,141 +128,126 @@
Source: core/GUI.js
// 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 &&
- typeof self._psychoJS.config.experiment.license !== 'undefined' &&
- self._psychoJS.config.experiment.runMode === 'LICENSE' &&
- typeof self._psychoJS.config.experiment.license.institutionLogo !== 'undefined')
+ if (
+ self._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER
+ && typeof self._psychoJS.config.experiment.license !== "undefined"
+ && self._psychoJS.config.experiment.runMode === "LICENSE"
+ && typeof self._psychoJS.config.experiment.license.institutionLogo !== "undefined"
+ )
{
logoUrl = self._psychoJS.config.experiment.license.institutionLogo;
}
// prepare jquery UI dialog box:
- let htmlCode =
- '<div id="expDialog" title="' + title + '">';
+ let htmlCode = '<div id="expDialog" title="' + title + '">';
// uncomment for older version of the library:
// htmlCode += '<p style="font-size: 0.8em; padding: 0.5em; margin-bottom: 0.5em; color: #FFAA00; border: 1px solid #FFAA00;">⚠ This experiment uses a deprecated version of the PsychoJS library. Consider updating to a newer version (e.g. by updating PsychoPy and re-exporting the experiment).</p>'+
// logo:
- if (typeof logoUrl === 'string')
+ if (typeof logoUrl === "string")
{
htmlCode += '<img id="dialog-logo" class="logo" alt="logo" src="' + logoUrl + '">';
}
// information text:
- if (typeof text === 'string' && text.length > 0)
+ if (typeof text === "string" && text.length > 0)
{
- htmlCode += '<p>' + text + '</p>';
+ htmlCode += "<p>" + text + "</p>";
}
-
// add a combobox or text areas for each entry in the dictionary:
// These may include Symbols as opposed to when using a for...in loop,
// but only strings are allowed in PsychoPy
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();
+ infoFromUrl.forEach((urlValue, urlKey) =>
{
- const value = dictionary[key];
- const keyId = 'form-input-' + keyIdx;
+ const cleanedUrlKey = urlKey.trim().toLowerCase();
+ if (cleanedUrlKey === cleanedDictKey)
+ {
+ inUrl = true;
+ // break;
+ }
+ });
- // only create an input if the key is not in the URL:
- let inUrl = false;
- const cleanedDictKey = key.trim().toLowerCase();
- infoFromUrl.forEach((urlValue, urlKey) =>
- {
- const cleanedUrlKey = urlKey.trim().toLowerCase();
- if (cleanedUrlKey === cleanedDictKey)
- {
- inUrl = true;
- // break;
- }
- });
+ if (!inUrl)
+ {
+ htmlCode += '<label for="' + keyId + '">' + key + "</label>";
- if (!inUrl)
+ // if the field is required:
+ if (key.slice(-1) === "*")
{
- htmlCode += '<label for="' + keyId + '">' + key + '</label>';
+ self._requiredKeys.push(keyId);
+ }
- // if the field is required:
- if (key.slice(-1) === '*')
- {
- self._requiredKeys.push(keyId);
- }
+ // if value is an array, we create a select drop-down menu:
+ if (Array.isArray(value))
+ {
+ htmlCode += '<select name="' + key + '" id="' + keyId + '" class="text ui-widget-content'
+ + ' ui-corner-all">';
- // if value is an array, we create a select drop-down menu:
- if (Array.isArray(value))
+ // if the field is required, we add an empty option and select it:
+ if (key.slice(-1) === "*")
{
- htmlCode += '<select name="' + key + '" id="' + keyId + '" class="text ui-widget-content' +
- ' ui-corner-all">';
-
- // if the field is required, we add an empty option and select it:
- if (key.slice(-1) === '*')
- {
- htmlCode += '<option disabled selected>...</option>';
- }
-
- for (const option of value)
- {
- htmlCode += '<option>' + option + '</option>';
- }
-
- htmlCode += '</select>';
- jQuery('#' + keyId).selectmenu({classes: {}});
+ htmlCode += "<option disabled selected>...</option>";
}
- // otherwise we use a single string input:
- else /*if (typeof value === 'string')*/
+ for (const option of value)
{
- htmlCode += '<input type="text" name="' + key + '" id="' + keyId;
- htmlCode += '" value="' + value + '" class="text ui-widget-content ui-corner-all">';
+ htmlCode += "<option>" + option + "</option>";
}
+
+ htmlCode += "</select>";
+ jQuery("#" + keyId).selectmenu({ classes: {} });
+ }
+ // otherwise we use a single string input:
+ /*if (typeof value === 'string')*/
+ else
+ {
+ htmlCode += '<input type="text" name="' + key + '" id="' + keyId;
+ htmlCode += '" value="' + value + '" class="text ui-widget-content ui-corner-all">';
}
}
- );
+ });
- htmlCode += '<p class="validateTips">Fields marked with an asterisk (*) are required.</p>';
+ if (this._requiredKeys.length > 0)
+ {
+ htmlCode += '<p class="validateTips">Fields marked with an asterisk (*) are required.</p>';
+ }
// add a progress bar:
- htmlCode += '<hr><div id="progressMsg" class="progress">' + self._progressMsg + '</div>';
+ htmlCode += '<hr><div id="progressMsg" class="progress">' + self._progressMsg + "</div>";
htmlCode += '<div id="progressbar"></div></div>';
-
// replace root by the html code:
- const dialogElement = document.getElementById('root');
+ const dialogElement = document.getElementById("root");
dialogElement.innerHTML = htmlCode;
-
- // when the logo is loaded, we call _onDialogOpen again to reset the dimensions and position of
- // the dialog box:
- if (typeof logoUrl === 'string')
- {
- jQuery("#dialog-logo").on('load', () =>
- {
- self._onDialogOpen('#expDialog')();
- });
- }
-
-
// setup change event handlers for all required keys:
this._requiredKeys.forEach((keyId) =>
+ {
+ const input = document.getElementById(keyId);
+ if (input)
{
- const input = document.getElementById(keyId);
- if (input)
- {
- input.oninput = (event) => GUI._onKeyChange(self, event);
- }
+ input.oninput = (event) => GUI._onKeyChange(self, event);
}
- );
+ });
// init and open the dialog box:
- self._dialogComponent.button = 'Cancel';
- self._estimateDialogScalingFactor();
- const dialogSize = self._getDialogSize();
+ self._dialogComponent.button = "Cancel";
jQuery("#expDialog").dialog({
- width: dialogSize[0],
- maxHeight: dialogSize[1],
+ width: "500",
autoOpen: true,
- modal: true,
+ modal: false,
closeOnEscape: false,
resizable: false,
draggable: false,
@@ -275,75 +256,62 @@
Source: core/GUI.js
{
id: "buttonCancel",
text: "Cancel",
- click: function ()
+ click: function()
{
- self._dialogComponent.button = 'Cancel';
- jQuery("#expDialog").dialog('close');
- }
+ self._dialogComponent.button = "Cancel";
+ jQuery("#expDialog").dialog("close");
+ },
},
{
id: "buttonOk",
text: "Ok",
- click: function ()
+ click: function()
{
-
// update dictionary:
Object.keys(dictionary).forEach((key, keyIdx) =>
+ {
+ const input = document.getElementById("form-input-" + keyIdx);
+ if (input)
{
- const input = document.getElementById('form-input-' + keyIdx);
- if (input)
- {
- dictionary[key] = input.value;
- }
+ dictionary[key] = input.value;
}
- );
-
+ });
- self._dialogComponent.button = 'OK';
- jQuery("#expDialog").dialog('close');
+ self._dialogComponent.button = "OK";
+ jQuery("#expDialog").dialog("close");
// Tackle browser demands on having user action initiate audio context
Tone.start();
// switch to full screen if requested:
self._psychoJS.window.adjustScreenSize();
-
- // Clear events (and keypresses) accumulated during the dialog
- self._psychoJS.eventManager.clearEvents();
- }
- }
- ],
- // open the dialog in the middle of the screen:
- open: self._onDialogOpen('#expDialog'),
+ // Clear events (and keypresses) accumulated during the dialog
+ self._psychoJS.eventManager.clearEvents();
+ },
+ },
+ ],
// close is called by both buttons and when the user clicks on the cross:
- close: function ()
+ close: function()
{
- //jQuery.unblockUI();
- jQuery(this).dialog('destroy').remove();
+ // jQuery.unblockUI();
+ jQuery(this).dialog("destroy").remove();
self._dialogComponent.status = PsychoJS.Status.FINISHED;
- }
-
+ },
})
- // change colour of title bar
+ // change colour of title bar
.prev(".ui-dialog-titlebar").css("background", "green");
-
// update the OK button status:
self._updateOkButtonStatus();
-
- // when the browser window is resize, we redimension and reposition the dialog:
- self._dialogResize('#expDialog');
-
-
// block UI until user has pressed dialog button:
// note: block UI does not allow for text to be entered in the dialog form boxes, alas!
- //jQuery.blockUI({ message: "", baseZ: 1});
+ // jQuery.blockUI({ message: "", baseZ: 1});
// show dialog box:
- jQuery("#progressbar").progressbar({value: self._progressBarCurrentValue});
+ jQuery("#progressbar").progressbar({ value: self._progressBarCurrentValue });
jQuery("#progressbar").progressbar("option", "max", self._progressBarMax);
}
@@ -358,7 +326,6 @@
});
}
-
- /**
- * Estimate the scaling factor for the dialog popup windows.
- *
- * @name module:core.GUI#_estimateDialogScalingFactor
- * @function
- * @private
- * @returns {boolean} whether or not the scaling factor is different from the previously estimated one
- */
- _estimateDialogScalingFactor()
- {
- const windowSize = [jQuery(window).width(), jQuery(window).height()];
-
- // desktop:
- let dialogScalingFactor = 1.0;
-
- // mobile or tablet:
- if (windowSize[0] < 1080)
- {
- // landscape:
- if (windowSize[0] > windowSize[1])
- {
- dialogScalingFactor = 1.5;
- }// portrait:
- else
- {
- dialogScalingFactor = 2.0;
- }
- }
-
- const isDifferent = (dialogScalingFactor !== this._dialogScalingFactor);
- this._dialogScalingFactor = dialogScalingFactor;
-
- return isDifferent;
- }
-
-
- /**
- * Get the size of the dialog.
- *
- * @name module:core.GUI#_getDialogSize
- * @private
- * @returns {number[]} the size of the popup dialog window
- */
- _getDialogSize()
- {
- const windowSize = [jQuery(window).width(), jQuery(window).height()];
- this._estimateDialogScalingFactor();
-
- return [
- Math.min(GUI.dialogMaxSize[0], (windowSize[0] - GUI.dialogMargin[0]) / this._dialogScalingFactor),
- Math.min(GUI.dialogMaxSize[1], (windowSize[1] - GUI.dialogMargin[1]) / this._dialogScalingFactor)];
- }
-
-
/**
* Listener for change event for required keys.
*
@@ -769,7 +601,7 @@
Source: core/GUI.js
const element = event.target;
const value = element.value;
- if (typeof value !== 'undefined' && value.length > 0)
+ if (typeof value !== "undefined" && value.length > 0)
{
gui._setRequiredKeys.set(event.target, true);
}
@@ -781,7 +613,6 @@
Source: core/GUI.js
gui._updateOkButtonStatus(false);
}
-
/**
* Get a more user-friendly html message.
*
@@ -796,91 +627,111 @@
Source: core/GUI.js
// INTERNAL_ERROR
case 1:
return {
- htmlCode: '<div id="msgDialog" title="Error"><p>Oops we encountered an internal server error.</p><p>Try to run the experiment again. If the error persists, contact the experiment designer.</p></div>',
- titleColour: 'red'
+ htmlCode:
+ '<div id="msgDialog" title="Error"><p>Oops we encountered an internal server error.</p><p>Try to run the experiment again. If the error persists, contact the experiment designer.</p></div>',
+ titleColour: "red",
};
// MONGODB_ERROR
+
case 2:
return {
- htmlCode: '<div id="msgDialog" title="Error"><p>Oops we encountered a database error.</p><p>Try to run the experiment again. If the error persists, contact the experiment designer.</p></div>',
- titleColour: 'red'
+ htmlCode:
+ '<div id="msgDialog" title="Error"><p>Oops we encountered a database error.</p><p>Try to run the experiment again. If the error persists, contact the experiment designer.</p></div>',
+ titleColour: "red",
};
// STATUS_NONE
+
case 20:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> does not have any status and cannot be run.</p><p>If you are the experiment designer, go to your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a> and change the experiment status to either PILOTING or RUNNING.</p><p>Otherwise please contact the experiment designer to let him or her know that the status must be changed to RUNNING for participants to be able to run it.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> does not have any status and cannot be run.</p><p>If you are the experiment designer, go to your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a> and change the experiment status to either PILOTING or RUNNING.</p><p>Otherwise please contact the experiment designer to let him or her know that the status must be changed to RUNNING for participants to be able to run it.</p></div>`,
+ titleColour: "orange",
};
// STATUS_INACTIVE
+
case 21:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> is currently inactive and cannot be run.</p><p>If you are the experiment designer, go to your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a> and change the experiment status to either PILOTING or RUNNING.</p><p>Otherwise please contact the experiment designer to let him or her know that the status must be changed to RUNNING for participants to be able to run it.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> is currently inactive and cannot be run.</p><p>If you are the experiment designer, go to your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a> and change the experiment status to either PILOTING or RUNNING.</p><p>Otherwise please contact the experiment designer to let him or her know that the status must be changed to RUNNING for participants to be able to run it.</p></div>`,
+ titleColour: "orange",
};
// STATUS_DELETED
+
case 22:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> has been deleted and cannot be run.</p><p>If you are the experiment designer, either go to your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a> and change the experiment status to either PILOTING or RUNNING, or generate a new experiment.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment has been deleted and cannot be run any longer.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> has been deleted and cannot be run.</p><p>If you are the experiment designer, either go to your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a> and change the experiment status to either PILOTING or RUNNING, or generate a new experiment.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment has been deleted and cannot be run any longer.</p></div>`,
+ titleColour: "orange",
};
// STATUS_ARCHIVED
+
case 23:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> has been archived and cannot be run.</p><p>If you are the experiment designer, go to your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a> and change the experiment status to either PILOTING or RUNNING.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment has been archived and cannot be run at the moment.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> has been archived and cannot be run.</p><p>If you are the experiment designer, go to your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a> and change the experiment status to either PILOTING or RUNNING.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment has been archived and cannot be run at the moment.</p></div>`,
+ titleColour: "orange",
};
// PILOTING_NO_TOKEN
+
case 30:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> is currently in PILOTING mode but the pilot token is missing from the URL.</p><p>If you are the experiment designer, you can pilot it by pressing the pilot button on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment status must be changed to RUNNING for participants to be able to run it.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> is currently in PILOTING mode but the pilot token is missing from the URL.</p><p>If you are the experiment designer, you can pilot it by pressing the pilot button on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment status must be changed to RUNNING for participants to be able to run it.</p></div>`,
+ titleColour: "orange",
};
// PILOTING_INVALID_TOKEN
+
case 31:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> cannot be run because the pilot token in the URL is invalid, possibly because it has expired.</p><p>If you are the experiment designer, you can generate a new token by pressing the pilot button on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment status must be changed to RUNNING for participants to be able to run it.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> cannot be run because the pilot token in the URL is invalid, possibly because it has expired.</p><p>If you are the experiment designer, you can generate a new token by pressing the pilot button on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment status must be changed to RUNNING for participants to be able to run it.</p></div>`,
+ titleColour: "orange",
};
// LICENSE_EXPIRED
+
case 50:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> is covered by a license that has expired. </p><p>If you are the experiment designer, you can either contact the license manager to inquire about the expiration, or you can run your experiments using credits. You will find all relevant details about the license on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>, where you will also be able to change its running mode to CREDIT.</p><p>Otherwise please contact the experiment designer to let him or her know that there is an issue with the experiment's license having expired.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> is covered by a license that has expired. </p><p>If you are the experiment designer, you can either contact the license manager to inquire about the expiration, or you can run your experiments using credits. You will find all relevant details about the license on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>, where you will also be able to change its running mode to CREDIT.</p><p>Otherwise please contact the experiment designer to let him or her know that there is an issue with the experiment's license having expired.</p></div>`,
+ titleColour: "orange",
};
// LICENSE_APPROVAL_NEEDED
+
case 51:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> is covered by a license that requires one or more documents to be approved before the experiment can be run. </p><p>If you are the experiment designer, please contact the license manager and ask him or her which documents must be approved. You will find all relevant details about the license on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>.</p><p>Otherwise please contact the experiment designer to let him or her know that there is an issue with the experiment's license requiring documents to be approved.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> is covered by a license that requires one or more documents to be approved before the experiment can be run. </p><p>If you are the experiment designer, please contact the license manager and ask him or her which documents must be approved. You will find all relevant details about the license on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>.</p><p>Otherwise please contact the experiment designer to let him or her know that there is an issue with the experiment's license requiring documents to be approved.</p></div>`,
+ titleColour: "orange",
};
// CREDIT_NOT_ENOUGH
+
case 60:
return {
- htmlCode: `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> does not have any assigned credit left and cannot be run.</p><p>If you are the experiment designer, you can assign more credits to it on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment requires more assigned credits to run.</p></div>`,
- titleColour: 'orange'
+ htmlCode:
+ `<div id="msgDialog" title="Warning"><p><strong>${this._psychoJS.config.experiment.fullpath}</strong> does not have any assigned credit left and cannot be run.</p><p>If you are the experiment designer, you can assign more credits to it on your <a href="https://pavlovia.org/${this._psychoJS.config.experiment.fullpath}">experiment page</a>.</p><p>Otherwise please contact the experiment designer to let him or her know that the experiment requires more assigned credits to run.</p></div>`,
+ titleColour: "orange",
};
default:
return {
- htmlCode: `<div id="msgDialog" title="Error"><p>Unfortunately we encountered an unspecified error (error code: ${errorCode}.</p><p>Try to run the experiment again. If the error persists, contact the experiment designer.</p></div>`,
- titleColour: 'red'
+ htmlCode:
+ `<div id="msgDialog" title="Error"><p>Unfortunately we encountered an unspecified error (error code: ${errorCode}.</p><p>Try to run the experiment again. If the error persists, contact the experiment designer.</p></div>`,
+ titleColour: "red",
};
}
}
-
}
-
/**
* Maximal dimensions of the dialog window.
*
@@ -891,7 +742,6 @@
* @license Distributed under the terms of the MIT License
*/
-import {Clock, MonotonicClock} from "../util/Clock";
-import {PsychObject} from "../util/PsychObject";
-import {PsychoJS} from "./PsychoJS";
-import {EventManager} from "./EventManager";
-
+import { Clock, MonotonicClock } from "../util/Clock.js";
+import { PsychObject } from "../util/PsychObject.js";
+import { EventManager } from "./EventManager.js";
+import { PsychoJS } from "./PsychoJS.js";
/**
* @name module:core.KeyPress
@@ -55,7 +54,7 @@
Source: core/Keyboard.js
{
this.code = code;
this.tDown = tDown;
- this.name = (typeof name !== 'undefined') ? name : EventManager.w3c2pyglet(code);
+ this.name = (typeof name !== "undefined") ? name : EventManager.w3c2pyglet(code);
// duration of the keypress (time between keydown and keyup events) or undefined if there was no keyup
this.duration = undefined;
@@ -65,7 +64,6 @@
Source: core/Keyboard.js
}
}
-
/**
* <p>This manager handles all keyboard events. It is a substitute for the keyboard component of EventManager. </p>
*
@@ -81,39 +79,35 @@
return [];
}
-
// iterate over the buffer, from start to end, and discard the null event:
let filteredEvents = [];
const bufferWrap = (this._bufferLength === this._bufferSize);
@@ -185,7 +176,6 @@
Source: core/Keyboard.js
return filteredEvents;
}
-
/**
* Get the list of keys pressed or pushed by the participant.
*
@@ -202,12 +192,11 @@
Source: core/Keyboard.js
* (keydown with no subsequent keyup at the time getKeys is called).
*/
getKeys({
- keyList = [],
- waitRelease = true,
- clear = true
- } = {})
+ keyList = [],
+ waitRelease = true,
+ clear = true,
+ } = {})
{
-
// if nothing in the buffer, return immediately:
if (this._bufferLength === 0)
{
@@ -231,7 +220,7 @@
Source: core/Keyboard.js
{
// look for a corresponding, preceding keydown event:
const precedingKeydownIndex = keyEvent.keydownIndex;
- if (typeof precedingKeydownIndex !== 'undefined')
+ if (typeof precedingKeydownIndex !== "undefined")
{
const precedingKeydownEvent = this._circularBuffer[precedingKeydownIndex];
if (precedingKeydownEvent)
@@ -278,13 +267,10 @@
Source: core/Keyboard.js
{
this._circularBuffer[i] = null;
}
-
}
}
-
} while (i !== this._bufferIndex);
-
// if waitRelease = false, we iterate again over the map of unmatched keydown events:
if (!waitRelease)
{
@@ -331,18 +317,15 @@
Source: core/Keyboard.js
} while (i !== this._bufferIndex);*/
}
-
// if clear = true and the keyList is empty, we clear all the events:
if (clear && keyList.length === 0)
{
this.clearEvents();
}
-
return keyPresses;
}
-
/**
* Clear all events and resets the circular buffers.
*
@@ -361,7 +344,6 @@
Source: core/Keyboard.js
this._unmatchedKeydownMap = new Map();
}
-
/**
* Test whether a list of KeyPress's contains one with a particular name.
*
@@ -380,10 +362,9 @@
Source: core/Keyboard.js
}
const value = keypressList.find((keypress) => keypress.name === keyName);
- return (typeof value !== 'undefined');
+ return (typeof value !== "undefined");
}
-
/**
* Add key listeners to the document.
*
@@ -396,10 +377,9 @@
Source: core/Keyboard.js
this._previousKeydownKey = undefined;
const self = this;
-
// add a keydown listener:
window.addEventListener("keydown", (event) =>
- // document.addEventListener("keydown", (event) =>
+ // document.addEventListener("keydown", (event) =>
{
// only consider non-repeat events, i.e. only the first keydown event associated with a participant
// holding a key down:
@@ -426,14 +406,13 @@
Source: core/Keyboard.js
let code = event.code;
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
- if (typeof code === 'undefined')
+ if (typeof code === "undefined")
{
code = EventManager.keycode2w3c(event.keyCode);
}
let pigletKey = EventManager.w3c2pyglet(code);
-
self._bufferIndex = (self._bufferIndex + 1) % self._bufferSize;
self._bufferLength = Math.min(self._bufferLength + 1, self._bufferSize);
self._circularBuffer[self._bufferIndex] = {
@@ -441,20 +420,19 @@
let code = event.code;
// take care of legacy Microsoft Edge:
- if (typeof code === 'undefined')
+ if (typeof code === "undefined")
{
code = EventManager.keycode2w3c(event.keyCode);
}
@@ -482,28 +460,26 @@
Source: core/Keyboard.js
key: event.key,
pigletKey,
status: Keyboard.KeyStatus.KEY_UP,
- timestamp
+ timestamp,
};
// get the corresponding keydown event
// note: if more keys are down than there are slots in the circular buffer, there might
// not be a corresponding keydown event
const correspondingKeydownIndex = self._unmatchedKeydownMap.get(event.code);
- if (typeof correspondingKeydownIndex !== 'undefined')
+ if (typeof correspondingKeydownIndex !== "undefined")
{
self._circularBuffer[self._bufferIndex].keydownIndex = correspondingKeydownIndex;
self._unmatchedKeydownMap.delete(event.code);
}
- self._psychoJS.logger.trace('keyup: ', event.key);
+ self._psychoJS.logger.trace("keyup: ", event.key);
event.stopPropagation();
});
-
}
}
-
/**
* Keyboard KeyStatus.
*
@@ -513,8 +489,8 @@
* @license Distributed under the terms of the MIT License
*/
-
-import log4javascript from 'log4javascript';
-import pako from 'pako';
-import * as util from '../util/Util';
-import {MonotonicClock} from '../util/Clock';
-import {ExperimentHandler} from '../data/ExperimentHandler';
+import log4javascript from "log4javascript";
+import pako from "pako";
+import { ExperimentHandler } from "../data/ExperimentHandler.js";
+import { MonotonicClock } from "../util/Clock.js";
+import * as util from "../util/Util.js";
/**
* <p>This class handles a variety of loggers, e.g. a browser console one (mostly for debugging),
@@ -54,13 +53,12 @@
// throttling message index:
index: 0,
// whether or not the designer has already been warned:
- designerWasWarned: false
+ designerWasWarned: false,
};
}
-
-
/**
* Change the logging level.
*
@@ -112,8 +107,6 @@
Source: core/Logger.js
this._serverLevelValue = this._getValue(this._serverLevel);
}
-
-
/**
* Log a server message at the EXP level.
*
@@ -128,8 +121,6 @@
Source: core/Logger.js
this.log(msg, Logger.ServerLevel.EXP, time, obj);
}
-
-
/**
* Log a server message at the DATA level.
*
@@ -144,8 +135,6 @@
Source: core/Logger.js
this.log(msg, Logger.ServerLevel.DATA, time, obj);
}
-
-
/**
* Log a server message.
*
@@ -165,7 +154,7 @@
Source: core/Logger.js
return;
}
- if (typeof time === 'undefined')
+ if (typeof time === "undefined")
{
time = MonotonicClock.getReferenceTime();
}
@@ -182,12 +171,10 @@
Source: core/Logger.js
msg,
level,
time,
- obj: util.toString(obj)
+ obj: util.toString(obj),
});
}
-
-
/**
* Check whether or not a log messages must be throttled.
*
@@ -209,24 +196,26 @@
Source: core/Logger.js
// warn the designer if we are not already throttling:
if (!this._throttling.isThrottling)
{
- const msg = `<p>[time= ${time.toFixed(3)}] More than ${this._throttling.threshold} messages were logged in the past ${this._throttling.window}s.</p>` +
- `<p>We are now throttling: only 1 in ${this._throttling.factor} messages will be logged.</p>` +
- `<p>You may want to change your experiment's logging level. Please see <a href="https://www.psychopy.org/api/logging.html">psychopy.org/api/logging.html</a> for details.</p>`;
+ const msg = `<p>[time= ${time.toFixed(3)}] More than ${this._throttling.threshold} messages were logged in the past ${this._throttling.window}s.</p>`
+ + `<p>We are now throttling: only 1 in ${this._throttling.factor} messages will be logged.</p>`
+ + `<p>You may want to change your experiment's logging level. Please see <a href="https://www.psychopy.org/api/logging.html">psychopy.org/api/logging.html</a> for details.</p>`;
// console warning:
this._psychoJS.logger.warn(msg);
// in PILOTING mode and locally, we also warn the experimenter with a dialog box,
// but only once:
- if (!this._throttling.designerWasWarned &&
- (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL ||
- this._psychoJS.config.experiment.status === 'PILOTING'))
+ if (
+ !this._throttling.designerWasWarned
+ && (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL
+ || this._psychoJS.config.experiment.status === "PILOTING")
+ )
{
this._throttling.designerWasWarned = true;
this._psychoJS.gui.dialog({
warning: msg,
- showOK: true
+ showOK: true,
});
}
@@ -235,7 +224,7 @@
* @license Distributed under the terms of the MIT License
*/
-
-import {PsychObject} from '../util/PsychObject';
-import {PsychoJS} from './PsychoJS';
-import * as util from '../util/Util';
-
-
+import { PsychObject } from "../util/PsychObject.js";
+import * as util from "../util/Util.js";
+import { PsychoJS } from "./PsychoJS.js";
/**
* <p>MinimalStim is the base class for all stimuli.</p>
@@ -56,7 +53,7 @@
*/
setAutoDraw(autoDraw, log = false)
{
- this._setAttribute('autoDraw', autoDraw, log);
+ this._setAttribute("autoDraw", autoDraw, log);
// autoDraw = true: add the stimulus to the draw list if it's not there already
if (this._autoDraw)
{
this.draw();
}
-
// autoDraw = false: remove the stimulus from the draw list (and from the root container if it's already there)
else
{
@@ -111,8 +105,6 @@
Source: core/MinimalStim.js
}
}
-
-
/**
* Draw this stimulus on the next frame draw.
*
@@ -131,13 +123,13 @@
Source: core/MinimalStim.js
{
// update the stimulus if need be before we add its PIXI representation to the window container:
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.");
}
else
{
- this.win._rootContainer.addChild(this._pixi);
+ this._win.addPixiObject(this._pixi);
this.win._drawList.push(this);
}
}
@@ -145,11 +137,11 @@
Source: core/MinimalStim.js
{
// the stimulus is already in the list, if it needs to be updated, we remove it
// from the window container, update it, then put it back:
- if (this._needUpdate && typeof this._pixi !== 'undefined')
+ if (this._needUpdate && typeof this._pixi !== "undefined")
{
- this.win._rootContainer.removeChild(this._pixi);
+ this._win.removePixiObject(this._pixi);
this._updateIfNeeded();
- this.win._rootContainer.addChild(this._pixi);
+ this._win.addPixiObject(this._pixi);
}
}
}
@@ -157,8 +149,6 @@
Source: core/MinimalStim.js
this.status = PsychoJS.Status.STARTED;
}
-
-
/**
* Hide this stimulus on the next frame draw.
*
@@ -176,17 +166,15 @@
Source: core/MinimalStim.js
this._win._drawList.splice(index, 1);
// if the stimulus has a pixi representation, remove it from the root container:
- if (typeof this._pixi !== 'undefined')
+ if (typeof this._pixi !== "undefined")
{
- this._win._rootContainer.removeChild(this._pixi);
+ this._win.removePixiObject(this._pixi);
}
}
this.status = PsychoJS.Status.STOPPED;
}
}
-
-
/**
* Determine whether an object is inside this stimulus.
*
@@ -200,14 +188,12 @@
Source: core/MinimalStim.js
contains(object, units)
{
throw {
- origin: 'MinimalStim.contains',
+ 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.'
+ error: "this method is abstract and should not be called.",
};
}
-
-
/**
* Release the PIXI representation, if there is one.
*
@@ -219,18 +205,16 @@
* @license Distributed under the terms of the MIT License
*/
-import {PsychoJS} from './PsychoJS';
-import {PsychObject} from '../util/PsychObject';
-import * as util from '../util/Util';
-
+import { PsychObject } from "../util/PsychObject.js";
+import * as util from "../util/Util.js";
+import { PsychoJS } from "./PsychoJS.js";
/**
* <p>This manager handles the interactions between the experiment's stimuli and the mouse.</p>
@@ -57,12 +56,11 @@
const units = win.units;
const visible = 1;
- this._addAttribute('win', win);
- this._addAttribute('units', units);
- this._addAttribute('visible', visible);
- this._addAttribute('autoLog', autoLog);
+ this._addAttribute("win", win);
+ this._addAttribute("units", units);
+ this._addAttribute("visible", visible);
+ this._addAttribute("autoLog", autoLog);
this.status = PsychoJS.Status.NOT_STARTED;
}
-
/**
* Get the current position of the mouse in mouse/Window units.
*
@@ -101,12 +98,11 @@
Source: core/Mouse.js
pos_px[1] = this.win.size[1] / 2 - pos_px[1];
// convert to window units:
- this._lastPos = util.to_win(pos_px, 'pix', this._win);
+ this._lastPos = util.to_win(pos_px, "pix", this._win);
return this._lastPos;
}
-
/**
* Get the position of the mouse relative to that at the last call to getRel
* or getPos, in mouse/Window units.
@@ -118,7 +114,7 @@
}
}
-
/**
* Get the travel of the mouse scroll wheel since the last call to getWheelRel.
*
@@ -149,13 +144,12 @@
Source: core/Mouse.js
const wheelRel_px = mouseInfo.wheelRel.slice();
// convert to window units:
- const wheelRel = util.to_win(wheelRel_px, 'pix', this._win);
+ const wheelRel = util.to_win(wheelRel_px, "pix", this._win);
mouseInfo.wheelRel = [0, 0];
return wheelRel;
}
-
/**
* Get the status of each button (pressed or released) and, optionally, the time elapsed between the last call to [clickReset]{@link module:core.Mouse#clickReset} and the pressing or releasing of the buttons.
*
@@ -181,7 +175,6 @@
Source: core/Mouse.js
}
}
-
/**
* Helper method for checking whether a stimulus has had any button presses within bounds.
*
@@ -198,14 +191,14 @@
Source: core/Mouse.js
isPressedIn(...args)
{
// Look for options given in object literal form, cut out falsy inputs
- const [{ shape: shapeMaybe, buttons: buttonsMaybe } = {}] = args.filter(v => !!v);
+ const [{ shape: shapeMaybe, buttons: buttonsMaybe } = {}] = args.filter((v) => !!v);
// Helper to check if some object features a certain key
- const hasKey = key => object => !!(object && object[key]);
+ const hasKey = (key) => (object) => !!(object && object[key]);
// Shapes are expected to be instances of stimuli, or at
// the very least objects featuring a `contains()` method
- const isShape = hasKey('contains');
+ const isShape = hasKey("contains");
// Go through arguments array looking for a shape if options object offers none
const shapeFound = isShape(shapeMaybe) ? shapeMaybe : args.find(isShape);
@@ -215,23 +208,23 @@
Source: core/Mouse.js
// Buttons values may be extracted from an object
// featuring the `buttons` key, or found as integers
// in the arguments array
- const hasButtons = hasKey('buttons');
+ const hasButtons = hasKey("buttons");
const { isInteger } = Number;
// Prioritize buttons value given as part of an options object,
// then look for the first occurrence in the arguments array of either
// an integer or an extra object with a `buttons` key
- const buttonsFound = isInteger(buttonsMaybe) ? buttonsMaybe : args.find(o => hasButtons(o) || isInteger(o));
+ const buttonsFound = isInteger(buttonsMaybe) ? buttonsMaybe : args.find((o) => hasButtons(o) || isInteger(o));
// Worst case scenario `wanted` ends up being an empty object
const { buttons: wanted = buttonsFound || buttonsMaybe } = buttonsFound || {};
// Will throw if stimulus is falsy or non-object like
- if (typeof shape.contains === 'function')
+ if (typeof shape.contains === "function")
{
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
const { pressed } = mouseInfo.buttons;
// If no specific button wanted, any pressed will do
- const hasButtonPressed = isInteger(wanted) ? pressed[wanted] > 0 : pressed.some(v => v > 0);
+ const hasButtonPressed = isInteger(wanted) ? pressed[wanted] > 0 : pressed.some((v) => v > 0);
return hasButtonPressed && shape.contains(this);
}
@@ -239,7 +232,6 @@
Source: core/Mouse.js
return false;
}
-
/**
* Determine whether the mouse has moved beyond a certain distance.
*
@@ -268,24 +260,26 @@
return false;
}
}
-
- else if (typeof reset === 'boolean' && reset == true)
+ else if (typeof reset === "boolean" && reset == true)
{
// reset the moveClock:
this.psychoJS.eventManager.getMouseInfo().moveClock.reset();
return false;
}
-
- else if (reset === 'here')
+ else if (reset === "here")
{
// set to wherever we are
this._prevPos = this._lastPos.clone();
return false;
}
-
else if (reset instanceof Array)
{
// an (x,y) array
@@ -322,36 +313,37 @@
Source: core/Mouse.js
if (!distance)
{
return false;
- }// just resetting prevPos, not checking distance
+ }
+ // just resetting prevPos, not checking distance
else
{
// checking distance of current pos to newly reset prevposition
- if (typeof distance === 'number')
+ if (typeof distance === "number")
{
- this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
+ this._movedistance = Math.sqrt(
+ (this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]),
+ );
return (this._movedistance > distance);
}
if (Math.abs(this._lastPos[0] - this._prevPos[0]) > distance[0])
{
return true;
- } // moved on X-axis
+ } // moved on X-axis
if (Math.abs(this._lastPos[1] - this._prevPos[1]) > distance[1])
{
return true;
- } // moved on Y-axis
+ } // moved on Y-axis
return false;
}
}
-
else
{
return false;
}
}
-
/**
* Get the amount of time elapsed since the last mouse movement.
*
@@ -365,7 +357,6 @@
Source: core/Mouse.js
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
}
-
/**
* Reset the clocks associated to the given mouse buttons.
*
@@ -383,10 +374,7 @@
* @license Distributed under the terms of the MIT License
*/
-import log4javascript from 'log4javascript';
-import {Scheduler} from '../util/Scheduler';
-import {ServerManager} from './ServerManager';
-import {ExperimentHandler} from '../data/ExperimentHandler';
-import {EventManager} from './EventManager';
-import {Window} from './Window';
-import {GUI} from './GUI';
-import {MonotonicClock} from '../util/Clock';
-import {Logger} from './Logger';
-import * as util from '../util/Util';
-// import {Shelf} from "../data/Shelf";
-
+import log4javascript from "log4javascript";
+import { ExperimentHandler } from "../data/ExperimentHandler.js";
+import { MonotonicClock } from "../util/Clock.js";
+import { Scheduler } from "../util/Scheduler.js";
+import * as util from "../util/Util.js";
+import { EventManager } from "./EventManager.js";
+import { GUI } from "./GUI.js";
+import { Logger } from "./Logger.js";
+import { ServerManager } from "./ServerManager.js";
+import { Window } from "./Window.js";
+import {Shelf} from "../data/Shelf";
/**
* <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>
@@ -59,7 +58,6 @@
// managers:
this._eventManager = new EventManager(this);
this._serverManager = new ServerManager({
- psychoJS: this
+ psychoJS: this,
});
- // to be loading `configURL` files in `_configure` calls from
- const hostsEvidently = new Set([...hosts, 'https://pavlovia.org/run/', 'https://run.pavlovia.org/']);
- this._hosts = Array.from(hostsEvidently);
+ // add the pavlovia server to the list of hosts:
+ const hostsWithPavlovia = new Set([...hosts, "https://pavlovia.org/run/", "https://run.pavlovia.org/"]);
+ this._hosts = Array.from(hostsWithPavlovia);
// GUI:
this._gui = new GUI(this);
@@ -189,8 +186,8 @@
*/
getEnvironment()
{
- if (typeof this._config === 'undefined')
+ if (typeof this._config === "undefined")
{
return undefined;
}
return this._config.environment;
}
-
/**
* Open a PsychoJS Window.
*
@@ -248,22 +243,23 @@
Source: core/PsychoJS.js
* @public
*/
openWindow({
- name,
- fullscr,
- color,
- units,
- waitBlanking,
- autoLog
- } = {})
+ name,
+ fullscr,
+ color,
+ gamma,
+ units,
+ waitBlanking,
+ 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.'
+ origin: "PsychoJS.openWindow",
+ context: "when opening a Window",
+ error: "A Window has already been opened.",
};
}
@@ -272,13 +268,13 @@
Source: core/PsychoJS.js
name,
fullscr,
color,
+ gamma,
units,
waitBlanking,
- autoLog
+ autoLog,
});
}
-
/**
* Set the completion and cancellation URL to which the participant will be redirect at the end of the experiment.
*
@@ -291,7 +287,6 @@
*/
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);
}
-
/**
* @callback PsychoJS.condition
* @return {boolean} true if the thenScheduler is to be run, false if the elseScheduler is to be run
@@ -321,12 +315,11 @@
event.preventDefault();
// Chrome requires returnValue to be set:
- event.returnValue = '';
+ event.returnValue = "";
};
- window.addEventListener('beforeunload', this.beforeunloadCallback);
-
+ 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) =>
+ window.addEventListener("unload", (event) =>
{
- if (self._config.session.status === 'OPEN')
+ if (self._config.session.status === "OPEN")
{
// save the incomplete results if need be:
if (self._config.experiment.saveIncompleteResults)
{
- self._experiment.save({sync: true});
+ self._experiment.save({ sync: true });
}
// close the session:
self._serverManager.closeSession(false, true);
}
- if (typeof self._window !== 'undefined')
+ if (typeof self._window !== "undefined")
{
self._window.close();
}
});
-
}
-
// start the asynchronous download of resources:
- await this._serverManager.prepareResources(resources);
+ this._serverManager.prepareResources(resources);
// start the experiment:
- this.logger.info('[PsychoJS] Start Experiment.');
+ this.logger.info("[PsychoJS] Start Experiment.");
await this._scheduler.start();
}
catch (error)
{
// this._gui.dialog({ error: { ...response, error } });
- this._gui.dialog({error: Object.assign(response, {error})});
+ this._gui.dialog({ error: Object.assign(response, { error }) });
}
}
-
-
/**
* Block the experiment until the specified resources have been downloaded.
*
@@ -467,8 +456,8 @@
Source: core/PsychoJS.js
waitForResources(resources = [])
{
const response = {
- origin: 'PsychoJS.waitForResources',
- context: 'while waiting for resources to be downloaded'
+ origin: "PsychoJS.waitForResources",
+ context: "while waiting for resources to be downloaded",
};
try
@@ -478,12 +467,10 @@
Source: core/PsychoJS.js
catch (error)
{
// this._gui.dialog({ error: { ...response, error } });
- this._gui.dialog({error: Object.assign(response, {error})});
+ this._gui.dialog({ error: Object.assign(response, { error }) });
}
}
-
-
/**
* Make the attributes of the given object those of PsychoJS and those of
* the top level variable (e.g. window) as well.
@@ -493,9 +480,9 @@
}
}
-
/**
* Close everything and exit nicely at the end of the experiment,
* potentially redirecting to one of the URLs previously specified by setRedirectUrls.
@@ -521,9 +507,9 @@
// remove the beforeunload listener:
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
{
- window.removeEventListener('beforeunload', this.beforeunloadCallback);
+ window.removeEventListener("beforeunload", this.beforeunloadCallback);
}
// save the results and the logs of the experiment:
this.gui.dialog({
- warning: 'Closing the session. Please wait a few moments.',
- showOK: false
+ warning: "Closing the session. Please wait a few moments.",
+ showOK: false,
});
if (isCompleted || this._config.experiment.saveIncompleteResults)
{
- if (!this._serverMsg.has('__noOutput'))
+ if (!this._serverMsg.has("__noOutput"))
{
await this._experiment.save();
await this._logger.flush();
@@ -560,8 +546,8 @@
Source: core/PsychoJS.js
}
// thank participant for waiting and either quit or redirect:
- let text = 'Thank you for your patience.<br/><br/>';
- text += (typeof message !== 'undefined') ? message : 'Goodbye!';
+ let text = "Thank you for your patience.<br/><br/>";
+ text += (typeof message !== "undefined") ? message : "Goodbye!";
const self = this;
this._gui.dialog({
message: text,
@@ -580,26 +566,24 @@
Source: core/PsychoJS.js
this._window.closeFullScreen();
// redirect if redirection URLs have been provided:
- if (isCompleted && typeof self._completionUrl !== 'undefined')
+ if (isCompleted && typeof self._completionUrl !== "undefined")
{
window.location = self._completionUrl;
}
- else if (!isCompleted && typeof self._cancellationUrl !== 'undefined')
+ else if (!isCompleted && typeof self._cancellationUrl !== "undefined")
{
window.location = self._cancellationUrl;
}
- }
+ },
});
-
}
catch (error)
{
console.error(error);
- this._gui.dialog({error});
+ this._gui.dialog({ error });
}
}
-
/**
* Configure PsychoJS for the running experiment.
*
@@ -611,68 +595,67 @@
Source: core/PsychoJS.js
async _configure(configURL, name)
{
const response = {
- origin: 'PsychoJS.configure',
- context: 'when configuring PsychoJS for the experiment'
+ origin: "PsychoJS.configure",
+ context: "when configuring PsychoJS for the experiment",
};
try
{
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 an approved hosts, e.e pavlovia.org,
+ // we read the configuration file:
const experimentUrl = window.location.href;
- // go through each url in allow list
const isHost = this._hosts.some(url => experimentUrl.indexOf(url) === 0);
if (isHost)
{
const serverResponse = await this._serverManager.getConfiguration(configURL);
this._config = serverResponse.config;
- // legacy experiments had a psychoJsManager block instead of a pavlovia block,
- // and the URL pointed to https://pavlovia.org/server
- if ('psychoJsManager' in this._config)
+ // update the configuration for legacy experiments, which had a psychoJsManager
+ // block instead of a pavlovia block, with URL pointing to https://pavlovia.org/server
+ if ("psychoJsManager" in this._config)
{
delete this._config.psychoJsManager;
this._config.pavlovia = {
- URL: 'https://pavlovia.org'
+ URL: "https://pavlovia.org",
};
}
// 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";
}
- if (!('gitlab' in this._config))
+ if (!("gitlab" in this._config))
{
- throw 'missing gitlab block in configuration';
+ throw "missing gitlab block in configuration";
}
- if (!('projectId' in this._config.gitlab))
+ if (!("projectId" in this._config.gitlab))
{
- throw 'missing projectId in gitlab block in configuration';
+ throw "missing projectId in gitlab block in configuration";
}
this._config.environment = ExperimentHandler.Environment.SERVER;
-
}
- else
// otherwise we create an ad-hoc configuration:
+ else
{
this._config = {
environment: ExperimentHandler.Environment.LOCAL,
@@ -680,8 +663,8 @@
this._serverMsg = new Map();
util.getUrlParameters().forEach((value, key) =>
{
- if (key.indexOf('__') === 0)
+ if (key.indexOf("__") === 0)
{
this._serverMsg.set(key, value);
}
});
-
this.status = PsychoJS.Status.CONFIGURED;
- this.logger.debug('configuration:', util.toString(this._config));
+ this.logger.debug("configuration:", util.toString(this._config));
}
catch (error)
{
// throw { ...response, error };
- throw Object.assign(response, {error});
+ throw Object.assign(response, { error });
}
}
-
/**
* Get the IP information of the participant, asynchronously.
*
@@ -716,33 +697,32 @@
Source: core/PsychoJS.js
async _getParticipantIPInfo()
{
const response = {
- origin: 'PsychoJS._getParticipantIPInfo',
- context: 'when getting the IP information of the participant'
+ 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 = {};
try
{
- const geoResponse = await jQuery.get('http://www.geoplugin.net/json.gp');
+ const geoResponse = await jQuery.get("http://www.geoplugin.net/json.gp");
const geoData = JSON.parse(geoResponse);
this._IP = {
IP: geoData.geoplugin_request,
country: geoData.geoplugin_countryName,
latitude: geoData.geoplugin_latitude,
- longitude: geoData.geoplugin_longitude
+ longitude: geoData.geoplugin_longitude,
};
- 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)
{
// throw { ...response, error };
- throw Object.assign(response, {error});
+ throw Object.assign(response, { error });
}
}
-
/**
* Capture all errors and display them in a pop-up error box.
*
@@ -750,41 +730,46 @@
Source: core/PsychoJS.js
*/
_captureErrors()
{
- this.logger.debug('capturing all errors and showing them in a pop up window');
+ this.logger.debug("capturing all errors and showing them in a pop up window");
const self = this;
- window.onerror = function (message, source, lineno, colno, error)
+ window.onerror = function(message, source, lineno, colno, error)
{
console.error(error);
- document.body.setAttribute('data-error', JSON.stringify({
- message: message,
- source: source,
- lineno: lineno,
- colno: colno,
- error: error
- }));
-
- self._gui.dialog({"error": error});
-
+ document.body.setAttribute(
+ "data-error",
+ JSON.stringify({
+ message: message,
+ source: source,
+ lineno: lineno,
+ colno: colno,
+ error: error,
+ }),
+ );
+
+ self._gui.dialog({ "error": error });
+
return true;
};
- window.onunhandledrejection = function (error)
+ window.onunhandledrejection = function(error)
{
console.error(error?.reason);
- if (error?.reason?.stack === undefined) {
+ if (error?.reason?.stack === undefined)
+ {
// No stack? Error thrown by PsychoJS; stringify whole error
- document.body.setAttribute('data-error', JSON.stringify(error?.reason));
- } else {
+ document.body.setAttribute("data-error", JSON.stringify(error?.reason));
+ }
+ else
+ {
// Yes stack? Error thrown by JS; stringify stack
- document.body.setAttribute('data-error', JSON.stringify(error?.reason?.stack));
+ document.body.setAttribute("data-error", JSON.stringify(error?.reason?.stack));
}
- self._gui.dialog({error: error?.reason});
+ self._gui.dialog({ error: error?.reason });
return true;
};
}
-
/**
* Make the various Status top level, in order to accommodate PsychoPy's Code Components.
* @private
@@ -796,10 +781,8 @@
* @license Distributed under the terms of the MIT License
*/
-import { Howl } from 'howler';
-import {PsychoJS} from './PsychoJS';
-import {PsychObject} from '../util/PsychObject';
-import * as util from '../util/Util';
-import {ExperimentHandler} from "../data/ExperimentHandler";
-import {MonotonicClock} from "../util/Clock";
-
+import { Howl } from "howler";
+import { ExperimentHandler } from "../data/ExperimentHandler.js";
+import { Clock, MonotonicClock } from "../util/Clock.js";
+import { PsychObject } from "../util/PsychObject.js";
+import * as util from "../util/Util.js";
+import { Scheduler } from "../util/Scheduler.js";
+import { PsychoJS } from "./PsychoJS.js";
/**
* <p>This manager handles all communications between the experiment running in the participant's browser and the [pavlovia.org]{@link http://pavlovia.org} server, <em>in an asynchronous manner</em>.</p>
@@ -56,7 +56,7 @@
Source: core/ServerManager.js
*/
export class ServerManager extends PsychObject
{
- /**
+ /****************************************************************************
* Used to indicate to the ServerManager that all resources must be registered (and
* subsequently downloaded)
*
@@ -64,13 +64,12 @@
// resources is a map of <name: string, { path: string, status: ResourceStatus, data: any }>
this._resources = new Map();
+ this._nbLoadedResources = 0;
+ this._setupPreloadQueue();
- this._addAttribute('autoLog', autoLog);
- this._addAttribute('status', ServerManager.Status.READY);
- }
+ this._addAttribute("autoLog", autoLog);
+ this._addAttribute("status", ServerManager.Status.READY);
+ }
- /**
+ /****************************************************************************
* @typedef ServerManager.GetConfigurationPromise
* @property {string} origin the calling method
* @property {string} context the context
* @property {Object.<string, *>} [config] the configuration
* @property {Object.<string, *>} [error] an error message if we could not read the configuration file
*/
- /**
+ /****************************************************************************
* Read the configuration file for the experiment.
*
* @name module:core.ServerManager#getConfiguration
@@ -105,41 +106,40 @@
Source: core/ServerManager.js
getConfiguration(configURL)
{
const response = {
- origin: 'ServerManager.getConfiguration',
- context: 'when reading the configuration file: ' + configURL
+ 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);
const self = this;
return new Promise((resolve, reject) =>
{
- jQuery.get(configURL, 'json')
+ jQuery.get(configURL, "json")
.done((config, textStatus) =>
{
// resolve({ ...response, config });
- resolve(Object.assign(response, {config}));
+ resolve(Object.assign(response, { config }));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
self.setStatus(ServerManager.Status.ERROR);
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 }));
});
});
}
-
- /**
+ /****************************************************************************
* @typedef ServerManager.OpenSessionPromise
* @property {string} origin the calling method
* @property {string} context the context
* @property {string} [token] the session token
* @property {Object.<string, *>} [error] an error message if we could not open the session
*/
- /**
+ /****************************************************************************
* Open a session for this experiment on the remote PsychoJS manager.
*
* @name module:core.ServerManager#openSession
@@ -150,45 +150,47 @@
Source: core/ServerManager.js
openSession()
{
const response = {
- origin: 'ServerManager.openSession',
- context: 'when opening a session for experiment: ' + this._psychoJS.config.experiment.fullpath
+ origin: "ServerManager.openSession",
+ context: "when opening a session for experiment: " + this._psychoJS.config.experiment.fullpath,
};
- this._psychoJS.logger.debug('opening a session for experiment: ' + this._psychoJS.config.experiment.fullpath);
+ this._psychoJS.logger.debug("opening a session for experiment: " + this._psychoJS.config.experiment.fullpath);
this.setStatus(ServerManager.Status.BUSY);
// prepare POST query:
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:
const self = this;
return new Promise((resolve, reject) =>
{
- const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) + '/sessions';
- jQuery.post(url, data, null, 'json')
+ const url = this._psychoJS.config.pavlovia.URL
+ + "/api/v2/experiments/" + this._psychoJS.config.gitlab.projectId
+ + "/sessions";
+ jQuery.post(url, data, null, "json")
.done((data, textStatus) =>
{
- if (!('token' in data))
+ if (!("token" in data))
{
self.setStatus(ServerManager.Status.ERROR);
- reject(Object.assign(response, {error: 'unexpected answer from server: no token'}));
+ reject(Object.assign(response, { error: "unexpected answer from server: no token" }));
// reject({...response, error: 'unexpected answer from server: no token'});
}
- if (!('experiment' in data))
+ if (!("experiment" in data))
{
self.setStatus(ServerManager.Status.ERROR);
// reject({...response, error: 'unexpected answer from server: no experiment'});
- reject(Object.assign(response, {error: 'unexpected answer from server: no experiment'}));
+ reject(Object.assign(response, { error: "unexpected answer from server: no experiment" }));
}
self._psychoJS.config.session = {
token: data.token,
- status: 'OPEN'
+ status: "OPEN",
};
self._psychoJS.config.experiment.status = data.experiment.status2;
self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
@@ -197,7 +199,7 @@
Source: core/ServerManager.js
self._psychoJS.config.experiment.runMode = data.experiment.runMode;
// secret keys for various services, e.g. Google Speech API
- if ('keys' in data.experiment)
+ if ("keys" in data.experiment)
{
self._psychoJS.config.experiment.keys = data.experiment.keys;
}
@@ -208,28 +210,27 @@
Source: core/ServerManager.js
self.setStatus(ServerManager.Status.READY);
// resolve({ ...response, token: data.token, status: data.status });
- resolve(Object.assign(response, {token: data.token, status: data.status}));
+ resolve(Object.assign(response, { token: data.token, status: data.status }));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
self.setStatus(ServerManager.Status.ERROR);
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 }));
});
});
}
-
- /**
+ /****************************************************************************
* @typedef ServerManager.CloseSessionPromise
* @property {string} origin the calling method
* @property {string} context the context
* @property {Object.<string, *>} [error] an error message if we could not close the session (e.g. if it has not previously been opened)
*/
- /**
+ /****************************************************************************
* Close the session for this experiment on the remote PsychoJS manager.
*
* @name module:core.ServerManager#closeSession
@@ -242,16 +243,18 @@
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.send(JSON.stringify(data));
*/
- /* This does not work in Chrome before of a CORS bug
+ /* This does not work in Chrome because of a CORS bug
await fetch(url, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
@@ -272,9 +275,9 @@
getResource(name, errorIfNotDownloaded = false)
{
const response = {
- origin: 'ServerManager.getResource',
- context: 'when getting the value of resource: ' + name
+ origin: "ServerManager.getResource",
+ context: "when getting the value of resource: " + name,
};
const pathStatusData = this._resources.get(name);
- if (typeof pathStatusData === 'undefined')
+ if (typeof pathStatusData === "undefined")
{
// throw { ...response, error: 'unknown resource' };
- throw Object.assign(response, {error: 'unknown resource'});
+ throw Object.assign(response, { error: "unknown resource" });
}
if (errorIfNotDownloaded && pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
{
throw Object.assign(response, {
- error: name + ' is not available for use (yet), its current status is: ' +
- util.toString(pathStatusData.status)
+ error: name + " is not available for use (yet), its current status is: "
+ + util.toString(pathStatusData.status),
});
}
return pathStatusData.data;
}
-
- /**
- * Get the status of a resource.
+ /****************************************************************************
+ * Get the status of a single resource or the reduced status of an array of resources.
+ *
+ * <p>If an array of resources is given, getResourceStatus returns a single, reduced status
+ * that is the status furthest away from DOWNLOADED, with the status ordered as follow:
+ * ERROR (furthest from DOWNLOADED), REGISTERED, DOWNLOADING, and DOWNLOADED</p>
+ * <p>For example, given three resources:
+ * <ul>
+ * <li>if at least one of the resource status is ERROR, the reduced status is ERROR</li>
+ * <li>if at least one of the resource status is DOWNLOADING, the reduced status is DOWNLOADING</li>
+ * <li>if the status of all three resources is REGISTERED, the reduced status is REGISTERED</li>
+ * <li>if the status of all three resources is DOWNLOADED, the reduced status is DOWNLOADED</li>
+ * </ul>
+ * </p>
*
* @name module:core.ServerManager#getResourceStatus
* @function
* @public
- * @param {string} name of the requested resource
- * @return {core.ServerManager.ResourceStatus} status of the resource
- * @throws {Object.<string, *>} exception if no resource with that name has previously been registered
+ * @param {string | string[]} names names of the resources whose statuses are requested
+ * @return {core.ServerManager.ResourceStatus} status of the resource if there is only one, or reduced status otherwise
+ * @throws {Object.<string, *>} if at least one of the names is not that of a previously
+ * registered resource
*/
- getResourceStatus(name)
+ getResourceStatus(names)
{
const response = {
- origin: 'ServerManager.getResourceStatus',
- context: 'when getting the status of resource: ' + name
+ origin: "ServerManager.getResourceStatus",
+ context: `when getting the status of resource(s): ${JSON.stringify(names)}`,
};
- const pathStatusData = this._resources.get(name);
- if (typeof pathStatusData === 'undefined')
+ // sanity checks:
+ if (typeof names === 'string')
{
- // throw { ...response, error: 'unknown resource' };
- throw Object.assign(response, {error: 'unknown resource'});
+ names = [names];
}
+ if (!Array.isArray(names))
+ {
+ throw Object.assign(response, { error: "names should be either a string or an array of strings" });
+ }
+ const statusOrder = new Map([
+ [Symbol.keyFor(ServerManager.ResourceStatus.ERROR), 0],
+ [Symbol.keyFor(ServerManager.ResourceStatus.REGISTERED), 1],
+ [Symbol.keyFor(ServerManager.ResourceStatus.DOWNLOADING), 2],
+ [Symbol.keyFor(ServerManager.ResourceStatus.DOWNLOADED), 3]
+ ]);
+ let reducedStatus = ServerManager.ResourceStatus.DOWNLOADED;
+ for (const name of names)
+ {
+ const pathStatusData = this._resources.get(name);
- return pathStatusData.status;
- }
+ if (typeof pathStatusData === "undefined")
+ {
+ // throw { ...response, error: 'unknown resource' };
+ throw Object.assign(response, {
+ error: `unable to find a previously registered resource with name: ${name}`
+ });
+ }
+ // update the reduced status according to the order given by statusOrder:
+ if (statusOrder.get(Symbol.keyFor(pathStatusData.status)) <
+ statusOrder.get(Symbol.keyFor(reducedStatus)))
+ {
+ reducedStatus = pathStatusData.status;
+ }
+ }
- /**
+ return reducedStatus;
+ }
+
+ /****************************************************************************
* Set the resource manager status.
*
* @name module:core.ServerManager#setStatus
@@ -388,21 +430,19 @@
Source: core/ServerManager.js
setStatus(status)
{
const response = {
- origin: 'ServerManager.setStatus',
- context: 'when changing the status of the server manager to: ' + util.toString(status)
+ origin: "ServerManager.setStatus",
+ context: "when changing the status of the server manager to: " + util.toString(status),
};
// check status:
- const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
+ const statusKey = (typeof status === "symbol") ? Symbol.keyFor(status) : null;
if (!statusKey)
- // throw { ...response, error: 'status must be a symbol' };
- {
- throw Object.assign(response, {error: 'status must be a symbol'});
+ { // throw { ...response, error: 'status must be a symbol' };
+ throw Object.assign(response, { error: "status must be a symbol" });
}
if (!ServerManager.Status.hasOwnProperty(statusKey))
- // throw { ...response, error: 'unknown status' };
- {
- throw Object.assign(response, {error: 'unknown status'});
+ { // throw { ...response, error: 'unknown status' };
+ throw Object.assign(response, { error: "unknown status" });
}
this._status = status;
@@ -413,8 +453,7 @@
Source: core/ServerManager.js
return this._status;
}
-
- /**
+ /****************************************************************************
* Reset the resource manager status to ServerManager.Status.READY.
*
* @name module:core.ServerManager#resetStatus
@@ -427,8 +466,7 @@
Source: core/ServerManager.js
return this.setStatus(ServerManager.Status.READY);
}
-
- /**
+ /****************************************************************************
* Prepare resources for the experiment: register them with the server manager and possibly
* start downloading them right away.
*
@@ -442,18 +480,18 @@
Source: core/ServerManager.js
* </ul>
*
* @name module:core.ServerManager#prepareResources
- * @param {Array.<{name: string, path: string, download: boolean} | Symbol>} [resources=[]] - the list of resources
+ * @param {String | Array.<{name: string, path: string, download: boolean} | String | Symbol>} [resources=[]] - the list of resources or a single resource
* @function
* @public
*/
async prepareResources(resources = [])
{
const response = {
- origin: 'ServerManager.prepareResources',
- context: 'when preparing resources for experiment: ' + this._psychoJS.config.experiment.name
+ origin: "ServerManager.prepareResources",
+ context: "when preparing resources for experiment: " + this._psychoJS.config.experiment.name,
};
- this._psychoJS.logger.debug('preparing resources for experiment: ' + this._psychoJS.config.experiment.name);
+ this._psychoJS.logger.debug("preparing resources for experiment: " + this._psychoJS.config.experiment.name);
try
{
@@ -462,19 +500,24 @@
Source: core/ServerManager.js
// register the resources:
if (resources !== null)
{
+ if (typeof resources === "string")
+ {
+ resources = [resources];
+ }
if (!Array.isArray(resources))
{
- throw "resources should be an array of objects";
+ throw "resources should be either (a) a string or (b) an array of string or objects";
}
// whether all resources have been requested:
- const allResources = (resources.length === 1 && resources[0] === ServerManager.ALL_RESOURCES);
+ const allResources = (resources.length === 1 &&
+ resources[0] === ServerManager.ALL_RESOURCES);
// if the experiment is hosted on the pavlovia.org server and
// resources is [ServerManager.ALL_RESOURCES], then we register all the resources
// in the "resources" sub-directory
- if (this._psychoJS.config.environment === ExperimentHandler.Environment.SERVER
- && allResources)
+ if (this._psychoJS.config.environment === ExperimentHandler.Environment.SERVER &&
+ allResources)
{
// list the resources from the resources directory of the experiment on the server:
const serverResponse = await this._listResources();
@@ -485,53 +528,66 @@
Source: core/ServerManager.js
{
if (!this._resources.has(name))
{
- const path = serverResponse.resourceDirectory + '/' + name;
+ const path = serverResponse.resourceDirectory + "/" + name;
this._resources.set(name, {
status: ServerManager.ResourceStatus.REGISTERED,
path,
- data: undefined
+ data: undefined,
});
- this._psychoJS.logger.debug('registered resource:', name, path);
+ this._psychoJS.logger.debug(`registered resource: name= ${name}, path= ${path}`);
resourcesToDownload.add(name);
}
}
}
-
// if the experiment is hosted locally (localhost) or if specific resources were given
// then we register those specific resources, if they have not been registered already
else
{
// we cannot ask for all resources to be registered locally, since we cannot list
// them:
- if (this._psychoJS.config.environment === ExperimentHandler.Environment.LOCAL
- && allResources)
+ if (this._psychoJS.config.environment === ExperimentHandler.Environment.LOCAL &&
+ allResources)
{
throw "resources must be manually specified when the experiment is running locally: ALL_RESOURCES cannot be used";
}
- for (let {name, path, download} of resources)
+ // convert those resources that are only a string to an object with name and path:
+ for (let r = 0; r < resources.length; ++r)
+ {
+ const resource = resources[r];
+ if (typeof resource === "string")
+ {
+ resources[r] = {
+ name: resource,
+ path: resource,
+ download: true
+ }
+ }
+ }
+
+ for (let { name, path, download } of resources)
{
if (!this._resources.has(name))
{
// to deal with potential CORS issues, we use the pavlovia.org proxy for resources
// not hosted on pavlovia.org:
- if ((path.toLowerCase().indexOf('www.') === 0 ||
- path.toLowerCase().indexOf('http:') === 0 ||
- path.toLowerCase().indexOf('https:') === 0) &&
- (path.indexOf('pavlovia.org') === -1))
+ if ( (path.toLowerCase().indexOf("www.") === 0 ||
+ path.toLowerCase().indexOf("http:") === 0 ||
+ path.toLowerCase().indexOf("https:") === 0) &&
+ (path.indexOf("pavlovia.org") === -1) )
{
- path = 'https://pavlovia.org/api/v2/proxy/' + path;
+ path = "https://pavlovia.org/api/v2/proxy/" + path;
}
this._resources.set(name, {
status: ServerManager.ResourceStatus.REGISTERED,
path,
- data: undefined
+ data: undefined,
});
- this._psychoJS.logger.debug('registered resource:', name, path);
+ this._psychoJS.logger.debug(`registered resource: name= ${name}, path= ${path}`);
// download resources by default:
- if (typeof download === 'undefined' || download)
+ if (typeof download === "undefined" || download)
{
resourcesToDownload.add(name);
}
@@ -540,19 +596,42 @@
Source: core/ServerManager.js
}
}
- // download those registered resources for which download = true:
- /*await*/ this._downloadResources(resourcesToDownload);
+ // download those registered resources for which download = true
+ // note: we return a Promise that will be resolved when all the resources are downloaded
+ if (resourcesToDownload.size === 0)
+ {
+ this.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOAD_COMPLETED,
+ });
+
+ return Promise.resolve();
+ }
+ else
+ {
+ return new Promise((resolve, reject) =>
+ {
+ const uuid = this.on(ServerManager.Event.RESOURCE, (signal) =>
+ {
+ if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED)
+ {
+ this.off(ServerManager.Event.RESOURCE, uuid);
+ resolve();
+ }
+ });
+
+ this._downloadResources(resourcesToDownload);
+ });
+ }
}
catch (error)
{
- console.log('error', error);
- throw Object.assign(response, {error});
+ console.error("error", error);
+ throw Object.assign(response, { error });
// throw { ...response, error: error };
}
}
-
- /**
+ /****************************************************************************
* Block the experiment until the specified resources have been downloaded.
*
* @name module:core.ServerManager#waitForResources
@@ -566,11 +645,11 @@
Source: core/ServerManager.js
this._waitForDownloadComponent = {
status: PsychoJS.Status.NOT_STARTED,
clock: new Clock(),
- resources: new Set()
+ resources: new Set(),
};
const self = this;
- return () =>
+ return async () =>
{
const t = self._waitForDownloadComponent.clock.getTime();
@@ -583,82 +662,84 @@
Source: core/ServerManager.js
// if resources is an empty array, we consider all registered resources:
if (resources.length === 0)
{
- for (const [name, {status, path, data}] of this._resources)
+ for (const [name, { status, path, data }] of this._resources)
{
- resources.append({ name, path });
+ resources.push({ name, path });
}
}
- // only download those resources not already downloaded or downloading:
+ // only download those resources not already downloaded and not downloading:
const resourcesToDownload = new Set();
- for (let {name, path} of resources)
+ for (let { name, path } of resources)
{
// to deal with potential CORS issues, we use the pavlovia.org proxy for resources
// not hosted on pavlovia.org:
- if ( (path.toLowerCase().indexOf('www.') === 0 ||
- path.toLowerCase().indexOf('http:') === 0 ||
- path.toLowerCase().indexOf('https:') === 0) &&
- (path.indexOf('pavlovia.org') === -1) )
+ if (
+ (path.toLowerCase().indexOf("www.") === 0
+ || path.toLowerCase().indexOf("http:") === 0
+ || path.toLowerCase().indexOf("https:") === 0)
+ && (path.indexOf("pavlovia.org") === -1)
+ )
{
- path = 'https://devlovia.org/api/v2/proxy/' + path;
+ path = "https://devlovia.org/api/v2/proxy/" + path;
}
const pathStatusData = this._resources.get(name);
// the resource has not been registered yet:
- if (typeof pathStatusData === 'undefined')
+ if (typeof pathStatusData === "undefined")
{
self._resources.set(name, {
status: ServerManager.ResourceStatus.REGISTERED,
path,
- data: undefined
+ data: undefined,
});
self._waitForDownloadComponent.resources.add(name);
resourcesToDownload.add(name);
- self._psychoJS.logger.debug('registered resource:', name, path);
+ self._psychoJS.logger.debug("registered resource:", name, path);
}
// the resource has been registered but is not downloaded yet:
else if (typeof pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
- // else if (typeof pathStatusData.data === 'undefined')
- {
+ { // else if (typeof pathStatusData.data === 'undefined')
self._waitForDownloadComponent.resources.add(name);
}
-
}
+ self._waitForDownloadComponent.status = PsychoJS.Status.STARTED;
+
// start the download:
self._downloadResources(resourcesToDownload);
}
- // check whether all resources have been downloaded:
- for (const name of self._waitForDownloadComponent.resources)
+ if (self._waitForDownloadComponent.status === PsychoJS.Status.STARTED)
{
- const pathStatusData = this._resources.get(name);
-
- // the resource has not been downloaded yet: loop this component
- if (typeof pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
- // if (typeof pathStatusData.data === 'undefined')
+ // check whether all resources have been downloaded:
+ for (const name of self._waitForDownloadComponent.resources)
{
- return Scheduler.Event.FLIP_REPEAT;
+ const pathStatusData = this._resources.get(name);
+
+ // the resource has not been downloaded yet: loop this component
+ if (pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
+ { // if (typeof pathStatusData.data === 'undefined')
+ return Scheduler.Event.FLIP_REPEAT;
+ }
}
- }
- // all resources have been downloaded: move to the next component:
- self._waitForDownloadComponent.status = PsychoJS.Status.FINISHED;
- return Scheduler.Event.NEXT;
+ // all resources have been downloaded: move to the next component:
+ self._waitForDownloadComponent.status = PsychoJS.Status.FINISHED;
+ return Scheduler.Event.NEXT;
+ }
};
-
}
-
- /**
+ /****************************************************************************
* @typedef ServerManager.UploadDataPromise
* @property {string} origin the calling method
* @property {string} context the context
* @property {Object.<string, *>} [error] an error message if we could not upload the data
*/
- /**
+ /****************************************************************************
* Asynchronously upload experiment data to the pavlovia server.
*
* @name module:core.ServerManager#uploadData
@@ -673,24 +754,24 @@
/**
* Event type: resource event
*/
- RESOURCE: Symbol.for('RESOURCE'),
+ RESOURCE: Symbol.for("RESOURCE"),
/**
* Event: resources have started to download
*/
- DOWNLOADING_RESOURCES: Symbol.for('DOWNLOADING_RESOURCES'),
+ DOWNLOADING_RESOURCES: Symbol.for("DOWNLOADING_RESOURCES"),
/**
* Event: a specific resource download has started
*/
- DOWNLOADING_RESOURCE: Symbol.for('DOWNLOADING_RESOURCE'),
+ DOWNLOADING_RESOURCE: Symbol.for("DOWNLOADING_RESOURCE"),
/**
* Event: a specific resource has been downloaded
*/
- RESOURCE_DOWNLOADED: Symbol.for('RESOURCE_DOWNLOADED'),
+ RESOURCE_DOWNLOADED: Symbol.for("RESOURCE_DOWNLOADED"),
/**
* Event: resources have all downloaded
*/
- DOWNLOADS_COMPLETED: Symbol.for('DOWNLOAD_COMPLETED'),
+ DOWNLOADS_COMPLETED: Symbol.for("DOWNLOAD_COMPLETED"),
/**
* Event type: status event
*/
- STATUS: Symbol.for('STATUS')
+ STATUS: Symbol.for("STATUS"),
};
-
-/**
+/****************************************************************************
* Server status
*
* @name module:core.ServerManager#Status
@@ -1224,21 +1420,20 @@
Source: core/ServerManager.js
/**
* The manager is ready.
*/
- READY: Symbol.for('READY'),
+ READY: Symbol.for("READY"),
/**
* The manager is busy, e.g. it is downloaded resources.
*/
- BUSY: Symbol.for('BUSY'),
+ BUSY: Symbol.for("BUSY"),
/**
* The manager has encountered an error, e.g. it was unable to download a resource.
*/
- ERROR: Symbol.for('ERROR')
+ ERROR: Symbol.for("ERROR"),
};
-
-/**
+/****************************************************************************
* Resource status
*
* @name module:core.ServerManager#ResourceStatus
@@ -1248,24 +1443,24 @@
Source: core/ServerManager.js
*/
ServerManager.ResourceStatus = {
/**
- * The resource has been registered.
+ * There was an error during downloading, or the resource is in an unknown state.
*/
- REGISTERED: Symbol.for('REGISTERED'),
+ ERROR: Symbol.for("ERROR"),
/**
- * The resource is currently downloading.
+ * The resource has been registered.
*/
- DOWNLOADING: Symbol.for('DOWNLOADING'),
+ REGISTERED: Symbol.for("REGISTERED"),
/**
- * The resource has been downloaded.
+ * The resource is currently downloading.
*/
- DOWNLOADED: Symbol.for('DOWNLOADED'),
+ DOWNLOADING: Symbol.for("DOWNLOADING"),
/**
- * There was an error during downloading, or the resource is in an unknown state.
+ * The resource has been downloaded.
*/
- ERROR: Symbol.for('ERROR'),
+ DOWNLOADED: Symbol.for("DOWNLOADED"),
};
@@ -1277,13 +1472,13 @@
* @license Distributed under the terms of the MIT License
*/
-import * as PIXI from 'pixi.js-legacy';
-import {Color} from '../util/Color';
-import {PsychObject} from '../util/PsychObject';
-import {MonotonicClock} from '../util/Clock';
-import {Logger} from "./Logger";
+import * as PIXI from "pixi.js-legacy";
+import {AdjustmentFilter} from "@pixi/filter-adjustment";
+import { MonotonicClock } from "../util/Clock.js";
+import { Color } from "../util/Color.js";
+import { PsychObject } from "../util/PsychObject.js";
+import { Logger } from "./Logger.js";
/**
* <p>Window displays the various stimuli of the experiment.</p>
@@ -53,6 +54,8 @@
Source: core/Window.js
* @param {string} [options.name] the name of the window
* @param {boolean} [options.fullscr= false] whether or not to go fullscreen
* @param {Color} [options.color= Color('black')] the background color of the window
+ * @param {number} [options.gamma= 1] sets the divisor for gamma correction. In other words gamma correction is calculated as pow(rgb, 1/gamma)
+ * @param {number} [options.contrast= 1] sets the contrast value
* @param {string} [options.units= 'pix'] the units of the window
* @param {boolean} [options.waitBlanking= false] whether or not to wait for all rendering operations to be done
* before flipping
@@ -60,7 +63,6 @@
Source: core/Window.js
*/
export class Window extends PsychObject
{
-
/**
* Getter for monitorFramePeriod.
*
@@ -74,30 +76,47 @@
Source: core/Window.js
}
constructor({
- psychoJS,
- name,
- fullscr = false,
- color = new Color('black'),
- units = 'pix',
- waitBlanking = false,
- autoLog = true
- } = {})
+ psychoJS,
+ name,
+ fullscr = false,
+ color = new Color("black"),
+ gamma = 1,
+ contrast = 1,
+ units = "pix",
+ waitBlanking = false,
+ autoLog = true,
+ } = {})
{
super(psychoJS, name);
// messages to be logged at the next "flip":
this._msgToBeLogged = [];
+ // storing AdjustmentFilter instance to access later;
+ this._adjustmentFilter = new AdjustmentFilter({
+ gamma,
+ contrast
+ });
+
// list of all elements, in the order they are currently drawn:
this._drawList = [];
- this._addAttribute('fullscr', fullscr);
- this._addAttribute('color', color);
- this._addAttribute('units', units);
- this._addAttribute('waitBlanking', waitBlanking);
- this._addAttribute('autoLog', autoLog);
- this._addAttribute('size', []);
-
+ this._addAttribute("fullscr", fullscr);
+ this._addAttribute("color", color, new Color("black"), () => {
+ if (this._backgroundSprite) {
+ this._backgroundSprite.tint = color.int;
+ }
+ });
+ this._addAttribute("gamma", gamma, 1, () => {
+ this._adjustmentFilter.gamma = this._gamma;
+ });
+ this._addAttribute("contrast", contrast, 1, () => {
+ this._adjustmentFilter.contrast = this._contrast;
+ });
+ this._addAttribute("units", units);
+ this._addAttribute("waitBlanking", waitBlanking);
+ this._addAttribute("autoLog", autoLog);
+ this._addAttribute("size", []);
// setup PIXI:
this._setupPixi();
@@ -106,15 +125,14 @@
Source: core/Window.js
this._flipCallbacks = [];
-
// fullscreen listener:
this._windowAlreadyInFullScreen = false;
const self = this;
- document.addEventListener('fullscreenchange', (event) =>
+ document.addEventListener("fullscreenchange", (event) =>
{
self._windowAlreadyInFullScreen = !!document.fullscreenElement;
- console.log('windowAlreadyInFullScreen:', self._windowAlreadyInFullScreen);
+ console.log("windowAlreadyInFullScreen:", self._windowAlreadyInFullScreen);
// the Window and all of the stimuli need to be updated:
self._needUpdate = true;
@@ -124,14 +142,12 @@
Source: core/Window.js
}
});
-
if (this._autoLog)
{
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
}
}
-
/**
* Close the window.
*
@@ -148,27 +164,28 @@
*/
callOnFlip(flipCallback, ...flipCallbackArgs)
{
- this._flipCallbacks.push({function: flipCallback, arguments: flipCallbackArgs});
+ this._flipCallbacks.push({ function: flipCallback, arguments: flipCallbackArgs });
+ }
+
+ /**
+ * Add PIXI.DisplayObject to the container displayed on the scene (window)
+ *
+ * @name module:core.Window#addPixiObject
+ * @function
+ * @public
+ */
+ addPixiObject (pixiObject) {
+ this._stimsContainer.addChild(pixiObject);
}
+ /**
+ * Remove PIXI.DisplayObject from the container displayed on the scene (window)
+ *
+ * @name module:core.Window#removePixiObject
+ * @function
+ * @public
+ */
+ removePixiObject (pixiObject) {
+ this._stimsContainer.removeChild(pixiObject);
+ }
/**
* Render the stimuli onto the canvas.
@@ -337,13 +369,12 @@
Source: core/Window.js
return;
}
-
this._frameCount++;
// render the PIXI container:
this._renderer.render(this._rootContainer);
- if (typeof this._renderer.gl !== 'undefined')
+ if (typeof this._renderer.gl !== "undefined")
{
// this is to make sure that the GPU is done rendering, it may not be necessary
// [http://www.html5gamedevs.com/topic/27849-detect-when-view-has-been-rendered/]
@@ -359,7 +390,7 @@
Source: core/Window.js
// call the callOnFlip functions and remove them:
for (let callback of this._flipCallbacks)
{
- callback['function'](...callback['arguments']);
+ callback["function"](...callback["arguments"]);
}
this._flipCallbacks = [];
@@ -370,7 +401,6 @@
Source: core/Window.js
this._refresh();
}
-
/**
* Update this window, if need be.
*
@@ -395,7 +425,6 @@
Source: core/Window.js
}
}
-
/**
* Recompute this window's draw list and _container children for the next animation frame.
*
@@ -411,16 +440,15 @@
Source: core/Window.js
// update it, then put it back
for (const stimulus of this._drawList)
{
- if (stimulus._needUpdate && typeof stimulus._pixi !== 'undefined')
+ if (stimulus._needUpdate && typeof stimulus._pixi !== "undefined")
{
- this._rootContainer.removeChild(stimulus._pixi);
+ this._stimsContainer.removeChild(stimulus._pixi);
stimulus._updateIfNeeded();
- this._rootContainer.addChild(stimulus._pixi);
+ this._stimsContainer.addChild(stimulus._pixi);
}
}
}
-
/**
* Force an update of all stimuli in this window's drawlist.
*
@@ -440,7 +468,6 @@
width: this._size[0],
height: this._size[1],
backgroundColor: this.color.int,
- resolution: window.devicePixelRatio
+ powerPreference: "high-performance",
+ resolution: window.devicePixelRatio,
});
- this._renderer.view.style.transform = 'translatez(0)';
- this._renderer.view.style.position = 'absolute';
+ this._renderer.view.style.transform = "translatez(0)";
+ this._renderer.view.style.position = "absolute";
document.body.appendChild(this._renderer.view);
// 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;
+ // filters in PIXI work in a slightly unexpected fashion:
+ // when setting this._rootContainer.filters, filtering itself
+ // ignores backgroundColor of this._renderer and in addition to that
+ // all child elements of this._rootContainer ignore backgroundColor when blending.
+ // To circumvent that creating a separate PIXI.Sprite that serves as background color.
+ // Then placing all Stims to a separate this._stimsContainer which hovers on top of
+ // background sprite so that if we need to move all stims at once, the background sprite
+ // won't get affected.
+ this._backgroundSprite = new PIXI.Sprite(PIXI.Texture.WHITE);
+ this._backgroundSprite.tint = this.color.int;
+ this._backgroundSprite.width = this._size[0];
+ this._backgroundSprite.height = this._size[1];
+ this._backgroundSprite.anchor.set(.5);
+ this._stimsContainer = new PIXI.Container();
+ this._stimsContainer.sortableChildren = true;
+
// create a top-level PIXI container:
this._rootContainer = new PIXI.Container();
+ this._rootContainer.addChild(this._backgroundSprite, this._stimsContainer);
+
+ // sorts children according to their zIndex value. Higher zIndex means it will be moved towards the end of the array,
+ // and thus rendered on top of previous one.
+ this._rootContainer.sortableChildren = true;
+
this._rootContainer.interactive = true;
+ this._rootContainer.filters = [this._adjustmentFilter];
// set the initial size of the PIXI renderer and the position of the root container:
Window._resizePixiRenderer(this);
@@ -487,11 +538,10 @@
Source: core/Window.js
Window._resizePixiRenderer(this, e);
this._fullRefresh();
};
- window.addEventListener('resize', this._resizeCallback);
- window.addEventListener('orientationchange', this._resizeCallback);
+ window.addEventListener("resize", this._resizeCallback);
+ window.addEventListener("orientationchange", this._resizeCallback);
}
-
/**
* Adjust the size of the renderer and the position of the root container
* in response to a change in the browser's size.
@@ -504,17 +554,17 @@
Source: core/Window.js
*/
static _resizePixiRenderer(pjsWindow, event)
{
- pjsWindow._psychoJS.logger.debug('resizing Window: ', pjsWindow._name, 'event:', JSON.stringify(event));
+ pjsWindow._psychoJS.logger.debug("resizing Window: ", pjsWindow._name, "event:", JSON.stringify(event));
// update the size of the PsychoJS Window:
pjsWindow._size[0] = window.innerWidth;
pjsWindow._size[1] = window.innerHeight;
// update the PIXI renderer:
- pjsWindow._renderer.view.style.width = pjsWindow._size[0] + 'px';
- pjsWindow._renderer.view.style.height = pjsWindow._size[1] + 'px';
- pjsWindow._renderer.view.style.left = '0px';
- pjsWindow._renderer.view.style.top = '0px';
+ pjsWindow._renderer.view.style.width = pjsWindow._size[0] + "px";
+ pjsWindow._renderer.view.style.height = pjsWindow._size[1] + "px";
+ pjsWindow._renderer.view.style.left = "0px";
+ pjsWindow._renderer.view.style.top = "0px";
pjsWindow._renderer.resize(pjsWindow._size[0], pjsWindow._size[1]);
// setup the container such that (0,0) is at the centre of the window
@@ -524,7 +574,6 @@
Source: core/Window.js
pjsWindow._rootContainer.scale.y = -1;
}
-
/**
* Send all logged messages to the {@link Logger}.
*
@@ -542,7 +591,6 @@
* @license Distributed under the terms of the MIT License
*/
-
/**
* <p>This mixin implements various unit-handling measurement methods.</p>
*
@@ -47,16 +46,15 @@
Source: core/WindowMixin.js
* @mixin
*
*/
-export let WindowMixin = (superclass) => class extends superclass
-{
- constructor(args)
+export let WindowMixin = (superclass) =>
+ class extends superclass
{
- super(args);
- }
-
-
+ constructor(args)
+ {
+ super(args);
+ }
- /**
+ /**
* Convert the given length from stimulus unit to pixel units.
*
* @name module:core.WindowMixin#_getLengthPix
@@ -66,85 +64,83 @@
Source: core/WindowMixin.js
* @param {boolean} [integerCoordinates = false] - whether or not to round the length.
* @return {number} - the length in pixel units
*/
- _getLengthPix(length, integerCoordinates = false)
- {
- let response = {
- origin: 'WindowMixin._getLengthPix',
- context: 'when converting a length from stimulus unit to pixel units'
- };
-
- let length_px;
-
- if (this._units === 'pix')
- {
- length_px = length;
- }
- else if (typeof this._units === 'undefined' || this._units === 'norm')
- {
- var winSize = this.win.size;
- length_px = length * winSize[1] / 2; // TODO: how do we handle norm when width != height?
- }
- else if (this._units === 'height')
- {
- const minSize = Math.min(this.win.size[0], this.win.size[1]);
- length_px = length * minSize;
- }
- else
- {
- // throw { ...response, error: 'unable to deal with unit: ' + this._units };
- throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
- }
-
- if (integerCoordinates)
- {
- return Math.round(length_px);
- }
- else
- {
- return length_px;
- }
- }
-
-
- /**
- * Convert the given length from pixel units to the stimulus units
- *
- * @name module:core.WindowMixin#_getLengthUnits
- * @function
- * @protected
- * @param {number} length_px - the length in pixel units
- * @return {number} - the length in stimulus units
- */
- _getLengthUnits(length_px)
- {
- let response = {
- origin: 'WindowMixin._getLengthUnits',
- context: 'when converting a length from pixel unit to stimulus units'
- };
-
- if (this._units === 'pix')
- {
- return length_px;
- }
- else if (typeof this._units === 'undefined' || this._units === 'norm')
- {
- const winSize = this.win.size;
- return length_px / (winSize[1] / 2); // TODO: how do we handle norm when width != height?
- }
- else if (this._units === 'height')
- {
- const minSize = Math.min(this.win.size[0], this.win.size[1]);
- return length_px / minSize;
- }
- else
- {
- // throw { ...response, error: 'unable to deal with unit: ' + this._units };
- throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
- }
- }
-
-
- /**
+ _getLengthPix(length, integerCoordinates = false)
+ {
+ let response = {
+ origin: "WindowMixin._getLengthPix",
+ context: "when converting a length from stimulus unit to pixel units",
+ };
+
+ let length_px;
+
+ if (this._units === "pix")
+ {
+ length_px = length;
+ }
+ else if (typeof this._units === "undefined" || this._units === "norm")
+ {
+ var winSize = this.win.size;
+ length_px = length * winSize[1] / 2; // TODO: how do we handle norm when width != height?
+ }
+ else if (this._units === "height")
+ {
+ const minSize = Math.min(this.win.size[0], this.win.size[1]);
+ length_px = length * minSize;
+ }
+ else
+ {
+ // throw { ...response, error: 'unable to deal with unit: ' + this._units };
+ throw Object.assign(response, { error: "unable to deal with unit: " + this._units });
+ }
+
+ if (integerCoordinates)
+ {
+ return Math.round(length_px);
+ }
+ else
+ {
+ return length_px;
+ }
+ }
+
+ /**
+ * Convert the given length from pixel units to the stimulus units
+ *
+ * @name module:core.WindowMixin#_getLengthUnits
+ * @function
+ * @protected
+ * @param {number} length_px - the length in pixel units
+ * @return {number} - the length in stimulus units
+ */
+ _getLengthUnits(length_px)
+ {
+ let response = {
+ origin: "WindowMixin._getLengthUnits",
+ context: "when converting a length from pixel unit to stimulus units",
+ };
+
+ if (this._units === "pix")
+ {
+ return length_px;
+ }
+ else if (typeof this._units === "undefined" || this._units === "norm")
+ {
+ const winSize = this.win.size;
+ return length_px / (winSize[1] / 2); // TODO: how do we handle norm when width != height?
+ }
+ else if (this._units === "height")
+ {
+ const minSize = Math.min(this.win.size[0], this.win.size[1]);
+ return length_px / minSize;
+ }
+ else
+ {
+ // throw { ...response, error: 'unable to deal with unit: ' + this._units };
+ throw Object.assign(response, { error: "unable to deal with unit: " + this._units });
+ }
+ }
+
+ /**
* Convert the given length from stimulus units to pixel units
*
* @name module:core.WindowMixin#_getHorLengthPix
@@ -153,35 +149,35 @@
Source: core/WindowMixin.js
* @param {number} length - the length in stimulus units
* @return {number} - the length in pixels
*/
- _getHorLengthPix(length)
- {
- let response = {
- origin: 'WindowMixin._getHorLengthPix',
- context: 'when converting a length from stimulus units to pixel units'
- };
-
- if (this._units === 'pix')
- {
- return length;
- }
- else if (typeof this._units === 'undefined' || this._units === 'norm')
- {
- var winSize = this.win.size;
- return length * winSize[0] / 2;
- }
- else if (this._units === 'height')
- {
- const minSize = Math.min(this.win.size[0], this.win.size[1]);
- return length * minSize;
- }
- else
- {
- // throw { ...response, error: 'unable to deal with unit: ' + this._units };
- throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
- }
- }
-
- /**
+ _getHorLengthPix(length)
+ {
+ let response = {
+ origin: "WindowMixin._getHorLengthPix",
+ context: "when converting a length from stimulus units to pixel units",
+ };
+
+ if (this._units === "pix")
+ {
+ return length;
+ }
+ else if (typeof this._units === "undefined" || this._units === "norm")
+ {
+ var winSize = this.win.size;
+ return length * winSize[0] / 2;
+ }
+ else if (this._units === "height")
+ {
+ const minSize = Math.min(this.win.size[0], this.win.size[1]);
+ return length * minSize;
+ }
+ else
+ {
+ // throw { ...response, error: 'unable to deal with unit: ' + this._units };
+ throw Object.assign(response, { error: "unable to deal with unit: " + this._units });
+ }
+ }
+
+ /**
* Convert the given length from pixel units to the stimulus units
*
* @name module:core.WindowMixin#_getVerLengthPix
@@ -190,35 +186,34 @@
Source: core/WindowMixin.js
* @param {number} length - the length in pixel units
* @return {number} - the length in stimulus units
*/
- _getVerLengthPix(length)
- {
- let response = {
- origin: 'WindowMixin._getVerLengthPix',
- context: 'when converting a length from pixel unit to stimulus units'
- };
-
- if (this._units === 'pix')
- {
- return length;
- }
- else if (typeof this._units === 'undefined' || this._units === 'norm')
- {
- var winSize = this.win.size;
- return length * winSize[1] / 2;
- }
- else if (this._units === 'height')
- {
- const minSize = Math.min(this.win.size[0], this.win.size[1]);
- return length * minSize;
- }
- else
- {
- // throw { ...response, error: 'unable to deal with unit: ' + this._units };
- throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
- }
- }
-
-};
+ _getVerLengthPix(length)
+ {
+ let response = {
+ origin: "WindowMixin._getVerLengthPix",
+ context: "when converting a length from pixel unit to stimulus units",
+ };
+
+ if (this._units === "pix")
+ {
+ return length;
+ }
+ else if (typeof this._units === "undefined" || this._units === "norm")
+ {
+ var winSize = this.win.size;
+ return length * winSize[1] / 2;
+ }
+ else if (this._units === "height")
+ {
+ const minSize = Math.min(this.win.size[0], this.win.size[1]);
+ return length * minSize;
+ }
+ else
+ {
+ // throw { ...response, error: 'unable to deal with unit: ' + this._units };
+ throw Object.assign(response, { error: "unable to deal with unit: " + this._units });
+ }
+ }
+ };
@@ -229,13 +224,13 @@
* @license Distributed under the terms of the MIT License
*/
-
-import * as XLSX from 'xlsx';
-import {PsychObject} from '../util/PsychObject';
-import {MonotonicClock} from '../util/Clock';
-import * as util from '../util/Util';
-
+import * as XLSX from "xlsx";
+import { MonotonicClock } from "../util/Clock.js";
+import { PsychObject } from "../util/PsychObject.js";
+import * as util from "../util/Util.js";
/**
* <p>An ExperimentHandler keeps track of multiple loops and handlers. It is particularly useful
@@ -57,11 +55,10 @@
this._experimentEnded = false;
}
-
/**
* Whether or not the current entry (i.e. trial data) is empty.
* <p>Note: this is mostly useful at the end of an experiment, in order to ensure that the last entry is saved.</p>
@@ -128,13 +143,13 @@
Source: data/ExperimentHandler.js
* @function
* @public
* @returns {boolean} whether or not the current entry is empty
+ * @todo This really should be renamed: IsCurrentEntryNotEmpty
*/
isEntryEmpty()
{
return (Object.keys(this._currentTrialData).length > 0);
}
-
/**
* Add a loop.
*
@@ -153,7 +168,6 @@
Source: data/ExperimentHandler.js
loop.experimentHandler = this;
}
-
/**
* Remove the given loop from the list of unfinished loops, e.g. when it has completed.
*
@@ -171,7 +185,6 @@
this._currentTrialData[key] = value;
}
-
/**
* Inform this ExperimentHandler that the current trial has ended. Further calls to {@link addData}
* will be associated with the next trial.
@@ -208,11 +220,11 @@
Source: data/ExperimentHandler.js
* @name module:data.ExperimentHandler#nextEntry
* @function
* @public
- * @param {Object[]} snapshots - array of loop snapshots
+ * @param {Object | Object[] | undefined} snapshots - array of loop snapshots
*/
nextEntry(snapshots)
{
- if (typeof snapshots !== 'undefined')
+ if (typeof snapshots !== "undefined")
{
// turn single snapshot into a one-element array:
if (!Array.isArray(snapshots))
@@ -231,7 +243,6 @@
Source: data/ExperimentHandler.js
}
}
}
-
}
// this is to support legacy generated JavaScript code and does not properly handle
// loops within loops:
@@ -264,7 +275,6 @@
Source: data/ExperimentHandler.js
this._currentTrialData = {};
}
-
/**
* Save the results of the experiment.
*
@@ -279,16 +289,20 @@
Source: data/ExperimentHandler.js
* @public
* @param {Object} options
* @param {Array.<Object>} [options.attributes] - the attributes to be saved
- * @param {Array.<Object>} [options.sync] - whether or not to communicate with the server in a synchronous manner
+ * @param {boolean} [options.sync=false] - whether or not to communicate with the server in a synchronous manner
+ * @param {string} [options.tag=''] - an optional tag to add to the filename to which the data is saved (for CSV and XLSX saving options)
+ * @param {boolean} [options.clear=false] - whether or not to clear all experiment results immediately after they are saved (this is useful when saving data in separate chunks, throughout an experiment)
*/
async save({
- attributes = [],
- sync = false
- } = {})
+ attributes = [],
+ sync = false,
+ tag = "",
+ clear = 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)
{
attributes = this._trialsKeys.slice();
@@ -314,74 +328,82 @@
Source: data/ExperimentHandler.js
}
}
+ let data = this._trialsData;
+ // if the experiment data have to be cleared, we first make a copy of them:
+ if (clear)
+ {
+ data = this._trialsData.slice();
+ this._trialsData = [];
+ }
- // (*) get various experiment info:
- const info = this.extraInfo;
- const __experimentName = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name;
- const __participant = ((typeof info.participant === 'string' && info.participant.length > 0) ? info.participant : 'PARTICIPANT');
- const __session = ((typeof info.session === 'string' && info.session.length > 0) ? info.session : 'SESSION');
- const __datetime = ((typeof info.date !== 'undefined') ? info.date : MonotonicClock.getDateStr());
- const gitlabConfig = this._psychoJS.config.gitlab;
- const __projectId = (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined') ? gitlabConfig.projectId : undefined;
-
-
- // (*) save to a .csv file:
+ // save to a .csv file:
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV)
{
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
// newlines, etc.
- const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
+ // TODO only save the given attributes
+ const worksheet = XLSX.utils.json_to_sheet(data);
// prepend BOM
- const csv = '\ufeff' + XLSX.utils.sheet_to_csv(worksheet);
+ const csv = "\ufeff" + XLSX.utils.sheet_to_csv(worksheet);
// upload data to the pavlovia server or offer them for download:
- const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
- if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
- this._psychoJS.config.experiment.status === 'RUNNING' &&
- !this._psychoJS._serverMsg.has('__pilotToken'))
+ const filenameWithoutPath = this._dataFileName.split(/[\\/]/).pop();
+ const key = `${filenameWithoutPath}${tag}.csv`;
+ if (
+ this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER
+ && this._psychoJS.config.experiment.status === "RUNNING"
+ && !this._psychoJS._serverMsg.has("__pilotToken")
+ )
{
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv, sync);
}
else
{
- util.offerDataForDownload(key, csv, 'text/csv');
+ util.offerDataForDownload(key, csv, "text/csv");
}
}
-
-
- // (*) save in the database on the remote server:
+ // save to the database on the pavlovia server:
else if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.DATABASE)
{
+ const gitlabConfig = this._psychoJS.config.gitlab;
+ const __projectId = (typeof gitlabConfig !== "undefined" && typeof gitlabConfig.projectId !== "undefined") ? gitlabConfig.projectId : undefined;
+
let documents = [];
- for (let r = 0; r < this._trialsData.length; r++)
+ for (let r = 0; r < data.length; r++)
{
- let doc = {__projectId, __experimentName, __participant, __session, __datetime};
+ let doc = {
+ __projectId,
+ __experimentName: this._experimentName,
+ __participant: this._participant,
+ __session: this._session,
+ __datetime: this._datetime
+ };
for (let h = 0; h < attributes.length; h++)
{
- doc[attributes[h]] = this._trialsData[r][attributes[h]];
+ doc[attributes[h]] = data[r][attributes[h]];
}
documents.push(doc);
}
// upload data to the pavlovia server or offer them for download:
- if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
- this._psychoJS.config.experiment.status === 'RUNNING' &&
- !this._psychoJS._serverMsg.has('__pilotToken'))
+ 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), sync);
}
else
{
- util.offerDataForDownload('results.json', JSON.stringify(documents), 'application/json');
+ util.offerDataForDownload("results.json", JSON.stringify(documents), "application/json");
}
-
}
}
-
/**
* 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>
@@ -395,20 +417,20 @@
* @license Distributed under the terms of the MIT License
*/
-
-import seedrandom from 'seedrandom';
-import * as XLSX from 'xlsx';
-import {PsychObject} from '../util/PsychObject';
-import * as util from '../util/Util';
+import seedrandom from "seedrandom";
+import * as XLSX from "xlsx";
+import { PsychObject } from "../util/PsychObject.js";
+import * as util from "../util/Util.js";
/**
* <p>A Trial Handler handles the importing and sequencing of conditions.</p>
*
* @class
* @extends PsychObject
- * @param {Object} options
+ * @param {Object} options - the handler options
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
* @param {Array.<Object> | String} [options.trialList= [undefined] ] - if it is a string, we treat it as the name of a condition resource
* @param {number} options.nReps - number of repetitions
@@ -59,7 +58,6 @@
Source: data/TrialHandler.js
*/
export class TrialHandler extends PsychObject
{
-
/**
* Getter for experimentHandler.
*
@@ -84,7 +82,6 @@
{
this._psychoJS.logger.warn(`attempt to replace the value of protected TrialHandler variable: ${attribute}`);
}
- snapshot.trialAttributes = trialAttributes;
}
+ snapshot.trialAttributes = trialAttributes;
// add the snapshot to the list:
this._snapshots.push(snapshot);
@@ -308,7 +301,6 @@
Source: data/TrialHandler.js
return snapshot;
}
-
/**
* Setter for the seed attribute.
*
@@ -317,9 +309,9 @@
}
}
-
/**
* Set the internal state of this trial handler from the given snapshot.
*
@@ -340,7 +331,7 @@
Source: data/TrialHandler.js
static fromSnapshot(snapshot)
{
// if snapshot is undefined, do nothing:
- if (typeof snapshot === 'undefined')
+ if (typeof snapshot === "undefined")
{
return;
}
@@ -357,14 +348,23 @@
Source: data/TrialHandler.js
snapshot.handler.thisTrial = snapshot.handler.getCurrentTrial();
- // add to the trial handler the snapshot's trial attributes:
+ // add the snapshot's trial attributes to a global variable, whose name is derived from
+ // that of the handler: loops -> thisLoop (note the dropped s):
+ let name = snapshot.name;
+ if (name[name.length - 1] === "s")
+ {
+ name = name.substr(0, name.length - 1);
+ }
+ name = `this${name[0].toUpperCase()}${name.substr(1)}`;
+
+ const value = {};
for (const attribute of snapshot.trialAttributes)
{
- snapshot.handler[attribute] = snapshot[attribute];
+ value[attribute] = snapshot[attribute];
}
+ window[name] = value;
}
-
/**
* Getter for the finished attribute.
*
@@ -375,7 +375,6 @@
Source: data/TrialHandler.js
return this._finished;
}
-
/**
* Setter for the finished attribute.
*
@@ -384,14 +383,13 @@
return this.thisIndex;
}
-
/**
* Set the trial index.
*
@@ -414,7 +411,6 @@
Source: data/TrialHandler.js
this.thisIndex = index;
}
-
/**
* Get the attributes of the trials.
*
@@ -440,7 +436,6 @@
Source: data/TrialHandler.js
return Object.keys(this.trialList[0]);
}
-
/**
* Get the current trial.
*
@@ -452,7 +447,6 @@
Source: data/TrialHandler.js
return this.trialList[this.thisIndex];
}
-
/**
* Get the nth trial.
*
@@ -469,7 +463,6 @@
Source: data/TrialHandler.js
return this.trialList[index];
}
-
/**
* Get the nth future or past trial, without advancing through the trial list.
*
@@ -488,7 +481,6 @@
Source: data/TrialHandler.js
return this.trialList[this.thisIndex + n];
}
-
/**
* Get the nth previous trial.
* <p> Note: this is useful for comparisons in n-back tasks.</p>
@@ -502,7 +494,6 @@
Source: data/TrialHandler.js
return getFutureTrial(-abs(n));
}
-
/**
* Add a key/value pair to data about the current trial held by the experiment handler
*
@@ -518,7 +509,6 @@
Source: data/TrialHandler.js
}
}
-
/**
* Import a list of conditions from a .xls, .xlsx, .odp, or .csv resource.
*
@@ -558,8 +548,8 @@
// which is then read in as a string
const decodedResourceMaybe = new Uint8Array(resourceValue);
// Could be set to 'buffer' for ASCII .csv
- const type = resourceExtension === 'csv' ? 'string' : 'array';
- const decodedResource = type === 'string' ? (new TextDecoder()).decode(decodedResourceMaybe) : decodedResourceMaybe;
+ const type = resourceExtension === "csv" ? "string" : "array";
+ const decodedResource = type === "string" ? (new TextDecoder()).decode(decodedResourceMaybe) : decodedResourceMaybe;
const workbook = XLSX.read(decodedResource, { type });
// we consider only the first worksheet:
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 worksheet = workbook.Sheets[sheetName];
// worksheet to array of arrays (the first array contains the fields):
- const sheet = XLSX.utils.sheet_to_json(worksheet, {header: 1, blankrows: false});
+ const sheet = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false });
const fields = sheet.shift();
// (*) select conditions:
@@ -589,9 +579,9 @@
Source: data/TrialHandler.js
// (*) return the selected conditions as an array of 'object as map':
// [
- // {field0: value0-0, field1: value0-1, ...}
- // {field0: value1-0, field1: value1-1, ...}
- // ...
+ // {field0: value0-0, field1: value0-1, ...}
+ // {field0: value1-0, field1: value1-1, ...}
+ // ...
// ]
let trialList = new Array(selectedRows.length - 1);
for (let r = 0; r < selectedRows.length; ++r)
@@ -614,7 +604,7 @@
Source: data/TrialHandler.js
value = arrayMaybe;
}
- if (typeof value === 'string')
+ if (typeof value === "string")
{
const numberMaybe = Number.parseFloat(value);
@@ -626,7 +616,7 @@
Source: data/TrialHandler.js
else
{
// Parse doubly escaped line feeds
- value = value.replace(/(\n)/g, '\n');
+ value = value.replace(/(\n)/g, "\n");
}
}
@@ -637,74 +627,69 @@
Source: data/TrialHandler.js
return trialList;
}
-
else
{
- throw 'extension: ' + resourceExtension + ' currently not supported.';
+ throw "extension: " + resourceExtension + " currently not supported.";
}
}
catch (error)
{
throw {
- origin: 'TrialHandler.importConditions',
+ origin: "TrialHandler.importConditions",
context: `when importing condition: ${resourceName}`,
- error
+ error,
};
}
}
-
/**
* Prepare the trial list.
*
+ * @function
* @protected
- * @param {Array.<Object> | String} trialList - a list of trials, or the name of a condition resource
+ * @returns {void}
*/
- _prepareTrialList(trialList)
+ _prepareTrialList()
{
const response = {
- origin: 'TrialHandler._prepareTrialList',
- context: 'when preparing the trial list'
+ origin: "TrialHandler._prepareTrialList",
+ context: "when preparing the trial list",
};
// we treat undefined trialList as a list with a single empty entry:
- if (typeof trialList === 'undefined')
+ if (typeof this._trialList === "undefined")
{
this.trialList = [undefined];
}
-
// if trialList is an array, we make sure it is not empty:
- else if (Array.isArray(trialList))
+ else if (Array.isArray(this._trialList))
{
- if (trialList.length === 0)
+ if (this._trialList.length === 0)
{
this.trialList = [undefined];
}
}
-
// if trialList is a string, we treat it as the name of the condition resource:
- else if (typeof trialList === 'string')
+ else if (typeof this._trialList === "string")
{
- this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, trialList);
+ this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, this._trialList);
}
-
// unknown type:
else
{
throw Object.assign(response, {
- error: 'unable to prepare trial list: unknown type: ' + (typeof trialList)
+ error: `unable to prepare trial list: unknown type: ${(typeof this._trialList)}`
});
}
}
-
/*
* Prepare the sequence of trials.
*
* <p>The returned sequence is a matrix (an array of arrays) of trial indices
* with nStim columns and nReps rows. Note that this is the transpose of the
* matrix return by PsychoPY.
- *
+ *
* Example: with 3 trial and 5 repetitions, we get:
* - sequential:
* [[0 1 2]
@@ -727,21 +712,20 @@
Source: data/TrialHandler.js
_prepareSequence()
{
const response = {
- origin: 'TrialHandler._prepareSequence',
- context: 'when preparing a sequence of trials'
+ 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());
- if (this.method === TrialHandler.Method.SEQUENTIAL)
+ if (this._method === TrialHandler.Method.SEQUENTIAL)
{
this._trialSequence = Array(this.nReps).fill(indices);
// transposed version:
- //this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] );
+ // this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] );
}
-
- else if (this.method === TrialHandler.Method.RANDOM)
+ else if (this._method === TrialHandler.Method.RANDOM)
{
this._trialSequence = [];
for (let i = 0; i < this.nReps; ++i)
@@ -749,11 +733,10 @@
Source: data/TrialHandler.js
this._trialSequence.push(util.shuffle(indices.slice(), this._randomNumberGenerator));
}
}
-
- else if (this.method === TrialHandler.Method.FULL_RANDOM)
+ else if (this._method === TrialHandler.Method.FULL_RANDOM)
{
// create a flat sequence with nReps repeats of indices:
- let flatSequence = [];
+ const flatSequence = [];
for (let i = 0; i < this.nReps; ++i)
{
flatSequence.push.apply(flatSequence, indices);
@@ -771,15 +754,13 @@
/**
* Conditions are presented in the order they are given.
*/
- SEQUENTIAL: Symbol.for('SEQUENTIAL'),
+ SEQUENTIAL: Symbol.for("SEQUENTIAL"),
/**
* Conditions are shuffled within each repeat.
*/
- RANDOM: Symbol.for('RANDOM'),
+ RANDOM: Symbol.for("RANDOM"),
/**
* Conditions are fully randomised across all repeats.
*/
- FULL_RANDOM: Symbol.for('FULL_RANDOM'),
+ FULL_RANDOM: Symbol.for("FULL_RANDOM"),
/**
* Same as above, but named to reflect PsychoPy boileplate.
*/
- FULLRANDOM: Symbol.for('FULL_RANDOM')
+ FULLRANDOM: Symbol.for("FULL_RANDOM"),
};
@@ -818,13 +799,13 @@
PsychoJS is a JavaScript library that makes it possible to run neuroscience, psychology, and psychophysics experiments in a browser. It is the online counterpart of the PsychoPy Python library.
You can create online experiments from the PsychoPy Builder, you can find and adapt existing experiments on pavlovia.org, or create them from scratch: the PsychoJS API is available here.
Many studies in behavioural sciences (e.g. psychology, neuroscience, linguistics or mental health) use computers to present stimuli and record responses in a precise manner. These studies are still typically conducted on small numbers of people in laboratory environments equipped with dedicated hardware.
-
With high-speed broadband, improved web technologies and smart devices everywhere, studies can now go online without sacrificing too much temporal precision. This is a “game changer”. Data can be collected on larger, more varied, international populations. We can study people in environments they do not find intimidating. Experiments can be run multiple times per day, without data collection becoming impractical.
+
With high-speed broadband, improved web technologies and smart devices everywhere, studies can now go online without sacrificing too much temporal precision. This is a "game changer". Data can be collected on larger, more varied, international populations. We can study people in environments they do not find intimidating. Experiments can be run multiple times per day, without data collection becoming impractical.
The idea behind PsychoJS is to make PsychoPy experiments available online, from a web page, so participants can run them on any device equipped with a web browser such as desktops, laptops, or tablets. In some circumstance, they can even use their phone!
Getting Started
Running PsychoPy experiments online requires the generation of an index.html file and of a javascript file that contains the code describing the experiment. Those files need to be hosted on a web server to which participants will point their browser in order to run the experiment. The server will also need to host the PsychoJS library.
We built the PsychoJS library to make the JavaScript experiment files look and behave in very much the same way as to the Builder-generated Python files. PsychoJS offers classes such as Window and ImageStim, with very similar attributes to their Python equivalents. Experiment designers familiar with the PsychoPy library should feel at home with PsychoJS, and can expect the same level of control they have with PsychoPy, from the structure of the trials/loops all the way down to frame-by-frame updates.
-
There are however notable differences between the PsychoJS and PsychoPy libraries, most of which have to do with the way a web browser interprets and runs JavaScript, deals with resources (such as images, sound or videos), or render stimuli. To manage those web-specific aspect, PsychoJS introduces the concept of Scheduler. As the name indicate, Scheduler's offer a way to organise various PsychoJS along a timeline, such as downloading resources, running a loop, checking for keyboard input, saving experiment results, etc. As an illustration, a Flow in PsychoPy can be conceptualised as a Schedule, with various tasks on it. Some of those tasks, such as trial loops, can also schedule further events (i.e. the individual trials to be run).
-
Under the hood PsychoJS relies on PixiJs to present stimuli and collect responses. PixiJs is a multi-platform, accelerated, 2-D renderer, that runs in most modern browsers. It uses WebGL wherever possible and silently falls back to HTML5 canvas where not. WebGL directly addresses the graphic card, thereby considerably improving the rendering performance.
+
We built the PsychoJS library to make the JavaScript experiment files look and behave in very much the same way as the Builder-generated Python files. PsychoJS offers classes such as Window and ImageStim, with very similar attributes to their Python equivalents. Experiment designers familiar with the PsychoPy library should feel at home with PsychoJS, and can expect the same level of control they have with PsychoPy, from the structure of the trials/loops all the way down to frame-by-frame updates.
+
There are however notable differences between the PsychoJS and PsychoPy libraries, most of which having to do with the way a web browser interprets and runs JavaScript, deals with resources (such as images, sound or videos), or render stimuli. To manage those web-specific aspect, PsychoJS introduces the concept of Scheduler. As the name indicate, Scheduler's offer a way to organise various tasks along a timeline, such as downloading resources, running a loop, checking for keyboard input, saving experiment results, etc. As an illustration, a Flow in PsychoPy can be conceptualised as a Schedule, with various tasks on it. Some of those tasks, such as trial loops, can also schedule further events (i.e. the individual trials to be run).
+taskshe hood PsychoJS relies on PixiJS to present stimuli and collect responses. PixiJS is a high performance, multi-platform 2D renderer, that runs in most modern browsers. It uses WebGL wherever possible and silently falls back to HTML5 canvas where not. WebGL directly addresses the graphic card, thereby considerably improving the rendering performance.
Hosting Experiments
A convenient way to make experiment available to participants is to host them on pavlovia.org, an open-science server. PsychoPy Builder offers the possibility of uploading the experiment directly to pavlovia.org.
Which PsychoPy Components are supported by PsychoJS?