|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
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 */ |
|
21 |
|
22 const HTML_NS = "http://www.w3.org/1999/xhtml"; |
|
23 |
|
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 } |
|
44 |
|
45 module.exports.CSSTransformPreviewer = CSSTransformPreviewer; |
|
46 |
|
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, |
|
62 |
|
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 }, |
|
75 |
|
76 _createMarkup: function() { |
|
77 this.canvas = this.doc.createElementNS(HTML_NS, "canvas"); |
|
78 |
|
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); |
|
84 |
|
85 this.ctx = this.canvas.getContext("2d"); |
|
86 }, |
|
87 |
|
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 } |
|
96 |
|
97 // Camelcase the name |
|
98 name = name.replace(/-([a-z]{1})/g, (m, letter) => letter.toUpperCase()); |
|
99 |
|
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"; |
|
103 |
|
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"; |
|
111 |
|
112 return computedValue; |
|
113 }, |
|
114 |
|
115 _getMatrixFromTransformString: function(transformStr) { |
|
116 let matrix = transformStr.substring(0, transformStr.length - 1). |
|
117 substring(transformStr.indexOf("(") + 1).split(","); |
|
118 |
|
119 matrix.forEach(function(value, index) { |
|
120 matrix[index] = parseFloat(value, 10); |
|
121 }); |
|
122 |
|
123 let transformMatrix = null; |
|
124 |
|
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 } |
|
142 |
|
143 return transformMatrix; |
|
144 }, |
|
145 |
|
146 _getOriginFromOriginString: function(originStr) { |
|
147 let offsets = originStr.split(" "); |
|
148 offsets.forEach(function(item, index) { |
|
149 offsets[index] = parseInt(item, 10); |
|
150 }); |
|
151 |
|
152 return offsets; |
|
153 }, |
|
154 |
|
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 }, |
|
165 |
|
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 }, |
|
170 |
|
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 }, |
|
177 |
|
178 /** |
|
179 * For canvas to avoid anti-aliasing |
|
180 */ |
|
181 _round: x => Math.round(x) + .5, |
|
182 |
|
183 _drawShape: function(points, fillStyle, strokeStyle) { |
|
184 this.ctx.save(); |
|
185 |
|
186 this.ctx.lineWidth = 1; |
|
187 this.ctx.strokeStyle = strokeStyle; |
|
188 this.ctx.fillStyle = fillStyle; |
|
189 |
|
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(); |
|
198 |
|
199 this.ctx.restore(); |
|
200 }, |
|
201 |
|
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 } |
|
207 |
|
208 this.ctx.save(); |
|
209 |
|
210 this.ctx.strokeStyle = this.ARROW_STROKE; |
|
211 this.ctx.fillStyle = this.ARROW_STROKE; |
|
212 this.ctx.lineWidth = 1; |
|
213 |
|
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(); |
|
218 |
|
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(); |
|
229 |
|
230 this.ctx.restore(); |
|
231 }, |
|
232 |
|
233 _drawOrigin: function(x, y) { |
|
234 this.ctx.save(); |
|
235 |
|
236 this.ctx.strokeStyle = this.ORIGIN_STROKE; |
|
237 this.ctx.fillStyle = this.ORIGIN_STROKE; |
|
238 |
|
239 this.ctx.beginPath(); |
|
240 this.ctx.arc(x, y, 4, 0, 2 * Math.PI, false); |
|
241 this.ctx.stroke(); |
|
242 this.ctx.fill(); |
|
243 |
|
244 this.ctx.restore(); |
|
245 }, |
|
246 |
|
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); |
|
265 |
|
266 let spanX = maxX - minX; |
|
267 let spanY = maxY - minY; |
|
268 let isWide = spanX > spanY; |
|
269 |
|
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); |
|
274 |
|
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); |
|
277 |
|
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 } |
|
284 |
|
285 return {w: cw, h: ch}; |
|
286 }, |
|
287 |
|
288 _drawShapes: function(shape, corner, transformed, transformedCorner) { |
|
289 this._drawOriginal(shape); |
|
290 this._drawOriginalCorner(corner); |
|
291 this._drawTransformed(transformed); |
|
292 this._drawTransformedCorner(transformedCorner); |
|
293 }, |
|
294 |
|
295 _drawOriginal: function(points) { |
|
296 this._drawShape(points, this.ORIGINAL_FILL, this.ORIGINAL_STROKE); |
|
297 }, |
|
298 |
|
299 _drawTransformed: function(points) { |
|
300 this._drawShape(points, this.TRANSFORMED_FILL, this.TRANSFORMED_STROKE); |
|
301 }, |
|
302 |
|
303 _drawOriginalCorner: function(points) { |
|
304 this._drawShape(points, this.ORIGINAL_STROKE, this.ORIGINAL_STROKE); |
|
305 }, |
|
306 |
|
307 _drawTransformedCorner: function(points) { |
|
308 this._drawShape(points, this.TRANSFORMED_STROKE, this.TRANSFORMED_STROKE); |
|
309 }, |
|
310 |
|
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 }, |
|
317 |
|
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); |
|
339 |
|
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); |
|
344 |
|
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); |
|
348 |
|
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); |
|
357 |
|
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); |
|
367 |
|
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 ]); |
|
376 |
|
377 this.canvas.setAttribute("width", w); |
|
378 this.canvas.setAttribute("height", h); |
|
379 |
|
380 this._drawShapes(shapePoints, cornerPoints, transformedPoints, transformedCornerPoints) |
|
381 this._drawArrows(shapePoints, transformedPoints); |
|
382 this._drawOrigin(originData[0], originData[1]); |
|
383 |
|
384 return true; |
|
385 } else { |
|
386 return false; |
|
387 } |
|
388 } |
|
389 }; |