// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
base.requireStylesheet('ui.quad_view');
base.require('base.color');
base.require('base.events');
base.require('base.raf');
base.require('ui');
base.require('ui.quad_view_viewport');
base.exportTo('ui', function() {
// FIXME(pdr): Remove this extra scaling so our rasters are pixel-perfect.
// https://code.google.com/p/trace-viewer/issues/detail?id=228
// FIXME(jjb): simplify until we have the camera working (or 228 happens ;-)
var RASTER_SCALE = 1.0; // Adjust the resolution of our backing canvases.
// Care of bckenney@ via
// http://extremelysatisfactorytotalitarianism.com/blog/?p=2120
function drawTexturedTriangle(
ctx,
img, x0, y0, x1, y1, x2, y2,
u0, v0, u1, v1, u2, v2) {
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
x1 -= x0;
y1 -= y0;
x2 -= x0;
y2 -= y0;
u1 -= u0;
v1 -= v0;
u2 -= u0;
v2 -= v0;
var det = 1 / (u1 * v2 - u2 * v1),
// linear transformation
a = (v2 * x1 - v1 * x2) * det,
b = (v2 * y1 - v1 * y2) * det,
c = (u1 * x2 - u2 * x1) * det,
d = (u1 * y2 - u2 * y1) * det,
// translation
e = x0 - a * u0 - c * v0,
f = y0 - b * u0 - d * v0;
ctx.save();
ctx.transform(a, b, c, d, e, f);
ctx.clip();
ctx.drawImage(img, 0, 0);
ctx.restore();
}
var QuadView = ui.define('quad-view');
QuadView.prototype = {
__proto__: HTMLUnknownElement.prototype,
decorate: function() {
base.EventTargetHelper.decorate(this);
this.quads_ = undefined;
this.viewport_ = undefined;
this.canvas_ = document.createElement('canvas');
this.appendChild(this.canvas_);
this.onViewportChanged_ = this.onViewportChanged_.bind(this);
this.onMouseDown_ = this.onMouseDown_.bind(this);
this.onMouseMove_ = this.onMouseMove_.bind(this);
this.onMouseUp_ = this.onMouseUp_.bind(this);
this.canvas_.addEventListener('mousedown', this.onMouseDown_);
this.canvas_.addEventListener('focus', this.redrawCanvas_.bind(this));
this.canvas_.addEventListener('blur', this.redrawCanvas_.bind(this));
this.canvas_.tabIndex = 0;
},
get viewport() {
return this.viewport_;
},
set viewport(viewport) {
if (this.viewport_)
this.viewport_.removeEventListener('change', this.onViewportChanged_);
this.viewport_ = viewport;
if (this.viewport_)
this.viewport_.addEventListener('change', this.onViewportChanged_);
this.updateChildren_();
},
onViewportChanged_: function() {
if (!this.hasRequiredProprties_)
return;
this.redrawCanvas_();
},
get quads() {
return this.quads_;
},
set quads(quads) {
this.quads_ = quads;
if (!this.quads_) {
this.updateChildren_();
return;
}
this.viewport_ = this.viewport_ ||
this.createViewportFromQuads_(this.quads_);
this.updateChildren_();
},
get hasRequiredProprties_() {
return this.quads_ &&
this.viewport_;
},
updateChildren_: function() {
var canvas = this.canvas_;
if (!this.hasRequiredProprties_) {
canvas.width = 0;
canvas.height = 0;
return;
}
this.scheduleRedrawCanvas_();
},
scheduleRedrawCanvas_: function() {
if (this.redrawScheduled_)
return false;
this.redrawScheduled_ = true;
base.requestAnimationFrameInThisFrameIfPossible(
this.redrawCanvas_, this);
},
redrawCanvas_: function() {
this.redrawScheduled_ = false;
var resizedCanvas = this.viewport_.updateBoxSize(this.canvas_);
var ctx = this.canvas_.getContext('2d');
var vp = this.viewport_;
if (!resizedCanvas) // Canvas resizing automatically clears the context.
ctx.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
ctx.save();
ctx.scale(ui.RASTER_SCALE, ui.RASTER_SCALE);
// The quads are in the world coordinate system. We are drawing
// into a canvas with 0,0 in the top left corner. Tell the canvas to
// transform drawing ops from world to canvas coordinates.
vp.applyTransformToContext(ctx);
ctx.lineWidth = vp.getDeviceLineWidthAssumingTransformIsApplied(1.0);
var quads = this.quads_ || [];
// Background colors.
for (var i = 0; i < quads.length; i++) {
var quad = quads[i];
if (quad.canvas) {
if (quad.isRectangle()) {
var bounds = quad.boundingRect();
ctx.drawImage(quad.canvas, 0, 0,
quad.canvas.width, quad.canvas.height,
bounds.x, bounds.y, bounds.width, bounds.height);
} else {
ctx.save();
var quadBBox = new base.BBox2();
quadBBox.addQuad(quad);
var iw = quad.canvas.width;
var ih = quad.canvas.height;
drawTexturedTriangle(
ctx, quad.canvas,
quad.p1[0], quad.p1[1],
quad.p2[0], quad.p2[1],
quad.p4[0], quad.p4[1],
0, 0, iw, 0, 0, ih);
drawTexturedTriangle(
ctx, quad.canvas,
quad.p2[0], quad.p2[1],
quad.p3[0], quad.p3[1],
quad.p4[0], quad.p4[1],
iw, 0, iw, ih, 0, ih);
ctx.restore();
}
}
if (quad.backgroundColor) {
ctx.fillStyle = quad.backgroundColor;
ctx.beginPath();
ctx.moveTo(quad.p1[0], quad.p1[1]);
ctx.lineTo(quad.p2[0], quad.p2[1]);
ctx.lineTo(quad.p3[0], quad.p3[1]);
ctx.lineTo(quad.p4[0], quad.p4[1]);
ctx.closePath();
ctx.fill();
}
}
// Outlines.
for (var i = 0; i < quads.length; i++) {
var quad = quads[i];
ctx.beginPath();
ctx.moveTo(quad.p1[0], quad.p1[1]);
ctx.lineTo(quad.p2[0], quad.p2[1]);
ctx.lineTo(quad.p3[0], quad.p3[1]);
ctx.lineTo(quad.p4[0], quad.p4[1]);
ctx.closePath();
if (quad.borderColor)
ctx.strokeStyle = quad.borderColor;
else
ctx.strokeStyle = 'rgb(128,128,128)';
ctx.stroke();
}
// Selection outlines.
ctx.lineWidth = vp.getDeviceLineWidthAssumingTransformIsApplied(8.0);
var rules = window.getMatchedCSSRules(this.canvas_);
// TODO(nduca): Figure out how to get these from css.
for (var i = 0; i < quads.length; i++) {
var quad = quads[i];
if (!quad.upperBorderColor)
continue;
if (document.activeElement == this.canvas_) {
var tmp = base.Color.fromString(quad.upperBorderColor).brighten(0.25);
ctx.strokeStyle = tmp.toString();
} else {
ctx.strokeStyle = quad.upperBorderColor;
}
ctx.beginPath();
ctx.moveTo(quad.p1[0], quad.p1[1]);
ctx.lineTo(quad.p2[0], quad.p2[1]);
ctx.lineTo(quad.p3[0], quad.p3[1]);
ctx.lineTo(quad.p4[0], quad.p4[1]);
ctx.closePath();
ctx.stroke();
}
ctx.restore();
},
selectQuadsAtCanvasClientPoint: function(clientX, clientY) {
clientX *= ui.RASTER_SCALE;
clientY *= ui.RASTER_SCALE;
var selectedQuadIndices = this.findQuadsAtCanvasClientPoint(
clientX, clientY);
var e = new base.Event('selectionChanged');
e.selectedQuadIndices = selectedQuadIndices;
this.dispatchEvent(e);
this.viewport_.forceRedrawAll();
},
findQuadsAtCanvasClientPoint: function(clientX, clientY) {
var bounds = this.canvas_.getBoundingClientRect();
var vecInLayout = vec2.createXY(clientX - bounds.left,
clientY - bounds.top);
var vecInWorldPixels =
this.viewport_.layoutPixelsToWorldPixels(vecInLayout);
var quads = this.quads_;
var hitIndices = [];
for (var i = 0; i < quads.length; i++) {
var hit = quads[i].vecInside(vecInWorldPixels);
if (hit)
hitIndices.push(i);
}
return hitIndices;
},
createViewportFromQuads_: function() {
var quads = this.quads_ || [];
var quadBBox = new base.BBox2();
quads.forEach(function(quad) {
quadBBox.addQuad(quad);
});
return new ui.QuadViewViewport(quadBBox.asRect());
},
onMouseDown_: function(e) {
if (!this.hasEventListener('selectionChanged'))
return;
this.selectQuadsAtCanvasClientPoint(e.clientX, e.clientY);
document.addEventListener('mousemove', this.onMouseMove_);
document.addEventListener('mouseup', this.onMouseUp_);
e.preventDefault();
this.canvas_.focus();
return true;
},
onMouseMove_: function(e) {
this.selectQuadsAtCanvasClientPoint(e.clientX, e.clientY);
},
onMouseUp_: function(e) {
this.selectQuadsAtCanvasClientPoint(e.clientX, e.clientY);
document.removeEventListener('mousemove', this.onMouseMove_);
document.removeEventListener('mouseup', this.onMouseUp_);
}
};
return {
QuadView: QuadView,
RASTER_SCALE: RASTER_SCALE
};
});