tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

drag-zoom.js (10298B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const { debounce } = require("resource://devtools/shared/debounce.js");
      8 const { lerp } = require("resource://devtools/client/memory/utils.js");
      9 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     10 
     11 const LERP_SPEED = 0.5;
     12 const ZOOM_SPEED = 0.01;
     13 const TRANSLATE_EPSILON = 1;
     14 const ZOOM_EPSILON = 0.001;
     15 const LINE_SCROLL_MODE = 1;
     16 const SCROLL_LINE_SIZE = 15;
     17 
     18 /**
     19 * DragZoom is a constructor that contains the state of the current dragging and
     20 * zooming behavior. It sets the scrolling and zooming behaviors.
     21 *
     22 * @param  {HTMLElement} container description
     23 *         The container for the canvases
     24 */
     25 function DragZoom(container, debounceRate, requestAnimationFrame) {
     26  EventEmitter.decorate(this);
     27 
     28  this.isDragging = false;
     29 
     30  // The current mouse position
     31  this.mouseX = container.offsetWidth / 2;
     32  this.mouseY = container.offsetHeight / 2;
     33 
     34  // The total size of the visualization after being zoomed, in pixels
     35  this.zoomedWidth = container.offsetWidth;
     36  this.zoomedHeight = container.offsetHeight;
     37 
     38  // How much the visualization has been zoomed in
     39  this.zoom = 0;
     40 
     41  // The offset of visualization from the container. This is applied after
     42  // the zoom, and the visualization by default is centered
     43  this.translateX = 0;
     44  this.translateY = 0;
     45 
     46  // The size of the offset between the top/left of the container, and the
     47  // top/left of the containing element. This value takes into account
     48  // the device pixel ratio for canvas draws.
     49  this.offsetX = 0;
     50  this.offsetY = 0;
     51 
     52  // The smoothed values that are animated and eventually match the target
     53  // values. The values are updated by the update loop
     54  this.smoothZoom = 0;
     55  this.smoothTranslateX = 0;
     56  this.smoothTranslateY = 0;
     57 
     58  // Add the constant values for testing purposes
     59  this.ZOOM_SPEED = ZOOM_SPEED;
     60  this.ZOOM_EPSILON = ZOOM_EPSILON;
     61 
     62  const update = createUpdateLoop(container, this, requestAnimationFrame);
     63 
     64  this.destroy = setHandlers(this, container, update, debounceRate);
     65 }
     66 
     67 module.exports = DragZoom;
     68 
     69 /**
     70 * Returns an update loop. This loop smoothly updates the visualization when
     71 * actions are performed. Once the animations have reached their target values
     72 * the animation loop is stopped.
     73 *
     74 * Any value in the `dragZoom` object that starts with "smooth" is the
     75 * smoothed version of a value that is interpolating toward the target value.
     76 * For instance `dragZoom.smoothZoom` approaches `dragZoom.zoom` on each
     77 * iteration of the update loop until it's sufficiently close as defined by
     78 * the epsilon values.
     79 *
     80 * Only these smoothed values and the container CSS are updated by the loop.
     81 *
     82 * @param {HTMLDivElement} container
     83 * @param {object} dragZoom
     84 *        The values that represent the current dragZoom state
     85 * @param {Function} requestAnimationFrame
     86 */
     87 function createUpdateLoop(container, dragZoom, requestAnimationFrame) {
     88  let isLooping = false;
     89 
     90  function update() {
     91    const isScrollChanging =
     92      Math.abs(dragZoom.smoothZoom - dragZoom.zoom) > ZOOM_EPSILON;
     93    const isTranslateChanging =
     94      Math.abs(dragZoom.smoothTranslateX - dragZoom.translateX) >
     95        TRANSLATE_EPSILON ||
     96      Math.abs(dragZoom.smoothTranslateY - dragZoom.translateY) >
     97        TRANSLATE_EPSILON;
     98 
     99    isLooping = isScrollChanging || isTranslateChanging;
    100 
    101    if (isScrollChanging) {
    102      dragZoom.smoothZoom = lerp(
    103        dragZoom.smoothZoom,
    104        dragZoom.zoom,
    105        LERP_SPEED
    106      );
    107    } else {
    108      dragZoom.smoothZoom = dragZoom.zoom;
    109    }
    110 
    111    if (isTranslateChanging) {
    112      dragZoom.smoothTranslateX = lerp(
    113        dragZoom.smoothTranslateX,
    114        dragZoom.translateX,
    115        LERP_SPEED
    116      );
    117      dragZoom.smoothTranslateY = lerp(
    118        dragZoom.smoothTranslateY,
    119        dragZoom.translateY,
    120        LERP_SPEED
    121      );
    122    } else {
    123      dragZoom.smoothTranslateX = dragZoom.translateX;
    124      dragZoom.smoothTranslateY = dragZoom.translateY;
    125    }
    126 
    127    const zoom = 1 + dragZoom.smoothZoom;
    128    const x = dragZoom.smoothTranslateX;
    129    const y = dragZoom.smoothTranslateY;
    130    container.style.transform = `translate(${x}px, ${y}px) scale(${zoom})`;
    131 
    132    if (isLooping) {
    133      requestAnimationFrame(update);
    134    }
    135  }
    136 
    137  // Go ahead and start the update loop
    138  update();
    139 
    140  return function restartLoopingIfStopped() {
    141    if (!isLooping) {
    142      update();
    143    }
    144  };
    145 }
    146 
    147 /**
    148 * Set the various event listeners and return a function to remove them
    149 *
    150 * @param  {object} dragZoom
    151 * @param  {HTMLElement} container
    152 * @param  {Function} update
    153 * @return {Function}  The function to remove the handlers
    154 */
    155 function setHandlers(dragZoom, container, update, debounceRate) {
    156  const emitChanged = debounce(() => dragZoom.emit("change"), debounceRate);
    157 
    158  const removeDragHandlers = setDragHandlers(
    159    container,
    160    dragZoom,
    161    emitChanged,
    162    update
    163  );
    164  const removeScrollHandlers = setScrollHandlers(
    165    container,
    166    dragZoom,
    167    emitChanged,
    168    update
    169  );
    170 
    171  return function removeHandlers() {
    172    removeDragHandlers();
    173    removeScrollHandlers();
    174  };
    175 }
    176 
    177 /**
    178 * Sets handlers for when the user drags on the canvas. It will update dragZoom
    179 * object with new translate and offset values.
    180 *
    181 * @param  {HTMLElement} container
    182 * @param  {object} dragZoom
    183 * @param  {Function} changed
    184 * @param  {Function} update
    185 */
    186 function setDragHandlers(container, dragZoom, emitChanged, update) {
    187  const parentEl = container.parentElement;
    188 
    189  function startDrag() {
    190    dragZoom.isDragging = true;
    191    container.style.cursor = "grabbing";
    192  }
    193 
    194  function stopDrag() {
    195    dragZoom.isDragging = false;
    196    container.style.cursor = "grab";
    197  }
    198 
    199  function drag(event) {
    200    const prevMouseX = dragZoom.mouseX;
    201    const prevMouseY = dragZoom.mouseY;
    202 
    203    dragZoom.mouseX = event.clientX - parentEl.offsetLeft;
    204    dragZoom.mouseY = event.clientY - parentEl.offsetTop;
    205 
    206    if (!dragZoom.isDragging) {
    207      return;
    208    }
    209 
    210    dragZoom.translateX += dragZoom.mouseX - prevMouseX;
    211    dragZoom.translateY += dragZoom.mouseY - prevMouseY;
    212 
    213    keepInView(container, dragZoom);
    214 
    215    emitChanged();
    216    update();
    217  }
    218 
    219  parentEl.addEventListener("mousedown", startDrag);
    220  parentEl.addEventListener("mouseup", stopDrag);
    221  parentEl.addEventListener("mouseout", stopDrag);
    222  parentEl.addEventListener("mousemove", drag);
    223 
    224  return function removeListeners() {
    225    parentEl.removeEventListener("mousedown", startDrag);
    226    parentEl.removeEventListener("mouseup", stopDrag);
    227    parentEl.removeEventListener("mouseout", stopDrag);
    228    parentEl.removeEventListener("mousemove", drag);
    229  };
    230 }
    231 
    232 /**
    233 * Sets the handlers for when the user scrolls. It updates the dragZoom object
    234 * and keeps the canvases all within the view. After changing values update
    235 * loop is called, and the changed event is emitted.
    236 *
    237 * @param  {HTMLDivElement} container
    238 * @param  {object} dragZoom
    239 * @param  {Function} changed
    240 * @param  {Function} update
    241 */
    242 function setScrollHandlers(container, dragZoom, emitChanged, update) {
    243  const window = container.ownerDocument.defaultView;
    244 
    245  function handleWheel(event) {
    246    event.preventDefault();
    247 
    248    if (dragZoom.isDragging) {
    249      return;
    250    }
    251 
    252    // Update the zoom level
    253    const scrollDelta = getScrollDelta(event, window);
    254    const prevZoom = dragZoom.zoom;
    255    dragZoom.zoom = Math.max(0, dragZoom.zoom - scrollDelta * ZOOM_SPEED);
    256 
    257    // Calculate the updated width and height
    258    const prevZoomedWidth = container.offsetWidth * (1 + prevZoom);
    259    const prevZoomedHeight = container.offsetHeight * (1 + prevZoom);
    260    dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
    261    dragZoom.zoomedHeight = container.offsetHeight * (1 + dragZoom.zoom);
    262    const deltaWidth = dragZoom.zoomedWidth - prevZoomedWidth;
    263    const deltaHeight = dragZoom.zoomedHeight - prevZoomedHeight;
    264 
    265    const mouseOffsetX = dragZoom.mouseX - container.offsetWidth / 2;
    266    const mouseOffsetY = dragZoom.mouseY - container.offsetHeight / 2;
    267 
    268    // The ratio of where the center of the mouse is in regards to the total
    269    // zoomed width/height
    270    const ratioZoomX =
    271      (prevZoomedWidth / 2 + mouseOffsetX - dragZoom.translateX) /
    272      prevZoomedWidth;
    273    const ratioZoomY =
    274      (prevZoomedHeight / 2 + mouseOffsetY - dragZoom.translateY) /
    275      prevZoomedHeight;
    276 
    277    // Distribute the change in width and height based on the above ratio
    278    dragZoom.translateX -= lerp(-deltaWidth / 2, deltaWidth / 2, ratioZoomX);
    279    dragZoom.translateY -= lerp(-deltaHeight / 2, deltaHeight / 2, ratioZoomY);
    280 
    281    // Keep the canvas in range of the container
    282    keepInView(container, dragZoom);
    283    emitChanged();
    284    update();
    285  }
    286 
    287  container.addEventListener("wheel", handleWheel);
    288 
    289  return function removeListener() {
    290    container.removeEventListener("wheel", handleWheel);
    291  };
    292 }
    293 
    294 /**
    295 * Account for the various mouse wheel event types, per pixel or per line
    296 *
    297 * @param  {WheelEvent} event
    298 * @return {number} The scroll size in pixels
    299 */
    300 function getScrollDelta(event) {
    301  if (event.deltaMode === LINE_SCROLL_MODE) {
    302    // Update by a fixed arbitrary value to normalize scroll types
    303    return event.deltaY * SCROLL_LINE_SIZE;
    304  }
    305  return event.deltaY;
    306 }
    307 
    308 /**
    309 * Keep the dragging and zooming within the view by updating the values in the
    310 * `dragZoom` object.
    311 *
    312 * @param  {HTMLDivElement} container
    313 * @param  {object} dragZoom
    314 */
    315 function keepInView(container, dragZoom) {
    316  const { devicePixelRatio } = container.ownerDocument.defaultView;
    317  const overdrawX = (dragZoom.zoomedWidth - container.offsetWidth) / 2;
    318  const overdrawY = (dragZoom.zoomedHeight - container.offsetHeight) / 2;
    319 
    320  dragZoom.translateX = Math.max(
    321    -overdrawX,
    322    Math.min(overdrawX, dragZoom.translateX)
    323  );
    324  dragZoom.translateY = Math.max(
    325    -overdrawY,
    326    Math.min(overdrawY, dragZoom.translateY)
    327  );
    328 
    329  dragZoom.offsetX =
    330    devicePixelRatio *
    331    ((dragZoom.zoomedWidth - container.offsetWidth) / 2 - dragZoom.translateX);
    332  dragZoom.offsetY =
    333    devicePixelRatio *
    334    ((dragZoom.zoomedHeight - container.offsetHeight) / 2 -
    335      dragZoom.translateY);
    336 }