Source code

Revision control

Copy as Markdown

Other Tools

/* 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,
const {
getCurrentZoom,
getViewportDimensions,
const {
getComputedStyle,
// 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
*
* @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;