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

Merge pull request #594 from psychopy/multitouch

Multitouch support
This commit is contained in:
Nikita Agafonov 2024-03-28 21:26:14 +00:00 committed by GitHub
commit 2de53fcf44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 330 additions and 58 deletions

View File

@ -49,6 +49,12 @@ export class EventManager
// clock reset when mouse is moved:
moveClock: new Clock(),
};
// storing touches in both map and array for fast search and fast access if touchID is known
this._touchInfo = {
touchesArray: [],
touchesMap: {}
};
}
/**
@ -140,6 +146,19 @@ export class EventManager
return this._mouseInfo;
}
/**
* Returns all the data gathered about touches.
*
* @name module:core.EventManager#getTouchInfo
* @function
* @public
* @return {object} the touch info.
*/
getTouchInfo ()
{
return this._touchInfo;
}
/**
* Clear all events from the event buffer.
*
@ -200,7 +219,6 @@ export class EventManager
self._mouseInfo.buttons.pressed[event.button] = 1;
self._mouseInfo.buttons.times[event.button] = self._psychoJS._monotonicClock.getTime() - self._mouseInfo.buttons.clocks[event.button].getLastResetTime();
self._mouseInfo.pos = [event.offsetX, event.offsetY];
this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button down, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
@ -212,10 +230,21 @@ export class EventManager
self._mouseInfo.buttons.pressed[0] = 1;
self._mouseInfo.buttons.times[0] = self._psychoJS._monotonicClock.getTime() - self._mouseInfo.buttons.clocks[0].getLastResetTime();
self._mouseInfo.pos = [event.changedTouches[0].pageX, event.changedTouches[0].pageY];
// we use the first touch, discarding all others:
const touches = event.changedTouches;
self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY];
this._touchInfo.touchesArray = new Array(event.touches.length);
this._touchInfo.touchesMap = {};
let i;
for (i = 0; i < event.touches.length; i++)
{
this._touchInfo.touchesArray[i] = {
id: event.touches[i].identifier,
force: event.touches[i].force,
pos: [event.touches[i].pageX, event.touches[i].pageY],
busy: false
};
this._touchInfo.touchesMap[event.touches[i].identifier] = this._touchInfo.touchesArray[i];
}
this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button down, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
}, false);
@ -249,10 +278,20 @@ export class EventManager
self._mouseInfo.buttons.pressed[0] = 0;
self._mouseInfo.buttons.times[0] = self._psychoJS._monotonicClock.getTime() - self._mouseInfo.buttons.clocks[0].getLastResetTime();
self._mouseInfo.pos = [event.changedTouches[0].pageX, event.changedTouches[0].pageY];
// we use the first touch, discarding all others:
const touches = event.changedTouches;
self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY];
this._touchInfo.touchesArray = new Array(event.touches.length);
this._touchInfo.touchesMap = {};
let i;
for (i = 0; i < event.touches.length; i++)
{
this._touchInfo.touchesArray[i] = {
id: event.touches[i].identifier,
force: event.touches[i].force,
pos: [event.touches[i].pageX, event.touches[i].pageY]
};
this._touchInfo.touchesMap[event.touches[i].identifier] = this._touchInfo.touchesArray[i];
}
this._psychoJS.experimentLogger.data("Mouse: " + event.button + " button up, pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
}, false);
@ -270,10 +309,20 @@ export class EventManager
event.preventDefault();
self._mouseInfo.moveClock.reset();
self._mouseInfo.pos = [event.changedTouches[0].pageX, event.changedTouches[0].pageY];
// we use the first touch, discarding all others:
const touches = event.changedTouches;
self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY];
this._touchInfo.touchesArray = new Array(event.touches.length);
this._touchInfo.touchesMap = {};
let i;
for (i = 0; i < event.touches.length; i++)
{
this._touchInfo.touchesArray[i] = {
id: event.touches[i].identifier,
force: event.touches[i].force,
pos: [event.touches[i].pageX, event.touches[i].pageY]
};
this._touchInfo.touchesMap[event.touches[i].identifier] = this._touchInfo.touchesArray[i];
}
}, false);
// (*) wheel

View File

@ -152,7 +152,7 @@ export class Window extends PsychObject
}
this._rootContainer.destroy();
if (document.body.contains(this._renderer.view))
{
document.body.removeChild(this._renderer.view);
@ -166,7 +166,6 @@ export class Window extends PsychObject
}
this._renderer.destroy();
window.removeEventListener("resize", this._resizeCallback);
window.removeEventListener("orientationchange", this._resizeCallback);
@ -316,7 +315,7 @@ export class Window extends PsychObject
*/
removePixiObject(pixiObject)
{
this._stimsContainer.removeChild(pixiObject);
this._stimsContainer.removeChild(pixiObject);
}
/**
@ -477,11 +476,11 @@ export class Window extends PsychObject
// create a top-level PIXI container:
this._rootContainer = new PIXI.Container();
this._rootContainer.addChild(this._backgroundSprite, this._stimsContainer);
// sorts children according to their zIndex value. Higher zIndex means it will be moved towards the end of the array,
// and thus rendered on top of previous one.
this._rootContainer.sortableChildren = true;
this._rootContainer.interactive = true;
this._rootContainer.filters = [this._adjustmentFilter];
@ -490,28 +489,7 @@ export class Window extends PsychObject
// touch/mouse events are treated by PsychoJS' event manager:
this.psychoJS.eventManager.addMouseListeners(this._renderer);
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
this._resizeCallback = (e) =>
{
// if the user device is a mobile phone or tablet (we use the presence of a touch screen as a
// proxy), we need to detect whether the change in size is due to the appearance of a virtual keyboard
// in which case we do not want to resize the canvas. This is rather tricky and so we resort to
// the below trick. It would be better to use the VirtualKeyboard API, but it is not widely
// available just yet, as of 2023-06.
const keyboardHeight = 300;
if (hasTouchScreen() && (window.screen.height - window.visualViewport.height) > keyboardHeight)
{
return;
}
Window._resizePixiRenderer(this, e);
this._backgroundSprite.width = this._size[0];
this._backgroundSprite.height = this._size[1];
this._fullRefresh();
};
window.addEventListener("resize", this._resizeCallback);
window.addEventListener("orientationchange", this._resizeCallback);
this._addEventListeners();
}
/**
@ -544,6 +522,80 @@ export class Window extends PsychObject
pjsWindow._rootContainer.scale.y = -1;
}
_handlePointerDown (e)
{
let i;
let pickedPixi;
let tmpPoint = new PIXI.Point();
const cursorPos = new PIXI.Point(e.pageX, e.pageY);
for (i = this._stimsContainer.children.length - 1; i >= 0; i--)
{
if (typeof this._stimsContainer.children[i].containsPoint === "function" &&
this._stimsContainer.children[i].containsPoint(cursorPos))
{
pickedPixi = this._stimsContainer.children[i];
break;
}
else if (this._stimsContainer.children[i].containsPoint === undefined &&
this._stimsContainer.children[i] instanceof PIXI.DisplayObject)
{
this._stimsContainer.children[i].worldTransform.applyInverse(cursorPos, tmpPoint);
if (this._stimsContainer.children[i].getLocalBounds().contains(tmpPoint.x, tmpPoint.y))
{
pickedPixi = this._stimsContainer.children[i];
break;
}
}
}
this.emit("pointerdown", {
pixi: pickedPixi,
originalEvent: e
});
}
_handlePointerUp (e)
{
this.emit("pointerup", {
originalEvent: e
});
}
_handlePointerMove (e)
{
this.emit("pointermove", {
originalEvent: e
});
}
_addEventListeners ()
{
this._renderer.view.addEventListener("pointerdown", this._handlePointerDown.bind(this));
this._renderer.view.addEventListener("pointerup", this._handlePointerUp.bind(this));
this._renderer.view.addEventListener("pointermove", this._handlePointerMove.bind(this));
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
this._resizeCallback = (e) =>
{
// if the user device is a mobile phone or tablet (we use the presence of a touch screen as a
// proxy), we need to detect whether the change in size is due to the appearance of a virtual keyboard
// in which case we do not want to resize the canvas. This is rather tricky and so we resort to
// the below trick. It would be better to use the VirtualKeyboard API, but it is not widely
// available just yet, as of 2023-06.
const keyboardHeight = 300;
if (hasTouchScreen() && (window.screen.height - window.visualViewport.height) > keyboardHeight)
{
return;
}
Window._resizePixiRenderer(this, e);
this._backgroundSprite.width = this._size[0];
this._backgroundSprite.height = this._size[1];
this._fullRefresh();
};
window.addEventListener("resize", this._resizeCallback);
window.addEventListener("orientationchange", this._resizeCallback);
}
/**
* Send all logged messages to the {@link Logger}.
*

View File

@ -39,6 +39,7 @@ export class ButtonStim extends TextBox
* @param {boolean} [options.italic= false] - whether or not the text is italic
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor(
{
@ -62,6 +63,7 @@ export class ButtonStim extends TextBox
italic,
autoDraw,
autoLog,
draggable,
boxFn,
multiline
} = {},
@ -90,6 +92,7 @@ export class ButtonStim extends TextBox
alignment: "center",
autoDraw,
autoLog,
draggable,
boxFn
});

View File

@ -42,10 +42,11 @@ export class FaceDetector extends VisualStim
* @param {number} [options.opacity= 1.0] - the opacity
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor({name, win, input, modelDir, faceApiUrl, units, ori, opacity, pos, size, autoDraw, autoLog} = {})
constructor({name, win, input, modelDir, faceApiUrl, units, ori, opacity, pos, size, autoDraw, autoLog, draggable} = {})
{
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog});
super({name, win, units, ori, opacity, pos, size, autoDraw, autoLog, draggable});
// TODO deal with onChange (see MovieStim and Camera)
this._addAttribute("input", input, undefined);

View File

@ -54,6 +54,7 @@ export class Form 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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor(
{
@ -82,10 +83,11 @@ export class Form extends util.mix(VisualStim).with(ColorMixin)
clipMask,
autoDraw,
autoLog,
draggable
} = {},
)
{
super({ name, win, units, opacity, depth, pos, size, clipMask, autoDraw, autoLog });
super({ name, win, units, opacity, depth, pos, size, clipMask, autoDraw, autoLog, draggable });
this._addAttribute(
"itemPadding",

View File

@ -426,6 +426,7 @@ export class GratingStim extends VisualStim
* @param {String} [options.blendmode= "avg"] - blend mode of the stimulus, determines how the stimulus is blended with the background. Supported values: "avg", "add", "mul", "screen".
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor({
name,
@ -448,10 +449,11 @@ export class GratingStim extends VisualStim
blendmode,
autoDraw,
autoLog,
maskParams
maskParams,
draggable
} = {})
{
super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog });
super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog, draggable });
this._adjustmentFilter = new AdjustmentFilter({
contrast

View File

@ -46,6 +46,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
* @param {boolean} [options.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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
* @param {ImageStim.AspectRatioStrategy} [options.aspectRatio= ImageStim.AspectRatioStrategy.VARIABLE] - the aspect ratio handling strategy
* @param {number} [options.blurVal= 0] - the blur value. Goes 0 to as hish as you like. 0 is no blur.
*/
@ -70,10 +71,11 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
autoDraw,
autoLog,
aspectRatio,
draggable,
blurVal
} = {})
{
super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog });
super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog, draggable });
// Holds an instance of PIXI blur filter. Used if blur value is passed.
this._blurFilter = undefined;

