1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 02:30:53 +00:00

improved documentation for FaceDetector, Camera, and QuestHandler

This commit is contained in:
Alain Pitiot 2021-07-23 08:33:09 +02:00
commit dd0c46ccd0
55 changed files with 3516 additions and 3984 deletions

71
.dprint.json Normal file
View File

@ -0,0 +1,71 @@
{
"$schema": "https://dprint.dev/schemas/v0.json",
"projectType": "openSource",
"incremental": true,
"lineWidth": 180,
"indentWidth": 2,
"useTabs": false,
"typescript": {
"useTabs": true,
"semiColons": "always",
"quoteStyle": "preferDouble",
"newLineKind": "lf",
"useBraces": "always",
"bracePosition": "nextLine",
"singleBodyPosition": "nextLine",
"nextControlFlowPosition": "nextLine",
"trailingCommas": "onlyMultiLine",
"operatorPosition": "nextLine",
"preferHanging": false,
"preferSingleLine": false,
"arrowFunction.useParentheses": "force",
"binaryExpression.linePerExpression": false,
"jsx.quoteStyle": "preferDouble",
"memberExpression.linePerExpression": false,
"typeLiteral.separatorKind": "semiColon",
"enumDeclaration.memberSpacing": "maintain",
"spaceSurroundingProperties": true,
"objectExpression.spaceSurroundingProperties": true,
"objectPattern.spaceSurroundingProperties": true,
"typeLiteral.spaceSurroundingProperties": true,
"binaryExpression.spaceSurroundingBitwiseAndArithmeticOperator": true,
"commentLine.forceSpaceAfterSlashes": true,
"constructor.spaceBeforeParentheses": false,
"constructorType.spaceAfterNewKeyword": false,
"constructSignature.spaceAfterNewKeyword": false,
"doWhileStatement.spaceAfterWhileKeyword": true,
"exportDeclaration.spaceSurroundingNamedExports": true,
"forInStatement.spaceAfterForKeyword": true,
"forOfStatement.spaceAfterForKeyword": true,
"forStatement.spaceAfterForKeyword": true,
"forStatement.spaceAfterSemiColons": true,
"functionDeclaration.spaceBeforeParentheses": false,
"functionExpression.spaceBeforeParentheses": false,
"functionExpression.spaceAfterFunctionKeyword": false,
"getAccessor.spaceBeforeParentheses": false,
"ifStatement.spaceAfterIfKeyword": true,
"importDeclaration.spaceSurroundingNamedImports": true,
"jsxExpressionContainer.spaceSurroundingExpression": false,
"method.spaceBeforeParentheses": false,
"setAccessor.spaceBeforeParentheses": false,
"taggedTemplate.spaceBeforeLiteral": true,
"typeAnnotation.spaceBeforeColon": false,
"typeAssertion.spaceBeforeExpression": true,
"whileStatement.spaceAfterWhileKeyword": true
},
"json": {},
"markdown": {},
"includes": ["**/*.{js,cjs,mjs,json,md}"],
"excludes": [
"dist",
"docs",
"node_modules",
"*-lock.json",
"out"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.48.0.wasm",
"https://plugins.dprint.dev/json-0.12.1.wasm",
"https://plugins.dprint.dev/markdown-0.9.2.wasm"
]
}

View File

@ -1,140 +1,135 @@
module.exports = {
extends: 'eslint:recommended',
parserOptions: {
ecmaVersion: 11,
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true
},
plugins: [],
rules: {
'accessor-pairs': 1,
'arrow-body-style': [2, 'as-needed'],
'arrow-parens': [2, 'as-needed', { requireForBlockBody: true }],
'arrow-spacing': 2,
'block-spacing': 2,
'brace-style': [2, 'allman', { allowSingleLine: true }],
'camelcase': 1,
'capitalized-comments': [1, 'always', { ignoreConsecutiveComments: true }],
'comma-dangle': 2,
'comma-spacing': 2,
'comma-style': 2,
'consistent-return': 1,
'consistent-this': 2,
'curly': 2,
'default-case': 2,
'dot-location': [2, 'property'],
'dot-notation': 1,
'eol-last': 2,
'eqeqeq': [2, 'always'],
'func-call-spacing': 2,
'func-style': [2, 'declaration', { allowArrowFunctions: true }],
'generator-star-spacing': 2,
'global-require': 2,
'handle-callback-err': [2, '^(err|error)$' ],
'indent': [2, 'tab'],
'key-spacing': 2,
'keyword-spacing': 2,
'line-comment-position': 2,
'lines-around-comment': [2, { afterLineComment: false }],
'max-len': [1, 100],
'new-cap': [2, { capIsNew: false }],
'new-parens': 2,
'no-array-constructor': 2,
'no-buffer-constructor': 2,
'no-caller': 2,
'no-confusing-arrow': [2, { allowParens: true }],
'no-console': 1,
'no-div-regex': 2,
'no-duplicate-imports': 2,
'no-else-return': 2,
'no-eval': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-label': 2,
'no-extra-parens': [2, 'functions'],
'no-floating-decimal': 2,
'no-implied-eval': 2,
'no-inline-comments': 2,
'no-invalid-this': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': 2,
'no-lone-blocks': 2,
'no-mixed-operators': 2,
'no-mixed-requires': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, { max: 1, maxEOF: 0 }],
'no-new': 2,
'no-new-func': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-wrappers': 2,
'no-octal-escape': 2,
'no-param-reassign': 1,
'no-path-concat': 2,
'no-plusplus': 2,
'no-proto': 2,
'no-restricted-properties': 2,
'no-return-assign': [2, 'except-parens'],
'no-return-await': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow': 2,
'no-shadow-restricted-names': 2,
'no-tabs': [1, { allowIndentationTabs: true }],
'no-template-curly-in-string': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef-init': 2,
'no-underscore-dangle': 0,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': 2,
'no-unused-expressions': 2,
'no-use-before-define': [2, { functions: false }],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-rename': 2,
'no-useless-return': 2,
'no-var': 2,
'no-void': 2,
'no-whitespace-before-property': 2,
'no-with': 2,
'object-property-newline': [2, { allowMultiplePropertiesPerLine: true }],
'one-var': [2, 'never'],
'one-var-declaration-per-line': 2,
'operator-linebreak': [2, 'before'],
'padded-blocks': [2, 'never'],
'padding-line-between-statements': 2,
'prefer-const': 2,
'prefer-promise-reject-errors': 2,
'prefer-rest-params': 1,
'prefer-template': 2,
'quote-props': [2, 'consistent-as-needed'],
'quotes': [2, 'single'],
'radix': 2,
'require-await': 2,
'rest-spread-spacing': 2,
// 'semi': [2, 'never'],
// 'semi-spacing': 2,
'space-before-blocks': 2,
'space-before-function-paren': [2, { named: 'never' }],
'space-in-parens': 2,
'space-infix-ops': 2,
'space-unary-ops': 2,
'spaced-comment': 2,
'symbol-description': 2,
'template-curly-spacing': 2,
'template-tag-spacing': 2,
'unicode-bom': 2,
'valid-jsdoc': 2,
'wrap-iife': [2, 'any', { functionPrototypeMethods: true }],
'yield-star-spacing': [2, 'both'],
'yoda': 2
}
}
extends: "eslint:recommended",
parserOptions: {
ecmaVersion: 11,
sourceType: "module",
},
env: {
browser: true,
node: true,
es6: true,
},
plugins: [],
rules: {
"accessor-pairs": 1,
"arrow-body-style": [2, "as-needed"],
"arrow-spacing": 2,
"block-spacing": 2,
"brace-style": [2, "allman", { allowSingleLine: true }],
"camelcase": 1,
"capitalized-comments": [1, "always", { ignoreConsecutiveComments: true }],
"comma-spacing": 2,
"comma-style": 2,
"consistent-return": 1,
"consistent-this": 2,
"curly": 2,
"default-case": 2,
"dot-location": [2, "property"],
"dot-notation": 1,
"eol-last": 2,
"eqeqeq": [2, "always"],
"func-call-spacing": 2,
"func-style": [2, "declaration", { allowArrowFunctions: true }],
"generator-star-spacing": 2,
"global-require": 2,
"handle-callback-err": [2, "^(err|error)$"],
"indent": ["error", "tab", { "SwitchCase": 1 }],
"key-spacing": 2,
"keyword-spacing": 2,
"line-comment-position": 2,
"max-len": 0,
"new-cap": [2, { capIsNew: false }],
"new-parens": 2,
"no-array-constructor": 2,
"no-buffer-constructor": 2,
"no-caller": 2,
"no-confusing-arrow": [2, { allowParens: true }],
"no-console": 1,
"no-div-regex": 2,
"no-duplicate-imports": 2,
"no-else-return": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-label": 2,
"no-extra-parens": [2, "functions"],
"no-floating-decimal": 2,
"no-implied-eval": 2,
"no-inline-comments": 2,
"no-invalid-this": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-mixed-operators": 2,
"no-mixed-requires": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-multiple-empty-lines": [2, { max: 1, maxEOF: 0 }],
"no-new": 2,
"no-new-func": 2,
"no-new-object": 2,
"no-new-require": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-param-reassign": 1,
"no-path-concat": 2,
"no-plusplus": 2,
"no-proto": 2,
"no-restricted-properties": 2,
"no-return-assign": [2, "except-parens"],
"no-return-await": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-tabs": [1, { allowIndentationTabs: true }],
"no-template-curly-in-string": 2,
"no-throw-literal": 2,
"no-trailing-spaces": 2,
"no-undef-init": 2,
// https://eslint.org/docs/rules/no-underscore-dangle#disallow-dangling-underscores-in-identifiers-no-underscore-dangle
"no-underscore-dangle": 1,
"no-unmodified-loop-condition": 2,
"no-unneeded-ternary": 2,
"no-unused-expressions": 2,
"no-use-before-define": [2, { functions: false }],
"no-useless-call": 2,
"no-useless-computed-key": 2,
"no-useless-constructor": 2,
"no-useless-rename": 2,
"no-useless-return": 2,
"no-var": 2,
"no-void": 2,
"no-whitespace-before-property": 2,
"no-with": 2,
"object-property-newline": [2, { allowMultiplePropertiesPerLine: true }],
"one-var": [2, "never"],
"one-var-declaration-per-line": 2,
"operator-linebreak": [2, "before"],
"padded-blocks": [2, "never"],
"padding-line-between-statements": 2,
"prefer-const": 2,
"prefer-promise-reject-errors": 2,
"prefer-rest-params": 1,
"prefer-template": 2,
"quote-props": [2, "consistent-as-needed"],
"quotes": [2, "double"],
"radix": 2,
"require-await": 2,
"rest-spread-spacing": 2,
"space-before-blocks": 2,
"space-in-parens": 2,
"space-infix-ops": 2,
"space-unary-ops": 2,
"spaced-comment": 2,
"symbol-description": 2,
"template-curly-spacing": 2,
"template-tag-spacing": 2,
"unicode-bom": 2,
"valid-jsdoc": 2,
"wrap-iife": [2, "any", { functionPrototypeMethods: true }],
"yield-star-spacing": [2, "both"],
"yoda": 2,
},
};

View File

@ -12,9 +12,9 @@ PsychoJS is an open-source project. You can contribute by submitting pull reques
## Motivation
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.
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!
@ -31,7 +31,7 @@ We built the PsychoJS library to make the JavaScript experiment files look and b
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](http://www.pixijs.com) 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.
Under the hood PsychoJS relies on [PixiJS](http://www.pixijs.com) to present stimuli and collect responses. PixiJS is a multi-platform, accelerated, 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

21
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "psychojs",
"version": "2021.2.0",
"version": "2021.2.x",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "psychojs",
"version": "2021.2.0",
"version": "2021.2.x",
"license": "MIT",
"dependencies": {
"howler": "^2.2.1",
@ -19,6 +19,7 @@
},
"devDependencies": {
"csslint": "^1.0.5",
"dprint": "^0.15.3",
"esbuild": "^0.12.1",
"eslint": "^7.26.0",
"jsdoc": "^3.6.7"
@ -919,6 +920,16 @@
"node": ">=6.0.0"
}
},
"node_modules/dprint": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/dprint/-/dprint-0.15.3.tgz",
"integrity": "sha512-x/7wc+7TMSj+gO0vzeyU6X/3RqWph1nariuYCuhaF9hFSkmzShMuAhw4aJLxZoS/V/qtDAbJtsZ4/mJeGU1qvg==",
"dev": true,
"hasInstallScript": true,
"bin": {
"dprint": "bin.js"
}
},
"node_modules/earcut": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz",
@ -3190,6 +3201,12 @@
"esutils": "^2.0.2"
}
},
"dprint": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/dprint/-/dprint-0.15.3.tgz",
"integrity": "sha512-x/7wc+7TMSj+gO0vzeyU6X/3RqWph1nariuYCuhaF9hFSkmzShMuAhw4aJLxZoS/V/qtDAbJtsZ4/mJeGU1qvg==",
"dev": true
},
"earcut": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz",

View File

@ -17,8 +17,9 @@
"scripts": {
"build": "npm run build:js && npm run build:css && npm run build:docs",
"build:css": "node ./scripts/build.css.cjs",
"build:docs": "jsdoc src -r -d docs -c jsdoc-conf.json --readme README.md",
"build:docs": "jsdoc src -r -d docs --readme README.md",
"build:js": "node ./scripts/build.js.cjs",
"fmt": "dprint fmt",
"lint": "npm run lint:js && npm run lint:css",
"lint:css": "csslint src",
"lint:js": "eslint src",
@ -36,6 +37,7 @@
},
"devDependencies": {
"csslint": "^1.0.5",
"dprint": "^0.15.3",
"esbuild": "^0.12.1",
"eslint": "^7.26.0",
"jsdoc": "^3.6.7"

View File

@ -1,13 +1,13 @@
const { buildSync } = require('esbuild');
const pkg = require('psychojs/package.json');
const { buildSync } = require("esbuild");
const pkg = require("psychojs/package.json");
const versionMaybe = process.env.npm_config_outver;
const dirMaybe = process.env.npm_config_outdir;
const [,,, dir = dirMaybe || 'out', version = versionMaybe || pkg.version] = process.argv;
const [, , , dir = dirMaybe || "out", version = versionMaybe || pkg.version] = process.argv;
buildSync({
bundle: true,
entryPoints: ['src/index.css'],
entryPoints: ["src/index.css"],
minify: true,
outfile: `./${dir}/psychojs-${version}.css`
outfile: `./${dir}/psychojs-${version}.css`,
});

View File

@ -1,24 +1,25 @@
const { buildSync } = require('esbuild');
const pkg = require('psychojs/package.json');
const { buildSync } = require("esbuild");
const pkg = require("psychojs/package.json");
const versionMaybe = process.env.npm_config_outver;
const dirMaybe = process.env.npm_config_outdir;
const [,,, dir = dirMaybe || 'out', version = versionMaybe || pkg.version] = process.argv;
const [, , , dir = dirMaybe || "out", version = versionMaybe || pkg.version] = process.argv;
[
// The ESM bundle
{
format: 'esm',
legalComments: 'external',
format: "esm",
legalComments: "external",
outfile: `./${dir}/psychojs-${version}.js`,
},
// The IIFE
{
globalName: 'PsychoJS',
legalComments: 'none',
outfile: `./${dir}/psychojs-${version}.iife.js`
}
].forEach(function(options) {
globalName: "PsychoJS",
legalComments: "none",
outfile: `./${dir}/psychojs-${version}.iife.js`,
},
].forEach(function(options)
{
buildSync({ ...this, ...options });
}, {
// Shared options
@ -27,12 +28,12 @@ const [,,, dir = dirMaybe || 'out', version = versionMaybe || pkg.version] = pro
},
bundle: true,
sourcemap: true,
entryPoints: ['src/index.js'],
entryPoints: ["src/index.js"],
minifySyntax: true,
minifyWhitespace: true,
target: [
// https://github.com/evanw/esbuild/issues/121#issuecomment-646956379
'es2017',
'node14',
]
"es2017",
"node14",
],
});

View File

@ -7,9 +7,8 @@
* @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
@ -22,7 +21,6 @@ import {PsychoJS} from './PsychoJS';
*/
export class EventManager
{
constructor(psychoJS)
{
this._psychoJS = psychoJS;
@ -47,14 +45,13 @@ export class EventManager
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.
*
@ -69,9 +66,9 @@ export class EventManager
* @return {string[]} the list of keys that were pressed.
*/
getKeys({
keyList = null,
timeStamped = false
} = {})
keyList = null,
timeStamped = false,
} = {})
{
if (keyList != null)
{
@ -123,7 +120,6 @@ export class EventManager
return keys;
}
/**
* @typedef EventManager.ButtonInfo
* @property {Array.number} pressed - the status of each mouse button [left, center, right]: 1 for pressed, 0 for released
@ -151,7 +147,6 @@ export class EventManager
return this._mouseInfo;
}
/**
* Clear all events from the event buffer.
*
@ -166,7 +161,6 @@ export class EventManager
this.clearKeys();
}
/**
* Clear all keys from the key buffer.
*
@ -179,7 +173,6 @@ export class EventManager
this._keyBuffer = [];
}
/**
* Start the move clock.
*
@ -193,7 +186,6 @@ export class EventManager
{
}
/**
* Stop the move clock.
*
@ -207,7 +199,6 @@ export class EventManager
{
}
/**
* Reset the move clock.
*
@ -221,7 +212,6 @@ export class EventManager
{
}
/**
* Add various mouse listeners to the Pixi renderer of the {@link Window}.
*
@ -246,7 +236,6 @@ export class EventManager
this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button down, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
}, false);
renderer.view.addEventListener("touchstart", (event) =>
{
event.preventDefault();
@ -261,7 +250,6 @@ export class EventManager
this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button down, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
}, false);
renderer.view.addEventListener("pointerup", (event) =>
{
event.preventDefault();
@ -273,7 +261,6 @@ export class EventManager
this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button down, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
}, false);
renderer.view.addEventListener("touchend", (event) =>
{
event.preventDefault();
@ -288,7 +275,6 @@ export class EventManager
this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button down, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
}, false);
renderer.view.addEventListener("pointermove", (event) =>
{
event.preventDefault();
@ -297,7 +283,6 @@ export class EventManager
self._mouseInfo.pos = [event.offsetX, event.offsetY];
}, false);
renderer.view.addEventListener("touchmove", (event) =>
{
event.preventDefault();
@ -309,19 +294,16 @@ export class EventManager
self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY];
}, false);
// (*) wheel
renderer.view.addEventListener("wheel", event =>
renderer.view.addEventListener("wheel", (event) =>
{
self._mouseInfo.wheelRel[0] += event.deltaX;
self._mouseInfo.wheelRel[1] += event.deltaY;
this._psychoJS.experimentLogger.data("Mouse: wheel shift=(" + event.deltaX + "," + event.deltaY + "), pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
}, false);
}
/**
* Add key listeners to the document.
*
@ -336,14 +318,14 @@ export class EventManager
// 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);
}
@ -352,17 +334,15 @@ export class EventManager
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>
@ -378,7 +358,7 @@ export class EventManager
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]);
}
@ -391,7 +371,6 @@ export class EventManager
return w3cKeyList;
}
/**
* Convert a W3C Key Code into a pyglet key.
*
@ -410,11 +389,10 @@ export class EventManager
}
else
{
return 'N/A';
return "N/A";
}
}
/**
* Convert a keycode to a W3C UI Event code.
* <p>This is for legacy browsers.</p>
@ -432,7 +410,6 @@ export class EventManager
}
}
/**
* <p>This map provides support for browsers that have not yet
* adopted the W3C KeyboardEvent.code standard for detecting key presses.
@ -522,10 +499,9 @@ EventManager._keycodeMap = {
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>
@ -625,10 +601,9 @@ EventManager._pygletMap = {
"num_multiply": "NumpadMultiply",
"num_divide": "NumpadDivide",
"num_equal": "NumpadEqual",
"num_numlock": "NumpadNumlock"
"num_numlock": "NumpadNumlock",
};
/**
* <p>This map associates W3C KeyboardEvent.codes to the corresponding pyglet key names.
*
@ -639,7 +614,6 @@ EventManager._pygletMap = {
*/
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)
*
@ -656,8 +630,8 @@ export class BuilderKeyResponse
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
}
}

View File

@ -8,14 +8,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
@ -27,7 +26,6 @@ import * as util from '../util/Util';
*/
export class GUI
{
get dialogComponent()
{
return this._dialogComponent;
@ -46,7 +44,6 @@ export class GUI
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
@ -71,22 +68,21 @@ export class GUI
* @param {String} options.title - name of the project
*/
DlgFromDict({
logoUrl,
text,
dictionary,
title
})
logoUrl,
text,
dictionary,
title,
})
{
// get info from URL:
const infoFromUrl = util.getUrlParameters();
this._progressMsg = '&nbsp;';
this._progressMsg = "&nbsp;";
this._progressBarMax = 0;
this._allResourcesDownloaded = false;
this._requiredKeys = [];
this._setRequiredKeys = new Map();
// prepare PsychoJS component:
this._dialogComponent = {};
this._dialogComponent.status = PsychoJS.Status.NOT_STARTED;
@ -104,141 +100,123 @@ export class GUI
// 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;">&#9888; 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;
// 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)
const cleanedUrlKey = urlKey.trim().toLowerCase();
if (cleanedUrlKey === cleanedDictKey)
{
htmlCode += '<label for="' + keyId + '">' + key + '</label>';
inUrl = true;
// break;
}
});
// if the field is required:
if (key.slice(-1) === '*')
if (!inUrl)
{
htmlCode += '<label for="' + keyId + '">' + key + "</label>";
// 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 the field is required, we add an empty option and select it:
if (key.slice(-1) === "*")
{
self._requiredKeys.push(keyId);
htmlCode += "<option disabled selected>...</option>";
}
// if value is an array, we create a select drop-down menu:
if (Array.isArray(value))
for (const option of value)
{
htmlCode += '<select name="' + key + '" id="' + keyId + '" class="text ui-widget-content' +
' ui-corner-all">';
// if the field is required, we add an empty option and select it:
if (key.slice(-1) === '*')
{
htmlCode += '<option disabled selected>...</option>';
}
for (const option of value)
{
htmlCode += '<option>' + option + '</option>';
}
htmlCode += '</select>';
jQuery('#' + keyId).selectmenu({classes: {}});
htmlCode += "<option>" + option + "</option>";
}
// otherwise we use a single string input:
else /*if (typeof value === 'string')*/
{
htmlCode += '<input type="text" name="' + key + '" id="' + keyId;
htmlCode += '" value="' + value + '" class="text ui-widget-content ui-corner-all">';
}
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>';
// 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,
@ -247,75 +225,62 @@ export class GUI
{
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();
}
}
// 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'),
// 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);
}
@ -330,7 +295,6 @@ export class GUI
};
}
/**
* @callback GUI.onOK
*/
@ -350,14 +314,13 @@ export class GUI
* @param {GUI.onOK} [options.onOK] - function called when the participant presses the OK button
*/
dialog({
message,
warning,
error,
showOK = true,
onOK
} = {})
message,
warning,
error,
showOK = true,
onOK,
} = {})
{
// close the previously opened dialog box, if there is one:
const expDialog = jQuery("#expDialog");
if (expDialog.length)
@ -374,31 +337,30 @@ export class GUI
let titleColour;
// we are displaying an error:
if (typeof error !== 'undefined')
if (typeof error !== "undefined")
{
this._psychoJS.logger.fatal(util.toString(error));
// deal with null error:
if (!error)
{
error = 'Unspecified JavaScript error';
error = "Unspecified JavaScript error";
}
let errorCode = null;
// go through the error stack and look for errorCode if there is one:
let stackCode = '<ul>';
let stackCode = "<ul>";
while (true)
{
if (typeof error === 'object' && 'errorCode' in error)
if (typeof error === "object" && "errorCode" in error)
{
errorCode = error.errorCode;
}
if (typeof error === 'object' && 'context' in error)
if (typeof error === "object" && "context" in error)
{
stackCode += '<li>' + error.context + '</li>';
stackCode += "<li>" + error.context + "</li>";
error = error.error;
}
else
@ -409,11 +371,11 @@ export class GUI
error = error.substring(1, 1000);
}
stackCode += '<li><b>' + error + '</b></li>';
stackCode += "<li><b>" + error + "</b></li>";
break;
}
}
stackCode += '</ul>';
stackCode += "</ul>";
// if we found an errorCode, we replace the stack-based message by a more user-friendly one:
if (errorCode)
@ -427,48 +389,42 @@ export class GUI
htmlCode = '<div id="msgDialog" title="Error">';
htmlCode += '<p class="validateTips">Unfortunately we encountered the following error:</p>';
htmlCode += stackCode;
htmlCode += '<p>Try to run the experiment again. If the error persists, contact the experiment designer.</p>';
htmlCode += '</div>';
htmlCode += "<p>Try to run the experiment again. If the error persists, contact the experiment designer.</p>";
htmlCode += "</div>";
titleColour = 'red';
titleColour = "red";
}
}
// we are displaying a message:
else if (typeof message !== 'undefined')
else if (typeof message !== "undefined")
{
htmlCode = '<div id="msgDialog" title="Message">' +
'<p class="validateTips">' + message + '</p>' +
'</div>';
titleColour = 'green';
htmlCode = '<div id="msgDialog" title="Message">'
+ '<p class="validateTips">' + message + "</p>"
+ "</div>";
titleColour = "green";
}
// we are displaying a warning:
else if (typeof warning !== 'undefined')
else if (typeof warning !== "undefined")
{
htmlCode = '<div id="msgDialog" title="Warning">' +
'<p class="validateTips">' + warning + '</p>' +
'</div>';
titleColour = 'orange';
htmlCode = '<div id="msgDialog" title="Warning">'
+ '<p class="validateTips">' + warning + "</p>"
+ "</div>";
titleColour = "orange";
}
// replace root by the html code:
const dialogElement = document.getElementById('root');
const dialogElement = document.getElementById("root");
dialogElement.innerHTML = htmlCode;
// init and open the dialog box:
this._estimateDialogScalingFactor();
const dialogSize = this._getDialogSize();
const self = this;
jQuery("#msgDialog").dialog({
dialogClass: 'no-close',
dialogClass: "no-close",
width: dialogSize[0],
maxHeight: dialogSize[1],
width: "500",
autoOpen: true,
modal: true,
modal: false,
closeOnEscape: false,
resizable: false,
draggable: false,
@ -476,107 +432,22 @@ export class GUI
buttons: (!showOK) ? [] : [{
id: "buttonOk",
text: "Ok",
click: function ()
click: function()
{
jQuery(this).dialog("destroy").remove();
// execute callback function:
if (typeof onOK !== 'undefined')
if (typeof onOK !== "undefined")
{
onOK();
}
}
},
}],
// open the dialog in the middle of the screen:
open: self._onDialogOpen('#msgDialog'),
})
// change colour of title bar
// change colour of title bar
.prev(".ui-dialog-titlebar").css("background", titleColour);
// when the browser window is resize, we redimension and reposition the dialog:
self._dialogResize('#msgDialog');
}
/**
* Callback triggered when the jQuery UI dialog box is open.
*
* @name module:core.GUI#_onDialogOpen
* @function
* @param {String} dialogId - the dialog ID
* @returns {Function} function setting the dimension and position of the dialog box
* @private
*/
_onDialogOpen(dialogId)
{
const self = this;
return () =>
{
const windowSize = [jQuery(window).width(), jQuery(window).height()];
// note: jQuery(dialogId) is the dialog-content, jQuery(dialogId).parent() is the actual widget
const parent = jQuery(dialogId).parent();
parent.css({
position: 'absolute',
left: Math.max(0, (windowSize[0] - parent.outerWidth()) / 2.0),
top: Math.max(0, (windowSize[1] - parent.outerHeight()) / 2.0)
});
// record width and height difference between dialog content and dialog:
self._contentDelta = [
parent.css('width').slice(0, -2) - jQuery(dialogId).css('width').slice(0, -2),
parent.css('height').slice(0, -2) - jQuery(dialogId).css('height').slice(0, -2)];
};
}
/**
* Ensure that the browser window's resize events redimension and reposition the dialog UI.
*
* @name module:core.GUI#_dialogResize
* @function
* @param {String} dialogId - the dialog ID
* @private
*/
_dialogResize(dialogId)
{
const self = this;
jQuery(window).resize(function ()
{
const parent = jQuery(dialogId).parent();
const windowSize = [jQuery(window).width(), jQuery(window).height()];
// size (we need to redimension both the dialog and the dialog content):
const dialogSize = self._getDialogSize();
parent.css({
width: dialogSize[0],
maxHeight: dialogSize[1]
});
const isDifferent = self._estimateDialogScalingFactor();
if (!isDifferent)
{
jQuery(dialogId).css({
width: dialogSize[0] - self._contentDelta[0],
maxHeight: dialogSize[1] - self._contentDelta[1]
});
}
// position:
parent.css({
position: 'absolute',
left: Math.max(0, (windowSize[0] - parent.outerWidth()) / 2.0),
top: Math.max(0, (windowSize[1] - parent.outerHeight()) / 2.0),
});
});
}
/**
* Listener for resource event from the [Server Manager]{@link ServerManager}.
*
@ -587,7 +458,7 @@ export class GUI
*/
_onResourceEvents(signal)
{
this._psychoJS.logger.debug('signal: ' + util.toString(signal));
this._psychoJS.logger.debug("signal: " + util.toString(signal));
// the download of the specified resources has started:
if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCES)
@ -598,20 +469,20 @@ export class GUI
this._progressBarCurrentValue = 0;
}
// all the resources have been downloaded: show the ok button
else if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED)
{
this._allResourcesDownloaded = true;
jQuery("#progressMsg").text('all resources downloaded.');
jQuery("#progressMsg").text("all resources downloaded.");
this._updateOkButtonStatus();
}
// update progress bar:
else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE
|| signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
else if (
signal.message === ServerManager.Event.DOWNLOADING_RESOURCE
|| signal.message === ServerManager.Event.RESOURCE_DOWNLOADED
)
{
if (typeof this._progressBarCurrentValue === 'undefined')
if (typeof this._progressBarCurrentValue === "undefined")
{
this._progressBarCurrentValue = 0;
}
@ -619,16 +490,15 @@ export class GUI
if (signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
{
jQuery("#progressMsg").text('downloaded ' + (this._progressBarCurrentValue / 2) + ' / ' + (this._progressBarMax / 2));
jQuery("#progressMsg").text("downloaded " + (this._progressBarCurrentValue / 2) + " / " + (this._progressBarMax / 2));
}
else
{
jQuery("#progressMsg").text('downloading ' + (this._progressBarCurrentValue / 2) + ' / ' + (this._progressBarMax / 2));
jQuery("#progressMsg").text("downloading " + (this._progressBarCurrentValue / 2) + " / " + (this._progressBarMax / 2));
}
// $("#progressMsg").text(signal.resource + ': downloaded.');
jQuery("#progressbar").progressbar("option", "value", this._progressBarCurrentValue);
}
// unknown message: we just display it
else
{
@ -636,7 +506,6 @@ export class GUI
}
}
/**
* Update the status of the OK button.
*
@ -647,14 +516,17 @@ export class GUI
*/
_updateOkButtonStatus(changeFocus = true)
{
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL || (this._allResourcesDownloaded && this._setRequiredKeys && this._setRequiredKeys.size >= this._requiredKeys.length))
if (
this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL
|| (this._allResourcesDownloaded && this._setRequiredKeys && this._setRequiredKeys.size >= this._requiredKeys.length)
)
{
if (changeFocus)
{
jQuery("#buttonOk").button("option", "disabled", false).focus();
}
else
{
{
jQuery("#buttonOk").button("option", "disabled", false).focus();
}
else
{
jQuery("#buttonOk").button("option", "disabled", false);
}
}
@ -671,61 +543,6 @@ export class GUI
});
}
/**
* 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.
*
@ -741,7 +558,7 @@ export class GUI
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);
}
@ -753,7 +570,6 @@ export class GUI
gui._updateOkButtonStatus(false);
}
/**
* Get a more user-friendly html message.
*
@ -768,91 +584,111 @@ export class GUI
// 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.
*
@ -863,7 +699,6 @@ export class GUI
*/
GUI.dialogMaxSize = [500, 600];
/**
* Dialog window margins.
*

View File

@ -7,11 +7,10 @@
* @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
@ -27,7 +26,7 @@ export class KeyPress
{
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;
@ -37,7 +36,6 @@ export class KeyPress
}
}
/**
* <p>This manager handles all keyboard events. It is a substitute for the keyboard component of EventManager. </p>
*
@ -53,39 +51,35 @@ export class KeyPress
*/
export class Keyboard extends PsychObject
{
constructor({
psychoJS,
bufferSize = 10000,
waitForStart = false,
clock,
autoLog = false,
} = {})
psychoJS,
bufferSize = 10000,
waitForStart = false,
clock,
autoLog = false,
} = {})
{
super(psychoJS);
if (typeof clock === 'undefined')
if (typeof clock === "undefined")
{
clock = new Clock();
} //this._psychoJS.monotonicClock;
} // this._psychoJS.monotonicClock;
this._addAttribute('bufferSize', bufferSize);
this._addAttribute('waitForStart', waitForStart);
this._addAttribute('clock', clock);
this._addAttribute('autoLog', autoLog);
this._addAttribute("bufferSize", bufferSize);
this._addAttribute("waitForStart", waitForStart);
this._addAttribute("clock", clock);
this._addAttribute("autoLog", autoLog);
// start recording key events if need be:
this._addAttribute('status', (waitForStart) ? PsychoJS.Status.NOT_STARTED : PsychoJS.Status.STARTED);
this._addAttribute("status", (waitForStart) ? PsychoJS.Status.NOT_STARTED : PsychoJS.Status.STARTED);
// setup circular buffer:
this.clearEvents();
// add key listeners:
this._addKeyListeners();
}
/**
* Start recording keyboard events.
*
@ -99,7 +93,6 @@ export class Keyboard extends PsychObject
this._status = PsychoJS.Status.STARTED;
}
/**
* Stop recording keyboard events.
*
@ -113,7 +106,6 @@ export class Keyboard extends PsychObject
this._status = PsychoJS.Status.STOPPED;
}
/**
* @typedef Keyboard.KeyEvent
*
@ -139,7 +131,6 @@ export class Keyboard extends PsychObject
return [];
}
// iterate over the buffer, from start to end, and discard the null event:
let filteredEvents = [];
const bufferWrap = (this._bufferLength === this._bufferSize);
@ -157,7 +148,6 @@ export class Keyboard extends PsychObject
return filteredEvents;
}
/**
* Get the list of keys pressed or pushed by the participant.
*
@ -174,12 +164,11 @@ export class Keyboard extends PsychObject
* (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)
{
@ -203,7 +192,7 @@ export class Keyboard extends PsychObject
{
// 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)
@ -250,13 +239,10 @@ export class Keyboard extends PsychObject
{
this._circularBuffer[i] = null;
}
}
}
} while (i !== this._bufferIndex);
// if waitRelease = false, we iterate again over the map of unmatched keydown events:
if (!waitRelease)
{
@ -303,18 +289,15 @@ export class Keyboard extends PsychObject
} 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.
*
@ -333,7 +316,6 @@ export class Keyboard extends PsychObject
this._unmatchedKeydownMap = new Map();
}
/**
* Test whether a list of KeyPress's contains one with a particular name.
*
@ -352,10 +334,9 @@ export class Keyboard extends PsychObject
}
const value = keypressList.find((keypress) => keypress.name === keyName);
return (typeof value !== 'undefined');
return (typeof value !== "undefined");
}
/**
* Add key listeners to the document.
*
@ -368,10 +349,9 @@ export class Keyboard extends PsychObject
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:
@ -398,14 +378,13 @@ export class Keyboard extends PsychObject
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] = {
@ -413,20 +392,19 @@ export class Keyboard extends PsychObject
key: event.key,
pigletKey,
status: Keyboard.KeyStatus.KEY_DOWN,
timestamp
timestamp,
};
self._unmatchedKeydownMap.set(event.code, self._bufferIndex);
self._psychoJS.logger.trace('keydown: ', event.key);
self._psychoJS.logger.trace("keydown: ", event.key);
event.stopPropagation();
});
// add a keyup listener:
window.addEventListener("keyup", (event) =>
// document.addEventListener("keyup", (event) =>
// document.addEventListener("keyup", (event) =>
{
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
@ -440,7 +418,7 @@ export class Keyboard extends PsychObject
let code = event.code;
// take care of legacy Microsoft Edge:
if (typeof code === 'undefined')
if (typeof code === "undefined")
{
code = EventManager.keycode2w3c(event.keyCode);
}
@ -454,28 +432,26 @@ export class Keyboard extends PsychObject
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.
*
@ -485,6 +461,6 @@ export class Keyboard extends PsychObject
* @public
*/
Keyboard.KeyStatus = {
KEY_DOWN: Symbol.for('KEY_DOWN'),
KEY_UP: Symbol.for('KEY_UP')
KEY_DOWN: Symbol.for("KEY_DOWN"),
KEY_UP: Symbol.for("KEY_UP"),
};

View File

@ -7,12 +7,11 @@
* @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),
@ -26,13 +25,12 @@ import {ExperimentHandler} from '../data/ExperimentHandler';
*/
export class Logger
{
constructor(psychoJS, threshold)
{
this._psychoJS = psychoJS;
// browser console logger:
this.consoleLogger = log4javascript.getLogger('psychojs');
this.consoleLogger = log4javascript.getLogger("psychojs");
const appender = new log4javascript.BrowserConsoleAppender();
appender.setLayout(this._customConsoleLayout());
@ -41,7 +39,6 @@ export class Logger
this.consoleLogger.addAppender(appender);
this.consoleLogger.setLevel(threshold);
// server logger:
this._serverLogs = [];
this._serverLevel = Logger.ServerLevel.WARNING;
@ -65,12 +62,10 @@ export class Logger
// throttling message index:
index: 0,
// whether or not the designer has already been warned:
designerWasWarned: false
designerWasWarned: false,
};
}
/**
* Change the logging level.
*
@ -84,8 +79,6 @@ export class Logger
this._serverLevelValue = this._getValue(this._serverLevel);
}
/**
* Log a server message at the EXP level.
*
@ -100,8 +93,6 @@ export class Logger
this.log(msg, Logger.ServerLevel.EXP, time, obj);
}
/**
* Log a server message at the DATA level.
*
@ -116,8 +107,6 @@ export class Logger
this.log(msg, Logger.ServerLevel.DATA, time, obj);
}
/**
* Log a server message.
*
@ -137,7 +126,7 @@ export class Logger
return;
}
if (typeof time === 'undefined')
if (typeof time === "undefined")
{
time = MonotonicClock.getReferenceTime();
}
@ -154,12 +143,10 @@ export class Logger
msg,
level,
time,
obj: util.toString(obj)
obj: util.toString(obj),
});
}
/**
* Check whether or not a log messages must be throttled.
*
@ -181,24 +168,26 @@ export class Logger
// 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,
});
}
@ -207,7 +196,7 @@ export class Logger
this._throttling.index = 0;
}
++ this._throttling.index;
++this._throttling.index;
if (this._throttling.index < this._throttling.factor)
{
// no logging
@ -220,8 +209,10 @@ export class Logger
}
else
{
if (this._throttling.isThrottling &&
(time - this._throttling.startOfThrottling) > this._throttling.minimumDuration)
if (
this._throttling.isThrottling
&& (time - this._throttling.startOfThrottling) > this._throttling.minimumDuration
)
{
this._psychoJS.logger.info(`[time= ${time.toFixed(3)}] Log messages are not throttled any longer.`);
this._throttling.isThrottling = false;
@ -232,8 +223,6 @@ export class Logger
return false;
}
/**
* Flush all server logs to the server.
*
@ -246,39 +235,41 @@ export class Logger
async flush()
{
const response = {
origin: 'Logger.flush',
context: 'when flushing participant\'s logs for experiment: ' + this._psychoJS.config.experiment.fullpath
origin: "Logger.flush",
context: "when flushing participant's logs for experiment: " + this._psychoJS.config.experiment.fullpath,
};
this._psychoJS.logger.info('[PsychoJS] Flush server logs.');
this._psychoJS.logger.info("[PsychoJS] Flush server logs.");
// prepare the formatted logs:
let formattedLogs = '';
let formattedLogs = "";
for (const log of this._serverLogs)
{
let formattedLog = util.toString(log.time) +
'\t' + Symbol.keyFor(log.level) +
'\t' + log.msg;
if (log.obj !== 'undefined')
let formattedLog = util.toString(log.time)
+ "\t" + Symbol.keyFor(log.level)
+ "\t" + log.msg;
if (log.obj !== "undefined")
{
formattedLog += '\t' + log.obj;
formattedLog += "\t" + log.obj;
}
formattedLog += '\n';
formattedLog += "\n";
formattedLogs += formattedLog;
}
// send logs to the server or display them in the console:
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
this._psychoJS.config.experiment.status === 'RUNNING' &&
!this._psychoJS._serverMsg.has('__pilotToken'))
if (
this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER
&& this._psychoJS.config.experiment.status === "RUNNING"
&& !this._psychoJS._serverMsg.has("__pilotToken")
)
{
// if the pako compression library is present, we compress the logs:
if (typeof pako !== 'undefined')
if (typeof pako !== "undefined")
{
try
{
const utf16DeflatedLogs = pako.deflate(formattedLogs, {to: 'string'});
const utf16DeflatedLogs = pako.deflate(formattedLogs, { to: "string" });
// const utf16DeflatedLogs = pako.deflate(unescape(encodeURIComponent(formattedLogs)), {to: 'string'});
const base64DeflatedLogs = btoa(utf16DeflatedLogs);
@ -286,24 +277,22 @@ export class Logger
}
catch (error)
{
console.error('log compression error:', error);
throw Object.assign(response, {error: error});
console.error("log compression error:", error);
throw Object.assign(response, { error: error });
}
}
else
// the pako compression library is not present, we do not compress the logs:
else
{
return await this._psychoJS.serverManager.uploadLog(formattedLogs, false);
}
}
else
{
this._psychoJS.logger.debug('\n' + formattedLogs);
this._psychoJS.logger.debug("\n" + formattedLogs);
}
}
/**
* Create a custom console layout.
*
@ -316,59 +305,59 @@ export class Logger
const detectedBrowser = util.detectBrowser();
const customLayout = new log4javascript.PatternLayout("%p %d{HH:mm:ss.SSS} %f{1} | %m");
customLayout.setCustomField('location', function (layout, loggingReference)
customLayout.setCustomField("location", function(layout, loggingReference)
{
// we throw a fake exception to retrieve the stack trace
try
{
// (0)();
throw Error('fake exception');
throw Error("fake exception");
}
catch (e)
{
const stackEntries = e.stack.replace(/^.*?\n/, '').replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anon}(').split("\n");
const stackEntries = e.stack.replace(/^.*?\n/, "").replace(/(?:\n@:0)?\s+$/m, "").replace(/^\(/gm, "{anon}(").split("\n");
let relevantEntry;
if (detectedBrowser === 'Firefox')
if (detectedBrowser === "Firefox")
{
// look for entry immediately after those of log4javascript:
for (let entry of stackEntries)
{
if (entry.indexOf('log4javascript.min.js') <= 0)
if (entry.indexOf("log4javascript.min.js") <= 0)
{
relevantEntry = entry;
break;
}
}
const buf = relevantEntry.split(':');
const buf = relevantEntry.split(":");
const line = buf[buf.length - 2];
const file = buf[buf.length - 3].split('/').pop();
const method = relevantEntry.split('@')[0];
const file = buf[buf.length - 3].split("/").pop();
const method = relevantEntry.split("@")[0];
return method + ' ' + file + ':' + line;
return method + " " + file + ":" + line;
}
else if (detectedBrowser === 'Safari')
else if (detectedBrowser === "Safari")
{
return 'unknown';
return "unknown";
}
else if (detectedBrowser === 'Chrome')
else if (detectedBrowser === "Chrome")
{
relevantEntry = stackEntries.pop();
let buf = relevantEntry.split(' ');
let buf = relevantEntry.split(" ");
let fileLine = buf.pop();
const method = buf.pop();
buf = fileLine.split(':');
buf = fileLine.split(":");
buf.pop();
const line = buf.pop();
const file = buf.pop().split('/').pop();
const file = buf.pop().split("/").pop();
return method + ' ' + file + ':' + line;
return method + " " + file + ":" + line;
}
else
{
return 'unknown';
return "unknown";
}
}
});
@ -376,8 +365,6 @@ export class Logger
return customLayout;
}
/**
* Get the integer value associated with a logging level.
*
@ -393,8 +380,6 @@ export class Logger
}
}
/**
* Server logging level.
*
@ -406,17 +391,16 @@ export class Logger
* @note These are similar to PsychoPy's logging levels, as defined in logging.py
*/
Logger.ServerLevel = {
CRITICAL: Symbol.for('CRITICAL'),
ERROR: Symbol.for('ERROR'),
WARNING: Symbol.for('WARNING'),
DATA: Symbol.for('DATA'),
EXP: Symbol.for('EXP'),
INFO: Symbol.for('INFO'),
DEBUG: Symbol.for('DEBUG'),
NOTSET: Symbol.for('NOTSET')
CRITICAL: Symbol.for("CRITICAL"),
ERROR: Symbol.for("ERROR"),
WARNING: Symbol.for("WARNING"),
DATA: Symbol.for("DATA"),
EXP: Symbol.for("EXP"),
INFO: Symbol.for("INFO"),
DEBUG: Symbol.for("DEBUG"),
NOTSET: Symbol.for("NOTSET"),
};
/**
* Server logging level values.
*
@ -428,12 +412,12 @@ Logger.ServerLevel = {
* @protected
*/
Logger._ServerLevelValue = {
'CRITICAL': 50,
'ERROR': 40,
'WARNING': 30,
'DATA': 25,
'EXP': 22,
'INFO': 20,
'DEBUG': 10,
'NOTSET': 0
"CRITICAL": 50,
"ERROR": 40,
"WARNING": 30,
"DATA": 25,
"EXP": 22,
"INFO": 20,
"DEBUG": 10,
"NOTSET": 0,
};

View File

@ -7,12 +7,9 @@
* @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>
@ -28,7 +25,7 @@ import * as util from '../util/Util';
*/
export class MinimalStim extends PsychObject
{
constructor({name, win, autoDraw, autoLog} = {})
constructor({ name, win, autoDraw, autoLog } = {})
{
super(win._psychoJS, name);
@ -36,27 +33,25 @@ export class MinimalStim extends PsychObject
this._pixi = undefined;
this._addAttribute(
'win',
"win",
win,
undefined
undefined,
);
this._addAttribute(
'autoDraw',
"autoDraw",
autoDraw,
false
false,
);
this._addAttribute(
'autoLog',
"autoLog",
autoLog,
(typeof win !== 'undefined' && win !== null) ? win.autoLog : false
(typeof win !== "undefined" && win !== null) ? win.autoLog : false,
);
this._needUpdate = false;
this.status = PsychoJS.Status.NOT_STARTED;
}
/**
* Setter for the autoDraw attribute.
*
@ -68,14 +63,13 @@ export class MinimalStim extends PsychObject
*/
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
{
@ -83,8 +77,6 @@ export class MinimalStim extends PsychObject
}
}
/**
* Draw this stimulus on the next frame draw.
*
@ -103,9 +95,9 @@ export class MinimalStim extends PsychObject
{
// 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
{
@ -117,7 +109,7 @@ export class MinimalStim extends PsychObject
{
// the stimulus is already in the list, if it needs to be updated, we remove it
// from the window container, update it, then put it back:
if (this._needUpdate && typeof this._pixi !== 'undefined')
if (this._needUpdate && typeof this._pixi !== "undefined")
{
this.win._rootContainer.removeChild(this._pixi);
this._updateIfNeeded();
@ -129,8 +121,6 @@ export class MinimalStim extends PsychObject
this.status = PsychoJS.Status.STARTED;
}
/**
* Hide this stimulus on the next frame draw.
*
@ -148,7 +138,7 @@ export class MinimalStim extends PsychObject
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);
}
@ -157,8 +147,6 @@ export class MinimalStim extends PsychObject
}
}
/**
* Determine whether an object is inside this stimulus.
*
@ -172,14 +160,12 @@ export class MinimalStim extends PsychObject
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.
*
@ -191,18 +177,16 @@ export class MinimalStim extends PsychObject
*/
release(log = false)
{
this._setAttribute('autoDraw', false, log);
this._setAttribute("autoDraw", false, log);
this.status = PsychoJS.Status.STOPPED;
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.destroy(true);
this._pixi = undefined;
}
}
/**
* Update the stimulus, if necessary.
*
@ -216,9 +200,9 @@ export class MinimalStim extends PsychObject
_updateIfNeeded()
{
throw {
origin: 'MinimalStim._updateIfNeeded',
context: 'when updating stimulus: ' + this._name,
error: 'this method is abstract and should not be called.'
origin: "MinimalStim._updateIfNeeded",
context: "when updating stimulus: " + this._name,
error: "this method is abstract and should not be called.",
};
}
}

View File

@ -8,10 +8,9 @@
* @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>
@ -29,12 +28,11 @@ import * as util from '../util/Util';
*/
export class Mouse extends PsychObject
{
constructor({
name,
win,
autoLog = true
} = {})
name,
win,
autoLog = true,
} = {})
{
super(win._psychoJS, name);
@ -45,15 +43,14 @@ export class Mouse extends PsychObject
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.
*
@ -73,12 +70,11 @@ export class Mouse extends PsychObject
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.
@ -90,7 +86,7 @@ export class Mouse extends PsychObject
*/
getRel()
{
if (typeof this._lastPos === 'undefined')
if (typeof this._lastPos === "undefined")
{
return this.getPos();
}
@ -103,7 +99,6 @@ export class Mouse extends PsychObject
}
}
/**
* Get the travel of the mouse scroll wheel since the last call to getWheelRel.
*
@ -121,13 +116,12 @@ export class Mouse extends PsychObject
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.
*
@ -153,7 +147,6 @@ export class Mouse extends PsychObject
}
}
/**
* Helper method for checking whether a stimulus has had any button presses within bounds.
*
@ -170,14 +163,14 @@ export class Mouse extends PsychObject
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);
@ -187,23 +180,23 @@ export class Mouse extends PsychObject
// 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);
}
@ -211,7 +204,6 @@ export class Mouse extends PsychObject
return false;
}
/**
* Determine whether the mouse has moved beyond a certain distance.
*
@ -240,24 +232,26 @@ export class Mouse extends PsychObject
mouseMoved(distance, reset = false)
{
// make sure that _lastPos is defined:
if (typeof this._lastPos === 'undefined')
if (typeof this._lastPos === "undefined")
{
this.getPos();
}
this._prevPos = this._lastPos.slice();
this.getPos();
if (typeof reset === 'boolean' && reset == false)
if (typeof reset === "boolean" && reset == false)
{
if (typeof distance === 'undefined')
if (typeof distance === "undefined")
{
return (this._prevPos[0] != this._lastPos[0]) || (this._prevPos[1] != this._lastPos[1]);
}
else
{
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 (this._prevPos[0] + distance[0] - this._lastPos[0] > 0.0)
@ -271,21 +265,18 @@ export class Mouse extends PsychObject
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
@ -294,36 +285,37 @@ export class Mouse extends PsychObject
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.
*
@ -337,7 +329,6 @@ export class Mouse extends PsychObject
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
}
/**
* Reset the clocks associated to the given mouse buttons.
*
@ -355,7 +346,4 @@ export class Mouse extends PsychObject
mouseInfo.buttons.times[b] = 0.0;
}
}
}

View File

@ -8,19 +8,18 @@
* @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 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>
*
@ -31,7 +30,6 @@ import * as util from '../util/Util';
*/
export class PsychoJS
{
/**
* Properties
*/
@ -116,17 +114,16 @@ export class PsychoJS
// return this._shelf;
// }
/**
* @constructor
* @public
*/
constructor({
debug = true,
collectIP = false,
hosts = [],
topLevelStatus = true
} = {})
debug = true,
collectIP = false,
hosts = [],
topLevelStatus = true,
} = {})
{
// logging:
this._logger = new Logger(this, (debug) ? log4javascript.Level.DEBUG : log4javascript.Level.INFO);
@ -134,7 +131,7 @@ export class PsychoJS
// detect the browser:
this._browser = util.detectBrowser();
this.logger.info('[PsychoJS] Detected browser:', this._browser);
this.logger.info("[PsychoJS] Detected browser:", this._browser);
// core clock:
this._monotonicClock = new MonotonicClock();
@ -142,11 +139,11 @@ export class PsychoJS
// managers:
this._eventManager = new EventManager(this);
this._serverManager = new ServerManager({
psychoJS: this
psychoJS: this,
});
// add the pavlovia server to the list of hosts:
const hostsWithPavlovia = new Set([...hosts, 'https://pavlovia.org/run/', 'https://run.pavlovia.org/']);
const hostsWithPavlovia = new Set([...hosts, "https://pavlovia.org/run/", "https://run.pavlovia.org/"]);
this._hosts = Array.from(hostsWithPavlovia);
// GUI:
@ -178,14 +175,13 @@ export class PsychoJS
this._makeStatusTopLevel();
}
this.logger.info('[PsychoJS] Initialised.');
this.logger.info('[PsychoJS] @version 2021.2.x');
this.logger.info("[PsychoJS] Initialised.");
this.logger.info("[PsychoJS] @version 2021.2.0");
// hide the initialisation message:
jQuery('#root').addClass('is-ready');
jQuery("#root").addClass("is-ready");
}
/**
* Get the experiment's environment.
*
@ -193,14 +189,13 @@ export class PsychoJS
*/
getEnvironment()
{
if (typeof this._config === 'undefined')
if (typeof this._config === "undefined")
{
return undefined;
}
return this._config.environment;
}
/**
* Open a PsychoJS Window.
*
@ -220,22 +215,22 @@ export class PsychoJS
* @public
*/
openWindow({
name,
fullscr,
color,
units,
waitBlanking,
autoLog
} = {})
name,
fullscr,
color,
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.",
};
}
@ -246,11 +241,10 @@ export class PsychoJS
color,
units,
waitBlanking,
autoLog
autoLog,
});
}
/**
* Set the completion and cancellation URL to which the participant will be redirect at the end of the experiment.
*
@ -263,7 +257,6 @@ export class PsychoJS
this._cancellationUrl = cancellationUrl;
}
/**
* Schedule a task.
*
@ -273,12 +266,11 @@ export class PsychoJS
*/
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
@ -293,12 +285,11 @@ export class PsychoJS
*/
scheduleCondition(condition, thenScheduler, elseScheduler)
{
this.logger.debug('schedule condition: ', condition.toString().substring(0, 50), '...');
this.logger.debug("schedule condition: ", condition.toString().substring(0, 50), "...");
this._scheduler.addConditional(condition, thenScheduler, elseScheduler);
}
/**
* Start the experiment.
*
@ -321,11 +312,11 @@ export class PsychoJS
* @async
* @public
*/
async start({configURL = 'config.json', expName = 'UNKNOWN', expInfo = {}, resources = []} = {})
async start({ configURL = "config.json", expName = "UNKNOWN", expInfo = {}, resources = [] } = {})
{
this.logger.debug();
const response = {origin: 'PsychoJS.start', context: 'when starting the experiment'};
const response = { origin: "PsychoJS.start", context: "when starting the experiment" };
try
{
@ -340,24 +331,24 @@ export class PsychoJS
else
{
this._IP = {
IP: 'X',
hostname: 'X',
city: 'X',
region: 'X',
country: 'X',
location: 'X'
IP: "X",
hostname: "X",
city: "X",
region: "X",
country: "X",
location: "X",
};
}
// setup the experiment handler:
this._experiment = new ExperimentHandler({
psychoJS: this,
extraInfo: expInfo
extraInfo: expInfo,
});
// setup the logger:
//my.logger.console.setLevel(psychoJS.logging.WARNING);
//my.logger.server.set({'level':psychoJS.logging.WARNING, 'experimentInfo': my.expInfo});
// my.logger.console.setLevel(psychoJS.logging.WARNING);
// my.logger.server.set({'level':psychoJS.logging.WARNING, 'experimentInfo': my.expInfo});
// if the experiment is running on the server:
if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
@ -372,54 +363,49 @@ export class PsychoJS
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);
// 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.
*
@ -439,8 +425,8 @@ export class PsychoJS
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
@ -450,12 +436,10 @@ export class PsychoJS
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.
@ -465,9 +449,9 @@ export class PsychoJS
*/
importAttributes(obj)
{
this.logger.debug('import attributes from: ', util.toString(obj));
this.logger.debug("import attributes from: ", util.toString(obj));
if (typeof obj === 'undefined')
if (typeof obj === "undefined")
{
return;
}
@ -479,7 +463,6 @@ export class PsychoJS
}
}
/**
* Close everything and exit nicely at the end of the experiment,
* potentially redirecting to one of the URLs previously specified by setRedirectUrls.
@ -493,9 +476,9 @@ export class PsychoJS
* @async
* @public
*/
async quit({message, isCompleted = false} = {})
async quit({ message, isCompleted = false } = {})
{
this.logger.info('[PsychoJS] Quit.');
this.logger.info("[PsychoJS] Quit.");
this._experiment.experimentEnded = true;
this._status = PsychoJS.Status.FINISHED;
@ -508,17 +491,17 @@ export class PsychoJS
// 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();
@ -532,8 +515,8 @@ export class PsychoJS
}
// 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,
@ -552,26 +535,24 @@ export class PsychoJS
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.
*
@ -583,8 +564,8 @@ export class PsychoJS
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
@ -600,51 +581,50 @@ export class PsychoJS
const serverResponse = await this._serverManager.getConfiguration(configURL);
this._config = serverResponse.config;
// update the configuration for legacy experiments, which had a psychoJsManager
// 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)
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,
@ -652,8 +632,8 @@ export class PsychoJS
name,
saveFormat: ExperimentHandler.SaveFormat.CSV,
saveIncompleteResults: true,
keys: []
}
keys: [],
},
};
}
@ -661,24 +641,22 @@ export class PsychoJS
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.
*
@ -688,33 +666,32 @@ export class PsychoJS
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.
*
@ -722,44 +699,46 @@ export class PsychoJS
*/
_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)
{
// No stack? Error thrown by PsychoJS; stringify whole error
document.body.setAttribute('data-error', JSON.stringify(error?.reason));
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
@ -771,10 +750,8 @@ export class PsychoJS
window[status] = PsychoJS.Status[status];
}
}
}
/**
* PsychoJS status.
*
@ -787,14 +764,13 @@ export class PsychoJS
* STOPPED in PsychoJS, but the Symbol is the same as that of FINISHED.
*/
PsychoJS.Status = {
NOT_CONFIGURED: Symbol.for('NOT_CONFIGURED'),
CONFIGURING: Symbol.for('CONFIGURING'),
CONFIGURED: Symbol.for('CONFIGURED'),
NOT_STARTED: Symbol.for('NOT_STARTED'),
STARTED: Symbol.for('STARTED'),
PAUSED: Symbol.for('PAUSED'),
FINISHED: Symbol.for('FINISHED'),
STOPPED: Symbol.for('FINISHED'), //Symbol.for('STOPPED')
ERROR: Symbol.for('ERROR')
NOT_CONFIGURED: Symbol.for("NOT_CONFIGURED"),
CONFIGURING: Symbol.for("CONFIGURING"),
CONFIGURED: Symbol.for("CONFIGURED"),
NOT_STARTED: Symbol.for("NOT_STARTED"),
STARTED: Symbol.for("STARTED"),
PAUSED: Symbol.for("PAUSED"),
FINISHED: Symbol.for("FINISHED"),
STOPPED: Symbol.for("FINISHED"), // Symbol.for('STOPPED')
ERROR: Symbol.for("ERROR"),
};

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,11 @@
* @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 { 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>
@ -32,7 +32,6 @@ import {Logger} from "./Logger";
*/
export class Window extends PsychObject
{
/**
* Getter for monitorFramePeriod.
*
@ -46,14 +45,14 @@ export class Window extends PsychObject
}
constructor({
psychoJS,
name,
fullscr = false,
color = new Color('black'),
units = 'pix',
waitBlanking = false,
autoLog = true
} = {})
psychoJS,
name,
fullscr = false,
color = new Color("black"),
units = "pix",
waitBlanking = false,
autoLog = true,
} = {})
{
super(psychoJS, name);
@ -63,13 +62,12 @@ export class Window extends PsychObject
// 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);
this._addAttribute("units", units);
this._addAttribute("waitBlanking", waitBlanking);
this._addAttribute("autoLog", autoLog);
this._addAttribute("size", []);
// setup PIXI:
this._setupPixi();
@ -78,15 +76,14 @@ export class Window extends PsychObject
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;
@ -96,14 +93,12 @@ export class Window extends PsychObject
}
});
if (this._autoLog)
{
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
}
}
/**
* Close the window.
*
@ -126,21 +121,20 @@ export class Window extends PsychObject
}
// destroy the renderer and the WebGL context:
if (typeof this._renderer.gl !== 'undefined')
if (typeof this._renderer.gl !== "undefined")
{
const extension = this._renderer.gl.getExtension('WEBGL_lose_context');
const extension = this._renderer.gl.getExtension("WEBGL_lose_context");
extension.loseContext();
}
this._renderer.destroy();
window.removeEventListener('resize', this._resizeCallback);
window.removeEventListener('orientationchange', this._resizeCallback);
window.removeEventListener("resize", this._resizeCallback);
window.removeEventListener("orientationchange", this._resizeCallback);
this._renderer = null;
}
/**
* Estimate the frame rate.
*
@ -158,7 +152,6 @@ export class Window extends PsychObject
return fps;
}
/**
* Take the browser full screen if possible.
*
@ -173,39 +166,37 @@ export class Window extends PsychObject
// test whether the window is already fullscreen.
// this._windowAlreadyInFullScreen = (!window.screenTop && !window.screenY);
if (this.fullscr/* && !this._windowAlreadyInFullScreen*/)
if (this.fullscr /* && !this._windowAlreadyInFullScreen*/)
{
this._psychoJS.logger.debug('Resizing Window: ', this._name, 'to full screen.');
this._psychoJS.logger.debug("Resizing Window: ", this._name, "to full screen.");
if (typeof document.documentElement.requestFullscreen === 'function')
if (typeof document.documentElement.requestFullscreen === "function")
{
document.documentElement.requestFullscreen()
.catch(() =>
{
this.psychoJS.logger.warn('Unable to go fullscreen.');
this.psychoJS.logger.warn("Unable to go fullscreen.");
});
}
else if (typeof document.documentElement.mozRequestFullScreen === 'function')
else if (typeof document.documentElement.mozRequestFullScreen === "function")
{
document.documentElement.mozRequestFullScreen();
}
else if (typeof document.documentElement.webkitRequestFullscreen === 'function')
else if (typeof document.documentElement.webkitRequestFullscreen === "function")
{
document.documentElement.webkitRequestFullscreen();
}
else if (typeof document.documentElement.msRequestFullscreen === 'function')
else if (typeof document.documentElement.msRequestFullscreen === "function")
{
document.documentElement.msRequestFullscreen();
}
else
{
this.psychoJS.logger.warn('Unable to go fullscreen.');
this.psychoJS.logger.warn("Unable to go fullscreen.");
}
}
}
/**
* Take the browser back from full screen if needed.
*
@ -217,37 +208,35 @@ export class Window extends PsychObject
{
if (this.fullscr)
{
this._psychoJS.logger.debug('Resizing Window: ', this._name, 'back from full screen.');
this._psychoJS.logger.debug("Resizing Window: ", this._name, "back from full screen.");
if (typeof document.exitFullscreen === 'function')
if (typeof document.exitFullscreen === "function")
{
document.exitFullscreen()
.catch(() =>
{
this.psychoJS.logger.warn('Unable to close fullscreen.');
this.psychoJS.logger.warn("Unable to close fullscreen.");
});
}
else if (typeof document.mozCancelFullScreen === 'function')
else if (typeof document.mozCancelFullScreen === "function")
{
document.mozCancelFullScreen();
}
else if (typeof document.webkitExitFullscreen === 'function')
else if (typeof document.webkitExitFullscreen === "function")
{
document.webkitExitFullscreen();
}
else if (typeof document.msExitFullscreen === 'function')
else if (typeof document.msExitFullscreen === "function")
{
document.msExitFullscreen();
}
else
{
this.psychoJS.logger.warn('Unable to close fullscreen.');
this.psychoJS.logger.warn("Unable to close fullscreen.");
}
}
}
/**
* Log a message.
*
@ -262,15 +251,14 @@ export class Window extends PsychObject
* @param {Object} [obj] the object associated with the message
*/
logOnFlip({
msg,
level = Logger.ServerLevel.EXP,
obj
} = {})
msg,
level = Logger.ServerLevel.EXP,
obj,
} = {})
{
this._msgToBeLogged.push({msg, level, obj});
this._msgToBeLogged.push({ msg, level, obj });
}
/**
* Callback function for callOnFlip.
*
@ -291,10 +279,9 @@ export class Window extends PsychObject
*/
callOnFlip(flipCallback, ...flipCallbackArgs)
{
this._flipCallbacks.push({function: flipCallback, arguments: flipCallbackArgs});
this._flipCallbacks.push({ function: flipCallback, arguments: flipCallbackArgs });
}
/**
* Render the stimuli onto the canvas.
*
@ -309,13 +296,12 @@ export class Window extends PsychObject
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/]
@ -331,7 +317,7 @@ export class Window extends PsychObject
// call the callOnFlip functions and remove them:
for (let callback of this._flipCallbacks)
{
callback['function'](...callback['arguments']);
callback["function"](...callback["arguments"]);
}
this._flipCallbacks = [];
@ -342,7 +328,6 @@ export class Window extends PsychObject
this._refresh();
}
/**
* Update this window, if need be.
*
@ -367,7 +352,6 @@ export class Window extends PsychObject
}
}
/**
* Recompute this window's draw list and _container children for the next animation frame.
*
@ -383,7 +367,7 @@ export class Window extends PsychObject
// update it, then put it back
for (const stimulus of this._drawList)
{
if (stimulus._needUpdate && typeof stimulus._pixi !== 'undefined')
if (stimulus._needUpdate && typeof stimulus._pixi !== "undefined")
{
this._rootContainer.removeChild(stimulus._pixi);
stimulus._updateIfNeeded();
@ -392,7 +376,6 @@ export class Window extends PsychObject
}
}
/**
* Force an update of all stimuli in this window's drawlist.
*
@ -412,7 +395,6 @@ export class Window extends PsychObject
this._refresh();
}
/**
* Setup PIXI.
*
@ -434,10 +416,10 @@ export class Window extends PsychObject
width: this._size[0],
height: this._size[1],
backgroundColor: this.color.int,
resolution: window.devicePixelRatio
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:
@ -459,11 +441,10 @@ export class Window extends PsychObject
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.
@ -476,17 +457,17 @@ export class Window extends PsychObject
*/
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
@ -496,7 +477,6 @@ export class Window extends PsychObject
pjsWindow._rootContainer.scale.y = -1;
}
/**
* Send all logged messages to the {@link Logger}.
*
@ -514,5 +494,4 @@ export class Window extends PsychObject
this._msgToBeLogged = [];
}
}

View File

@ -7,7 +7,6 @@
* @license Distributed under the terms of the MIT License
*/
/**
* <p>This mixin implements various unit-handling measurement methods.</p>
*
@ -19,16 +18,15 @@
* @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
@ -38,47 +36,46 @@ export let WindowMixin = (superclass) => class extends superclass
* @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'
};
_getLengthPix(length, integerCoordinates = false)
{
let response = {
origin: "WindowMixin._getLengthPix",
context: "when converting a length from stimulus unit to pixel units",
};
let length_px;
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 (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;
}
}
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
@ -87,36 +84,35 @@ export let WindowMixin = (superclass) => class extends superclass
* @param {number} length_px - the length in pixel units
* @return {number} - the length in stimulus units
*/
_getLengthUnits(length_px)
{
let response = {
origin: 'WindowMixin._getLengthUnits',
context: 'when converting a length from pixel unit to stimulus units'
};
_getLengthUnits(length_px)
{
let response = {
origin: "WindowMixin._getLengthUnits",
context: "when converting a length from pixel unit to stimulus units",
};
if (this._units === 'pix')
{
return length_px;
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 });
}
}
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
@ -125,35 +121,35 @@ export let WindowMixin = (superclass) => class extends superclass
* @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'
};
_getHorLengthPix(length)
{
let response = {
origin: "WindowMixin._getHorLengthPix",
context: "when converting a length from stimulus units to pixel units",
};
if (this._units === 'pix')
{
return length;
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 });
}
}
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
@ -162,32 +158,31 @@ export let WindowMixin = (superclass) => class extends superclass
* @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'
};
_getVerLengthPix(length)
{
let response = {
origin: "WindowMixin._getVerLengthPix",
context: "when converting a length from pixel unit to stimulus units",
};
if (this._units === 'pix')
{
return length;
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 });
}
}
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});
}
}
};
};

View File

@ -1,10 +1,10 @@
export * from './EventManager.js';
export * from './GUI.js';
export * from './Keyboard.js';
export * from './Logger.js';
export * from './MinimalStim.js';
export * from './Mouse.js';
export * from './PsychoJS.js';
export * from './ServerManager.js';
export * from './Window.js';
export * from './WindowMixin.js';
export * from "./EventManager.js";
export * from "./GUI.js";
export * from "./Keyboard.js";
export * from "./Logger.js";
export * from "./MinimalStim.js";
export * from "./Mouse.js";
export * from "./PsychoJS.js";
export * from "./ServerManager.js";
export * from "./Window.js";
export * from "./WindowMixin.js";

View File

@ -7,12 +7,10 @@
* @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
@ -29,7 +27,6 @@ import * as util from '../util/Util';
*/
export class ExperimentHandler extends PsychObject
{
/**
* Getter for experimentEnded.
*
@ -54,7 +51,6 @@ export class ExperimentHandler extends PsychObject
this._experimentEnded = ended;
}
/**
* Legacy experiment getters.
*/
@ -68,16 +64,15 @@ export class ExperimentHandler extends PsychObject
return this._trialsData;
}
constructor({
psychoJS,
name,
extraInfo
} = {})
psychoJS,
name,
extraInfo,
} = {})
{
super(psychoJS, name);
this._addAttribute('extraInfo', extraInfo);
this._addAttribute("extraInfo", extraInfo);
// loop handlers:
this._loops = [];
@ -91,7 +86,6 @@ export class ExperimentHandler extends PsychObject
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>
@ -106,7 +100,6 @@ export class ExperimentHandler extends PsychObject
return (Object.keys(this._currentTrialData).length > 0);
}
/**
* Add a loop.
*
@ -125,7 +118,6 @@ export class ExperimentHandler extends PsychObject
loop.experimentHandler = this;
}
/**
* Remove the given loop from the list of unfinished loops, e.g. when it has completed.
*
@ -143,7 +135,6 @@ export class ExperimentHandler extends PsychObject
}
}
/**
* Add the key/value pair.
*
@ -172,7 +163,6 @@ export class ExperimentHandler extends PsychObject
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.
@ -184,7 +174,7 @@ export class ExperimentHandler extends PsychObject
*/
nextEntry(snapshots)
{
if (typeof snapshots !== 'undefined')
if (typeof snapshots !== "undefined")
{
// turn single snapshot into a one-element array:
if (!Array.isArray(snapshots))
@ -203,7 +193,6 @@ export class ExperimentHandler extends PsychObject
}
}
}
}
// this is to support legacy generated JavaScript code and does not properly handle
// loops within loops:
@ -236,7 +225,6 @@ export class ExperimentHandler extends PsychObject
this._currentTrialData = {};
}
/**
* Save the results of the experiment.
*
@ -254,11 +242,11 @@ export class ExperimentHandler extends PsychObject
* @param {Array.<Object>} [options.sync] - whether or not to communicate with the server in a synchronous manner
*/
async save({
attributes = [],
sync = false
} = {})
attributes = [],
sync = false,
} = {})
{
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
this._psychoJS.logger.info("[PsychoJS] Save experiment results.");
// (*) get attributes:
if (attributes.length === 0)
@ -286,16 +274,14 @@ export class ExperimentHandler extends PsychObject
}
}
// (*) 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 __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;
const __projectId = (typeof gitlabConfig !== "undefined" && typeof gitlabConfig.projectId !== "undefined") ? gitlabConfig.projectId : undefined;
// (*) save to a .csv file:
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV)
@ -304,23 +290,23 @@ export class ExperimentHandler extends PsychObject
// newlines, etc.
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
// 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 key = __participant + "_" + __experimentName + "_" + __datetime + ".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:
else if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.DATABASE)
{
@ -328,7 +314,7 @@ export class ExperimentHandler extends PsychObject
for (let r = 0; r < this._trialsData.length; r++)
{
let doc = {__projectId, __experimentName, __participant, __session, __datetime};
let doc = { __projectId, __experimentName, __participant, __session, __datetime };
for (let h = 0; h < attributes.length; h++)
{
doc[attributes[h]] = this._trialsData[r][attributes[h]];
@ -338,22 +324,22 @@ export class ExperimentHandler extends PsychObject
}
// 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>
@ -367,20 +353,20 @@ export class ExperimentHandler extends PsychObject
static _getLoopAttributes(loop)
{
// standard trial attributes:
const properties = ['thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'stepSizeCurrent', 'ran', 'order'];
const properties = ["thisRepN", "thisTrialN", "thisN", "thisIndex", "stepSizeCurrent", "ran", "order"];
let attributes = {};
const loopName = loop.name;
for (const loopProperty in loop)
{
if (properties.includes(loopProperty))
{
const key = (loopProperty === 'stepSizeCurrent') ? loopName + '.stepSize' : loopName + '.' + loopProperty;
const key = (loopProperty === "stepSizeCurrent") ? loopName + ".stepSize" : loopName + "." + loopProperty;
attributes[key] = loop[loopProperty];
}
}
// specific trial attributes:
if (typeof loop.getCurrentTrial === 'function')
if (typeof loop.getCurrentTrial === "function")
{
const currentTrial = loop.getCurrentTrial();
for (const trialProperty in currentTrial)
@ -404,7 +390,7 @@ export class ExperimentHandler extends PsychObject
else:
names.append(loopName+'.thisTrial')
vals.append(trial)
// single StairHandler
elif hasattr(loop, 'intensities'):
names.append(loopName+'.intensity')
@ -415,10 +401,8 @@ export class ExperimentHandler extends PsychObject
return attributes;
}
}
/**
* Experiment result format
*
@ -431,15 +415,14 @@ ExperimentHandler.SaveFormat = {
/**
* Results are saved to a .csv file
*/
CSV: Symbol.for('CSV'),
CSV: Symbol.for("CSV"),
/**
* Results are saved to a database
*/
DATABASE: Symbol.for('DATABASE')
DATABASE: Symbol.for("DATABASE"),
};
/**
* Experiment environment.
*
@ -448,6 +431,6 @@ ExperimentHandler.SaveFormat = {
* @public
*/
ExperimentHandler.Environment = {
SERVER: Symbol.for('SERVER'),
LOCAL: Symbol.for('LOCAL')
SERVER: Symbol.for("SERVER"),
LOCAL: Symbol.for("LOCAL"),
};

View File

@ -10,11 +10,11 @@
import {TrialHandler} from "./TrialHandler";
import {TrialHandler} from "./TrialHandler.js";
/**
* <p>A Trial Handler that implements the Quest algorithm for quick measurement of
psychophysical thresholds.QuestHandler relies on the [jsQuest]{@link https://github.com/kurokida/jsQUEST} library, a port of Prof Dennis Pelli's QUEST algorithm by [Daiichiro Kuroki]{@link https://github.com/kurokida}.</p>
psychophysical thresholds. QuestHandler relies on the [jsQuest]{@link https://github.com/kurokida/jsQUEST} library, a port of Prof Dennis Pelli's QUEST algorithm by [Daiichiro Kuroki]{@link https://github.com/kurokida}.</p>
*
* @class module.data.QuestHandler
* @extends TrialHandler

View File

@ -9,11 +9,10 @@
* @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>
@ -31,7 +30,6 @@ import * as util from '../util/Util';
*/
export class TrialHandler extends PsychObject
{
/**
* Getter for experimentHandler.
*
@ -56,7 +54,6 @@ export class TrialHandler extends PsychObject
this._experimentHandler = exp;
}
/**
* @constructor
* @public
@ -64,27 +61,27 @@ export class TrialHandler extends PsychObject
* @todo extraInfo is not taken into account, we use the expInfo of the ExperimentHandler instead
*/
constructor({
psychoJS,
trialList = [undefined],
nReps,
method = TrialHandler.Method.RANDOM,
extraInfo = [],
seed,
name,
autoLog = true
} = {})
psychoJS,
trialList = [undefined],
nReps,
method = TrialHandler.Method.RANDOM,
extraInfo = [],
seed,
name,
autoLog = true,
} = {})
{
super(psychoJS);
this._addAttribute('trialList', trialList);
this._addAttribute('nReps', nReps);
this._addAttribute('method', method);
this._addAttribute('extraInfo', extraInfo);
this._addAttribute('name', name);
this._addAttribute('autoLog', autoLog);
this._addAttribute('seed', seed);
this._addAttribute("trialList", trialList);
this._addAttribute("nReps", nReps);
this._addAttribute("method", method);
this._addAttribute("extraInfo", extraInfo);
this._addAttribute("name", name);
this._addAttribute("autoLog", autoLog);
this._addAttribute("seed", seed);
this._prepareTrialList(trialList);
// number of stimuli
this.nStim = this.trialList.length;
@ -112,7 +109,6 @@ export class TrialHandler extends PsychObject
// array of current snapshots:
this._snapshots = [];
// setup the trial sequence:
this._prepareSequence();
@ -121,18 +117,17 @@ export class TrialHandler extends PsychObject
this._finished = false;
}
/**
* Helps go through each trial in the sequence one by one, mirrors PsychoPy.
*/
next() {
next()
{
const trialIterator = this[Symbol.iterator]();
const { value } = trialIterator.next();
return value;
}
/**
* Iterator over the trial sequence.
*
@ -168,7 +163,7 @@ export class TrialHandler extends PsychObject
if (this.thisRepN >= this.nReps)
{
this.thisTrial = null;
return {done: true};
return { done: true };
}
this.thisIndex = this._trialSequence[this.thisRepN][this.thisTrialN];
@ -181,12 +176,11 @@ export class TrialHandler extends PsychObject
vals = (self.thisRepN, self.thisTrialN, self.thisTrial)
logging.exp(msg % vals, obj=self.thisTrial)*/
return {value: this.thisTrial, done: false};
}
return { value: this.thisTrial, done: false };
},
};
}
/**
* Execute the callback for each trial in the sequence.
*
@ -208,7 +202,6 @@ export class TrialHandler extends PsychObject
}
}
/**
* @typedef {Object} Snapshot
* @property {TrialHandler} handler - the trialHandler
@ -253,12 +246,12 @@ export class TrialHandler extends PsychObject
getCurrentTrial: () => this.getTrial(currentIndex),
getTrial: (index = 0) => this.getTrial(index),
addData: (key, value) => this.addData(key, value)
addData: (key, value) => this.addData(key, value),
};
// add to the snapshots the current trial's attributes:
const currentTrial = this.getCurrentTrial();
const excludedAttributes = ['handler', 'name', 'nStim', 'nRemaining', 'thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'ran', 'finished'];
const excludedAttributes = ["handler", "name", "nStim", "nRemaining", "thisRepN", "thisTrialN", "thisN", "thisIndex", "ran", "finished"];
const trialAttributes = [];
for (const attribute in currentTrial)
{
@ -280,7 +273,6 @@ export class TrialHandler extends PsychObject
return snapshot;
}
/**
* Setter for the seed attribute.
*
@ -289,9 +281,9 @@ export class TrialHandler extends PsychObject
*/
setSeed(seed, log)
{
this._setAttribute('seed', seed, log);
this._setAttribute("seed", seed, log);
if (typeof seed !== 'undefined')
if (typeof seed !== "undefined")
{
this._randomNumberGenerator = seedrandom(seed);
}
@ -301,7 +293,6 @@ export class TrialHandler extends PsychObject
}
}
/**
* Set the internal state of this trial handler from the given snapshot.
*
@ -312,12 +303,11 @@ export class TrialHandler extends PsychObject
static fromSnapshot(snapshot)
{
// if snapshot is undefined, do nothing:
if (typeof snapshot === 'undefined')
if (typeof snapshot === "undefined")
{
return;
}
snapshot.handler.nStim = snapshot.nStim;
snapshot.handler.nTotal = snapshot.nTotal;
snapshot.handler.nRemaining = snapshot.nRemaining;
@ -330,13 +320,12 @@ export class TrialHandler extends PsychObject
snapshot.handler.thisTrial = snapshot.handler.getCurrentTrial();
// 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')
if (name[name.length - 1] === "s")
{
name = name.substr(0, name.length-1);
name = name.substr(0, name.length - 1);
}
name = `this${name[0].toUpperCase()}${name.substr(1)}`;
@ -348,7 +337,6 @@ export class TrialHandler extends PsychObject
window[name] = value;
}
/**
* Getter for the finished attribute.
*
@ -359,7 +347,6 @@ export class TrialHandler extends PsychObject
return this._finished;
}
/**
* Setter for the finished attribute.
*
@ -368,14 +355,13 @@ export class TrialHandler extends PsychObject
set finished(isFinished)
{
this._finished = isFinished;
this._snapshots.forEach( snapshot =>
this._snapshots.forEach((snapshot) =>
{
snapshot.finished = isFinished;
});
}
/**
* Get the trial index.
*
@ -387,7 +373,6 @@ export class TrialHandler extends PsychObject
return this.thisIndex;
}
/**
* Set the trial index.
*
@ -398,7 +383,6 @@ export class TrialHandler extends PsychObject
this.thisIndex = index;
}
/**
* Get the attributes of the trials.
*
@ -424,7 +408,6 @@ export class TrialHandler extends PsychObject
return Object.keys(this.trialList[0]);
}
/**
* Get the current trial.
*
@ -436,7 +419,6 @@ export class TrialHandler extends PsychObject
return this.trialList[this.thisIndex];
}
/**
* Get the nth trial.
*
@ -453,7 +435,6 @@ export class TrialHandler extends PsychObject
return this.trialList[index];
}
/**
* Get the nth future or past trial, without advancing through the trial list.
*
@ -472,7 +453,6 @@ export class TrialHandler extends PsychObject
return this.trialList[this.thisIndex + n];
}
/**
* Get the nth previous trial.
* <p> Note: this is useful for comparisons in n-back tasks.</p>
@ -486,7 +466,6 @@ export class TrialHandler extends PsychObject
return getFutureTrial(-abs(n));
}
/**
* Add a key/value pair to data about the current trial held by the experiment handler
*
@ -502,7 +481,6 @@ export class TrialHandler extends PsychObject
}
}
/**
* Import a list of conditions from a .xls, .xlsx, .odp, or .csv resource.
*
@ -542,8 +520,8 @@ export class TrialHandler extends PsychObject
{
try
{
let resourceExtension = resourceName.split('.').pop();
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(resourceExtension) > -1)
let resourceExtension = resourceName.split(".").pop();
if (["csv", "odp", "xls", "xlsx"].indexOf(resourceExtension) > -1)
{
// (*) read conditions from resource:
const resourceValue = serverManager.getResource(resourceName, true);
@ -552,20 +530,20 @@ export class TrialHandler extends PsychObject
// 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:
@ -574,8 +552,8 @@ export class TrialHandler extends PsychObject
// (*) return the selected conditions as an array of 'object as map':
// [
// {field0: value0-0, field1: value0-1, ...}
// {field0: value1-0, field1: value1-1, ...}
// ...
// {field0: value1-0, field1: value1-1, ...}
// ...
// ]
let trialList = new Array(selectedRows.length - 1);
for (let r = 0; r < selectedRows.length; ++r)
@ -598,7 +576,7 @@ export class TrialHandler extends PsychObject
value = arrayMaybe;
}
if (typeof value === 'string')
if (typeof value === "string")
{
const numberMaybe = Number.parseFloat(value);
@ -610,7 +588,7 @@ export class TrialHandler extends PsychObject
else
{
// Parse doubly escaped line feeds
value = value.replace(/(\n)/g, '\n');
value = value.replace(/(\n)/g, "\n");
}
}
@ -621,23 +599,21 @@ export class TrialHandler extends PsychObject
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.
*
@ -647,16 +623,15 @@ export class TrialHandler extends PsychObject
_prepareTrialList(trialList)
{
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 trialList === "undefined")
{
this.trialList = [undefined];
}
// if trialList is an array, we make sure it is not empty:
else if (Array.isArray(trialList))
{
@ -665,30 +640,27 @@ export class TrialHandler extends PsychObject
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 trialList === "string")
{
this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, 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 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]
@ -711,8 +683,8 @@ export class TrialHandler extends PsychObject
_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 :
@ -722,9 +694,8 @@ export class TrialHandler extends PsychObject
{
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)
{
this._trialSequence = [];
@ -733,7 +704,6 @@ export class TrialHandler extends PsychObject
this._trialSequence.push(util.shuffle(indices.slice(), this._randomNumberGenerator));
}
}
else if (this.method === TrialHandler.Method.FULL_RANDOM)
{
// create a flat sequence with nReps repeats of indices:
@ -755,15 +725,13 @@ export class TrialHandler extends PsychObject
}
else
{
throw Object.assign(response, {error: 'unknown method'});
throw Object.assign(response, { error: "unknown method" });
}
return this._trialSequence;
}
}
/**
* TrialHandler method
*
@ -775,20 +743,20 @@ TrialHandler.Method = {
/**
* 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"),
};

View File

@ -1,7 +1,16 @@
body {
align-items: center;
display: flex;
height: 100vh;
justify-content: center;
margin: 0;
}
/* Project and resource dialogs */
label,
input,
select {
box-sizing: border-box;
display: block;
padding-bottom: 0.5em;
}
@ -10,7 +19,7 @@ input.text,
select.text {
margin-bottom: 1em;
padding: 0.5em;
width: 95%;
width: 100%;
}
fieldset {
@ -32,83 +41,43 @@ a:hover {
}
.progress {
box-sizing: border-box;
padding: 0.5em 0;
}
.logo {
display: block;
margin: 0 auto 1em;
max-width: 100%;
max-height: 20vh;
max-width: 100%;
}
.ui-dialog {
margin: auto;
max-width: 88vw;
position: relative;
}
/* Don't display close button in the top right corner of the box */
.no-close .ui-dialog-titlebar-close {
.ui-dialog.no-close .ui-dialog-titlebar-close {
display: none;
}
.ui-dialog-content {
.ui-dialog .ui-dialog-content {
margin-top: 1em;
max-height: calc(100vh - 12em) !important;
overflow-y: auto;
}
.ui-dialog .ui-dialog-buttonpane {
/* Avoid padding related overflow */
box-sizing: border-box;
}
@media only screen and (max-width: 1080px) {
.ui-widget {
transform: scale(2);
}
.ui-widget .ui-progressbar {
transform: scale(1);
}
.ui-dialog-titlebar .ui-button {
margin-right: 1em;
}
.ui-dialog-titlebar .ui-dialog-titlebar-close {
transform: scale(1);
}
.ui-dialog .ui-dialog-buttonpane {
padding-top: 1em;
}
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button {
transform: scale(1);
}
.ui-dialog .ui-dialog-titlebar {
padding: 1em 2em;
}
}
@media only screen and (max-width: 1080px) and (orientation: landscape) {
.ui-widget {
transform: scale(1.5);
}
.ui-widget .ui-progressbar {
transform: scale(1);
}
.ui-dialog-titlebar .ui-button {
margin-right: 1em;
}
.ui-dialog-titlebar .ui-dialog-titlebar-close {
transform: scale(1);
}
.ui-dialog .ui-dialog-buttonpane {
padding-top: 1em;
}
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button {
transform: scale(1);
}
.ui-dialog .ui-dialog-titlebar {
padding: 1em 2em;
}
}
/* Initialisation message (which will disappear behind the canvas) */
@ -123,8 +92,8 @@ a:hover {
/* Initialisation message for IE11 */
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
#root::after {
font-weight: bold;
color: #a05000;
content: "initialising the experiment... | Internet Explorer / Edge [beta]";
font-weight: bold;
}
}

View File

@ -7,11 +7,10 @@
* @license Distributed under the terms of the MIT License
*/
import {PsychObject} from '../util/PsychObject';
import {PsychoJS} from '../core/PsychoJS';
import {ExperimentHandler} from '../data/ExperimentHandler';
import * as util from '../util/Util';
import { PsychoJS } from "../core/PsychoJS.js";
import { ExperimentHandler } from "../data/ExperimentHandler.js";
import { PsychObject } from "../util/PsychObject.js";
import * as util from "../util/Util.js";
/**
* <p>AudioClip encapsulates an audio recording.</p>
@ -28,20 +27,19 @@ import * as util from '../util/Util';
*/
export class AudioClip extends PsychObject
{
constructor({psychoJS, name, sampleRateHz, format, data, autoLog} = {})
constructor({ psychoJS, name, sampleRateHz, format, data, autoLog } = {})
{
super(psychoJS);
this._addAttribute('name', name, 'audioclip');
this._addAttribute('format', format);
this._addAttribute('sampleRateHz', sampleRateHz);
this._addAttribute('data', data);
this._addAttribute('autoLog', false, autoLog);
this._addAttribute('status', AudioClip.Status.CREATED);
this._addAttribute("name", name, "audioclip");
this._addAttribute("format", format);
this._addAttribute("sampleRateHz", sampleRateHz);
this._addAttribute("data", data);
this._addAttribute("autoLog", false, autoLog);
this._addAttribute("status", AudioClip.Status.CREATED);
// add a volume attribute, for playback:
this._addAttribute('volume', 1.0);
this._addAttribute("volume", 1.0);
if (this._autoLog)
{
@ -52,7 +50,6 @@ export class AudioClip extends PsychObject
this._decodeAudio();
}
/**
* Set the volume of the playback.
*
@ -66,7 +63,6 @@ export class AudioClip extends PsychObject
this._volume = volume;
}
/**
* Start playing the audio clip.
*
@ -76,7 +72,7 @@ export class AudioClip extends PsychObject
*/
async startPlayback()
{
this._psychoJS.logger.debug('request to play the audio clip');
this._psychoJS.logger.debug("request to play the audio clip");
// wait for the decoding to complete:
await this._decodeAudio();
@ -103,7 +99,6 @@ export class AudioClip extends PsychObject
this._source.start();
}
/**
* Stop playing the audio clip.
*
@ -120,7 +115,6 @@ export class AudioClip extends PsychObject
this._source.stop();
}
/**
* Get the duration of the audio clip, in seconds.
*
@ -137,7 +131,6 @@ export class AudioClip extends PsychObject
return this._audioBuffer.duration;
}
/**
* Upload the audio clip to the pavlovia server.
*
@ -147,27 +140,26 @@ export class AudioClip extends PsychObject
*/
upload()
{
this._psychoJS.logger.debug('request to upload the audio clip to pavlovia.org');
this._psychoJS.logger.debug("request to upload the audio clip to pavlovia.org");
// add a format-dependent audio extension to the name:
const filename = this._name + util.extensionFromMimeType(this._format);
// if the audio recording cannot be uploaded, e.g. the experiment is running locally, or
// if it is piloting mode, then we offer the audio clip as a file 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")
)
{
return this.download(filename);
}
// upload the data:
return this._psychoJS.serverManager.uploadAudio(this._data, filename);
return this._psychoJS.serverManager.uploadAudioVideo(this._data, filename);
}
/**
* Offer the audio clip to the participant as a sound file to download.
*
@ -175,9 +167,9 @@ export class AudioClip extends PsychObject
* @function
* @public
*/
download(filename = 'audio.webm')
download(filename = "audio.webm")
{
const anchor = document.createElement('a');
const anchor = document.createElement("a");
anchor.href = window.URL.createObjectURL(this._data);
anchor.download = filename;
document.body.appendChild(anchor);
@ -185,7 +177,6 @@ export class AudioClip extends PsychObject
document.body.removeChild(anchor);
}
/**
* Transcribe the audio clip.
*
@ -196,10 +187,10 @@ export class AudioClip extends PsychObject
* @return {Promise<>} a promise resolving to the transcript and associated
* transcription confidence
*/
async transcribe({engine, languageCode} = {})
async transcribe({ engine, languageCode } = {})
{
const response = {
origin: 'AudioClip.transcribe',
origin: "AudioClip.transcribe",
context: `when transcribing audio clip: ${this._name}`,
};
@ -215,11 +206,11 @@ export class AudioClip extends PsychObject
transcriptionKey = key.value;
}
}
if (typeof transcriptionKey === 'undefined')
if (typeof transcriptionKey === "undefined")
{
throw {
...response,
error: `missing key for engine: ${fullEngineName}`
error: `missing key for engine: ${fullEngineName}`,
};
}
@ -235,13 +226,11 @@ export class AudioClip extends PsychObject
{
throw {
...response,
error: `unsupported speech-to-text engine: ${engine}`
error: `unsupported speech-to-text engine: ${engine}`,
};
}
}
/**
* Transcribe the audio clip using the Google Cloud Speech-To-Text Engine.
*
@ -272,31 +261,31 @@ export class AudioClip extends PsychObject
// query the Google speech-to-text service:
const body = {
config: {
encoding: 'LINEAR16',
encoding: "LINEAR16",
sampleRateHertz: this._sampleRateHz,
languageCode
languageCode,
},
audio: {
content: base64Data
content: base64Data,
},
};
const url = `https://speech.googleapis.com/v1/speech:recognize?key=${transcriptionKey}`;
const response = await fetch(url, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify(body)
body: JSON.stringify(body),
});
// convert the response to json:
const decodedResponse = await response.json();
this._psychoJS.logger.debug('speech.googleapis.com response:', JSON.stringify(decodedResponse));
this._psychoJS.logger.debug("speech.googleapis.com response:", JSON.stringify(decodedResponse));
// TODO deal with more than one results and/or alternatives
if (('results' in decodedResponse) && (decodedResponse.results.length > 0))
if (("results" in decodedResponse) && (decodedResponse.results.length > 0))
{
resolve(decodedResponse.results[0].alternatives[0]);
}
@ -304,21 +293,20 @@ export class AudioClip extends PsychObject
{
// no transcription available:
resolve({
transcript: '',
confidence: -1
transcript: "",
confidence: -1,
});
}
});
}
/**
* Decode the formatted audio data (e.g. webm) into a 32bit float PCM audio buffer.
*
*/
_decodeAudio()
{
this._psychoJS.logger.debug('request to decode the data of the audio clip');
this._psychoJS.logger.debug("request to decode the data of the audio clip");
// if the audio clip is ready, the PCM audio data is available in _audioData, a Float32Array:
if (this._status === AudioClip.Status.READY)
@ -326,12 +314,11 @@ export class AudioClip extends PsychObject
return;
}
// if we are already decoding, wait until the process completed:
if (this._status === AudioClip.Status.DECODING)
{
const self = this;
return new Promise(function (resolve, reject)
return new Promise(function(resolve, reject)
{
self._decodingCallbacks.push(resolve);
@ -339,7 +326,6 @@ export class AudioClip extends PsychObject
}.bind(this));
}
// otherwise, start decoding the input formatted audio data:
this._status = AudioClip.Status.DECODING;
this._audioData = null;
@ -348,7 +334,7 @@ export class AudioClip extends PsychObject
this._decodingCallbacks = [];
this._audioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: this._sampleRateHz
sampleRate: this._sampleRateHz,
});
const reader = new window.FileReader();
@ -383,12 +369,11 @@ export class AudioClip extends PsychObject
reader.onerror = (error) =>
{
// TODO
}
};
reader.readAsArrayBuffer(this._data);
}
/**
* Convert an array buffer to a base64 string.
*
@ -403,63 +388,65 @@ export class AudioClip extends PsychObject
*/
_base64ArrayBuffer(arrayBuffer)
{
let base64 = '';
const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let base64 = "";
const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const bytes = new Uint8Array(arrayBuffer);
const byteLength = bytes.byteLength;
const byteRemainder = byteLength % 3;
const mainLength = byteLength - byteRemainder;
const bytes = new Uint8Array(arrayBuffer);
const byteLength = bytes.byteLength;
const byteRemainder = byteLength % 3;
const mainLength = byteLength - byteRemainder;
let a;
let b;
let c;
let d;
let chunk;
let a;
let b;
let c;
let d;
let chunk;
// Main loop deals with bytes in chunks of 3
for (let i = 0; i < mainLength; i += 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Main loop deals with bytes in chunks of 3
for (let i = 0; i < mainLength; i += 3)
{
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder === 1)
{
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += `${encodings[a]}${encodings[b]}==`;
}
else if (byteRemainder === 2)
{
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += `${encodings[a]}${encodings[b]}${encodings[c]}=`;
}
return base64;
}
// Deal with the remaining bytes and padding
if (byteRemainder === 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += `${encodings[a]}${encodings[b]}==`;
} else if (byteRemainder === 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += `${encodings[a]}${encodings[b]}${encodings[c]}=`;
}
return base64;
}
}
/**
* Recognition engines.
*
@ -472,10 +459,9 @@ AudioClip.Engine = {
/**
* Google Cloud Speech-to-Text.
*/
GOOGLE: Symbol.for('GOOGLE')
GOOGLE: Symbol.for("GOOGLE"),
};
/**
* AudioClip status.
*
@ -484,9 +470,9 @@ AudioClip.Engine = {
* @public
*/
AudioClip.Status = {
CREATED: Symbol.for('CREATED'),
CREATED: Symbol.for("CREATED"),
DECODING: Symbol.for('DECODING'),
DECODING: Symbol.for("DECODING"),
READY: Symbol.for('READY')
READY: Symbol.for("READY"),
};

View File

@ -7,9 +7,8 @@
* @license Distributed under the terms of the MIT License
*/
import {SoundPlayer} from './SoundPlayer';
import {AudioClip} from "./AudioClip";
import { AudioClip } from "./AudioClip.js";
import { SoundPlayer } from "./SoundPlayer.js";
/**
* <p>This class handles the playback of an audio clip, e.g. a microphone recording.</p>
@ -29,28 +28,27 @@ import {AudioClip} from "./AudioClip";
export class AudioClipPlayer extends SoundPlayer
{
constructor({
psychoJS,
audioClip,
startTime = 0,
stopTime = -1,
stereo = true,
volume = 0,
loops = 0
} = {})
psychoJS,
audioClip,
startTime = 0,
stopTime = -1,
stereo = true,
volume = 0,
loops = 0,
} = {})
{
super(psychoJS);
this._addAttribute('audioClip', audioClip);
this._addAttribute('startTime', startTime);
this._addAttribute('stopTime', stopTime);
this._addAttribute('stereo', stereo);
this._addAttribute('loops', loops);
this._addAttribute('volume', volume);
this._addAttribute("audioClip", audioClip);
this._addAttribute("startTime", startTime);
this._addAttribute("stopTime", stopTime);
this._addAttribute("stereo", stereo);
this._addAttribute("loops", loops);
this._addAttribute("volume", volume);
this._currentLoopIndex = -1;
}
/**
* Determine whether this player can play the given sound.
*
@ -73,7 +71,7 @@ export class AudioClipPlayer extends SoundPlayer
stopTime: sound.stopTime,
stereo: sound.stereo,
loops: sound.loops,
volume: sound.volume
volume: sound.volume,
});
return player;
}
@ -82,7 +80,6 @@ export class AudioClipPlayer extends SoundPlayer
return undefined;
}
/**
* Get the duration of the AudioClip, in seconds.
*
@ -96,7 +93,6 @@ export class AudioClipPlayer extends SoundPlayer
return this._audioClip.getDuration();
}
/**
* Set the duration of the audio clip.
*
@ -110,13 +106,12 @@ export class AudioClipPlayer extends SoundPlayer
// TODO
throw {
origin: 'AudioClipPlayer.setDuration',
context: 'when setting the duration of the playback for audio clip player: ' + this._name,
error: 'not implemented yet'
origin: "AudioClipPlayer.setDuration",
context: "when setting the duration of the playback for audio clip player: " + this._name,
error: "not implemented yet",
};
}
/**
* Set the volume of the playback.
*
@ -133,7 +128,6 @@ export class AudioClipPlayer extends SoundPlayer
this._audioClip.setVolume((mute) ? 0.0 : volume);
}
/**
* Set the number of loops.
*
@ -150,7 +144,6 @@ export class AudioClipPlayer extends SoundPlayer
// TODO
}
/**
* Start playing the sound.
*
@ -162,7 +155,7 @@ export class AudioClipPlayer extends SoundPlayer
*/
play(loops, fadeDuration = 17)
{
if (typeof loops !== 'undefined')
if (typeof loops !== "undefined")
{
this.setLoops(loops);
}
@ -176,7 +169,6 @@ export class AudioClipPlayer extends SoundPlayer
this._audioClip.startPlayback();
}
/**
* Stop playing the sound immediately.
*
@ -189,5 +181,4 @@ export class AudioClipPlayer extends SoundPlayer
{
this._audioClip.stopPlayback(fadeDuration);
}
}

View File

@ -7,12 +7,12 @@
* @license Distributed under the terms of the MIT License
*/
import {Clock} from "../util/Clock";
import {PsychObject} from "../util/PsychObject";
import {PsychoJS} from "../core/PsychoJS";
import * as util from '../util/Util';
import {ExperimentHandler} from "../data/ExperimentHandler";
import {AudioClip} from "./AudioClip";
import { PsychoJS } from "../core/PsychoJS.js";
import { ExperimentHandler } from "../data/ExperimentHandler.js";
import { Clock } from "../util/Clock.js";
import { PsychObject } from "../util/PsychObject.js";
import * as util from "../util/Util.js";
import { AudioClip } from "./AudioClip.js";
/**
* <p>This manager handles the recording of audio signal.</p>
@ -29,18 +29,17 @@ import {AudioClip} from "./AudioClip";
*/
export class Microphone extends PsychObject
{
constructor({win, name, format, sampleRateHz, clock, autoLog} = {})
constructor({ win, name, format, sampleRateHz, clock, autoLog } = {})
{
super(win._psychoJS);
this._addAttribute('win', win, undefined);
this._addAttribute('name', name, 'microphone');
this._addAttribute('format', format, 'audio/webm;codecs=opus', this._onChange);
this._addAttribute('sampleRateHz', sampleRateHz, 48000, this._onChange);
this._addAttribute('clock', clock, new Clock());
this._addAttribute('autoLog', autoLog, false);
this._addAttribute('status', PsychoJS.Status.NOT_STARTED);
this._addAttribute("win", win, undefined);
this._addAttribute("name", name, "microphone");
this._addAttribute("format", format, "audio/webm;codecs=opus", this._onChange);
this._addAttribute("sampleRateHz", sampleRateHz, 48000, this._onChange);
this._addAttribute("clock", clock, new Clock());
this._addAttribute("autoLog", autoLog, autoLog);
this._addAttribute("status", PsychoJS.Status.NOT_STARTED);
// prepare the recording:
this._prepareRecording();
@ -51,7 +50,6 @@ export class Microphone extends PsychObject
}
}
/**
* Submit a request to start the recording.
*
@ -68,19 +66,18 @@ export class Microphone extends PsychObject
// with a new recording:
if (this._status === PsychoJS.Status.PAUSED)
{
return this.resume({clear: true});
return this.resume({ clear: true });
}
if (this._status !== PsychoJS.Status.STARTED)
{
this._psychoJS.logger.debug('request to start audio recording');
this._psychoJS.logger.debug("request to start audio recording");
try
{
if (!this._recorder)
{
throw 'the recorder has not been created yet, possibly because the participant has not given the authorisation to record audio';
throw "the recorder has not been created yet, possibly because the participant has not given the authorisation to record audio";
}
this._recorder.start();
@ -96,21 +93,18 @@ export class Microphone extends PsychObject
}
catch (error)
{
this._psychoJS.logger.error('unable to start the audio recording: ' + JSON.stringify(error));
this._psychoJS.logger.error("unable to start the audio recording: " + JSON.stringify(error));
this._status = PsychoJS.Status.ERROR;
throw {
origin: 'Microphone.start',
context: 'when starting the audio recording for microphone: ' + this._name,
error
origin: "Microphone.start",
context: "when starting the audio recording for microphone: " + this._name,
error,
};
}
}
}
/**
* Submit a request to stop the recording.
*
@ -122,14 +116,14 @@ export class Microphone extends PsychObject
* @return {Promise} promise fulfilled when the recording actually stopped, and the recorded
* data was made available
*/
stop({filename} = {})
stop({ filename } = {})
{
if (this._status === PsychoJS.Status.STARTED || this._status === PsychoJS.Status.PAUSED)
{
this._psychoJS.logger.debug('request to stop audio recording');
this._psychoJS.logger.debug("request to stop audio recording");
this._stopOptions = {
filename
filename,
};
// note: calling the stop method of the MediaRecorder will first raise a dataavailable event,
@ -148,7 +142,6 @@ export class Microphone extends PsychObject
}
}
/**
* Submit a request to pause the recording.
*
@ -160,13 +153,13 @@ export class Microphone extends PsychObject
{
if (this._status === PsychoJS.Status.STARTED)
{
this._psychoJS.logger.debug('request to pause audio recording');
this._psychoJS.logger.debug("request to pause audio recording");
try
{
if (!this._recorder)
{
throw 'the recorder has not been created yet, possibly because the participant has not given the authorisation to record audio';
throw "the recorder has not been created yet, possibly because the participant has not given the authorisation to record audio";
}
// note: calling the pause method of the MediaRecorder raises a pause event
@ -182,20 +175,18 @@ export class Microphone extends PsychObject
}
catch (error)
{
self._psychoJS.logger.error('unable to pause the audio recording: ' + JSON.stringify(error));
self._psychoJS.logger.error("unable to pause the audio recording: " + JSON.stringify(error));
this._status = PsychoJS.Status.ERROR;
throw {
origin: 'Microphone.pause',
context: 'when pausing the audio recording for microphone: ' + this._name,
error
origin: "Microphone.pause",
context: "when pausing the audio recording for microphone: " + this._name,
error,
};
}
}
}
/**
* Submit a request to resume the recording.
*
@ -207,17 +198,17 @@ export class Microphone extends PsychObject
* resuming the recording
* @return {Promise} promise fulfilled when the recording actually resumed
*/
resume({clear = false } = {})
resume({ clear = false } = {})
{
if (this._status === PsychoJS.Status.PAUSED)
{
this._psychoJS.logger.debug('request to resume audio recording');
this._psychoJS.logger.debug("request to resume audio recording");
try
{
if (!this._recorder)
{
throw 'the recorder has not been created yet, possibly because the participant has not given the authorisation to record audio';
throw "the recorder has not been created yet, possibly because the participant has not given the authorisation to record audio";
}
// empty the audio buffer is needed:
@ -239,20 +230,18 @@ export class Microphone extends PsychObject
}
catch (error)
{
self._psychoJS.logger.error('unable to resume the audio recording: ' + JSON.stringify(error));
self._psychoJS.logger.error("unable to resume the audio recording: " + JSON.stringify(error));
this._status = PsychoJS.Status.ERROR;
throw {
origin: 'Microphone.resume',
context: 'when resuming the audio recording for microphone: ' + this._name,
error
origin: "Microphone.resume",
context: "when resuming the audio recording for microphone: " + this._name,
error,
};
}
}
}
/**
* Submit a request to flush the recording.
*
@ -264,7 +253,7 @@ export class Microphone extends PsychObject
{
if (this._status === PsychoJS.Status.STARTED || this._status === PsychoJS.Status.PAUSED)
{
this._psychoJS.logger.debug('request to flush audio recording');
this._psychoJS.logger.debug("request to flush audio recording");
// note: calling the requestData method of the MediaRecorder will raise a
// dataavailable event
@ -281,7 +270,6 @@ export class Microphone extends PsychObject
}
}
/**
* Offer the audio recording to the participant as a sound file to download.
*
@ -290,11 +278,11 @@ export class Microphone extends PsychObject
* @public
* @param {string} filename the filename
*/
download(filename = 'audio.webm')
download(filename = "audio.webm")
{
const audioBlob = new Blob(this._audioBuffer);
const anchor = document.createElement('a');
const anchor = document.createElement("a");
anchor.href = window.URL.createObjectURL(audioBlob);
anchor.download = filename;
document.body.appendChild(anchor);
@ -302,7 +290,6 @@ export class Microphone extends PsychObject
document.body.removeChild(anchor);
}
/**
* Upload the audio recording to the pavlovia server.
*
@ -311,10 +298,10 @@ export class Microphone extends PsychObject
* @public
* @param {string} tag an optional tag for the audio file
*/
async upload({tag} = {})
async upload({ tag } = {})
{
// default tag: the name of this Microphone object
if (typeof tag === 'undefined')
if (typeof tag === "undefined")
{
tag = this._name;
}
@ -322,22 +309,22 @@ export class Microphone extends PsychObject
// add a format-dependent audio extension to the tag:
tag += util.extensionFromMimeType(this._format);
// if the audio recording cannot be uploaded, e.g. the experiment is running locally, or
// if it is piloting mode, then we offer the audio recording as a file 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")
)
{
return this.download(tag);
}
// upload the blob:
const audioBlob = new Blob(this._audioBuffer);
return this._psychoJS.serverManager.uploadAudio(audioBlob, tag);
return this._psychoJS.serverManager.uploadAudioVideo(audioBlob, tag);
}
/**
* Get the current audio recording as an AudioClip in the given format.
*
@ -347,27 +334,25 @@ export class Microphone extends PsychObject
* @param {string} tag an optional tag for the audio clip
* @param {boolean} [flush=false] whether or not to first flush the recording
*/
async getRecording({tag, flush = false} = {})
async getRecording({ tag, flush = false } = {})
{
// default tag: the name of this Microphone object
if (typeof tag === 'undefined')
if (typeof tag === "undefined")
{
tag = this._name;
}
const audioClip = new AudioClip({
psychoJS: this._psychoJS,
name: tag,
format: this._format,
sampleRateHz: this._sampleRateHz,
data: new Blob(this._audioBuffer)
data: new Blob(this._audioBuffer),
});
return audioClip;
}
/**
* Callback for changes to the recording settings.
*
@ -389,7 +374,6 @@ export class Microphone extends PsychObject
this.start();
}
/**
* Prepare the recording.
*
@ -409,15 +393,15 @@ export class Microphone extends PsychObject
advanced: [
{
channelCount: 1,
sampleRate: this._sampleRateHz
}
]
}
sampleRate: this._sampleRateHz,
},
],
},
});
// check that the specified format is supported, use default if it is not:
let options;
if (typeof this._format === 'string' && MediaRecorder.isTypeSupported(this._format))
if (typeof this._format === "string" && MediaRecorder.isTypeSupported(this._format))
{
options = { type: this._format };
}
@ -428,7 +412,6 @@ export class Microphone extends PsychObject
this._recorder = new MediaRecorder(stream, options);
// setup the callbacks:
const self = this;
@ -440,7 +423,7 @@ export class Microphone extends PsychObject
self._audioBuffer.length = 0;
self._clock.reset();
self._status = PsychoJS.Status.STARTED;
self._psychoJS.logger.debug('audio recording started');
self._psychoJS.logger.debug("audio recording started");
// resolve the Microphone.start promise:
if (self._startCallback)
@ -453,7 +436,7 @@ export class Microphone extends PsychObject
this._recorder.onpause = () =>
{
self._status = PsychoJS.Status.PAUSED;
self._psychoJS.logger.debug('audio recording paused');
self._psychoJS.logger.debug("audio recording paused");
// resolve the Microphone.pause promise:
if (self._pauseCallback)
@ -466,7 +449,7 @@ export class Microphone extends PsychObject
this._recorder.onresume = () =>
{
self._status = PsychoJS.Status.STARTED;
self._psychoJS.logger.debug('audio recording resumed');
self._psychoJS.logger.debug("audio recording resumed");
// resolve the Microphone.resume promise:
if (self._resumeCallback)
@ -482,7 +465,7 @@ export class Microphone extends PsychObject
// add data to the buffer:
self._audioBuffer.push(data);
self._psychoJS.logger.debug('audio data added to the buffer');
self._psychoJS.logger.debug("audio data added to the buffer");
// resolve the data available promise, if needed:
if (self._dataAvailableCallback)
@ -494,7 +477,7 @@ export class Microphone extends PsychObject
// called upon Microphone.stop(), after data has been made available:
this._recorder.onstop = () =>
{
self._psychoJS.logger.debug('audio recording stopped');
self._psychoJS.logger.debug("audio recording stopped");
self._status = PsychoJS.Status.NOT_STARTED;
// resolve the Microphone.stop promise:
@ -506,7 +489,7 @@ export class Microphone extends PsychObject
// treat stop options if there are any:
// download to a file, immediately offered to the participant:
if (typeof self._stopOptions.filename === 'string')
if (typeof self._stopOptions.filename === "string")
{
self.download(self._stopOptions.filename);
}
@ -516,12 +499,8 @@ export class Microphone extends PsychObject
this._recorder.onerror = (event) =>
{
// TODO
self._psychoJS.logger.error('audio recording error: ' + JSON.stringify(event));
self._psychoJS.logger.error("audio recording error: " + JSON.stringify(event));
self._status = PsychoJS.Status.ERROR;
};
}
}

View File

@ -8,12 +8,11 @@
* @license Distributed under the terms of the MIT License
*/
import {PsychoJS} from '../core/PsychoJS';
import {PsychObject} from '../util/PsychObject';
import {TonePlayer} from './TonePlayer';
import {TrackPlayer} from './TrackPlayer';
import {AudioClipPlayer} from './AudioClipPlayer';
import { PsychoJS } from "../core/PsychoJS.js";
import { PsychObject } from "../util/PsychObject.js";
import { AudioClipPlayer } from "./AudioClipPlayer.js";
import { TonePlayer } from "./TonePlayer.js";
import { TrackPlayer } from "./TrackPlayer.js";
/**
* <p>This class handles sound playing (tones and tracks)</p>
@ -54,35 +53,35 @@ import {AudioClipPlayer} from './AudioClipPlayer';
export class Sound extends PsychObject
{
constructor({
name,
win,
value = 'C',
octave = 4,
secs = 0.5,
startTime = 0,
stopTime = -1,
stereo = true,
volume = 1.0,
loops = 0,
//hamming = true,
autoLog = true
} = {})
name,
win,
value = "C",
octave = 4,
secs = 0.5,
startTime = 0,
stopTime = -1,
stereo = true,
volume = 1.0,
loops = 0,
// hamming = true,
autoLog = true,
} = {})
{
super(win._psychoJS, name);
// the SoundPlayer, e.g. TonePlayer:
this._player = undefined;
this._addAttribute('win', win);
this._addAttribute('value', value);
this._addAttribute('octave', octave);
this._addAttribute('secs', secs);
this._addAttribute('startTime', startTime);
this._addAttribute('stopTime', stopTime);
this._addAttribute('stereo', stereo);
this._addAttribute('volume', volume);
this._addAttribute('loops', loops);
this._addAttribute('autoLog', autoLog);
this._addAttribute("win", win);
this._addAttribute("value", value);
this._addAttribute("octave", octave);
this._addAttribute("secs", secs);
this._addAttribute("startTime", startTime);
this._addAttribute("stopTime", stopTime);
this._addAttribute("stereo", stereo);
this._addAttribute("volume", volume);
this._addAttribute("loops", loops);
this._addAttribute("autoLog", autoLog);
// identify an appropriate player:
this._getPlayer();
@ -90,7 +89,6 @@ export class Sound extends PsychObject
this.status = PsychoJS.Status.NOT_STARTED;
}
/**
* Start playing the sound.
*
@ -107,7 +105,6 @@ export class Sound extends PsychObject
this._player.play(loops);
}
/**
* Stop playing the sound immediately.
*
@ -116,14 +113,13 @@ export class Sound extends PsychObject
* @param {boolean} [options.log= true] - whether or not to log
*/
stop({
log = true
} = {})
log = true,
} = {})
{
this._player.stop();
this.status = PsychoJS.Status.STOPPED;
}
/**
* Get the duration of the sound, in seconds.
*
@ -135,7 +131,6 @@ export class Sound extends PsychObject
return this._player.getDuration();
}
/**
* Set the playing volume of the sound.
*
@ -146,15 +141,14 @@ export class Sound extends PsychObject
*/
setVolume(volume, mute = false, log = true)
{
this._setAttribute('volume', volume, log);
this._setAttribute("volume", volume, log);
if (typeof this._player !== 'undefined')
if (typeof this._player !== "undefined")
{
this._player.setVolume(volume, mute);
}
}
/**
* Set the sound value on demand past initialisation.
*
@ -166,9 +160,9 @@ export class Sound extends PsychObject
{
if (sound instanceof Sound)
{
this._setAttribute('value', sound.value, log);
this._setAttribute("value", sound.value, log);
if (typeof this._player !== 'undefined')
if (typeof this._player !== "undefined")
{
this._player = this._player.constructor.accept(this);
}
@ -178,13 +172,12 @@ export class Sound extends PsychObject
}
throw {
origin: 'Sound.setSound',
context: 'when replacing the current sound',
error: 'invalid input, need an instance of the Sound class.'
origin: "Sound.setSound",
context: "when replacing the current sound",
error: "invalid input, need an instance of the Sound class.",
};
}
/**
* Set the number of loops.
*
@ -194,15 +187,14 @@ export class Sound extends PsychObject
*/
setLoops(loops = 0, log = true)
{
this._setAttribute('loops', loops, log);
this._setAttribute("loops", loops, log);
if (typeof this._player !== 'undefined')
if (typeof this._player !== "undefined")
{
this._player.setLoops(loops);
}
}
/**
* Set the duration (in seconds)
*
@ -212,15 +204,14 @@ export class Sound extends PsychObject
*/
setSecs(secs = 0.5, log = true)
{
this._setAttribute('secs', secs, log);
this._setAttribute("secs", secs, log);
if (typeof this._player !== 'undefined')
if (typeof this._player !== "undefined")
{
this._player.setDuration(secs);
}
}
/**
* Identify the appropriate player for the sound.
*
@ -231,26 +222,24 @@ export class Sound extends PsychObject
_getPlayer()
{
const acceptFns = [
sound => TonePlayer.accept(sound),
sound => TrackPlayer.accept(sound),
sound => AudioClipPlayer.accept(sound)
(sound) => TonePlayer.accept(sound),
(sound) => TrackPlayer.accept(sound),
(sound) => AudioClipPlayer.accept(sound),
];
for (const acceptFn of acceptFns)
{
this._player = acceptFn(this);
if (typeof this._player !== 'undefined')
if (typeof this._player !== "undefined")
{
return this._player;
}
}
throw {
origin: 'SoundPlayer._getPlayer',
context: 'when finding a player for the sound',
error: 'could not find an appropriate player.'
origin: "SoundPlayer._getPlayer",
context: "when finding a player for the sound",
error: "could not find an appropriate player.",
};
}
}

View File

@ -7,8 +7,7 @@
* @license Distributed under the terms of the MIT License
*/
import {PsychObject} from '../util/PsychObject';
import { PsychObject } from "../util/PsychObject.js";
/**
* <p>SoundPlayer is an interface for the sound players, who are responsible for actually playing the sounds, i.e. the tracks or the tones.</p>
@ -25,7 +24,6 @@ export class SoundPlayer extends PsychObject
super(psychoJS);
}
/**
* Determine whether this player can play the given sound.
*
@ -40,13 +38,12 @@ export class SoundPlayer extends PsychObject
static accept(sound)
{
throw {
origin: 'SoundPlayer.accept',
context: 'when evaluating whether this player can play a given sound',
error: 'this method is abstract and should not be called.'
origin: "SoundPlayer.accept",
context: "when evaluating whether this player can play a given sound",
error: "this method is abstract and should not be called.",
};
}
/**
* Start playing the sound.
*
@ -59,13 +56,12 @@ export class SoundPlayer extends PsychObject
play(loops)
{
throw {
origin: 'SoundPlayer.play',
context: 'when starting the playback of a sound',
error: 'this method is abstract and should not be called.'
origin: "SoundPlayer.play",
context: "when starting the playback of a sound",
error: "this method is abstract and should not be called.",
};
}
/**
* Stop playing the sound immediately.
*
@ -77,13 +73,12 @@ export class SoundPlayer extends PsychObject
stop()
{
throw {
origin: 'SoundPlayer.stop',
context: 'when stopping the playback of a sound',
error: 'this method is abstract and should not be called.'
origin: "SoundPlayer.stop",
context: "when stopping the playback of a sound",
error: "this method is abstract and should not be called.",
};
}
/**
* Get the duration of the sound, in seconds.
*
@ -95,13 +90,12 @@ export class SoundPlayer extends PsychObject
getDuration()
{
throw {
origin: 'SoundPlayer.getDuration',
context: 'when getting the duration of the sound',
error: 'this method is abstract and should not be called.'
origin: "SoundPlayer.getDuration",
context: "when getting the duration of the sound",
error: "this method is abstract and should not be called.",
};
}
/**
* Set the duration of the sound, in seconds.
*
@ -113,13 +107,12 @@ export class SoundPlayer extends PsychObject
setDuration(duration_s)
{
throw {
origin: 'SoundPlayer.setDuration',
context: 'when setting the duration of the sound',
error: 'this method is abstract and should not be called.'
origin: "SoundPlayer.setDuration",
context: "when setting the duration of the sound",
error: "this method is abstract and should not be called.",
};
}
/**
* Set the number of loops.
*
@ -132,13 +125,12 @@ export class SoundPlayer extends PsychObject
setLoops(loops)
{
throw {
origin: 'SoundPlayer.setLoops',
context: 'when setting the number of loops',
error: 'this method is abstract and should not be called.'
origin: "SoundPlayer.setLoops",
context: "when setting the number of loops",
error: "this method is abstract and should not be called.",
};
}
/**
* Set the volume of the tone.
*
@ -152,10 +144,9 @@ export class SoundPlayer extends PsychObject
setVolume(volume, mute = false)
{
throw {
origin: 'SoundPlayer.setVolume',
context: 'when setting the volume of the sound',
error: 'this method is abstract and should not be called.'
origin: "SoundPlayer.setVolume",
context: "when setting the volume of the sound",
error: "this method is abstract and should not be called.",
};
}
}

View File

@ -7,9 +7,9 @@
* @license Distributed under the terms of the MIT License
*/
import * as Tone from 'tone';
import {SoundPlayer} from './SoundPlayer';
import * as Tone from "tone";
import { isNumeric } from "../util/Util.js";
import { SoundPlayer } from "./SoundPlayer.js";
/**
* <p>This class handles the playing of tones.</p>
@ -27,23 +27,23 @@ import {SoundPlayer} from './SoundPlayer';
export class TonePlayer extends SoundPlayer
{
constructor({
psychoJS,
note = 'C4',
duration_s = 0.5,
volume = 1.0,
loops = 0,
soundLibrary = TonePlayer.SoundLibrary.TONE_JS,
autoLog = true
} = {})
psychoJS,
note = "C4",
duration_s = 0.5,
volume = 1.0,
loops = 0,
soundLibrary = TonePlayer.SoundLibrary.TONE_JS,
autoLog = true,
} = {})
{
super(psychoJS);
this._addAttribute('note', note);
this._addAttribute('duration_s', duration_s);
this._addAttribute('volume', volume);
this._addAttribute('loops', loops);
this._addAttribute('soundLibrary', soundLibrary);
this._addAttribute('autoLog', autoLog);
this._addAttribute("note", note);
this._addAttribute("duration_s", duration_s);
this._addAttribute("volume", volume);
this._addAttribute("loops", loops);
this._addAttribute("soundLibrary", soundLibrary);
this._addAttribute("autoLog", autoLog);
// initialise the sound library:
this._initSoundLibrary();
@ -57,7 +57,6 @@ export class TonePlayer extends SoundPlayer
}
}
/**
* Determine whether this player can play the given sound.
*
@ -74,39 +73,39 @@ export class TonePlayer extends SoundPlayer
static accept(sound)
{
// if the sound's value is an integer, we interpret it as a frequency:
if ($.isNumeric(sound.value))
if (isNumeric(sound.value))
{
return new TonePlayer({
psychoJS: sound.psychoJS,
note: sound.value,
duration_s: sound.secs,
volume: sound.volume,
loops: sound.loops
loops: sound.loops,
});
}
// if the sound's value is a string, we check whether it is a note:
if (typeof sound.value === 'string')
if (typeof sound.value === "string")
{
// mapping between the PsychoPY notes and the standard ones:
let psychopyToToneMap = new Map();
for (const note of ['A', 'B', 'C', 'D', 'E', 'F', 'G'])
for (const note of ["A", "B", "C", "D", "E", "F", "G"])
{
psychopyToToneMap.set(note, note);
psychopyToToneMap.set(note + 'fl', note + 'b');
psychopyToToneMap.set(note + 'sh', note + '#');
psychopyToToneMap.set(note + "fl", note + "b");
psychopyToToneMap.set(note + "sh", note + "#");
}
// check whether the sound's value is a recognised note:
const note = psychopyToToneMap.get(sound.value);
if (typeof note !== 'undefined')
if (typeof note !== "undefined")
{
return new TonePlayer({
psychoJS: sound.psychoJS,
note: note + sound.octave,
duration_s: sound.secs,
volume: sound.volume,
loops: sound.loops
loops: sound.loops,
});
}
}
@ -115,7 +114,6 @@ export class TonePlayer extends SoundPlayer
return undefined;
}
/**
* Get the duration of the sound.
*
@ -129,7 +127,6 @@ export class TonePlayer extends SoundPlayer
return this.duration_s;
}
/**
* Set the duration of the tone.
*
@ -143,7 +140,6 @@ export class TonePlayer extends SoundPlayer
this.duration_s = duration_s;
}
/**
* Set the number of loops.
*
@ -157,7 +153,6 @@ export class TonePlayer extends SoundPlayer
this._loops = loops;
}
/**
* Set the volume of the tone.
*
@ -173,7 +168,7 @@ export class TonePlayer extends SoundPlayer
if (this._soundLibrary === TonePlayer.SoundLibrary.TONE_JS)
{
if (typeof this._volumeNode !== 'undefined')
if (typeof this._volumeNode !== "undefined")
{
this._volumeNode.mute = mute;
this._volumeNode.volume.value = -60 + volume * 66;
@ -190,7 +185,6 @@ export class TonePlayer extends SoundPlayer
}
}
/**
* Start playing the sound.
*
@ -201,7 +195,7 @@ export class TonePlayer extends SoundPlayer
*/
play(loops)
{
if (typeof loops !== 'undefined')
if (typeof loops !== "undefined")
{
this._loops = loops;
}
@ -222,7 +216,7 @@ export class TonePlayer extends SoundPlayer
playToneCallback = () =>
{
self._webAudioOscillator = self._audioContext.createOscillator();
self._webAudioOscillator.type = 'sine';
self._webAudioOscillator.type = "sine";
self._webAudioOscillator.frequency.value = 440;
self._webAudioOscillator.connect(self._audioContext.destination);
const contextCurrentTime = self._audioContext.currentTime;
@ -236,7 +230,6 @@ export class TonePlayer extends SoundPlayer
{
playToneCallback();
}
// repeat forever:
else if (this.loops === -1)
{
@ -244,22 +237,21 @@ export class TonePlayer extends SoundPlayer
playToneCallback,
this.duration_s,
Tone.now(),
Infinity
Infinity,
);
}
else
// repeat this._loops times:
else
{
this._toneId = Tone.Transport.scheduleRepeat(
playToneCallback,
this.duration_s,
Tone.now(),
this.duration_s * (this._loops + 1)
this.duration_s * (this._loops + 1),
);
}
}
/**
* Stop playing the sound immediately.
*
@ -287,7 +279,6 @@ export class TonePlayer extends SoundPlayer
}
}
/**
* Initialise the sound library.
*
@ -301,24 +292,24 @@ export class TonePlayer extends SoundPlayer
_initSoundLibrary()
{
const response = {
origin: 'TonePlayer._initSoundLibrary',
context: 'when initialising the sound library'
origin: "TonePlayer._initSoundLibrary",
context: "when initialising the sound library",
};
if (this._soundLibrary === TonePlayer.SoundLibrary.TONE_JS)
{
// check that Tone.js is available:
if (typeof Tone === 'undefined')
if (typeof Tone === "undefined")
{
throw Object.assign(response, {
error: "Tone.js is not available. A different sound library must be selected. Please contact the experiment designer."
error: "Tone.js is not available. A different sound library must be selected. Please contact the experiment designer.",
});
}
// start the Tone Transport if it has not started already:
if (typeof Tone !== 'undefined' && Tone.Transport.state !== 'started')
if (typeof Tone !== "undefined" && Tone.Transport.state !== "started")
{
this.psychoJS.logger.info('[PsychoJS] start Tone Transport');
this.psychoJS.logger.info("[PsychoJS] start Tone Transport");
Tone.Transport.start(Tone.now());
// this is necessary to prevent Tone from introducing a delay when triggering a note
@ -329,14 +320,14 @@ export class TonePlayer extends SoundPlayer
// create a synth: we use a triangular oscillator with hardly any envelope:
this._synthOtions = {
oscillator: {
type: 'square' //'triangle'
type: "square", // 'triangle'
},
envelope: {
attack: 0.001, // 1ms
decay: 0.001, // 1ms
decay: 0.001, // 1ms
sustain: 1,
release: 0.001 // 1ms
}
release: 0.001, // 1ms
},
};
this._synth = new Tone.Synth(this._synthOtions);
@ -345,7 +336,7 @@ export class TonePlayer extends SoundPlayer
this._synth.connect(this._volumeNode);
// connect the volume node to the master output:
if (typeof this._volumeNode.toDestination === 'function')
if (typeof this._volumeNode.toDestination === "function")
{
this._volumeNode.toDestination();
}
@ -357,15 +348,15 @@ export class TonePlayer extends SoundPlayer
else
{
// create an AudioContext:
if (typeof this._audioContext === 'undefined')
if (typeof this._audioContext === "undefined")
{
const AudioContext = window.AudioContext || window.webkitAudioContext;
// if AudioContext is not available (e.g. on IE), we throw an exception:
if (typeof AudioContext === 'undefined')
if (typeof AudioContext === "undefined")
{
throw Object.assign(response, {
error: `AudioContext is not available on your browser, ${this._psychoJS.browser}, please contact the experiment designer.`
error: `AudioContext is not available on your browser, ${this._psychoJS.browser}, please contact the experiment designer.`,
});
}
@ -373,15 +364,13 @@ export class TonePlayer extends SoundPlayer
}
}
}
}
/**
*
* @type {{TONE_JS: *, AUDIO_CONTEXT: *}}
*/
TonePlayer.SoundLibrary = {
AUDIO_CONTEXT: Symbol.for('AUDIO_CONTEXT'),
TONE_JS: Symbol.for('TONE_JS')
AUDIO_CONTEXT: Symbol.for("AUDIO_CONTEXT"),
TONE_JS: Symbol.for("TONE_JS"),
};

View File

@ -7,8 +7,7 @@
* @license Distributed under the terms of the MIT License
*/
import {SoundPlayer} from './SoundPlayer';
import { SoundPlayer } from "./SoundPlayer.js";
/**
* <p>This class handles the playback of sound tracks.</p>
@ -30,28 +29,27 @@ import {SoundPlayer} from './SoundPlayer';
export class TrackPlayer extends SoundPlayer
{
constructor({
psychoJS,
howl,
startTime = 0,
stopTime = -1,
stereo = true,
volume = 0,
loops = 0
} = {})
psychoJS,
howl,
startTime = 0,
stopTime = -1,
stereo = true,
volume = 0,
loops = 0,
} = {})
{
super(psychoJS);
this._addAttribute('howl', howl);
this._addAttribute('startTime', startTime);
this._addAttribute('stopTime', stopTime);
this._addAttribute('stereo', stereo);
this._addAttribute('loops', loops);
this._addAttribute('volume', volume);
this._addAttribute("howl", howl);
this._addAttribute("startTime", startTime);
this._addAttribute("stopTime", stopTime);
this._addAttribute("stereo", stereo);
this._addAttribute("loops", loops);
this._addAttribute("volume", volume);
this._currentLoopIndex = -1;
}
/**
* Determine whether this player can play the given sound.
*
@ -66,10 +64,10 @@ export class TrackPlayer extends SoundPlayer
static accept(sound)
{
// if the sound's value is a string, we check whether it is the name of a resource:
if (typeof sound.value === 'string')
if (typeof sound.value === "string")
{
const howl = sound.psychoJS.serverManager.getResource(sound.value);
if (typeof howl !== 'undefined')
if (typeof howl !== "undefined")
{
// build the player:
const player = new TrackPlayer({
@ -79,7 +77,7 @@ export class TrackPlayer extends SoundPlayer
stopTime: sound.stopTime,
stereo: sound.stereo,
loops: sound.loops,
volume: sound.volume
volume: sound.volume,
});
return player;
}
@ -89,7 +87,6 @@ export class TrackPlayer extends SoundPlayer
return undefined;
}
/**
* Get the duration of the sound, in seconds.
*
@ -103,7 +100,6 @@ export class TrackPlayer extends SoundPlayer
return this._howl.duration();
}
/**
* Set the duration of the track.
*
@ -114,14 +110,13 @@ export class TrackPlayer extends SoundPlayer
*/
setDuration(duration_s)
{
if (typeof this._howl !== 'undefined')
if (typeof this._howl !== "undefined")
{
// Unfortunately Howler.js provides duration setting method
this._howl._duration = duration_s;
}
}
/**
* Set the volume of the tone.
*
@ -139,7 +134,6 @@ export class TrackPlayer extends SoundPlayer
this._howl.mute(mute);
}
/**
* Set the number of loops.
*
@ -163,7 +157,6 @@ export class TrackPlayer extends SoundPlayer
}
}
/**
* Start playing the sound.
*
@ -175,7 +168,7 @@ export class TrackPlayer extends SoundPlayer
*/
play(loops, fadeDuration = 17)
{
if (typeof loops !== 'undefined')
if (typeof loops !== "undefined")
{
this.setLoops(loops);
}
@ -184,7 +177,7 @@ export class TrackPlayer extends SoundPlayer
if (loops > 0)
{
const self = this;
this._howl.on('end', (event) =>
this._howl.on("end", (event) =>
{
++this._currentLoopIndex;
if (self._currentLoopIndex > self._loops)
@ -205,7 +198,6 @@ export class TrackPlayer extends SoundPlayer
this._howl.fade(0, this._volume, fadeDuration, this._id);
}
/**
* Stop playing the sound immediately.
*
@ -216,11 +208,11 @@ export class TrackPlayer extends SoundPlayer
*/
stop(fadeDuration = 17)
{
this._howl.once('fade', (id) => {
this._howl.once("fade", (id) =>
{
this._howl.stop(id);
this._howl.off('end');
this._howl.off("end");
});
this._howl.fade(this._howl.volume(), 0, fadeDuration, this._id);
}
}

View File

@ -1,9 +1,9 @@
export * from './Sound.js';
export * from './SoundPlayer.js';
export * from './TonePlayer.js';
export * from './TrackPlayer.js';
export * from "./Sound.js";
export * from "./SoundPlayer.js";
export * from "./TonePlayer.js";
export * from "./TrackPlayer.js";
export * from './Microphone.js';
export * from './AudioClip.js';
export * from './AudioClipPlayer.js';
//export * from './Transcriber.js';
export * from "./AudioClip.js";
export * from "./AudioClipPlayer.js";
export * from "./Microphone.js";
// export * from './Transcriber.js';

View File

@ -7,7 +7,6 @@
* @license Distributed under the terms of the MIT License
*/
/**
* <p>MonotonicClock offers a convenient way to keep track of time during experiments. An experiment can have as many independent clocks as needed, e.g. one to time responses, another one to keep track of stimuli, etc.</p>
*
@ -22,7 +21,6 @@ export class MonotonicClock
this._timeAtLastReset = startTime;
}
/**
* Get the current time on this clock.
*
@ -36,7 +34,6 @@ export class MonotonicClock
return MonotonicClock.getReferenceTime() - this._timeAtLastReset;
}
/**
* Get the current offset being applied to the high resolution timebase used by this Clock.
*
@ -50,7 +47,6 @@ export class MonotonicClock
return this._timeAtLastReset;
}
/**
* Get the time elapsed since the reference point.
*
@ -65,7 +61,6 @@ export class MonotonicClock
// return (new Date().getTime()) / 1000.0 - MonotonicClock._referenceTime;
}
/**
* Get the current timestamp with language-sensitive formatting rules applied.
*
@ -79,18 +74,18 @@ export class MonotonicClock
* @param {object} options - An object with detailed date and time styling information.
* @return {string} The current timestamp in the chosen format.
*/
static getDate(locales = 'en-CA', optionsMaybe)
static getDate(locales = "en-CA", optionsMaybe)
{
const date = new Date();
const options = Object.assign({
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
fractionalSecondDigits: 3
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "numeric",
minute: "numeric",
second: "numeric",
fractionalSecondDigits: 3,
}, optionsMaybe);
const dateTimeFormat = new Intl.DateTimeFormat(locales, options);
@ -114,15 +109,14 @@ export class MonotonicClock
// yyyy-mm-dd, hh:mm:ss.sss
return MonotonicClock.getDate()
// yyyy-mm-dd_hh:mm:ss.sss
.replace(', ', '_')
.replace(", ", "_")
// yyyy-mm-dd_hh[h]mm:ss.sss
.replace(':', 'h')
.replace(":", "h")
// yyyy-mm-dd_hh[h]mm.ss.sss
.replace(':', '.');
.replace(":", ".");
}
}
/**
* The clock's referenceTime is the time when the module was loaded (in seconds).
*
@ -135,7 +129,6 @@ MonotonicClock._referenceTime = performance.now() / 1000.0;
// MonotonicClock._referenceTime = new Date().getTime() / 1000.0;
/**
* <p>Clock is a MonotonicClock that also offers the possibility of being reset.</p>
*
@ -164,7 +157,6 @@ export class Clock extends MonotonicClock
this._timeAtLastReset = MonotonicClock.getReferenceTime() + newTime;
}
/**
* Add more time to the clock's 'start' time (t0).
*
@ -182,7 +174,6 @@ export class Clock extends MonotonicClock
}
}
/**
* <p>CountdownTimer is a clock counts down from the time of last reset.</p.
*
@ -205,7 +196,6 @@ export class CountdownTimer extends Clock
}
}
/**
* Add more time to the clock's 'start' time (t0).
*
@ -222,7 +212,6 @@ export class CountdownTimer extends Clock
this._timeAtLastReset += deltaTime;
}
/**
* Reset the time on the countdown.
*
@ -234,7 +223,7 @@ export class CountdownTimer extends Clock
*/
reset(newTime = undefined)
{
if (typeof newTime == 'undefined')
if (typeof newTime == "undefined")
{
this._timeAtLastReset = MonotonicClock.getReferenceTime() + this._countdown_duration;
}
@ -245,7 +234,6 @@ export class CountdownTimer extends Clock
}
}
/**
* Get the time currently left on the countdown.
*
@ -259,4 +247,3 @@ export class CountdownTimer extends Clock
return this._timeAtLastReset - MonotonicClock.getReferenceTime();
}
}

View File

@ -7,7 +7,6 @@
* @license Distributed under the terms of the MIT License
*/
/**
* <p>This class handles multiple color spaces, and offers various
* static methods for converting colors from one space to another.</p>
@ -32,27 +31,26 @@
*/
export class Color
{
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB)
constructor(obj = "black", colorspace = Color.COLOR_SPACE.RGB)
{
const response = {
origin: 'Color',
context: 'when defining a color'
origin: "Color",
context: "when defining a color",
};
// named color (e.g. 'seagreen') or string hexadecimal representation (e.g. '#FF0000'):
// note: we expect the color space to be RGB
if (typeof obj == 'string')
if (typeof obj == "string")
{
if (colorspace !== Color.COLOR_SPACE.RGB)
{
throw Object.assign(response, {
error: 'the colorspace must be RGB for a named color'
error: "the colorspace must be RGB for a named color",
});
}
// hexademical representation:
if (obj[0] === '#')
if (obj[0] === "#")
{
this._hex = obj;
}
@ -61,7 +59,7 @@ export class Color
{
if (!(obj.toLowerCase() in Color.NAMED_COLORS))
{
throw Object.assign(response, {error: 'unknown named color: ' + obj});
throw Object.assign(response, { error: "unknown named color: " + obj });
}
this._hex = Color.NAMED_COLORS[obj.toLowerCase()];
@ -69,23 +67,21 @@ export class Color
this._rgb = Color.hexToRgb(this._hex);
}
// hexadecimal number representation (e.g. 0xFF0000)
// note: we expect the color space to be RGB
else if (typeof obj == 'number')
else if (typeof obj == "number")
{
if (colorspace !== Color.COLOR_SPACE.RGB)
{
throw Object.assign(response, {
error: 'the colorspace must be RGB for' +
' a' +
' named color'
error: "the colorspace must be RGB for"
+ " a"
+ " named color",
});
}
this._rgb = Color._intToRgb(obj);
}
// array of numbers:
else if (Array.isArray(obj))
{
@ -124,17 +120,15 @@ export class Color
break;
default:
throw Object.assign(response, {error: 'unknown colorspace: ' + colorspace});
throw Object.assign(response, { error: "unknown colorspace: " + colorspace });
}
}
else if (obj instanceof Color)
{
this._rgb = obj._rgb.slice();
}
}
/**
* Get the [0,1] RGB triplet equivalent of this Color.
*
@ -148,7 +142,6 @@ export class Color
return this._rgb;
}
/**
* Get the [0,255] RGB triplet equivalent of this Color.
*
@ -162,7 +155,6 @@ export class Color
return [Math.round(this._rgb[0] * 255.0), Math.round(this._rgb[1] * 255.0), Math.round(this._rgb[2] * 255.0)];
}
/**
* Get the hexadecimal color code equivalent of this Color.
*
@ -173,7 +165,7 @@ export class Color
*/
get hex()
{
if (typeof this._hex === 'undefined')
if (typeof this._hex === "undefined")
{
this._hex = Color._rgbToHex(this._rgb);
}
@ -190,14 +182,13 @@ export class Color
*/
get int()
{
if (typeof this._int === 'undefined')
if (typeof this._int === "undefined")
{
this._int = Color._rgbToInt(this._rgb);
}
return this._int;
}
/*
get hsv() {
if (typeof this._hsv === 'undefined')
@ -216,7 +207,6 @@ export class Color
}
*/
/**
* String representation of the color, i.e. the hexadecimal representation.
*
@ -230,7 +220,6 @@ export class Color
return this.hex;
}
/**
* Get the [0,255] RGB triplet equivalent of the hexadecimal color code.
*
@ -247,16 +236,15 @@ export class Color
if (result == null)
{
throw {
origin: 'Color.hexToRgb255',
context: 'when converting an hexadecimal color code to its 255- or [0,1]-based RGB color representation',
error: 'unable to parse the argument: wrong type or wrong code'
origin: "Color.hexToRgb255",
context: "when converting an hexadecimal color code to its 255- or [0,1]-based RGB color representation",
error: "unable to parse the argument: wrong type or wrong code",
};
}
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
}
/**
* Get the [0,1] RGB triplet equivalent of the hexadecimal color code.
*
@ -273,7 +261,6 @@ export class Color
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
}
/**
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
*
@ -287,8 +274,8 @@ export class Color
static rgb255ToHex(rgb255)
{
const response = {
origin: 'Color.rgb255ToHex',
context: 'when converting an rgb triplet to its hexadecimal color representation'
origin: "Color.rgb255ToHex",
context: "when converting an rgb triplet to its hexadecimal color representation",
};
try
@ -298,11 +285,10 @@ export class Color
}
catch (error)
{
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
*
@ -316,8 +302,8 @@ export class Color
static rgbToHex(rgb)
{
const response = {
origin: 'Color.rgbToHex',
context: 'when converting an rgb triplet to its hexadecimal color representation'
origin: "Color.rgbToHex",
context: "when converting an rgb triplet to its hexadecimal color representation",
};
try
@ -327,11 +313,10 @@ export class Color
}
catch (error)
{
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Get the integer equivalent of the [0, 1] RGB triplet.
*
@ -345,8 +330,8 @@ export class Color
static rgbToInt(rgb)
{
const response = {
origin: 'Color.rgbToInt',
context: 'when converting an rgb triplet to its integer representation'
origin: "Color.rgbToInt",
context: "when converting an rgb triplet to its integer representation",
};
try
@ -356,11 +341,10 @@ export class Color
}
catch (error)
{
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Get the integer equivalent of the [0, 255] RGB triplet.
*
@ -374,8 +358,8 @@ export class Color
static rgb255ToInt(rgb255)
{
const response = {
origin: 'Color.rgb255ToInt',
context: 'when converting an rgb triplet to its integer representation'
origin: "Color.rgb255ToInt",
context: "when converting an rgb triplet to its integer representation",
};
try
{
@ -384,11 +368,10 @@ export class Color
}
catch (error)
{
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
*
@ -406,7 +389,6 @@ export class Color
return "#" + ((1 << 24) + (rgb255[0] << 16) + (rgb255[1] << 8) + rgb255[2]).toString(16).slice(1);
}
/**
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
*
@ -425,7 +407,6 @@ export class Color
return Color._rgb255ToHex(rgb255);
}
/**
* Get the integer equivalent of the [0, 1] RGB triplet.
*
@ -444,7 +425,6 @@ export class Color
return Color._rgb255ToInt(rgb255);
}
/**
* Get the integer equivalent of the [0, 255] RGB triplet.
*
@ -462,7 +442,6 @@ export class Color
return rgb255[0] * 0x10000 + rgb255[1] * 0x100 + rgb255[2];
}
/**
* Get the [0, 255] based RGB triplet equivalent of the integer color code.
*
@ -484,7 +463,6 @@ export class Color
return [r255, g255, b255];
}
/**
* Get the [0, 1] based RGB triplet equivalent of the integer color code.
*
@ -517,20 +495,21 @@ export class Color
*/
static _checkTypeAndRange(arg, range = undefined)
{
if (!Array.isArray(arg) || arg.length !== 3 ||
typeof arg[0] !== 'number' || typeof arg[1] !== 'number' || typeof arg[2] !== 'number')
if (
!Array.isArray(arg) || arg.length !== 3
|| typeof arg[0] !== "number" || typeof arg[1] !== "number" || typeof arg[2] !== "number"
)
{
throw 'the argument should be an array of numbers of length 3';
throw "the argument should be an array of numbers of length 3";
}
if (typeof range !== 'undefined' && (arg[0] < range[0] || arg[0] > range[1] || arg[1] < range[0] || arg[1] > range[1] || arg[2] < range[0] || arg[2] > range[1]))
if (typeof range !== "undefined" && (arg[0] < range[0] || arg[0] > range[1] || arg[1] < range[0] || arg[1] > range[1] || arg[2] < range[0] || arg[2] > range[1]))
{
throw 'the color components should all belong to [' + range[0] + ', ' + range[1] + ']';
throw "the color components should all belong to [" + range[0] + ", " + range[1] + "]";
}
}
}
/**
* Color spaces.
*
@ -543,13 +522,12 @@ Color.COLOR_SPACE = {
/**
* RGB colorspace: [r,g,b] with r,g,b in [-1, 1]
*/
RGB: Symbol.for('RGB'),
RGB: Symbol.for("RGB"),
/**
* RGB255 colorspace: [r,g,b] with r,g,b in [0, 255]
*/
RGB255: Symbol.for('RGB255'),
RGB255: Symbol.for("RGB255"),
/*
HSV: Symbol.for('HSV'),
DKL: Symbol.for('DKL'),
@ -557,7 +535,6 @@ Color.COLOR_SPACE = {
*/
};
/**
* Named colors.
*
@ -567,151 +544,151 @@ Color.COLOR_SPACE = {
* @public
*/
Color.NAMED_COLORS = {
'aliceblue': '#F0F8FF',
'antiquewhite': '#FAEBD7',
'aqua': '#00FFFF',
'aquamarine': '#7FFFD4',
'azure': '#F0FFFF',
'beige': '#F5F5DC',
'bisque': '#FFE4C4',
'black': '#000000',
'blanchedalmond': '#FFEBCD',
'blue': '#0000FF',
'blueviolet': '#8A2BE2',
'brown': '#A52A2A',
'burlywood': '#DEB887',
'cadetblue': '#5F9EA0',
'chartreuse': '#7FFF00',
'chocolate': '#D2691E',
'coral': '#FF7F50',
'cornflowerblue': '#6495ED',
'cornsilk': '#FFF8DC',
'crimson': '#DC143C',
'cyan': '#00FFFF',
'darkblue': '#00008B',
'darkcyan': '#008B8B',
'darkgoldenrod': '#B8860B',
'darkgray': '#A9A9A9',
'darkgrey': '#A9A9A9',
'darkgreen': '#006400',
'darkkhaki': '#BDB76B',
'darkmagenta': '#8B008B',
'darkolivegreen': '#556B2F',
'darkorange': '#FF8C00',
'darkorchid': '#9932CC',
'darkred': '#8B0000',
'darksalmon': '#E9967A',
'darkseagreen': '#8FBC8B',
'darkslateblue': '#483D8B',
'darkslategray': '#2F4F4F',
'darkslategrey': '#2F4F4F',
'darkturquoise': '#00CED1',
'darkviolet': '#9400D3',
'deeppink': '#FF1493',
'deepskyblue': '#00BFFF',
'dimgray': '#696969',
'dimgrey': '#696969',
'dodgerblue': '#1E90FF',
'firebrick': '#B22222',
'floralwhite': '#FFFAF0',
'forestgreen': '#228B22',
'fuchsia': '#FF00FF',
'gainsboro': '#DCDCDC',
'ghostwhite': '#F8F8FF',
'gold': '#FFD700',
'goldenrod': '#DAA520',
'gray': '#808080',
'grey': '#808080',
'green': '#008000',
'greenyellow': '#ADFF2F',
'honeydew': '#F0FFF0',
'hotpink': '#FF69B4',
'indianred': '#CD5C5C',
'indigo': '#4B0082',
'ivory': '#FFFFF0',
'khaki': '#F0E68C',
'lavender': '#E6E6FA',
'lavenderblush': '#FFF0F5',
'lawngreen': '#7CFC00',
'lemonchiffon': '#FFFACD',
'lightblue': '#ADD8E6',
'lightcoral': '#F08080',
'lightcyan': '#E0FFFF',
'lightgoldenrodyellow': '#FAFAD2',
'lightgray': '#D3D3D3',
'lightgrey': '#D3D3D3',
'lightgreen': '#90EE90',
'lightpink': '#FFB6C1',
'lightsalmon': '#FFA07A',
'lightseagreen': '#20B2AA',
'lightskyblue': '#87CEFA',
'lightslategray': '#778899',
'lightslategrey': '#778899',
'lightsteelblue': '#B0C4DE',
'lightyellow': '#FFFFE0',
'lime': '#00FF00',
'limegreen': '#32CD32',
'linen': '#FAF0E6',
'magenta': '#FF00FF',
'maroon': '#800000',
'mediumaquamarine': '#66CDAA',
'mediumblue': '#0000CD',
'mediumorchid': '#BA55D3',
'mediumpurple': '#9370DB',
'mediumseagreen': '#3CB371',
'mediumslateblue': '#7B68EE',
'mediumspringgreen': '#00FA9A',
'mediumturquoise': '#48D1CC',
'mediumvioletred': '#C71585',
'midnightblue': '#191970',
'mintcream': '#F5FFFA',
'mistyrose': '#FFE4E1',
'moccasin': '#FFE4B5',
'navajowhite': '#FFDEAD',
'navy': '#000080',
'oldlace': '#FDF5E6',
'olive': '#808000',
'olivedrab': '#6B8E23',
'orange': '#FFA500',
'orangered': '#FF4500',
'orchid': '#DA70D6',
'palegoldenrod': '#EEE8AA',
'palegreen': '#98FB98',
'paleturquoise': '#AFEEEE',
'palevioletred': '#DB7093',
'papayawhip': '#FFEFD5',
'peachpuff': '#FFDAB9',
'peru': '#CD853F',
'pink': '#FFC0CB',
'plum': '#DDA0DD',
'powderblue': '#B0E0E6',
'purple': '#800080',
'red': '#FF0000',
'rosybrown': '#BC8F8F',
'royalblue': '#4169E1',
'saddlebrown': '#8B4513',
'salmon': '#FA8072',
'sandybrown': '#F4A460',
'seagreen': '#2E8B57',
'seashell': '#FFF5EE',
'sienna': '#A0522D',
'silver': '#C0C0C0',
'skyblue': '#87CEEB',
'slateblue': '#6A5ACD',
'slategray': '#708090',
'slategrey': '#708090',
'snow': '#FFFAFA',
'springgreen': '#00FF7F',
'steelblue': '#4682B4',
'tan': '#D2B48C',
'teal': '#008080',
'thistle': '#D8BFD8',
'tomato': '#FF6347',
'turquoise': '#40E0D0',
'violet': '#EE82EE',
'wheat': '#F5DEB3',
'white': '#FFFFFF',
'whitesmoke': '#F5F5F5',
'yellow': '#FFFF00',
'yellowgreen': '#9ACD32'
"aliceblue": "#F0F8FF",
"antiquewhite": "#FAEBD7",
"aqua": "#00FFFF",
"aquamarine": "#7FFFD4",
"azure": "#F0FFFF",
"beige": "#F5F5DC",
"bisque": "#FFE4C4",
"black": "#000000",
"blanchedalmond": "#FFEBCD",
"blue": "#0000FF",
"blueviolet": "#8A2BE2",
"brown": "#A52A2A",
"burlywood": "#DEB887",
"cadetblue": "#5F9EA0",
"chartreuse": "#7FFF00",
"chocolate": "#D2691E",
"coral": "#FF7F50",
"cornflowerblue": "#6495ED",
"cornsilk": "#FFF8DC",
"crimson": "#DC143C",
"cyan": "#00FFFF",
"darkblue": "#00008B",
"darkcyan": "#008B8B",
"darkgoldenrod": "#B8860B",
"darkgray": "#A9A9A9",
"darkgrey": "#A9A9A9",
"darkgreen": "#006400",
"darkkhaki": "#BDB76B",
"darkmagenta": "#8B008B",
"darkolivegreen": "#556B2F",
"darkorange": "#FF8C00",
"darkorchid": "#9932CC",
"darkred": "#8B0000",
"darksalmon": "#E9967A",
"darkseagreen": "#8FBC8B",
"darkslateblue": "#483D8B",
"darkslategray": "#2F4F4F",
"darkslategrey": "#2F4F4F",
"darkturquoise": "#00CED1",
"darkviolet": "#9400D3",
"deeppink": "#FF1493",
"deepskyblue": "#00BFFF",
"dimgray": "#696969",
"dimgrey": "#696969",
"dodgerblue": "#1E90FF",
"firebrick": "#B22222",
"floralwhite": "#FFFAF0",
"forestgreen": "#228B22",
"fuchsia": "#FF00FF",
"gainsboro": "#DCDCDC",
"ghostwhite": "#F8F8FF",
"gold": "#FFD700",
"goldenrod": "#DAA520",
"gray": "#808080",
"grey": "#808080",
"green": "#008000",
"greenyellow": "#ADFF2F",
"honeydew": "#F0FFF0",
"hotpink": "#FF69B4",
"indianred": "#CD5C5C",
"indigo": "#4B0082",
"ivory": "#FFFFF0",
"khaki": "#F0E68C",
"lavender": "#E6E6FA",
"lavenderblush": "#FFF0F5",
"lawngreen": "#7CFC00",
"lemonchiffon": "#FFFACD",
"lightblue": "#ADD8E6",
"lightcoral": "#F08080",
"lightcyan": "#E0FFFF",
"lightgoldenrodyellow": "#FAFAD2",
"lightgray": "#D3D3D3",
"lightgrey": "#D3D3D3",
"lightgreen": "#90EE90",
"lightpink": "#FFB6C1",
"lightsalmon": "#FFA07A",
"lightseagreen": "#20B2AA",
"lightskyblue": "#87CEFA",
"lightslategray": "#778899",
"lightslategrey": "#778899",
"lightsteelblue": "#B0C4DE",
"lightyellow": "#FFFFE0",
"lime": "#00FF00",
"limegreen": "#32CD32",
"linen": "#FAF0E6",
"magenta": "#FF00FF",
"maroon": "#800000",
"mediumaquamarine": "#66CDAA",
"mediumblue": "#0000CD",
"mediumorchid": "#BA55D3",
"mediumpurple": "#9370DB",
"mediumseagreen": "#3CB371",
"mediumslateblue": "#7B68EE",
"mediumspringgreen": "#00FA9A",
"mediumturquoise": "#48D1CC",
"mediumvioletred": "#C71585",
"midnightblue": "#191970",
"mintcream": "#F5FFFA",
"mistyrose": "#FFE4E1",
"moccasin": "#FFE4B5",
"navajowhite": "#FFDEAD",
"navy": "#000080",
"oldlace": "#FDF5E6",
"olive": "#808000",
"olivedrab": "#6B8E23",
"orange": "#FFA500",
"orangered": "#FF4500",
"orchid": "#DA70D6",
"palegoldenrod": "#EEE8AA",
"palegreen": "#98FB98",
"paleturquoise": "#AFEEEE",
"palevioletred": "#DB7093",
"papayawhip": "#FFEFD5",
"peachpuff": "#FFDAB9",
"peru": "#CD853F",
"pink": "#FFC0CB",
"plum": "#DDA0DD",
"powderblue": "#B0E0E6",
"purple": "#800080",
"red": "#FF0000",
"rosybrown": "#BC8F8F",
"royalblue": "#4169E1",
"saddlebrown": "#8B4513",
"salmon": "#FA8072",
"sandybrown": "#F4A460",
"seagreen": "#2E8B57",
"seashell": "#FFF5EE",
"sienna": "#A0522D",
"silver": "#C0C0C0",
"skyblue": "#87CEEB",
"slateblue": "#6A5ACD",
"slategray": "#708090",
"slategrey": "#708090",
"snow": "#FFFAFA",
"springgreen": "#00FF7F",
"steelblue": "#4682B4",
"tan": "#D2B48C",
"teal": "#008080",
"thistle": "#D8BFD8",
"tomato": "#FF6347",
"turquoise": "#40E0D0",
"violet": "#EE82EE",
"wheat": "#F5DEB3",
"white": "#FFFFFF",
"whitesmoke": "#F5F5F5",
"yellow": "#FFFF00",
"yellowgreen": "#9ACD32",
};

View File

@ -7,9 +7,7 @@
* @license Distributed under the terms of the MIT License
*/
import {Color} from './Color';
import { Color } from "./Color.js";
/**
* <p>This mixin implement color and contrast changes for visual stimuli</p>
@ -17,15 +15,15 @@ import {Color} from './Color';
* @name module:util.ColorMixin
* @mixin
*/
export let ColorMixin = (superclass) => class extends superclass
{
constructor(args)
export let ColorMixin = (superclass) =>
class extends superclass
{
super(args);
}
constructor(args)
{
super(args);
}
/**
/**
* Setter for Color attribute.
*
* @name module:util.ColorMixin#setColor
@ -34,16 +32,15 @@ export let ColorMixin = (superclass) => class extends superclass
* @param {Color} color - the new color
* @param {boolean} [log= false] - whether or not to log
*/
setColor(color, log)
{
this._setAttribute('color', color, log);
setColor(color, log)
{
this._setAttribute("color", color, log);
this._needUpdate = true;
this._needPixiUpdate = true;
}
this._needUpdate = true;
this._needPixiUpdate = true;
}
/**
/**
* Setter for Contrast attribute.
*
* @name module:util.ColorMixin#setContrast
@ -52,16 +49,15 @@ export let ColorMixin = (superclass) => class extends superclass
* @param {number} contrast - the new contrast (must be between 0 and 1)
* @param {boolean} [log= false] - whether or not to log
*/
setContrast(contrast, log)
{
this._setAttribute('contrast', contrast, log);
setContrast(contrast, log)
{
this._setAttribute("contrast", contrast, log);
this._needUpdate = true;
this._needPixiUpdate = true;
}
this._needUpdate = true;
this._needPixiUpdate = true;
}
/**
/**
* Get a new contrasted Color.
*
* @name module:util.ColorMixin#getContrastedColor
@ -70,10 +66,9 @@ export let ColorMixin = (superclass) => class extends superclass
* @param {string|number|Array.<number>} color - the color
* @param {number} contrast - the contrast (must be between 0 and 1)
*/
getContrastedColor(color, contrast)
{
const rgb = color.rgb.map(c => (c * 2.0 - 1.0) * contrast);
return new Color(rgb, Color.COLOR_SPACE.RGB);
}
};
getContrastedColor(color, contrast)
{
const rgb = color.rgb.map((c) => (c * 2.0 - 1.0) * contrast);
return new Color(rgb, Color.COLOR_SPACE.RGB);
}
};

View File

@ -7,9 +7,7 @@
* @license Distributed under the terms of the MIT License
*/
import * as util from './Util';
import * as util from "./Util.js";
/**
* <p>EventEmitter implements the classic observer/observable pattern.</p>
@ -34,7 +32,6 @@ export class EventEmitter
this._onceUuids = new Map();
}
/**
* Listener called when this instance emits an event for which it is registered.
*
@ -42,7 +39,6 @@ export class EventEmitter
* @param {object} data - the data passed to the listener
*/
/**
* Register a new listener for events with the given name emitted by this instance.
*
@ -56,9 +52,9 @@ export class EventEmitter
on(name, listener)
{
// check that the listener is a function:
if (typeof listener !== 'function')
if (typeof listener !== "function")
{
throw new TypeError('listener must be a function');
throw new TypeError("listener must be a function");
}
// generate a new uuid:
@ -69,12 +65,11 @@ export class EventEmitter
{
this._listeners.set(name, []);
}
this._listeners.get(name).push({uuid, listener});
this._listeners.get(name).push({ uuid, listener });
return uuid;
}
/**
* Register a new listener for the given event name, and remove it as soon as the event has been emitted.
*
@ -98,7 +93,6 @@ export class EventEmitter
return uuid;
}
/**
* Remove the listener with the given uuid associated to the given event name.
*
@ -114,13 +108,12 @@ export class EventEmitter
if (relevantUuidListeners && relevantUuidListeners.length)
{
this._listeners.set(name, relevantUuidListeners.filter(uuidlistener => (uuidlistener.uuid != uuid)));
this._listeners.set(name, relevantUuidListeners.filter((uuidlistener) => (uuidlistener.uuid != uuid)));
return true;
}
return false;
}
/**
* Emit an event with a given name and associated data.
*
@ -138,11 +131,11 @@ export class EventEmitter
{
let onceUuids = this._onceUuids.get(name);
let self = this;
relevantUuidListeners.forEach(({uuid, listener}) =>
relevantUuidListeners.forEach(({ uuid, listener }) =>
{
listener(data);
if (typeof onceUuids !== 'undefined' && onceUuids.includes(uuid))
if (typeof onceUuids !== "undefined" && onceUuids.includes(uuid))
{
self.off(name, uuid);
}
@ -152,6 +145,4 @@ export class EventEmitter
return false;
}
}

36
src/util/Pixi.js Normal file
View File

@ -0,0 +1,36 @@
/**
* PIXI utilities.
*
* @authors Alain Pitiot, Sotiri Bakagiannis, Thomas Pronk
* @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 { to_px } from "./Util.js";
/**
* Convert a position to a PIXI Point.
*
* @name module:util.to_pixiPoint
* @function
* @public
* @param {number[]} pos - the input position
* @param {string} posUnit - the position units
* @param {Window} win - the associated Window
* @param {boolean} [integerCoordinates = false] - whether or not to round the PIXI Point coordinates.
* @returns {number[]} the position as a PIXI Point
*/
export function to_pixiPoint(pos, posUnit, win, integerCoordinates = false)
{
const pos_px = to_px(pos, posUnit, win);
if (integerCoordinates)
{
return new PIXI.Point(Math.round(pos_px[0]), Math.round(pos_px[1]));
}
else
{
return new PIXI.Point(pos_px[0], pos_px[1]);
}
}

View File

@ -8,10 +8,8 @@
* @license Distributed under the terms of the MIT License
*/
import {EventEmitter} from './EventEmitter';
import * as util from './Util';
import { EventEmitter } from "./EventEmitter.js";
import * as util from "./Util.js";
/**
* <p>PsychoObject is the base class for all PsychoJS objects.
@ -32,14 +30,13 @@ export class PsychObject extends EventEmitter
this._userAttributes = new Set();
// name:
if (typeof name === 'undefined')
if (typeof name === "undefined")
{
name = this.constructor.name;
}
this._addAttribute('name', name);
this._addAttribute("name", name);
}
/**
* Get the PsychoJS instance.
*
@ -51,7 +48,6 @@ export class PsychObject extends EventEmitter
return this._psychoJS;
}
/**
* Setter for the PsychoJS attribute.
*
@ -63,7 +59,6 @@ export class PsychObject extends EventEmitter
this._psychoJS = psychoJS;
}
/**
* String representation of the PsychObject.
*
@ -74,38 +69,37 @@ export class PsychObject extends EventEmitter
*/
toString()
{
let representation = this.constructor.name + '( ';
let representation = this.constructor.name + "( ";
let addComma = false;
for (const attribute of this._userAttributes)
{
if (addComma)
{
representation += ', ';
representation += ", ";
}
addComma = true;
let value = util.toString(this['_' + attribute]);
let value = util.toString(this["_" + attribute]);
const l = value.length;
if (l > 50)
{
if (value[l - 1] === ')')
if (value[l - 1] === ")")
{
value = value.substring(0, 50) + '~)';
value = value.substring(0, 50) + "~)";
}
else
{
value = value.substring(0, 50) + '~';
value = value.substring(0, 50) + "~";
}
}
representation += attribute + '=' + value;
representation += attribute + "=" + value;
}
representation += ' )';
representation += " )";
return representation;
}
/**
* Set the value of an attribute.
*
@ -121,31 +115,30 @@ export class PsychObject extends EventEmitter
_setAttribute(attributeName, attributeValue, log = false, operation = undefined, stealth = false)
{
const response = {
origin: 'PsychObject.setAttribute',
context: 'when setting the attribute of an object'
origin: "PsychObject.setAttribute",
context: "when setting the attribute of an object",
};
if (typeof attributeName == 'undefined')
if (typeof attributeName == "undefined")
{
throw Object.assign(response, {
error: 'the attribute name cannot be' +
' undefined'
error: "the attribute name cannot be"
+ " undefined",
});
}
if (typeof attributeValue == 'undefined')
if (typeof attributeValue == "undefined")
{
this._psychoJS.logger.warn('setting the value of attribute: ' + attributeName + ' in PsychObject: ' + this._name + ' as: undefined');
this._psychoJS.logger.warn("setting the value of attribute: " + attributeName + " in PsychObject: " + this._name + " as: undefined");
}
// (*) apply operation to old and new values:
if (typeof operation !== 'undefined' && this.hasOwnProperty('_' + attributeName))
if (typeof operation !== "undefined" && this.hasOwnProperty("_" + attributeName))
{
let oldValue = this['_' + attributeName];
let oldValue = this["_" + attributeName];
// operations can only be applied to numbers and array of numbers (which can be empty):
if (typeof attributeValue == 'number' || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == 'number')))
if (typeof attributeValue == "number" || (Array.isArray(attributeValue) && (attributeValue.length === 0 || typeof attributeValue[0] == "number")))
{
// value is an array:
if (Array.isArray(attributeValue))
{
@ -155,160 +148,158 @@ export class PsychObject extends EventEmitter
if (attributeValue.length !== oldValue.length)
{
throw Object.assign(response, {
error: 'old and new' +
' value should have' +
' the same size when they are both arrays'
error: "old and new"
+ " value should have"
+ " the same size when they are both arrays",
});
}
switch (operation)
{
case '':
case "":
// no change to value;
break;
case '+':
case "+":
attributeValue = attributeValue.map((v, i) => oldValue[i] + v);
break;
case '*':
case "*":
attributeValue = attributeValue.map((v, i) => oldValue[i] * v);
break;
case '-':
case "-":
attributeValue = attributeValue.map((v, i) => oldValue[i] - v);
break;
case '/':
case "/":
attributeValue = attributeValue.map((v, i) => oldValue[i] / v);
break;
case '**':
case "**":
attributeValue = attributeValue.map((v, i) => oldValue[i] ** v);
break;
case '%':
case "%":
attributeValue = attributeValue.map((v, i) => oldValue[i] % v);
break;
default:
throw Object.assign(response, {
error: 'unsupported' +
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
error: "unsupported"
+ " operation: " + operation + " when setting: " + attributeName + " in: " + this.name,
});
}
}
else
// old value is a scalar
else
{
switch (operation)
{
case '':
case "":
// no change to value;
break;
case '+':
attributeValue = attributeValue.map(v => oldValue + v);
case "+":
attributeValue = attributeValue.map((v) => oldValue + v);
break;
case '*':
attributeValue = attributeValue.map(v => oldValue * v);
case "*":
attributeValue = attributeValue.map((v) => oldValue * v);
break;
case '-':
attributeValue = attributeValue.map(v => oldValue - v);
case "-":
attributeValue = attributeValue.map((v) => oldValue - v);
break;
case '/':
attributeValue = attributeValue.map(v => oldValue / v);
case "/":
attributeValue = attributeValue.map((v) => oldValue / v);
break;
case '**':
attributeValue = attributeValue.map(v => oldValue ** v);
case "**":
attributeValue = attributeValue.map((v) => oldValue ** v);
break;
case '%':
attributeValue = attributeValue.map(v => oldValue % v);
case "%":
attributeValue = attributeValue.map((v) => oldValue % v);
break;
default:
throw Object.assign(response, {
error: 'unsupported' +
' value: ' + JSON.stringify(attributeValue) + ' for' +
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
error: "unsupported"
+ " value: " + JSON.stringify(attributeValue) + " for"
+ " operation: " + operation + " when setting: " + attributeName + " in: " + this.name,
});
}
}
}
else
// value is a scalar
else
{
// old value is an array
if (Array.isArray(oldValue))
{
switch (operation)
{
case '':
attributeValue = oldValue.map(v => attributeValue);
case "":
attributeValue = oldValue.map((v) => attributeValue);
break;
case '+':
attributeValue = oldValue.map(v => v + attributeValue);
case "+":
attributeValue = oldValue.map((v) => v + attributeValue);
break;
case '*':
attributeValue = oldValue.map(v => v * attributeValue);
case "*":
attributeValue = oldValue.map((v) => v * attributeValue);
break;
case '-':
attributeValue = oldValue.map(v => v - attributeValue);
case "-":
attributeValue = oldValue.map((v) => v - attributeValue);
break;
case '/':
attributeValue = oldValue.map(v => v / attributeValue);
case "/":
attributeValue = oldValue.map((v) => v / attributeValue);
break;
case '**':
attributeValue = oldValue.map(v => v ** attributeValue);
case "**":
attributeValue = oldValue.map((v) => v ** attributeValue);
break;
case '%':
attributeValue = oldValue.map(v => v % attributeValue);
case "%":
attributeValue = oldValue.map((v) => v % attributeValue);
break;
default:
throw Object.assign(response, {
error: 'unsupported' +
' operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
error: "unsupported"
+ " operation: " + operation + " when setting: " + attributeName + " in: " + this.name,
});
}
}
else
// old value is a scalar
else
{
switch (operation)
{
case '':
case "":
// no change to value;
break;
case '+':
case "+":
attributeValue = oldValue + attributeValue;
break;
case '*':
case "*":
attributeValue = oldValue * attributeValue;
break;
case '-':
case "-":
attributeValue = oldValue - attributeValue;
break;
case '/':
case "/":
attributeValue = oldValue / attributeValue;
break;
case '**':
case "**":
attributeValue = oldValue ** attributeValue;
break;
case '%':
case "%":
attributeValue = oldValue % attributeValue;
break;
default:
throw Object.assign(response, {
error: 'unsupported' +
' value: ' + JSON.stringify(attributeValue) + ' for operation: ' + operation + ' when setting: ' + attributeName + ' in: ' + this.name
error: "unsupported"
+ " value: " + JSON.stringify(attributeValue) + " for operation: " + operation + " when setting: " + attributeName + " in: " + this.name,
});
}
}
}
}
else
{
throw Object.assign(response, {error: 'operation: ' + operation + ' is invalid for old value: ' + JSON.stringify(oldValue) + ' and new value: ' + JSON.stringify(attributeValue)});
throw Object.assign(response, {
error: "operation: " + operation + " is invalid for old value: " + JSON.stringify(oldValue) + " and new value: " + JSON.stringify(attributeValue),
});
}
}
// (*) log if appropriate:
if (!stealth && (log || this._autoLog) && (typeof this.win !== 'undefined'))
if (!stealth && (log || this._autoLog) && (typeof this.win !== "undefined"))
{
const msg = this.name + ": " + attributeName + " = " + util.toString(attributeValue);
this.win.logOnFlip({
@ -317,13 +308,12 @@ export class PsychObject extends EventEmitter
});
}
// (*) set the value of the attribute and return whether it has changed:
const previousAttributeValue = this['_' + attributeName];
this['_' + attributeName] = attributeValue;
const previousAttributeValue = this["_" + attributeName];
this["_" + attributeName] = attributeValue;
// Things seem OK without this check except for 'vertices'
if (typeof previousAttributeValue === 'undefined')
if (typeof previousAttributeValue === "undefined")
{
// Not that any of the following lines should throw, but evaluating
// `this._vertices.map` on `ShapeStim._getVertices_px()` seems to
@ -342,10 +332,9 @@ export class PsychObject extends EventEmitter
// `Util.toString()` might try, but fail to stringify in a meaningful way are assigned
// an 'Object (circular)' string representation. For being opaque as to their raw
// value, those types of input are liable to produce PIXI updates.
return prev === 'Object (circular)' || next === 'Object (circular)' || prev !== next;
return prev === "Object (circular)" || next === "Object (circular)" || prev !== next;
}
/**
* Add an attribute to this instance (e.g. define setters and getters) and affect a value to it.
*
@ -355,20 +344,21 @@ export class PsychObject extends EventEmitter
* @param {object} [defaultValue] - the default value for the attribute
* @param {function} [onChange] - function called upon changes to the attribute value
*/
_addAttribute(name, value, defaultValue = undefined, onChange = () => {})
_addAttribute(name, value, defaultValue = undefined, onChange = () =>
{})
{
const getPropertyName = 'get' + name[0].toUpperCase() + name.substr(1);
if (typeof this[getPropertyName] === 'undefined')
const getPropertyName = "get" + name[0].toUpperCase() + name.substr(1);
if (typeof this[getPropertyName] === "undefined")
{
this[getPropertyName] = () => this['_' + name];
this[getPropertyName] = () => this["_" + name];
}
const setPropertyName = 'set' + name[0].toUpperCase() + name.substr(1);
if (typeof this[setPropertyName] === 'undefined')
const setPropertyName = "set" + name[0].toUpperCase() + name.substr(1);
if (typeof this[setPropertyName] === "undefined")
{
this[setPropertyName] = (value, log = false) =>
{
if (typeof value === 'undefined' || value === null)
if (typeof value === "undefined" || value === null)
{
value = defaultValue;
}
@ -382,7 +372,7 @@ export class PsychObject extends EventEmitter
else
{
// deal with default value:
if (typeof value === 'undefined' || value === null)
if (typeof value === "undefined" || value === null)
{
value = defaultValue;
}
@ -397,16 +387,14 @@ export class PsychObject extends EventEmitter
set(value)
{
this[setPropertyName](value);
}
},
});
// note: we use this[name] instead of this['_' + name] since a this.set<Name> method may available
// in the object, in which case we need to call it
this[name] = value;
//this['_' + name] = value;
// this['_' + name] = value;
this._userAttributes.add(name);
}
}

View File

@ -7,7 +7,6 @@
* @license Distributed under the terms of the MIT License
*/
/**
* <p>A scheduler helps run the main loop by managing scheduled functions,
* called tasks, after each frame is displayed.</p>
@ -53,7 +52,6 @@ export class Scheduler
this._status = Scheduler.Status.STOPPED;
}
/**
* Get the status of the scheduler.
*
@ -66,7 +64,6 @@ export class Scheduler
return this._status;
}
/**
* Task to be run by the scheduler.
*
@ -87,7 +84,6 @@ export class Scheduler
this._argsList.push(args);
}
/**
* Condition evaluated when the task is run.
*
@ -108,7 +104,7 @@ export class Scheduler
addConditional(condition, thenScheduler, elseScheduler)
{
const self = this;
let task = function ()
let task = function()
{
if (condition())
{
@ -125,7 +121,6 @@ export class Scheduler
this.add(task);
}
/**
* Start this scheduler.
*
@ -173,7 +168,6 @@ export class Scheduler
requestAnimationFrame(update);
}
/**
* Stop this scheduler.
*
@ -187,7 +181,6 @@ export class Scheduler
this._stopAtNextUpdate = true;
}
/**
* Run the next scheduled tasks, in sequence, until a rendering of the scene is requested.
*
@ -209,9 +202,8 @@ export class Scheduler
}
// if there is no current task, we look for the next one in the list or quit if there is none:
if (typeof this._currentTask == 'undefined')
if (typeof this._currentTask == "undefined")
{
// a task is available in the taskList:
if (this._taskList.length > 0)
{
@ -259,15 +251,12 @@ export class Scheduler
this._currentTask = undefined;
this._currentArgs = undefined;
}
}
return state;
}
}
/**
* Events.
*
@ -280,25 +269,24 @@ Scheduler.Event = {
/**
* Move onto the next task *without* rendering the scene first.
*/
NEXT: Symbol.for('NEXT'),
NEXT: Symbol.for("NEXT"),
/**
* Render the scene and repeat the task.
*/
FLIP_REPEAT: Symbol.for('FLIP_REPEAT'),
FLIP_REPEAT: Symbol.for("FLIP_REPEAT"),
/**
* Render the scene and move onto the next task.
*/
FLIP_NEXT: Symbol.for('FLIP_NEXT'),
FLIP_NEXT: Symbol.for("FLIP_NEXT"),
/**
* Quit the scheduler.
*/
QUIT: Symbol.for('QUIT')
QUIT: Symbol.for("QUIT"),
};
/**
* Status.
*
@ -311,10 +299,10 @@ Scheduler.Status = {
/**
* The Scheduler is running.
*/
RUNNING: Symbol.for('RUNNING'),
RUNNING: Symbol.for("RUNNING"),
/**
* The Scheduler is stopped.
*/
STOPPED: Symbol.for('STOPPED')
STOPPED: Symbol.for("STOPPED"),
};

File diff suppressed because it is too large Load Diff

113
src/util/Util.test.js Normal file
View File

@ -0,0 +1,113 @@
import assert from "assert";
import { isNumeric, randint, round, sum, toNumerical, turnSquareBracketsIntoArrays } from "./Util.js";
assert(isNumeric("1.2"));
assert(isNumeric(0));
assert(!isNumeric("NaN"));
assert(!isNumeric("hey"));
// number -> number, e.g. 2 -> 2
assert.equal(2, toNumerical(2));
// [number] -> [number], e.g. [1,2,3] -> [1,2,3]
assert.deepEqual([1, 2, 3], toNumerical([1, 2, 3]));
assert(Array.isArray(toNumerical([0])));
// numeral string -> number, e.g. "8" -> 8
assert.deepEqual(8, toNumerical("8"));
// [number | numeral string] -> [number], e.g. [1, 2, "3"] -> [1,2,3]
assert.deepEqual([1, 2, 3], toNumerical([1, 2, "3"]));
// Establish what happens when fed an array-like string
assert.deepEqual([1, 2, 3], toNumerical(...turnSquareBracketsIntoArrays("[1, 2, 3][]]", 2)));
// Throws
(async () =>
{
await assert.rejects(
async () =>
{
toNumerical(turnSquareBracketsIntoArrays([1, 2]));
},
{
origin: "util.toNumerical",
context: "when converting an object to its numerical form",
error: "unable to convert undefined to a number",
},
);
})();
// Towards a NumPy inspired bound random integer producer
for (let i = 0; i < 100; i += 1)
{
// Calling sans arguments gives back zero no matter what
assert.equal(randint(), 0);
}
for (let i = 0; i < 100; i += 1)
{
// Same when calling with a min of one sans max
assert.equal(randint(1), 0);
}
// Expect min to be zero, max to be one, result to be zero
assert(randint(1) >= 0 === randint(1) < 1);
// Same when calling with a min of one sans max
assert.equal(randint(1), 0);
for (let i = 0; i < 100; i += 1)
{
// Same with null
assert.equal(randint(null), 0);
}
for (let i = 100; i > 0; i -= 1)
{
// Try out a few ranges in the positive
assert(randint(i) < i);
}
for (let i = -99; i < 0; i += 1)
{
// What happens when using negative parameters?
assert(randint(2 * i, i) <= i);
}
try
{
randint(0, -10);
}
catch ({ error })
{
assert.equal(error, "min should be <= max");
}
// Implement Crib Sheet math extras
// These are taken from the SO question above
// https://stackoverflow.com/questions/11832914
const actual = [
10,
1.7777777,
9.1,
];
const expected = [
10,
1.78,
9.1,
];
const got = actual.map((input) => round(input, 2));
assert.deepEqual(expected, got);
assert.equal(sum(null), 0);
assert.equal(sum(), 0);
assert(!sum([0]));
assert.equal(sum([1, NaN, null, undefined]), 1);
assert.equal(sum([1, 2, -3]), 0);
// Careful Thomas!
assert.equal(sum(["a1", 2]), 2);

View File

@ -1,7 +1,8 @@
export * from './Clock.js';
export * from './Color.js';
export * from './ColorMixin.js';
export * from './EventEmitter.js';
export * from './PsychObject.js';
export * from './Scheduler.js';
export * from './Util.js';
export * from "./Clock.js";
export * from "./Color.js";
export * from "./ColorMixin.js";
export * from "./EventEmitter.js";
export * from "./Pixi.js";
export * from "./PsychObject.js";
export * from "./Scheduler.js";
export * from "./Util.js";

View File

@ -7,10 +7,8 @@
* @license Distributed under the terms of the MIT License
*/
import {TextBox} from './TextBox.js';
import {Mouse} from '../core/Mouse.js';
import { Mouse } from "../core/Mouse.js";
import { TextBox } from "./TextBox.js";
/**
* <p>ButtonStim visual stimulus.</p>
@ -39,28 +37,71 @@ import {Mouse} from '../core/Mouse.js';
*/
export class ButtonStim extends TextBox
{
constructor({win, name, text, font, pos, size, padding, anchor = 'center', units, color, fillColor = 'darkgrey', borderColor, borderWidth = 0, opacity, letterHeight, bold = true, italic, autoDraw, autoLog} = {})
constructor(
{
win,
name,
text,
font,
pos,
size,
padding,
anchor = "center",
units,
color,
fillColor = "darkgrey",
borderColor,
borderWidth = 0,
opacity,
letterHeight,
bold = true,
italic,
autoDraw,
autoLog,
} = {},
)
{
super({win, name, text, font, pos, size, padding, anchor, units, color, fillColor, borderColor, borderWidth, opacity, letterHeight, bold, italic, alignment: 'center', autoDraw, autoLog});
super({
win,
name,
text,
font,
pos,
size,
padding,
anchor,
units,
color,
fillColor,
borderColor,
borderWidth,
opacity,
letterHeight,
bold,
italic,
alignment: "center",
autoDraw,
autoLog,
});
this.psychoJS.logger.debug('create a new Button with name: ', name);
this.psychoJS.logger.debug("create a new Button with name: ", name);
this.listener = new Mouse({name, win, autoLog});
this.listener = new Mouse({ name, win, autoLog });
this._addAttribute(
'wasClicked',
false
"wasClicked",
false,
);
// Arrays to store times of clicks on and off
this._addAttribute(
'timesOn',
[]
"timesOn",
[],
);
this._addAttribute(
'timesOff',
[]
"timesOff",
[],
);
if (this._autoLog)
@ -69,8 +110,6 @@ export class ButtonStim extends TextBox
}
}
/**
* How many times has this button been clicked on?
*
@ -82,8 +121,6 @@ export class ButtonStim extends TextBox
return this.timesOn.length;
}
/**
* Is this button currently being clicked on?
*
@ -94,5 +131,4 @@ export class ButtonStim extends TextBox
{
return this.listener.isPressedIn(this, [1, 0, 0]);
}
}

View File

@ -7,11 +7,11 @@
* @license Distributed under the terms of the MIT License
*/
import {Clock} from "../util/Clock";
import {PsychObject} from "../util/PsychObject";
import {PsychoJS} from "../core/PsychoJS";
import * as util from '../util/Util';
import {ExperimentHandler} from "../data/ExperimentHandler";
import {Clock} from "../util/Clock.js";
import {PsychObject} from "../util/PsychObject.js";
import {PsychoJS} from "../core/PsychoJS.js";
import * as util from "../util/Util.js";
import {ExperimentHandler} from "../data/ExperimentHandler.js";
// import {VideoClip} from "./VideoClip";
@ -25,20 +25,25 @@ import {ExperimentHandler} from "../data/ExperimentHandler";
* @param {string} [options.format='video/webm;codecs=vp9'] the video format
* @param {Clock} [options.clock= undefined] - an optional clock
* @param {boolean} [options.autoLog= false] - whether or not to log
*
* @todo add video constraints as parameter
*/
export class Camera extends PsychObject
{
/**
* @constructor
* @public
*/
constructor({win, name, format, clock, autoLog} = {})
{
super(win._psychoJS);
this._addAttribute('win', win, undefined);
this._addAttribute('name', name, 'camera');
this._addAttribute('format', format, 'video/webm;codecs=vp9', this._onChange);
this._addAttribute('clock', clock, new Clock());
this._addAttribute('autoLog', autoLog, false);
this._addAttribute('status', PsychoJS.Status.NOT_STARTED);
this._addAttribute("win", win, undefined);
this._addAttribute("name", name, "camera");
this._addAttribute("format", format, "video/webm;codecs=vp9", this._onChange);
this._addAttribute("clock", clock, new Clock());
this._addAttribute("autoLog", autoLog, false);
this._addAttribute("status", PsychoJS.Status.NOT_STARTED);
// prepare the recording:
this._prepareRecording();
@ -54,6 +59,7 @@ export class Camera extends PsychObject
* Get the underlying video stream.
*
* @name module:visual.Camera#getStream
* @function
* @public
* @returns {MediaStream} the video stream
*/
@ -67,6 +73,7 @@ export class Camera extends PsychObject
* Get a video element pointing to the Camera stream.
*
* @name module:visual.Camera#getVideo
* @function
* @public
* @returns {HTMLVideoElement} a video element
*/
@ -76,7 +83,7 @@ export class Camera extends PsychObject
// several stimuli and one of them might pause the feed
// create a video with the appropriate size:
const video = document.createElement('video');
const video = document.createElement("video");
this._videos.push(video);
video.width = this._streamSettings.width;
@ -101,6 +108,7 @@ export class Camera extends PsychObject
* Submit a request to start the recording.
*
* @name module:visual.Camera#start
* @function
* @public
* @return {Promise} promise fulfilled when the recording actually started
*/
@ -116,13 +124,13 @@ export class Camera extends PsychObject
if (this._status !== PsychoJS.Status.STARTED)
{
this._psychoJS.logger.debug('request to start video recording');
this._psychoJS.logger.debug("request to start video recording");
try
{
if (!this._recorder)
{
throw 'the recorder has not been created yet, possibly because the participant has not given the authorisation to record video';
throw "the recorder has not been created yet, possibly because the participant has not given the authorisation to record video";
}
this._recorder.start();
@ -138,12 +146,12 @@ export class Camera extends PsychObject
}
catch (error)
{
this._psychoJS.logger.error('unable to start the video recording: ' + JSON.stringify(error));
this._psychoJS.logger.error("unable to start the video recording: " + JSON.stringify(error));
this._status = PsychoJS.Status.ERROR;
throw {
origin: 'Camera.start',
context: 'when starting the video recording for camera: ' + this._name,
origin: "Camera.start",
context: "when starting the video recording for camera: " + this._name,
error
};
}
@ -157,6 +165,7 @@ export class Camera extends PsychObject
* Submit a request to stop the recording.
*
* @name module:visual.Camera#stop
* @function
* @public
* @param {Object} options
* @param {string} [options.filename] the name of the file to which the video recording
@ -168,7 +177,7 @@ export class Camera extends PsychObject
{
if (this._status === PsychoJS.Status.STARTED || this._status === PsychoJS.Status.PAUSED)
{
this._psychoJS.logger.debug('request to stop video recording');
this._psychoJS.logger.debug("request to stop video recording");
// stop the videos:
for (const video of this._videos)
@ -201,6 +210,7 @@ export class Camera extends PsychObject
* Submit a request to pause the recording.
*
* @name module:visual.Camera#pause
* @function
* @public
* @return {Promise} promise fulfilled when the recording actually paused
*/
@ -208,13 +218,13 @@ export class Camera extends PsychObject
{
if (this._status === PsychoJS.Status.STARTED)
{
this._psychoJS.logger.debug('request to pause video recording');
this._psychoJS.logger.debug("request to pause video recording");
try
{
if (!this._recorder)
{
throw 'the recorder has not been created yet, possibly because the participant has not given the authorisation to record video';
throw "the recorder has not been created yet, possibly because the participant has not given the authorisation to record video";
}
// note: calling the pause method of the MediaRecorder raises a pause event
@ -230,12 +240,12 @@ export class Camera extends PsychObject
}
catch (error)
{
self._psychoJS.logger.error('unable to pause the video recording: ' + JSON.stringify(error));
self._psychoJS.logger.error("unable to pause the video recording: " + JSON.stringify(error));
this._status = PsychoJS.Status.ERROR;
throw {
origin: 'Camera.pause',
context: 'when pausing the video recording for camera: ' + this._name,
origin: "Camera.pause",
context: "when pausing the video recording for camera: " + this._name,
error
};
}
@ -250,6 +260,7 @@ export class Camera extends PsychObject
* <p>resume has no effect if the recording was not previously paused.</p>
*
* @name module:visual.Camera#resume
* @function
* @param {Object} options
* @param {boolean} [options.clear= false] whether or not to empty the video buffer before
* resuming the recording
@ -259,13 +270,13 @@ export class Camera extends PsychObject
{
if (this._status === PsychoJS.Status.PAUSED)
{
this._psychoJS.logger.debug('request to resume video recording');
this._psychoJS.logger.debug("request to resume video recording");
try
{
if (!this._recorder)
{
throw 'the recorder has not been created yet, possibly because the participant has not given the authorisation to record video';
throw "the recorder has not been created yet, possibly because the participant has not given the authorisation to record video";
}
// empty the audio buffer is needed:
@ -287,12 +298,12 @@ export class Camera extends PsychObject
}
catch (error)
{
self._psychoJS.logger.error('unable to resume the video recording: ' + JSON.stringify(error));
self._psychoJS.logger.error("unable to resume the video recording: " + JSON.stringify(error));
this._status = PsychoJS.Status.ERROR;
throw {
origin: 'Camera.resume',
context: 'when resuming the video recording for camera: ' + this._name,
origin: "Camera.resume",
context: "when resuming the video recording for camera: " + this._name,
error
};
}
@ -305,6 +316,7 @@ export class Camera extends PsychObject
* Submit a request to flush the recording.
*
* @name module:visual.Camera#flush
* @function
* @public
* @return {Promise} promise fulfilled when the data has actually been made available
*/
@ -312,7 +324,7 @@ export class Camera extends PsychObject
{
if (this._status === PsychoJS.Status.STARTED || this._status === PsychoJS.Status.PAUSED)
{
this._psychoJS.logger.debug('request to flush video recording');
this._psychoJS.logger.debug("request to flush video recording");
// note: calling the requestData method of the MediaRecorder will raise a
// dataavailable event
@ -336,13 +348,13 @@ export class Camera extends PsychObject
* @name module:visual.Camera#download
* @function
* @public
* @param {string} filename the filename
* @param {string} filename - the filename of the video file
*/
download(filename = 'video.webm')
download(filename = "video.webm")
{
const videoBlob = new Blob(this._videoBuffer);
const anchor = document.createElement('a');
const anchor = document.createElement("a");
anchor.href = window.URL.createObjectURL(videoBlob);
anchor.download = filename;
document.body.appendChild(anchor);
@ -362,7 +374,7 @@ export class Camera extends PsychObject
async upload({tag} = {})
{
// default tag: the name of this Camera object
if (typeof tag === 'undefined')
if (typeof tag === "undefined")
{
tag = this._name;
}
@ -374,16 +386,15 @@ export class Camera extends PsychObject
// if the video recording cannot be uploaded, e.g. the experiment is running locally, or
// if it is piloting mode, then we offer the video recording as a file for download:
if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER ||
this._psychoJS.config.experiment.status !== 'RUNNING' ||
this._psychoJS._serverMsg.has('__pilotToken'))
this._psychoJS.config.experiment.status !== "RUNNING" ||
this._psychoJS._serverMsg.has("__pilotToken"))
{
return this.download(tag);
}
// upload the blob:
// TODO uploadAudio -> uploadAudioVideo
const videoBlob = new Blob(this._videoBuffer);
return this._psychoJS.serverManager.uploadAudio(videoBlob, tag);
return this._psychoJS.serverManager.uploadAudioVideo(videoBlob, tag);
}
@ -399,22 +410,12 @@ export class Camera extends PsychObject
async getRecording({tag, flush = false} = {})
{
// default tag: the name of this Microphone object
if (typeof tag === 'undefined')
if (typeof tag === "undefined")
{
tag = this._name;
}
// TODO
/*
const videoClip = new VideoClip({
psychoJS: this._psychoJS,
name: tag,
format: this._format,
data: new Blob(this._videoBuffer)
});
return videoClip;
*/
}
@ -455,16 +456,9 @@ export class Camera extends PsychObject
this._videos = [];
// create a new stream with ideal dimensions:
// TODO use size constraints
this._stream = await navigator.mediaDevices.getUserMedia({
video: true
/*video: {
width: {
ideal: 640 //1920
},
height: {
ideal: 480 //1080
}
}*/
});
// check the actual width and height:
@ -474,7 +468,7 @@ export class Camera extends PsychObject
// check that the specified format is supported, use default if it is not:
let options;
if (typeof this._format === 'string' && MediaRecorder.isTypeSupported(this._format))
if (typeof this._format === "string" && MediaRecorder.isTypeSupported(this._format))
{
options = { type: this._format };
}
@ -499,7 +493,7 @@ export class Camera extends PsychObject
self._videoBuffer.length = 0;
self._clock.reset();
self._status = PsychoJS.Status.STARTED;
self._psychoJS.logger.debug('video recording started');
self._psychoJS.logger.debug("video recording started");
// resolve the Microphone.start promise:
if (self._startCallback)
@ -512,7 +506,7 @@ export class Camera extends PsychObject
this._recorder.onpause = () =>
{
self._status = PsychoJS.Status.PAUSED;
self._psychoJS.logger.debug('video recording paused');
self._psychoJS.logger.debug("video recording paused");
// resolve the Microphone.pause promise:
if (self._pauseCallback)
@ -525,7 +519,7 @@ export class Camera extends PsychObject
this._recorder.onresume = () =>
{
self._status = PsychoJS.Status.STARTED;
self._psychoJS.logger.debug('video recording resumed');
self._psychoJS.logger.debug("video recording resumed");
// resolve the Microphone.resume promise:
if (self._resumeCallback)
@ -541,7 +535,7 @@ export class Camera extends PsychObject
// add data to the buffer:
self._videoBuffer.push(data);
self._psychoJS.logger.debug('video data added to the buffer');
self._psychoJS.logger.debug("video data added to the buffer");
// resolve the data available promise, if needed:
if (self._dataAvailableCallback)
@ -553,7 +547,7 @@ export class Camera extends PsychObject
// called upon Camera.stop(), after data has been made available:
this._recorder.onstop = () =>
{
self._psychoJS.logger.debug('video recording stopped');
self._psychoJS.logger.debug("video recording stopped");
self._status = PsychoJS.Status.NOT_STARTED;
// resolve the Microphone.stop promise:
@ -565,7 +559,7 @@ export class Camera extends PsychObject
// treat stop options if there are any:
// download to a file, immediately offered to the participant:
if (typeof self._stopOptions.filename === 'string')
if (typeof self._stopOptions.filename === "string")
{
self.download(self._stopOptions.filename);
}
@ -575,7 +569,7 @@ export class Camera extends PsychObject
this._recorder.onerror = (event) =>
{
// TODO
self._psychoJS.logger.error('video recording error: ' + JSON.stringify(event));
self._psychoJS.logger.error("video recording error: " + JSON.stringify(event));
self._status = PsychoJS.Status.ERROR;
};

View File

@ -7,17 +7,19 @@
* @license Distributed under the terms of the MIT License
*/
import {PsychoJS} from "../core/PsychoJS";
import * as util from '../util/Util';
import {Color} from '../util/Color';
import {Camera} from "./Camera";
import {VisualStim} from "./VisualStim";
import {PsychoJS} from "../core/PsychoJS.js";
import * as util from "../util/Util.js";
import { to_pixiPoint } from "../util/Pixi.js";
import {Color} from "../util/Color.js";
import {Camera} from "./Camera.js";
import {VisualStim} from "./VisualStim.js";
import * as PIXI from "pixi.js-legacy";
/**
* <p>This manager handles the detecting of faces in video streams.</p>
* <p>The detection is performed using the Face-API library: https://github.com/justadudewhohacks/face-api.js</p>
* <p>This manager handles the detecting of faces in video streams. FaceDetector relies on the
* [Face-API library]{@link https://github.com/justadudewhohacks/face-api.js} developed by
* [Vincent Muehler]{@link https://github.com/justadudewhohacks}</p>
*
* @name module:visual.FaceDetector
* @class
@ -39,17 +41,20 @@ import * as PIXI from "pixi.js-legacy";
*/
export class FaceDetector extends VisualStim
{
/**
* @constructor
* @public
*/
constructor({name, win, input, modelDir, faceApiUrl, units, ori, opacity, pos, size, autoDraw, autoLog} = {})
{
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
// TODO deal with onChange (see MovieStim and Camera)
this._addAttribute('input', input, undefined);
this._addAttribute('faceApiUrl', faceApiUrl, 'face-api.js');
this._addAttribute('modelDir', modelDir, 'models');
this._addAttribute('autoLog', autoLog, false);
this._addAttribute('status', PsychoJS.Status.NOT_STARTED);
this._addAttribute("input", input, undefined);
this._addAttribute("faceApiUrl", faceApiUrl, "face-api.js");
this._addAttribute("modelDir", modelDir, "models");
this._addAttribute("autoLog", autoLog, false);
this._addAttribute("status", PsychoJS.Status.NOT_STARTED);
// init face-api:
this._initFaceApi();
@ -65,6 +70,7 @@ export class FaceDetector extends VisualStim
* Setter for the video attribute.
*
* @name module:visual.FaceDetector#setCamera
* @function
* @public
* @param {string | HTMLVideoElement | module:visual.Camera} input - the name of a
* movie resource or a HTMLVideoElement or a Camera component
@ -73,23 +79,23 @@ export class FaceDetector extends VisualStim
setInput(input, log = false)
{
const response = {
origin: 'FaceDetector.setInput',
context: 'when setting the video of FaceDetector: ' + this._name
origin: "FaceDetector.setInput",
context: "when setting the video of FaceDetector: " + this._name
};
try
{
// movie is undefined: that's fine but we raise a warning in case this is
// a symptom of an actual problem
if (typeof input === 'undefined')
if (typeof input === "undefined")
{
this.psychoJS.logger.warn('setting the movie of MovieStim: ' + this._name + ' with argument: undefined.');
this.psychoJS.logger.debug('set the movie of MovieStim: ' + this._name + ' as: undefined');
this.psychoJS.logger.warn("setting the movie of MovieStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the movie of MovieStim: " + this._name + " as: undefined");
}
else
{
// if movie is a string, then it should be the name of a resource, which we get:
if (typeof input === 'string')
if (typeof input === "string")
{
// TODO create a movie with that resource, and use the movie as input
}
@ -106,7 +112,7 @@ export class FaceDetector extends VisualStim
// check that video is now an HTMLVideoElement
if (!(input instanceof HTMLVideoElement))
{
throw input.toString() + ' is not a video';
throw input.toString() + " is not a video";
}
this.psychoJS.logger.debug(`set the video of FaceDetector: ${this._name} as: src= ${input.src}, size= ${input.videoWidth}x${input.videoHeight}, duration= ${input.duration}s`);
@ -123,7 +129,7 @@ export class FaceDetector extends VisualStim
}
}
this._setAttribute('input', input, log);
this._setAttribute("input", input, log);
this._needUpdate = true;
this._needPixiUpdate = true;
}
@ -138,6 +144,7 @@ export class FaceDetector extends VisualStim
* Start detecting faces.
*
* @name module:visual.FaceDetector#start
* @function
* @public
* @param {number} period - the detection period, in ms (e.g. 100 ms for 10Hz)
* @param detectionCallback - the callback triggered when detection results are available
@ -147,7 +154,7 @@ export class FaceDetector extends VisualStim
{
this.status = PsychoJS.Status.STARTED;
if (typeof this._detectionId !== 'undefined')
if (typeof this._detectionId !== "undefined")
{
clearInterval(this._detectionId);
this._detectionId = undefined;
@ -176,6 +183,7 @@ export class FaceDetector extends VisualStim
* Stop detecting faces.
*
* @name module:visual.FaceDetector#stop
* @function
* @public
* @param {boolean} [log= false] - whether of not to log
*/
@ -183,7 +191,7 @@ export class FaceDetector extends VisualStim
{
this.status = PsychoJS.Status.NOT_STARTED;
if (typeof this._detectionId !== 'undefined')
if (typeof this._detectionId !== "undefined")
{
clearInterval(this._detectionId);
this._detectionId = undefined;
@ -195,16 +203,17 @@ export class FaceDetector extends VisualStim
* Init the Face-API library.
*
* @name module:visual.FaceDetector#_initFaceApi
* @private
* @function
* @protected
*/
async _initFaceApi()
{/*
// load the library:
await this._psychoJS.serverManager.prepareResources([
{
'name': 'face-api.js',
'path': this.faceApiUrl,
'download': true
"name": "face-api.js",
"path": this.faceApiUrl,
"download": true
}
]);*/
@ -220,7 +229,8 @@ export class FaceDetector extends VisualStim
* Update the visual representation of the detected faces, if necessary.
*
* @name module:visual.FaceDetector#_updateIfNeeded
* @private
* @function
* @protected
*/
_updateIfNeeded()
{
@ -234,7 +244,7 @@ export class FaceDetector extends VisualStim
{
this._needPixiUpdate = false;
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.destroy(true);
}
@ -246,7 +256,7 @@ export class FaceDetector extends VisualStim
this._pixi.addChild(this._body);
const size_px = util.to_px(this.size, this.units, this.win);
if (typeof this._detections !== 'undefined')
if (typeof this._detections !== "undefined")
{
for (const detection of this._detections)
{
@ -256,7 +266,7 @@ export class FaceDetector extends VisualStim
for (const position of landmarks.positions)
{
this._body.beginFill(new Color('red').int, this._opacity);
this._body.beginFill(new Color("red").int, this._opacity);
this._body.drawCircle(
position._x / imageWidth * size_px[0] - size_px[0] / 2,
position._y / imageHeight * size_px[1] - size_px[1] / 2,
@ -273,7 +283,7 @@ export class FaceDetector extends VisualStim
this._pixi.scale.y = -1;
this._pixi.rotation = this.ori * Math.PI / 180;
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
this._pixi.position = to_pixiPoint(this.pos, this.units, this.win);
this._pixi.alpha = this._opacity;
}
@ -290,15 +300,6 @@ export class FaceDetector extends VisualStim
_estimateBoundingBox()
{
// TODO
/*this._boundingBox = new PIXI.Rectangle(
this._pos[0] + this._getLengthUnits(limits_px[0]),
this._pos[1] + this._getLengthUnits(limits_px[1]),
this._getLengthUnits(limits_px[2] - limits_px[0]),
this._getLengthUnits(limits_px[3] - limits_px[1])
);*/
// TODO take the orientation into account
}
}

View File

@ -7,18 +7,16 @@
* @license Distributed under the terms of the MIT License
*/
import * as PIXI from 'pixi.js-legacy';
import {Color} from '../util/Color';
import {ColorMixin} from '../util/ColorMixin';
import * as util from '../util/Util';
import {TrialHandler} from '../data/TrialHandler';
import {TextStim} from './TextStim';
import {TextBox} from './TextBox';
import {VisualStim} from './VisualStim';
import {Slider} from './Slider';
import * as PIXI from "pixi.js-legacy";
import { TrialHandler } from "../data/TrialHandler.js";
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 { Slider } from "./Slider.js";
import { TextBox } from "./TextBox.js";
import { TextStim } from "./TextStim.js";
import { VisualStim } from "./VisualStim.js";
/**
* Form stimulus.
@ -57,99 +55,127 @@ import {Slider} from './Slider';
*/
export class Form extends util.mix(VisualStim).with(ColorMixin)
{
constructor({name, win, pos, size, units, borderColor, fillColor, itemColor, markerColor, responseColor, color, contrast, opacity, depth, items, randomize, itemPadding, font, fontFamily, bold, italic, fontSize, clipMask, autoDraw, autoLog} = {})
constructor(
{
name,
win,
pos,
size,
units,
borderColor,
fillColor,
itemColor,
markerColor,
responseColor,
color,
contrast,
opacity,
depth,
items,
randomize,
itemPadding,
font,
fontFamily,
bold,
italic,
fontSize,
clipMask,
autoDraw,
autoLog,
} = {},
)
{
super({name, win, units, opacity, depth, pos, size, clipMask, autoDraw, autoLog});
super({ name, win, units, opacity, depth, pos, size, clipMask, autoDraw, autoLog });
this._addAttribute(
'itemPadding',
"itemPadding",
itemPadding,
util.to_unit([20, 0], 'pix', win, this._units)[0],
this._onChange(true, false)
util.to_unit([20, 0], "pix", win, this._units)[0],
this._onChange(true, false),
);
// colors:
this._addAttribute(
'color',
"color",
// Same as itemColor
color,
undefined,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'borderColor',
"borderColor",
borderColor,
fillColor,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'fillColor',
"fillColor",
fillColor,
undefined,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'itemColor',
"itemColor",
itemColor,
undefined,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'markerColor',
"markerColor",
markerColor,
undefined,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'responseColor',
"responseColor",
responseColor,
undefined,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'contrast',
"contrast",
contrast,
1.0,
this._onChange(true, false)
this._onChange(true, false),
);
// fonts:
this._addAttribute(
'font',
"font",
font,
'Arial',
this._onChange(true, true)
"Arial",
this._onChange(true, true),
);
// Not in use at present
this._addAttribute(
'fontFamily',
"fontFamily",
fontFamily,
'Helvetica',
this._onChange(true, true)
"Helvetica",
this._onChange(true, true),
);
this._addAttribute(
'fontSize',
"fontSize",
fontSize,
(this._units === 'pix') ? 14 : 0.03,
this._onChange(true, true)
(this._units === "pix") ? 14 : 0.03,
this._onChange(true, true),
);
this._addAttribute(
'bold',
"bold",
bold,
false,
this._onChange(true, true)
this._onChange(true, true),
);
this._addAttribute(
'italic',
"italic",
italic,
false,
this._onChange(true, true)
this._onChange(true, true),
);
// callback to deal with changes to items:
@ -165,16 +191,17 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
};
this._addAttribute(
'items',
"items",
items,
[],
onItemChange);
onItemChange,
);
this._addAttribute(
'randomize',
"randomize",
randomize,
false,
onItemChange);
onItemChange,
);
this._scrollbarWidth = 0.02;
this._responseTextHeightRatio = 0.8;
@ -191,8 +218,6 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Force a refresh of the stimulus.
*
@ -217,8 +242,6 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Overridden draw that also calls the draw method of all form elements.
*
@ -259,8 +282,6 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._scrollbar.draw();
}
/**
* Overridden hide that also calls the hide method of all form elements.
*
@ -275,7 +296,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
super.hide();
// hide the stimuli:
if (typeof this._items !== 'undefined')
if (typeof this._items !== "undefined")
{
for (let i = 0; i < this._items.length; ++i)
{
@ -297,8 +318,6 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Reset the form.
*
@ -308,7 +327,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
*/
reset()
{
this.psychoJS.logger.debug('reset Form: ', this._name);
this.psychoJS.logger.debug("reset Form: ", this._name);
// reset the stimuli:
for (let i = 0; i < this._items.length; ++i)
@ -326,8 +345,6 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._needUpdate = true;
}
/**
* Collate the questions and responses into a single dataset.
*
@ -351,9 +368,9 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
item.response = responseStim.getRating();
item.rt = responseStim.getRT();
if (typeof item.response === 'undefined')
if (typeof item.response === "undefined")
{
++ nbIncompleteResponse;
++nbIncompleteResponse;
}
}
else if (item.type === Form.Types.FREE_TEXT)
@ -363,7 +380,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
if (item.response.length === 0)
{
++ nbIncompleteResponse;
++nbIncompleteResponse;
}
}
}
@ -371,9 +388,8 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._items._complete = (nbIncompleteResponse === 0);
// return a copy of this._items:
return this._items.map(item => Object.assign({}, item));
return this._items.map((item) => Object.assign({}, item));
}
/**
* Check if the form is complete.
@ -385,7 +401,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
*/
formComplete()
{
//same as complete but might be used by some experiments before 2020.2
// same as complete but might be used by some experiments before 2020.2
this.getData();
return this._items._complete;
}
@ -398,15 +414,21 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
* @param {module:data.ExperimentHandler} experiment - the experiment into which to insert the form data
* @param {string} [format= 'rows'] - whether to insert the data as rows or as columns
*/
addDataToExp(experiment, format = 'rows')
addDataToExp(experiment, format = "rows")
{
const addAsColumns = ['cols', 'columns'].includes(format.toLowerCase());
const addAsColumns = ["cols", "columns"].includes(format.toLowerCase());
const data = this.getData();
const _doNotSave = [
'itemCtrl', 'responseCtrl',
'itemColor', 'options', 'ticks', 'tickLabels',
'responseWidth', 'responseColor', 'layout'
"itemCtrl",
"responseCtrl",
"itemColor",
"options",
"ticks",
"tickLabels",
"responseWidth",
"responseColor",
"layout",
];
for (const item of this.getData())
@ -419,7 +441,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
const columnName = (addAsColumns) ? `${this._name}[${index}]${field}` : `${this._name}${field}`;
experiment.addData(columnName, item[field]);
}
++ index;
++index;
}
if (!addAsColumns)
@ -434,8 +456,6 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Import and process the form items from either a spreadsheet resource files (.csv, .xlsx, etc.) or from an array.
*
@ -446,8 +466,8 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
_processItems()
{
const response = {
origin: 'Form._processItems',
context: 'when processing the form items'
origin: "Form._processItems",
context: "when processing the form items",
};
try
@ -455,7 +475,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
if (this._autoLog)
{
// note: we use the same log message as PsychoPy even though we called this method differently
this._psychoJS.experimentLogger.exp('Importing items...');
this._psychoJS.experimentLogger.exp("Importing items...");
}
// import the items:
@ -473,12 +493,10 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
catch (error)
{
// throw { ...response, error };
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Import the form items from either a spreadsheet resource files (.csv, .xlsx, etc.) or from an array.
*
@ -489,8 +507,8 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
_importItems()
{
const response = {
origin: 'Form._importItems',
context: 'when importing the form items'
origin: "Form._importItems",
context: "when importing the form items",
};
try
@ -498,17 +516,15 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
const itemsType = typeof this._items;
// we treat undefined items as a list with a single default entry:
if (itemsType === 'undefined')
if (itemsType === "undefined")
{
this._items = [Form._defaultItems];
}
// if items is a string, we treat it as the name of a resource file and import it:
else if (itemsType === 'string')
else if (itemsType === "string")
{
this._items = TrialHandler.importConditions(this._psychoJS.serverManager, this._items);
}
// unknown items type:
else
{
@ -520,17 +536,14 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
{
this._items = [Form._defaultItems];
}
}
catch (error)
{
// throw { ...response, error };
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Sanitize the form items: check that the keys are valid, and fill in default values.
*
@ -541,8 +554,8 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
_sanitizeItems()
{
const response = {
origin: 'Form._sanitizeItems',
context: 'when sanitizing the form items'
origin: "Form._sanitizeItems",
context: "when sanitizing the form items",
};
try
@ -551,7 +564,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
for (const item of this._items)
{
// old style forms have questionText instead of itemText:
if (typeof item.questionText !== 'undefined')
if (typeof item.questionText !== "undefined")
{
item.itemText = item.questionText;
delete item.questionText;
@ -560,12 +573,11 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
delete item.questionWidth;
// for items of type 'rating, the ticks are in 'options' instead of in 'ticks':
if (item.type === 'rating' || item.type === 'slider')
if (item.type === "rating" || item.type === "slider")
{
item.ticks = item.options;
item.options = undefined;
}
}
}
@ -583,9 +595,8 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
missingKeys.add(key);
item[key] = Form._defaultItems[key];
}
// undefined value:
else if (typeof item[key] === 'undefined')
else if (typeof item[key] === "undefined")
{
// TODO: options = '' for FREE_TEXT
item[key] = Form._defaultItems[key];
@ -595,16 +606,17 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
if (missingKeys.size > 0)
{
this._psychoJS.logger.warn(`Missing headers: ${Array.from(missingKeys).join(', ')}\nNote, headers are case sensitive and must match: ${Array.from(defaultKeys).join(', ')}`);
this._psychoJS.logger.warn(
`Missing headers: ${Array.from(missingKeys).join(", ")}\nNote, headers are case sensitive and must match: ${Array.from(defaultKeys).join(", ")}`,
);
}
// check the types and options:
const formTypes = Object.getOwnPropertyNames(Form.Types);
for (const item of this._items)
{
// convert type to upper case, replace spaces by underscores
item.type = item.type.toUpperCase().replace(' ', '_');
item.type = item.type.toUpperCase().replace(" ", "_");
// check that the type is valid:
if (!formTypes.includes(item.type))
@ -613,9 +625,9 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
// Support the 'radio' type found on older versions of PsychoPy
if (item.type === 'RADIO')
if (item.type === "RADIO")
{
item.type = 'CHOICE';
item.type = "CHOICE";
}
// convert item type to symbol:
@ -624,18 +636,17 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
// turn the option into an array and check length, where applicable:
if (item.type === Form.Types.CHOICE)
{
item.options = item.options.split(',');
item.options = item.options.split(",");
if (item.options.length < 2)
{
throw `at least two choices should be provided for choice item: ${item.itemText}`;
}
}
// turn the ticks and tickLabels into arrays, where applicable:
else if (item.type === Form.Types.RATING || item.type === Form.Types.SLIDER)
{
item.ticks = item.ticks.split(',').map( (_,t) => parseInt(t) );
item.tickLabels = (item.tickLabels.length > 0) ? item.tickLabels.split(',') : [];
item.ticks = item.ticks.split(",").map((_, t) => parseInt(t));
item.tickLabels = (item.tickLabels.length > 0) ? item.tickLabels.split(",") : [];
}
// TODO
@ -644,7 +655,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
// check the layout:
const formLayouts = ['HORIZ', 'VERT'];
const formLayouts = ["HORIZ", "VERT"];
for (const item of this._items)
{
// convert layout to upper case:
@ -657,18 +668,16 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
// convert item layout to symbol:
item.layout = (item.layout === 'HORIZ') ? Form.Layout.HORIZONTAL : Form.Layout.VERTICAL;
item.layout = (item.layout === "HORIZ") ? Form.Layout.HORIZONTAL : Form.Layout.VERTICAL;
}
}
catch (error)
{
// throw { ...response, error };
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Estimate the bounding box.
*
@ -684,12 +693,10 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._pos[0] - this._size[0] / 2.0,
this._pos[1] - this._size[1] / 2.0,
this._size[0],
this._size[1]
this._size[1],
);
}
/**
* Setup the stimuli, and the scrollbar.
*
@ -705,7 +712,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
// clean up the previously setup stimuli:
if (typeof this._visual !== 'undefined')
if (typeof this._visual !== "undefined")
{
for (const textStim of this._visual.textStims)
{
@ -723,31 +730,30 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
textStims: [],
responseStims: [],
visibles: [],
stimuliTotalHeight: 0
stimuliTotalHeight: 0,
};
// instantiate the clip mask that will be used by all stimuli:
this._stimuliClipMask = new PIXI.Graphics();
// default stimulus options:
const textStimOption = {
win: this._win,
name: 'item text',
name: "item text",
font: this.font,
units: this._units,
alignHoriz: 'left',
alignVert: 'top',
alignHoriz: "left",
alignVert: "top",
height: this._fontSize,
color: this.itemColor,
ori: 0,
opacity: 1,
depth: this._depth + 1,
clipMask: this._stimuliClipMask
clipMask: this._stimuliClipMask,
};
const sliderOption = {
win: this._win,
name: 'choice response',
name: "choice response",
units: this._units,
flip: false,
// Not part of Slider options as things stand
@ -762,13 +768,13 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
opacity: 1,
depth: this._depth + 1,
clipMask: this._stimuliClipMask,
granularity: 1
granularity: 1,
};
const textBoxOption = {
win: this._win,
name: 'free text response',
name: "free text response",
units: this._units,
anchor: 'left-top',
anchor: "left-top",
flip: false,
opacity: 1,
depth: this._depth + 1,
@ -776,7 +782,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
letterHeight: this._fontSize * this._responseTextHeightRatio,
bold: false,
italic: false,
alignment: 'left',
alignment: "left",
color: this.responseColor,
fillColor: this.fillColor,
contrast: 1.0,
@ -784,17 +790,16 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
borderWidth: 0.002,
padding: 0.01,
editable: true,
clipMask: this._stimuliClipMask
clipMask: this._stimuliClipMask,
};
// we use for the slider's tick size the height of a word:
const textStim = new TextStim(Object.assign(textStimOption, { text: 'Ag', pos: [0, 0]}));
const textStim = new TextStim(Object.assign(textStimOption, { text: "Ag", pos: [0, 0] }));
const textMetrics_px = textStim.getTextMetrics();
const sliderTickSize = this._getLengthUnits(textMetrics_px.height) / 2;
textStim.release(false);
let stimulusOffset = - this._itemPadding;
let stimulusOffset = -this._itemPadding;
for (const item of this._items)
{
// initially, all items are invisible:
@ -805,8 +810,10 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
// - description: <padding> + <item> + <padding> + <scrollbar> = this._size[0]
// - choice with vert layout: <padding> + <item> + <padding> + <scrollbar> = this._size[0]
let rowWidth;
if (item.type === Form.Types.HEADING || item.type === Form.Types.DESCRIPTION ||
(item.type === Form.Types.CHOICE && item.layout === Form.Layout.VERTICAL))
if (
item.type === Form.Types.HEADING || item.type === Form.Types.DESCRIPTION
|| (item.type === Form.Types.CHOICE && item.layout === Form.Layout.VERTICAL)
)
{
rowWidth = (this._size[0] - this._itemPadding * 2 - this._scrollbarWidth);
}
@ -817,12 +824,13 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
// item text
const itemWidth = rowWidth * item.itemWidth;
const itemWidth = rowWidth * item.itemWidth;
const textStim = new TextStim(
Object.assign(textStimOption, {
text: item.itemText,
wrapWidth: itemWidth
}));
wrapWidth: itemWidth,
}),
);
textStim._relativePos = [this._itemPadding, stimulusOffset];
const textHeight = textStim.boundingBox.height;
this._visual.textStims.push(textStim);
@ -846,7 +854,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
else
{
sliderSize = [sliderTickSize, (sliderTickSize*1.5) * item.options.length];
sliderSize = [sliderTickSize, (sliderTickSize * 1.5) * item.options.length];
compact = false;
flip = true;
}
@ -881,23 +889,23 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
labels,
ticks,
compact,
flip
})
flip,
}),
);
responseHeight = responseStim.boundingBox.height;
if (item.layout === Form.Layout.HORIZONTAL)
{
responseStim._relativePos = [
this._itemPadding * 2 + itemWidth + responseWidth / 2,
stimulusOffset
//- Math.max(0, (textHeight - responseHeight) / 2) // (vertical centering)
stimulusOffset,
// - Math.max(0, (textHeight - responseHeight) / 2) // (vertical centering)
];
}
else
{
responseStim._relativePos = [
this._itemPadding * 2 + itemWidth, //this._itemPadding + sliderTickSize,
stimulusOffset - responseHeight / 2 - textHeight - this._itemPadding
this._itemPadding * 2 + itemWidth, // this._itemPadding + sliderTickSize,
stimulusOffset - responseHeight / 2 - textHeight - this._itemPadding,
];
// since rowHeight will be the max of itemHeight and responseHeight, we need to alter responseHeight
@ -905,20 +913,19 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
responseHeight += textHeight + this._itemPadding;
}
}
// FREE TEXT
else if (item.type === Form.Types.FREE_TEXT)
{
responseStim = new TextBox(
Object.assign(textBoxOption, {
text: item.options,
size: [responseWidth, -1]
})
size: [responseWidth, -1],
}),
);
responseHeight = responseStim.boundingBox.height;
responseStim._relativePos = [
this._itemPadding * 2 + itemWidth,
stimulusOffset
stimulusOffset,
];
}
@ -931,13 +938,12 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
}
this._visual.stimuliTotalHeight = stimulusOffset;
// scrollbar
// note: we add this Form as a dependent stimulus such that the Form is redrawn whenever
// the slider is updated
this._scrollbar = new Slider({
win: this._win,
name: 'scrollbar',
name: "scrollbar",
units: this._units,
color: this.itemColor,
depth: this._depth + 1,
@ -945,24 +951,20 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
size: [this._scrollbarWidth, this._size[1]],
style: [Slider.Style.SLIDER],
ticks: [0, -this._visual.stimuliTotalHeight / this._size[1]],
dependentStims: [this]
dependentStims: [this],
});
this._prevScrollbarMarkerPos = 0;
this._scrollbar.setMarkerPos(this._prevScrollbarMarkerPos);
// estimate the bounding box:
this._estimateBoundingBox();
if (this._autoLog)
{
this._psychoJS.experimentLogger.exp(`Layout set for: ${this.name}`);
}
}
/**
* Update the form visual representation, if necessary.
*
@ -990,17 +992,18 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
[this._leftEdge, this._topEdge],
this.units,
this.win,
true);
true,
);
[this._rightEdge_px, this._bottomEdge_px] = util.to_px(
[this._rightEdge, this._bottomEdge],
this.units,
this.win,
true);
true,
);
this._itemPadding_px = this._getLengthPix(this._itemPadding);
this._scrollbarWidth_px = this._getLengthPix(this._scrollbarWidth, true);
this._size_px = util.to_px(this._size, this.units, this.win, true);
// update the stimuli clip mask
// note: the clip mask is in screen coordinates
this._stimuliClipMask.clear();
@ -1009,11 +1012,10 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._win._rootContainer.position.x + this._leftEdge_px + 2,
this._win._rootContainer.position.y + this._bottomEdge_px + 2,
this._size_px[0] - 4,
this._size_px[1] - 6
this._size_px[1] - 6,
);
this._stimuliClipMask.endFill();
// position the scrollbar and get the scrollbar offset, in form units:
this._scrollbar.setPos([this._rightEdge - this._scrollbarWidth / 2, this._pos[1]], false);
this._scrollbar.setOpacity(0.5);
@ -1024,8 +1026,6 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._updateDecorations();
}
/**
* Update the visible stimuli.
*
@ -1041,7 +1041,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
const textStim = this._visual.textStims[i];
const textStimPos = [
this._leftEdge + textStim._relativePos[0],
this._topEdge + textStim._relativePos[1] - this._scrollbarOffset
this._topEdge + textStim._relativePos[1] - this._scrollbarOffset,
];
textStim.setPos(textStimPos);
@ -1051,7 +1051,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
{
const responseStimPos = [
this._leftEdge + responseStim._relativePos[0],
this._topEdge + responseStim._relativePos[1] - this._scrollbarOffset
this._topEdge + responseStim._relativePos[1] - this._scrollbarOffset,
];
responseStim.setPos(responseStimPos);
}
@ -1077,11 +1077,8 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._visual.visibles[i] = false;
}
}
}
/**
* Update the form decorations (bounding box, lines between items, etc.)
*
@ -1091,7 +1088,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
*/
_updateDecorations()
{
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.destroy(true);
}
@ -1100,7 +1097,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._pixi.scale.x = 1;
this._pixi.scale.y = 1;
this._pixi.rotation = 0;
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
this._pixi.position = to_pixiPoint(this.pos, this.units, this.win);
this._pixi.alpha = this._opacity;
this._pixi.zIndex = this._depth;
@ -1108,7 +1105,6 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
// apply the form clip mask (n.b., that is not the stimuli clip mask):
this._pixi.mask = this._clipMask;
// form background:
this._pixi.lineStyle(1, new Color(this.borderColor).int, this._opacity, 0.5);
// this._decorations.beginFill(this._barFillColor.int, this._opacity);
@ -1121,7 +1117,7 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
this._decorations = new PIXI.Graphics();
this._pixi.addChild(this._decorations);
this._decorations.mask = this._stimuliClipMask;
this._decorations.lineStyle(1, new Color('gray').int, this._opacity, 0.5);
this._decorations.lineStyle(1, new Color("gray").int, this._opacity, 0.5);
this._decorations.alpha = 0.5;
for (let i = 0; i < this._items.length; ++i)
@ -1135,27 +1131,23 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
const textStim = this._visual.textStims[i];
const textStimPos = [
this._leftEdge + textStim._relativePos[0],
this._topEdge + textStim._relativePos[1] - this._scrollbarOffset
this._topEdge + textStim._relativePos[1] - this._scrollbarOffset,
];
const textStimPos_px = util.to_px(textStimPos, this._units, this._win);
this._decorations.beginFill(new Color('darkgray').int);
this._decorations.beginFill(new Color("darkgray").int);
this._decorations.drawRect(
textStimPos_px[0] - this._itemPadding_px / 2,
textStimPos_px[1] + this._itemPadding_px / 2,
this._size_px[0] - this._itemPadding_px - this._scrollbarWidth_px,
-this._getLengthPix(this._visual.rowHeights[i]) - this._itemPadding_px
-this._getLengthPix(this._visual.rowHeights[i]) - this._itemPadding_px,
);
this._decorations.endFill();
}
}
}
}
}
/**
* Form item types.
*
@ -1164,17 +1156,15 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
* @public
*/
Form.Types = {
HEADING: Symbol.for('HEADING'),
DESCRIPTION: Symbol.for('DESCRIPTION'),
RATING: Symbol.for('RATING'),
SLIDER: Symbol.for('SLIDER'),
FREE_TEXT: Symbol.for('FREE_TEXT'),
CHOICE: Symbol.for('CHOICE'),
RADIO: Symbol.for('RADIO')
HEADING: Symbol.for("HEADING"),
DESCRIPTION: Symbol.for("DESCRIPTION"),
RATING: Symbol.for("RATING"),
SLIDER: Symbol.for("SLIDER"),
FREE_TEXT: Symbol.for("FREE_TEXT"),
CHOICE: Symbol.for("CHOICE"),
RADIO: Symbol.for("RADIO"),
};
/**
* Form item layout.
*
@ -1183,12 +1173,10 @@ Form.Types = {
* @public
*/
Form.Layout = {
HORIZONTAL: Symbol.for('HORIZONTAL'),
VERTICAL: Symbol.for('VERTICAL')
HORIZONTAL: Symbol.for("HORIZONTAL"),
VERTICAL: Symbol.for("VERTICAL"),
};
/**
* Default form item.
*
@ -1197,18 +1185,16 @@ Form.Layout = {
*
*/
Form._defaultItems = {
'itemText': 'Default question',
'type': 'rating',
'options': 'Yes, No',
'tickLabels': '',
'itemWidth': 0.7,
'itemColor': 'white',
"itemText": "Default question",
"type": "rating",
"options": "Yes, No",
"tickLabels": "",
"itemWidth": 0.7,
"itemColor": "white",
'responseWidth': 0.3,
'responseColor': 'white',
"responseWidth": 0.3,
"responseColor": "white",
'index': 0,
'layout': 'horiz'
"index": 0,
"layout": "horiz",
};

View File

@ -7,13 +7,12 @@
* @license Distributed under the terms of the MIT License
*/
import * as PIXI from 'pixi.js-legacy';
import {VisualStim} from './VisualStim';
import {Color} from '../util/Color';
import {ColorMixin} from '../util/ColorMixin';
import * as util from '../util/Util';
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";
/**
* Image Stimulus.
@ -45,53 +44,53 @@ import * as util from '../util/Util';
*/
export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
{
constructor({name, win, image, mask, pos, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog} = {})
constructor({ name, win, image, mask, pos, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {})
{
super({name, win, units, ori, opacity, depth, pos, size, autoDraw, autoLog});
super({ name, win, units, ori, opacity, depth, pos, size, autoDraw, autoLog });
this._addAttribute(
'image',
image
"image",
image,
);
this._addAttribute(
'mask',
mask
"mask",
mask,
);
this._addAttribute(
'color',
"color",
color,
'white',
this._onChange(true, false)
"white",
this._onChange(true, false),
);
this._addAttribute(
'contrast',
"contrast",
contrast,
1.0,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'texRes',
"texRes",
texRes,
128,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'interpolate',
"interpolate",
interpolate,
false,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'flipHoriz',
"flipHoriz",
flipHoriz,
false,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'flipVert',
"flipVert",
flipVert,
false,
this._onChange(false, false)
this._onChange(false, false),
);
// estimate the bounding box:
@ -103,8 +102,6 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Setter for the image attribute.
*
@ -116,22 +113,22 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
setImage(image, log = false)
{
const response = {
origin: 'ImageStim.setImage',
context: 'when setting the image of ImageStim: ' + this._name
origin: "ImageStim.setImage",
context: "when setting the image of ImageStim: " + this._name,
};
try
{
// image is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
if (typeof image === 'undefined')
if (typeof image === "undefined")
{
this.psychoJS.logger.warn('setting the image of ImageStim: ' + this._name + ' with argument: undefined.');
this.psychoJS.logger.debug('set the image of ImageStim: ' + this._name + ' as: undefined');
this.psychoJS.logger.warn("setting the image of ImageStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the image of ImageStim: " + this._name + " as: undefined");
}
else
{
// image is a string: it should be the name of a resource, which we load
if (typeof image === 'string')
if (typeof image === "string")
{
image = this.psychoJS.serverManager.getResource(image);
}
@ -139,16 +136,16 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
// image should now be an actual HTMLImageElement: we raise an error if it is not
if (!(image instanceof HTMLImageElement))
{
throw 'the argument: ' + image.toString() + ' is not an image" }';
throw "the argument: " + image.toString() + ' is not an image" }';
}
this.psychoJS.logger.debug('set the image of ImageStim: ' + this._name + ' as: src= ' + image.src + ', size= ' + image.width + 'x' + image.height);
this.psychoJS.logger.debug("set the image of ImageStim: " + this._name + " as: src= " + image.src + ", size= " + image.width + "x" + image.height);
}
const existingImage = this.getImage();
const hasChanged = existingImage ? existingImage.src !== image.src : true;
this._setAttribute('image', image, log);
this._setAttribute("image", image, log);
if (hasChanged)
{
@ -157,12 +154,10 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
}
catch (error)
{
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Setter for the mask attribute.
*
@ -174,22 +169,22 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
setMask(mask, log = false)
{
const response = {
origin: 'ImageStim.setMask',
context: 'when setting the mask of ImageStim: ' + this._name
origin: "ImageStim.setMask",
context: "when setting the mask of ImageStim: " + 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')
if (typeof mask === "undefined")
{
this.psychoJS.logger.warn('setting the mask of ImageStim: ' + this._name + ' with argument: undefined.');
this.psychoJS.logger.debug('set the mask of ImageStim: ' + this._name + ' as: undefined');
this.psychoJS.logger.warn("setting the mask of ImageStim: " + this._name + " with argument: undefined.");
this.psychoJS.logger.debug("set the mask of ImageStim: " + this._name + " as: undefined");
}
else
{
// mask is a string: it should be the name of a resource, which we load
if (typeof mask === 'string')
if (typeof mask === "string")
{
mask = this.psychoJS.serverManager.getResource(mask);
}
@ -197,24 +192,22 @@ export class ImageStim 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 ImageStim: ' + this._name + ' as: src= ' + mask.src + ', size= ' + mask.width + 'x' + mask.height);
this.psychoJS.logger.debug("set the mask of ImageStim: " + this._name + " as: src= " + mask.src + ", size= " + mask.width + "x" + mask.height);
}
this._setAttribute('mask', mask, log);
this._setAttribute("mask", mask, log);
this._onChange(true, false)();
}
catch (error)
{
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Estimate the bounding box.
*
@ -226,21 +219,19 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
_estimateBoundingBox()
{
const size = this._getDisplaySize();
if (typeof size !== 'undefined')
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]
size[1],
);
}
// TODO take the orientation into account
}
/**
* Update the stimulus, if necessary.
*
@ -260,14 +251,14 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
{
this._needPixiUpdate = false;
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.destroy(true);
}
this._pixi = undefined;
// no image to draw: return immediately
if (typeof this._image === 'undefined')
if (typeof this._image === "undefined")
{
return;
}
@ -278,7 +269,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
this._pixi = PIXI.Sprite.from(this._texture);
// add a mask if need be:
if (typeof this._mask !== 'undefined')
if (typeof this._mask !== "undefined")
{
this._pixi.mask = PIXI.Sprite.from(this._mask);
@ -320,7 +311,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
// set the position, rotation, and anchor (image centered on pos):
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
this._pixi.position = to_pixiPoint(this.pos, this.units, this.win);
this._pixi.rotation = this.ori * Math.PI / 180;
this._pixi.anchor.x = 0.5;
this._pixi.anchor.y = 0.5;
@ -329,8 +320,6 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
this._estimateBoundingBox();
}
/**
* Get the size of the display image, which is either that of the ImageStim or that of the image
* it contains.
@ -343,18 +332,16 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
{
let displaySize = this.size;
if (typeof displaySize === 'undefined')
if (typeof displaySize === "undefined")
{
// use the size of the texture, if we have access to it:
if (typeof this._texture !== 'undefined' && this._texture.width > 0)
if (typeof this._texture !== "undefined" && this._texture.width > 0)
{
const textureSize = [this._texture.width, this._texture.height];
displaySize = util.to_unit(textureSize, 'pix', this.win, this.units);
displaySize = util.to_unit(textureSize, "pix", this.win, this.units);
}
}
return displaySize;
}
}

View File

@ -7,13 +7,14 @@
* @license Distributed under the terms of the MIT License
*/
import * as PIXI from 'pixi.js-legacy';
import {VisualStim} from './VisualStim';
import {Color} from '../util/Color';
import * as util from '../util/Util';
import {PsychoJS} from "../core/PsychoJS";
import {Camera} from "./Camera";
import * as PIXI from "pixi.js-legacy";
import { PsychoJS } from "../core/PsychoJS.js";
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 {Camera} from "./Camera.js";
/**
@ -49,82 +50,81 @@ import {Camera} from "./Camera";
*/
export class MovieStim extends VisualStim
{
constructor({name, win, movie, pos, units, ori, size, color, opacity, contrast, interpolate, flipHoriz, flipVert, loop, volume, noAudio, autoPlay, autoDraw, autoLog} = {})
constructor({ name, win, movie, pos, units, ori, size, color, opacity, contrast, interpolate, flipHoriz, flipVert, loop, volume, noAudio, autoPlay, autoDraw, autoLog } = {})
{
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
this.psychoJS.logger.debug('create a new MovieStim with name: ', name);
this.psychoJS.logger.debug("create a new MovieStim with name: ", name);
// movie and movie control:
this._addAttribute(
'movie',
movie
"movie",
movie,
);
this._addAttribute(
'volume',
"volume",
volume,
1.0,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'noAudio',
"noAudio",
noAudio,
false,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'autoPlay',
"autoPlay",
autoPlay,
true,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'flipHoriz',
"flipHoriz",
flipHoriz,
false,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'flipVert',
"flipVert",
flipVert,
false,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'interpolate',
"interpolate",
interpolate,
false,
this._onChange(true, false)
this._onChange(true, false),
);
// colors:
this._addAttribute(
'color',
"color",
color,
'white',
this._onChange(true, false)
"white",
this._onChange(true, false),
);
this._addAttribute(
'contrast',
"contrast",
contrast,
1.0,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'loop',
"loop",
loop,
false,
this._onChange(false, false)
this._onChange(false, false),
);
// estimate the bounding box:
this._estimateBoundingBox();
// check whether the fastSeek method on HTMLVideoElement is implemented:
const videoElement = document.createElement('video');
this._hasFastSeek = (typeof videoElement.fastSeek === 'function');
const videoElement = document.createElement("video");
this._hasFastSeek = (typeof videoElement.fastSeek === "function");
if (this._autoLog)
{
@ -132,8 +132,6 @@ export class MovieStim extends VisualStim
}
}
/**
* Setter for the movie attribute.
*
@ -146,8 +144,8 @@ export class MovieStim extends VisualStim
setMovie(movie, log = false)
{
const response = {
origin: 'MovieStim.setMovie',
context: 'when setting the movie of MovieStim: ' + this._name
origin: "MovieStim.setMovie",
context: "when setting the movie of MovieStim: " + this._name,
};
try
@ -164,7 +162,7 @@ export class MovieStim extends VisualStim
else
{
// if movie is a string, then it should be the name of a resource, which we get:
if (typeof movie === 'string')
if (typeof movie === "string")
{
movie = this.psychoJS.serverManager.getResource(movie);
}
@ -181,7 +179,7 @@ export class MovieStim extends VisualStim
// check that movie is now an HTMLVideoElement
if (!(movie instanceof HTMLVideoElement))
{
throw movie.toString() + ' is not a video';
throw movie.toString() + " is not a video";
}
this.psychoJS.logger.debug(`set the movie of MovieStim: ${this._name} as: src= ${movie.src}, size= ${movie.videoWidth}x${movie.videoHeight}, duration= ${movie.duration}s`);
@ -198,19 +196,16 @@ export class MovieStim extends VisualStim
}
}
this._setAttribute('movie', movie, log);
this._setAttribute("movie", movie, log);
this._needUpdate = true;
this._needPixiUpdate = true;
}
catch (error)
{
throw Object.assign(response, {error});
throw Object.assign(response, { error });
}
}
/**
* Reset the stimulus.
*
@ -223,8 +218,6 @@ export class MovieStim extends VisualStim
this.seek(0, log);
}
/**
* Start playing the movie.
*
@ -239,18 +232,17 @@ export class MovieStim extends VisualStim
if (playPromise !== undefined)
{
playPromise.catch((error) => {
playPromise.catch((error) =>
{
throw {
origin: 'MovieStim.play',
origin: "MovieStim.play",
context: `when attempting to play MovieStim: ${this._name}`,
error
error,
};
});
}
}
/**
* Pause the movie.
*
@ -262,8 +254,6 @@ export class MovieStim extends VisualStim
this._movie.pause();
}
/**
* Stop the movie and reset to 0s.
*
@ -276,8 +266,6 @@ export class MovieStim extends VisualStim
this.seek(0, log);
}
/**
* Jump to a specific timepoint
*
@ -291,9 +279,9 @@ export class MovieStim extends VisualStim
if (timePoint < 0 || timePoint > this._movie.duration)
{
throw {
origin: 'MovieStim.seek',
origin: "MovieStim.seek",
context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`,
error: `the timepoint does not belong to [0, ${this._movie.duration}`
error: `the timepoint does not belong to [0, ${this._movie.duration}`,
};
}
@ -310,16 +298,14 @@ export class MovieStim extends VisualStim
catch (error)
{
throw {
origin: 'MovieStim.seek',
origin: "MovieStim.seek",
context: `when seeking to timepoint: ${timePoint} of MovieStim: ${this._name}`,
error
error,
};
}
}
}
/**
* Estimate the bounding box.
*
@ -331,21 +317,19 @@ export class MovieStim extends VisualStim
_estimateBoundingBox()
{
const size = this._getDisplaySize();
if (typeof size !== 'undefined')
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]
size[1],
);
}
// TODO take the orientation into account
}
/**
* Update the stimulus, if necessary.
*
@ -365,20 +349,20 @@ export class MovieStim extends VisualStim
{
this._needPixiUpdate = false;
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
// Leave original video in place
// https://pixijs.download/dev/docs/PIXI.Sprite.html#destroy
this._pixi.destroy({
children: true,
texture: true,
baseTexture: false
baseTexture: false,
});
}
this._pixi = undefined;
// no movie to draw: return immediately
if (typeof this._movie === 'undefined')
if (typeof this._movie === "undefined")
{
return;
}
@ -416,7 +400,7 @@ export class MovieStim extends VisualStim
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
// set the position, rotation, and anchor (movie centered on pos):
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
this._pixi.position = to_pixiPoint(this.pos, this.units, this.win);
this._pixi.rotation = this.ori * Math.PI / 180;
this._pixi.anchor.x = 0.5;
this._pixi.anchor.y = 0.5;
@ -425,8 +409,6 @@ export class MovieStim extends VisualStim
this._estimateBoundingBox();
}
/**
* Get the size of the display image, which is either that of the ImageStim or that of the image
* it contains.
@ -439,18 +421,16 @@ export class MovieStim extends VisualStim
{
let displaySize = this.size;
if (typeof displaySize === 'undefined')
if (typeof displaySize === "undefined")
{
// use the size of the texture, if we have access to it:
if (typeof this._texture !== 'undefined' && this._texture.width > 0)
if (typeof this._texture !== "undefined" && this._texture.width > 0)
{
const textureSize = [this._texture.width, this._texture.height];
displaySize = util.to_unit(textureSize, 'pix', this.win, this.units);
displaySize = util.to_unit(textureSize, "pix", this.win, this.units);
}
}
return displaySize;
}
}

View File

@ -7,10 +7,8 @@
* @license Distributed under the terms of the MIT License
*/
import {ShapeStim} from './ShapeStim';
import {Color} from '../util/Color';
import { Color } from "../util/Color.js";
import { ShapeStim } from "./ShapeStim.js";
/**
* <p>Polygonal visual stimulus.</p>
@ -39,7 +37,7 @@ import {Color} from '../util/Color';
*/
export class Polygon extends ShapeStim
{
constructor({name, win, lineWidth, lineColor, fillColor, opacity, edges, radius, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog} = {})
constructor({ name, win, lineWidth, lineColor, fillColor, opacity, edges, radius, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog } = {})
{
super({
name,
@ -56,20 +54,20 @@ export class Polygon extends ShapeStim
depth,
interpolate,
autoDraw,
autoLog
autoLog,
});
this._psychoJS.logger.debug('create a new Polygon with name: ', name);
this._psychoJS.logger.debug("create a new Polygon with name: ", name);
this._addAttribute(
'edges',
"edges",
edges,
3
3,
);
this._addAttribute(
'radius',
"radius",
radius,
0.5
0.5,
);
this._updateVertices();
@ -80,8 +78,6 @@ export class Polygon extends ShapeStim
}
}
/**
* Setter for the radius attribute.
*
@ -92,7 +88,7 @@ export class Polygon extends ShapeStim
*/
setRadius(radius, log = false)
{
const hasChanged = this._setAttribute('radius', radius, log);
const hasChanged = this._setAttribute("radius", radius, log);
if (hasChanged)
{
@ -100,8 +96,6 @@ export class Polygon extends ShapeStim
}
}
/**
* Setter for the edges attribute.
*
@ -112,7 +106,7 @@ export class Polygon extends ShapeStim
*/
setEdges(edges, log = false)
{
const hasChanged = this._setAttribute('edges', Math.round(edges), log);
const hasChanged = this._setAttribute("edges", Math.round(edges), log);
if (hasChanged)
{
@ -120,8 +114,6 @@ export class Polygon extends ShapeStim
}
}
/**
* Update the vertices.
*
@ -130,7 +122,7 @@ export class Polygon extends ShapeStim
*/
_updateVertices()
{
this._psychoJS.logger.debug('update the vertices of Polygon: ', this.name);
this._psychoJS.logger.debug("update the vertices of Polygon: ", this.name);
const angle = 2.0 * Math.PI / this._edges;
const vertices = [];
@ -141,5 +133,4 @@ export class Polygon extends ShapeStim
this.setVertices(vertices);
}
}

View File

@ -7,10 +7,8 @@
* @license Distributed under the terms of the MIT License
*/
import {ShapeStim} from './ShapeStim';
import {Color} from '../util/Color';
import { Color } from "../util/Color.js";
import { ShapeStim } from "./ShapeStim.js";
/**
* <p>Rectangular visual stimulus.</p>
@ -39,7 +37,7 @@ import {Color} from '../util/Color';
*/
export class Rect extends ShapeStim
{
constructor({name, win, lineWidth, lineColor, fillColor, opacity, width, height, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog} = {})
constructor({ name, win, lineWidth, lineColor, fillColor, opacity, width, height, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog } = {})
{
super({
name,
@ -56,20 +54,20 @@ export class Rect extends ShapeStim
depth,
interpolate,
autoDraw,
autoLog
autoLog,
});
this._psychoJS.logger.debug('create a new Rect with name: ', name);
this._psychoJS.logger.debug("create a new Rect with name: ", name);
this._addAttribute(
'width',
"width",
width,
0.5
0.5,
);
this._addAttribute(
'height',
"height",
height,
0.5
0.5,
);
this._updateVertices();
@ -80,8 +78,6 @@ export class Rect extends ShapeStim
}
}
/**
* Setter for the width attribute.
*
@ -92,9 +88,9 @@ export class Rect extends ShapeStim
*/
setWidth(width, log = false)
{
this._psychoJS.logger.debug('set the width of Rect: ', this.name, 'to: ', width);
this._psychoJS.logger.debug("set the width of Rect: ", this.name, "to: ", width);
const hasChanged = this._setAttribute('width', width, log);
const hasChanged = this._setAttribute("width", width, log);
if (hasChanged)
{
@ -102,8 +98,6 @@ export class Rect extends ShapeStim
}
}
/**
* Setter for the height attribute.
*
@ -114,9 +108,9 @@ export class Rect extends ShapeStim
*/
setHeight(height, log = false)
{
this._psychoJS.logger.debug('set the height of Rect: ', this.name, 'to: ', height);
this._psychoJS.logger.debug("set the height of Rect: ", this.name, "to: ", height);
const hasChanged = this._setAttribute('height', height, log);
const hasChanged = this._setAttribute("height", height, log);
if (hasChanged)
{
@ -124,8 +118,6 @@ export class Rect extends ShapeStim
}
}
/**
* Update the vertices.
*
@ -134,7 +126,7 @@ export class Rect extends ShapeStim
*/
_updateVertices()
{
this._psychoJS.logger.debug('update the vertices of Rect: ', this.name);
this._psychoJS.logger.debug("update the vertices of Rect: ", this.name);
const halfWidth = this._width / 2.0;
const halfHeight = this._height / 2.0;
@ -143,8 +135,7 @@ export class Rect extends ShapeStim
[-halfWidth, -halfHeight],
[halfWidth, -halfHeight],
[halfWidth, halfHeight],
[-halfWidth, halfHeight]
[-halfWidth, halfHeight],
]);
}
}

View File

@ -8,14 +8,13 @@
* @license Distributed under the terms of the MIT License
*/
import * as PIXI from 'pixi.js-legacy';
import {VisualStim} from './VisualStim';
import {Color} from '../util/Color';
import {ColorMixin} from '../util/ColorMixin';
import * as util from '../util/Util';
import {WindowMixin} from "../core/WindowMixin";
import * as PIXI from "pixi.js-legacy";
import { WindowMixin } from "../core/WindowMixin.js";
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";
/**
* <p>This class provides the basic functionality of shape stimuli.</p>
@ -44,9 +43,9 @@ import {WindowMixin} from "../core/WindowMixin";
*/
export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
{
constructor({name, win, lineWidth, lineColor, fillColor, opacity, vertices, closeShape, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog} = {})
constructor({ name, win, lineWidth, lineColor, fillColor, opacity, vertices, closeShape, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog } = {})
{
super({name, win, units, ori, opacity, pos, depth, size, autoDraw, autoLog});
super({ name, win, units, ori, opacity, pos, depth, size, autoDraw, autoLog });
// the PIXI polygon corresponding to the vertices, in pixel units:
this._pixiPolygon_px = undefined;
@ -54,58 +53,56 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
this._vertices_px = undefined;
// shape:
if (typeof size === 'undefined' || size === null)
if (typeof size === "undefined" || size === null)
{
this.size = [1.0, 1.0];
}
this._addAttribute(
'vertices',
"vertices",
vertices,
[[-0.5, 0], [0, 0.5], [0.5, 0]]
[[-0.5, 0], [0, 0.5], [0.5, 0]],
);
this._addAttribute(
'closeShape',
"closeShape",
closeShape,
true,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'interpolate',
"interpolate",
interpolate,
true,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'lineWidth',
"lineWidth",
lineWidth,
1.5,
this._onChange(true, true)
this._onChange(true, true),
);
// colors:
this._addAttribute(
'lineColor',
"lineColor",
lineColor,
'white',
this._onChange(true, false)
"white",
this._onChange(true, false),
);
this._addAttribute(
'fillColor',
"fillColor",
fillColor,
undefined,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'contrast',
"contrast",
contrast,
1.0,
this._onChange(true, false)
this._onChange(true, false),
);
}
/**
* Setter for the vertices attribute.
*
@ -117,16 +114,16 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
setVertices(vertices, log = false)
{
const response = {
origin: 'ShapeStim.setVertices',
context: 'when setting the vertices of ShapeStim: ' + this._name
origin: "ShapeStim.setVertices",
context: "when setting the vertices of ShapeStim: " + this._name,
};
this._psychoJS.logger.debug('set the vertices of ShapeStim:', this.name);
this._psychoJS.logger.debug("set the vertices of ShapeStim:", this.name);
try
{
// if vertices is a string, we check whether it is a known shape:
if (typeof vertices === 'string')
if (typeof vertices === "string")
{
if (vertices in ShapeStim.KnownShapes)
{
@ -138,18 +135,16 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
}
}
this._setAttribute('vertices', vertices, log);
this._setAttribute("vertices", vertices, log);
this._onChange(true, true)();
}
catch (error)
{
throw Object.assign(response, {error: error});
throw Object.assign(response, { error: error });
}
}
/**
* Determine whether an object is inside the bounding box of the ShapeStim.
*
@ -167,24 +162,22 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
// get the position of the object, in pixel coordinates:
const objectPos_px = util.getPositionFromObject(object, units);
if (typeof objectPos_px === 'undefined')
if (typeof objectPos_px === "undefined")
{
throw {
origin: 'VisualStim.contains',
context: 'when determining whether VisualStim: ' + this._name + ' contains object: ' + util.toString(object),
error: 'unable to determine the position of the object'
origin: "VisualStim.contains",
context: "when determining whether VisualStim: " + this._name + " contains object: " + util.toString(object),
error: "unable to determine the position of the object",
};
}
// test for inclusion:
const pos_px = util.to_px(this.pos, this.units, this.win);
this._getVertices_px();
const polygon_px = this._vertices_px.map(v => [v[0] + pos_px[0], v[1] + pos_px[1]]);
const polygon_px = this._vertices_px.map((v) => [v[0] + pos_px[0], v[1] + pos_px[1]]);
return util.IsPointInsidePolygon(objectPos_px, polygon_px);
}
/**
* Estimate the bounding box.
*
@ -202,7 +195,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY,
Number.NEGATIVE_INFINITY
Number.NEGATIVE_INFINITY,
];
for (const vertex of this._vertices_px)
{
@ -216,14 +209,12 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
this._pos[0] + this._getLengthUnits(limits_px[0]),
this._pos[1] + this._getLengthUnits(limits_px[1]),
this._getLengthUnits(limits_px[2] - limits_px[0]),
this._getLengthUnits(limits_px[3] - limits_px[1])
this._getLengthUnits(limits_px[3] - limits_px[1]),
);
// TODO take the orientation into account
}
/**
* Update the stimulus, if necessary.
*
@ -243,7 +234,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
{
this._needPixiUpdate = false;
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.destroy(true);
}
@ -255,25 +246,23 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
// prepare the polygon in the given color and opacity:
this._pixi = new PIXI.Graphics();
this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5);
if (typeof this._fillColor !== 'undefined' && this._fillColor !== null)
if (typeof this._fillColor !== "undefined" && this._fillColor !== null)
{
const contrastedColor = this.getContrastedColor(new Color(this._fillColor), this._contrast);
this._pixi.beginFill(contrastedColor.int, this._opacity);
}
this._pixi.drawPolygon(this._pixiPolygon_px);
if (typeof this._fillColor !== 'undefined' && this._fillColor !== null)
if (typeof this._fillColor !== "undefined" && this._fillColor !== null)
{
this._pixi.endFill();
}
}
// set polygon position and rotation:
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
this._pixi.position = to_pixiPoint(this.pos, this.units, this.win);
this._pixi.rotation = this.ori * Math.PI / 180.0;
}
/**
* Get the PIXI polygon (in pixel units) corresponding to the vertices.
*
@ -311,8 +300,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
return this._pixiPolygon_px;
}
/**
* Get the vertices in pixel units.
*
@ -324,28 +311,28 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
{
// handle flipping:
let flip = [1.0, 1.0];
if ('_flipHoriz' in this && this._flipHoriz)
if ("_flipHoriz" in this && this._flipHoriz)
{
flip[0] = -1.0;
}
if ('_flipVert' in this && this._flipVert)
if ("_flipVert" in this && this._flipVert)
{
flip[1] = -1.0;
}
// handle size, flipping, and convert to pixel units:
this._vertices_px = this._vertices.map(v => util.to_px(
[v[0] * this._size[0] * flip[0], v[1] * this._size[1] * flip[1]],
this._units,
this._win)
this._vertices_px = this._vertices.map((v) =>
util.to_px(
[v[0] * this._size[0] * flip[0], v[1] * this._size[1] * flip[1]],
this._units,
this._win,
)
);
return this._vertices_px;
}
}
/**
* Known shapes.
*
@ -357,15 +344,15 @@ ShapeStim.KnownShapes = {
[-0.1, +0.5], // up
[+0.1, +0.5],
[+0.1, +0.1],
[+0.5, +0.1], // right
[+0.5, +0.1], // right
[+0.5, -0.1],
[+0.1, -0.1],
[+0.1, -0.5], // down
[+0.1, -0.5], // down
[-0.1, -0.5],
[-0.1, -0.1],
[-0.5, -0.1], // left
[-0.5, -0.1], // left
[-0.5, +0.1],
[-0.1, +0.1]
[-0.1, +0.1],
],
star7: [
@ -382,7 +369,6 @@ ShapeStim.KnownShapes = {
[-0.49, -0.11],
[-0.19, 0.04],
[-0.39, 0.31],
[-0.09, 0.18]
]
[-0.09, 0.18],
],
};

View File

@ -7,16 +7,15 @@
* @license Distributed under the terms of the MIT License
*/
import * as PIXI from 'pixi.js-legacy';
import {VisualStim} from './VisualStim';
import {Color} from '../util/Color';
import {ColorMixin} from '../util/ColorMixin';
import {WindowMixin} from '../core/WindowMixin';
import {Clock} from '../util/Clock';
import * as util from '../util/Util';
import {PsychoJS} from "../core/PsychoJS";
import * as PIXI from "pixi.js-legacy";
import { PsychoJS } from "../core/PsychoJS.js";
import { WindowMixin } from "../core/WindowMixin.js";
import { Clock } from "../util/Clock.js";
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";
/**
* Slider stimulus.
@ -69,9 +68,38 @@ import {PsychoJS} from "../core/PsychoJS";
*/
export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
{
constructor({name, win, pos, size, ori, units, color, markerColor, lineColor, contrast, opacity, style, ticks, labels, granularity, flip, readOnly, font, bold, italic, fontSize, compact, clipMask, autoDraw, autoLog, dependentStims} = {})
constructor(
{
name,
win,
pos,
size,
ori,
units,
color,
markerColor,
lineColor,
contrast,
opacity,
style,
ticks,
labels,
granularity,
flip,
readOnly,
font,
bold,
italic,
fontSize,
compact,
clipMask,
autoDraw,
autoLog,
dependentStims,
} = {},
)
{
super({name, win, units, ori, opacity, pos, size, clipMask, autoDraw, autoLog});
super({ name, win, units, ori, opacity, pos, size, clipMask, autoDraw, autoLog });
this._needMarkerUpdate = false;
@ -95,119 +123,117 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
};
this._addAttribute(
'style',
"style",
style,
[Slider.Style.RATING],
onChange(true, true, true)
onChange(true, true, true),
);
this._addAttribute(
'ticks',
"ticks",
ticks,
[1, 2, 3, 4, 5],
onChange(true, true, true)
onChange(true, true, true),
);
this._addAttribute(
'labels',
"labels",
labels,
[],
onChange(true, true, true)
onChange(true, true, true),
);
this._addAttribute(
'granularity',
"granularity",
granularity,
0,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'readOnly',
"readOnly",
readOnly,
false
false,
);
this._addAttribute(
'compact',
"compact",
compact,
false,
this._onChange(true, true)
this._onChange(true, true),
);
// font:
this._addAttribute(
'font',
"font",
font,
'Arial',
this._onChange(true, true)
"Arial",
this._onChange(true, true),
);
this._addAttribute(
'fontSize',
"fontSize",
fontSize,
(this._units === 'pix') ? 14 : 0.03,
this._onChange(true, true)
(this._units === "pix") ? 14 : 0.03,
this._onChange(true, true),
);
this._addAttribute(
'bold',
"bold",
bold,
true,
this._onChange(true, true)
this._onChange(true, true),
);
this._addAttribute(
'italic',
"italic",
italic,
false,
this._onChange(true, true)
this._onChange(true, true),
);
this._addAttribute(
'flip',
"flip",
flip,
false,
this._onChange(true, true)
this._onChange(true, true),
);
// color:
this._addAttribute(
'color',
"color",
color,
'lightgray',
this._onChange(true, false)
"lightgray",
this._onChange(true, false),
);
this._addAttribute(
'lineColor',
"lineColor",
lineColor,
'lightgray',
this._onChange(true, false)
"lightgray",
this._onChange(true, false),
);
this._addAttribute(
'markerColor',
"markerColor",
markerColor,
'red',
this._onChange(true, false)
"red",
this._onChange(true, false),
);
this._addAttribute(
'contrast',
"contrast",
contrast,
1.0,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'dependentStims',
"dependentStims",
dependentStims,
[],
this._onChange(false, false)
this._onChange(false, false),
);
// slider rating (which might be different from the visible marker rating):
this._addAttribute('rating', undefined);
this._addAttribute("rating", undefined);
// visible marker rating (which might be different from the actual rating):
this._addAttribute('markerPos', undefined);
this._addAttribute("markerPos", undefined);
// full history of ratings and response times:
this._addAttribute('history', []);
this._addAttribute("history", []);
// various graphical components:
this._addAttribute('lineAspectRatio', 0.01);
this._addAttribute("lineAspectRatio", 0.01);
// check for attribute conflicts, missing values, etc.:
this._sanitizeAttributes();
@ -224,8 +250,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Force a refresh of the stimulus.
*
@ -239,8 +263,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._needMarkerUpdate = true;
}
/**
* Reset the slider.
*
@ -249,7 +271,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
*/
reset()
{
this.psychoJS.logger.debug('reset Slider: ', this._name);
this.psychoJS.logger.debug("reset Slider: ", this._name);
this._markerPos = undefined;
this._history = [];
@ -261,14 +283,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._needUpdate = true;
// the marker should be invisible when markerPos is undefined:
if (typeof this._marker !== 'undefined')
if (typeof this._marker !== "undefined")
{
this._marker.alpha = 0;
}
}
/**
* Get the current value of the rating.
*
@ -289,8 +309,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Get the response time of the most recent change to the rating.
*
@ -311,8 +329,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Setter for the readOnly attribute.
*
@ -326,7 +342,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
*/
setReadOnly(readOnly = true, log = false)
{
const hasChanged = this._setAttribute('readOnly', readOnly, log);
const hasChanged = this._setAttribute("readOnly", readOnly, log);
if (hasChanged)
{
@ -344,8 +360,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Setter for the markerPos attribute.
*
@ -371,8 +385,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Setter for the rating attribute.
*
@ -392,60 +404,51 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
rating = this._labels[Math.round(rating)];
}
this._setAttribute('rating', rating, log);
this._setAttribute("rating", rating, log);
}
/** Let `borderColor` alias `lineColor` to parallel PsychoPy */
set borderColor(color) {
set borderColor(color)
{
this.lineColor = color;
}
setBorderColor(color) {
setBorderColor(color)
{
this.setLineColor(color);
}
get borderColor() {
get borderColor()
{
return this.lineColor;
}
getBorderColor() {
getBorderColor()
{
return this.getLineColor();
}
/** Let `fillColor` alias `markerColor` to parallel PsychoPy */
set fillColor(color) {
set fillColor(color)
{
this.markerColor = color;
}
setFillColor(color) {
setFillColor(color)
{
this.setMarkerColor(color);
}
get fillColor() {
get fillColor()
{
return this.markerColor;
}
getFillColor() {
getFillColor()
{
return this.getMarkerColor();
}
/**
* Estimate the bounding box.
*
@ -461,18 +464,18 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
{
// setup the slider's style (taking into account the Window dimension, etc.):
this._setupStyle();
// calculate various values in pixel units:
this._tickSize_px = util.to_px(this._tickSize, this._units, this._win);
this._fontSize_px = this._getLengthPix(this._fontSize);
this._barSize_px = util.to_px(this._barSize, this._units, this._win, true).map(v => Math.max(1, v));
this._barSize_px = util.to_px(this._barSize, this._units, this._win, true).map((v) => Math.max(1, v));
this._markerSize_px = util.to_px(this._markerSize, this._units, this._win, true);
const pos_px = util.to_px(this._pos, this._units, this._win);
const size_px = util.to_px(this._size, this._units, this._win);
// calculate the position of the ticks:
const tickPositions = this._ratingToPos(this._ticks);
this._tickPositions_px = tickPositions.map(p => util.to_px(p, this._units, this._win));
this._tickPositions_px = tickPositions.map((p) => util.to_px(p, this._units, this._win));
// left, top, right, bottom limits:
const limits_px = [0, 0, size_px[0], size_px[1]];
@ -489,7 +492,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
for (let l = 0; l < this._labels.length; ++l)
{
const tickPositionIndex = Math.round( l / (this._labels.length - 1) * (this._ticks.length - 1) );
const tickPositionIndex = Math.round(l / (this._labels.length - 1) * (this._ticks.length - 1));
this._labelPositions_px[l] = this._tickPositions_px[tickPositionIndex];
const labelBounds = PIXI.TextMetrics.measureText(this._labels[l].toString(), labelTextStyle);
@ -514,8 +517,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
// ensure that that labels are not overlapping:
if (prevLabelBounds &&
(this._labelPositions_px[l - 1][0] + prevLabelBounds.width + tolerance >= this._labelPositions_px[l][0]))
if (
prevLabelBounds
&& (this._labelPositions_px[l - 1][0] + prevLabelBounds.width + tolerance >= this._labelPositions_px[l][0])
)
{
if (prevNonOverlapOffset === 0)
{
@ -575,12 +580,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._getLengthUnits(position_px.x + limits_px[0]),
this._getLengthUnits(position_px.y + limits_px[1]),
this._getLengthUnits(limits_px[2] - limits_px[0]),
this._getLengthUnits(limits_px[3] - limits_px[1])
this._getLengthUnits(limits_px[3] - limits_px[1]),
);
}
/**
* Sanitize the slider attributes: check for attribute conflicts, missing values, etc.
*
@ -591,9 +594,9 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
_sanitizeAttributes()
{
// convert potential string styles into Symbols:
this._style.forEach( (style, index) =>
this._style.forEach((style, index) =>
{
if (typeof style === 'string')
if (typeof style === "string")
{
this._style[index] = Symbol.for(style.toUpperCase());
}
@ -605,14 +608,11 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._isCategorical = (this._ticks.length === 0);
if (this._isCategorical)
{
this._ticks = [...Array(this._labels.length)].map( (_, i) => i );
this._ticks = [...Array(this._labels.length)].map((_, i) => i);
this._granularity = 1.0;
}
}
/**
* Set the current rating.
*
@ -628,7 +628,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
recordRating(rating, responseTime = undefined, log = false)
{
// get response time:
if (typeof responseTime === 'undefined')
if (typeof responseTime === "undefined")
{
responseTime = this._responseClock.getTime();
}
@ -639,15 +639,14 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this.setRating(rating, log);
// add rating and response time to history:
this._history.push({rating: this._rating, responseTime});
this.psychoJS.logger.debug('record a new rating: ', this._rating, 'with response time: ', responseTime, 'for Slider: ', this._name);
this._history.push({ rating: this._rating, responseTime });
this.psychoJS.logger.debug("record a new rating: ", this._rating, "with response time: ", responseTime, "for Slider: ", this._name);
// update slider:
this._needMarkerUpdate = true;
this._needUpdate = true;
}
/**
* Update the stimulus, if necessary.
*
@ -681,7 +680,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Estimate the position of the slider, taking the compactness into account.
*
@ -691,9 +689,11 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
*/
_getPosition_px()
{
const position = util.to_pixiPoint(this.pos, this.units, this.win, true);
if (this._compact &&
(this._style.indexOf(Slider.Style.RADIO) > -1 || this._style.indexOf(Slider.Style.RATING) > -1))
const position = to_pixiPoint(this.pos, this.units, this.win, true);
if (
this._compact
&& (this._style.indexOf(Slider.Style.RADIO) > -1 || this._style.indexOf(Slider.Style.RATING) > -1)
)
{
if (this._isHorizontal())
{
@ -708,8 +708,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
return position;
}
/**
* Update the position of the marker if necessary.
*
@ -724,12 +722,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
this._needMarkerUpdate = false;
if (typeof this._marker !== 'undefined')
if (typeof this._marker !== "undefined")
{
if (typeof this._markerPos !== 'undefined')
if (typeof this._markerPos !== "undefined")
{
const visibleMarkerPos = this._ratingToPos([this._markerPos]);
this._marker.position = util.to_pixiPoint(visibleMarkerPos[0], this.units, this.win, true);
this._marker.position = to_pixiPoint(visibleMarkerPos[0], this.units, this.win, true);
this._marker.alpha = 1;
}
else
@ -739,8 +737,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Setup the PIXI components of the slider (bar, ticks, labels, marker, etc.).
*
@ -758,17 +754,15 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._setupStyle();
// calculate various values in pixel units:
this._tickSize_px = util.to_px(this._tickSize, this._units, this._win);
this._fontSize_px = this._getLengthPix(this._fontSize);
this._barSize_px = util.to_px(this._barSize, this._units, this._win, true).map(v => Math.max(1, v));
this._barSize_px = util.to_px(this._barSize, this._units, this._win, true).map((v) => Math.max(1, v));
this._markerSize_px = util.to_px(this._markerSize, this._units, this._win, true);
const tickPositions = this._ratingToPos(this._ticks);
this._tickPositions_px = tickPositions.map(p => util.to_px(p, this._units, this._win));
this._tickPositions_px = tickPositions.map((p) => util.to_px(p, this._units, this._win));
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.destroy(true);
}
@ -781,7 +775,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._body.interactive = true;
this._pixi.addChild(this._body);
// ensure that pointer events will be captured along the slider body, even outside of
// marker and labels:
if (this._tickType === Slider.Shape.DISC)
@ -791,7 +784,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
-this._barSize_px[0] / 2 - maxTickSize_px,
-this._barSize_px[1] / 2 - maxTickSize_px,
this._barSize_px[0] + maxTickSize_px * 2,
this._barSize_px[1] + maxTickSize_px * 2);
this._barSize_px[1] + maxTickSize_px * 2,
);
}
else
{
@ -799,7 +793,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
-this._barSize_px[0] / 2 - this._tickSize_px[0] / 2,
-this._barSize_px[1] / 2 - this._tickSize_px[1] / 2,
this._barSize_px[0] + this._tickSize_px[0],
this._barSize_px[1] + this._tickSize_px[1]);
this._barSize_px[1] + this._tickSize_px[1],
);
}
// central bar:
@ -815,8 +810,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._setupMarker();
}
/**
* Setup the central bar.
*
@ -829,7 +822,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
if (this._barLineWidth_px > 0)
{
this._body.lineStyle(this._barLineWidth_px, this._barLineColor.int, 1, 0.5);
if (typeof this._barFillColor !== 'undefined')
if (typeof this._barFillColor !== "undefined")
{
this._body.beginFill(this._barFillColor.int, 1);
}
@ -837,17 +830,15 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
Math.round(-this._barSize_px[0] / 2),
Math.round(-this._barSize_px[1] / 2),
Math.round(this._barSize_px[0]),
Math.round(this._barSize_px[1])
Math.round(this._barSize_px[1]),
);
if (typeof this._barFillColor !== 'undefined')
if (typeof this._barFillColor !== "undefined")
{
this._body.endFill();
}
}
}
/**
* Setup the marker, and the associated mouse events.
*
@ -857,7 +848,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
*/
_setupMarker()
{
/* this is now deprecated and replaced by _body.hitArea
/* this is now deprecated and replaced by _body.hitArea
// transparent rectangle necessary to capture pointer events outside of marker and labels:
const eventCaptureRectangle = new PIXI.Graphics();
eventCaptureRectangle.beginFill(0, 0);
@ -869,7 +860,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
);
eventCaptureRectangle.endFill();
this._pixi.addChild(eventCaptureRectangle);
*/
*/
// marker:
this._marker = new PIXI.Graphics();
@ -926,7 +917,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
Math.round(-this._markerSize_px[0] / 2),
Math.round(-this._markerSize_px[1] / 2),
this._markerSize_px[0],
this._markerSize_px[1]
this._markerSize_px[1],
);
this._marker.endFill();
@ -934,7 +925,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
// this._marker.drawCircle(0, 0, this._markerSize_px[0] / 3);
}
// marker mouse events:
const self = this;
self._markerDragging = false;
@ -1000,7 +990,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
};
// (*) slider mouse events outside of marker
// note: this only works thanks to eventCaptureRectangle
/* not quite right just yet (as of May 2020)
@ -1034,8 +1023,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
};
}
/**
* Setup the ticks.
*
@ -1071,8 +1058,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Get the PIXI Text Style applied to the PIXI.Text labels.
*
@ -1083,19 +1068,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
_getTextStyle()
{
this._fontSize_px = this._getLengthPix(this._fontSize);
return new PIXI.TextStyle({
fontFamily: this._font,
fontSize: Math.round(this._fontSize_px),
fontWeight: (this._bold) ? 'bold' : 'normal',
fontStyle: (this._italic) ? 'italic' : 'normal',
fontWeight: (this._bold) ? "bold" : "normal",
fontStyle: (this._italic) ? "italic" : "normal",
fill: this.getContrastedColor(this._labelColor, this._contrast).hex,
align: 'center',
align: "center",
});
}
/**
* Setup the labels.
*
@ -1120,8 +1103,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Apply a particular style to the slider.
*
@ -1157,7 +1138,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._tickType = Slider.Shape.LINE;
this._tickColor = (!skin.TICK_COLOR) ? new Color(this._lineColor) : skin.TICK_COLOR;
if (this.markerColor === undefined) {
if (this.markerColor === undefined)
{
this.markerColor = skin.MARKER_COLOR;
}
@ -1170,7 +1152,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._labelOri = 0;
// rating:
if (this._style.indexOf(Slider.Style.RATING) > -1)
{
@ -1183,8 +1164,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._markerType = Slider.Shape.TRIANGLE;
if (!this._skin.MARKER_SIZE)
{
this._markerSize = this._markerSize.map(s => s * 2);
}
this._markerSize = this._markerSize.map((s) => s * 2);
}
}
// slider:
@ -1193,9 +1174,9 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._markerType = Slider.Shape.BOX;
if (!this._skin.MARKER_SIZE)
{
this._markerSize = (this._isHorizontal()) ?
[this._size[0] / (this._ticks[this._ticks.length - 1] - this._ticks[0]), this._size[1]] :
[this._size[0], this._size[1] / (this._ticks[this._ticks.length - 1] - this._ticks[0])];
this._markerSize = (this._isHorizontal())
? [this._size[0] / (this._ticks[this._ticks.length - 1] - this._ticks[0]), this._size[1]]
: [this._size[0], this._size[1] / (this._ticks[this._ticks.length - 1] - this._ticks[0])];
}
this._barSize = [this._size[0], this._size[1]];
this._barFillColor = this.getContrastedColor(new Color(this.color), 0.5);
@ -1234,12 +1215,10 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
if (!this._skin.MARKER_SIZE)
{
this._markerSize = this._markerSize.map(s => s * 0.7);
this._markerSize = this._markerSize.map((s) => s * 0.7);
}
}
}
}
/**
* Convert an array of ratings into an array of [x,y] positions (in Slider units, with 0 at the center of the Slider)
@ -1259,20 +1238,22 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
// in compact mode the circular markers of RADIO sliders must fit within the width:
if (this._compact && this._style.indexOf(Slider.Style.RADIO) > -1)
{
return ratings.map(v => [
((v - this._ticks[0]) / range) * (this._size[0] - this._tickSize[1]*2) -
(this._size[0] / 2) + this._tickSize[1],
0]);
return ratings.map((v) => [
((v - this._ticks[0]) / range) * (this._size[0] - this._tickSize[1] * 2)
- (this._size[0] / 2) + this._tickSize[1],
0,
]);
}
else if (this._style.indexOf(Slider.Style.SLIDER) > -1)
{
return ratings.map(v => [
return ratings.map((v) => [
((v - this._ticks[0]) / range - 0.5) * (this._size[0] - this._markerSize[0]),
0]);
0,
]);
}
else
{
return ratings.map(v => [((v - this._ticks[0]) / range - 0.5) * this._size[0], 0]);
return ratings.map((v) => [((v - this._ticks[0]) / range - 0.5) * this._size[0], 0]);
}
}
else
@ -1280,25 +1261,26 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
// in compact mode the circular markers of RADIO sliders must fit within the height:
if (this._compact && this._style.indexOf(Slider.Style.RADIO) > -1)
{
return ratings.map(v => [0,
((v - this._ticks[0]) / range) * (this._size[1] - this._tickSize[0]*2) -
(this._size[1] / 2) + this._tickSize[0]]);
return ratings.map((v) => [
0,
((v - this._ticks[0]) / range) * (this._size[1] - this._tickSize[0] * 2)
- (this._size[1] / 2) + this._tickSize[0],
]);
}
else if (this._style.indexOf(Slider.Style.SLIDER) > -1)
{
return ratings.map(v => [
return ratings.map((v) => [
0,
((v - this._ticks[0]) / range - 0.5) * (this._size[1] - this._markerSize[1])]);
((v - this._ticks[0]) / range - 0.5) * (this._size[1] - this._markerSize[1]),
]);
}
else
{
return ratings.map(v => [0, (1.0 - (v - this._ticks[0]) / range - 0.5) * this._size[1]]);
return ratings.map((v) => [0, (1.0 - (v - this._ticks[0]) / range - 0.5) * this._size[1]]);
}
}
}
/**
* Convert a [x,y] position, in pixel units, relative to the slider, into a rating.
*
@ -1338,8 +1320,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Determine whether the slider is horizontal.
*
@ -1355,8 +1335,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
return (this._size[0] > this._size[1]);
}
/**
* Calculate the rating once granularity has been taken into account.
*
@ -1368,7 +1346,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
*/
_granularise(rating)
{
if (typeof rating === 'undefined')
if (typeof rating === "undefined")
{
return undefined;
}
@ -1381,10 +1359,8 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
return rating;
}
}
/**
* Shape of the marker and of the ticks.
*
@ -1394,13 +1370,12 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
* @public
*/
Slider.Shape = {
DISC: Symbol.for('DISC'),
TRIANGLE: Symbol.for('TRIANGLE'),
LINE: Symbol.for('LINE'),
BOX: Symbol.for('BOX')
DISC: Symbol.for("DISC"),
TRIANGLE: Symbol.for("TRIANGLE"),
LINE: Symbol.for("LINE"),
BOX: Symbol.for("BOX"),
};
/**
* Styles.
*
@ -1410,15 +1385,14 @@ Slider.Shape = {
* @public
*/
Slider.Style = {
RATING: Symbol.for('RATING'),
TRIANGLE_MARKER: Symbol.for('TRIANGLE_MARKER'),
SLIDER: Symbol.for('SLIDER'),
WHITE_ON_BLACK: Symbol.for('WHITE_ON_BLACK'),
LABELS_45: Symbol.for('LABELS_45'),
RADIO: Symbol.for('RADIO')
RATING: Symbol.for("RATING"),
TRIANGLE_MARKER: Symbol.for("TRIANGLE_MARKER"),
SLIDER: Symbol.for("SLIDER"),
WHITE_ON_BLACK: Symbol.for("WHITE_ON_BLACK"),
LABELS_45: Symbol.for("LABELS_45"),
RADIO: Symbol.for("RADIO"),
};
/**
* Skin.
*
@ -1432,15 +1406,15 @@ Slider.Style = {
Slider.Skin = {
MARKER_SIZE: null,
STANDARD: {
MARKER_COLOR: new Color('red'),
MARKER_COLOR: new Color("red"),
BAR_LINE_COLOR: null,
TICK_COLOR: null,
LABEL_COLOR: null
LABEL_COLOR: null,
},
WHITE_ON_BLACK: {
MARKER_COLOR: new Color('white'),
BAR_LINE_COLOR: new Color('black'),
TICK_COLOR: new Color('black'),
LABEL_COLOR: new Color('black')
}
MARKER_COLOR: new Color("white"),
BAR_LINE_COLOR: new Color("black"),
TICK_COLOR: new Color("black"),
LABEL_COLOR: new Color("black"),
},
};

View File

@ -7,14 +7,13 @@
* @license Distributed under the terms of the MIT License
*/
import * as PIXI from 'pixi.js-legacy';
import {VisualStim} from './VisualStim';
import {Color} from '../util/Color';
import {ColorMixin} from '../util/ColorMixin';
import {TextInput} from './TextInput';
import {ButtonStim} from './ButtonStim.js';
import * as util from '../util/Util';
import * as PIXI from "pixi.js-legacy";
import { Color } from "../util/Color.js";
import { ColorMixin } from "../util/ColorMixin.js";
import * as util from "../util/Util.js";
import { ButtonStim } from "./ButtonStim.js";
import { TextInput } from "./TextInput.js";
import { VisualStim } from "./VisualStim.js";
// TODO finish documenting all options
/**
@ -50,122 +49,154 @@ import * as util from '../util/Util';
*/
export class TextBox extends util.mix(VisualStim).with(ColorMixin)
{
constructor({name, win, pos, anchor, size, units, ori, opacity, depth, text, font, letterHeight, bold, italic, alignment, color, contrast, flipHoriz, flipVert, fillColor, borderColor, borderWidth, padding, editable, multiline, autofocus, clipMask, autoDraw, autoLog} = {})
constructor(
{
name,
win,
pos,
anchor,
size,
units,
ori,
opacity,
depth,
text,
font,
letterHeight,
bold,
italic,
alignment,
color,
contrast,
flipHoriz,
flipVert,
fillColor,
borderColor,
borderWidth,
padding,
editable,
multiline,
autofocus,
clipMask,
autoDraw,
autoLog,
} = {},
)
{
super({name, win, pos, size, units, ori, opacity, depth, clipMask, autoDraw, autoLog});
super({ name, win, pos, size, units, ori, opacity, depth, clipMask, autoDraw, autoLog });
this._addAttribute(
'text',
"text",
text,
'',
this._onChange(true, true)
"",
this._onChange(true, true),
);
this._addAttribute(
'placeholder',
"placeholder",
text,
'',
this._onChange(true, true)
);
"",
this._onChange(true, true),
);
this._addAttribute(
'anchor',
"anchor",
anchor,
'center',
this._onChange(false, true)
"center",
this._onChange(false, true),
);
this._addAttribute(
'flipHoriz',
"flipHoriz",
flipHoriz,
false,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'flipVert',
"flipVert",
flipVert,
false,
this._onChange(false, false)
this._onChange(false, false),
);
// font:
this._addAttribute(
'font',
"font",
font,
'Arial',
this._onChange(true, true)
"Arial",
this._onChange(true, true),
);
this._addAttribute(
'letterHeight',
"letterHeight",
letterHeight,
this._getDefaultLetterHeight(),
this._onChange(true, true)
this._onChange(true, true),
);
this._addAttribute(
'bold',
"bold",
bold,
false,
this._onChange(true, true)
this._onChange(true, true),
);
this._addAttribute(
'italic',
"italic",
italic,
false,
this._onChange(true, true)
this._onChange(true, true),
);
this._addAttribute(
'alignment',
"alignment",
alignment,
'left',
this._onChange(true, true)
"left",
this._onChange(true, true),
);
// colors:
this._addAttribute(
'color',
"color",
color,
'white',
this._onChange(true, false)
"white",
this._onChange(true, false),
);
this._addAttribute(
'fillColor',
"fillColor",
fillColor,
'lightgrey',
this._onChange(true, false)
"lightgrey",
this._onChange(true, false),
);
this._addAttribute(
'borderColor',
"borderColor",
borderColor,
this.fillColor,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'contrast',
"contrast",
contrast,
1.0,
this._onChange(true, false)
this._onChange(true, false),
);
// default border width: 1px
this._addAttribute(
'borderWidth',
"borderWidth",
borderWidth,
util.to_unit([1, 0], 'pix', win, this._units)[0],
this._onChange(true, true)
util.to_unit([1, 0], "pix", win, this._units)[0],
this._onChange(true, true),
);
// default padding: half of the letter height
this._addAttribute(
'padding',
"padding",
padding,
this._letterHeight / 2.0,
this._onChange(true, true)
this._onChange(true, true),
);
this._addAttribute('multiline', multiline, false, this._onChange(true, true));
this._addAttribute('editable', editable, false, this._onChange(true, true));
this._addAttribute('autofocus', autofocus, true, this._onChange(true, false));
// this._setAttribute({
// name: 'vertices',
// value: vertices,
// assert: v => (v != null) && (typeof v !== 'undefined') && Array.isArray(v) )
// log);
this._addAttribute("multiline", multiline, false, this._onChange(true, true));
this._addAttribute("editable", editable, false, this._onChange(true, true));
this._addAttribute("autofocus", autofocus, true, this._onChange(true, false));
// this._setAttribute({
// name: 'vertices',
// value: vertices,
// assert: v => (v != null) && (typeof v !== 'undefined') && Array.isArray(v) )
// log);
// estimate the bounding box:
this._estimateBoundingBox();
@ -176,7 +207,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Clears the current text value or sets it back to match the placeholder.
*
@ -185,13 +215,11 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
*/
reset()
{
const text = this.editable ? '' : this.placeholder;
const text = this.editable ? "" : this.placeholder;
this.setText(this.placeholder);
}
/**
* Clears the current text value.
*
@ -203,8 +231,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
this.setText();
}
/**
* For tweaking the underlying input value.
*
@ -212,9 +238,9 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
* @public
* @param {string} text
*/
setText(text = '')
setText(text = "")
{
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.text = text;
}
@ -222,7 +248,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
this._text = text;
}
/**
* For accessing the underlying input value.
*
@ -232,7 +257,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
*/
getText()
{
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
return this._pixi.text;
}
@ -240,7 +265,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
return this._text;
}
/**
* Setter for the size attribute.
*
@ -253,25 +277,25 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
{
// test with the size is undefined, or [undefined, undefined]:
let isSizeUndefined = (
(typeof size === 'undefined') || (size === null) ||
( Array.isArray(size) && size.every( v => typeof v === 'undefined' || v === null) )
);
(typeof size === "undefined") || (size === null)
|| (Array.isArray(size) && size.every((v) => typeof v === "undefined" || v === null))
);
if (isSizeUndefined)
{
size = TextBox._defaultSizeMap.get(this._units);
if (typeof size === 'undefined')
if (typeof size === "undefined")
{
throw {
origin: 'TextBox.setSize',
context: 'when setting the size of TextBox: ' + this._name,
error: 'no default size for unit: ' + this._units
origin: "TextBox.setSize",
context: "when setting the size of TextBox: " + this._name,
error: "no default size for unit: " + this._units,
};
}
}
const hasChanged = this._setAttribute('size', size, log);
const hasChanged = this._setAttribute("size", size, log);
if (hasChanged)
{
@ -283,8 +307,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Get the default letter height given the stimulus' units.
*
@ -296,20 +318,18 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
{
const height = TextBox._defaultLetterHeightMap.get(this._units);
if (typeof height === 'undefined')
if (typeof height === "undefined")
{
throw {
origin: 'TextBox._getDefaultLetterHeight',
context: 'when getting the default height of TextBox: ' + this._name,
error: 'no default letter height for unit: ' + this._units
origin: "TextBox._getDefaultLetterHeight",
context: "when getting the default height of TextBox: " + this._name,
error: "no default letter height for unit: " + this._units,
};
}
return height;
}
/**
* Get the TextInput options applied to the PIXI.TextInput.
*
@ -328,24 +348,24 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
return {
input: {
fontFamily: this._font,
fontSize: letterHeight_px + 'px',
fontSize: letterHeight_px + "px",
color: new Color(this._color).hex,
fontWeight: (this._bold) ? 'bold' : 'normal',
fontStyle: (this._italic) ? 'italic' : 'normal',
fontWeight: (this._bold) ? "bold" : "normal",
fontStyle: (this._italic) ? "italic" : "normal",
padding: padding_px + 'px',
padding: padding_px + "px",
multiline,
text: this._text,
height: multiline ? (height_px - 2 * padding_px) + 'px' : undefined,
width: (width_px - 2 * padding_px) + 'px'
height: multiline ? (height_px - 2 * padding_px) + "px" : undefined,
width: (width_px - 2 * padding_px) + "px",
},
box: {
fill: new Color(this._fillColor).int,
rounded: 5,
stroke: {
color: new Color(this._borderColor).int,
width: borderWidth_px
}
width: borderWidth_px,
},
/*default: {
fill: new Color(this._fillColor).int,
rounded: 5,
@ -370,12 +390,10 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
width: borderWidth_px
}
}*/
}
},
};
}
/**
* Estimate the bounding box.
*
@ -395,14 +413,12 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
this._pos[0] - anchor[0] * this._size[0],
this._pos[1] - anchor[1] * boxHeight,
this._size[0],
boxHeight
boxHeight,
);
// TODO take the orientation into account
}
/**
* Update the stimulus, if necessary.
*
@ -424,13 +440,13 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
{
this._needPixiUpdate = false;
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.destroy(true);
}
// Get the currently entered text
let enteredText = this._pixi !== undefined? this._pixi.text: '';
// Create new TextInput
let enteredText = this._pixi !== undefined ? this._pixi.text : "";
// Create new TextInput
this._pixi = new TextInput(this._getTextInputOptions());
// listeners required for regular textboxes, but may cause problems with button stimuli
@ -441,7 +457,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
// check if other TextBox instances are already in focus
const { _drawList = [] } = this.psychoJS.window;
const otherTextBoxWithFocus = _drawList.some(item => item instanceof TextBox && item._pixi && item._pixi._hasFocus());
const otherTextBoxWithFocus = _drawList.some((item) => item instanceof TextBox && item._pixi && item._pixi._hasFocus());
if (this._autofocus && !otherTextBoxWithFocus)
{
this._pixi._onSurrogateFocus();
@ -452,7 +468,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
}
if (this._editable)
{
this.text = enteredText;
this.text = enteredText;
this._pixi.placeholder = this._placeholder;
}
else
@ -479,8 +495,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
this._pixi.mask = this._clipMask;
}
/**
* Convert the anchor attribute into numerical values.
*
@ -493,30 +507,27 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
{
const anchor = [0.5, 0.5];
if (this._anchor.indexOf('left') > -1)
if (this._anchor.indexOf("left") > -1)
{
anchor[0] = 0;
}
else if (this._anchor.indexOf('right') > -1)
else if (this._anchor.indexOf("right") > -1)
{
anchor[0] = 1;
}
if (this._anchor.indexOf('top') > -1)
if (this._anchor.indexOf("top") > -1)
{
anchor[1] = 0;
}
else if (this._anchor.indexOf('bottom') > -1)
else if (this._anchor.indexOf("bottom") > -1)
{
anchor[1] = 1;
}
return anchor;
}
}
/**
* <p>This map associates units to default letter height.</p>
*
@ -525,18 +536,17 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
* @private
*/
TextBox._defaultLetterHeightMap = new Map([
['cm', 1.0],
['deg', 1.0],
['degs', 1.0],
['degFlatPos', 1.0],
['degFlat', 1.0],
['norm', 0.1],
['height', 0.2],
['pix', 20],
['pixels', 20]
["cm", 1.0],
["deg", 1.0],
["degs", 1.0],
["degFlatPos", 1.0],
["degFlat", 1.0],
["norm", 0.1],
["height", 0.2],
["pix", 20],
["pixels", 20],
]);
/**
* <p>This map associates units to default sizes.</p>
*
@ -545,13 +555,13 @@ TextBox._defaultLetterHeightMap = new Map([
* @private
*/
TextBox._defaultSizeMap = new Map([
['cm', [15.0, -1]],
['deg', [15.0, -1]],
['degs', [15.0, -1]],
['degFlatPos', [15.0, -1]],
['degFlat', [15.0, -1]],
['norm', [1, -1]],
['height', [1, -1]],
['pix', [500, -1]],
['pixels', [500, -1]]
["cm", [15.0, -1]],
["deg", [15.0, -1]],
["degs", [15.0, -1]],
["degFlatPos", [15.0, -1]],
["degFlat", [15.0, -1]],
["norm", [1, -1]],
["height", [1, -1]],
["pix", [500, -1]],
["pixels", [500, -1]],
]);

View File

@ -9,7 +9,7 @@
* We are currently using it almost as is but will be making modification in the near future.
*/
import * as PIXI from 'pixi.js-legacy';
import * as PIXI from "pixi.js-legacy";
export class TextInput extends PIXI.Container
{
@ -18,27 +18,27 @@ export class TextInput extends PIXI.Container
super();
this._input_style = Object.assign(
{
position: 'absolute',
background: 'none',
border: 'none',
outline: 'none',
text: '',
transformOrigin: '0 0',
lineHeight: '1'
position: "absolute",
background: "none",
border: "none",
outline: "none",
text: "",
transformOrigin: "0 0",
lineHeight: "1",
},
styles.input
styles.input,
);
if (styles.box)
{
this._box_generator = typeof styles.box === 'function' ? styles.box : new DefaultBoxGenerator(styles.box);
this._box_generator = typeof styles.box === "function" ? styles.box : new DefaultBoxGenerator(styles.box);
}
else
{
this._box_generator = null;
}
if (this._input_style.hasOwnProperty('multiline'))
if (this._input_style.hasOwnProperty("multiline"))
{
this._multiline = !!this._input_style.multiline;
delete this._input_style.multiline;
@ -52,16 +52,15 @@ export class TextInput extends PIXI.Container
this._previous = {};
this._dom_added = false;
this._dom_visible = true;
this._placeholder = '';
this._placeholder = "";
this._placeholderColor = 0xa9a9a9;
this._selection = [0, 0];
this._restrict_value = '';
this._restrict_value = "";
this._createDOMInput();
this.substituteText = !this._multiline;
this._setState('DEFAULT');
this._setState("DEFAULT");
}
// GETTERS & SETTERS
get substituteText()
@ -103,7 +102,7 @@ export class TextInput extends PIXI.Container
if (this._substituted)
{
this._updateSurrogate();
this._dom_input.placeholder = '';
this._dom_input.placeholder = "";
}
else
{
@ -120,7 +119,7 @@ export class TextInput extends PIXI.Container
{
this._disabled = disabled;
this._dom_input.disabled = disabled;
this._setState(disabled ? 'DISABLED' : 'DEFAULT');
this._setState(disabled ? "DISABLED" : "DEFAULT");
}
get maxLength()
@ -131,7 +130,7 @@ export class TextInput extends PIXI.Container
set maxLength(length)
{
this._max_length = length;
this._dom_input.setAttribute('maxlength', length);
this._dom_input.setAttribute("maxlength", length);
}
get restrict()
@ -145,21 +144,21 @@ export class TextInput extends PIXI.Container
{
regex = regex.toString().slice(1, -1);
if (regex.charAt(0) !== '^')
if (regex.charAt(0) !== "^")
{
regex = '^' + regex;
regex = "^" + regex;
}
if (regex.charAt(regex.length - 1) !== '$')
if (regex.charAt(regex.length - 1) !== "$")
{
regex = regex + '$';
regex = regex + "$";
}
regex = new RegExp(regex);
}
else
{
regex = new RegExp('^[' + regex + ']*$');
regex = new RegExp("^[" + regex + "]*$");
}
this._restrict_regex = regex;
@ -192,7 +191,6 @@ export class TextInput extends PIXI.Container
}
this._dom_input.focus(options);
}
blur()
@ -211,7 +209,7 @@ export class TextInput extends PIXI.Container
this._input_style[key] = value;
this._dom_input.style[key] = value;
if (this._substituted && (key === 'fontFamily' || key === 'fontSize'))
if (this._substituted && (key === "fontFamily" || key === "fontSize"))
{
this._updateFontMetrics();
}
@ -228,20 +226,19 @@ export class TextInput extends PIXI.Container
super.destroy(options);
}
// SETUP
_createDOMInput()
{
if (this._multiline)
{
this._dom_input = document.createElement('textarea');
this._dom_input.style.resize = 'none';
this._dom_input = document.createElement("textarea");
this._dom_input.style.resize = "none";
}
else
{
this._dom_input = document.createElement('input');
this._dom_input.type = 'text';
this._dom_input = document.createElement("input");
this._dom_input.type = "text";
}
for (let key in this._input_style)
@ -252,23 +249,23 @@ export class TextInput extends PIXI.Container
_addListeners()
{
this.on('added', this._onAdded.bind(this));
this.on('removed', this._onRemoved.bind(this));
this._dom_input.addEventListener('keydown', this._onInputKeyDown.bind(this));
this._dom_input.addEventListener('input', this._onInputInput.bind(this));
this._dom_input.addEventListener('keyup', this._onInputKeyUp.bind(this));
this._dom_input.addEventListener('focus', this._onFocused.bind(this));
this._dom_input.addEventListener('blur', this._onBlurred.bind(this));
this.on("added", this._onAdded.bind(this));
this.on("removed", this._onRemoved.bind(this));
this._dom_input.addEventListener("keydown", this._onInputKeyDown.bind(this));
this._dom_input.addEventListener("input", this._onInputInput.bind(this));
this._dom_input.addEventListener("keyup", this._onInputKeyUp.bind(this));
this._dom_input.addEventListener("focus", this._onFocused.bind(this));
this._dom_input.addEventListener("blur", this._onBlurred.bind(this));
}
_onInputKeyDown(e)
{
this._selection = [
this._dom_input.selectionStart,
this._dom_input.selectionEnd
this._dom_input.selectionEnd,
];
this.emit('keydown', e.keyCode);
this.emit("keydown", e.keyCode);
}
_onInputInput(e)
@ -283,30 +280,30 @@ export class TextInput extends PIXI.Container
this._updateSubstitution();
}
this.emit('input', this.text);
this.emit("input", this.text);
}
_onInputKeyUp(e)
{
this.emit('keyup', e.keyCode);
this.emit("keyup", e.keyCode);
}
_onFocused()
{
this._setState('FOCUSED');
this.emit('focus');
this._setState("FOCUSED");
this.emit("focus");
}
_onBlurred()
{
this._setState('DEFAULT');
this.emit('blur');
this._setState("DEFAULT");
this.emit("blur");
}
_onAdded()
{
document.body.appendChild(this._dom_input);
this._dom_input.style.display = 'none';
this._dom_input.style.display = "none";
this._dom_added = true;
}
@ -326,7 +323,6 @@ export class TextInput extends PIXI.Container
}
}
// RENDER & UPDATE
// for pixi v4
@ -383,7 +379,10 @@ export class TextInput extends PIXI.Container
this._buildBoxCache();
}
if (this.state == this._previous.state && this._box == this._box_cache[this.state])
if (
this.state == this._previous.state
&& this._box == this._box_cache[this.state]
)
{
return;
}
@ -400,7 +399,7 @@ export class TextInput extends PIXI.Container
_updateSubstitution()
{
if (this.state === 'FOCUSED')
if (this.state === "FOCUSED")
{
this._dom_visible = true;
this._surrogate.visible = this.text.length === 0;
@ -421,8 +420,8 @@ export class TextInput extends PIXI.Container
return;
}
this._dom_input.style.top = (this._canvas_bounds.top || 0) + 'px';
this._dom_input.style.left = (this._canvas_bounds.left || 0) + 'px';
this._dom_input.style.top = (this._canvas_bounds.top || 0) + "px";
this._dom_input.style.left = (this._canvas_bounds.left || 0) + "px";
this._dom_input.style.transform = this._pixiMatrixToCSS(this._getDOMRelativeWorldTransform());
this._dom_input.style.opacity = this.worldAlpha;
this._setDOMInputVisible(this.worldVisible && this._dom_visible);
@ -444,21 +443,20 @@ export class TextInput extends PIXI.Container
this.text = this._restrict_value;
this._dom_input.setSelectionRange(
this._selection[0],
this._selection[1]
this._selection[1],
);
}
}
// STATE COMPAIRSON (FOR PERFORMANCE BENEFITS)
_needsUpdate()
{
return (
!this._comparePixiMatrices(this.worldTransform, this._previous.world_transform) ||
!this._compareClientRects(this._canvas_bounds, this._previous.canvas_bounds) ||
this.worldAlpha != this._previous.world_alpha ||
this.worldVisible != this._previous.world_visible
!this._comparePixiMatrices(this.worldTransform, this._previous.world_transform)
|| !this._compareClientRects(this._canvas_bounds, this._previous.canvas_bounds)
|| this.worldAlpha != this._previous.world_alpha
|| this.worldVisible != this._previous.world_visible
);
}
@ -466,13 +464,12 @@ export class TextInput extends PIXI.Container
{
let input_bounds = this._getDOMInputBounds();
return (
!this._previous.input_bounds ||
input_bounds.width != this._previous.input_bounds.width ||
input_bounds.height != this._previous.input_bounds.height
!this._previous.input_bounds
|| input_bounds.width != this._previous.input_bounds.width
|| input_bounds.height != this._previous.input_bounds.height
);
}
// INPUT SUBSTITUTION
_createSurrogate()
@ -480,14 +477,14 @@ export class TextInput extends PIXI.Container
this._surrogate_hitbox = new PIXI.Graphics();
this._surrogate_hitbox.alpha = 0;
this._surrogate_hitbox.interactive = true;
this._surrogate_hitbox.cursor = 'text';
this._surrogate_hitbox.on('pointerdown', this._onSurrogateFocus.bind(this));
this._surrogate_hitbox.cursor = "text";
this._surrogate_hitbox.on("pointerdown", this._onSurrogateFocus.bind(this));
this.addChild(this._surrogate_hitbox);
this._surrogate_mask = new PIXI.Graphics();
this.addChild(this._surrogate_mask);
this._surrogate = new PIXI.Text('', {});
this._surrogate = new PIXI.Text("", {});
this.addChild(this._surrogate);
this._surrogate.mask = this._surrogate_mask;
@ -509,15 +506,15 @@ export class TextInput extends PIXI.Container
switch (this._surrogate.style.align)
{
case 'left':
case "left":
this._surrogate.x = padding[3];
break;
case 'center':
case "center":
this._surrogate.x = input_bounds.width * 0.5 - this._surrogate.width * 0.5;
break;
case 'right':
case "right":
this._surrogate.x = input_bounds.width - padding[1] - this._surrogate.width;
break;
}
@ -563,7 +560,7 @@ export class TextInput extends PIXI.Container
_onSurrogateFocus()
{
this._setDOMInputVisible(true);
//sometimes the input is not being focused by the mouseclick
// sometimes the input is not being focused by the mouseclick
setTimeout(this._ensureFocus.bind(this), 10);
}
@ -583,23 +580,23 @@ export class TextInput extends PIXI.Container
{
switch (key)
{
case 'color':
case "color":
style.fill = this._input_style.color;
break;
case 'fontFamily':
case 'fontSize':
case 'fontWeight':
case 'fontVariant':
case 'fontStyle':
case "fontFamily":
case "fontSize":
case "fontWeight":
case "fontVariant":
case "fontStyle":
style[key] = this._input_style[key];
break;
case 'letterSpacing':
case "letterSpacing":
style.letterSpacing = parseFloat(this._input_style.letterSpacing);
break;
case 'textAlign':
case "textAlign":
style.align = this._input_style.textAlign;
break;
}
@ -626,7 +623,7 @@ export class TextInput extends PIXI.Container
if (this._input_style.padding && this._input_style.padding.length > 0)
{
let components = this._input_style.padding.trim().split(' ');
let components = this._input_style.padding.trim().split(" ");
if (components.length == 1)
{
@ -641,7 +638,7 @@ export class TextInput extends PIXI.Container
}
else if (components.length == 4)
{
let padding = components.map(component =>
let padding = components.map((component) =>
{
return parseFloat(component);
});
@ -655,7 +652,17 @@ export class TextInput extends PIXI.Container
_deriveSurrogateText()
{
return this._dom_input.value.length === 0 ? this._placeholder : this._dom_input.value;
if (this._dom_input.value.length === 0)
{
return this._placeholder;
}
if (this._dom_input.type == "password")
{
return "•".repeat(this._dom_input.value.length);
}
return this._dom_input.value;
}
_updateFontMetrics()
@ -666,25 +673,23 @@ export class TextInput extends PIXI.Container
this._font_metrics = PIXI.TextMetrics.measureFont(font);
}
// CACHING OF INPUT BOX GRAPHICS
_buildBoxCache()
{
this._destroyBoxCache();
let states = ['DEFAULT', 'FOCUSED', 'DISABLED'];
let states = ["DEFAULT", "FOCUSED", "DISABLED"];
let input_bounds = this._getDOMInputBounds();
states.forEach((state) =>
{
this._box_cache[state] = this._box_generator(
input_bounds.width,
input_bounds.height,
state
);
}
);
{
this._box_cache[state] = this._box_generator(
input_bounds.width,
input_bounds.height,
state,
);
});
this._previous.input_bounds = input_bounds;
}
@ -705,7 +710,6 @@ export class TextInput extends PIXI.Container
}
}
// HELPER FUNCTIONS
_hasFocus()
@ -715,13 +719,13 @@ export class TextInput extends PIXI.Container
_setDOMInputVisible(visible)
{
this._dom_input.style.display = visible ? 'block' : 'none';
this._dom_input.style.display = visible ? "block" : "none";
}
_getCanvasBounds()
{
let rect = this._last_renderer.view.getBoundingClientRect();
let bounds = {top: rect.top, left: rect.left, width: rect.width, height: rect.height};
let bounds = { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
bounds.left += window.scrollX;
bounds.top += window.scrollY;
return bounds;
@ -739,8 +743,8 @@ export class TextInput extends PIXI.Container
let org_transform = this._dom_input.style.transform;
let org_display = this._dom_input.style.display;
this._dom_input.style.transform = '';
this._dom_input.style.display = 'block';
this._dom_input.style.transform = "";
this._dom_input.style.display = "block";
let bounds = this._dom_input.getBoundingClientRect();
this._dom_input.style.transform = org_transform;
this._dom_input.style.display = org_display;
@ -759,14 +763,13 @@ export class TextInput extends PIXI.Container
let matrix = this.worldTransform.clone();
matrix.scale(this._resolution, this._resolution);
matrix.scale(canvas_bounds.width / this._last_renderer.width,
canvas_bounds.height / this._last_renderer.height);
matrix.scale(canvas_bounds.width / this._last_renderer.width, canvas_bounds.height / this._last_renderer.height);
return matrix;
}
_pixiMatrixToCSS(m)
{
return 'matrix(' + [m.a, m.b, m.c, m.d, m.tx, m.ty].join(',') + ')';
return "matrix(" + [m.a, m.b, m.c, m.d, m.tx, m.ty].join(",") + ")";
}
_comparePixiMatrices(m1, m2)
@ -776,12 +779,12 @@ export class TextInput extends PIXI.Container
return false;
}
return (
m1.a == m2.a &&
m1.b == m2.b &&
m1.c == m2.c &&
m1.d == m2.d &&
m1.tx == m2.tx &&
m1.ty == m2.ty
m1.a == m2.a
&& m1.b == m2.b
&& m1.c == m2.c
&& m1.d == m2.d
&& m1.tx == m2.tx
&& m1.ty == m2.ty
);
}
@ -792,20 +795,17 @@ export class TextInput extends PIXI.Container
return false;
}
return (
r1.left == r2.left &&
r1.top == r2.top &&
r1.width == r2.width &&
r1.height == r2.height
r1.left == r2.left
&& r1.top == r2.top
&& r1.width == r2.width
&& r1.height == r2.height
);
}
}
function DefaultBoxGenerator(styles)
{
styles = styles || {fill: 0xcccccc};
styles = styles || { fill: 0xcccccc };
if (styles.default)
{
@ -819,7 +819,7 @@ function DefaultBoxGenerator(styles)
styles.default = styles.focused = styles.disabled = temp_styles;
}
return function (w, h, state)
return function(w, h, state)
{
let style = styles[state.toLowerCase()];
let box = new PIXI.Graphics();
@ -834,7 +834,7 @@ function DefaultBoxGenerator(styles)
box.lineStyle(
style.stroke.width ?? 1,
style.stroke.color ?? 0,
style.stroke.alpha ?? 1
style.stroke.alpha ?? 1,
);
}

View File

@ -7,13 +7,12 @@
* @license Distributed under the terms of the MIT License
*/
import * as PIXI from 'pixi.js-legacy';
import {VisualStim} from './VisualStim';
import {Color} from '../util/Color';
import {ColorMixin} from '../util/ColorMixin';
import * as util from '../util/Util';
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";
/**
* @name module:visual.TextStim
@ -48,9 +47,34 @@ import * as util from '../util/Util';
*/
export class TextStim extends util.mix(VisualStim).with(ColorMixin)
{
constructor({name, win, text, font, pos, color, opacity, depth, contrast, units, ori, height, bold, italic, alignHoriz, alignVert, wrapWidth, flipHoriz, flipVert, clipMask, autoDraw, autoLog} = {})
constructor(
{
name,
win,
text,
font,
pos,
color,
opacity,
depth,
contrast,
units,
ori,
height,
bold,
italic,
alignHoriz,
alignVert,
wrapWidth,
flipHoriz,
flipVert,
clipMask,
autoDraw,
autoLog,
} = {},
)
{
super({name, win, units, ori, opacity, depth, pos, clipMask, autoDraw, autoLog});
super({ name, win, units, ori, opacity, depth, pos, clipMask, autoDraw, autoLog });
// callback to deal with text metrics invalidation:
const onChange = (withPixi = false, withBoundingBox = false, withMetrics = false) =>
@ -68,81 +92,80 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
// text and font:
this._addAttribute(
'text',
"text",
text,
'Hello World',
onChange(true, true, true)
"Hello World",
onChange(true, true, true),
);
this._addAttribute(
'alignHoriz',
"alignHoriz",
alignHoriz,
'center',
onChange(true, true, true)
"center",
onChange(true, true, true),
);
this._addAttribute(
'alignVert',
"alignVert",
alignVert,
'center',
onChange(true, true, true)
"center",
onChange(true, true, true),
);
this._addAttribute(
'flipHoriz',
"flipHoriz",
flipHoriz,
false,
onChange(true, true, true)
onChange(true, true, true),
);
this._addAttribute(
'flipVert',
"flipVert",
flipVert,
false,
onChange(true, true, true)
onChange(true, true, true),
);
this._addAttribute(
'font',
"font",
font,
'Arial',
this._onChange(true, true)
"Arial",
this._onChange(true, true),
);
this._addAttribute(
'height',
"height",
height,
this._getDefaultLetterHeight(),
onChange(true, true, true)
onChange(true, true, true),
);
this._addAttribute(
'wrapWidth',
"wrapWidth",
wrapWidth,
this._getDefaultWrapWidth(),
onChange(true, true, true)
onChange(true, true, true),
);
this._addAttribute(
'bold',
"bold",
bold,
false,
onChange(true, true, true)
onChange(true, true, true),
);
this._addAttribute(
'italic',
"italic",
italic,
false,
onChange(true, true, true)
onChange(true, true, true),
);
// color:
this._addAttribute(
'color',
"color",
color,
'white',
this._onChange(true, false)
"white",
this._onChange(true, false),
);
this._addAttribute(
'contrast',
"contrast",
contrast,
1.0,
this._onChange(true, false)
this._onChange(true, false),
);
// estimate the bounding box (using TextMetrics):
this._estimateBoundingBox();
@ -152,8 +175,6 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Get the metrics estimated for the text and style.
*
@ -165,7 +186,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
*/
getTextMetrics()
{
if (typeof this._textMetrics === 'undefined')
if (typeof this._textMetrics === "undefined")
{
this._textMetrics = PIXI.TextMetrics.measureText(this._text, this._getTextStyle());
}
@ -173,8 +194,6 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
return this._textMetrics;
}
/**
* Get the default letter height given the stimulus' units.
*
@ -186,20 +205,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
{
const height = TextStim._defaultLetterHeightMap.get(this._units);
if (typeof height === 'undefined')
if (typeof height === "undefined")
{
throw {
origin: 'TextStim._getDefaultLetterHeight',
context: 'when getting the default height of TextStim: ' + this._name,
error: 'no default letter height for unit: ' + this._units
origin: "TextStim._getDefaultLetterHeight",
context: "when getting the default height of TextStim: " + this._name,
error: "no default letter height for unit: " + this._units,
};
}
return height;
}
/**
* Get the default wrap width given the stimulus' units.
*
@ -211,20 +228,18 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
{
const wrapWidth = TextStim._defaultWrapWidthMap.get(this._units);
if (typeof wrapWidth === 'undefined')
if (typeof wrapWidth === "undefined")
{
throw {
origin: 'TextStim._getDefaultWrapWidth',
context: 'when getting the default wrap width of TextStim: ' + this._name,
error: 'no default wrap width for unit: ' + this._units
origin: "TextStim._getDefaultWrapWidth",
context: "when getting the default wrap width of TextStim: " + this._name,
error: "no default wrap width for unit: " + this._units,
};
}
return wrapWidth;
}
/**
* Estimate the bounding box.
*
@ -237,11 +252,11 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
{
// size of the text, irrespective of the orientation:
const textMetrics = this.getTextMetrics();
const textSize = util.to_unit(
const textSize = util.to_unit(
[textMetrics.width, textMetrics.height],
'pix',
"pix",
this._win,
this._units
this._units,
);
// take the alignment into account:
@ -250,14 +265,12 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
this._pos[0] - anchor[0] * textSize[0],
this._pos[1] - anchor[1] * textSize[1],
textSize[0],
textSize[1]
textSize[1],
);
// TODO take the orientation into account
}
/**
* Get the PIXI Text Style applied to the PIXI.Text
*
@ -269,17 +282,15 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
return new PIXI.TextStyle({
fontFamily: this._font,
fontSize: Math.round(this._getLengthPix(this._height)),
fontWeight: (this._bold) ? 'bold' : 'normal',
fontStyle: (this._italic) ? 'italic' : 'normal',
fontWeight: (this._bold) ? "bold" : "normal",
fontStyle: (this._italic) ? "italic" : "normal",
fill: this.getContrastedColor(new Color(this._color), this._contrast).hex,
align: this._alignHoriz,
wordWrap: (typeof this._wrapWidth !== 'undefined'),
wordWrapWidth: (typeof this._wrapWidth !== 'undefined') ? this._getHorLengthPix(this._wrapWidth) : 0
wordWrap: (typeof this._wrapWidth !== "undefined"),
wordWrapWidth: (typeof this._wrapWidth !== "undefined") ? this._getHorLengthPix(this._wrapWidth) : 0,
});
}
/**
* Update the stimulus, if necessary.
*
@ -300,7 +311,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
{
this._needPixiUpdate = false;
if (typeof this._pixi !== 'undefined')
if (typeof this._pixi !== "undefined")
{
this._pixi.destroy(true);
}
@ -314,7 +325,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
this._pixi.scale.y = this._flipVert ? 1 : -1;
this._pixi.rotation = this._ori * Math.PI / 180;
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
this._pixi.position = to_pixiPoint(this.pos, this.units, this.win);
this._pixi.alpha = this._opacity;
this._pixi.zIndex = this._depth;
@ -325,7 +336,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
// update the size attributes:
this._size = [
this._getLengthUnits(Math.abs(this._pixi.width)),
this._getLengthUnits(Math.abs(this._pixi.height))
this._getLengthUnits(Math.abs(this._pixi.height)),
];
// refine the estimate of the bounding box:
@ -333,12 +344,10 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
this._pos[0] - anchor[0] * this._size[0],
this._pos[1] - anchor[1] * this._size[1],
this._size[0],
this._size[1]
this._size[1],
);
}
/**
* Convert the alignment attributes into an anchor.
*
@ -353,36 +362,33 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
switch (this._alignHoriz)
{
case 'left':
case "left":
anchor.push(0);
break;
case 'right':
case "right":
anchor.push(1);
break;
default:
case 'center':
case "center":
anchor.push(0.5);
}
switch (this._alignVert)
{
case 'top':
case "top":
anchor.push(0);
break;
case 'bottom':
case "bottom":
anchor.push(1);
break;
default:
case 'center':
case "center":
anchor.push(0.5);
}
return anchor;
}
}
/**
* <p>This map associates units to default letter height.</p>
*
@ -391,19 +397,17 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
* @private
*/
TextStim._defaultLetterHeightMap = new Map([
['cm', 1.0],
['deg', 1.0],
['degs', 1.0],
['degFlatPos', 1.0],
['degFlat', 1.0],
['norm', 0.1],
['height', 0.2],
['pix', 20],
['pixels', 20]
["cm", 1.0],
["deg", 1.0],
["degs", 1.0],
["degFlatPos", 1.0],
["degFlat", 1.0],
["norm", 0.1],
["height", 0.2],
["pix", 20],
["pixels", 20],
]);
/**
* <p>This map associates units to default wrap width.</p>
*
@ -412,13 +416,13 @@ TextStim._defaultLetterHeightMap = new Map([
* @private
*/
TextStim._defaultWrapWidthMap = new Map([
['cm', 15.0],
['deg', 15.0],
['degs', 15.0],
['degFlatPos', 15.0],
['degFlat', 15.0],
['norm', 1],
['height', 1],
['pix', 500],
['pixels', 500]
["cm", 15.0],
["deg", 15.0],
["degs", 15.0],
["degFlatPos", 15.0],
["degFlat", 15.0],
["norm", 1],
["height", 1],
["pix", 500],
["pixels", 500],
]);

View File

@ -7,12 +7,10 @@
* @license Distributed under the terms of the MIT License
*/
import * as PIXI from 'pixi.js-legacy';
import {MinimalStim} from '../core/MinimalStim';
import {WindowMixin} from '../core/WindowMixin';
import * as util from '../util/Util';
import * as PIXI from "pixi.js-legacy";
import { MinimalStim } from "../core/MinimalStim.js";
import { WindowMixin } from "../core/WindowMixin.js";
import * as util from "../util/Util.js";
/**
* Base class for all visual stimuli.
@ -36,55 +34,54 @@ import * as util from '../util/Util';
*/
export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
{
constructor({name, win, units, ori, opacity, depth, pos, size, clipMask, autoDraw, autoLog} = {})
constructor({ name, win, units, ori, opacity, depth, pos, size, clipMask, autoDraw, autoLog } = {})
{
super({win, name, autoDraw, autoLog});
super({ win, name, autoDraw, autoLog });
this._addAttribute(
'units',
"units",
units,
(typeof win !== 'undefined' && win !== null) ? win.units : 'height',
this._onChange(true, true)
(typeof win !== "undefined" && win !== null) ? win.units : "height",
this._onChange(true, true),
);
this._addAttribute(
'pos',
"pos",
pos,
[0, 0]
[0, 0],
);
this._addAttribute(
'size',
"size",
size,
undefined
undefined,
);
this._addAttribute(
'ori',
"ori",
ori,
0.0
0.0,
);
this._addAttribute(
'opacity',
"opacity",
opacity,
1.0,
this._onChange(true, false)
this._onChange(true, false),
);
this._addAttribute(
'depth',
"depth",
depth,
0,
this._onChange(false, false)
this._onChange(false, false),
);
this._addAttribute(
'clipMask',
"clipMask",
clipMask,
null,
this._onChange(false, false)
this._onChange(false, false),
);
// bounding box of the stimulus, in stimulus units
// note: boundingBox does not take the orientation into account
this._addAttribute('boundingBox', PIXI.Rectangle.EMPTY);
this._addAttribute("boundingBox", PIXI.Rectangle.EMPTY);
// the stimulus need to be updated:
this._needUpdate = true;
@ -92,8 +89,6 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
this._needPixiUpdate = true;
}
/**
* Force a refresh of the stimulus.
*
@ -107,8 +102,6 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
this._onChange(true, true)();
}
/**
* Setter for the size attribute.
*
@ -120,7 +113,7 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
setSize(size, log = false)
{
// size is either undefined, null, or a tuple of numbers:
if (typeof size !== 'undefined' && size !== null)
if (typeof size !== "undefined" && size !== null)
{
size = util.toNumerical(size);
if (!Array.isArray(size))
@ -129,7 +122,7 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
}
}
const hasChanged = this._setAttribute('size', size, log);
const hasChanged = this._setAttribute("size", size, log);
if (hasChanged)
{
@ -137,8 +130,6 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
}
}
/**
* Setter for the orientation attribute.
*
@ -149,20 +140,17 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
*/
setOri(ori, log = false)
{
const hasChanged = this._setAttribute('ori', ori, log);
const hasChanged = this._setAttribute("ori", ori, log);
if (hasChanged)
{
let radians = -ori * 0.017453292519943295;
this._rotationMatrix = [[Math.cos(radians), -Math.sin(radians)],
[Math.sin(radians), Math.cos(radians)]];
this._rotationMatrix = [[Math.cos(radians), -Math.sin(radians)], [Math.sin(radians), Math.cos(radians)]];
this._onChange(true, true)();
}
}
/**
* Setter for the position attribute.
*
@ -174,20 +162,18 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
setPos(pos, log = false)
{
const prevPos = this._pos;
const hasChanged = this._setAttribute('pos', util.toNumerical(pos), log);
const hasChanged = this._setAttribute("pos", util.toNumerical(pos), log);
if (hasChanged)
{
this._needUpdate = true;
// update the bounding box, without calling _estimateBoundingBox:
this._boundingBox.x += this._pos[0] - prevPos[0];
this._boundingBox.y += this._pos[1] - prevPos[1];
}
}
/**
* Determine whether an object is inside the bounding box of the stimulus.
*
@ -202,12 +188,12 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
// get the position of the object, in pixel coordinates:
const objectPos_px = util.getPositionFromObject(object, units);
if (typeof objectPos_px === 'undefined')
if (typeof objectPos_px === "undefined")
{
throw {
origin: 'VisualStim.contains',
context: 'when determining whether VisualStim: ' + this._name + ' contains object: ' + util.toString(object),
error: 'unable to determine the position of the object'
origin: "VisualStim.contains",
context: "when determining whether VisualStim: " + this._name + " contains object: " + util.toString(object),
error: "unable to determine the position of the object",
};
}
@ -215,8 +201,6 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
return this._getBoundingBox_px().contains(objectPos_px[0], objectPos_px[1]);
}
/**
* Estimate the bounding box.
*
@ -227,14 +211,12 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
_estimateBoundingBox()
{
throw {
origin: 'VisualStim._estimateBoundingBox',
origin: "VisualStim._estimateBoundingBox",
context: `when estimating the bounding box of visual stimulus: ${this._name}`,
error: 'this method is abstract and should not be called.'
error: "this method is abstract and should not be called.",
};
}
/**
* Get the bounding box in pixel coordinates
*
@ -245,37 +227,35 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
*/
_getBoundingBox_px()
{
if (this._units === 'pix')
if (this._units === "pix")
{
return this._boundingBox.clone();
}
else if (this._units === 'norm')
else if (this._units === "norm")
{
return new PIXI.Rectangle(
this._boundingBox.x * this._win.size[0] / 2,
this._boundingBox.y * this._win.size[1] / 2,
this._boundingBox.width * this._win.size[0] / 2,
this._boundingBox.height * this._win.size[1] / 2
this._boundingBox.height * this._win.size[1] / 2,
);
}
else if (this._units === 'height')
else if (this._units === "height")
{
const minSize = Math.min(this._win.size[0], this._win.size[1]);
return new PIXI.Rectangle(
this._boundingBox.x * minSize,
this._boundingBox.y * minSize,
this._boundingBox.width * minSize,
this._boundingBox.height * minSize
this._boundingBox.height * minSize,
);
}
else
{
throw Object.assign(response, {error: `unknown units: ${this._units}`});
throw Object.assign(response, { error: `unknown units: ${this._units}` });
}
}
/**
* Generate a callback that prepares updates to the stimulus.
* This is typically called in the constructor of a stimulus, when attributes are added with _addAttribute.
@ -302,5 +282,4 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
}
};
}
}

View File

@ -1,15 +1,14 @@
export * from './ButtonStim.js';
export * from './Form.js';
export * from './ImageStim.js';
export * from './MovieStim.js';
export * from './Polygon.js';
export * from './Rect.js';
export * from './ShapeStim.js';
export * from './Slider.js';
export * from './TextBox.js';
export * from './TextInput.js';
export * from './TextStim.js';
export * from './VisualStim.js';
export * from './Camera.js';
export * from './FaceDetector.js';
export * from "./ButtonStim.js";
export * from "./Form.js";
export * from "./ImageStim.js";
export * from "./MovieStim.js";
export * from "./Polygon.js";
export * from "./Rect.js";
export * from "./ShapeStim.js";
export * from "./Slider.js";
export * from "./TextBox.js";
export * from "./TextInput.js";
export * from "./TextStim.js";
export * from "./VisualStim.js";
export * from "./Camera.js";
export * from "./FaceDetector.js";