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;