summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory/components/tree-map/drag-zoom.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/memory/components/tree-map/drag-zoom.js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/memory/components/tree-map/drag-zoom.js')
-rw-r--r--devtools/client/memory/components/tree-map/drag-zoom.js337
1 files changed, 337 insertions, 0 deletions
diff --git a/devtools/client/memory/components/tree-map/drag-zoom.js b/devtools/client/memory/components/tree-map/drag-zoom.js
new file mode 100644
index 0000000000..034017e086
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/drag-zoom.js
@@ -0,0 +1,337 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { debounce } = require("resource://devtools/shared/debounce.js");
+const { lerp } = require("resource://devtools/client/memory/utils.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const LERP_SPEED = 0.5;
+const ZOOM_SPEED = 0.01;
+const TRANSLATE_EPSILON = 1;
+const ZOOM_EPSILON = 0.001;
+const LINE_SCROLL_MODE = 1;
+const SCROLL_LINE_SIZE = 15;
+
+/**
+ * DragZoom is a constructor that contains the state of the current dragging and
+ * zooming behavior. It sets the scrolling and zooming behaviors.
+ *
+ * @param {HTMLElement} container description
+ * The container for the canvases
+ */
+function DragZoom(container, debounceRate, requestAnimationFrame) {
+ EventEmitter.decorate(this);
+
+ this.isDragging = false;
+
+ // The current mouse position
+ this.mouseX = container.offsetWidth / 2;
+ this.mouseY = container.offsetHeight / 2;
+
+ // The total size of the visualization after being zoomed, in pixels
+ this.zoomedWidth = container.offsetWidth;
+ this.zoomedHeight = container.offsetHeight;
+
+ // How much the visualization has been zoomed in
+ this.zoom = 0;
+
+ // The offset of visualization from the container. This is applied after
+ // the zoom, and the visualization by default is centered
+ this.translateX = 0;
+ this.translateY = 0;
+
+ // The size of the offset between the top/left of the container, and the
+ // top/left of the containing element. This value takes into account
+ // the device pixel ratio for canvas draws.
+ this.offsetX = 0;
+ this.offsetY = 0;
+
+ // The smoothed values that are animated and eventually match the target
+ // values. The values are updated by the update loop
+ this.smoothZoom = 0;
+ this.smoothTranslateX = 0;
+ this.smoothTranslateY = 0;
+
+ // Add the constant values for testing purposes
+ this.ZOOM_SPEED = ZOOM_SPEED;
+ this.ZOOM_EPSILON = ZOOM_EPSILON;
+
+ const update = createUpdateLoop(container, this, requestAnimationFrame);
+
+ this.destroy = setHandlers(this, container, update, debounceRate);
+}
+
+module.exports = DragZoom;
+
+/**
+ * Returns an update loop. This loop smoothly updates the visualization when
+ * actions are performed. Once the animations have reached their target values
+ * the animation loop is stopped.
+ *
+ * Any value in the `dragZoom` object that starts with "smooth" is the
+ * smoothed version of a value that is interpolating toward the target value.
+ * For instance `dragZoom.smoothZoom` approaches `dragZoom.zoom` on each
+ * iteration of the update loop until it's sufficiently close as defined by
+ * the epsilon values.
+ *
+ * Only these smoothed values and the container CSS are updated by the loop.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ * The values that represent the current dragZoom state
+ * @param {Function} requestAnimationFrame
+ */
+function createUpdateLoop(container, dragZoom, requestAnimationFrame) {
+ let isLooping = false;
+
+ function update() {
+ const isScrollChanging =
+ Math.abs(dragZoom.smoothZoom - dragZoom.zoom) > ZOOM_EPSILON;
+ const isTranslateChanging =
+ Math.abs(dragZoom.smoothTranslateX - dragZoom.translateX) >
+ TRANSLATE_EPSILON ||
+ Math.abs(dragZoom.smoothTranslateY - dragZoom.translateY) >
+ TRANSLATE_EPSILON;
+
+ isLooping = isScrollChanging || isTranslateChanging;
+
+ if (isScrollChanging) {
+ dragZoom.smoothZoom = lerp(
+ dragZoom.smoothZoom,
+ dragZoom.zoom,
+ LERP_SPEED
+ );
+ } else {
+ dragZoom.smoothZoom = dragZoom.zoom;
+ }
+
+ if (isTranslateChanging) {
+ dragZoom.smoothTranslateX = lerp(
+ dragZoom.smoothTranslateX,
+ dragZoom.translateX,
+ LERP_SPEED
+ );
+ dragZoom.smoothTranslateY = lerp(
+ dragZoom.smoothTranslateY,
+ dragZoom.translateY,
+ LERP_SPEED
+ );
+ } else {
+ dragZoom.smoothTranslateX = dragZoom.translateX;
+ dragZoom.smoothTranslateY = dragZoom.translateY;
+ }
+
+ const zoom = 1 + dragZoom.smoothZoom;
+ const x = dragZoom.smoothTranslateX;
+ const y = dragZoom.smoothTranslateY;
+ container.style.transform = `translate(${x}px, ${y}px) scale(${zoom})`;
+
+ if (isLooping) {
+ requestAnimationFrame(update);
+ }
+ }
+
+ // Go ahead and start the update loop
+ update();
+
+ return function restartLoopingIfStopped() {
+ if (!isLooping) {
+ update();
+ }
+ };
+}
+
+/**
+ * Set the various event listeners and return a function to remove them
+ *
+ * @param {Object} dragZoom
+ * @param {HTMLElement} container
+ * @param {Function} update
+ * @return {Function} The function to remove the handlers
+ */
+function setHandlers(dragZoom, container, update, debounceRate) {
+ const emitChanged = debounce(() => dragZoom.emit("change"), debounceRate);
+
+ const removeDragHandlers = setDragHandlers(
+ container,
+ dragZoom,
+ emitChanged,
+ update
+ );
+ const removeScrollHandlers = setScrollHandlers(
+ container,
+ dragZoom,
+ emitChanged,
+ update
+ );
+
+ return function removeHandlers() {
+ removeDragHandlers();
+ removeScrollHandlers();
+ };
+}
+
+/**
+ * Sets handlers for when the user drags on the canvas. It will update dragZoom
+ * object with new translate and offset values.
+ *
+ * @param {HTMLElement} container
+ * @param {Object} dragZoom
+ * @param {Function} changed
+ * @param {Function} update
+ */
+function setDragHandlers(container, dragZoom, emitChanged, update) {
+ const parentEl = container.parentElement;
+
+ function startDrag() {
+ dragZoom.isDragging = true;
+ container.style.cursor = "grabbing";
+ }
+
+ function stopDrag() {
+ dragZoom.isDragging = false;
+ container.style.cursor = "grab";
+ }
+
+ function drag(event) {
+ const prevMouseX = dragZoom.mouseX;
+ const prevMouseY = dragZoom.mouseY;
+
+ dragZoom.mouseX = event.clientX - parentEl.offsetLeft;
+ dragZoom.mouseY = event.clientY - parentEl.offsetTop;
+
+ if (!dragZoom.isDragging) {
+ return;
+ }
+
+ dragZoom.translateX += dragZoom.mouseX - prevMouseX;
+ dragZoom.translateY += dragZoom.mouseY - prevMouseY;
+
+ keepInView(container, dragZoom);
+
+ emitChanged();
+ update();
+ }
+
+ parentEl.addEventListener("mousedown", startDrag);
+ parentEl.addEventListener("mouseup", stopDrag);
+ parentEl.addEventListener("mouseout", stopDrag);
+ parentEl.addEventListener("mousemove", drag);
+
+ return function removeListeners() {
+ parentEl.removeEventListener("mousedown", startDrag);
+ parentEl.removeEventListener("mouseup", stopDrag);
+ parentEl.removeEventListener("mouseout", stopDrag);
+ parentEl.removeEventListener("mousemove", drag);
+ };
+}
+
+/**
+ * Sets the handlers for when the user scrolls. It updates the dragZoom object
+ * and keeps the canvases all within the view. After changing values update
+ * loop is called, and the changed event is emitted.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ * @param {Function} changed
+ * @param {Function} update
+ */
+function setScrollHandlers(container, dragZoom, emitChanged, update) {
+ const window = container.ownerDocument.defaultView;
+
+ function handleWheel(event) {
+ event.preventDefault();
+
+ if (dragZoom.isDragging) {
+ return;
+ }
+
+ // Update the zoom level
+ const scrollDelta = getScrollDelta(event, window);
+ const prevZoom = dragZoom.zoom;
+ dragZoom.zoom = Math.max(0, dragZoom.zoom - scrollDelta * ZOOM_SPEED);
+
+ // Calculate the updated width and height
+ const prevZoomedWidth = container.offsetWidth * (1 + prevZoom);
+ const prevZoomedHeight = container.offsetHeight * (1 + prevZoom);
+ dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
+ dragZoom.zoomedHeight = container.offsetHeight * (1 + dragZoom.zoom);
+ const deltaWidth = dragZoom.zoomedWidth - prevZoomedWidth;
+ const deltaHeight = dragZoom.zoomedHeight - prevZoomedHeight;
+
+ const mouseOffsetX = dragZoom.mouseX - container.offsetWidth / 2;
+ const mouseOffsetY = dragZoom.mouseY - container.offsetHeight / 2;
+
+ // The ratio of where the center of the mouse is in regards to the total
+ // zoomed width/height
+ const ratioZoomX =
+ (prevZoomedWidth / 2 + mouseOffsetX - dragZoom.translateX) /
+ prevZoomedWidth;
+ const ratioZoomY =
+ (prevZoomedHeight / 2 + mouseOffsetY - dragZoom.translateY) /
+ prevZoomedHeight;
+
+ // Distribute the change in width and height based on the above ratio
+ dragZoom.translateX -= lerp(-deltaWidth / 2, deltaWidth / 2, ratioZoomX);
+ dragZoom.translateY -= lerp(-deltaHeight / 2, deltaHeight / 2, ratioZoomY);
+
+ // Keep the canvas in range of the container
+ keepInView(container, dragZoom);
+ emitChanged();
+ update();
+ }
+
+ container.addEventListener("wheel", handleWheel);
+
+ return function removeListener() {
+ container.removeEventListener("wheel", handleWheel);
+ };
+}
+
+/**
+ * Account for the various mouse wheel event types, per pixel or per line
+ *
+ * @param {WheelEvent} event
+ * @param {Window} window
+ * @return {Number} The scroll size in pixels
+ */
+function getScrollDelta(event, window) {
+ if (event.deltaMode === LINE_SCROLL_MODE) {
+ // Update by a fixed arbitrary value to normalize scroll types
+ return event.deltaY * SCROLL_LINE_SIZE;
+ }
+ return event.deltaY;
+}
+
+/**
+ * Keep the dragging and zooming within the view by updating the values in the
+ * `dragZoom` object.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ */
+function keepInView(container, dragZoom) {
+ const { devicePixelRatio } = container.ownerDocument.defaultView;
+ const overdrawX = (dragZoom.zoomedWidth - container.offsetWidth) / 2;
+ const overdrawY = (dragZoom.zoomedHeight - container.offsetHeight) / 2;
+
+ dragZoom.translateX = Math.max(
+ -overdrawX,
+ Math.min(overdrawX, dragZoom.translateX)
+ );
+ dragZoom.translateY = Math.max(
+ -overdrawY,
+ Math.min(overdrawY, dragZoom.translateY)
+ );
+
+ dragZoom.offsetX =
+ devicePixelRatio *
+ ((dragZoom.zoomedWidth - container.offsetWidth) / 2 - dragZoom.translateX);
+ dragZoom.offsetY =
+ devicePixelRatio *
+ ((dragZoom.zoomedHeight - container.offsetHeight) / 2 -
+ dragZoom.translateY);
+}