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

Merge pull request #519 from lightest/CU-25m6jrz_anchor_for_web

Unified anchor implementation for visual stims
This commit is contained in:
Alain Pitiot 2022-08-03 10:49:56 +02:00 committed by GitHub
commit e79f0d8e1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 163 additions and 86 deletions

View File

@ -468,7 +468,7 @@ export class GratingStim extends VisualStim
*/
_getDisplaySize()
{
let displaySize = this.size;
let displaySize = this._size;
if (typeof displaySize === "undefined")
{
@ -518,10 +518,10 @@ export class GratingStim extends VisualStim
geometry.addAttribute(
"aVertexPosition",
[
0, 0,
this._size_px[0], 0,
this._size_px[0], this._size_px[1],
0, this._size_px[1]
-this._size_px[0] * .5, -this._size_px[1] * .5,
this._size_px[0] * .5, -this._size_px[1] * .5,
this._size_px[0] * .5, this._size_px[1] * .5,
-this._size_px[0] * .5, this._size_px[1] * .5
],
2
);
@ -643,6 +643,25 @@ export class GratingStim extends VisualStim
}
}
/**
* Setter for the anchor attribute.
*
* @param {string} anchor - anchor of the stim
* @param {boolean} [log= false] - whether or not to log
*/
setAnchor (anchor = "center", log = false)
{
this._setAttribute("anchor", anchor, log);
if (this._pixi !== undefined)
{
// Vertices are set directly with origin at [0, 0], centered around it.
// Subtracting 0.5 from anchorNum vals to get desired effect.
const anchorNum = this._anchorTextToNum(this._anchor);
this._pixi.pivot.x = (anchorNum[0] - 0.5) * this._pixi.scale.x * this._pixi.width;
this._pixi.pivot.y = (anchorNum[1] - 0.5) * this._pixi.scale.y * this._pixi.height;
}
}
/**
* Update the stimulus, if necessary.
*
@ -660,6 +679,7 @@ export class GratingStim extends VisualStim
if (this._needPixiUpdate)
{
this._needPixiUpdate = false;
this._size_px = util.to_px(this._size, this.units, this.win);
let shaderName;
let shaderUniforms;
let currentUniforms = {};
@ -706,7 +726,6 @@ export class GratingStim extends VisualStim
};
}
this._pixi = this._getPixiMeshFromPredefinedShaders(shaderName, Object.assign(shaderUniforms, currentUniforms));
this._pixi.pivot.set(this._pixi.width * 0.5, this._pixi.width * 0.5);
this._pixi.filters = [this._adjustmentFilter];
// add a mask if need be:
@ -751,16 +770,12 @@ export class GratingStim extends VisualStim
this._pixi.zIndex = -this._depth;
this.opacity = this._opacity;
this.anchor = this._anchor;
// set the scale:
const displaySize = this._getDisplaySize();
this._size_px = util.to_px(displaySize, this.units, this.win);
const scaleX = this._size_px[0] / this._pixi.width;
const scaleY = this._size_px[1] / this._pixi.height;
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
this._pixi.scale.x = 1;
this._pixi.scale.y = -1;
// set the position, rotation, and anchor (image centered on pos):
let pos = to_pixiPoint(this.pos, this.units, this.win);
this._pixi.position.set(pos.x, pos.y);
this._pixi.rotation = -this.ori * Math.PI / 180;

View File

@ -2,8 +2,8 @@
* Image Stimulus.
*
* @author Alain Pitiot
* @version 2021.2.3
* @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2021 Open Science Tools Ltd. (https://opensciencetools.org)
* @version 2022.2.3
* @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org)
* @license Distributed under the terms of the MIT License
*/
@ -32,6 +32,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
* @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.anchor = "center"] - sets the origin point of the stim
* @param {string} [options.units= 'norm'] - the units of the stimulus vertices, size and position
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
* @param {number} [options.size] - the size of the rendered image (the size of the image will be used if size is not specified)
@ -46,9 +47,9 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
* @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
*/
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, anchor, 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, anchor, size, autoDraw, autoLog });
this._addAttribute(
"image",
@ -350,14 +351,13 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
const size_px = util.to_px(displaySize, this.units, this.win);
const scaleX = size_px[0] / this._texture.width;
const scaleY = size_px[1] / this._texture.height;
this.anchor = this._anchor;
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
// set the position, rotation, and anchor (image centered on pos):
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;
// re-estimate the bounding box, as the texture's width may now be available:
this._estimateBoundingBox();

View File

@ -34,6 +34,7 @@ export class MovieStim extends VisualStim
* movie resource or of a HTMLVideoElement or of a Camera component
* @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.anchor = "center"] - sets the origin point of the stim
* @param {string} [options.units= 'norm'] - the units of the stimulus vertices, size and position
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
* @param {number} [options.size] - the size of the rendered image (the size of the image will be used if size is not specified)
@ -50,9 +51,9 @@ export class MovieStim extends VisualStim
* @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
*/
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, anchor, 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, anchor, size, autoDraw, autoLog });
this.psychoJS.logger.debug("create a new MovieStim with name: ", name);
@ -403,8 +404,7 @@ export class MovieStim extends VisualStim
// set the position, rotation, and anchor (movie centered on pos):
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;
this.anchor = this._anchor;
// re-estimate the bounding box, as the texture's width may now be available:
this._estimateBoundingBox();

