browser/devtools/shared/widgets/CSSTransformPreviewer.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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/. */
     5 "use strict";
     7 /**
     8  * The CSSTransformPreview module displays, using a <canvas> a rectangle, with
     9  * a given width and height and its transformed version, given a css transform
    10  * property and origin. It also displays arrows from/to each corner.
    11  *
    12  * It is useful to visualize how a css transform affected an element. It can
    13  * help debug tricky transformations. It is used today in a tooltip, and this
    14  * tooltip is shown when hovering over a css transform declaration in the rule
    15  * and computed view panels.
    16  *
    17  * TODO: For now, it multiplies matrices itself to calculate the coordinates of
    18  * the transformed box, but that should be removed as soon as we can get access
    19  * to getQuads().
    20  */
    22 const HTML_NS = "http://www.w3.org/1999/xhtml";
    24 /**
    25  * The TransformPreview needs an element to output a canvas tag.
    26  *
    27  * Usage example:
    28  *
    29  * let t = new CSSTransformPreviewer(myRootElement);
    30  * t.preview("rotate(45deg)", "top left", 200, 400);
    31  * t.preview("skew(19deg)", "center", 100, 500);
    32  * t.preview("matrix(1, -0.2, 0, 1, 0, 0)");
    33  * t.destroy();
    34  *
    35  * @param {nsIDOMElement} parentEl
    36  *        Where the canvas will go
    37  */
    38 function CSSTransformPreviewer(parentEl) {
    39   this.parentEl = parentEl;
    40   this.doc = this.parentEl.ownerDocument;
    41   this.canvas = null;
    42   this.ctx = null;
    43 }
    45 module.exports.CSSTransformPreviewer = CSSTransformPreviewer;
    47 CSSTransformPreviewer.prototype = {
    48   /**
    49    * The preview look-and-feel can be changed using these properties
    50    */
    51   MAX_DIM: 250,
    52   PAD: 5,
    53   ORIGINAL_FILL: "#1F303F",
    54   ORIGINAL_STROKE: "#B2D8FF",
    55   TRANSFORMED_FILL: "rgba(200, 200, 200, .5)",
    56   TRANSFORMED_STROKE: "#B2D8FF",
    57   ARROW_STROKE: "#329AFF",
    58   ORIGIN_STROKE: "#329AFF",
    59   ARROW_TIP_HEIGHT: 10,
    60   ARROW_TIP_WIDTH: 8,
    61   CORNER_SIZE_RATIO: 6,
    63   /**
    64    * Destroy removes the canvas from the parentelement passed in the constructor
    65    */
    66   destroy: function() {
    67     if (this.canvas) {
    68       this.parentEl.removeChild(this.canvas);
    69     }
    70     if (this._hiddenDiv) {
    71       this.parentEl.removeChild(this._hiddenDiv);
    72     }
    73     this.parentEl = this.canvas = this.ctx = this.doc = null;
    74   },
    76   _createMarkup: function() {
    77     this.canvas = this.doc.createElementNS(HTML_NS, "canvas");
    79     this.canvas.setAttribute("id", "canvas");
    80     this.canvas.setAttribute("width", this.MAX_DIM);
    81     this.canvas.setAttribute("height", this.MAX_DIM);
    82     this.canvas.style.position = "relative";
    83     this.parentEl.appendChild(this.canvas);
    85     this.ctx = this.canvas.getContext("2d");
    86   },
    88   _getComputed: function(name, value, width, height) {
    89     if (!this._hiddenDiv) {
    90       // Create a hidden element to apply the style to
    91       this._hiddenDiv = this.doc.createElementNS(HTML_NS, "div");
    92       this._hiddenDiv.style.visibility = "hidden";
    93       this._hiddenDiv.style.position = "absolute";
    94       this.parentEl.appendChild(this._hiddenDiv);
    95     }
    97     // Camelcase the name
    98     name = name.replace(/-([a-z]{1})/g, (m, letter) => letter.toUpperCase());
   100     // Apply width and height to make sure computation is made correctly
   101     this._hiddenDiv.style.width = width + "px";
   102     this._hiddenDiv.style.height = height + "px";
   104     // Show the hidden div, apply the style, read the computed style, hide the
   105     // hidden div again
   106     this._hiddenDiv.style.display = "block";
   107     this._hiddenDiv.style[name] = value;
   108     let computed = this.doc.defaultView.getComputedStyle(this._hiddenDiv);
   109     let computedValue = computed[name];
   110     this._hiddenDiv.style.display = "none";
   112     return computedValue;
   113   },
   115   _getMatrixFromTransformString: function(transformStr) {
   116     let matrix = transformStr.substring(0, transformStr.length - 1).
   117       substring(transformStr.indexOf("(") + 1).split(",");
   119     matrix.forEach(function(value, index) {
   120       matrix[index] = parseFloat(value, 10);
   121     });
   123     let transformMatrix = null;
   125     if (matrix.length === 6) {
   126       // 2d transform
   127       transformMatrix = [
   128         [matrix[0], matrix[2], matrix[4], 0],
   129         [matrix[1], matrix[3], matrix[5], 0],
   130         [0,     0,     1,     0],
   131         [0,     0,     0,     1]
   132       ];
   133     } else {
   134       // 3d transform
   135       transformMatrix = [
   136         [matrix[0], matrix[4], matrix[8],  matrix[12]],
   137         [matrix[1], matrix[5], matrix[9],  matrix[13]],
   138         [matrix[2], matrix[6], matrix[10], matrix[14]],
   139         [matrix[3], matrix[7], matrix[11], matrix[15]]
   140       ];
   141     }
   143     return transformMatrix;
   144   },
   146   _getOriginFromOriginString: function(originStr) {
   147     let offsets = originStr.split(" ");
   148     offsets.forEach(function(item, index) {
   149       offsets[index] = parseInt(item, 10);
   150     });
   152     return offsets;
   153   },
   155   _multiply: function(m1, m2) {
   156     let m = [];
   157     for (let m1Line = 0; m1Line < m1.length; m1Line++) {
   158       m[m1Line] = 0;
   159       for (let m2Col = 0; m2Col < m2.length; m2Col++) {
   160         m[m1Line] += m1[m1Line][m2Col] * m2[m2Col];
   161       }
   162     }
   163     return [m[0], m[1]];
   164   },
   166   _getTransformedPoint: function(matrix, point, origin) {
   167     let pointMatrix = [point[0] - origin[0], point[1] - origin[1], 1, 1];
   168     return this._multiply(matrix, pointMatrix);
   169   },
   171   _getTransformedPoints: function(matrix, rect, origin) {
   172     return rect.map(point => {
   173       let tPoint = this._getTransformedPoint(matrix, [point[0], point[1]], origin);
   174       return [tPoint[0] + origin[0], tPoint[1] + origin[1]];
   175     });
   176   },
   178   /**
   179    * For canvas to avoid anti-aliasing
   180    */
   181   _round: x => Math.round(x) + .5,
   183   _drawShape: function(points, fillStyle, strokeStyle) {
   184     this.ctx.save();
   186     this.ctx.lineWidth = 1;
   187     this.ctx.strokeStyle = strokeStyle;
   188     this.ctx.fillStyle = fillStyle;
   190     this.ctx.beginPath();
   191     this.ctx.moveTo(this._round(points[0][0]), this._round(points[0][1]));
   192     for (var i = 1; i < points.length; i++) {
   193       this.ctx.lineTo(this._round(points[i][0]), this._round(points[i][1]));
   194     }
   195     this.ctx.lineTo(this._round(points[0][0]), this._round(points[0][1]));
   196     this.ctx.fill();
   197     this.ctx.stroke();
   199     this.ctx.restore();
   200   },
   202   _drawArrow: function(x1, y1, x2, y2) {
   203     // do not draw if the line is too small
   204     if (Math.abs(x2-x1) < 20 && Math.abs(y2-y1) < 20) {
   205       return;
   206     }
   208     this.ctx.save();
   210     this.ctx.strokeStyle = this.ARROW_STROKE;
   211     this.ctx.fillStyle = this.ARROW_STROKE;
   212     this.ctx.lineWidth = 1;
   214     this.ctx.beginPath();
   215     this.ctx.moveTo(this._round(x1), this._round(y1));
   216     this.ctx.lineTo(this._round(x2), this._round(y2));
   217     this.ctx.stroke();
   219     this.ctx.beginPath();
   220     this.ctx.translate(x2, y2);
   221     let radians = Math.atan((y1 - y2) / (x1 - x2));
   222     radians += ((x1 >= x2) ? -90 : 90) * Math.PI / 180;
   223     this.ctx.rotate(radians);
   224     this.ctx.moveTo(0, 0);
   225     this.ctx.lineTo(this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
   226     this.ctx.lineTo(-this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
   227     this.ctx.closePath();
   228     this.ctx.fill();
   230     this.ctx.restore();
   231   },
   233   _drawOrigin: function(x, y) {
   234     this.ctx.save();
   236     this.ctx.strokeStyle = this.ORIGIN_STROKE;
   237     this.ctx.fillStyle = this.ORIGIN_STROKE;
   239     this.ctx.beginPath();
   240     this.ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
   241     this.ctx.stroke();
   242     this.ctx.fill();
   244     this.ctx.restore();
   245   },
   247   /**
   248    * Computes the largest width and height of all the given shapes and changes
   249    * all of the shapes' points (by reference) so they fit into the configured
   250    * MAX_DIM - 2*PAD area.
   251    * @return {Object} A {w, h} giving the size the canvas should be
   252    */
   253   _fitAllShapes: function(allShapes) {
   254     let allXs = [], allYs = [];
   255     for (let shape of allShapes) {
   256       for (let point of shape) {
   257         allXs.push(point[0]);
   258         allYs.push(point[1]);
   259       }
   260     }
   261     let minX = Math.min.apply(Math, allXs);
   262     let maxX = Math.max.apply(Math, allXs);
   263     let minY = Math.min.apply(Math, allYs);
   264     let maxY = Math.max.apply(Math, allYs);
   266     let spanX = maxX - minX;
   267     let spanY = maxY - minY;
   268     let isWide = spanX > spanY;
   270     let cw = isWide ? this.MAX_DIM :
   271       this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
   272     let ch = !isWide ? this.MAX_DIM :
   273       this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
   275     let mapX = x => this.PAD + ((cw - 2 * this.PAD) / (maxX - minX)) * (x - minX);
   276     let mapY = y => this.PAD + ((ch - 2 * this.PAD) / (maxY - minY)) * (y - minY);
   278     for (let shape of allShapes) {
   279       for (let point of shape) {
   280         point[0] = mapX(point[0]);
   281         point[1] = mapY(point[1]);
   282       }
   283     }
   285     return {w: cw, h: ch};
   286   },
   288   _drawShapes: function(shape, corner, transformed, transformedCorner) {
   289     this._drawOriginal(shape);
   290     this._drawOriginalCorner(corner);
   291     this._drawTransformed(transformed);
   292     this._drawTransformedCorner(transformedCorner);
   293   },
   295   _drawOriginal: function(points) {
   296     this._drawShape(points, this.ORIGINAL_FILL, this.ORIGINAL_STROKE);
   297   },
   299   _drawTransformed: function(points) {
   300     this._drawShape(points, this.TRANSFORMED_FILL, this.TRANSFORMED_STROKE);
   301   },
   303   _drawOriginalCorner: function(points) {
   304     this._drawShape(points, this.ORIGINAL_STROKE, this.ORIGINAL_STROKE);
   305   },
   307   _drawTransformedCorner: function(points) {
   308     this._drawShape(points, this.TRANSFORMED_STROKE, this.TRANSFORMED_STROKE);
   309   },
   311   _drawArrows: function(shape, transformed) {
   312     this._drawArrow(shape[0][0], shape[0][1], transformed[0][0], transformed[0][1]);
   313     this._drawArrow(shape[1][0], shape[1][1], transformed[1][0], transformed[1][1]);
   314     this._drawArrow(shape[2][0], shape[2][1], transformed[2][0], transformed[2][1]);
   315     this._drawArrow(shape[3][0], shape[3][1], transformed[3][0], transformed[3][1]);
   316   },
   318   /**
   319    * Draw a transform preview
   320    *
   321    * @param {String} transform
   322    *        The css transform value as a string, as typed by the user, as long
   323    *        as it can be computed by the browser
   324    * @param {String} origin
   325    *        Same as above for the transform-origin value. Defaults to "center"
   326    * @param {Number} width
   327    *        The width of the container. Defaults to 200
   328    * @param {Number} height
   329    *        The height of the container. Defaults to 200
   330    * @return {Boolean} Whether or not the preview could be created. Will return
   331    *         false for instance if the transform is invalid
   332    */
   333   preview: function(transform, origin="center", width=200, height=200) {
   334     // Create/clear the canvas
   335     if (!this.canvas) {
   336       this._createMarkup();
   337     }
   338     this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
   340     // Get computed versions of transform and origin
   341     transform = this._getComputed("transform", transform, width, height);
   342     if (transform && transform !== "none") {
   343       origin = this._getComputed("transform-origin", origin, width, height);
   345       // Get the matrix, origin and width height data for the previewed element
   346       let originData = this._getOriginFromOriginString(origin);
   347       let matrixData = this._getMatrixFromTransformString(transform);
   349       // Compute the original box rect and transformed box rect
   350       let shapePoints = [
   351         [0, 0],
   352         [width, 0],
   353         [width, height],
   354         [0, height]
   355       ];
   356       let transformedPoints = this._getTransformedPoints(matrixData, shapePoints, originData);
   358       // Do the same for the corner triangle shape
   359       let cornerSize = Math.min(shapePoints[2][1] - shapePoints[1][1],
   360         shapePoints[1][0] - shapePoints[0][0]) / this.CORNER_SIZE_RATIO;
   361       let cornerPoints = [
   362         [shapePoints[1][0], shapePoints[1][1]],
   363         [shapePoints[1][0], shapePoints[1][1] + cornerSize],
   364         [shapePoints[1][0] - cornerSize, shapePoints[1][1]]
   365       ];
   366       let transformedCornerPoints = this._getTransformedPoints(matrixData, cornerPoints, originData);
   368       // Resize points to fit everything in the canvas
   369       let {w, h} = this._fitAllShapes([
   370         shapePoints,
   371         transformedPoints,
   372         cornerPoints,
   373         transformedCornerPoints,
   374         [originData]
   375       ]);
   377       this.canvas.setAttribute("width", w);
   378       this.canvas.setAttribute("height", h);
   380       this._drawShapes(shapePoints, cornerPoints, transformedPoints, transformedCornerPoints)
   381       this._drawArrows(shapePoints, transformedPoints);
   382       this._drawOrigin(originData[0], originData[1]);
   384       return true;
   385     } else {
   386       return false;
   387     }
   388   }
   389 };

mercurial