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

fully documented the visual module; various bug fixes

This commit is contained in:
Alain Pitiot 2018-10-24 15:05:28 +02:00
parent cc9a4202e9
commit 14390697f1
10 changed files with 1107 additions and 19 deletions

View File

@ -50,7 +50,7 @@ We are constantly adding new Components and are regularly updating this list.
## Authors
The PsychoJs library is written and maintained by Ilixa Ltd. (http://www.ilixa.com). The PsychoPy Builder's javascript code generator is built and maintained by the creators of PsychoPy, at the University of Nottingham (https://www.nottingham.ac.uk). Both efforts are generously supported by the Wellcome Trust (https://wellcome.ac.uk).
The PsychoJs library is written and maintained by Ilixa Ltd. (http://www.ilixa.com). The PsychoPy Builder's javascript code generator is built and maintained by the creators of PsychoPy at the University of Nottingham (https://www.nottingham.ac.uk). Both efforts are generously supported by the Wellcome Trust (https://wellcome.ac.uk).
## License

View File

@ -44,7 +44,7 @@ export class MinimalStim extends PsychObject {
/**
* Setter for autoDraw attribute.
* Setter for the autoDraw attribute.
*
* @name module:core.MinimalStim#setAutoDraw
* @function

View File

@ -433,7 +433,8 @@ export class ServerManager extends PsychObject {
// error: we throw an exception
this._resourceQueue.addEventListener("error", event => {
self.setStatus(ServerManager.Status.ERROR);
throw { ...response, error: 'unable to download resource: ' + event.data.id + ' (' + event.title + ')' };
const resourceId = (typeof event.data !== 'undefined')?event.data.id:'UNKNOWN RESOURCE';
throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
});
@ -458,7 +459,14 @@ export class ServerManager extends PsychObject {
// (*) start loading non-sound resources:
this._resourceQueue.loadManifest(manifest);
if (manifest.length > 0)
this._resourceQueue.loadManifest(manifest);
else {
if (this._nbLoadedResources == this._nbResources) {
this.setStatus(ServerManager.Status.READY);
this.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
}
}
// (*) prepare and start loading sound resources:

View File

@ -104,7 +104,6 @@ export class ExperimentHandler extends PsychObject {
/**
* Add the key/value pair.
*
* <p> This method is typically called by a {@link TrialHandler}. </p>
* <p> Multiple key/value pairs can be added to any given entry of the data file. There are
* considered part of the same entry until a call to {@link nextEntry} is made. </p>
*
@ -133,15 +132,15 @@ export class ExperimentHandler extends PsychObject {
*/
nextEntry() {
// fetch data from each (potentially-nested) loop:
for (const loop of this._unfinishedLoops) {
for (let loop of this._unfinishedLoops) {
var attributes = this.getLoopAttributes(loop);
for (const a in attributes)
for (let a in attributes)
if (attributes.hasOwnProperty(a))
this._currentTrialData[a] = attributes[a];
}
// add the extraInfo dict to the data:
for (const a in this.extraInfo)
for (let a in this.extraInfo)
if (this.extraInfo.hasOwnProperty(a))
this._currentTrialData[a] = this.extraInfo[a];
@ -177,22 +176,22 @@ export class ExperimentHandler extends PsychObject {
// data is in the csv format:
// build the csv header:
var csv = "";
var header = this._trialsKeys;
for (var l = 0; l < this._loops.length; l++) {
var loop = this._loops[l];
let csv = "";
let header = this._trialsKeys.slice();
for (let l = 0; l < this._loops.length; l++) {
const loop = this._loops[l];
var loopAttributes = this.getLoopAttributes(loop);
for (var a in loopAttributes)
const loopAttributes = this.getLoopAttributes(loop);
for (let a in loopAttributes)
if (loopAttributes.hasOwnProperty(a))
header.push(a);
}
for (var a in this.extraInfo) {
for (let a in this.extraInfo) {
if (this.extraInfo.hasOwnProperty(a))
header.push(a);
}
for (var h = 0; h < header.length; h++) {
for (let h = 0; h < header.length; h++) {
if (h > 0)
csv = csv + ', ';
csv = csv + header[h];
@ -200,8 +199,8 @@ export class ExperimentHandler extends PsychObject {
csv = csv + '\n';
// build the records:
for (var r = 0; r < this._trialsData.length; r++) {
for (var h = 0; h < header.length; h++) {
for (let r = 0; r < this._trialsData.length; r++) {
for (let h = 0; h < header.length; h++) {
if (h > 0)
csv = csv + ', ';
csv = csv + this._trialsData[r][header[h]];

View File

@ -1,6 +1,6 @@
/** @module sound */
/**
* @file Sound stimulus
* @file Sound stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11

239
js/visual/BaseShapeStim.js Normal file
View File

@ -0,0 +1,239 @@
/** @module visual */
/**
* @file Basic Shape Stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
* @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
import { BaseVisualStim } from './BaseVisualStim';
import { Color } from '../util/Color';
import { ColorMixin } from '../util/ColorMixin';
import * as util from '../util/Util';
/**
* <p>This class provides the basic functionalities of shape stimuli.</p>
*
* @class
* @extends BaseVisualStim
* @mixes ColorMixin
* @param {Object} options
* @param {String} options.name - the name used when logging messages from this stimulus
* @param {Window} options.win - the associated Window
* @param {number} options.lineWidth - the line width
* @param {Color} [options.lineColor= Color('white')] the line color
* @param {Color} options.fillColor - the fill color
* @param {number} [options.opacity= 1.0] - the opacity
* @param {Array.<Array.<number>>} [options.vertices= [[-0.5, 0], [0, 0.5], [0.5, 0]]] - the shape vertices
* @param {boolean} [options.closeShape= true] - whether or not the shape is closed
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the shape
* @param {number} [options.size= 1.0] - the size
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
* @param {string} options.units - the units of the stimulus vertices, size and position
* @param {number} [options.contrast= 1.0] - the contrast
* @param {number} [options.depth= 0] - the depth
* @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
export class BaseShapeStim extends util.mix(BaseVisualStim).with(ColorMixin)
{
/**
* @constructor
* @public
*/
constructor({
name,
win,
lineWidth = 1.5,
lineColor = new Color('white'),
fillColor,
opacity = 1.0,
vertices = [[-0.5, 0], [0, 0.5], [0.5, 0]],
closeShape = true,
pos = [0, 0],
size = 1.0,
ori = 0.0,
units,
contrast = 1.0,
depth = 0,
interpolate = true,
autoDraw,
autoLog
} = {}) {
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
// the PIXI polygon corresponding to the vertices, in pixel units:
this._pixiPolygon_px = undefined;
this._addAttributes(BaseShapeStim, lineWidth, lineColor, fillColor, vertices, closeShape, contrast, depth, interpolate);
/*if (autoLog)
logging.exp("Created %s = %s" % (self.name, str(self)));*/
}
/**
* Setter for the line width attribute.
*
* @public
* @param {number} lineWidth - the line width
* @param {boolean} [log= false] - whether of not to log
*/
setLineWidth(lineWidth, log = false) {
this._setAttribute('lineWidth', lineWidth, log);
this._needUpdate = true;
}
/**
* Setter for the line color attribute.
*
* @public
* @param {Color} lineColor - the line color
* @param {boolean} [log= false] - whether of not to log
*/
setLineColor(lineColor, log = false) {
this._setAttribute('lineColor', lineColor, log);
this._needUpdate = true;
}
/**
* Setter for the fill color attribute.
*
* @public
* @param {Color} fillColor - the fill color
* @param {boolean} [log= false] - whether of not to log
*/
setFillColor(fillColor, log = false) {
this._setAttribute('fillColor', fillColor, log);
this._needUpdate = true;
}
/**
* Setter for the vertices attribute.
*
* @public
* @param {Array.<Array.<number>>} vertices - the vertices
* @param {boolean} [log= false] - whether of not to log
*/
setVertices(vertices, log = false) {
this._psychoJS.logger.debug('set the vertices of BaseShapeStim:', this.name);
this._setAttribute('vertices', vertices, log);
/*this._setAttribute({
name: 'vertices',
value: vertices,
assert: v => (v != null) && (typeof v !== 'undefined') && Array.isArray(v) )
log);
*/
this._needVertexUpdate = true;
this._needUpdate = true;
}
/**
* Determine whether this stimulus contains the given object.
*
* @public
* @param {Object} object - the object
* @param {string} units - the units
* @return {boolean} whether or not the stimulus contains the object
*/
contains(object, units) {
this._psychoJS.logger.debug('test whether BaseShameStim:', this.name, 'contains object: ', ('name' in object) ? object.name : object);
// get position of object:
const objectPos_px = util.getPositionFromObject(object, units);
if (typeof objectPos_px === 'undefined')
throw { origin : 'BaseShapeStim.contains', context : 'when determining whether BaseShameStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
// test for inclusion
// note: the vertices are centered around (0, 0) so we need to add to them the stimulus' position
const pos_px = util.to_px(this.pos, this.units, this.win);
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);
}
/**
* Update the stimulus, if necessary.
*
* @private
*/
_updateIfNeeded() {
if (!this._needUpdate)
return;
this._needUpdate = false;
this._getPolygon(/*true*/); // this also updates _vertices_px
this._pixi = undefined;
// no polygon to draw: return immediately
if (typeof this._pixiPolygon_px === 'undefined')
return;
// 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._pixi.beginFill(this._fillColor.int, this._opacity);
this._pixi.drawPolygon(this._pixiPolygon_px);
if (typeof this._fillColor !== 'undefined')
this._pixi.endFill();
// set polygon position and rotation:
this._pixi.position = util.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.
*
* @private
* @return {Object} the PIXI polygon corresponding to this stimulus vertices.
*/
_getPolygon(/*force = false*/) {
if (!this._needVertexUpdate)
return;
this._needVertexUpdate = false;
//if (!force && typeof this._pixiPolygon_px !== 'undefined')
// return this._pixiPolygon_px;
// make sure the vertices in pixel units are available, and flatten the array of arrays:
this._getVertices_px(/*force*/);
let coords_px = [];
for (const vertex_px of this._vertices_px)
coords_px.push.apply(coords_px, vertex_px);
// close the polygon if need be:
if (coords_px.length >= 6 && this._closeShape) {
// note: we first check whether the vertices already define a closed polygon:
const n = coords_px.length;
if (coords_px[0] != coords_px[n - 2] || coords_px[1] != coords_px[n - 1]) {
coords_px.push(coords_px[0]);
coords_px.push(coords_px[1]);
}
}
// create the PIXI polygon:
this._pixiPolygon_px = new PIXI.Polygon(coords_px);
return this._pixiPolygon_px;
}
}

163
js/visual/BaseVisualStim.js Normal file
View File

@ -0,0 +1,163 @@
/**
* @file Base class for all visual stimuli.
*
* @author Alain Pitiot
* @version 3.0.0b11
* @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
import {MinimalStim} from '../core/MinimalStim';
import {WindowMixin} from '../core/WindowMixin';
import * as util from '../util/Util';
/**
* Base class for all visual stimuli.
*
* @name module:visual.BaseVisualStim
* @class
* @extends MinimalStim
* @mixes WindowMixin
* @param {Object} options
* @param {String} options.name - the name used when logging messages from this stimulus
* @param {Window} options.win - the associated Window
* @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
* @param {number} [options.opacity= 1.0] - the opacity
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
* @param {number} [options.size= 1.0] - the size
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
export class BaseVisualStim extends util.mix(MinimalStim).with(WindowMixin)
{
constructor({
name,
win,
units = 'norm',
ori = 0.0,
opacity = 1.0,
pos = [0, 0],
size,
autoDraw,
autoLog = false
} = {})
{
super({win, name, autoDraw, autoLog});
// whether the vertices need to be updated:
this._needVertexUpdate = true;
// the vertices (in pixel units):
this._vertices_px = undefined;
this._addAttributes(BaseVisualStim, units, ori, opacity, pos, size);
this._needUpdate = true;
}
/**
* Setter for the size attribute.
*
* @name module:visual.BaseVisualStim#setSize
* @public
* @param {number} size - the stimulus size
* @param {boolean} [log= false] - whether of not to log
*/
setSize(size, log = false)
{
// size is either undefined or a tuple of numbers:
if (typeof size !== 'undefined') {
size = util.toNumerical(size);
if (!Array.isArray(size))
size = [size, size];
}
this._setAttribute('size', size, log);
this._needVertexUpdate = true;
this._needUpdate = true;
}
/**
* Setter for the orientation attribute.
*
* @name module:visual.BaseVisualStim#setOri
* @public
* @param {number} ori - the orientation in degree with 0 as the vertical position, positive values rotate clockwise.
* @param {boolean} [log= false] - whether of not to log
*/
setOri(ori, log = false)
{
this._setAttribute('ori', ori, log);
let radians = ori * 0.017453292519943295;
this._rotationMatrix = [[Math.cos(radians), -Math.sin(radians)],
[Math.sin(radians), Math.cos(radians)]];
//this._needVertexUpdate = true ; // need to update update vertices
this._needUpdate = true;
}
/**
* Setter for the position attribute.
*
* @name module:visual.BaseVisualStim#setPos
* @public
* @param {Array.<number>} pos - position of the center of the stimulus, in stimulus units
* @param {boolean} [log= false] - whether of not to log
*/
setPos(pos, log = false)
{
this._setAttribute('pos', util.toNumerical(pos), log);
this._needUpdate = true;
}
/**
* Setter for the opacity attribute.
*
* @name module:visual.BaseVisualStim#setOpacity
* @public
* @param {number} opacity - the opacity: 0 is completely transparent, 1 is fully opaque
* @param {boolean} [log= false] - whether of not to log
*/
setOpacity(opacity, log = false)
{
this._setAttribute('opacity', opacity, log);
this._needUpdate = true;
}
/*
* Get the vertices in pixel units.
*
* @name module:visual.BaseVisualStim#_getVertices_px
* @private
* @return {Array.<[number, number]>} the vertices
*/
_getVertices_px(/*force = false*/)
{
/*if (!force && typeof this._vertices_px !== 'undefined')
return this._vertices_px;*/
// handle flipping:
let flip = [1.0, 1.0];
if ('_flipHoriz' in this && this._flipHoriz)
flip[0] = -1.0;
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) );
return this._vertices_px;
}
}

281
js/visual/ImageStim.js Normal file
View File

@ -0,0 +1,281 @@
/**
* @file Image Stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
* @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
import { BaseVisualStim } from './BaseVisualStim';
import { Color } from '../util/Color';
import { ColorMixin } from '../util/ColorMixin';
import * as util from '../util/Util';
/**
* Image Simulus.
*
* @name module:visual.ImageStim
* @class
* @extends BaseVisualStim
* @mixes ColorMixin
* @param {Object} options
* @param {String} options.name - the name used when logging messages from this stimulus
* @param {Window} options.win - the associated Window
* @param {string | HTMLImageElement} options.image - the name of the image resource or HTMLImageElement corresponding to the image
* @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask
* @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
* @param {string} options.units - the units of the stimulus vertices, size and position
* @param {number} options.ori - the orientation (in degrees)
* @param {number} options.size - the size
* @param {Color} [options.color= Color('white')] the background color
* @param {number} [options.opacity= 1.0] - the opacity
* @param {number} [options.contrast= 1.0] - the contrast
* @param {number} [options.depth= 0] - the depth
* @param {number} [options.texRes= 128] - the resolution of the text
* @param {boolean} [options.interpolate= false] - whether or not the image is interpolated
* @param {boolean} [flipHoriz= false] - whether or not to flip horizontally
* @param {boolean} [flipVert= false] - whether or not to flip vertically
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
export class ImageStim extends util.mix(BaseVisualStim).with(ColorMixin)
{
constructor({
name,
win,
image,
mask,
pos,
units,
ori,
size,
color = new Color('white'),
opacity = 1.0,
contrast = 1.0,
texRes = 128,
depth = 0,
interpolate = false,
flipHoriz = false,
flipVert = false,
autoDraw,
autoLog
} = {}) {
super({ name, win, units, ori, opacity, pos, size, autoDraw, autoLog });
this.psychoJS.logger.debug('create a new ImageStim with name: ', name);
this._addAttributes(ImageStim, image, mask, color, contrast, texRes, interpolate, depth, flipHoriz, flipVert);
/*if (autoLog)
logging.exp("Created %s = %s" % (self.name, str(self)));*/
}
/**
* Setter for the image attribute.
*
* @name module:visual.ImageStim#setImage
* @public
* @param {HTMLImageElement | string} image - the name of the image resource or HTMLImageElement corresponding to the image
* @param {boolean} [log= false] - whether of not to log
*/
setImage(image, log = false) {
let response = { 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 sympton of an actual problem
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');
}
else {
// image is a string: it should be the name of a resource, which we load
if (typeof image === 'string')
image = this.psychoJS.serverManager.getResource(image);
// 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" }';
this.psychoJS.logger.debug('set the image of ImageStim: ' + this._name + ' as: src= ' + image.src + ', size= ' + image.width + 'x' + image.height);
}
this._setAttribute('image', image, log);
this._needUpdate = true;
}
catch (error) {
throw { ...response, error };
}
}
/**
* Setter for the mask attribute.
*
* @name module:visual.ImageStim#setImage
* @public
* @param {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask
* @param {boolean} [log= false] - whether of not to log
*/
setMask(mask, log = false) {
let response = { 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') {
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')
mask = this.psychoJS.serverManager.getResource(mask);
// mask should now be an actual HTMLImageElement: we raise an error if it is not
if (!(mask instanceof HTMLImageElement))
throw 'the argument: ' + mask.toString() + ' is not an image" }';
this.psychoJS.logger.debug('set the mask of ImageStim: ' + this._name + ' as: src= ' + mask.src + ', size= ' + mask.width + 'x' + mask.height);
}
this._setAttribute('mask', mask, log);
this._needUpdate = true;
}
catch (error) {
throw { ...response, error };
}
}
/**
* Setter for the flipVert attribute.
*
* @name module:visual.ImageStim#setFlipVert
* @public
* @param {boolean} flipVert - whether or not to flip vertically
* @param {boolean} [log= false] - whether of not to log
*/
setFlipVert(flipVert, log = false) {
this._setAttribute('flipVert', flipVert, log);
this._needUpdate = true;
}
/**
* Setter for the flipHoriz attribute.
*
* @name module:visual.ImageStim#setFlipHoriz
* @public
* @param {boolean} flipHoriz - whether or not to flip horizontally
* @param {boolean} [log= false] - whether of not to log
*/
setFlipHoriz(flipHoriz, log = false) {
this._setAttribute('flipHoriz', flipHoriz, log);
this._needUpdate = true;
}
/**
* Determine whether the given object is inside this image.
*
* @name module:visual.ImageStim#contains
* @public
* @param {Object} object - the object
* @param {string} units - the units
* @return {boolean} whether or not the image contains the object
*/
contains(object, units) {
// get position of object:
let objectPos_px = util.getPositionFromObject(object, units);
if (typeof objectPos_px === 'undefined')
throw { origin : 'ImageStim.contains', context : 'when determining whether ImageStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
// test for inclusion:
// note: since _pixi.anchor is [0.5, 0.5] the image is actually centered on pos
let pos_px = util.to_px(this.pos, this.units, this._win);
let size_px = util.to_px(this.size, this.units, this._win);
const polygon_px = [
[pos_px[0] - size_px[0] / 2, pos_px[1] - size_px[1] / 2],
[pos_px[0] + size_px[0] / 2, pos_px[1] - size_px[1] / 2],
[pos_px[0] + size_px[0] / 2, pos_px[1] + size_px[1] / 2],
[pos_px[0] - size_px[0] / 2, pos_px[1] + size_px[1] / 2]];
return util.IsPointInsidePolygon(objectPos_px, polygon_px);
}
/**
* Update the stimulus, if necessary.
*
* @name module:visual.ImageStim#_updateIfNeeded
* @private
*/
_updateIfNeeded() {
if (!this._needUpdate)
return;
this._needUpdate = false;
this._pixi = undefined;
// no image to draw: return immediately
if (typeof this._image === 'undefined')
return;
// prepare the image:
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image));
//this._texture = new PIXI.Texture(PIXI.BaseTexture.fromImage(this._image));
this._pixi = new PIXI.Sprite(this._texture);
this._pixi.zOrder = this.depth;
// add a mask if need be:
if (typeof this._mask !== 'undefined') {
this._maskTexture = new PIXI.Texture(new PIXI.BaseTexture(this._mask));
this._pixi.mask = new PIXI.Sprite(this._maskTexture); //PIXI.Sprite.fromImage(this._mask);
// the following is required for the mask to be aligned with the image
this._pixi.mask.anchor.x = 0.5;
this._pixi.mask.anchor.y = 0.5;
this._pixi.addChild(this._pixi.mask);
}
// 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:
if (this._texture.width == 0) {
this._needUpdate = true;
return;
}
this._pixi.alpha = this.opacity;
// stimulus size:
// note: we use the size of the texture if ImageStim has no specified size:
let stimSize = this.size;
if (typeof stimSize === 'undefined') {
const textureSize = [this._texture.width, this._texture.height];
stimSize = util.to_unit(textureSize, 'pix', this.win, this.units);
}
// set the scale:
const size_px = util.to_px(stimSize, this.units, this.win);
var scaleX = size_px[0] / this._texture.width;
var scaleY = size_px[1] / this._texture.height;
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
this._pixi.position = util.to_pixiPoint(this.pos, this.units, this.win);
this._pixi.rotation = this.ori * Math.PI / 180;
// the image is centered on pos:
this._pixi.anchor.x = 0.5;
this._pixi.anchor.y = 0.5;
}
}

122
js/visual/Rect.js Normal file
View File

@ -0,0 +1,122 @@
/**
* @file Rectangular Stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
* @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
import { BaseShapeStim } from './BaseShapeStim';
import { Color } from '../util/Color';
/**
* <p>Rectangular visual stimulus.</p>
*
* @name module:visual.Rect
* @class
* @extends BaseShapeStim
* @param {Object} options
* @param {String} options.name - the name used when logging messages from this stimulus
* @param {Window} options.win - the associated Window
* @param {number} [options.lineWidth= 1.5] - the line width
* @param {Color} [options.lineColor= Color('white')] the line color
* @param {Color} options.fillColor - the fill color
* @param {number} [options.opacity= 1.0] - the opacity
* @param {number} [options.width= 0.5] - the width of the rectangle
* @param {number} [options.height= 0.5] - the height of the rectangle
* @param {Array.<number>} [options.pos= [0, 0]] - the position
* @param {number} [options.size= 1.0] - the size
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
* @param {string} options.units - the units of the stimulus vertices, size and position
* @param {number} [options.contrast= 1.0] - the contrast
* @param {number} [options.depth= 0] - the depth
* @param {boolean} [options.interpolate= true] - whether or not the shape is interpolated
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
export class Rect extends BaseShapeStim {
constructor({
name,
win,
lineWidth = 1.5,
lineColor = new Color('white'),
fillColor,
opacity = 1.0,
width = 0.5,
height = 0.5,
pos = [0, 0],
size = 1.0,
ori = 0.0,
units,
contrast = 1.0,
depth = 0,
interpolate = true,
autoDraw,
autoLog
} = {}) {
super({ name, win, lineWidth, lineColor, fillColor, opacity, pos, ori, size, units, contrast, depth, interpolate, autoDraw, autoLog });
this._psychoJS.logger.debug('create a new Rect with name: ', name);
this._addAttributes(Rect, width, height);
this._updateVertices();
/*if (autoLog)
logging.exp("Created %s = %s" % (self.name, str(self)));*/
}
/**
* Setter for the width attribute.
*
* @name module:visual.Rect#setWidth
* @public
* @param {number} width - the rectange width
* @param {boolean} [log= false] - whether of not to log
*/
setWidth(width, log = false) {
this._psychoJS.logger.debug('set the width of Rect: ', this.name, 'to: ', width);
this._setAttribute('width', width, log);
this._updateVertices();
}
/**
* Setter for the height attribute.
*
* @name module:visual.Rect#setHeight
* @public
* @param {number} height - the rectange height
* @param {boolean} [log= false] - whether of not to log
*/
setHeight(height, log = false) {
this._psychoJS.logger.debug('set the height of Rect: ', this.name, 'to: ', height);
this._setAttribute('height', height, log);
this._updateVertices();
}
/**
* Update the base shape vertices.
*
* @name module:visual.Rect#_updateVertices
* @private
*/
_updateVertices() {
this._psychoJS.logger.debug('update the vertices of Rect: ', this.name);
this.setVertices([
[-this._width / 2.0, -this._height / 2.0],
[this._width / 2.0, -this._height / 2.0],
[this._width / 2.0, this._height / 2.0],
[-this._width / 2.0, this._height / 2.0]
]);
}
}

276
js/visual/TextStim.js Normal file
View File

@ -0,0 +1,276 @@
/**
* @file Text Stimulus.
*
* @author Alain Pitiot
* @version 3.0.0b11
* @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
import { BaseVisualStim } from './BaseVisualStim';
import { Color } from '../util/Color';
import { ColorMixin } from '../util/ColorMixin';
import * as util from '../util/Util';
/**
* @name module:visual.TextStim
* @class
* @extends BaseVisualStim
* @mixes ColorMixin
* @param {Object} options
* @param {String} options.name - the name used when logging messages from this stimulus
* @param {string} [options.text="Hello World"] - the text to be rendered
* @param {string} [options.font= "Arial"] - the text font
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the text
* @param {Color} [options.color= Color('white')] the background color
* @param {number} [options.opacity= 1.0] - the opacity
* @param {number} [options.contrast= 1.0] - the contrast
* @param {string} [options.units= "norm"] - the units of the text size and position
* @param {number} options.ori - the orientation (in degrees)
* @param {number} [options.height] - 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} [alignHoriz = 'center'] - horizontal alignment
* @param {string} [alignVert = 'center'] - vertical alignment
* @param {boolean} wrapWidth - whether or not to wrapthe text horizontally
* @param {boolean} [flipHoriz= false] - whether or not to flip the text horizontally
* @param {boolean} [flipVert= false] - whether or not to flip the text vertically
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
* @param {boolean} [options.autoLog= false] - whether or not to log
*
* @todo vertical alignment, and orientation are currently NOT implemented
*/
export class TextStim extends util.mix(BaseVisualStim).with(ColorMixin)
{
constructor({
name,
win,
text = 'Hello World',
font = 'Arial',
pos,
color = new Color('white'),
opacity,
contrast = 1.0,
units = 'norm',
ori,
height,
bold = false,
italic = false,
alignHoriz = 'center',
alignVert = 'center',
wrapWidth,
flipHoriz = false,
flipVert = false,
autoDraw,
autoLog
} = {}) {
super({ name, win, units, ori, opacity, pos, autoDraw, autoLog });
this._addAttributes(TextStim, text, font, color, contrast, height, bold, italic, alignHoriz, alignVert, wrapWidth, flipHoriz, flipVert);
/*if (autoLog)
logging.exp("Created %s = %s" % (self.name, str(self)));*/
}
/**
* Setter for the text attribute.
*
* @name module:visual.TextStim#setText
* @public
* @param {string} text - the text
* @param {boolean} [log= false] - whether of not to log
*/
setText(text, log) {
this._setAttribute('text', text, log);
this._needUpdate = true;
//this._needVertexUpdate = true;
}
/**
* Setter for the alignHoriz attribute.
*
* @name module:visual.TextStim#setAlignHoriz
* @public
* @param {string} alignHoriz - the text horizontal alignment, e.g. 'center'
* @param {boolean} [log= false] - whether of not to log
*/
setAlignHoriz(alignHoriz, log) {
this._setAttribute('alignHoriz', alignHoriz, log);
this._needUpdate = true;
//this._needVertexUpdate = true;
}
/**
* Setter for the wrapWidth attribute.
*
* @name module:visual.TextStim#setWrapWidth
* @public
* @param {boolean} wrapWidth - whether or not to wrap the text at the given width
* @param {boolean} [log= false] - whether of not to log
*/
setWrapWidth(wrapWidth, log) {
this._setAttribute('wrapWidth', wrapWidth, log);
this._needUpdate = true;
//this._needVertexUpdate = true;
}
/**
* Setter for the height attribute.
*
* @name module:visual.TextStim#setHeight
* @public
* @param {number} height - text height
* @param {boolean} [log= false] - whether of not to log
*/
setHeight(height, log) {
this._setAttribute('height', height, log);
this._needUpdate = true;
//this._needVertexUpdate = true;
}
/**
* Setter for the italic attribute.
*
* @name module:visual.TextStim#setItalic
* @public
* @param {boolean} italic - whether or not the text is italic
* @param {boolean} [log= false] - whether of not to log
*/
setItalic(italic, log) {
this._setAttribute('italic', italic, log);
this._needUpdate = true;
//this._needVertexUpdate = true;
}
/**
* Setter for the bold attribute.
*
* @name module:visual.TextStim#setBold
* @public
* @param {boolean} bold - whether or not the text is bold
* @param {boolean} [log= false] - whether of not to log
*/
setBold(bold, log) {
this._setAttribute('bold', bold, log);
this._needUpdate = true;
//this._needVertexUpdate = true;
}
/**
* Setter for the flipVert attribute.
*
* @name module:visual.TextStim#setFlipVert
* @public
* @param {boolean} flipVert - whether or not to flip vertically
* @param {boolean} [log= false] - whether of not to log
*/
setFlipVert(flipVert, log) {
this._setAttribute('flipVert', flipVert, log);
this._needUpdate = true;
//this._needVertexUpdate = true;
}
/**
* Setter for the flipHoriz attribute.
*
* @name module:visual.TextStim#setFlipHoriz
* @public
* @param {boolean} flipHoriz - whether or not to flip horizontally
* @param {boolean} [log= false] - whether of not to log
*/
setFlipHoriz(flipHoriz, log) {
this._setAttribute('flipHoriz', flipHoriz, log);
this._needUpdate = true;
//this._needVertexUpdate = true;
}
/**
* Determine whether an object is inside the bounding box of the text.
*
* @name module:visual.TextStim#contains
* @public
* @param {Object} object - the object
* @param {string} units - the units
* @return {boolean} whether or not the object is inside the bounding box of the text
*
* @todo this is currently NOT implemented
*/
contains(object, units) {
// get position of object:
let objectPos_px = util.getPositionFromObject(object, units);
if (typeof objectPos_px === 'undefined')
throw { origin : 'TextStim.contains', context : 'when determining whether ImageStim: ' + this._name + ' contains object: ' + util.toString(object), error : 'unable to determine the position of the object' };
// test for inclusion:
// TODO
return false;
}
/**
* Update the stimulus, if necessary.
*
* @name module:visual.TextStim#_updateIfNeeded
* @private
*
* @todo take size into account
*/
_updateIfNeeded() {
if (this._needUpdate) {
let height = this._height || 0.1;
this._heightPix = this._getLengthPix(height);
var fontSize = Math.round(this._heightPix);
let color = this._getDesiredColor(this._color, this._contrast);
var font =
(this._bold ? 'bold ' : '') +
(this._italic ? 'italic ' : '') +
fontSize + 'px ' + this._font;
this._pixi = new PIXI.Text(this._text, {
font: font,
fill: color.hex,
align: this._alignHoriz,
wordWrap: this._wrapWidth != undefined,
wordWrapWidth: this._wrapWidth ? this._getHorLengthPix(this._wrapWidth) : 0
});
this._pixi.anchor.x = 0.5;
this._pixi.anchor.y = 0.5;
this._pixi.scale.x = this._flipHoriz ? -1 : 1;
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.alpha = this._opacity;
this._size = [
this._getLengthUnits(Math.abs(this._pixi.width)),
this._getLengthUnits(Math.abs(this._pixi.height))];
this._needUpdate = false;
}
}
}