browser/devtools/shared/widgets/CSSTransformPreviewer.js

Thu, 15 Jan 2015 15:55:04 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 15:55:04 +0100
branch
TOR_BUG_9701
changeset 9
a63d609f5ebe
permissions
-rw-r--r--

Back out 97036ab72558 which inappropriately compared turds to third parties.

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

mercurial