1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-12 08:38:10 +00:00

Merge branch 'master' into bf#114--pixi

This commit is contained in:
Alain Pitiot 2021-02-18 13:23:12 +01:00 committed by GitHub
commit 7020ee1752
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 98 additions and 92 deletions

View File

@ -21,9 +21,9 @@ Running PsychoPy experiments online requires the generation of an index.html fil
Starting with PsychoPy version 3.0, [PsychoPy Builder](http://www.psychopy.org/builder/builder.html) can automatically generate the javascript and html files. Many of the existing Builder experiments should "just work", subject to the Components being currently supported by PsychoJS (see below).
### JavaScript Code
We built the PsychoJS library to make the JavaScript experiment files look and behave in very much the same way as to the Builder-generated Python files. PsychoJS offers classes such as `Window` and `ImageStim`, with very similar attributes to their Python equivalents. Experiment designers familiar with the PsychoPy library should feel at home with PsychoJS, and can expect the same level of control they have with PsychoPy, from the the structure of the trials/loops all the way down to frame-by-frame updates.
We built the PsychoJS library to make the JavaScript experiment files look and behave in very much the same way as to the Builder-generated Python files. PsychoJS offers classes such as `Window` and `ImageStim`, with very similar attributes to their Python equivalents. Experiment designers familiar with the PsychoPy library should feel at home with PsychoJS, and can expect the same level of control they have with PsychoPy, from the structure of the trials/loops all the way down to frame-by-frame updates.
There are however notable differences between the 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).
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.

View File

@ -70,7 +70,10 @@ export class Keyboard extends PsychObject
clock = new Clock();
} //this._psychoJS.monotonicClock;
this._addAttributes(Keyboard, bufferSize, waitForStart, clock, 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);
@ -418,6 +421,7 @@ export class Keyboard extends PsychObject
self._psychoJS.logger.trace('keydown: ', event.key);
event.stopPropagation();
event.preventDefault();
});

View File

@ -45,7 +45,10 @@ export class Mouse extends PsychObject
const units = win.units;
const visible = 1;
this._addAttributes(Mouse, win, units, visible, autoLog);
this._addAttribute('win', win);
this._addAttribute('units', units);
this._addAttribute('visible', visible);
this._addAttribute('autoLog', autoLog);
this.status = PsychoJS.Status.NOT_STARTED;
}

View File

@ -117,6 +117,7 @@ export class PsychoJS
constructor({
debug = true,
collectIP = false,
hosts = [],
topLevelStatus = true
} = {})
{
@ -137,6 +138,10 @@ export class PsychoJS
psychoJS: this
});
// to be loading `configURL` files in `_configure` calls from
const hostsEvidently = new Set([...hosts, 'https://pavlovia.org/run/', 'https://run.pavlovia.org/']);
this._hosts = Array.from(hostsEvidently);
// GUI:
this._gui = new GUI(this);
@ -560,7 +565,9 @@ export class PsychoJS
// if the experiment is running from the pavlovia.org server, we read the configuration file:
const experimentUrl = window.location.href;
if (experimentUrl.indexOf('https://run.pavlovia.org/') === 0 || experimentUrl.indexOf('https://pavlovia.org/run/') === 0)
// go through each url in allow list
const isHost = this._hosts.some(url => experimentUrl.indexOf(url) === 0);
if (isHost)
{
const serverResponse = await this._serverManager.getConfiguration(configURL);
this._config = serverResponse.config;

View File

@ -46,7 +46,7 @@ export class ServerManager extends PsychObject
this._resources = new Map();
this._nbResources = -1;
this._addAttributes(ServerManager, autoLog);
this._addAttribute('autoLog', autoLog);
this._addAttribute('status', ServerManager.Status.READY);
}

View File

