soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
7.59 kB
const MathUtil = require('../util/math-util');
const { translateScreenPos } = require('../util/pos-math');
const roundToThreeDecimals = number => Math.round(number * 1000) / 1000;
class Mouse {
constructor (runtime) {
this._clientX = 0;
this._clientY = 0;
this._scratchX = 0;
this._scratchY = 0;
this._buttons = new Set();
this._isDown = false;
this.usesRightClickDown = false;
// pm: keep track of clicks
this._isClicked = false;
this._clickOnStep = -1;
/**
* Reference to the owning Runtime.
* Can be used, for example, to activate hats.
* @type{!Runtime}
*/
this.runtime = runtime;
this.cameraBound = null;
// after processing all blocks, we can check if this step is after the one we clicked on
this.runtime.on("RUNTIME_STEP_END", () => {
if (this.runtime.frameLoop._stepCounter > this._clickOnStep) {
this._isClicked = false;
}
});
}
bindToCamera(screen) {
this.cameraBound = screen;
}
removeCameraBinding() {
this.cameraBound = null;
}
/**
* Activate "event_whenthisspriteclicked" hats.
* @param {Target} target to trigger hats on.
* @private
*/
_activateClickHats (target) {
// Activate both "this sprite clicked" and "stage clicked"
// They were separated into two opcodes for labeling,
// but should act the same way.
// Intentionally not checking isStage to make it work when sharing blocks.
this.runtime.startHats('event_whenthisspriteclicked', null, target);
this.runtime.startHats('event_whenstageclicked', null, target);
if (target.isStage) {
this.runtime.startHats('pmEventsExpansion_whenSpriteClicked', { SPRITE: '_stage_' });
return;
}
if (target.sprite) {
this.runtime.startHats('pmEventsExpansion_whenSpriteClicked', { SPRITE: target.sprite.name });
}
}
/**
* Find a target by XY location
* @param {number} x X position to be sent to the renderer.
* @param {number} y Y position to be sent to the renderer.
* @return {Target} the target at that location
* @private
*/
_pickTarget (x, y) {
if (this.runtime.renderer) {
const drawableID = this.runtime.renderer.pick(x, y);
for (let i = 0; i < this.runtime.targets.length; i++) {
const target = this.runtime.targets[i];
if (target.hasOwnProperty('drawableID') &&
target.drawableID === drawableID) {
return target;
}
}
}
// Return the stage if no target was found
return this.runtime.getTargetForStage();
}
/**
* Mouse DOM event handler.
* @param {object} data Data from DOM event.
*/
postData (data) {
if (typeof data.x === 'number') {
this._clientX = data.x;
this._scratchX = MathUtil.clamp(
this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5),
-(this.runtime.stageWidth / 2),
(this.runtime.stageWidth / 2)
);
}
if (typeof data.y === 'number') {
this._clientY = data.y;
this._scratchY = MathUtil.clamp(
-this.runtime.stageHeight * ((data.y / data.canvasHeight) - 0.5),
-(this.runtime.stageHeight / 2),
(this.runtime.stageHeight / 2)
);
}
if (typeof data.isDown !== 'undefined') {
// If no button specified, default to left button for compatibility
const button = typeof data.button === 'undefined' ? 0 : data.button;
if (data.isDown) {
this._buttons.add(button);
} else {
this._buttons.delete(button);
}
const previousDownState = this._isDown;
this._isDown = data.isDown;
if (data.isDown) {
this._isClicked = true;
this._clickOnStep = this.runtime.frameLoop._stepCounter;
}
// Do not trigger if down state has not changed
if (previousDownState === this._isDown) return;
// Never trigger click hats at the end of a drag
if (data.wasDragged) return;
// Do not activate click hats for clicks outside canvas bounds
if (!(data.x > 0 && data.x < data.canvasWidth &&
data.y > 0 && data.y < data.canvasHeight)) return;
const target = this._pickTarget(data.x, data.y);
const isNewMouseDown = !previousDownState && this._isDown;
const isNewMouseUp = previousDownState && !this._isDown;
// Draggable targets start click hats on mouse up.
// Non-draggable targets start click hats on mouse down.
if (target.draggable && isNewMouseUp) {
this._activateClickHats(target);
} else if (!target.draggable && isNewMouseDown) {
this._activateClickHats(target);
}
}
}
/**
* Get the X position of the mouse in client coordinates.
* @return {number} Non-clamped X position of the mouse cursor.
*/
getClientX () {
return this._clientX;
}
/**
* Get the Y position of the mouse in client coordinates.
* @return {number} Non-clamped Y position of the mouse cursor.
*/
getClientY () {
return this._clientY;
}
/**
* Get the X position of the mouse in scratch coordinates.
* @return {number} Clamped and integer rounded X position of the mouse cursor.
*/
getScratchX () {
const mouseX = this.cameraBound
? translateScreenPos(this.runtime, this.cameraBound, this._scratchX, this._scratchY)[0]
// ? (this._scratchX * cameraState.scale) - cameraState.pos[0]
: this._scratchX;
if (this.runtime.runtimeOptions.miscLimits) {
return Math.round(mouseX);
}
return roundToThreeDecimals(mouseX);
}
/**
* Get the Y position of the mouse in scratch coordinates.
* @return {number} Clamped and integer rounded Y position of the mouse cursor.
*/
getScratchY () {
const mouseY = this.cameraBound
? translateScreenPos(this.runtime, this.cameraBound, this._scratchX, this._scratchY)[1]
// ? (this._scratchY * cameraState.scale) - cameraState.pos[1]
: this._scratchY;
if (this.runtime.runtimeOptions.miscLimits) {
return Math.round(mouseY);
}
return roundToThreeDecimals(mouseY);
}
/**
* Get the down state of the mouse.
* @return {boolean} Is the mouse down?
*/
getIsDown () {
return this._isDown;
}
/**
* pm: Get if the mouse was pressed down on this tick.
* @return {boolean} Is the mouse clicked?
*/
getIsClicked () {
return this._isClicked;
}
/**
* tw: Get the down state of a specific button of the mouse.
* @param {number} button The ID of the button. 0 = left, 1 = middle, 2 = right
* @return {boolean} Is the mouse button down?
*/
getButtonIsDown (button) {
if (button === 2) {
this.usesRightClickDown = true;
}
return this._buttons.has(button);
}
}
module.exports = Mouse;