browser/devtools/shared/widgets/CSSTransformPreviewer.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/shared/widgets/CSSTransformPreviewer.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,389 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +/**
    1.11 + * The CSSTransformPreview module displays, using a <canvas> a rectangle, with
    1.12 + * a given width and height and its transformed version, given a css transform
    1.13 + * property and origin. It also displays arrows from/to each corner.
    1.14 + *
    1.15 + * It is useful to visualize how a css transform affected an element. It can
    1.16 + * help debug tricky transformations. It is used today in a tooltip, and this
    1.17 + * tooltip is shown when hovering over a css transform declaration in the rule
    1.18 + * and computed view panels.
    1.19 + *
    1.20 + * TODO: For now, it multiplies matrices itself to calculate the coordinates of
    1.21 + * the transformed box, but that should be removed as soon as we can get access
    1.22 + * to getQuads().
    1.23 + */
    1.24 +
    1.25 +const HTML_NS = "http://www.w3.org/1999/xhtml";
    1.26 +
    1.27 +/**
    1.28 + * The TransformPreview needs an element to output a canvas tag.
    1.29 + *
    1.30 + * Usage example:
    1.31 + *
    1.32 + * let t = new CSSTransformPreviewer(myRootElement);
    1.33 + * t.preview("rotate(45deg)", "top left", 200, 400);
    1.34 + * t.preview("skew(19deg)", "center", 100, 500);
    1.35 + * t.preview("matrix(1, -0.2, 0, 1, 0, 0)");
    1.36 + * t.destroy();
    1.37 + *
    1.38 + * @param {nsIDOMElement} parentEl
    1.39 + *        Where the canvas will go
    1.40 + */
    1.41 +function CSSTransformPreviewer(parentEl) {
    1.42 +  this.parentEl = parentEl;
    1.43 +  this.doc = this.parentEl.ownerDocument;
    1.44 +  this.canvas = null;
    1.45 +  this.ctx = null;
    1.46 +}
    1.47 +
    1.48 +module.exports.CSSTransformPreviewer = CSSTransformPreviewer;
    1.49 +
    1.50 +CSSTransformPreviewer.prototype = {
    1.51 +  /**
    1.52 +   * The preview look-and-feel can be changed using these properties
    1.53 +   */
    1.54 +  MAX_DIM: 250,
    1.55 +  PAD: 5,
    1.56 +  ORIGINAL_FILL: "#1F303F",
    1.57 +  ORIGINAL_STROKE: "#B2D8FF",
    1.58 +  TRANSFORMED_FILL: "rgba(200, 200, 200, .5)",
    1.59 +  TRANSFORMED_STROKE: "#B2D8FF",
    1.60 +  ARROW_STROKE: "#329AFF",
    1.61 +  ORIGIN_STROKE: "#329AFF",
    1.62 +  ARROW_TIP_HEIGHT: 10,
    1.63 +  ARROW_TIP_WIDTH: 8,
    1.64 +  CORNER_SIZE_RATIO: 6,
    1.65 +
    1.66 +  /**
    1.67 +   * Destroy removes the canvas from the parentelement passed in the constructor
    1.68 +   */
    1.69 +  destroy: function() {
    1.70 +    if (this.canvas) {
    1.71 +      this.parentEl.removeChild(this.canvas);
    1.72 +    }
    1.73 +    if (this._hiddenDiv) {
    1.74 +      this.parentEl.removeChild(this._hiddenDiv);
    1.75 +    }
    1.76 +    this.parentEl = this.canvas = this.ctx = this.doc = null;
    1.77 +  },
    1.78 +
    1.79 +  _createMarkup: function() {
    1.80 +    this.canvas = this.doc.createElementNS(HTML_NS, "canvas");
    1.81 +
    1.82 +    this.canvas.setAttribute("id", "canvas");
    1.83 +    this.canvas.setAttribute("width", this.MAX_DIM);
    1.84 +    this.canvas.setAttribute("height", this.MAX_DIM);
    1.85 +    this.canvas.style.position = "relative";
    1.86 +    this.parentEl.appendChild(this.canvas);
    1.87 +
    1.88 +    this.ctx = this.canvas.getContext("2d");
    1.89 +  },
    1.90 +
    1.91 +  _getComputed: function(name, value, width, height) {
    1.92 +    if (!this._hiddenDiv) {
    1.93 +      // Create a hidden element to apply the style to
    1.94 +      this._hiddenDiv = this.doc.createElementNS(HTML_NS, "div");
    1.95 +      this._hiddenDiv.style.visibility = "hidden";
    1.96 +      this._hiddenDiv.style.position = "absolute";
    1.97 +      this.parentEl.appendChild(this._hiddenDiv);
    1.98 +    }
    1.99 +
   1.100 +    // Camelcase the name
   1.101 +    name = name.replace(/-([a-z]{1})/g, (m, letter) => letter.toUpperCase());
   1.102 +
   1.103 +    // Apply width and height to make sure computation is made correctly
   1.104 +    this._hiddenDiv.style.width = width + "px";
   1.105 +    this._hiddenDiv.style.height = height + "px";
   1.106 +
   1.107 +    // Show the hidden div, apply the style, read the computed style, hide the
   1.108 +    // hidden div again
   1.109 +    this._hiddenDiv.style.display = "block";
   1.110 +    this._hiddenDiv.style[name] = value;
   1.111 +    let computed = this.doc.defaultView.getComputedStyle(this._hiddenDiv);
   1.112 +    let computedValue = computed[name];
   1.113 +    this._hiddenDiv.style.display = "none";
   1.114 +
   1.115 +    return computedValue;
   1.116 +  },
   1.117 +
   1.118 +  _getMatrixFromTransformString: function(transformStr) {
   1.119 +    let matrix = transformStr.substring(0, transformStr.length - 1).
   1.120 +      substring(transformStr.indexOf("(") + 1).split(",");
   1.121 +
   1.122 +    matrix.forEach(function(value, index) {
   1.123 +      matrix[index] = parseFloat(value, 10);
   1.124 +    });
   1.125 +
   1.126 +    let transformMatrix = null;
   1.127 +
   1.128 +    if (matrix.length === 6) {
   1.129 +      // 2d transform
   1.130 +      transformMatrix = [
   1.131 +        [matrix[0], matrix[2], matrix[4], 0],
   1.132 +        [matrix[1], matrix[3], matrix[5], 0],
   1.133 +        [0,     0,     1,     0],
   1.134 +        [0,     0,     0,     1]
   1.135 +      ];
   1.136 +    } else {
   1.137 +      // 3d transform
   1.138 +      transformMatrix = [
   1.139 +        [matrix[0], matrix[4], matrix[8],  matrix[12]],
   1.140 +        [matrix[1], matrix[5], matrix[9],  matrix[13]],
   1.141 +        [matrix[2], matrix[6], matrix[10], matrix[14]],
   1.142 +        [matrix[3], matrix[7], matrix[11], matrix[15]]
   1.143 +      ];
   1.144 +    }
   1.145 +
   1.146 +    return transformMatrix;
   1.147 +  },
   1.148 +
   1.149 +  _getOriginFromOriginString: function(originStr) {
   1.150 +    let offsets = originStr.split(" ");
   1.151 +    offsets.forEach(function(item, index) {
   1.152 +      offsets[index] = parseInt(item, 10);
   1.153 +    });
   1.154 +
   1.155 +    return offsets;
   1.156 +  },
   1.157 +
   1.158 +  _multiply: function(m1, m2) {
   1.159 +    let m = [];
   1.160 +    for (let m1Line = 0; m1Line < m1.length; m1Line++) {
   1.161 +      m[m1Line] = 0;
   1.162 +      for (let m2Col = 0; m2Col < m2.length; m2Col++) {
   1.163 +        m[m1Line] += m1[m1Line][m2Col] * m2[m2Col];
   1.164 +      }
   1.165 +    }
   1.166 +    return [m[0], m[1]];
   1.167 +  },
   1.168 +
   1.169 +  _getTransformedPoint: function(matrix, point, origin) {
   1.170 +    let pointMatrix = [point[0] - origin[0], point[1] - origin[1], 1, 1];
   1.171 +    return this._multiply(matrix, pointMatrix);
   1.172 +  },
   1.173 +
   1.174 +  _getTransformedPoints: function(matrix, rect, origin) {
   1.175 +    return rect.map(point => {
   1.176 +      let tPoint = this._getTransformedPoint(matrix, [point[0], point[1]], origin);
   1.177 +      return [tPoint[0] + origin[0], tPoint[1] + origin[1]];
   1.178 +    });
   1.179 +  },
   1.180 +
   1.181 +  /**
   1.182 +   * For canvas to avoid anti-aliasing
   1.183 +   */
   1.184 +  _round: x => Math.round(x) + .5,
   1.185 +
   1.186 +  _drawShape: function(points, fillStyle, strokeStyle) {
   1.187 +    this.ctx.save();
   1.188 +
   1.189 +    this.ctx.lineWidth = 1;
   1.190 +    this.ctx.strokeStyle = strokeStyle;
   1.191 +    this.ctx.fillStyle = fillStyle;
   1.192 +
   1.193 +    this.ctx.beginPath();
   1.194 +    this.ctx.moveTo(this._round(points[0][0]), this._round(points[0][1]));
   1.195 +    for (var i = 1; i < points.length; i++) {
   1.196 +      this.ctx.lineTo(this._round(points[i][0]), this._round(points[i][1]));
   1.197 +    }
   1.198 +    this.ctx.lineTo(this._round(points[0][0]), this._round(points[0][1]));
   1.199 +    this.ctx.fill();
   1.200 +    this.ctx.stroke();
   1.201 +
   1.202 +    this.ctx.restore();
   1.203 +  },
   1.204 +
   1.205 +  _drawArrow: function(x1, y1, x2, y2) {
   1.206 +    // do not draw if the line is too small
   1.207 +    if (Math.abs(x2-x1) < 20 && Math.abs(y2-y1) < 20) {
   1.208 +      return;
   1.209 +    }
   1.210 +
   1.211 +    this.ctx.save();
   1.212 +
   1.213 +    this.ctx.strokeStyle = this.ARROW_STROKE;
   1.214 +    this.ctx.fillStyle = this.ARROW_STROKE;
   1.215 +    this.ctx.lineWidth = 1;
   1.216 +
   1.217 +    this.ctx.beginPath();
   1.218 +    this.ctx.moveTo(this._round(x1), this._round(y1));
   1.219 +    this.ctx.lineTo(this._round(x2), this._round(y2));
   1.220 +    this.ctx.stroke();
   1.221 +
   1.222 +    this.ctx.beginPath();
   1.223 +    this.ctx.translate(x2, y2);
   1.224 +    let radians = Math.atan((y1 - y2) / (x1 - x2));
   1.225 +    radians += ((x1 >= x2) ? -90 : 90) * Math.PI / 180;
   1.226 +    this.ctx.rotate(radians);
   1.227 +    this.ctx.moveTo(0, 0);
   1.228 +    this.ctx.lineTo(this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
   1.229 +    this.ctx.lineTo(-this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
   1.230 +    this.ctx.closePath();
   1.231 +    this.ctx.fill();
   1.232 +
   1.233 +    this.ctx.restore();
   1.234 +  },
   1.235 +
   1.236 +  _drawOrigin: function(x, y) {
   1.237 +    this.ctx.save();
   1.238 +
   1.239 +    this.ctx.strokeStyle = this.ORIGIN_STROKE;
   1.240 +    this.ctx.fillStyle = this.ORIGIN_STROKE;
   1.241 +
   1.242 +    this.ctx.beginPath();
   1.243 +    this.ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
   1.244 +    this.ctx.stroke();
   1.245 +    this.ctx.fill();
   1.246 +
   1.247 +    this.ctx.restore();
   1.248 +  },
   1.249 +
   1.250 +  /**
   1.251 +   * Computes the largest width and height of all the given shapes and changes
   1.252 +   * all of the shapes' points (by reference) so they fit into the configured
   1.253 +   * MAX_DIM - 2*PAD area.
   1.254 +   * @return {Object} A {w, h} giving the size the canvas should be
   1.255 +   */
   1.256 +  _fitAllShapes: function(allShapes) {
   1.257 +    let allXs = [], allYs = [];
   1.258 +    for (let shape of allShapes) {
   1.259 +      for (let point of shape) {
   1.260 +        allXs.push(point[0]);
   1.261 +        allYs.push(point[1]);
   1.262 +      }
   1.263 +    }
   1.264 +    let minX = Math.min.apply(Math, allXs);
   1.265 +    let maxX = Math.max.apply(Math, allXs);
   1.266 +    let minY = Math.min.apply(Math, allYs);
   1.267 +    let maxY = Math.max.apply(Math, allYs);
   1.268 +
   1.269 +    let spanX = maxX - minX;
   1.270 +    let spanY = maxY - minY;
   1.271 +    let isWide = spanX > spanY;
   1.272 +
   1.273 +    let cw = isWide ? this.MAX_DIM :
   1.274 +      this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
   1.275 +    let ch = !isWide ? this.MAX_DIM :
   1.276 +      this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
   1.277 +
   1.278 +    let mapX = x => this.PAD + ((cw - 2 * this.PAD) / (maxX - minX)) * (x - minX);
   1.279 +    let mapY = y => this.PAD + ((ch - 2 * this.PAD) / (maxY - minY)) * (y - minY);
   1.280 +
   1.281 +    for (let shape of allShapes) {
   1.282 +      for (let point of shape) {
   1.283 +        point[0] = mapX(point[0]);
   1.284 +        point[1] = mapY(point[1]);
   1.285 +      }
   1.286 +    }
   1.287 +
   1.288 +    return {w: cw, h: ch};
   1.289 +  },
   1.290 +
   1.291 +  _drawShapes: function(shape, corner, transformed, transformedCorner) {
   1.292 +    this._drawOriginal(shape);
   1.293 +    this._drawOriginalCorner(corner);
   1.294 +    this._drawTransformed(transformed);
   1.295 +    this._drawTransformedCorner(transformedCorner);
   1.296 +  },
   1.297 +
   1.298 +  _drawOriginal: function(points) {
   1.299 +    this._drawShape(points, this.ORIGINAL_FILL, this.ORIGINAL_STROKE);
   1.300 +  },
   1.301 +
   1.302 +  _drawTransformed: function(points) {
   1.303 +    this._drawShape(points, this.TRANSFORMED_FILL, this.TRANSFORMED_STROKE);
   1.304 +  },
   1.305 +
   1.306 +  _drawOriginalCorner: function(points) {
   1.307 +    this._drawShape(points, this.ORIGINAL_STROKE, this.ORIGINAL_STROKE);
   1.308 +  },
   1.309 +
   1.310 +  _drawTransformedCorner: function(points) {
   1.311 +    this._drawShape(points, this.TRANSFORMED_STROKE, this.TRANSFORMED_STROKE);
   1.312 +  },
   1.313 +
   1.314 +  _drawArrows: function(shape, transformed) {
   1.315 +    this._drawArrow(shape[0][0], shape[0][1], transformed[0][0], transformed[0][1]);
   1.316 +    this._drawArrow(shape[1][0], shape[1][1], transformed[1][0], transformed[1][1]);
   1.317 +    this._drawArrow(shape[2][0], shape[2][1], transformed[2][0], transformed[2][1]);
   1.318 +    this._drawArrow(shape[3][0], shape[3][1], transformed[3][0], transformed[3][1]);
   1.319 +  },
   1.320 +
   1.321 +  /**
   1.322 +   * Draw a transform preview
   1.323 +   *
   1.324 +   * @param {String} transform
   1.325 +   *        The css transform value as a string, as typed by the user, as long
   1.326 +   *        as it can be computed by the browser
   1.327 +   * @param {String} origin
   1.328 +   *        Same as above for the transform-origin value. Defaults to "center"
   1.329 +   * @param {Number} width
   1.330 +   *        The width of the container. Defaults to 200
   1.331 +   * @param {Number} height
   1.332 +   *        The height of the container. Defaults to 200
   1.333 +   * @return {Boolean} Whether or not the preview could be created. Will return
   1.334 +   *         false for instance if the transform is invalid
   1.335 +   */
   1.336 +  preview: function(transform, origin="center", width=200, height=200) {
   1.337 +    // Create/clear the canvas
   1.338 +    if (!this.canvas) {
   1.339 +      this._createMarkup();
   1.340 +    }
   1.341 +    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
   1.342 +
   1.343 +    // Get computed versions of transform and origin
   1.344 +    transform = this._getComputed("transform", transform, width, height);
   1.345 +    if (transform && transform !== "none") {
   1.346 +      origin = this._getComputed("transform-origin", origin, width, height);
   1.347 +
   1.348 +      // Get the matrix, origin and width height data for the previewed element
   1.349 +      let originData = this._getOriginFromOriginString(origin);
   1.350 +      let matrixData = this._getMatrixFromTransformString(transform);
   1.351 +
   1.352 +      // Compute the original box rect and transformed box rect
   1.353 +      let shapePoints = [
   1.354 +        [0, 0],
   1.355 +        [width, 0],
   1.356 +        [width, height],
   1.357 +        [0, height]
   1.358 +      ];
   1.359 +      let transformedPoints = this._getTransformedPoints(matrixData, shapePoints, originData);
   1.360 +
   1.361 +      // Do the same for the corner triangle shape
   1.362 +      let cornerSize = Math.min(shapePoints[2][1] - shapePoints[1][1],
   1.363 +        shapePoints[1][0] - shapePoints[0][0]) / this.CORNER_SIZE_RATIO;
   1.364 +      let cornerPoints = [
   1.365 +        [shapePoints[1][0], shapePoints[1][1]],
   1.366 +        [shapePoints[1][0], shapePoints[1][1] + cornerSize],
   1.367 +        [shapePoints[1][0] - cornerSize, shapePoints[1][1]]
   1.368 +      ];
   1.369 +      let transformedCornerPoints = this._getTransformedPoints(matrixData, cornerPoints, originData);
   1.370 +
   1.371 +      // Resize points to fit everything in the canvas
   1.372 +      let {w, h} = this._fitAllShapes([
   1.373 +        shapePoints,
   1.374 +        transformedPoints,
   1.375 +        cornerPoints,
   1.376 +        transformedCornerPoints,
   1.377 +        [originData]
   1.378 +      ]);
   1.379 +
   1.380 +      this.canvas.setAttribute("width", w);
   1.381 +      this.canvas.setAttribute("height", h);
   1.382 +
   1.383 +      this._drawShapes(shapePoints, cornerPoints, transformedPoints, transformedCornerPoints)
   1.384 +      this._drawArrows(shapePoints, transformedPoints);
   1.385 +      this._drawOrigin(originData[0], originData[1]);
   1.386 +
   1.387 +      return true;
   1.388 +    } else {
   1.389 +      return false;
   1.390 +    }
   1.391 +  }
   1.392 +};

mercurial