@ -41,7 +41,7 @@ export class Window extends PsychObject
*/
get monitorFramePeriod()
{
return this._monitorFramePeriod;
return 1.0 / this.getActualFrameRate();
}
constructor({
@ -62,16 +62,17 @@ export class Window extends PsychObject
// list of all elements, in the order they are currently drawn:
this._drawList = [];
this._addAttributes(Window, fullscr, color, units, waitBlanking, autoLog);
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();
// monitor frame period:
this._monitorFramePeriod = 1.0 / this.getActualFrameRate();
this._frameCount = 0;
this._flipCallbacks = [];
@ -145,14 +146,15 @@ export class Window extends PsychObject
* @name module:core.Window#getActualFrameRate
* @function
* @public
* @return {number} always returns 60.0 at the moment
*
* @todo estimate the actual frame rate.
* @return {number} rAF based delta time based approximation, 60.0 by default
*/
getActualFrameRate()
{
// TODO
return 60.0;
// gets updated frame by frame
const lastDelta = this.psychoJS.scheduler._lastDelta;
const fps = lastDelta === 0 ? 60.0 : 1000 / lastDelta;
return fps;
}

View File

@ -76,7 +76,7 @@ export class ExperimentHandler extends PsychObject
{
super(psychoJS, name);
this._addAttributes(ExperimentHandler, extraInfo);
this._addAttribute('extraInfo', extraInfo);
// loop handlers:
this._loops = [];
@ -105,11 +105,6 @@ export class ExperimentHandler extends PsychObject
return (Object.keys(this._currentTrialData).length > 0);
}
isEntryEmtpy()
{
return (Object.keys(this._currentTrialData).length > 0);
}
/**
* Add a loop.

View File

@ -75,7 +75,13 @@ export class TrialHandler extends PsychObject
{
super(psychoJS);
this._addAttributes(TrialHandler, trialList, nReps, method, extraInfo, seed, name, autoLog);
this._addAttribute('trialList', trialList);
this._addAttribute('nReps', nReps);
this._addAttribute('method', method);
this._addAttribute('extraInfo', extraInfo);
this._addAttribute('seed', seed);
this._addAttribute('name', name);
this._addAttribute('autoLog', autoLog);
this._prepareTrialList(trialList);

View File

@ -72,7 +72,16 @@ export class Sound extends PsychObject
// the SoundPlayer, e.g. TonePlayer:
this._player = undefined;
this._addAttributes(Sound, win, value, octave, secs, startTime, stopTime, stereo, volume, loops, /*hamming,*/ 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();

View File

@ -37,7 +37,12 @@ export class TonePlayer extends SoundPlayer
{
super(psychoJS);
this._addAttributes(TonePlayer, note, duration_s, volume, loops, soundLibrary, 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();

View File

@ -41,7 +41,12 @@ export class TrackPlayer extends SoundPlayer
{
super(psychoJS);
this._addAttributes(TrackPlayer, howl, startTime, stopTime, stereo, loops, 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;
}

View File

@ -343,47 +343,6 @@ export class PsychObject extends EventEmitter
}
/**
* Add attributes to this instance (e.g. define setters and getters) and affect values to them.
*
* <p>Notes:
* <ul>
* <li> If the object already has a set<attributeName> method, we do not redefine it,
* and the setter for this attribute calls that method instead of _setAttribute.</li>
* <li> _addAttributes is typically called in the constructor of an object, after
* the call to super (see module:visual.ImageStim for an illustration).</li>
* </ul></p>
*
* @protected
* @param {Object} cls - the class object of the subclass of PsychoObject whose attributes we will set
* @param {...*} [args] - the values for the attributes (this also determines which attributes will be set)
*
*/
_addAttributes(cls, ...args)
{
// (*) look for the line in the subclass constructor where addAttributes is called
// and extract its arguments:
const callLine = cls.toString().match(/this.*\._addAttributes\(.*\;/)[0];
const startIndex = callLine.indexOf('._addAttributes(') + 16;
const endIndex = callLine.indexOf(');');
const callArgs = callLine.substr(startIndex, endIndex - startIndex).split(',').map((s) => s.trim());
// (*) add (argument name, argument value) pairs to the attribute map:
const attributeMap = new Map();
for (let i = 1; i < callArgs.length; ++i)
{
attributeMap.set(callArgs[i], args[i - 1]);
}
// (*) set the value, define the get/set<attributeName> properties and define the getter and setter:
for (let [name, value] of attributeMap.entries())
{
this._addAttribute(name, value);
}
}
/**
* Add an attribute to this instance (e.g. define setters and getters) and affect a value to it.
*

View File

@ -137,7 +137,7 @@ export class Scheduler
start()
{
const self = this;
let update = () =>
let update = (timestamp) =>
{
// stop the animation if need be:
if (self._stopAtNextUpdate)
@ -156,6 +156,12 @@ export class Scheduler
return;
}
// store frame delta for `Window.getActualFrameRate()`
const lastTimestamp = self._lastTimestamp === undefined ? timestamp : self._lastTimestamp;
self._lastDelta = timestamp - lastTimestamp;
self._lastTimestamp = timestamp;
// render the scene in the window:
self._psychoJS.window.render();

View File

@ -373,8 +373,8 @@ export class MovieStim extends VisualStim
}
// create a PixiJS video sprite:
this._texture = PIXI.Texture.from(this._movie);
this._pixi = PIXI.Sprite.from(this._texture);
this._texture = PIXI.Texture.from(this._movie, { resourceOptions: { autoPlay: this.autoPlay } });
this._pixi = new PIXI.Sprite(this._texture);
// since _texture.width may not be immedialy available but the rest of the code needs its value
// we arrange for repeated calls to _updateIfNeeded until we have a width:
@ -390,8 +390,7 @@ export class MovieStim extends VisualStim
this._movie.muted = this._noAudio;
this._movie.volume = this._volume;
// autoplay and loop:
this._texture.baseTexture.autoPlay = this.autoPlay;
// loop:
this._movie.loop = this._loop;
// opacity:

View File

@ -262,7 +262,8 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5);
if (typeof this._fillColor !== 'undefined' && this._fillColor !== null)
{
this._pixi.beginFill(this._fillColor.int, this._opacity);
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)
@ -311,6 +312,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
// destroy the previous PIXI polygon and create a new one:
this._pixiPolygon_px = new PIXI.Polygon(coords_px);
this._pixiPolygon_px.closeStroke = this._closeShape;
return this._pixiPolygon_px;
}

View File

@ -65,7 +65,7 @@ import {PsychoJS} from "../core/PsychoJS";
*/
export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
{
constructor({name, win, pos, size, ori, units, color, contrast, opacity, style, ticks, labels, labelHeight, granularity, flip, readOnly, font, bold, italic, fontSize, compact, clipMask, autoDraw, autoLog} = {})
constructor({name, win, pos, size, ori, units, color, contrast, opacity, style, ticks, labels, granularity, flip, readOnly, font, bold, italic, fontSize, compact, clipMask, autoDraw, autoLog} = {})
{
super({name, win, units, ori, opacity, pos, size, clipMask, autoDraw, autoLog});
@ -151,12 +151,6 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
false,
this._onChange(true, true)
);
this._addAttribute(
'labelHeight',
labelHeight,
undefined,
this._onChange(true, true)
);
this._addAttribute(
'flip',
flip,

View File

@ -39,6 +39,7 @@ import * as util from '../util/Util';
* @param {string} [options.anchor = 'left'] - horizontal alignment
*
* @param {boolean} [options.multiline= false] - whether or not a textarea is used
* @param {boolean} [options.autofocus= true] - whether or not the first input should receive focus by default
* @param {boolean} [options.flipHoriz= false] - whether or not to flip the text horizontally
* @param {boolean} [options.flipVert= false] - whether or not to flip the text vertically
* @param {PIXI.Graphics} [options.clipMask= null] - the clip mask
@ -47,7 +48,7 @@ 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, 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});
@ -151,6 +152,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
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,
@ -402,6 +404,13 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
this._pixi.destroy(true);
}
this._pixi = new TextInput(this._getTextInputOptions());
// 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());
if (this._autofocus && !otherTextBoxWithFocus)
{
this._pixi._onSurrogateFocus();
}
if (this._multiline)
{
this._pixi._multiline = this._multiline;

View File

@ -182,14 +182,14 @@ export class TextInput extends PIXI.Container
return this._dom_input;
}
focus()
focus(options = { preventScroll: true })
{
if (this._substituted && !this.dom_visible)
{
this._setDOMInputVisible(true);
}
this._dom_input.focus();
this._dom_input.focus(options);
}
@ -674,14 +674,15 @@ export class TextInput extends PIXI.Container
let states = ['DEFAULT', 'FOCUSED', 'DISABLED'];
let input_bounds = this._getDOMInputBounds();
for (let i in states)
{
this._box_cache[states[i]] = this._box_generator(
input_bounds.width,
input_bounds.height,
states[i]
);
}
states.forEach((state) =>
{
this._box_cache[state] = this._box_generator(
input_bounds.width,
input_bounds.height,
state
);
}
);
this._previous.input_bounds = input_bounds;
}

View File

@ -34,7 +34,7 @@ import * as util from '../util/Util';
* @param {number} [options.height= 0.1] - the height of the text
* @param {boolean} [options.bold= false] - whether or not the text is bold
* @param {boolean} [options.italic= false] - whether or not the text is italic
* @param {string} [options.alignHoriz = 'left'] - horizontal alignment
* @param {string} [options.alignHoriz = 'center'] - horizontal alignment
* @param {string} [options.alignVert = 'center'] - vertical alignment
* @param {boolean} options.wrapWidth - whether or not to wrap the text horizontally
* @param {boolean} [options.flipHoriz= false] - whether or not to flip the text horizontally