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:
commit
7020ee1752
@ -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.
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user