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 +};