Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
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 | }; |