Bug 1297072 - added support for matrices to handle CSS 2D transformation on grid inspector; r=pbro

- Fixed a bug on `getNodeBounds` that would makes the calculation wrong in case
  of nested frames.
- Centralized all the transformation in `updateCurrentMatrix` function,
  including the scaling due the zoom and display's pixel ratio, and the
  translation to the top left corner of the node inspected.
- Added the transformation from the inspected node to the `currentMatrix`.
- Added `drawLine` and `drawRect` functions, that takes a matrix as argument.
- Position the line's number to the grid itself even when we've infinite lines
  (it's not a regression, it is intended since if a grid is transformed, we
  could have weird results otherwise, so we decided to uniform the behaviors).

MozReview-Commit-ID: 7OUfb6u63Qj
This commit is contained in:
Matteo Ferretti 2017-04-20 19:17:02 +02:00
Родитель 108b6eefd9
Коммит bd64999657
2 изменённых файлов: 207 добавлений и 60 удалений

Просмотреть файл

@ -17,8 +17,18 @@ const {
getCurrentZoom,
getDisplayPixelRatio,
setIgnoreLayoutChanges,
getNodeBounds,
getViewportDimensions,
} = require("devtools/shared/layout/utils");
const {
identity,
apply,
translate,
multiply,
scale,
getNodeTransformationMatrix,
getNodeTransformOrigin
} = require("devtools/shared/layout/dom-matrix-2d");
const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils");
const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
@ -73,6 +83,69 @@ const gCachedGridPattern = new Map();
// Using a fixed value should also solve bug 1348293.
const CANVAS_SIZE = 4096;
// This constant is used as value to draw infinite lines on canvas; since we cannot use
// the canvas boundaries as coordinates to draw the lines, and then applying
// transformations on top of them (the resulting coordinates might ending before reaching
// the viewport's edges, therefore the lines won't looks as "infinite").
const CANVAS_INFINITY = CANVAS_SIZE << 8;
/**
* Draws a line to the context given, applying 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 {Array} [matrix=identity()]
* The transformation matrix to apply.
*/
function drawLine(ctx, x1, y1, x2, y2, matrix = identity()) {
let fromPoint = apply(matrix, [x1, y1]);
let toPoint = apply(matrix, [x2, y2]);
ctx.moveTo(Math.round(fromPoint[0]), Math.round(fromPoint[1]));
ctx.lineTo(Math.round(toPoint[0]), Math.round(toPoint[1]));
}
/**
* Draws a rect to the context given, applying 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()) {
let p = [
[x1, y1],
[x2, y1],
[x2, y2],
[x1, y2]
].map(point => apply(matrix, point).map(Math.round));
ctx.beginPath();
ctx.moveTo(p[0][0], p[0][1]);
ctx.lineTo(p[1][0], p[1][1]);
ctx.lineTo(p[2][0], p[2][1]);
ctx.lineTo(p[3][0], p[3][1]);
ctx.closePath();
}
/**
* Utility method to draw a rounded rectangle in the provided canvas context.
*
@ -89,7 +162,7 @@ const CANVAS_SIZE = 4096;
* @param {Number} radius
* The radius of the rounding.
*/
const roundedRect = function (ctx, x, y, width, height, radius) {
function drawRoundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
@ -102,7 +175,7 @@ const roundedRect = function (ctx, x, y, width, height, radius) {
ctx.arcTo(x, y, x, y + radius, radius);
ctx.stroke();
ctx.fill();
};
}
/**
* The CssGridHighlighter is the class that overlays a visual grid on top of
@ -662,11 +735,13 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
this.clearGridAreas();
this.clearGridCell();
// Update the current matrix used in our canvas' rendering
this.updateCurrentMatrix();
// Start drawing the grid fragments.
for (let i = 0; i < this.gridData.length; i++) {
let fragment = this.gridData[i];
let quad = this.currentQuads.content[i];
this.renderFragment(fragment, quad);
this.renderFragment(fragment);
}
// Display the grid area highlights if needed.
@ -876,6 +951,49 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
this.ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
},
/**
* Updates the current matrix taking in 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. Any CSS transformation applied directly to the element (only 2D
* transformation; the 3D transformation are flattened, see `dom-matrix-2d` module
* for further details.)
*
* The transformations of the element's ancestors are not currently computed (see
* bug 1355675).
*/
updateCurrentMatrix() {
let origin = getNodeTransformOrigin(this.currentNode);
let bounds = getNodeBounds(this.win, this.currentNode);
let nodeMatrix = getNodeTransformationMatrix(this.currentNode);
let ox = origin[0];
let oy = origin[1];
let m = identity();
// First, we scale based on the display's current pixel ratio.
m = multiply(m, scale(getDisplayPixelRatio(this.win)));
// Then we translate the origin to the node's top left corner.
m = multiply(m, translate(bounds.p1.x, bounds.p1.y));
// And scale based on the current zoom factor.
m = multiply(m, scale(getCurrentZoom(this.win)));
// Finally, we can apply the current node's transformation matrix, taking in account
// the `transform-origin` property.
if (nodeMatrix) {
m = multiply(m, translate(ox, oy));
m = multiply(m, nodeMatrix);
m = multiply(m, translate(-ox, -oy));
this.hasNodeTransformations = true;
} else {
this.hasNodeTransformations = false;
}
this.currentMatrix = m;
},
getFirstRowLinePos(fragment) {
return fragment.rows.lines[0].start;
},
@ -911,19 +1029,19 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
return trackIndex + 1;
},
renderFragment(fragment, quad) {
this.renderLines(fragment.cols, quad, COLUMNS, "left", "top", "height",
renderFragment(fragment) {
this.renderLines(fragment.cols, COLUMNS, "left", "top", "height",
this.getFirstRowLinePos(fragment),
this.getLastRowLinePos(fragment));
this.renderLines(fragment.rows, quad, ROWS, "top", "left", "width",
this.renderLines(fragment.rows, ROWS, "top", "left", "width",
this.getFirstColLinePos(fragment),
this.getLastColLinePos(fragment));
// Line numbers are rendered in a 2nd step to avoid overlapping with existing lines.
if (this.options.showGridLineNumbers) {
this.renderLineNumbers(fragment.cols, quad, COLUMNS, "left", "top",
this.renderLineNumbers(fragment.cols, COLUMNS, "left", "top",
this.getFirstRowLinePos(fragment));
this.renderLineNumbers(fragment.rows, quad, ROWS, "top", "left",
this.renderLineNumbers(fragment.rows, ROWS, "top", "left",
this.getFirstColLinePos(fragment));
}
},
@ -952,11 +1070,10 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
* @param {Number} endPos
* The end position of the cross side of the grid dimension.
*/
renderLines(gridDimension, {bounds}, dimensionType, mainSide, crossSide,
renderLines(gridDimension, dimensionType, mainSide, crossSide,
mainSize, startPos, endPos) {
let currentZoom = getCurrentZoom(this.win);
let lineStartPos = (bounds[crossSide] / currentZoom) + startPos;
let lineEndPos = (bounds[crossSide] / currentZoom) + endPos;
let lineStartPos = startPos;
let lineEndPos = endPos;
if (this.options.showInfiniteLines) {
lineStartPos = 0;
@ -967,7 +1084,7 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
for (let i = 0; i < gridDimension.lines.length; i++) {
let line = gridDimension.lines[i];
let linePos = (bounds[mainSide] / currentZoom) + line.start;
let linePos = line.start;
if (i == 0 || i == lastEdgeLineIndex) {
this.renderLine(linePos, lineStartPos, lineEndPos, dimensionType, "edge");
@ -992,17 +1109,13 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
*
* see @param for renderLines.
*/
renderLineNumbers(gridDimension, {bounds}, dimensionType, mainSide, crossSide,
renderLineNumbers(gridDimension, dimensionType, mainSide, crossSide,
startPos) {
let zoom = getCurrentZoom(this.win);
let lineStartPos = (bounds[crossSide] / zoom) + startPos;
if (this.options.showInfiniteLines) {
lineStartPos = 0;
}
let lineStartPos = startPos;
for (let i = 0; i < gridDimension.lines.length; i++) {
let line = gridDimension.lines[i];
let linePos = (bounds[mainSide] / zoom) + line.start;
let linePos = line.start;
this.renderGridLineNumber(line.number, linePos, lineStartPos, line.breadth,
dimensionType);
}
@ -1031,8 +1144,8 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
let x = Math.round(this._canvasPosition.x * devicePixelRatio);
let y = Math.round(this._canvasPosition.y * devicePixelRatio);
linePos = Math.round(linePos * devicePixelRatio);
startPos = Math.round(startPos * devicePixelRatio);
linePos = Math.round(linePos);
startPos = Math.round(startPos);
this.ctx.save();
this.ctx.setLineDash(GRID_LINES_PROPERTIES[lineType].lineDash);
@ -1041,13 +1154,21 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
this.ctx.lineWidth = lineWidth;
if (dimensionType === COLUMNS) {
endPos = isFinite(endPos) ? endPos * devicePixelRatio : CANVAS_SIZE + y;
this.ctx.moveTo(linePos, startPos);
this.ctx.lineTo(linePos, endPos);
if (isFinite(endPos)) {
endPos = Math.round(endPos);
} else {
endPos = CANVAS_INFINITY;
startPos = -endPos;
}
drawLine(this.ctx, linePos, startPos, linePos, endPos, this.currentMatrix);
} else {
endPos = isFinite(endPos) ? endPos * devicePixelRatio : CANVAS_SIZE + x;
this.ctx.moveTo(startPos, linePos);
this.ctx.lineTo(endPos, linePos);
if (isFinite(endPos)) {
endPos = Math.round(endPos);
} else {
endPos = CANVAS_INFINITY;
startPos = -endPos;
}
drawLine(this.ctx, startPos, linePos, endPos, linePos, this.currentMatrix);
}
this.ctx.strokeStyle = this.color;
@ -1073,12 +1194,13 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
* The grid dimension type which is either the constant COLUMNS or ROWS.
*/
renderGridLineNumber(lineNumber, linePos, startPos, breadth, dimensionType) {
let { devicePixelRatio } = this.win;
let displayPixelRatio = getDisplayPixelRatio(this.win);
let { devicePixelRatio } = this.win;
let offset = (displayPixelRatio / 2) % 1;
linePos = Math.round(linePos * devicePixelRatio);
startPos = Math.round(startPos * devicePixelRatio);
breadth = Math.round(breadth * devicePixelRatio);
linePos = Math.round(linePos);
startPos = Math.round(startPos);
breadth = Math.round(breadth);
if (linePos + breadth < 0) {
// The line is not visible on screen, don't render the line number
@ -1088,7 +1210,7 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
this.ctx.save();
let canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
let canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
this.ctx.translate(.5 - canvasX, .5 - canvasY);
this.ctx.translate(offset - canvasX, offset - canvasY);
let fontSize = (GRID_FONT_SIZE * displayPixelRatio);
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
@ -1107,26 +1229,32 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
// Calculate the x & y coordinates for the line number container, so that it is
// centered on the line, and in the middle of the gap if there is any.
let x, y;
if (dimensionType === COLUMNS) {
x = linePos - boxWidth / 2;
y = startPos - boxHeight / 2;
x += breadth / 2;
x = linePos + breadth / 2;
y = startPos;
} else {
x = startPos - boxWidth / 2;
y = linePos - boxHeight / 2;
y += breadth / 2;
x = startPos;
y = linePos + breadth / 2;
}
x = Math.max(x, padding);
y = Math.max(y, padding);
[x, y] = apply(this.currentMatrix, [x, y]);
// Draw a rounded rectangle with a border width of 4 pixels, a border color matching
x -= boxWidth / 2;
y -= boxHeight / 2;
if (!this.hasNodeTransformations) {
x = Math.max(x, padding);
y = Math.max(y, padding);
}
// Draw a rounded rectangle with a border width of 2 pixels, a border color matching
// the grid color and a white background (the line number will be written in black).
this.ctx.lineWidth = 2 * displayPixelRatio;
this.ctx.strokeStyle = this.color;
this.ctx.fillStyle = "white";
let radius = 2 * displayPixelRatio;
roundedRect(this.ctx, x, y, boxWidth, boxHeight, radius);
drawRoundedRect(this.ctx, x, y, boxWidth, boxHeight, radius);
// Write the line number inside of the rectangle.
this.ctx.fillStyle = "black";
@ -1152,24 +1280,40 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
*/
renderGridGap(linePos, startPos, endPos, breadth, dimensionType) {
let { devicePixelRatio } = this.win;
let x = Math.round(this._canvasPosition.x * devicePixelRatio);
let y = Math.round(this._canvasPosition.y * devicePixelRatio);
let displayPixelRatio = getDisplayPixelRatio(this.win);
let offset = (displayPixelRatio / 2) % 1;
linePos = Math.round(linePos * devicePixelRatio);
startPos = Math.round(startPos * devicePixelRatio);
breadth = Math.round(breadth * devicePixelRatio);
let canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
let canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
linePos = Math.round(linePos);
startPos = Math.round(startPos);
breadth = Math.round(breadth);
this.ctx.save();
this.ctx.fillStyle = this.getGridGapPattern(devicePixelRatio, dimensionType);
this.ctx.translate(.5 - x, .5 - y);
this.ctx.translate(offset - canvasX, offset - canvasY);
if (dimensionType === COLUMNS) {
endPos = isFinite(endPos) ? Math.round(endPos * devicePixelRatio) : CANVAS_SIZE + y;
this.ctx.fillRect(linePos, startPos, breadth, endPos - startPos);
if (isFinite(endPos)) {
endPos = Math.round(endPos);
} else {
endPos = this._winDimensions.height;
startPos = -endPos;
}
drawRect(this.ctx, linePos, startPos, linePos + breadth, endPos,
this.currentMatrix);
} else {
endPos = isFinite(endPos) ? Math.round(endPos * devicePixelRatio) : CANVAS_SIZE + x;
this.ctx.fillRect(startPos, linePos, endPos - startPos, breadth);
if (isFinite(endPos)) {
endPos = Math.round(endPos);
} else {
endPos = this._winDimensions.width;
startPos = -endPos;
}
drawRect(this.ctx, startPos, linePos, endPos, linePos + breadth,
this.currentMatrix);
}
this.ctx.fill();
this.ctx.restore();
},

Просмотреть файл

@ -357,11 +357,8 @@ function getNodeBounds(boundaryWindow, node) {
// And add the potential frame offset if the node is nested
let [xOffset, yOffset] = getFrameOffsets(boundaryWindow, node);
xOffset += offsetLeft + scrollX;
yOffset += offsetTop + scrollY;
xOffset *= scale;
yOffset *= scale;
xOffset += (offsetLeft + scrollX) * scale;
yOffset += (offsetTop + scrollY) * scale;
// Get the width and height
let width = node.offsetWidth * scale;
@ -371,7 +368,13 @@ function getNodeBounds(boundaryWindow, node) {
p1: {x: xOffset, y: yOffset},
p2: {x: xOffset + width, y: yOffset},
p3: {x: xOffset + width, y: yOffset + height},
p4: {x: xOffset, y: yOffset + height}
p4: {x: xOffset, y: yOffset + height},
top: yOffset,
right: xOffset + width,
bottom: yOffset + height,
left: xOffset,
width,
height
};
}
exports.getNodeBounds = getNodeBounds;