View File

@ -29,6 +29,7 @@ export class Rect extends ShapeStim
* @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 {string} [options.anchor = "center"] - sets the origin point of the stim
* @param {number} [options.size= 1.0] - the size
* @param {number} [options.ori= 0.0] - the orientation (in degrees)
* @param {string} [options.units= "height"] - the units of the stimulus vertices, size and position
@ -38,7 +39,7 @@ export class Rect extends ShapeStim
* @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
*/
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, anchor, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog } = {})
{
super({
name,
@ -48,6 +49,7 @@ export class Rect extends ShapeStim
fillColor,
opacity,
pos,
anchor,
ori,
size,
units,

View File

@ -35,6 +35,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
* @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 {string} [options.anchor = "center"] - sets the origin point of the stim
* @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
@ -44,9 +45,9 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
* @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
*/
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, anchor, 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, anchor, depth, size, autoDraw, autoLog });
// the PIXI polygon corresponding to the vertices, in pixel units:
this._pixiPolygon_px = undefined;
@ -175,6 +176,26 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
return util.IsPointInsidePolygon(objectPos_px, polygon_px);
}
/**
* Setter for the anchor attribute.
*
* @param {string} anchor - anchor of the stim
* @param {boolean} [log= false] - whether or not to log
*/
setAnchor (anchor = "center", log = false)
{
this._setAttribute("anchor", anchor, log);
if (this._pixi !== undefined)
{
// since vertices are passed directly, usually assuming origin at [0, 0]
// and already being centered around it, subtracting 0.5 from anchorNum vals
// to get a desired effect.
const anchorNum = this._anchorTextToNum(this._anchor);
this._pixi.pivot.x = (anchorNum[0] - 0.5) * this._pixi.scale.x * this._pixi.width;
this._pixi.pivot.y = (anchorNum[1] - 0.5) * -this._pixi.scale.y * this._pixi.height;
}
}
/**
* Estimate the bounding box.
*
@ -252,6 +273,7 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
}
this._pixi.zIndex = -this._depth;
this.anchor = this._anchor;
}
// set polygon position and rotation:
@ -288,7 +310,6 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
coords_px.push(coords_px[1]);
}
}
// destroy the previous PIXI polygon and create a new one:
this._pixiPolygon_px = new PIXI.Polygon(coords_px);
this._pixiPolygon_px.closeStroke = this._closeShape;

View File

@ -432,6 +432,24 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
}
}
/**
* Setter for the anchor attribute.
*
* @param {string} anchor - anchor of the stim
* @param {boolean} [log= false] - whether or not to log
*/
setAnchor (anchor = "center", log = false)
{
this._setAttribute("anchor", anchor, log);
if (this._pixi !== undefined)
{
// container has origin at [0, 0], subtracting 0.5 from anchorNum vals to get a desired effect.
const anchorNum = this._anchorTextToNum(this._anchor);
this._pixi.pivot.x = (anchorNum[0] - 0.5) * this._pixi.scale.x * this._pixi.width;
this._pixi.pivot.y = (anchorNum[1] - 0.5) * this._pixi.scale.y * this._pixi.height;
}
}
/** Let `borderColor` alias `lineColor` to parallel PsychoPy */
set borderColor(color)
{
@ -748,6 +766,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
this._pixi.alpha = this._opacity;
this._pixi.zIndex = -this._depth;
this.anchor = this._anchor;
// make sure that the dependent Stimuli are also updated:
for (const dependentStim of this._dependentStims)

View File

@ -39,7 +39,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
* @param {number} [options.letterHeight= <default value>] - 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.anchor = 'left'] - horizontal alignment
* @param {string} [options.anchor = "center"] - sets the origin point of the stim
*
* @param {boolean} [options.multiline= false] - whether or not a multiline element is used
* @param {boolean} [options.autofocus= true] - whether or not the first input should receive focus by default
@ -89,7 +89,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
} = {},
)
{
super({ name, win, pos, size, units, ori, opacity, depth, clipMask, autoDraw, autoLog });
super({ name, win, pos, anchor, size, units, ori, opacity, depth, clipMask, autoDraw, autoLog });
this._addAttribute(
"text",
@ -102,11 +102,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
"",
this._onChange(true, true),
);
this._addAttribute(
"anchor",
anchor,
"center"
);
this._addAttribute(
"flipHoriz",
flipHoriz,
@ -269,22 +264,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
}
}
/**
* Setter for the anchor attribute.
*
* @param {boolean} anchor - anchor of the textbox
* @param {boolean} [log= false] - whether or not to log
*/
setAnchor (anchor = "center", log = false)
{
this._setAttribute("anchor", anchor, log);
if (this._pixi !== undefined) {
const anchorUnits = this._getAnchor();
this._pixi.anchor.x = anchorUnits[0];
this._pixi.anchor.y = anchorUnits[1];
}
}
/**
* For tweaking the underlying input value.
*
@ -574,7 +553,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
const boxHeight = this._letterHeight + 2 * this._padding + 2 * this._borderWidth;
// take the alignment into account:
const anchor = this._getAnchor();
const anchor = this._anchorTextToNum(this._anchor);
this._boundingBox = new PIXI.Rectangle(
this._pos[0] - anchor[0] * this._size[0],
this._pos[1] - anchor[1] * boxHeight,
@ -665,36 +644,6 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
// apply the clip mask:
this._pixi.mask = this._clipMask;
}
/**
* Convert the anchor attribute into numerical values.
*
* @protected
* @return {number[]} - the anchor, as an array of numbers in [0,1]
*/
_getAnchor()
{
const anchor = [0.5, 0.5];
if (this._anchor.indexOf("left") > -1)
{
anchor[0] = 0;
}
else if (this._anchor.indexOf("right") > -1)
{
anchor[0] = 1;
}
if (this._anchor.indexOf("top") > -1)
{
anchor[1] = 0;
}
else if (this._anchor.indexOf("bottom") > -1)
{
anchor[1] = 1;
}
return anchor;
}
}
TextBox._alignmentToFlexboxMap = new Map([

View File

@ -31,6 +31,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
* @param {string} [options.text="Hello World"] - the text to be rendered
* @param {string} [options.font= "Arial"] - the font family
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the text
* @param {string} [options.anchor = "center"] - sets the origin point of the stim
* @param {Color} [options.color= 'white'] the background color
* @param {number} [options.opacity= 1.0] - the opacity
* @param {number} [options.depth= 0] - the depth (i.e. the z order)
@ -56,6 +57,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
text,
font,
pos,
anchor,
color,
opacity,
depth,
@ -76,7 +78,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
} = {},
)
{
super({ name, win, units, ori, opacity, depth, pos, clipMask, autoDraw, autoLog });
super({ name, win, units, ori, opacity, depth, pos, anchor, clipMask, autoDraw, autoLog });
// callback to deal with text metrics invalidation:
const onChange = (withPixi = false, withBoundingBox = false, withMetrics = false) =>
@ -166,6 +168,13 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
this._onChange(true, false)
);
// alignHoriz and alignVert should be deprecated and replaced by anchor, but leaving this here
// for compatibility for a while.
if (typeof alignVert === "string" && typeof alignHoriz === "string")
{
this.anchor = `${alignVert}-${alignHoriz}`;
}
// estimate the bounding box (using TextMetrics):
this._estimateBoundingBox();
@ -328,7 +337,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
);
// take the alignment into account:
const anchor = this._getAnchor();
const anchor = this._anchorTextToNum(this._anchor);
this._boundingBox = new PIXI.Rectangle(
this._pos[0] - anchor[0] * textSize[0],
this._pos[1] - textSize[1] + anchor[1] * textSize[1],
@ -405,7 +414,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
// this._pixi.updateText();
}
const anchor = this._getAnchor();
const anchor = this._anchorTextToNum(this._anchor);
[this._pixi.anchor.x, this._pixi.anchor.y] = anchor;
this._pixi.scale.x = this._flipHoriz ? -1 : 1;

