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