1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * @fileoverview QuadStackView controls the content and viewing angle a
11 base.requireStylesheet('ui.quad_stack_view');
13 base.requireTemplate('ui.quad_stack_view');
15 base.require('base.bbox2');
16 base.require('base.gl_matrix');
17 base.require('base.quad');
18 base.require('base.raf');
19 base.require('base.rect');
20 base.require('base.settings');
21 base.require('ui.camera');
22 base.require('ui.mouse_mode_selector');
23 base.require('ui.mouse_tracker');
25 base.exportTo('ui', function() {
27 constants.IMAGE_LOAD_RETRY_TIME_MS = 500;
28 constants.SUBDIVISION_MINIMUM = 1;
29 constants.SUBDIVISION_RECURSION_DEPTH = 3;
30 constants.SUBDIVISION_DEPTH_THRESHOLD = 100;
31 constants.FAR_PLANE_DISTANCE = 10000;
33 // Care of bckenney@ via
34 // http://extremelysatisfactorytotalitarianism.com/blog/?p=2120
35 function drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2) {
36 var tmp_p0 = [p0[0], p0[1]];
37 var tmp_p1 = [p1[0], p1[1]];
38 var tmp_p2 = [p2[0], p2[1]];
39 var tmp_t0 = [t0[0], t0[1]];
40 var tmp_t1 = [t1[0], t1[1]];
41 var tmp_t2 = [t2[0], t2[1]];
44 ctx.moveTo(tmp_p0[0], tmp_p0[1]);
45 ctx.lineTo(tmp_p1[0], tmp_p1[1]);
46 ctx.lineTo(tmp_p2[0], tmp_p2[1]);
49 tmp_p1[0] -= tmp_p0[0];
50 tmp_p1[1] -= tmp_p0[1];
51 tmp_p2[0] -= tmp_p0[0];
52 tmp_p2[1] -= tmp_p0[1];
54 tmp_t1[0] -= tmp_t0[0];
55 tmp_t1[1] -= tmp_t0[1];
56 tmp_t2[0] -= tmp_t0[0];
57 tmp_t2[1] -= tmp_t0[1];
59 var det = 1 / (tmp_t1[0] * tmp_t2[1] - tmp_t2[0] * tmp_t1[1]),
61 // linear transformation
62 a = (tmp_t2[1] * tmp_p1[0] - tmp_t1[1] * tmp_p2[0]) * det,
63 b = (tmp_t2[1] * tmp_p1[1] - tmp_t1[1] * tmp_p2[1]) * det,
64 c = (tmp_t1[0] * tmp_p2[0] - tmp_t2[0] * tmp_p1[0]) * det,
65 d = (tmp_t1[0] * tmp_p2[1] - tmp_t2[0] * tmp_p1[1]) * det,
68 e = tmp_p0[0] - a * tmp_t0[0] - c * tmp_t0[1],
69 f = tmp_p0[1] - b * tmp_t0[0] - d * tmp_t0[1];
72 ctx.transform(a, b, c, d, e, f);
74 ctx.drawImage(img, 0, 0);
78 function drawTriangleSub(
79 ctx, img, p0, p1, p2, t0, t1, t2, opt_recursion_depth) {
80 var depth = opt_recursion_depth || 0;
82 // We may subdivide if we are not at the limit of recursion.
83 var subdivisionIndex = 0;
84 if (depth < constants.SUBDIVISION_MINIMUM) {
86 } else if (depth < constants.SUBDIVISION_RECURSION_DEPTH) {
87 if (Math.abs(p0[2] - p1[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
88 subdivisionIndex += 1;
89 if (Math.abs(p0[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
90 subdivisionIndex += 2;
91 if (Math.abs(p1[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
92 subdivisionIndex += 4;
95 // These need to be created every time, since temporaries
96 // outside of the scope will be rewritten in recursion.
97 var p01 = vec4.create();
98 var p02 = vec4.create();
99 var p12 = vec4.create();
100 var t01 = vec2.create();
101 var t02 = vec2.create();
102 var t12 = vec2.create();
104 // Calculate the position before w-divide.
105 for (var i = 0; i < 2; ++i) {
111 // Interpolate the 3d position.
112 for (var i = 0; i < 4; ++i) {
113 p01[i] = (p0[i] + p1[i]) / 2;
114 p02[i] = (p0[i] + p2[i]) / 2;
115 p12[i] = (p1[i] + p2[i]) / 2;
118 // Re-apply w-divide to the original points and the interpolated ones.
119 for (var i = 0; i < 2; ++i) {
129 // Interpolate the texture coordinates.
130 for (var i = 0; i < 2; ++i) {
131 t01[i] = (t0[i] + t1[i]) / 2;
132 t02[i] = (t0[i] + t2[i]) / 2;
133 t12[i] = (t1[i] + t2[i]) / 2;
136 // Based on the index, we subdivide the triangle differently.
137 // Assuming the triangle is p0, p1, p2 and points between i j
138 // are represented as pij (that is, a point between p2 and p0
139 // is p02, etc), then the new triangles are defined by
140 // the 3rd 4th and 5th arguments into the function.
141 switch (subdivisionIndex) {
143 drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
144 drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
147 drawTriangleSub(ctx, img, p0, p1, p02, t0, t1, t02, depth + 1);
148 drawTriangleSub(ctx, img, p1, p02, p2, t1, t02, t2, depth + 1);
151 drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
152 drawTriangleSub(ctx, img, p02, p01, p2, t02, t01, t2, depth + 1);
153 drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
156 drawTriangleSub(ctx, img, p0, p12, p2, t0, t12, t2, depth + 1);
157 drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
160 drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
161 drawTriangleSub(ctx, img, p2, p01, p12, t2, t01, t12, depth + 1);
162 drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
165 drawTriangleSub(ctx, img, p0, p12, p02, t0, t12, t02, depth + 1);
166 drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
167 drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
170 drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
171 drawTriangleSub(ctx, img, p01, p12, p02, t01, t12, t02, depth + 1);
172 drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
173 drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
176 // In the 0 case and all other cases, we simply draw the triangle.
177 drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2);
182 // Created to avoid creating garbage when doing bulk transforms.
183 var tmp_vec4 = vec4.create();
184 function transform(transformed, point, matrix, viewport) {
185 vec4.set(tmp_vec4, point[0], point[1], 0, 1);
186 vec4.transformMat4(tmp_vec4, tmp_vec4, matrix);
189 if (w < 1e-6) w = 1e-6;
191 transformed[0] = ((tmp_vec4[0] / w) + 1) * viewport.width / 2;
192 transformed[1] = ((tmp_vec4[1] / w) + 1) * viewport.height / 2;
196 function drawProjectedQuadBackgroundToContext(
197 quad, p1, p2, p3, p4, ctx, quadCanvas) {
198 if (quad.imageData) {
199 quadCanvas.width = quad.imageData.width;
200 quadCanvas.height = quad.imageData.height;
201 quadCanvas.getContext('2d').putImageData(quad.imageData, 0, 0);
202 var quadBBox = new base.BBox2();
203 quadBBox.addQuad(quad);
204 var iw = quadCanvas.width;
205 var ih = quadCanvas.height;
209 [0, 0], [iw, 0], [0, ih]);
213 [iw, 0], [iw, ih], [0, ih]);
216 if (quad.backgroundColor) {
217 ctx.fillStyle = quad.backgroundColor;
219 ctx.moveTo(p1[0], p1[1]);
220 ctx.lineTo(p2[0], p2[1]);
221 ctx.lineTo(p3[0], p3[1]);
222 ctx.lineTo(p4[0], p4[1]);
228 function drawProjectedQuadOutlineToContext(
229 quad, p1, p2, p3, p4, ctx, quadCanvas) {
231 ctx.moveTo(p1[0], p1[1]);
232 ctx.lineTo(p2[0], p2[1]);
233 ctx.lineTo(p3[0], p3[1]);
234 ctx.lineTo(p4[0], p4[1]);
237 if (quad.borderColor)
238 ctx.strokeStyle = quad.borderColor;
240 ctx.strokeStyle = 'rgb(128,128,128)';
242 if (quad.shadowOffset) {
243 ctx.shadowColor = 'rgb(0, 0, 0)';
244 ctx.shadowOffsetX = quad.shadowOffset[0];
245 ctx.shadowOffsetY = quad.shadowOffset[1];
247 ctx.shadowBlur = quad.shadowBlur;
250 if (quad.borderWidth)
251 ctx.lineWidth = quad.borderWidth;
259 function drawProjectedQuadSelectionOutlineToContext(
260 quad, p1, p2, p3, p4, ctx, quadCanvas) {
261 if (!quad.upperBorderColor)
265 ctx.strokeStyle = quad.upperBorderColor;
268 ctx.moveTo(p1[0], p1[1]);
269 ctx.lineTo(p2[0], p2[1]);
270 ctx.lineTo(p3[0], p3[1]);
271 ctx.lineTo(p4[0], p4[1]);
276 function drawProjectedQuadToContext(
277 passNumber, quad, p1, p2, p3, p4, ctx, quadCanvas) {
278 if (passNumber === 0) {
279 drawProjectedQuadBackgroundToContext(
280 quad, p1, p2, p3, p4, ctx, quadCanvas);
281 } else if (passNumber === 1) {
282 drawProjectedQuadOutlineToContext(
283 quad, p1, p2, p3, p4, ctx, quadCanvas);
284 } else if (passNumber === 2) {
285 drawProjectedQuadSelectionOutlineToContext(
286 quad, p1, p2, p3, p4, ctx, quadCanvas);
288 throw new Error('Invalid pass number');
292 var tmp_p1 = vec3.create();
293 var tmp_p2 = vec3.create();
294 var tmp_p3 = vec3.create();
295 var tmp_p4 = vec3.create();
296 function transformAndProcessQuads(
297 matrix, viewport, quads, numPasses, handleQuadFunc, opt_arg1, opt_arg2) {
299 for (var passNumber = 0; passNumber < numPasses; passNumber++) {
300 for (var i = 0; i < quads.length; i++) {
302 transform(tmp_p1, quad.p1, matrix, viewport);
303 transform(tmp_p2, quad.p2, matrix, viewport);
304 transform(tmp_p3, quad.p3, matrix, viewport);
305 transform(tmp_p4, quad.p4, matrix, viewport);
306 handleQuadFunc(passNumber, quad,
307 tmp_p1, tmp_p2, tmp_p3, tmp_p4,
316 var QuadStackView = ui.define('quad-stack-view');
318 QuadStackView.prototype = {
319 __proto__: HTMLUnknownElement.prototype,
321 decorate: function() {
322 this.className = 'quad-stack-view';
324 var node = base.instantiateTemplate('#quad-stack-view-template');
325 this.appendChild(node);
327 this.canvas_ = this.querySelector('#canvas');
328 this.chromeImages_ = {
329 left: this.querySelector('#chrome-left'),
330 mid: this.querySelector('#chrome-mid'),
331 right: this.querySelector('#chrome-right')
336 this.camera_ = new ui.Camera(this.mouseModeSelector_);
337 this.camera_.addEventListener('renderrequired',
338 this.onRenderRequired_.bind(this));
339 this.cameraWasReset_ = false;
340 this.camera_.canvas = this.canvas_;
342 this.viewportRect_ = base.Rect.fromXYWH(0, 0, 0, 0);
344 this.stackingDistance_ = 45;
345 this.pixelRatio_ = window.devicePixelRatio || 1;
348 onStackingDistanceChange: function(e) {
349 this.stackingDistance_ = parseInt(e.target.value);
350 this.scheduleRender();
353 get mouseModeSelector() {
354 return this.mouseModeSelector_;
363 this.scheduleRender();
366 set deviceRect(rect) {
367 if (!rect || rect.equalTo(this.deviceRect_))
370 this.deviceRect_ = rect;
371 this.camera_.deviceRect = rect;
372 this.chromeQuad_ = undefined;
376 if (!this.offsetParent)
379 var width = parseInt(window.getComputedStyle(this.offsetParent).width);
380 var height = parseInt(window.getComputedStyle(this.offsetParent).height);
381 var rect = base.Rect.fromXYWH(0, 0, width, height);
383 if (rect.equalTo(this.viewportRect_))
386 this.viewportRect_ = rect;
387 this.style.width = width + 'px';
388 this.style.height = height + 'px';
389 this.canvas_.style.width = width + 'px';
390 this.canvas_.style.height = height + 'px';
391 this.canvas_.width = this.pixelRatio_ * width;
392 this.canvas_.height = this.pixelRatio_ * height;
393 if (!this.cameraWasReset_) {
394 this.camera_.resetCamera();
395 this.cameraWasReset_ = true;
400 readyToDraw: function() {
401 // If src isn't set yet, set it to ensure we can use
402 // the image to draw onto a canvas.
403 if (!this.chromeImages_.left.src) {
405 window.getComputedStyle(this.chromeImages_.left).content;
406 leftContent = leftContent.replace(/url\((.*)\)/, '$1');
409 window.getComputedStyle(this.chromeImages_.mid).content;
410 midContent = midContent.replace(/url\((.*)\)/, '$1');
413 window.getComputedStyle(this.chromeImages_.right).content;
414 rightContent = rightContent.replace(/url\((.*)\)/, '$1');
416 this.chromeImages_.left.src = leftContent;
417 this.chromeImages_.mid.src = midContent;
418 this.chromeImages_.right.src = rightContent;
421 // If all of the images are loaded (height > 0), then
422 // we are ready to draw.
423 return (this.chromeImages_.left.height > 0) &&
424 (this.chromeImages_.mid.height > 0) &&
425 (this.chromeImages_.right.height > 0);
429 if (this.chromeQuad_)
430 return this.chromeQuad_;
432 // Draw the chrome border into a separate canvas.
433 var chromeCanvas = document.createElement('canvas');
434 var offsetY = this.chromeImages_.left.height;
436 chromeCanvas.width = this.deviceRect_.width;
437 chromeCanvas.height = this.deviceRect_.height + offsetY;
439 var leftWidth = this.chromeImages_.left.width;
440 var midWidth = this.chromeImages_.mid.width;
441 var rightWidth = this.chromeImages_.right.width;
443 var chromeCtx = chromeCanvas.getContext('2d');
444 chromeCtx.drawImage(this.chromeImages_.left, 0, 0);
447 chromeCtx.translate(leftWidth, 0);
449 // Calculate the scale of the mid image.
450 var s = (this.deviceRect_.width - leftWidth - rightWidth) / midWidth;
451 chromeCtx.scale(s, 1);
453 chromeCtx.drawImage(this.chromeImages_.mid, 0, 0);
457 this.chromeImages_.right, leftWidth + s * midWidth, 0);
459 // Construct the quad.
460 var chromeRect = base.Rect.fromXYWH(
462 this.deviceRect_.y - offsetY,
463 this.deviceRect_.width,
464 this.deviceRect_.height + offsetY);
465 var chromeQuad = base.Quad.fromRect(chromeRect);
466 chromeQuad.stackingGroupId = this.maxStackingGroupId_ + 1;
467 chromeQuad.imageData = chromeCtx.getImageData(
468 0, 0, chromeCanvas.width, chromeCanvas.height);
469 chromeQuad.shadowOffset = [0, 0];
470 chromeQuad.shadowBlur = 5;
471 chromeQuad.borderWidth = 3;
472 this.chromeQuad_ = chromeQuad;
473 return this.chromeQuad_;
476 scheduleRender: function() {
477 if (this.redrawScheduled_)
479 this.redrawScheduled_ = true;
480 base.requestAnimationFrame(this.render, this);
483 onRenderRequired_: function(e) {
484 this.scheduleRender();
487 stackTransformAndProcessQuads_: function(
488 numPasses, handleQuadFunc, includeChromeQuad, opt_arg1, opt_arg2) {
489 var mv = this.camera_.modelViewMatrix;
490 var p = this.camera_.projectionMatrix;
492 var viewport = base.Rect.fromXYWH(
493 0, 0, this.canvas_.width, this.canvas_.height);
495 // Calculate the quad stacks.
497 for (var i = 0; i < this.quads_.length; ++i) {
498 var quad = this.quads_[i];
499 var stackingId = quad.stackingGroupId || 0;
500 while (stackingId >= quadStacks.length)
503 quadStacks[stackingId].push(quad);
506 var mvp = mat4.create();
507 this.maxStackingGroupId_ = quadStacks.length;
508 var stackingDistance =
509 this.stackingDistance_ * this.camera_.stackingDistanceDampening;
511 // Draw the quad stacks, raising each subsequent level.
512 mat4.multiply(mvp, p, mv);
513 for (var i = 0; i < quadStacks.length; ++i) {
514 transformAndProcessQuads(mvp, viewport, quadStacks[i],
515 numPasses, handleQuadFunc,
518 mat4.translate(mv, mv, [0, 0, stackingDistance]);
519 mat4.multiply(mvp, p, mv);
522 if (includeChromeQuad && this.deviceRect_) {
523 transformAndProcessQuads(mvp, viewport, [this.chromeQuad],
524 numPasses, drawProjectedQuadToContext,
530 this.redrawScheduled_ = false;
532 if (!this.readyToDraw()) {
533 setTimeout(this.scheduleRender.bind(this),
534 constants.IMAGE_LOAD_RETRY_TIME_MS);
541 var canvasCtx = this.canvas_.getContext('2d');
543 canvasCtx.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
545 var quadCanvas = document.createElement('canvas');
546 this.stackTransformAndProcessQuads_(
547 3, drawProjectedQuadToContext, true,
548 canvasCtx, quadCanvas);
549 quadCanvas.width = 0; // Hack: Frees the quadCanvas' resources.
552 trackMouse_: function() {
553 this.mouseModeSelector_ = new ui.MouseModeSelector(this);
554 this.mouseModeSelector_.supportedModeMask =
555 ui.MOUSE_SELECTOR_MODE.SELECTION |
556 ui.MOUSE_SELECTOR_MODE.PANSCAN |
557 ui.MOUSE_SELECTOR_MODE.ZOOM |
558 ui.MOUSE_SELECTOR_MODE.ROTATE;
559 this.mouseModeSelector_.mode = ui.MOUSE_SELECTOR_MODE.PANSCAN;
560 this.mouseModeSelector_.pos = {x: 0, y: 100};
561 this.appendChild(this.mouseModeSelector_);
562 this.mouseModeSelector_.settingsKey =
563 'quadStackView.mouseModeSelector';
565 this.mouseModeSelector_.setModifierForAlternateMode(
566 ui.MOUSE_SELECTOR_MODE.ROTATE, ui.MODIFIER.SHIFT);
567 this.mouseModeSelector_.setModifierForAlternateMode(
568 ui.MOUSE_SELECTOR_MODE.PANSCAN, ui.MODIFIER.SPACE);
569 this.mouseModeSelector_.setModifierForAlternateMode(
570 ui.MOUSE_SELECTOR_MODE.ZOOM, ui.MODIFIER.CMD_OR_CTRL);
572 this.mouseModeSelector_.addEventListener('updateselection',
573 this.onSelectionUpdate_.bind(this));
574 this.mouseModeSelector_.addEventListener('endselection',
575 this.onSelectionUpdate_.bind(this));
578 extractRelativeMousePosition_: function(e) {
579 var br = this.canvas_.getBoundingClientRect();
581 this.pixelRatio_ * (e.clientX - this.canvas_.offsetLeft - br.left),
582 this.pixelRatio_ * (e.clientY - this.canvas_.offsetTop - br.top)
586 onSelectionUpdate_: function(e) {
587 var mousePos = this.extractRelativeMousePosition_(e);
589 function handleQuad(passNumber, quad, p1, p2, p3, p4) {
590 if (base.pointInImplicitQuad(mousePos, p1, p2, p3, p4))
593 this.stackTransformAndProcessQuads_(1, handleQuad, false);
594 var e = new Event('selectionchange', false, false);
596 this.dispatchEvent(e);
601 QuadStackView: QuadStackView