summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/highlighters/utils/canvas.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/highlighters/utils/canvas.js')
-rw-r--r--devtools/server/actors/highlighters/utils/canvas.js596
1 files changed, 596 insertions, 0 deletions
diff --git a/devtools/server/actors/highlighters/utils/canvas.js b/devtools/server/actors/highlighters/utils/canvas.js
new file mode 100644
index 0000000000..24285f02e0
--- /dev/null
+++ b/devtools/server/actors/highlighters/utils/canvas.js
@@ -0,0 +1,596 @@
+/* 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 {
+ apply,
+ getNodeTransformationMatrix,
+ getWritingModeMatrix,
+ identity,
+ isIdentity,
+ multiply,
+ scale,
+ translate,
+} = require("resource://devtools/shared/layout/dom-matrix-2d.js");
+const {
+ getCurrentZoom,
+ getViewportDimensions,
+} = require("resource://devtools/shared/layout/utils.js");
+const {
+ getComputedStyle,
+} = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+
+// A set of utility functions for highlighters that render their content to a <canvas>
+// element.
+
+// We create a <canvas> element that has always 4096x4096 physical pixels, to displays
+// our grid's overlay.
+// Then, we move the element around when needed, to give the perception that it always
+// covers the screen (See bug 1345434).
+//
+// This canvas size value is the safest we can use because most GPUs can handle it.
+// It's also far from the maximum canvas memory allocation limit (4096x4096x4 is
+// 67.108.864 bytes, where the limit is 500.000.000 bytes, see
+// gfx_max_alloc_size in modules/libpref/init/StaticPrefList.yaml.
+//
+// Note:
+// Once bug 1232491 lands, we could try to refactor this code to use the values from
+// the displayport API instead.
+//
+// Using a fixed value should also solve bug 1348293.
+const CANVAS_SIZE = 4096;
+
+// The default color used for the canvas' font, fill and stroke colors.
+const DEFAULT_COLOR = "#9400FF";
+
+/**
+ * Draws a rect to the context given and applies a transformation matrix if passed.
+ * The coordinates are the start and end points of the rectangle's diagonal.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * The 2D canvas context.
+ * @param {Number} x1
+ * The x-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} y1
+ * The y-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} x2
+ * The x-axis coordinate of the rectangle's diagonal end point.
+ * @param {Number} y2
+ * The y-axis coordinate of the rectangle's diagonal end point.
+ * @param {Array} [matrix=identity()]
+ * The transformation matrix to apply.
+ */
+function clearRect(ctx, x1, y1, x2, y2, matrix = identity()) {
+ const p = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
+
+ // We are creating a clipping path and want it removed after we clear it's
+ // contents so we need to save the context.
+ ctx.save();
+
+ // Create a path to be cleared.
+ ctx.beginPath();
+ ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
+ ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
+ ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
+ ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
+ ctx.closePath();
+
+ // Restrict future drawing to the inside of the path.
+ ctx.clip();
+
+ // Clear any transforms applied to the canvas so that clearRect() really does
+ // clear everything.
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+
+ // Clear the contents of our clipped path by attempting to clear the canvas.
+ ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
+
+ // Restore the context to the state it was before changing transforms and
+ // adding clipping paths.
+ ctx.restore();
+}
+
+/**
+ * Draws an arrow-bubble rectangle in the provided canvas context.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * The 2D canvas context.
+ * @param {Number} x
+ * The x-axis origin of the rectangle.
+ * @param {Number} y
+ * The y-axis origin of the rectangle.
+ * @param {Number} width
+ * The width of the rectangle.
+ * @param {Number} height
+ * The height of the rectangle.
+ * @param {Number} radius
+ * The radius of the rounding.
+ * @param {Number} margin
+ * The distance of the origin point from the pointer.
+ * @param {Number} arrowSize
+ * The size of the arrow.
+ * @param {String} alignment
+ * The alignment of the rectangle in relation to its position to the grid.
+ */
+function drawBubbleRect(
+ ctx,
+ x,
+ y,
+ width,
+ height,
+ radius,
+ margin,
+ arrowSize,
+ alignment
+) {
+ let angle = 0;
+
+ if (alignment === "bottom") {
+ angle = 180;
+ } else if (alignment === "right") {
+ angle = 90;
+ [width, height] = [height, width];
+ } else if (alignment === "left") {
+ [width, height] = [height, width];
+ angle = 270;
+ }
+
+ const originX = x;
+ const originY = y;
+
+ ctx.save();
+ ctx.translate(originX, originY);
+ ctx.rotate(angle * (Math.PI / 180));
+ ctx.translate(-originX, -originY);
+ ctx.translate(-width / 2, -height - arrowSize - margin);
+
+ // The contour of the bubble is drawn with a path. The canvas context will have taken
+ // care of transforming the coordinates before calling the function, so we just always
+ // draw with the arrow pointing down. The top edge has rounded corners too.
+ ctx.beginPath();
+ // Start at the top/left corner (below the rounded corner).
+ ctx.moveTo(x, y + radius);
+ // Go down.
+ ctx.lineTo(x, y + height);
+ // Go down and the right, to draw the first half of the arrow tip.
+ ctx.lineTo(x + width / 2, y + height + arrowSize);
+ // Go back up and to the right, to draw the second half of the arrow tip.
+ ctx.lineTo(x + width, y + height);
+ // Go up to just below the top/right rounded corner.
+ ctx.lineTo(x + width, y + radius);
+ // Draw the top/right rounded corner.
+ ctx.arcTo(x + width, y, x + width - radius, y, radius);
+ // Go to the left.
+ ctx.lineTo(x + radius, y);
+ // Draw the top/left rounded corner.
+ ctx.arcTo(x, y, x, y + radius, radius);
+
+ ctx.stroke();
+ ctx.fill();
+
+ ctx.restore();
+}
+
+/**
+ * Draws a line to the context given and applies a transformation matrix if passed.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * The 2D canvas context.
+ * @param {Number} x1
+ * The x-axis of the coordinate for the begin of the line.
+ * @param {Number} y1
+ * The y-axis of the coordinate for the begin of the line.
+ * @param {Number} x2
+ * The x-axis of the coordinate for the end of the line.
+ * @param {Number} y2
+ * The y-axis of the coordinate for the end of the line.
+ * @param {Object} [options]
+ * The options object.
+ * @param {Array} [options.matrix=identity()]
+ * The transformation matrix to apply.
+ * @param {Array} [options.extendToBoundaries]
+ * If set, the line will be extended to reach the boundaries specified.
+ */
+function drawLine(ctx, x1, y1, x2, y2, options) {
+ const matrix = options.matrix || identity();
+
+ const p1 = apply(matrix, [x1, y1]);
+ const p2 = apply(matrix, [x2, y2]);
+
+ x1 = p1[0];
+ y1 = p1[1];
+ x2 = p2[0];
+ y2 = p2[1];
+
+ if (options.extendToBoundaries) {
+ if (p1[1] === p2[1]) {
+ x1 = options.extendToBoundaries[0];
+ x2 = options.extendToBoundaries[2];
+ } else {
+ y1 = options.extendToBoundaries[1];
+ x1 = ((p2[0] - p1[0]) * (y1 - p1[1])) / (p2[1] - p1[1]) + p1[0];
+ y2 = options.extendToBoundaries[3];
+ x2 = ((p2[0] - p1[0]) * (y2 - p1[1])) / (p2[1] - p1[1]) + p1[0];
+ }
+ }
+
+ ctx.beginPath();
+ ctx.moveTo(Math.round(x1), Math.round(y1));
+ ctx.lineTo(Math.round(x2), Math.round(y2));
+}
+
+/**
+ * Draws a rect to the context given and applies a transformation matrix if passed.
+ * The coordinates are the start and end points of the rectangle's diagonal.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * The 2D canvas context.
+ * @param {Number} x1
+ * The x-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} y1
+ * The y-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} x2
+ * The x-axis coordinate of the rectangle's diagonal end point.
+ * @param {Number} y2
+ * The y-axis coordinate of the rectangle's diagonal end point.
+ * @param {Array} [matrix=identity()]
+ * The transformation matrix to apply.
+ */
+function drawRect(ctx, x1, y1, x2, y2, matrix = identity()) {
+ const p = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
+
+ ctx.beginPath();
+ ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
+ ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
+ ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
+ ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
+ ctx.closePath();
+}
+
+/**
+ * Draws a rounded rectangle in the provided canvas context.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * The 2D canvas context.
+ * @param {Number} x
+ * The x-axis origin of the rectangle.
+ * @param {Number} y
+ * The y-axis origin of the rectangle.
+ * @param {Number} width
+ * The width of the rectangle.
+ * @param {Number} height
+ * The height of the rectangle.
+ * @param {Number} radius
+ * The radius of the rounding.
+ */
+function drawRoundedRect(ctx, x, y, width, height, radius) {
+ ctx.beginPath();
+ ctx.moveTo(x, y + radius);
+ ctx.lineTo(x, y + height - radius);
+ ctx.arcTo(x, y + height, x + radius, y + height, radius);
+ ctx.lineTo(x + width - radius, y + height);
+ ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
+ ctx.lineTo(x + width, y + radius);
+ ctx.arcTo(x + width, y, x + width - radius, y, radius);
+ ctx.lineTo(x + radius, y);
+ ctx.arcTo(x, y, x, y + radius, radius);
+ ctx.stroke();
+ ctx.fill();
+}
+
+/**
+ * Given an array of four points and returns a DOMRect-like object representing the
+ * boundaries defined by the four points.
+ *
+ * @param {Array} points
+ * An array with 4 pointer objects {x, y} representing the box quads.
+ * @return {Object} DOMRect-like object of the 4 points.
+ */
+function getBoundsFromPoints(points) {
+ const bounds = {};
+
+ bounds.left = Math.min(points[0].x, points[1].x, points[2].x, points[3].x);
+ bounds.right = Math.max(points[0].x, points[1].x, points[2].x, points[3].x);
+ bounds.top = Math.min(points[0].y, points[1].y, points[2].y, points[3].y);
+ bounds.bottom = Math.max(points[0].y, points[1].y, points[2].y, points[3].y);
+
+ bounds.x = bounds.left;
+ bounds.y = bounds.top;
+ bounds.width = bounds.right - bounds.left;
+ bounds.height = bounds.bottom - bounds.top;
+
+ return bounds;
+}
+
+/**
+ * Returns the current matrices for both canvas drawing and SVG taking into account the
+ * following transformations, in this order:
+ * 1. The scale given by the display pixel ratio.
+ * 2. The translation to the top left corner of the element.
+ * 3. The scale given by the current zoom.
+ * 4. The translation given by the top and left padding of the element.
+ * 5. Any CSS transformation applied directly to the element (only 2D
+ * transformation; the 3D transformation are flattened, see `dom-matrix-2d` module
+ * for further details.)
+ * 6. Rotate, translate, and reflect as needed to match the writing mode and text
+ * direction of the element.
+ *
+ * The transformations of the element's ancestors are not currently computed (see
+ * bug 1355675).
+ *
+ * @param {Element} element
+ * The current element.
+ * @param {Window} window
+ * The window object.
+ * @param {Object} [options.ignoreWritingModeAndTextDirection=false]
+ * Avoid transforming the current matrix to match the text direction
+ * and writing mode.
+ * @return {Object} An object with the following properties:
+ * - {Array} currentMatrix
+ * The current matrix.
+ * - {Boolean} hasNodeTransformations
+ * true if the node has transformed and false otherwise.
+ */
+function getCurrentMatrix(
+ element,
+ window,
+ { ignoreWritingModeAndTextDirection } = {}
+) {
+ const computedStyle = getComputedStyle(element);
+
+ const paddingTop = parseFloat(computedStyle.paddingTop);
+ const paddingRight = parseFloat(computedStyle.paddingRight);
+ const paddingBottom = parseFloat(computedStyle.paddingBottom);
+ const paddingLeft = parseFloat(computedStyle.paddingLeft);
+ const borderTop = parseFloat(computedStyle.borderTopWidth);
+ const borderRight = parseFloat(computedStyle.borderRightWidth);
+ const borderBottom = parseFloat(computedStyle.borderBottomWidth);
+ const borderLeft = parseFloat(computedStyle.borderLeftWidth);
+
+ const nodeMatrix = getNodeTransformationMatrix(
+ element,
+ window.document.documentElement
+ );
+
+ let currentMatrix = identity();
+ let hasNodeTransformations = false;
+
+ // Scale based on the device pixel ratio.
+ currentMatrix = multiply(currentMatrix, scale(window.devicePixelRatio));
+
+ // Apply the current node's transformation matrix, relative to the inspected window's
+ // root element, but only if it's not a identity matrix.
+ if (isIdentity(nodeMatrix)) {
+ hasNodeTransformations = false;
+ } else {
+ currentMatrix = multiply(currentMatrix, nodeMatrix);
+ hasNodeTransformations = true;
+ }
+
+ // Translate the origin based on the node's padding and border values.
+ currentMatrix = multiply(
+ currentMatrix,
+ translate(paddingLeft + borderLeft, paddingTop + borderTop)
+ );
+
+ // Adjust as needed to match the writing mode and text direction of the element.
+ const size = {
+ width:
+ element.offsetWidth -
+ borderLeft -
+ borderRight -
+ paddingLeft -
+ paddingRight,
+ height:
+ element.offsetHeight -
+ borderTop -
+ borderBottom -
+ paddingTop -
+ paddingBottom,
+ };
+
+ if (!ignoreWritingModeAndTextDirection) {
+ const writingModeMatrix = getWritingModeMatrix(size, computedStyle);
+ if (!isIdentity(writingModeMatrix)) {
+ currentMatrix = multiply(currentMatrix, writingModeMatrix);
+ }
+ }
+
+ return { currentMatrix, hasNodeTransformations };
+}
+
+/**
+ * Given an array of four points, returns a string represent a path description.
+ *
+ * @param {Array} points
+ * An array with 4 pointer objects {x, y} representing the box quads.
+ * @return {String} a Path Description that can be used in svg's <path> element.
+ */
+function getPathDescriptionFromPoints(points) {
+ return (
+ "M" +
+ points[0].x +
+ "," +
+ points[0].y +
+ " " +
+ "L" +
+ points[1].x +
+ "," +
+ points[1].y +
+ " " +
+ "L" +
+ points[2].x +
+ "," +
+ points[2].y +
+ " " +
+ "L" +
+ points[3].x +
+ "," +
+ points[3].y
+ );
+}
+
+/**
+ * Given the rectangle's diagonal start and end coordinates, returns an array containing
+ * the four coordinates of a rectangle. If a matrix is provided, applies the matrix
+ * function to each of the coordinates' value.
+ *
+ * @param {Number} x1
+ * The x-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} y1
+ * The y-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} x2
+ * The x-axis coordinate of the rectangle's diagonal end point.
+ * @param {Number} y2
+ * The y-axis coordinate of the rectangle's diagonal end point.
+ * @param {Array} [matrix=identity()]
+ * A transformation matrix to apply.
+ * @return {Array} the four coordinate points of the given rectangle transformed by the
+ * matrix given.
+ */
+function getPointsFromDiagonal(x1, y1, x2, y2, matrix = identity()) {
+ return [
+ [x1, y1],
+ [x2, y1],
+ [x2, y2],
+ [x1, y2],
+ ].map(point => {
+ const transformedPoint = apply(matrix, point);
+
+ return { x: transformedPoint[0], y: transformedPoint[1] };
+ });
+}
+
+/**
+ * Updates the <canvas> element's style in accordance with the current window's
+ * device pixel ratio, and the position calculated in `getCanvasPosition`. It also
+ * clears the drawing context. This is called on canvas update after a scroll event where
+ * `getCanvasPosition` updates the new canvasPosition.
+ *
+ * @param {Canvas} canvas
+ * The <canvas> element.
+ * @param {Object} canvasPosition
+ * A pointer object {x, y} representing the <canvas> position to the top left
+ * corner of the page.
+ * @param {Number} devicePixelRatio
+ * The device pixel ratio.
+ * @param {Window} [options.zoomWindow]
+ * Optional window object used to calculate zoom (default = undefined).
+ */
+function updateCanvasElement(
+ canvas,
+ canvasPosition,
+ devicePixelRatio,
+ { zoomWindow } = {}
+) {
+ let { x, y } = canvasPosition;
+ const size = CANVAS_SIZE / devicePixelRatio;
+
+ if (zoomWindow) {
+ const zoom = getCurrentZoom(zoomWindow);
+ x *= zoom;
+ y *= zoom;
+ }
+
+ // Resize the canvas taking the dpr into account so as to have crisp lines, and
+ // translating it to give the perception that it always covers the viewport.
+ canvas.setAttribute(
+ "style",
+ `width: ${size}px; height: ${size}px; transform: translate(${x}px, ${y}px);`
+ );
+ canvas.getCanvasContext("2d").clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
+}
+
+/**
+ * Calculates and returns the <canvas>'s position in accordance with the page's scroll,
+ * document's size, canvas size, and viewport's size. This is called when a page's scroll
+ * is detected.
+ *
+ * @param {Object} canvasPosition
+ * A pointer object {x, y} representing the <canvas> position to the top left
+ * corner of the page.
+ * @param {Object} scrollPosition
+ * A pointer object {x, y} representing the window's pageXOffset and pageYOffset.
+ * @param {Window} window
+ * The window object.
+ * @param {Object} windowDimensions
+ * An object {width, height} representing the window's dimensions for the
+ * `window` given.
+ * @return {Boolean} true if the <canvas> position was updated and false otherwise.
+ */
+function updateCanvasPosition(
+ canvasPosition,
+ scrollPosition,
+ window,
+ windowDimensions
+) {
+ let { x: canvasX, y: canvasY } = canvasPosition;
+ const { x: scrollX, y: scrollY } = scrollPosition;
+ const cssCanvasSize = CANVAS_SIZE / window.devicePixelRatio;
+ const viewportSize = getViewportDimensions(window);
+ const { height, width } = windowDimensions;
+ const canvasWidth = cssCanvasSize;
+ const canvasHeight = cssCanvasSize;
+ let hasUpdated = false;
+
+ // Those values indicates the relative horizontal and vertical space the page can
+ // scroll before we have to reposition the <canvas>; they're 1/4 of the delta between
+ // the canvas' size and the viewport's size: that's because we want to consider both
+ // sides (top/bottom, left/right; so 1/2 for each side) and also we don't want to
+ // shown the edges of the canvas in case of fast scrolling (to avoid showing undraw
+ // areas, therefore another 1/2 here).
+ const bufferSizeX = (canvasWidth - viewportSize.width) >> 2;
+ const bufferSizeY = (canvasHeight - viewportSize.height) >> 2;
+
+ // Defines the boundaries for the canvas.
+ const leftBoundary = 0;
+ const rightBoundary = width - canvasWidth;
+ const topBoundary = 0;
+ const bottomBoundary = height - canvasHeight;
+
+ // Defines the thresholds that triggers the canvas' position to be updated.
+ const leftThreshold = scrollX - bufferSizeX;
+ const rightThreshold =
+ scrollX - canvasWidth + viewportSize.width + bufferSizeX;
+ const topThreshold = scrollY - bufferSizeY;
+ const bottomThreshold =
+ scrollY - canvasHeight + viewportSize.height + bufferSizeY;
+
+ if (canvasX < rightBoundary && canvasX < rightThreshold) {
+ canvasX = Math.min(leftThreshold, rightBoundary);
+ hasUpdated = true;
+ } else if (canvasX > leftBoundary && canvasX > leftThreshold) {
+ canvasX = Math.max(rightThreshold, leftBoundary);
+ hasUpdated = true;
+ }
+
+ if (canvasY < bottomBoundary && canvasY < bottomThreshold) {
+ canvasY = Math.min(topThreshold, bottomBoundary);
+ hasUpdated = true;
+ } else if (canvasY > topBoundary && canvasY > topThreshold) {
+ canvasY = Math.max(bottomThreshold, topBoundary);
+ hasUpdated = true;
+ }
+
+ // Update the canvas position with the calculated canvasX and canvasY positions.
+ canvasPosition.x = canvasX;
+ canvasPosition.y = canvasY;
+
+ return hasUpdated;
+}
+
+exports.CANVAS_SIZE = CANVAS_SIZE;
+exports.DEFAULT_COLOR = DEFAULT_COLOR;
+exports.clearRect = clearRect;
+exports.drawBubbleRect = drawBubbleRect;
+exports.drawLine = drawLine;
+exports.drawRect = drawRect;
+exports.drawRoundedRect = drawRoundedRect;
+exports.getBoundsFromPoints = getBoundsFromPoints;
+exports.getCurrentMatrix = getCurrentMatrix;
+exports.getPathDescriptionFromPoints = getPathDescriptionFromPoints;
+exports.getPointsFromDiagonal = getPointsFromDiagonal;
+exports.updateCanvasElement = updateCanvasElement;
+exports.updateCanvasPosition = updateCanvasPosition;