View File

@ -50,10 +50,11 @@ export class MovieStim extends VisualStim
* @param {boolean} [options.autoPlay= true] - whether or not to autoplay the video
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor({ name, win, movie, pos, anchor, 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, draggable } = {})
{
super({ name, win, units, ori, opacity, pos, anchor, size, autoDraw, autoLog });
super({ name, win, units, ori, opacity, pos, anchor, size, autoDraw, autoLog, draggable });
this.psychoJS.logger.debug("create a new MovieStim with name: ", name);

View File

@ -39,8 +39,9 @@ export class Polygon extends ShapeStim
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor({ name, win, lineWidth, lineColor, fillColor, opacity, edges, radius, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog } = {})
constructor({ name, win, lineWidth, lineColor, fillColor, opacity, edges, radius, pos, size, ori, units, contrast, depth, interpolate, autoDraw, autoLog, draggable } = {})
{
super({
name,
@ -58,9 +59,11 @@ export class Polygon extends ShapeStim
interpolate,
autoDraw,
autoLog,
draggable
});
this._psychoJS.logger.debug("create a new Polygon with name: ", name);
this._psychoJS.logger.debug("create a new Polygon with name: ",
name);
this._addAttribute(
"edges",

View File

@ -38,8 +38,9 @@ export class Rect extends ShapeStim
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor({ name, win, lineWidth, lineColor, fillColor, opacity, width, height, pos, anchor, 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, draggable } = {})
{
super({
name,
@ -58,6 +59,7 @@ export class Rect extends ShapeStim
interpolate,
autoDraw,
autoLog,
draggable
});
this._psychoJS.logger.debug("create a new Rect with name: ", name);

View File

@ -44,10 +44,11 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor({ name, win, lineWidth, lineColor, fillColor, opacity, vertices, closeShape, pos, anchor, 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, draggable } = {})
{
super({ name, win, units, ori, opacity, pos, anchor, depth, size, autoDraw, autoLog });
super({ name, win, units, ori, opacity, pos, anchor, depth, size, autoDraw, autoLog, draggable });
// the PIXI polygon corresponding to the vertices, in pixel units:
this._pixiPolygon_px = undefined;
@ -163,8 +164,8 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
if (typeof objectPos_px === "undefined")
{
throw {
origin: "VisualStim.contains",
context: "when determining whether VisualStim: " + this._name + " contains object: " + util.toString(object),
origin: "ShapeStim.contains",
context: "when determining whether ShapeStim: " + this._name + " contains object: " + util.toString(object),
error: "unable to determine the position of the object",
};
}
@ -176,6 +177,22 @@ export class ShapeStim extends util.mix(VisualStim).with(ColorMixin, WindowMixin
return util.IsPointInsidePolygon(objectPos_px, polygon_px);
}
/**
* Determine whether a point that is nown to have pixel dimensions is inside the bounding box of the stimulus.
*
* @name module:visual.ShapeStim#containsPointPx
* @public
* @param {number[]} point_px - the point in pixels
* @return {boolean} whether or not the object is inside the bounding box of the stimulus
*/
containsPointPx (point_px)
{
const pos_px = util.to_px(this.pos, this.units, this.win);
this._getVertices_px();
const polygon_px = this._vertices_px.map((v) => [v[0] + pos_px[0], v[1] + pos_px[1]]);
return util.IsPointInsidePolygon(point_px, polygon_px);
}
/**
* Setter for the anchor attribute.
*

View File

@ -65,6 +65,7 @@ export class Slider 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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*
* @param {core.MinimalStim[]} [options.dependentStims = [] ] - the list of dependent stimuli,
* which must be updated when this Slider is updated, e.g. a Form.
@ -99,10 +100,11 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin)
autoDraw,
autoLog,
dependentStims,
draggable
} = {},
)
{
super({ name, win, units, ori, opacity, depth, pos, size, clipMask, autoDraw, autoLog });
super({ name, win, units, ori, opacity, depth, pos, size, clipMask, autoDraw, autoLog, draggable });
this._needMarkerUpdate = false;

View File

@ -52,6 +52,7 @@ export class TextBox 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
* @param {boolean} [options.fitToContent = false] - whether or not to resize itself automaitcally to fit to the text content
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor(
{
@ -87,11 +88,12 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
autoDraw,
autoLog,
fitToContent,
draggable,
boxFn
} = {},
)
{
super({ name, win, pos, anchor, size, units, ori, opacity, depth, clipMask, autoDraw, autoLog });
super({ name, win, pos, anchor, size, units, ori, opacity, depth, clipMask, autoDraw, autoLog, draggable });
this._addAttribute(
"text",

View File

@ -49,6 +49,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor(
{
@ -75,10 +76,11 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
clipMask,
autoDraw,
autoLog,
draggable
} = {},
)
{
super({ name, win, units, ori, opacity, depth, pos, anchor, clipMask, autoDraw, autoLog });
super({ name, win, units, ori, opacity, depth, pos, anchor, clipMask, autoDraw, autoLog, draggable });
// callback to deal with text metrics invalidation:
const onChange = (withPixi = false, withBoundingBox = false, withMetrics = false) =>

View File

@ -35,8 +35,9 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
* @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
* @param {boolean} [options.draggable= false] - whether or not to make stim draggable with mouse/touch/other pointer device
*/
constructor({ name, win, units, ori, opacity, depth, pos, anchor, size, clipMask, autoDraw, autoLog } = {})
constructor({ name, win, units, ori, opacity, depth, pos, anchor, size, clipMask, autoDraw, autoLog, draggable } = {})
{
super({ win, name, autoDraw, autoLog });
@ -84,6 +85,12 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
null,
this._onChange(false, false),
);
this._addAttribute("draggable", draggable, false);
// data needed to properly support drag and drop functionality
this._associatedPointerId = undefined;
this._initialPointerOffset = [0, 0];
this._pointerEventHandlersUuids = {};
// bounding box of the stimulus, in stimulus units
// note: boundingBox does not take the orientation into account
@ -96,6 +103,14 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
this._needPixiUpdate = true;
}
/**
* Whether or not stimuli is being dragged by pointer. Works in conjunction with draggable attribute.
*/
get isDragging()
{
return this._associatedPointerId !== undefined;
}
/**
* Force a refresh of the stimulus.
*
@ -179,15 +194,45 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
}
}
/**
* Setter for the draggable attribute.
*
* @name module:visual.VisualStim#setDraggable
* @public
* @param {boolean} [draggable=false] - whether or not to make stim draggable using mouse/touch/other pointer device
* @param {boolean} [log= false] - whether of not to log
*/
setDraggable(draggable = false, log = false)
{
const hasChanged = this._setAttribute("draggable", draggable, log);
if (hasChanged)
{
if (draggable)
{
this._pointerEventHandlersUuids[ "pointerdown" ] = this._win.on("pointerdown", this._handlePointerDown.bind(this));
this._pointerEventHandlersUuids[ "pointerup" ] = this._win.on("pointerup", this._handlePointerUp.bind(this));
this._pointerEventHandlersUuids[ "pointermove" ] = this._win.on("pointermove", this._handlePointerMove.bind(this));
}
else
{
this._win.off("pointerdown", this._pointerEventHandlersUuids[ "pointerdown" ]);
this._win.off("pointerup", this._pointerEventHandlersUuids[ "pointerup" ]);
this._win.off("pointermove", this._pointerEventHandlersUuids[ "pointermove" ]);
}
}
}
/**
* Setter for the depth attribute.
*
* @param {Array.<number>} depth - order in which stimuli is rendered, kind of css's z-index with a negative sign.
* @param {boolean} [log= false] - whether of not to log
*/
setDepth (depth = 0, log = false) {
setDepth(depth = 0, log = false)
{
this._setAttribute("depth", depth, log);
if (this._pixi) {
if (this._pixi)
{
this._pixi.zIndex = -this._depth;
}
}
@ -217,6 +262,93 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
return this._getBoundingBox_px().contains(objectPos_px[0], objectPos_px[1]);
}
/**
* Determine whether a point that is nown to have pixel dimensions is inside the bounding box of the stimulus.
*
* @name module:visual.VisualStim#containsPointPx
* @public
* @param {number[]} point_px - the point in pixels
* @return {boolean} whether or not the object is inside the bounding box of the stimulus
*/
containsPointPx (point_px)
{
return this._getBoundingBox_px().contains(point_px[0], point_px[1]);
}
/**
* Release the PIXI representation, if there is one.
*
* @name module:core.VisualStim#release
* @function
* @public
*
* @param {boolean} [log= false] - whether or not to log
*/
release(log = false)
{
this.draggable = false;
super.release(log);
}
/**
* Handler of pointerdown event.
*
* @name module:visual.VisualStim#_handlePointerDown
* @private
* @param {Object} e - pointerdown event data.
*/
_handlePointerDown (e)
{
if (e.pixi === undefined || e.pixi !== this._pixi)
{
return;
}
let relativePos = [];
let pixPos = util.to_unit(this._pos, this._units, this._win, "pix");
relativePos[0] = e.originalEvent.pageX - this._win.size[0] * 0.5 - this._pixi.parent.position.x;
relativePos[1] = -(e.originalEvent.pageY - this._win.size[1] * 0.5) - this._pixi.parent.position.y;
this._associatedPointerId = e.originalEvent.pointerId;
this._initialPointerOffset[0] = relativePos[0] - pixPos[0];
this._initialPointerOffset[1] = relativePos[1] - pixPos[1];
this.emit("pointerdown", e);
}
/**
* Handler of pointerup event.
*
* @name module:visual.VisualStim#_handlePointerUp
* @private
* @param {Object} e - pointerup event data.
*/
_handlePointerUp (e)
{
if (e.originalEvent.pointerId === this._associatedPointerId)
{
this._associatedPointerId = undefined;
this._initialPointerOffset.fill(0);
this.emit("pointerup", e);
}
}
/**
* Handler of pointermove event.
*
* @name module:visual.VisualStim#_handlePointerMove
* @private
* @param {Object} e - pointermove event data.
*/
_handlePointerMove (e)
{
if (e.originalEvent.pointerId === this._associatedPointerId)
{
let newPos = [];
newPos[ 0 ] = e.originalEvent.pageX - this._win.size[ 0 ] * 0.5 - this._pixi.parent.position.x - this._initialPointerOffset[ 0 ];
newPos[ 1 ] = -(e.originalEvent.pageY - this._win.size[ 1 ] * 0.5) - this._pixi.parent.position.y - this._initialPointerOffset[ 1 ];
this.setPos(util.to_unit(newPos, "pix", this._win, this._units));
this.emit("pointermove", e);
}
}
/**
* Setter for the anchor attribute.
*