View File

@ -3,7 +3,7 @@
* Base class for all visual stimuli.
*
* @author Alain Pitiot
* @version 2022.2.0
* @version 2022.2.3
* @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org)
* @license Distributed under the terms of the MIT License
*/
@ -30,12 +30,13 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
* @param {number} [options.opacity= 1.0] - the opacity
* @param {number} [options.depth= 0] - the depth (i.e. the z order)
* @param {Array.<number>} [options.pos= [0, 0]] - the position of the center of the stimulus
* @param {string} [options.anchor = "center"] - sets the origin point of the stim
* @param {number} [options.size= 1.0] - the size
* @param {PIXI.Graphics} [options.clipMask= null] - the clip mask
* @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
*/
constructor({ name, win, units, ori, opacity, depth, pos, size, clipMask, autoDraw, autoLog } = {})
constructor({ name, win, units, ori, opacity, depth, pos, anchor, size, clipMask, autoDraw, autoLog } = {})
{
super({ win, name, autoDraw, autoLog });
@ -50,6 +51,11 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
pos,
[0, 0],
);
this._addAttribute(
"anchor",
anchor,
"center",
);
this._addAttribute(
"size",
size,
@ -211,6 +217,62 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
return this._getBoundingBox_px().contains(objectPos_px[0], objectPos_px[1]);
}
/**
* Setter for the anchor attribute.
*
* @param {string} anchor - anchor of the stim
* @param {boolean} [log= false] - whether or not to log
*/
setAnchor (anchor = "center", log = false)
{
this._setAttribute("anchor", anchor, log);
if (this._pixi !== undefined)
{
const anchorNum = this._anchorTextToNum(this._anchor);
if (this._pixi.anchor !== undefined)
{
this._pixi.anchor.x = anchorNum[0];
this._pixi.anchor.y = anchorNum[1];
}
else
{
this._pixi.pivot.x = anchorNum[0] * this._pixi.scale.x * this._pixi.width;
this._pixi.pivot.y = anchorNum[1] * this._pixi.scale.y * this._pixi.height;
}
}
}
/**
* Convert the anchor attribute into numerical values.
*
* @protected
* @param {string} anchorText - text version of anchor value ["top-left", "top-right", "center", ...]
* @return {number[]} - the anchor, as an array of numbers in [0,1]
*/
_anchorTextToNum(anchorText = "")
{
const anchor = [0.5, 0.5];
if (anchorText.indexOf("left") > -1)
{
anchor[0] = 0.0;
}
else if (anchorText.indexOf("right") > -1)
{
anchor[0] = 1.0;
}
if (anchorText.indexOf("top") > -1)
{
anchor[1] = 0.0;
}
else if (anchorText.indexOf("bottom") > -1)
{
anchor[1] = 1.0;
}
return anchor;
}
/**
* Estimate the bounding box.
*