From cfce244ed1accb0a9d25a1902ad42d298869f2b1 Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Mon, 7 Jun 2021 14:10:41 -0700 Subject: [PATCH 1/3] Add additional logic for when to scroll the workspace --- .../scroll-options/src/ScrollBlockDragger.js | 308 +++++++++++++++--- 1 file changed, 259 insertions(+), 49 deletions(-) diff --git a/plugins/scroll-options/src/ScrollBlockDragger.js b/plugins/scroll-options/src/ScrollBlockDragger.js index 22e9d819f6..800d543ac0 100644 --- a/plugins/scroll-options/src/ScrollBlockDragger.js +++ b/plugins/scroll-options/src/ScrollBlockDragger.js @@ -12,6 +12,11 @@ import * as Blockly from 'blockly/core'; import {AutoScroll} from './AutoScroll'; +/** + * @typedef {!Object>} + */ +let CandidateScrolls; + /** * A block dragger that adds the functionality for a block to be moved while * someone is dragging it. @@ -34,7 +39,51 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { * @protected */ this.dragDelta_ = new Blockly.utils.Coordinate(0, 0); + + // TODO(maribethb): See if we can actually check this + /** + * Possible directions the workspace could be scrolled. + * @type {!Array} + * @protected + */ + this.scrollDirections_ = ['top', 'bottom', 'left', 'right']; + + /** + * Configuration options for the scroll-options settings. + * @type {!Object} + * @protected + */ + this.options_ = { + // Pixels per ms to scroll based on how far the block is from the edge of + // the viewport. + slowBlockSpeed: 0.28, + fastBlockSpeed: 1.4, + // Distance in workspace units that the edge of the block is from the edge + // of the viewport before the corresponding scroll speed will be used. Can + // be negative to start scrolling before the block extends over the edge. + slowBlockStartDistance: 0, + // Must be larger than slowBlockStartDistance. + fastBlockStartDistance: 50, + // If a block takes up this percentage of the viewport or more, it will be + // considered oversized. Rather than using the block edge, we use the + // mouse cursor plus the given margin size to activate block-based + // scrolling. + oversizeBlockThreshold: 0.85, + // A bigger value will cause the workspace to scroll sooner, i.e., the + // mouse can be further inward from the edge when scrolling begins. + oversizeBlockMargin: 15, + // Pixels per ms to scroll based on how far the mouse is from the edge of + // the viewport. + slowMouseSpeed: 0.5, + fastMouseSpeed: 1.6, + // Distance in workspace units that the mouse is from the edge of the + // viewport before the corresponding scroll speed will be used. Can be + // negative to start scrolling before the mouse extends over the edge. + slowMouseStartDistance: 0, + fastMouseStartDistance: 35, + }; } + /** * Updates the location of the block that is being dragged. * @param {number} deltaX Horizontal offset in pixel units. @@ -86,11 +135,7 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { super.drag(e, totalDelta); this.dragDelta_ = currentDragDeltaXY; - // Calculate the location the block is being dragged to, in ws units. - // This same calculation is done in super.drag(). - const deltaPx = this.pixelsToWorkspaceUnits_(totalDelta); - const newLoc = Blockly.utils.Coordinate.sum(this.startXY_, deltaPx); - this.scrollWorkspaceWhileDragging(newLoc); + this.scrollWorkspaceWhileDragging_(e); } /** @@ -99,7 +144,7 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { * @override */ endDrag(e, currentDragDeltaXY) { - // We can not override this method similar to the others because we call + // We cannot override this method similar to the others because we call // drag here with the passed in value. // Make sure internal state is fresh. this.drag(e, currentDragDeltaXY); @@ -140,61 +185,51 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { * The workspace will not resize as the block is dragged. The workspace should * appear to move out from under the block, i.e., the block should stay under * the user's mouse. - * @param {!Blockly.utils.Coordinate} newLoc New coordinate the block is being - * dragged to. + * @param {!Event} e The mouse/touch event for the drag. + * @protected */ - scrollWorkspaceWhileDragging(newLoc) { - const SCROLL_DIRECTION_VECTORS = { + scrollWorkspaceWhileDragging_(e) { + /** + * Unit vector for each direction that could be scrolled. This vector will + * be scaled to get the calculated velocity in each direction. Must be a + * dict because the properties are accessed based on the members of + * `this.scrollDirections_`. + * @dict + * @private + */ + this.SCROLL_DIRECTION_VECTORS_ = { top: new Blockly.utils.Coordinate(0, 1), bottom: new Blockly.utils.Coordinate(0, -1), left: new Blockly.utils.Coordinate(1, 0), right: new Blockly.utils.Coordinate(-1, 0), }; - // TODO(maribethb): I just made this up, pick a better one - // and make this configurable. - const SCROLL_SPEED = 0.4; + const mouse = Blockly.utils.screenToWsCoordinates( + this.workspace_, new Blockly.utils.Coordinate(e.clientX, e.clientY)); - const candidateScrolls = []; - let overallScrollVector = new Blockly.utils.Coordinate(0, 0); + /** + * List of possible scrolls in each direction. This will be modified in + * place. Must be a dict because the properties are accessed based on the + * members of `this.scrollDirections_`. + * @dict + * @type {!CandidateScrolls} + */ + const candidateScrolls = { + top: [], + bottom: [], + left: [], + right: [], + }; // Get ViewMetrics in workspace coordinates. const metrics = this.workspace_.getMetricsManager().getViewMetrics(true); - // TODO(maribethb): Add fancier logic based on how far out of bounds the - // block is held. - - // See Blockly.MetricsManager for more information on the metrics used. - // In particular, it uses workspace coordinates where the top and left - // of the workspace are negative. - // More than one scroll vector may apply, for example if the block is - // dragged to a corner. - if (newLoc.y < metrics.top) { - const scrollVector = SCROLL_DIRECTION_VECTORS['top'].scale(SCROLL_SPEED); - candidateScrolls.push(scrollVector); - } - if (newLoc.y > metrics.top + metrics.height) { - const scrollVector = - SCROLL_DIRECTION_VECTORS['bottom'].scale(SCROLL_SPEED); - candidateScrolls.push(scrollVector); - } - if (newLoc.x < metrics.left) { - const scrollVector = SCROLL_DIRECTION_VECTORS['left'].scale(SCROLL_SPEED); - candidateScrolls.push(scrollVector); - } - if (newLoc.x > metrics.left + metrics.width) { - const scrollVector = - SCROLL_DIRECTION_VECTORS['right'].scale(SCROLL_SPEED); - candidateScrolls.push(scrollVector); - } + // Get possible scroll velocities based on the location of both the block + // and the mouse. - // Get the overall scroll direction vector (could scroll diagonally). - // Note: code.org reduces down to just one vector per direction from - // all the possible ones they generate. Currently we just have one per - // direction so we don't need to do anything else. - candidateScrolls.forEach(function(scroll) { - overallScrollVector = - Blockly.utils.Coordinate.sum(overallScrollVector, scroll); - }); + this.getBlockCandidateScrolls_(candidateScrolls, metrics, mouse); + this.getMouseCandidateScrolls_(candidateScrolls, metrics, mouse); + // Calculate the final scroll vector we should actually scroll to. + const overallScrollVector = this.getOverallScrollVector_(candidateScrolls); // If the workspace should not be scrolled any longer, cancel the // autoscroll. @@ -204,11 +239,186 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { return; } + // Update the autoscroll or start a new one. this.activeAutoScroll_ = this.activeAutoScroll_ || new AutoScroll(this.workspace_); this.activeAutoScroll_.updateProperties(overallScrollVector); } + /** + * There could be multiple candidate scrolls for each direction, such as one + * for block position and one for mouse position. We should first find the + * fastest scroll in each direction. Then, we sum those to find the overall + * scroll vector. + * + * For example, we may have a fast block scroll and a slow + * mouse scroll candidate in both the top and left directions. First, we + * reduce to only the fast block scroll. Then, we sum the vectors in each + * direction to get a resulting fast scroll in a diagonal direction to the top + * left. + * @param {!CandidateScrolls} candidateScrolls Existing lists of candidate + * scrolls. Will be modified in place. + * @return {!Blockly.utils.Coordinate} Overall scroll vector. + * @protected + */ + getOverallScrollVector_(candidateScrolls) { + let overallScrollVector = new Blockly.utils.Coordinate(0, 0); + for (const direction of this.scrollDirections_) { + const scroll = candidateScrolls[direction].reduce((fastest, current) => { + if (!fastest) { + return current; + } + return Blockly.utils.Coordinate.magnitude(fastest) > + Blockly.utils.Coordinate.magnitude(current) ? + fastest : + current; + }, new Blockly.utils.Coordinate(0, 0)); // Initial value + overallScrollVector = + Blockly.utils.Coordinate.sum(overallScrollVector, scroll); + } + return overallScrollVector; + } + + /** + * Gets the candidate scrolls based on the position of the block on the + * workspace. If the block is near/over the edge, a candidate scroll will be + * added based on the options provided. + * + * This method can be overridden to further customize behavior, e.g. To add a + * third speed option. + * @param {!CandidateScrolls} candidateScrolls Existing list of candidate + * scrolls. Will be modified in place. + * @param {!Blockly.MetricsManager.ContainerRegion} metrics View metrics for + * the workspace. + * @param {!Blockly.utils.Coordinate} mouse Mouse coordinates. + * @protected + */ + getBlockCandidateScrolls_(candidateScrolls, metrics, mouse) { + const blockOverflows = this.getBlockBoundsOverflows_(metrics, mouse); + for (const [direction, overflow] of Object.entries(blockOverflows)) { + if (overflow > this.options_.slowBlockStartDistance) { + const speed = overflow > this.options_.fastBlockStartDistance ? + this.options_.fastBlockSpeed : + this.options_.slowBlockSpeed; + const scrollVector = + this.SCROLL_DIRECTION_VECTORS_[direction].scale(speed); + candidateScrolls[direction].push(scrollVector); + } + } + } + + /** + * Gets the candidate scrolls based on the position of the mouse cursor + * relative to the workspace. If the mouse is near/over the edge, a candidate + * scroll will be added based on the options provided. + * + * This method can be overridden to further customize behavior, e.g. To add a + * third speed option. + * @param {!CandidateScrolls} candidateScrolls Existing list of candidate + * scrolls. Will be modified in place. + * @param {!Blockly.MetricsManager.ContainerRegion} metrics View metrics for + * the workspace. + * @param {!Blockly.utils.Coordinate} mouse Mouse coordinates. + * @protected + */ + getMouseCandidateScrolls_(candidateScrolls, metrics, mouse) { + const mouseOverflows = this.getMouseOverflows_(metrics, mouse); + for (const direction of this.scrollDirections_) { + const overflow = mouseOverflows[direction]; + if (overflow > this.options_.slowMouseStartDistance) { + const speed = overflow > this.options_.fastMouseStartDistance ? + this.options_.fastMouseSpeed : + this.options_.slowMouseSpeed; + const scrollVector = + this.SCROLL_DIRECTION_VECTORS_[direction].scale(speed); + candidateScrolls[direction].push(scrollVector); + } + } + } + + /** + * Gets the amount of overflow of a box relative to the workspace viewport. + * + * The value for each direction will be how far the given block edge is from + * the given edge of the viewport. If the block edge is outside the viewport, + * the value will be positive. If the block edge is inside the viewport, the + * value will be negative. + * + * This method also checks for oversized blocks. If the block is very large + * relative to the viewport size, then we will actually use a small zone + * around the cursor, rather than the edge of the block, to calculate the + * overflow values. This calculation is done independently in both the + * horizontal and vertical directions. These values can be configured in the + * options for the plugin. + * + * @param {!Blockly.MetricsManager.ContainerRegion} metrics View metrics for + * the workspace. + * @param {!Blockly.utils.Coordinate} mouse Mouse coordinates. + * @return {!Object} An object describing the amount of + * overflow in each direction. + * @protected + */ + getBlockBoundsOverflows_(metrics, mouse) { + const blockBounds = this.draggingBlock_.getBoundingRectangle(); + + // Handle large blocks. If the block is nearly as tall as the viewport, + // use a margin around the cursor rather than the height of the block. + const blockHeight = blockBounds.bottom - blockBounds.top; + if (blockHeight > metrics.height * this.options_.oversizeBlockThreshold) { + blockBounds.top = Math.max( + blockBounds.top, mouse.y - this.options_.oversizeBlockMargin); + blockBounds.bottom = Math.min( + blockBounds.bottom, mouse.y + this.options_.oversizeBlockMargin); + } + + // Same logic, but for block width. + const blockWidth = blockBounds.right - blockBounds.left; + if (blockWidth > metrics.width * this.options_.oversizeBlockThreshold) { + blockBounds.left = Math.max( + blockBounds.left, mouse.x - this.options_.oversizeBlockMargin); + blockBounds.right = Math.min( + blockBounds.right, mouse.x + this.options_.oversizeBlockMargin); + } + + // The coordinate system is negative in the top and left directions, and + // positive in the bottom and right directions. Therefore, the direction of + // the comparison must be switched for bottom and right. + return { + top: metrics.top - blockBounds.top, + bottom: -(metrics.top + metrics.height - blockBounds.bottom), + left: metrics.left - blockBounds.left, + right: -(metrics.left + metrics.width - blockBounds.right), + }; + } + + /** + * Gets the amount of overflow of the mouse coordinates relative to the + * viewport. + * + * The value for each direction will be how far the pointer is from + * the given edge of the viewport. If the pointer is outside the viewport, + * the value will be positive. If the pointer is inside the viewport, the + * value will be negative. + * + * @param {!Blockly.MetricsManager.ContainerRegion} metrics View metrics for + * the workspace. + * @param {!Blockly.utils.Coordinate} mouse Mouse coordinates. + * @return {!Object} An object describing the amount of + * overflow in each direction. + * @protected + */ + getMouseOverflows_(metrics, mouse) { + // The coordinate system is negative in the top and left directions, and + // positive in the bottom and right directions. Therefore, the direction of + // the comparison must be switched for bottom and right. + return { + top: metrics.top - mouse.y, + bottom: -(metrics.top + metrics.height - mouse.y), + left: metrics.left - mouse.x, + right: -(metrics.left + metrics.width - mouse.x), + }; + } + /** * Cancel any AutoScroll. This must be called when there is no need to scroll * further, e.g., when no longer dragging near the edge of the workspace, or From ba2cfc8dc75ae637a1765a852f97bbe06828cd14 Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Mon, 7 Jun 2021 14:19:35 -0700 Subject: [PATCH 2/3] small fixes --- plugins/scroll-options/src/ScrollBlockDragger.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/scroll-options/src/ScrollBlockDragger.js b/plugins/scroll-options/src/ScrollBlockDragger.js index 800d543ac0..252a6871ea 100644 --- a/plugins/scroll-options/src/ScrollBlockDragger.js +++ b/plugins/scroll-options/src/ScrollBlockDragger.js @@ -228,7 +228,7 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { this.getBlockCandidateScrolls_(candidateScrolls, metrics, mouse); this.getMouseCandidateScrolls_(candidateScrolls, metrics, mouse); - // Calculate the final scroll vector we should actually scroll to. + // Calculate the final scroll vector we should actually use. const overallScrollVector = this.getOverallScrollVector_(candidateScrolls); // If the workspace should not be scrolled any longer, cancel the @@ -295,7 +295,8 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { */ getBlockCandidateScrolls_(candidateScrolls, metrics, mouse) { const blockOverflows = this.getBlockBoundsOverflows_(metrics, mouse); - for (const [direction, overflow] of Object.entries(blockOverflows)) { + for (const direction of this.scrollDirections_) { + const overflow = blockOverflows[direction]; if (overflow > this.options_.slowBlockStartDistance) { const speed = overflow > this.options_.fastBlockStartDistance ? this.options_.fastBlockSpeed : From c9c95b318ab7bb107d4ef2ffb473a50eaf56940a Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Wed, 9 Jun 2021 19:49:03 -0700 Subject: [PATCH 3/3] Use better names --- .../scroll-options/src/ScrollBlockDragger.js | 79 ++++++++++--------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/plugins/scroll-options/src/ScrollBlockDragger.js b/plugins/scroll-options/src/ScrollBlockDragger.js index 252a6871ea..6cc01233a9 100644 --- a/plugins/scroll-options/src/ScrollBlockDragger.js +++ b/plugins/scroll-options/src/ScrollBlockDragger.js @@ -40,7 +40,7 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { */ this.dragDelta_ = new Blockly.utils.Coordinate(0, 0); - // TODO(maribethb): See if we can actually check this + // TODO(maribethb): Use `isMoveable` etc. to get this list /** * Possible directions the workspace could be scrolled. * @type {!Array} @@ -221,13 +221,14 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { }; // Get ViewMetrics in workspace coordinates. - const metrics = this.workspace_.getMetricsManager().getViewMetrics(true); + const viewMetrics = + this.workspace_.getMetricsManager().getViewMetrics(true); // Get possible scroll velocities based on the location of both the block // and the mouse. - this.getBlockCandidateScrolls_(candidateScrolls, metrics, mouse); - this.getMouseCandidateScrolls_(candidateScrolls, metrics, mouse); + this.computeBlockCandidateScrolls_(candidateScrolls, viewMetrics, mouse); + this.computeMouseCandidateScrolls_(candidateScrolls, viewMetrics, mouse); // Calculate the final scroll vector we should actually use. const overallScrollVector = this.getOverallScrollVector_(candidateScrolls); @@ -264,17 +265,18 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { getOverallScrollVector_(candidateScrolls) { let overallScrollVector = new Blockly.utils.Coordinate(0, 0); for (const direction of this.scrollDirections_) { - const scroll = candidateScrolls[direction].reduce((fastest, current) => { - if (!fastest) { - return current; - } - return Blockly.utils.Coordinate.magnitude(fastest) > - Blockly.utils.Coordinate.magnitude(current) ? - fastest : - current; - }, new Blockly.utils.Coordinate(0, 0)); // Initial value + const fastestScroll = + candidateScrolls[direction].reduce((fastest, current) => { + if (!fastest) { + return current; + } + return Blockly.utils.Coordinate.magnitude(fastest) > + Blockly.utils.Coordinate.magnitude(current) ? + fastest : + current; + }, new Blockly.utils.Coordinate(0, 0)); // Initial value overallScrollVector = - Blockly.utils.Coordinate.sum(overallScrollVector, scroll); + Blockly.utils.Coordinate.sum(overallScrollVector, fastestScroll); } return overallScrollVector; } @@ -288,13 +290,13 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { * third speed option. * @param {!CandidateScrolls} candidateScrolls Existing list of candidate * scrolls. Will be modified in place. - * @param {!Blockly.MetricsManager.ContainerRegion} metrics View metrics for - * the workspace. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics + * for the workspace. * @param {!Blockly.utils.Coordinate} mouse Mouse coordinates. * @protected */ - getBlockCandidateScrolls_(candidateScrolls, metrics, mouse) { - const blockOverflows = this.getBlockBoundsOverflows_(metrics, mouse); + computeBlockCandidateScrolls_(candidateScrolls, viewMetrics, mouse) { + const blockOverflows = this.getBlockBoundsOverflows_(viewMetrics, mouse); for (const direction of this.scrollDirections_) { const overflow = blockOverflows[direction]; if (overflow > this.options_.slowBlockStartDistance) { @@ -317,13 +319,13 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { * third speed option. * @param {!CandidateScrolls} candidateScrolls Existing list of candidate * scrolls. Will be modified in place. - * @param {!Blockly.MetricsManager.ContainerRegion} metrics View metrics for - * the workspace. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics + * for the workspace. * @param {!Blockly.utils.Coordinate} mouse Mouse coordinates. * @protected */ - getMouseCandidateScrolls_(candidateScrolls, metrics, mouse) { - const mouseOverflows = this.getMouseOverflows_(metrics, mouse); + computeMouseCandidateScrolls_(candidateScrolls, viewMetrics, mouse) { + const mouseOverflows = this.getMouseOverflows_(viewMetrics, mouse); for (const direction of this.scrollDirections_) { const overflow = mouseOverflows[direction]; if (overflow > this.options_.slowMouseStartDistance) { @@ -352,20 +354,21 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { * horizontal and vertical directions. These values can be configured in the * options for the plugin. * - * @param {!Blockly.MetricsManager.ContainerRegion} metrics View metrics for - * the workspace. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics + * for the workspace. * @param {!Blockly.utils.Coordinate} mouse Mouse coordinates. * @return {!Object} An object describing the amount of * overflow in each direction. * @protected */ - getBlockBoundsOverflows_(metrics, mouse) { + getBlockBoundsOverflows_(viewMetrics, mouse) { const blockBounds = this.draggingBlock_.getBoundingRectangle(); // Handle large blocks. If the block is nearly as tall as the viewport, // use a margin around the cursor rather than the height of the block. const blockHeight = blockBounds.bottom - blockBounds.top; - if (blockHeight > metrics.height * this.options_.oversizeBlockThreshold) { + if (blockHeight > + viewMetrics.height * this.options_.oversizeBlockThreshold) { blockBounds.top = Math.max( blockBounds.top, mouse.y - this.options_.oversizeBlockMargin); blockBounds.bottom = Math.min( @@ -374,7 +377,7 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { // Same logic, but for block width. const blockWidth = blockBounds.right - blockBounds.left; - if (blockWidth > metrics.width * this.options_.oversizeBlockThreshold) { + if (blockWidth > viewMetrics.width * this.options_.oversizeBlockThreshold) { blockBounds.left = Math.max( blockBounds.left, mouse.x - this.options_.oversizeBlockMargin); blockBounds.right = Math.min( @@ -385,10 +388,10 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { // positive in the bottom and right directions. Therefore, the direction of // the comparison must be switched for bottom and right. return { - top: metrics.top - blockBounds.top, - bottom: -(metrics.top + metrics.height - blockBounds.bottom), - left: metrics.left - blockBounds.left, - right: -(metrics.left + metrics.width - blockBounds.right), + top: viewMetrics.top - blockBounds.top, + bottom: -(viewMetrics.top + viewMetrics.height - blockBounds.bottom), + left: viewMetrics.left - blockBounds.left, + right: -(viewMetrics.left + viewMetrics.width - blockBounds.right), }; } @@ -401,22 +404,22 @@ export class ScrollBlockDragger extends Blockly.BlockDragger { * the value will be positive. If the pointer is inside the viewport, the * value will be negative. * - * @param {!Blockly.MetricsManager.ContainerRegion} metrics View metrics for - * the workspace. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics + * for the workspace. * @param {!Blockly.utils.Coordinate} mouse Mouse coordinates. * @return {!Object} An object describing the amount of * overflow in each direction. * @protected */ - getMouseOverflows_(metrics, mouse) { + getMouseOverflows_(viewMetrics, mouse) { // The coordinate system is negative in the top and left directions, and // positive in the bottom and right directions. Therefore, the direction of // the comparison must be switched for bottom and right. return { - top: metrics.top - mouse.y, - bottom: -(metrics.top + metrics.height - mouse.y), - left: metrics.left - mouse.x, - right: -(metrics.left + metrics.width - mouse.x), + top: viewMetrics.top - mouse.y, + bottom: -(viewMetrics.top + viewMetrics.height - mouse.y), + left: viewMetrics.left - mouse.x, + right: -(viewMetrics.left + viewMetrics.width - mouse.x), }; }