1<!DOCTYPE html>
2<!--
3Copyright (c) 2013 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7<link rel="import" href="/tracing/base/bbox2.html">
8<link rel="import" href="/tracing/base/math.html">
9<link rel="import" href="/tracing/base/quad.html">
10<link rel="import" href="/tracing/base/raf.html">
11<link rel="import" href="/tracing/base/rect.html">
12<link rel="import" href="/tracing/base/settings.html">
13<link rel="import" href="/tracing/ui/base/camera.html">
14<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
15<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
16<link rel="import" href="/tracing/ui/base/utils.html">
17
18<style>
19* /deep/ quad-stack-view {
20  display: block;
21  float: left;
22  height: 100%;
23  overflow: hidden;
24  position: relative; /* For the absolute positioned mouse-mode-selector */
25  width: 100%;
26}
27
28* /deep/ quad-stack-view > #header {
29  position: absolute;
30  font-size: 70%;
31  top: 10px;
32  left: 10px;
33  width: 800px;
34}
35* /deep/ quad-stack-view > #stacking-distance-slider {
36  position: absolute;
37  font-size: 70%;
38  top: 10px;
39  right: 10px;
40}
41
42* /deep/ quad-stack-view > #chrome-left {
43  content: url('../images/chrome-left.png');
44  display: none;
45}
46
47* /deep/ quad-stack-view > #chrome-mid {
48  content: url('../images/chrome-mid.png');
49  display: none;
50}
51
52* /deep/ quad-stack-view > #chrome-right {
53  content: url('../images/chrome-right.png');
54  display: none;
55}
56</style>
57
58<template id='quad-stack-view-template'>
59  <div id="header"></div>
60  <input id="stacking-distance-slider" type="range" min=1 max=400 step=1>
61  </input>
62  <canvas id='canvas'></canvas>
63  <img id='chrome-left'/>
64  <img id='chrome-mid'/>
65  <img id='chrome-right'/>
66</template>
67
68<script>
69'use strict';
70
71/**
72 * @fileoverview QuadStackView controls the content and viewing angle a
73 * QuadStack.
74 */
75tr.exportTo('tr.ui.b', function() {
76  var THIS_DOC = document.currentScript.ownerDocument;
77
78  var constants = {};
79  constants.IMAGE_LOAD_RETRY_TIME_MS = 500;
80  constants.SUBDIVISION_MINIMUM = 1;
81  constants.SUBDIVISION_RECURSION_DEPTH = 3;
82  constants.SUBDIVISION_DEPTH_THRESHOLD = 100;
83  constants.FAR_PLANE_DISTANCE = 10000;
84
85  // Care of bckenney@ via
86  // http://extremelysatisfactorytotalitarianism.com/blog/?p=2120
87  function drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2) {
88    var tmp_p0 = [p0[0], p0[1]];
89    var tmp_p1 = [p1[0], p1[1]];
90    var tmp_p2 = [p2[0], p2[1]];
91    var tmp_t0 = [t0[0], t0[1]];
92    var tmp_t1 = [t1[0], t1[1]];
93    var tmp_t2 = [t2[0], t2[1]];
94
95    ctx.beginPath();
96    ctx.moveTo(tmp_p0[0], tmp_p0[1]);
97    ctx.lineTo(tmp_p1[0], tmp_p1[1]);
98    ctx.lineTo(tmp_p2[0], tmp_p2[1]);
99    ctx.closePath();
100
101    tmp_p1[0] -= tmp_p0[0];
102    tmp_p1[1] -= tmp_p0[1];
103    tmp_p2[0] -= tmp_p0[0];
104    tmp_p2[1] -= tmp_p0[1];
105
106    tmp_t1[0] -= tmp_t0[0];
107    tmp_t1[1] -= tmp_t0[1];
108    tmp_t2[0] -= tmp_t0[0];
109    tmp_t2[1] -= tmp_t0[1];
110
111    var det = 1 / (tmp_t1[0] * tmp_t2[1] - tmp_t2[0] * tmp_t1[1]),
112
113        // linear transformation
114        a = (tmp_t2[1] * tmp_p1[0] - tmp_t1[1] * tmp_p2[0]) * det,
115        b = (tmp_t2[1] * tmp_p1[1] - tmp_t1[1] * tmp_p2[1]) * det,
116        c = (tmp_t1[0] * tmp_p2[0] - tmp_t2[0] * tmp_p1[0]) * det,
117        d = (tmp_t1[0] * tmp_p2[1] - tmp_t2[0] * tmp_p1[1]) * det,
118
119        // translation
120        e = tmp_p0[0] - a * tmp_t0[0] - c * tmp_t0[1],
121        f = tmp_p0[1] - b * tmp_t0[0] - d * tmp_t0[1];
122
123    ctx.save();
124    ctx.transform(a, b, c, d, e, f);
125    ctx.clip();
126    ctx.drawImage(img, 0, 0);
127    ctx.restore();
128  }
129
130  function drawTriangleSub(
131      ctx, img, p0, p1, p2, t0, t1, t2, opt_recursion_depth) {
132    var depth = opt_recursion_depth || 0;
133
134    // We may subdivide if we are not at the limit of recursion.
135    var subdivisionIndex = 0;
136    if (depth < constants.SUBDIVISION_MINIMUM) {
137      subdivisionIndex = 7;
138    } else if (depth < constants.SUBDIVISION_RECURSION_DEPTH) {
139      if (Math.abs(p0[2] - p1[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
140        subdivisionIndex += 1;
141      if (Math.abs(p0[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
142        subdivisionIndex += 2;
143      if (Math.abs(p1[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
144        subdivisionIndex += 4;
145    }
146
147    // These need to be created every time, since temporaries
148    // outside of the scope will be rewritten in recursion.
149    var p01 = vec4.create();
150    var p02 = vec4.create();
151    var p12 = vec4.create();
152    var t01 = vec2.create();
153    var t02 = vec2.create();
154    var t12 = vec2.create();
155
156    // Calculate the position before w-divide.
157    for (var i = 0; i < 2; ++i) {
158      p0[i] *= p0[2];
159      p1[i] *= p1[2];
160      p2[i] *= p2[2];
161    }
162
163    // Interpolate the 3d position.
164    for (var i = 0; i < 4; ++i) {
165      p01[i] = (p0[i] + p1[i]) / 2;
166      p02[i] = (p0[i] + p2[i]) / 2;
167      p12[i] = (p1[i] + p2[i]) / 2;
168    }
169
170    // Re-apply w-divide to the original points and the interpolated ones.
171    for (var i = 0; i < 2; ++i) {
172      p0[i] /= p0[2];
173      p1[i] /= p1[2];
174      p2[i] /= p2[2];
175
176      p01[i] /= p01[2];
177      p02[i] /= p02[2];
178      p12[i] /= p12[2];
179    }
180
181    // Interpolate the texture coordinates.
182    for (var i = 0; i < 2; ++i) {
183      t01[i] = (t0[i] + t1[i]) / 2;
184      t02[i] = (t0[i] + t2[i]) / 2;
185      t12[i] = (t1[i] + t2[i]) / 2;
186    }
187
188    // Based on the index, we subdivide the triangle differently.
189    // Assuming the triangle is p0, p1, p2 and points between i j
190    // are represented as pij (that is, a point between p2 and p0
191    // is p02, etc), then the new triangles are defined by
192    // the 3rd 4th and 5th arguments into the function.
193    switch (subdivisionIndex) {
194      case 1:
195        drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
196        drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
197        break;
198      case 2:
199        drawTriangleSub(ctx, img, p0, p1, p02, t0, t1, t02, depth + 1);
200        drawTriangleSub(ctx, img, p1, p02, p2, t1, t02, t2, depth + 1);
201        break;
202      case 3:
203        drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
204        drawTriangleSub(ctx, img, p02, p01, p2, t02, t01, t2, depth + 1);
205        drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
206        break;
207      case 4:
208        drawTriangleSub(ctx, img, p0, p12, p2, t0, t12, t2, depth + 1);
209        drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
210        break;
211      case 5:
212        drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
213        drawTriangleSub(ctx, img, p2, p01, p12, t2, t01, t12, depth + 1);
214        drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
215        break;
216      case 6:
217        drawTriangleSub(ctx, img, p0, p12, p02, t0, t12, t02, depth + 1);
218        drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
219        drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
220        break;
221      case 7:
222        drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
223        drawTriangleSub(ctx, img, p01, p12, p02, t01, t12, t02, depth + 1);
224        drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
225        drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
226        break;
227      default:
228        // In the 0 case and all other cases, we simply draw the triangle.
229        drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2);
230        break;
231    }
232  }
233
234  // Created to avoid creating garbage when doing bulk transforms.
235  var tmp_vec4 = vec4.create();
236  function transform(transformed, point, matrix, viewport) {
237    vec4.set(tmp_vec4, point[0], point[1], 0, 1);
238    vec4.transformMat4(tmp_vec4, tmp_vec4, matrix);
239
240    var w = tmp_vec4[3];
241    if (w < 1e-6) w = 1e-6;
242
243    transformed[0] = ((tmp_vec4[0] / w) + 1) * viewport.width / 2;
244    transformed[1] = ((tmp_vec4[1] / w) + 1) * viewport.height / 2;
245    transformed[2] = w;
246  }
247
248  function drawProjectedQuadBackgroundToContext(
249      quad, p1, p2, p3, p4, ctx, quadCanvas) {
250    if (quad.imageData) {
251      quadCanvas.width = quad.imageData.width;
252      quadCanvas.height = quad.imageData.height;
253      quadCanvas.getContext('2d').putImageData(quad.imageData, 0, 0);
254      var quadBBox = new tr.b.BBox2();
255      quadBBox.addQuad(quad);
256      var iw = quadCanvas.width;
257      var ih = quadCanvas.height;
258      drawTriangleSub(
259          ctx, quadCanvas,
260          p1, p2, p4,
261          [0, 0], [iw, 0], [0, ih]);
262      drawTriangleSub(
263          ctx, quadCanvas,
264          p2, p3, p4,
265          [iw, 0], [iw, ih], [0, ih]);
266    }
267
268    if (quad.backgroundColor) {
269      ctx.fillStyle = quad.backgroundColor;
270      ctx.beginPath();
271      ctx.moveTo(p1[0], p1[1]);
272      ctx.lineTo(p2[0], p2[1]);
273      ctx.lineTo(p3[0], p3[1]);
274      ctx.lineTo(p4[0], p4[1]);
275      ctx.closePath();
276      ctx.fill();
277    }
278  }
279
280  function drawProjectedQuadOutlineToContext(
281      quad, p1, p2, p3, p4, ctx, quadCanvas) {
282    ctx.beginPath();
283    ctx.moveTo(p1[0], p1[1]);
284    ctx.lineTo(p2[0], p2[1]);
285    ctx.lineTo(p3[0], p3[1]);
286    ctx.lineTo(p4[0], p4[1]);
287    ctx.closePath();
288    ctx.save();
289    if (quad.borderColor)
290      ctx.strokeStyle = quad.borderColor;
291    else
292      ctx.strokeStyle = 'rgb(128,128,128)';
293
294    if (quad.shadowOffset) {
295      ctx.shadowColor = 'rgb(0, 0, 0)';
296      ctx.shadowOffsetX = quad.shadowOffset[0];
297      ctx.shadowOffsetY = quad.shadowOffset[1];
298      if (quad.shadowBlur)
299        ctx.shadowBlur = quad.shadowBlur;
300    }
301
302    if (quad.borderWidth)
303      ctx.lineWidth = quad.borderWidth;
304    else
305      ctx.lineWidth = 1;
306
307    ctx.stroke();
308    ctx.restore();
309  }
310
311  function drawProjectedQuadSelectionOutlineToContext(
312      quad, p1, p2, p3, p4, ctx, quadCanvas) {
313    if (!quad.upperBorderColor)
314      return;
315
316    ctx.lineWidth = 8;
317    ctx.strokeStyle = quad.upperBorderColor;
318
319    ctx.beginPath();
320    ctx.moveTo(p1[0], p1[1]);
321    ctx.lineTo(p2[0], p2[1]);
322    ctx.lineTo(p3[0], p3[1]);
323    ctx.lineTo(p4[0], p4[1]);
324    ctx.closePath();
325    ctx.stroke();
326  }
327
328  function drawProjectedQuadToContext(
329      passNumber, quad, p1, p2, p3, p4, ctx, quadCanvas) {
330    if (passNumber === 0) {
331      drawProjectedQuadBackgroundToContext(
332          quad, p1, p2, p3, p4, ctx, quadCanvas);
333    } else if (passNumber === 1) {
334      drawProjectedQuadOutlineToContext(
335          quad, p1, p2, p3, p4, ctx, quadCanvas);
336    } else if (passNumber === 2) {
337      drawProjectedQuadSelectionOutlineToContext(
338          quad, p1, p2, p3, p4, ctx, quadCanvas);
339    } else {
340      throw new Error('Invalid pass number');
341    }
342  }
343
344  var tmp_p1 = vec3.create();
345  var tmp_p2 = vec3.create();
346  var tmp_p3 = vec3.create();
347  var tmp_p4 = vec3.create();
348  function transformAndProcessQuads(
349      matrix, viewport, quads, numPasses, handleQuadFunc, opt_arg1, opt_arg2) {
350
351    for (var passNumber = 0; passNumber < numPasses; passNumber++) {
352      for (var i = 0; i < quads.length; i++) {
353        var quad = quads[i];
354        transform(tmp_p1, quad.p1, matrix, viewport);
355        transform(tmp_p2, quad.p2, matrix, viewport);
356        transform(tmp_p3, quad.p3, matrix, viewport);
357        transform(tmp_p4, quad.p4, matrix, viewport);
358        handleQuadFunc(passNumber, quad,
359                       tmp_p1, tmp_p2, tmp_p3, tmp_p4,
360                       opt_arg1, opt_arg2);
361      }
362    }
363  }
364
365  /**
366   * @constructor
367   */
368  var QuadStackView = tr.ui.b.define('quad-stack-view');
369
370  QuadStackView.prototype = {
371    __proto__: HTMLUnknownElement.prototype,
372
373    decorate: function() {
374      this.className = 'quad-stack-view';
375
376      var node = tr.ui.b.instantiateTemplate('#quad-stack-view-template',
377                                          THIS_DOC);
378      this.appendChild(node);
379      this.updateHeaderVisibility_();
380      this.canvas_ = this.querySelector('#canvas');
381      this.chromeImages_ = {
382        left: this.querySelector('#chrome-left'),
383        mid: this.querySelector('#chrome-mid'),
384        right: this.querySelector('#chrome-right')
385      };
386
387      var stackingDistanceSlider = this.querySelector(
388          '#stacking-distance-slider');
389      stackingDistanceSlider.value = tr.b.Settings.get(
390          'quadStackView.stackingDistance', 45);
391      stackingDistanceSlider.addEventListener(
392          'change', this.onStackingDistanceChange_.bind(this));
393      stackingDistanceSlider.addEventListener(
394          'input', this.onStackingDistanceChange_.bind(this));
395
396      this.trackMouse_();
397
398      this.camera_ = new tr.ui.b.Camera(this.mouseModeSelector_);
399      this.camera_.addEventListener('renderrequired',
400          this.onRenderRequired_.bind(this));
401      this.cameraWasReset_ = false;
402      this.camera_.canvas = this.canvas_;
403
404      this.viewportRect_ = tr.b.Rect.fromXYWH(0, 0, 0, 0);
405
406      this.pixelRatio_ = window.devicePixelRatio || 1;
407    },
408
409    updateHeaderVisibility_: function() {
410      if (this.headerText)
411        this.querySelector('#header').style.display = '';
412      else
413        this.querySelector('#header').style.display = 'none';
414    },
415
416    get headerText() {
417      return this.querySelector('#header').textContent;
418    },
419
420    set headerText(headerText) {
421      this.querySelector('#header').textContent = headerText;
422      this.updateHeaderVisibility_();
423    },
424
425    onStackingDistanceChange_: function(e) {
426      tr.b.Settings.set('quadStackView.stackingDistance',
427                        this.stackingDistance);
428      this.scheduleRender();
429      e.stopPropagation();
430    },
431
432    get stackingDistance() {
433      return this.querySelector('#stacking-distance-slider').value;
434    },
435
436    get mouseModeSelector() {
437      return this.mouseModeSelector_;
438    },
439
440    get camera() {
441      return this.camera_;
442    },
443
444    set quads(q) {
445      this.quads_ = q;
446      this.scheduleRender();
447    },
448
449    set deviceRect(rect) {
450      if (!rect || rect.equalTo(this.deviceRect_))
451        return;
452
453      this.deviceRect_ = rect;
454      this.camera_.deviceRect = rect;
455      this.chromeQuad_ = undefined;
456    },
457
458    resize: function() {
459      if (!this.offsetParent)
460        return true;
461
462      var width = parseInt(window.getComputedStyle(this.offsetParent).width);
463      var height = parseInt(window.getComputedStyle(this.offsetParent).height);
464      var rect = tr.b.Rect.fromXYWH(0, 0, width, height);
465
466      if (rect.equalTo(this.viewportRect_))
467        return false;
468
469      this.viewportRect_ = rect;
470      this.style.width = width + 'px';
471      this.style.height = height + 'px';
472      this.canvas_.style.width = width + 'px';
473      this.canvas_.style.height = height + 'px';
474      this.canvas_.width = this.pixelRatio_ * width;
475      this.canvas_.height = this.pixelRatio_ * height;
476      if (!this.cameraWasReset_) {
477        this.camera_.resetCamera();
478        this.cameraWasReset_ = true;
479      }
480      return true;
481    },
482
483    readyToDraw: function() {
484      // If src isn't set yet, set it to ensure we can use
485      // the image to draw onto a canvas.
486      if (!this.chromeImages_.left.src) {
487        var leftContent =
488            window.getComputedStyle(this.chromeImages_.left).content;
489        leftContent = tr.ui.b.extractUrlString(leftContent);
490
491        var midContent =
492            window.getComputedStyle(this.chromeImages_.mid).content;
493        midContent = tr.ui.b.extractUrlString(midContent);
494
495        var rightContent =
496            window.getComputedStyle(this.chromeImages_.right).content;
497        rightContent = tr.ui.b.extractUrlString(rightContent);
498
499        this.chromeImages_.left.src = leftContent;
500        this.chromeImages_.mid.src = midContent;
501        this.chromeImages_.right.src = rightContent;
502      }
503
504      // If all of the images are loaded (height > 0), then
505      // we are ready to draw.
506      return (this.chromeImages_.left.height > 0) &&
507             (this.chromeImages_.mid.height > 0) &&
508             (this.chromeImages_.right.height > 0);
509    },
510
511    get chromeQuad() {
512      if (this.chromeQuad_)
513        return this.chromeQuad_;
514
515      // Draw the chrome border into a separate canvas.
516      var chromeCanvas = document.createElement('canvas');
517      var offsetY = this.chromeImages_.left.height;
518
519      chromeCanvas.width = this.deviceRect_.width;
520      chromeCanvas.height = this.deviceRect_.height + offsetY;
521
522      var leftWidth = this.chromeImages_.left.width;
523      var midWidth = this.chromeImages_.mid.width;
524      var rightWidth = this.chromeImages_.right.width;
525
526      var chromeCtx = chromeCanvas.getContext('2d');
527      chromeCtx.drawImage(this.chromeImages_.left, 0, 0);
528
529      chromeCtx.save();
530      chromeCtx.translate(leftWidth, 0);
531
532      // Calculate the scale of the mid image.
533      var s = (this.deviceRect_.width - leftWidth - rightWidth) / midWidth;
534      chromeCtx.scale(s, 1);
535
536      chromeCtx.drawImage(this.chromeImages_.mid, 0, 0);
537      chromeCtx.restore();
538
539      chromeCtx.drawImage(
540          this.chromeImages_.right, leftWidth + s * midWidth, 0);
541
542      // Construct the quad.
543      var chromeRect = tr.b.Rect.fromXYWH(
544          this.deviceRect_.x,
545          this.deviceRect_.y - offsetY,
546          this.deviceRect_.width,
547          this.deviceRect_.height + offsetY);
548      var chromeQuad = tr.b.Quad.fromRect(chromeRect);
549      chromeQuad.stackingGroupId = this.maxStackingGroupId_ + 1;
550      chromeQuad.imageData = chromeCtx.getImageData(
551          0, 0, chromeCanvas.width, chromeCanvas.height);
552      chromeQuad.shadowOffset = [0, 0];
553      chromeQuad.shadowBlur = 5;
554      chromeQuad.borderWidth = 3;
555      this.chromeQuad_ = chromeQuad;
556      return this.chromeQuad_;
557    },
558
559    scheduleRender: function() {
560      if (this.redrawScheduled_)
561        return false;
562      this.redrawScheduled_ = true;
563      tr.b.requestAnimationFrame(this.render, this);
564    },
565
566    onRenderRequired_: function(e) {
567      this.scheduleRender();
568    },
569
570    stackTransformAndProcessQuads_: function(
571        numPasses, handleQuadFunc, includeChromeQuad, opt_arg1, opt_arg2) {
572      var mv = this.camera_.modelViewMatrix;
573      var p = this.camera_.projectionMatrix;
574
575      var viewport = tr.b.Rect.fromXYWH(
576          0, 0, this.canvas_.width, this.canvas_.height);
577
578      // Calculate the quad stacks.
579      var quadStacks = [];
580      for (var i = 0; i < this.quads_.length; ++i) {
581        var quad = this.quads_[i];
582        var stackingId = quad.stackingGroupId || 0;
583        while (stackingId >= quadStacks.length)
584          quadStacks.push([]);
585
586        quadStacks[stackingId].push(quad);
587      }
588
589      var mvp = mat4.create();
590      this.maxStackingGroupId_ = quadStacks.length;
591      var effectiveStackingDistance =
592          this.stackingDistance * this.camera_.stackingDistanceDampening;
593
594      // Draw the quad stacks, raising each subsequent level.
595      mat4.multiply(mvp, p, mv);
596      for (var i = 0; i < quadStacks.length; ++i) {
597        transformAndProcessQuads(mvp, viewport, quadStacks[i],
598                                 numPasses, handleQuadFunc,
599                                 opt_arg1, opt_arg2);
600
601        mat4.translate(mv, mv, [0, 0, effectiveStackingDistance]);
602        mat4.multiply(mvp, p, mv);
603      }
604
605      if (includeChromeQuad && this.deviceRect_) {
606        transformAndProcessQuads(mvp, viewport, [this.chromeQuad],
607                                 numPasses, drawProjectedQuadToContext,
608                                 opt_arg1, opt_arg2);
609      }
610    },
611
612    render: function() {
613      this.redrawScheduled_ = false;
614
615      if (!this.readyToDraw()) {
616        setTimeout(this.scheduleRender.bind(this),
617                   constants.IMAGE_LOAD_RETRY_TIME_MS);
618        return;
619      }
620
621      if (!this.quads_)
622        return;
623
624      var canvasCtx = this.canvas_.getContext('2d');
625      if (!this.resize())
626        canvasCtx.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
627
628      var quadCanvas = document.createElement('canvas');
629      this.stackTransformAndProcessQuads_(
630          3, drawProjectedQuadToContext, true,
631          canvasCtx, quadCanvas);
632      quadCanvas.width = 0; // Hack: Frees the quadCanvas' resources.
633    },
634
635    trackMouse_: function() {
636      this.mouseModeSelector_ = document.createElement(
637          'tr-ui-b-mouse-mode-selector');
638      this.mouseModeSelector_.targetElement = this.canvas_;
639      this.mouseModeSelector_.supportedModeMask =
640          tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION |
641          tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN |
642          tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM |
643          tr.ui.b.MOUSE_SELECTOR_MODE.ROTATE;
644      this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN;
645      this.mouseModeSelector_.pos = {x: 0, y: 100};
646      this.appendChild(this.mouseModeSelector_);
647      this.mouseModeSelector_.settingsKey =
648          'quadStackView.mouseModeSelector';
649
650      this.mouseModeSelector_.setModifierForAlternateMode(
651          tr.ui.b.MOUSE_SELECTOR_MODE.ROTATE, tr.ui.b.MODIFIER.SHIFT);
652      this.mouseModeSelector_.setModifierForAlternateMode(
653          tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN, tr.ui.b.MODIFIER.SPACE);
654      this.mouseModeSelector_.setModifierForAlternateMode(
655          tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM, tr.ui.b.MODIFIER.CMD_OR_CTRL);
656
657      this.mouseModeSelector_.addEventListener('updateselection',
658          this.onSelectionUpdate_.bind(this));
659      this.mouseModeSelector_.addEventListener('endselection',
660          this.onSelectionUpdate_.bind(this));
661    },
662
663    extractRelativeMousePosition_: function(e) {
664      var br = this.canvas_.getBoundingClientRect();
665      return [
666        this.pixelRatio_ * (e.clientX - this.canvas_.offsetLeft - br.left),
667        this.pixelRatio_ * (e.clientY - this.canvas_.offsetTop - br.top)
668      ];
669    },
670
671    onSelectionUpdate_: function(e) {
672      var mousePos = this.extractRelativeMousePosition_(e);
673      var res = [];
674      function handleQuad(passNumber, quad, p1, p2, p3, p4) {
675        if (tr.b.pointInImplicitQuad(mousePos, p1, p2, p3, p4))
676          res.push(quad);
677      }
678      this.stackTransformAndProcessQuads_(1, handleQuad, false);
679      var e = new tr.b.Event('selectionchange');
680      e.quads = res;
681      this.dispatchEvent(e);
682    }
683  };
684
685  return {
686    QuadStackView: QuadStackView
687  };
688});
689</script>
690