// Copyright (c) 2011 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. /** * @fileoverview Renders an array of slices into the provided div, * using a child canvas element. Uses a FastRectRenderer to draw only * the visible slices. */ cr.define('gpu', function() { const palletteBase = [ {r: 0x45, g: 0x85, b: 0xaa}, {r: 0xdc, g: 0x73, b: 0xa8}, {r: 0x77, g: 0xb6, b: 0x94}, {r: 0x23, g: 0xae, b: 0x6e}, {r: 0x76, g: 0x5d, b: 0x9e}, {r: 0x48, g: 0xd8, b: 0xfb}, {r: 0xa9, g: 0xd7, b: 0x93}, {r: 0x7c, g: 0x2d, b: 0x52}, {r: 0x69, g: 0xc2, b: 0x75}, {r: 0x76, g: 0xcf, b: 0xee}, {r: 0x3d, g: 0x85, b: 0xd1}, {r: 0x71, g: 0x0b, b: 0x54}]; function brighten(c) { return {r: Math.min(255, c.r + Math.floor(c.r * 0.45)), g: Math.min(255, c.g + Math.floor(c.g * 0.45)), b: Math.min(255, c.b + Math.floor(c.b * 0.45))}; } function colorToString(c) { return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')'; } const selectedIdBoost = palletteBase.length; const pallette = palletteBase.concat(palletteBase.map(brighten)). map(colorToString); var textWidthMap = { }; function quickMeasureText(ctx, text) { var w = textWidthMap[text]; if (!w) { w = ctx.measureText(text).width; textWidthMap[text] = w; } return w; } /** * Generic base class for timeline tracks */ TimelineThreadTrack = cr.ui.define('div'); TimelineThreadTrack.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.className = 'timeline-thread-track'; }, set thread(thread) { this.thread_ = thread; this.updateChildTracks_(); }, set viewport(v) { this.viewport_ = v; for (var i = 0; i < this.tracks_.length; i++) this.tracks_[i].viewport = v; this.invalidate(); }, invalidate: function() { if (this.parentNode) this.parentNode.invalidate(); }, onResize: function() { for (var i = 0; i < this.tracks_.length; i++) this.tracks_[i].onResize(); }, get firstCanvas() { if (this.tracks_.length) return this.tracks_[0].firstCanvas; return undefined; }, redraw: function() { for (var i = 0; i < this.tracks_.length; i++) this.tracks_[i].redraw(); }, updateChildTracks_: function() { this.textContent = ''; this.tracks_ = []; if (this.thread_) { for (var srI = 0; srI < this.thread_.subRows.length; ++srI) { var track = new TimelineSliceTrack(); if (srI == 0) track.heading = this.thread_.parent.pid + ': ' + this.thread_.tid + ': '; else track.heading = ''; track.slices = this.thread_.subRows[srI]; track.viewport = this.viewport_; this.tracks_.push(track); this.appendChild(track); } } }, /** * Picks a slice, if any, at a given location. * @param {number} wX X location to search at, in worldspace. * @param {number} wY Y location to search at, in offset space. * offset space. * @param {function():*} onHitCallback Callback to call with the slice, * if one is found. * @return {boolean} true if a slice was found, otherwise false. */ pick: function(wX, wY, onHitCallback) { for (var i = 0; i < this.tracks_.length; i++) { var track = this.tracks_[i]; if (wY >= track.offsetTop && wY < track.offsetTop + track.offsetHeight) return track.pick(wX, onHitCallback); } return false; }, /** * Finds slices intersecting the given interval. * @param {number} loWX Lower X bound of the interval to search, in * worldspace. * @param {number} hiWX Upper X bound of the interval to search, in * worldspace. * @param {number} loY Lower Y bound of the interval to search, in * offset space. * @param {number} hiY Upper Y bound of the interval to search, in * offset space. * @param {function():*} onHitCallback Function to call for each slice * intersecting the interval. */ pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { for (var i = 0; i < this.tracks_.length; i++) { var a = Math.max(loY, this.tracks_[i].offsetTop); var b = Math.min(hiY, this.tracks_[i].offsetTop + this.tracks_[i].offsetHeight); if (a <= b) this.tracks_[i].pickRange(loWX, hiWX, loY, hiY, onHitCallback); } } }; /** * Creates a new timeline track div element * @constructor * @extends {HTMLDivElement} */ TimelineSliceTrack = cr.ui.define('div'); TimelineSliceTrack.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.className = 'timeline-slice-track'; this.slices_ = null; this.titleDiv_ = document.createElement('div'); this.titleDiv_.className = 'timeline-slice-track-title'; this.appendChild(this.titleDiv_); this.canvasContainer_ = document.createElement('div'); this.canvasContainer_.className = 'timeline-slice-track-canvas-container'; this.appendChild(this.canvasContainer_); this.canvas_ = document.createElement('canvas'); this.canvas_.className = 'timeline-slice-track-canvas'; this.canvasContainer_.appendChild(this.canvas_); this.ctx_ = this.canvas_.getContext('2d'); }, set heading(text) { this.titleDiv_.textContent = text; }, set slices(slices) { this.slices_ = slices; this.invalidate(); }, set viewport(v) { this.viewport_ = v; this.invalidate(); }, invalidate: function() { if (this.parentNode) this.parentNode.invalidate(); }, get firstCanvas() { return this.canvas_; }, onResize: function() { this.canvas_.width = this.canvasContainer_.clientWidth; this.canvas_.height = this.canvasContainer_.clientHeight; this.invalidate(); }, redraw: function() { if (!this.viewport_) return; var ctx = this.ctx_; var canvasW = this.canvas_.width; var canvasH = this.canvas_.height; ctx.clearRect(0, 0, canvasW, canvasH); // culling... var vp = this.viewport_; var pixWidth = vp.xViewVectorToWorld(1); var viewLWorld = vp.xViewToWorld(0); var viewRWorld = vp.xViewToWorld(this.width); // begin rendering in world space ctx.save(); vp.applyTransformToCanavs(ctx); // tracks var tr = new gpu.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth, 2 * pixWidth, viewRWorld, pallette); tr.setYandH(0, canvasH); var slices = this.slices_; for (var i = 0; i < slices.length; ++i) { var slice = slices[i]; var x = slice.start; var w = slice.duration; var colorId; colorId = slice.selected ? slice.colorId + selectedIdBoost : slice.colorId; if (w < pixWidth) w = pixWidth; tr.fillRect(x, w, colorId); } tr.flush(); ctx.restore(); // labels ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.font = '10px sans-serif'; ctx.strokeStyle = 'rgb(0,0,0)'; ctx.fillStyle = 'rgb(0,0,0)'; var quickDiscardThresshold = pixWidth * 20; // dont render until 20px wide for (var i = 0; i < slices.length; ++i) { var slice = slices[i]; if (slice.duration > quickDiscardThresshold) { var labelWidth = quickMeasureText(ctx, slice.title) + 2; var labelWidthWorld = pixWidth * labelWidth; if (labelWidthWorld < slice.duration) { var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration); ctx.fillText(slice.title, cX, 2.5); } } } }, /** * Picks a slice, if any, at a given location. * @param {number} wX X location to search at, in worldspace. * @param {number} wY Y location to search at, in offset space. * offset space. * @param {function():*} onHitCallback Callback to call with the slice, * if one is found. * @return {boolean} true if a slice was found, otherwise false. */ pick: function(wX, wY, onHitCallback) { if (wY < this.offsetTop || wY >= this.offsetTop + this.offsetHeight) return false; var x = gpu.findLowIndexInSortedIntervals(this.slices_, function(x) { return x.start; }, function(x) { return x.duration; }, wX); if (x >= 0 && x < this.slices_.length) { onHitCallback('slice', this, this.slices_[x]); return true; } return false; }, /** * Finds slices intersecting the given interval. * @param {number} loWX Lower X bound of the interval to search, in * worldspace. * @param {number} hiWX Upper X bound of the interval to search, in * worldspace. * @param {number} loY Lower Y bound of the interval to search, in * offset space. * @param {number} hiY Upper Y bound of the interval to search, in * offset space. * @param {function():*} onHitCallback Function to call for each slice * intersecting the interval. */ pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { var a = Math.max(loY, this.offsetTop); var b = Math.min(hiY, this.offsetTop + this.offsetHeight); if (a > b) return; function onPickHit(slice) { onHitCallback('slice', this, slice); } gpu.iterateOverIntersectingIntervals(this.slices_, function(x) { return x.start; }, function(x) { return x.duration; }, loWX, hiWX, onPickHit); } }; return { TimelineSliceTrack: TimelineSliceTrack, TimelineThreadTrack: TimelineThreadTrack }; });