tor-browser

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

canvas.js (20377B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const {
      8  apply,
      9  getNodeTransformationMatrix,
     10  getWritingModeMatrix,
     11  identity,
     12  isIdentity,
     13  multiply,
     14  scale,
     15  translate,
     16 } = require("resource://devtools/shared/layout/dom-matrix-2d.js");
     17 const {
     18  getCurrentZoom,
     19  getViewportDimensions,
     20 } = require("resource://devtools/shared/layout/utils.js");
     21 const {
     22  getComputedStyle,
     23 } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
     24 
     25 // A set of utility functions for highlighters that render their content to a <canvas>
     26 // element.
     27 
     28 // We create a <canvas> element that has always 4096x4096 physical pixels, to displays
     29 // our grid's overlay.
     30 // Then, we move the element around when needed, to give the perception that it always
     31 // covers the screen (See bug 1345434).
     32 //
     33 // This canvas size value is the safest we can use because most GPUs can handle it.
     34 // It's also far from the maximum canvas memory allocation limit (4096x4096x4 is
     35 // 67.108.864 bytes, where the limit is 500.000.000 bytes, see
     36 // gfx_max_alloc_size in modules/libpref/init/StaticPrefList.yaml.
     37 //
     38 // Note:
     39 // Once bug 1232491 lands, we could try to refactor this code to use the values from
     40 // the displayport API instead.
     41 //
     42 // Using a fixed value should also solve bug 1348293.
     43 const CANVAS_SIZE = 4096;
     44 
     45 // The default color used for the canvas' font, fill and stroke colors.
     46 const DEFAULT_COLOR = "#9400FF";
     47 
     48 /**
     49 * Draws a rect to the context given and applies a transformation matrix if passed.
     50 * The coordinates are the start and end points of the rectangle's diagonal.
     51 *
     52 * @param  {CanvasRenderingContext2D} ctx
     53 *         The 2D canvas context.
     54 * @param  {number} x1
     55 *         The x-axis coordinate of the rectangle's diagonal start point.
     56 * @param  {number} y1
     57 *         The y-axis coordinate of the rectangle's diagonal start point.
     58 * @param  {number} x2
     59 *         The x-axis coordinate of the rectangle's diagonal end point.
     60 * @param  {number} y2
     61 *         The y-axis coordinate of the rectangle's diagonal end point.
     62 * @param  {Array} [matrix=identity()]
     63 *         The transformation matrix to apply.
     64 */
     65 function clearRect(ctx, x1, y1, x2, y2, matrix = identity()) {
     66  const p = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
     67 
     68  // We are creating a clipping path and want it removed after we clear it's
     69  // contents so we need to save the context.
     70  ctx.save();
     71 
     72  // Create a path to be cleared.
     73  ctx.beginPath();
     74  ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
     75  ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
     76  ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
     77  ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
     78  ctx.closePath();
     79 
     80  // Restrict future drawing to the inside of the path.
     81  ctx.clip();
     82 
     83  // Clear any transforms applied to the canvas so that clearRect() really does
     84  // clear everything.
     85  ctx.setTransform(1, 0, 0, 1, 0, 0);
     86 
     87  // Clear the contents of our clipped path by attempting to clear the canvas.
     88  ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
     89 
     90  // Restore the context to the state it was before changing transforms and
     91  // adding clipping paths.
     92  ctx.restore();
     93 }
     94 
     95 /**
     96 * Draws an arrow-bubble rectangle in the provided canvas context.
     97 *
     98 * @param  {CanvasRenderingContext2D} ctx
     99 *         The 2D canvas context.
    100 * @param  {number} x
    101 *         The x-axis origin of the rectangle.
    102 * @param  {number} y
    103 *         The y-axis origin of the rectangle.
    104 * @param  {number} width
    105 *         The width of the rectangle.
    106 * @param  {number} height
    107 *         The height of the rectangle.
    108 * @param  {number} radius
    109 *         The radius of the rounding.
    110 * @param  {number} margin
    111 *         The distance of the origin point from the pointer.
    112 * @param  {number} arrowSize
    113 *         The size of the arrow.
    114 * @param  {string} alignment
    115 *         The alignment of the rectangle in relation to its position to the grid.
    116 */
    117 function drawBubbleRect(
    118  ctx,
    119  x,
    120  y,
    121  width,
    122  height,
    123  radius,
    124  margin,
    125  arrowSize,
    126  alignment
    127 ) {
    128  let angle = 0;
    129 
    130  if (alignment === "bottom") {
    131    angle = 180;
    132  } else if (alignment === "right") {
    133    angle = 90;
    134    [width, height] = [height, width];
    135  } else if (alignment === "left") {
    136    [width, height] = [height, width];
    137    angle = 270;
    138  }
    139 
    140  const originX = x;
    141  const originY = y;
    142 
    143  ctx.save();
    144  ctx.translate(originX, originY);
    145  ctx.rotate(angle * (Math.PI / 180));
    146  ctx.translate(-originX, -originY);
    147  ctx.translate(-width / 2, -height - arrowSize - margin);
    148 
    149  // The contour of the bubble is drawn with a path. The canvas context will have taken
    150  // care of transforming the coordinates before calling the function, so we just always
    151  // draw with the arrow pointing down. The top edge has rounded corners too.
    152  ctx.beginPath();
    153  // Start at the top/left corner (below the rounded corner).
    154  ctx.moveTo(x, y + radius);
    155  // Go down.
    156  ctx.lineTo(x, y + height);
    157  // Go down and the right, to draw the first half of the arrow tip.
    158  ctx.lineTo(x + width / 2, y + height + arrowSize);
    159  // Go back up and to the right, to draw the second half of the arrow tip.
    160  ctx.lineTo(x + width, y + height);
    161  // Go up to just below the top/right rounded corner.
    162  ctx.lineTo(x + width, y + radius);
    163  // Draw the top/right rounded corner.
    164  ctx.arcTo(x + width, y, x + width - radius, y, radius);
    165  // Go to the left.
    166  ctx.lineTo(x + radius, y);
    167  // Draw the top/left rounded corner.
    168  ctx.arcTo(x, y, x, y + radius, radius);
    169 
    170  ctx.stroke();
    171  ctx.fill();
    172 
    173  ctx.restore();
    174 }
    175 
    176 /**
    177 * Draws a line to the context given and applies a transformation matrix if passed.
    178 *
    179 * @param  {CanvasRenderingContext2D} ctx
    180 *         The 2D canvas context.
    181 * @param  {number} x1
    182 *         The x-axis of the coordinate for the begin of the line.
    183 * @param  {number} y1
    184 *         The y-axis of the coordinate for the begin of the line.
    185 * @param  {number} x2
    186 *         The x-axis of the coordinate for the end of the line.
    187 * @param  {number} y2
    188 *         The y-axis of the coordinate for the end of the line.
    189 * @param  {object} [options]
    190 *         The options object.
    191 * @param  {Array} [options.matrix=identity()]
    192 *         The transformation matrix to apply.
    193 * @param  {Array} [options.extendToBoundaries]
    194 *         If set, the line will be extended to reach the boundaries specified.
    195 */
    196 function drawLine(ctx, x1, y1, x2, y2, options) {
    197  const matrix = options.matrix || identity();
    198 
    199  const p1 = apply(matrix, [x1, y1]);
    200  const p2 = apply(matrix, [x2, y2]);
    201 
    202  x1 = p1[0];
    203  y1 = p1[1];
    204  x2 = p2[0];
    205  y2 = p2[1];
    206 
    207  if (options.extendToBoundaries) {
    208    if (p1[1] === p2[1]) {
    209      x1 = options.extendToBoundaries[0];
    210      x2 = options.extendToBoundaries[2];
    211    } else {
    212      y1 = options.extendToBoundaries[1];
    213      x1 = ((p2[0] - p1[0]) * (y1 - p1[1])) / (p2[1] - p1[1]) + p1[0];
    214      y2 = options.extendToBoundaries[3];
    215      x2 = ((p2[0] - p1[0]) * (y2 - p1[1])) / (p2[1] - p1[1]) + p1[0];
    216    }
    217  }
    218 
    219  ctx.beginPath();
    220  ctx.moveTo(Math.round(x1), Math.round(y1));
    221  ctx.lineTo(Math.round(x2), Math.round(y2));
    222 }
    223 
    224 /**
    225 * Draws a rect to the context given and applies a transformation matrix if passed.
    226 * The coordinates are the start and end points of the rectangle's diagonal.
    227 *
    228 * @param  {CanvasRenderingContext2D} ctx
    229 *         The 2D canvas context.
    230 * @param  {number} x1
    231 *         The x-axis coordinate of the rectangle's diagonal start point.
    232 * @param  {number} y1
    233 *         The y-axis coordinate of the rectangle's diagonal start point.
    234 * @param  {number} x2
    235 *         The x-axis coordinate of the rectangle's diagonal end point.
    236 * @param  {number} y2
    237 *         The y-axis coordinate of the rectangle's diagonal end point.
    238 * @param  {Array} [matrix=identity()]
    239 *         The transformation matrix to apply.
    240 */
    241 function drawRect(ctx, x1, y1, x2, y2, matrix = identity()) {
    242  const p = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
    243 
    244  ctx.beginPath();
    245  ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
    246  ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
    247  ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
    248  ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
    249  ctx.closePath();
    250 }
    251 
    252 /**
    253 * Draws a rounded rectangle in the provided canvas context.
    254 *
    255 * @param  {CanvasRenderingContext2D} ctx
    256 *         The 2D canvas context.
    257 * @param  {number} x
    258 *         The x-axis origin of the rectangle.
    259 * @param  {number} y
    260 *         The y-axis origin of the rectangle.
    261 * @param  {number} width
    262 *         The width of the rectangle.
    263 * @param  {number} height
    264 *         The height of the rectangle.
    265 * @param  {number} radius
    266 *         The radius of the rounding.
    267 */
    268 function drawRoundedRect(ctx, x, y, width, height, radius) {
    269  ctx.beginPath();
    270  ctx.moveTo(x, y + radius);
    271  ctx.lineTo(x, y + height - radius);
    272  ctx.arcTo(x, y + height, x + radius, y + height, radius);
    273  ctx.lineTo(x + width - radius, y + height);
    274  ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
    275  ctx.lineTo(x + width, y + radius);
    276  ctx.arcTo(x + width, y, x + width - radius, y, radius);
    277  ctx.lineTo(x + radius, y);
    278  ctx.arcTo(x, y, x, y + radius, radius);
    279  ctx.stroke();
    280  ctx.fill();
    281 }
    282 
    283 /**
    284 * Given an array of four points and returns a DOMRect-like object representing the
    285 * boundaries defined by the four points.
    286 *
    287 * @param  {Array} points
    288 *         An array with 4 pointer objects {x, y} representing the box quads.
    289 * @return {object} DOMRect-like object of the 4 points.
    290 */
    291 function getBoundsFromPoints(points) {
    292  const bounds = {};
    293 
    294  bounds.left = Math.min(points[0].x, points[1].x, points[2].x, points[3].x);
    295  bounds.right = Math.max(points[0].x, points[1].x, points[2].x, points[3].x);
    296  bounds.top = Math.min(points[0].y, points[1].y, points[2].y, points[3].y);
    297  bounds.bottom = Math.max(points[0].y, points[1].y, points[2].y, points[3].y);
    298 
    299  bounds.x = bounds.left;
    300  bounds.y = bounds.top;
    301  bounds.width = bounds.right - bounds.left;
    302  bounds.height = bounds.bottom - bounds.top;
    303 
    304  return bounds;
    305 }
    306 
    307 /**
    308 * Returns the current matrices for both canvas drawing and SVG taking into account the
    309 * following transformations, in this order:
    310 *   1. The scale given by the display pixel ratio.
    311 *   2. The translation to the top left corner of the element.
    312 *   3. The scale given by the current zoom.
    313 *   4. The translation given by the top and left padding of the element.
    314 *   5. Any CSS transformation applied directly to the element (only 2D
    315 *      transformation; the 3D transformation are flattened, see `dom-matrix-2d` module
    316 *      for further details.)
    317 *   6. Rotate, translate, and reflect as needed to match the writing mode and text
    318 *      direction of the element.
    319 *
    320 *  The transformations of the element's ancestors are not currently computed (see
    321 *  bug 1355675).
    322 *
    323 * @param  {Element} element
    324 *         The current element.
    325 * @param  {Window} window
    326 *         The window object.
    327 * @param  {object} [options.ignoreWritingModeAndTextDirection=false]
    328 *                  Avoid transforming the current matrix to match the text direction
    329 *                  and writing mode.
    330 * @return {object} An object with the following properties:
    331 *         - {Array} currentMatrix
    332 *           The current matrix.
    333 *         - {Boolean} hasNodeTransformations
    334 *           true if the node has transformed and false otherwise.
    335 */
    336 function getCurrentMatrix(
    337  element,
    338  window,
    339  { ignoreWritingModeAndTextDirection } = {}
    340 ) {
    341  const computedStyle = getComputedStyle(element);
    342 
    343  const paddingTop = parseFloat(computedStyle.paddingTop);
    344  const paddingRight = parseFloat(computedStyle.paddingRight);
    345  const paddingBottom = parseFloat(computedStyle.paddingBottom);
    346  const paddingLeft = parseFloat(computedStyle.paddingLeft);
    347  const borderTop = parseFloat(computedStyle.borderTopWidth);
    348  const borderRight = parseFloat(computedStyle.borderRightWidth);
    349  const borderBottom = parseFloat(computedStyle.borderBottomWidth);
    350  const borderLeft = parseFloat(computedStyle.borderLeftWidth);
    351 
    352  const nodeMatrix = getNodeTransformationMatrix(
    353    element,
    354    window.document.documentElement
    355  );
    356 
    357  let currentMatrix = identity();
    358  let hasNodeTransformations = false;
    359 
    360  // Scale based on the device pixel ratio.
    361  currentMatrix = multiply(currentMatrix, scale(window.devicePixelRatio));
    362 
    363  // Apply the current node's transformation matrix, relative to the inspected window's
    364  // root element, but only if it's not a identity matrix.
    365  if (isIdentity(nodeMatrix)) {
    366    hasNodeTransformations = false;
    367  } else {
    368    currentMatrix = multiply(currentMatrix, nodeMatrix);
    369    hasNodeTransformations = true;
    370  }
    371 
    372  // Translate the origin based on the node's padding and border values.
    373  currentMatrix = multiply(
    374    currentMatrix,
    375    translate(paddingLeft + borderLeft, paddingTop + borderTop)
    376  );
    377 
    378  // Adjust as needed to match the writing mode and text direction of the element.
    379  const size = {
    380    width:
    381      element.offsetWidth -
    382      borderLeft -
    383      borderRight -
    384      paddingLeft -
    385      paddingRight,
    386    height:
    387      element.offsetHeight -
    388      borderTop -
    389      borderBottom -
    390      paddingTop -
    391      paddingBottom,
    392  };
    393 
    394  if (!ignoreWritingModeAndTextDirection) {
    395    const writingModeMatrix = getWritingModeMatrix(size, computedStyle);
    396    if (!isIdentity(writingModeMatrix)) {
    397      currentMatrix = multiply(currentMatrix, writingModeMatrix);
    398    }
    399  }
    400 
    401  return { currentMatrix, hasNodeTransformations };
    402 }
    403 
    404 /**
    405 * Given an array of four points, returns a string represent a path description.
    406 *
    407 * @param  {Array} points
    408 *         An array with 4 pointer objects {x, y} representing the box quads.
    409 * @return {string} a Path Description that can be used in svg's <path> element.
    410 */
    411 function getPathDescriptionFromPoints(points) {
    412  return (
    413    "M" +
    414    points[0].x +
    415    "," +
    416    points[0].y +
    417    " " +
    418    "L" +
    419    points[1].x +
    420    "," +
    421    points[1].y +
    422    " " +
    423    "L" +
    424    points[2].x +
    425    "," +
    426    points[2].y +
    427    " " +
    428    "L" +
    429    points[3].x +
    430    "," +
    431    points[3].y
    432  );
    433 }
    434 
    435 /**
    436 * Given the rectangle's diagonal start and end coordinates, returns an array containing
    437 * the four coordinates of a rectangle. If a matrix is provided, applies the matrix
    438 * function to each of the coordinates' value.
    439 *
    440 * @param  {number} x1
    441 *         The x-axis coordinate of the rectangle's diagonal start point.
    442 * @param  {number} y1
    443 *         The y-axis coordinate of the rectangle's diagonal start point.
    444 * @param  {number} x2
    445 *         The x-axis coordinate of the rectangle's diagonal end point.
    446 * @param  {number} y2
    447 *         The y-axis coordinate of the rectangle's diagonal end point.
    448 * @param  {Array} [matrix=identity()]
    449 *         A transformation matrix to apply.
    450 * @return {Array} the four coordinate points of the given rectangle transformed by the
    451 * matrix given.
    452 */
    453 function getPointsFromDiagonal(x1, y1, x2, y2, matrix = identity()) {
    454  return [
    455    [x1, y1],
    456    [x2, y1],
    457    [x2, y2],
    458    [x1, y2],
    459  ].map(point => {
    460    const transformedPoint = apply(matrix, point);
    461 
    462    return { x: transformedPoint[0], y: transformedPoint[1] };
    463  });
    464 }
    465 
    466 /**
    467 * Updates the <canvas> element's style in accordance with the current window's
    468 * device pixel ratio, and the position calculated in `getCanvasPosition`. It also
    469 * clears the drawing context. This is called on canvas update after a scroll event where
    470 * `getCanvasPosition` updates the new canvasPosition.
    471 *
    472 * @param  {Canvas} canvas
    473 *         The <canvas> element.
    474 * @param  {object} canvasPosition
    475 *         A pointer object {x, y} representing the <canvas> position to the top left
    476 *         corner of the page.
    477 * @param  {number} devicePixelRatio
    478 *         The device pixel ratio.
    479 * @param  {Window} [options.zoomWindow]
    480 *         Optional window object used to calculate zoom (default = undefined).
    481 */
    482 function updateCanvasElement(
    483  canvas,
    484  canvasPosition,
    485  devicePixelRatio,
    486  { zoomWindow } = {}
    487 ) {
    488  let { x, y } = canvasPosition;
    489  const size = CANVAS_SIZE / devicePixelRatio;
    490 
    491  if (zoomWindow) {
    492    const zoom = getCurrentZoom(zoomWindow);
    493    x *= zoom;
    494    y *= zoom;
    495  }
    496 
    497  // Resize the canvas taking the dpr into account so as to have crisp lines, and
    498  // translating it to give the perception that it always covers the viewport.
    499  canvas.setAttribute(
    500    "style",
    501    `width: ${size}px; height: ${size}px; transform: translate(${x}px, ${y}px);`
    502  );
    503  canvas.getCanvasContext("2d").clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
    504 }
    505 
    506 /**
    507 * Calculates and returns the <canvas>'s position in accordance with the page's scroll,
    508 * document's size, canvas size, and viewport's size. This is called when a page's scroll
    509 * is detected.
    510 *
    511 * @param  {object} canvasPosition
    512 *         A pointer object {x, y} representing the <canvas> position to the top left
    513 *         corner of the page.
    514 * @param  {object} scrollPosition
    515 *         A pointer object {x, y} representing the window's pageXOffset and pageYOffset.
    516 * @param  {Window} window
    517 *         The window object.
    518 * @param  {object} windowDimensions
    519 *         An object {width, height} representing the window's dimensions for the
    520 *         `window` given.
    521 * @return {boolean} true if the <canvas> position was updated and false otherwise.
    522 */
    523 function updateCanvasPosition(
    524  canvasPosition,
    525  scrollPosition,
    526  window,
    527  windowDimensions
    528 ) {
    529  let { x: canvasX, y: canvasY } = canvasPosition;
    530  const { x: scrollX, y: scrollY } = scrollPosition;
    531  const cssCanvasSize = CANVAS_SIZE / window.devicePixelRatio;
    532  const viewportSize = getViewportDimensions(window);
    533  const { height, width } = windowDimensions;
    534  const canvasWidth = cssCanvasSize;
    535  const canvasHeight = cssCanvasSize;
    536  let hasUpdated = false;
    537 
    538  // Those values indicates the relative horizontal and vertical space the page can
    539  // scroll before we have to reposition the <canvas>; they're 1/4 of the delta between
    540  // the canvas' size and the viewport's size: that's because we want to consider both
    541  // sides (top/bottom, left/right; so 1/2 for each side) and also we don't want to
    542  // shown the edges of the canvas in case of fast scrolling (to avoid showing undraw
    543  // areas, therefore another 1/2 here).
    544  const bufferSizeX = (canvasWidth - viewportSize.width) >> 2;
    545  const bufferSizeY = (canvasHeight - viewportSize.height) >> 2;
    546 
    547  // Defines the boundaries for the canvas.
    548  const leftBoundary = 0;
    549  const rightBoundary = width - canvasWidth;
    550  const topBoundary = 0;
    551  const bottomBoundary = height - canvasHeight;
    552 
    553  // Defines the thresholds that triggers the canvas' position to be updated.
    554  const leftThreshold = scrollX - bufferSizeX;
    555  const rightThreshold =
    556    scrollX - canvasWidth + viewportSize.width + bufferSizeX;
    557  const topThreshold = scrollY - bufferSizeY;
    558  const bottomThreshold =
    559    scrollY - canvasHeight + viewportSize.height + bufferSizeY;
    560 
    561  if (canvasX < rightBoundary && canvasX < rightThreshold) {
    562    canvasX = Math.min(leftThreshold, rightBoundary);
    563    hasUpdated = true;
    564  } else if (canvasX > leftBoundary && canvasX > leftThreshold) {
    565    canvasX = Math.max(rightThreshold, leftBoundary);
    566    hasUpdated = true;
    567  }
    568 
    569  if (canvasY < bottomBoundary && canvasY < bottomThreshold) {
    570    canvasY = Math.min(topThreshold, bottomBoundary);
    571    hasUpdated = true;
    572  } else if (canvasY > topBoundary && canvasY > topThreshold) {
    573    canvasY = Math.max(bottomThreshold, topBoundary);
    574    hasUpdated = true;
    575  }
    576 
    577  // Update the canvas position with the calculated canvasX and canvasY positions.
    578  canvasPosition.x = canvasX;
    579  canvasPosition.y = canvasY;
    580 
    581  return hasUpdated;
    582 }
    583 
    584 exports.CANVAS_SIZE = CANVAS_SIZE;
    585 exports.DEFAULT_COLOR = DEFAULT_COLOR;
    586 exports.clearRect = clearRect;
    587 exports.drawBubbleRect = drawBubbleRect;
    588 exports.drawLine = drawLine;
    589 exports.drawRect = drawRect;
    590 exports.drawRoundedRect = drawRoundedRect;
    591 exports.getBoundsFromPoints = getBoundsFromPoints;
    592 exports.getCurrentMatrix = getCurrentMatrix;
    593 exports.getPathDescriptionFromPoints = getPathDescriptionFromPoints;
    594 exports.getPointsFromDiagonal = getPointsFromDiagonal;
    595 exports.updateCanvasElement = updateCanvasElement;
    596 exports.updateCanvasPosition = updateCanvasPosition;