1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {TimeRange} from 'app/timeline_data'; 18import {TRACE_INFO} from 'app/trace_info'; 19import {Timestamp, TimestampType} from 'trace/timestamp'; 20import {Trace} from 'trace/trace'; 21import {Traces} from 'trace/traces'; 22import {TraceType} from 'trace/trace_type'; 23import {Color} from '../../colors'; 24import {CanvasDrawer} from '../canvas/canvas_drawer'; 25import {CanvasMouseHandler} from '../canvas/canvas_mouse_handler'; 26import {DraggableCanvasObject} from '../canvas/draggable_canvas_object'; 27import {Segment} from './utils'; 28 29export class MiniCanvasDrawerInput { 30 constructor( 31 public fullRange: TimeRange, 32 public selectedPosition: Timestamp, 33 public selection: TimeRange, 34 public traces: Traces 35 ) {} 36 37 transform(mapToRange: Segment): MiniCanvasDrawerData { 38 const transformer = new Transformer(this.fullRange, mapToRange); 39 return new MiniCanvasDrawerData( 40 transformer.transform(this.selectedPosition), 41 { 42 from: transformer.transform(this.selection.from), 43 to: transformer.transform(this.selection.to), 44 }, 45 this.transformTracesTimestamps(transformer), 46 transformer 47 ); 48 } 49 50 private transformTracesTimestamps(transformer: Transformer): Map<TraceType, number[]> { 51 const transformedTraceSegments = new Map<TraceType, number[]>(); 52 53 this.traces.forEachTrace((trace) => { 54 transformedTraceSegments.set(trace.type, this.transformTraceTimestamps(transformer, trace)); 55 }); 56 57 return transformedTraceSegments; 58 } 59 60 private transformTraceTimestamps(transformer: Transformer, trace: Trace<{}>): number[] { 61 const result: number[] = []; 62 63 trace.forEachTimestamp((timestamp) => { 64 result.push(transformer.transform(timestamp)); 65 }); 66 67 return result; 68 } 69} 70 71export class Transformer { 72 private timestampType: TimestampType; 73 74 private fromWidth: bigint; 75 private targetWidth: number; 76 77 private fromOffset: bigint; 78 private toOffset: number; 79 80 constructor(private fromRange: TimeRange, private toRange: Segment) { 81 this.timestampType = fromRange.from.getType(); 82 83 this.fromWidth = this.fromRange.to.getValueNs() - this.fromRange.from.getValueNs(); 84 // Needs to be a whole number to be compatible with bigints 85 this.targetWidth = Math.round(this.toRange.to - this.toRange.from); 86 87 this.fromOffset = this.fromRange.from.getValueNs(); 88 // Needs to be a whole number to be compatible with bigints 89 this.toOffset = Math.round(this.toRange.from); 90 } 91 92 transform(x: Timestamp): number { 93 return ( 94 this.toOffset + 95 (this.targetWidth * Number(x.getValueNs() - this.fromOffset)) / Number(this.fromWidth) 96 ); 97 } 98 99 untransform(x: number): Timestamp { 100 x = Math.round(x); 101 const valueNs = 102 this.fromOffset + (BigInt(x - this.toOffset) * this.fromWidth) / BigInt(this.targetWidth); 103 return new Timestamp(this.timestampType, valueNs); 104 } 105} 106 107class MiniCanvasDrawerOutput { 108 constructor(public selectedPosition: Timestamp, public selection: TimeRange) {} 109} 110 111class MiniCanvasDrawerData { 112 constructor( 113 public selectedPosition: number, 114 public selection: Segment, 115 public timelineEntries: Map<TraceType, number[]>, 116 public transformer: Transformer 117 ) {} 118 119 toOutput(): MiniCanvasDrawerOutput { 120 return new MiniCanvasDrawerOutput(this.transformer.untransform(this.selectedPosition), { 121 from: this.transformer.untransform(this.selection.from), 122 to: this.transformer.untransform(this.selection.to), 123 }); 124 } 125} 126 127export class MiniCanvasDrawer implements CanvasDrawer { 128 ctx: CanvasRenderingContext2D; 129 handler: CanvasMouseHandler; 130 131 private activePointer: DraggableCanvasObject; 132 private leftFocusSectionSelector: DraggableCanvasObject; 133 private rightFocusSectionSelector: DraggableCanvasObject; 134 135 private get pointerWidth() { 136 return this.getHeight() / 6; 137 } 138 139 getXScale() { 140 return this.ctx.getTransform().m11; 141 } 142 143 getYScale() { 144 return this.ctx.getTransform().m22; 145 } 146 147 getWidth() { 148 return this.canvas.width / this.getXScale(); 149 } 150 151 getHeight() { 152 return this.canvas.height / this.getYScale(); 153 } 154 155 get usableRange() { 156 return { 157 from: this.padding.left, 158 to: this.getWidth() - this.padding.left - this.padding.right, 159 }; 160 } 161 162 get input() { 163 return this.inputGetter().transform(this.usableRange); 164 } 165 166 constructor( 167 public canvas: HTMLCanvasElement, 168 private inputGetter: () => MiniCanvasDrawerInput, 169 private onPointerPositionDragging: (pos: Timestamp) => void, 170 private onPointerPositionChanged: (pos: Timestamp) => void, 171 private onSelectionChanged: (selection: TimeRange) => void, 172 private onUnhandledClick: (pos: Timestamp) => void 173 ) { 174 const ctx = canvas.getContext('2d'); 175 176 if (ctx === null) { 177 throw Error('MiniTimeline canvas context was null!'); 178 } 179 180 this.ctx = ctx; 181 182 const onUnhandledClickInternal = (x: number, y: number) => { 183 this.onUnhandledClick(this.input.transformer.untransform(x)); 184 }; 185 this.handler = new CanvasMouseHandler(this, 'pointer', onUnhandledClickInternal); 186 187 this.activePointer = new DraggableCanvasObject( 188 this, 189 () => this.selectedPosition, 190 (ctx: CanvasRenderingContext2D, position: number) => { 191 const barWidth = 3; 192 const triangleHeight = this.pointerWidth / 2; 193 194 ctx.beginPath(); 195 ctx.moveTo(position - triangleHeight, 0); 196 ctx.lineTo(position + triangleHeight, 0); 197 ctx.lineTo(position + barWidth / 2, triangleHeight); 198 ctx.lineTo(position + barWidth / 2, this.getHeight()); 199 ctx.lineTo(position - barWidth / 2, this.getHeight()); 200 ctx.lineTo(position - barWidth / 2, triangleHeight); 201 ctx.closePath(); 202 }, 203 { 204 fillStyle: Color.ACTIVE_POINTER, 205 fill: true, 206 }, 207 (x) => { 208 this.input.selectedPosition = x; 209 this.onPointerPositionDragging(this.input.transformer.untransform(x)); 210 }, 211 (x) => { 212 this.input.selectedPosition = x; 213 this.onPointerPositionChanged(this.input.transformer.untransform(x)); 214 }, 215 () => this.usableRange 216 ); 217 218 const focusSelectorDrawConfig = { 219 fillStyle: Color.SELECTOR_COLOR, 220 fill: true, 221 }; 222 223 const onLeftSelectionChanged = (x: number) => { 224 this.selection.from = x; 225 this.onSelectionChanged({ 226 from: this.input.transformer.untransform(x), 227 to: this.input.transformer.untransform(this.selection.to), 228 }); 229 }; 230 const onRightSelectionChanged = (x: number) => { 231 this.selection.to = x; 232 this.onSelectionChanged({ 233 from: this.input.transformer.untransform(this.selection.from), 234 to: this.input.transformer.untransform(x), 235 }); 236 }; 237 238 const barWidth = 6; 239 const selectorArrowWidth = this.innerHeight / 12; 240 const selectorArrowHeight = selectorArrowWidth * 2; 241 242 this.leftFocusSectionSelector = new DraggableCanvasObject( 243 this, 244 () => this.selection.from, 245 (ctx: CanvasRenderingContext2D, position: number) => { 246 ctx.beginPath(); 247 ctx.moveTo(position - barWidth, this.padding.top); 248 ctx.lineTo(position, this.padding.top); 249 ctx.lineTo(position + selectorArrowWidth, this.padding.top + selectorArrowWidth); 250 ctx.lineTo(position, this.padding.top + selectorArrowHeight); 251 ctx.lineTo(position, this.padding.top + this.innerHeight); 252 ctx.lineTo(position - barWidth, this.padding.top + this.innerHeight); 253 ctx.lineTo(position - barWidth, this.padding.top); 254 ctx.closePath(); 255 }, 256 focusSelectorDrawConfig, 257 onLeftSelectionChanged, 258 onLeftSelectionChanged, 259 () => { 260 return { 261 from: this.usableRange.from, 262 to: this.rightFocusSectionSelector.position - selectorArrowWidth - barWidth, 263 }; 264 } 265 ); 266 267 this.rightFocusSectionSelector = new DraggableCanvasObject( 268 this, 269 () => this.selection.to, 270 (ctx: CanvasRenderingContext2D, position: number) => { 271 ctx.beginPath(); 272 ctx.moveTo(position + barWidth, this.padding.top); 273 ctx.lineTo(position, this.padding.top); 274 ctx.lineTo(position - selectorArrowWidth, this.padding.top + selectorArrowWidth); 275 ctx.lineTo(position, this.padding.top + selectorArrowHeight); 276 ctx.lineTo(position, this.padding.top + this.innerHeight); 277 ctx.lineTo(position + barWidth, this.padding.top + this.innerHeight); 278 ctx.closePath(); 279 }, 280 focusSelectorDrawConfig, 281 onRightSelectionChanged, 282 onRightSelectionChanged, 283 () => { 284 return { 285 from: this.leftFocusSectionSelector.position + selectorArrowWidth + barWidth, 286 to: this.usableRange.to, 287 }; 288 } 289 ); 290 } 291 292 get selectedPosition() { 293 return this.input.selectedPosition; 294 } 295 296 get selection() { 297 return this.input.selection; 298 } 299 300 get timelineEntries() { 301 return this.input.timelineEntries; 302 } 303 304 get padding() { 305 return { 306 top: Math.ceil(this.getHeight() / 5), 307 bottom: Math.ceil(this.getHeight() / 5), 308 left: Math.ceil(this.pointerWidth / 2), 309 right: Math.ceil(this.pointerWidth / 2), 310 }; 311 } 312 313 get innerHeight() { 314 return this.getHeight() - this.padding.top - this.padding.bottom; 315 } 316 317 draw() { 318 this.ctx.clearRect(0, 0, this.getWidth(), this.getHeight()); 319 320 this.drawSelectionBackground(); 321 322 this.drawTraceLines(); 323 324 this.drawTimelineGuides(); 325 326 this.leftFocusSectionSelector.draw(this.ctx); 327 this.rightFocusSectionSelector.draw(this.ctx); 328 329 this.activePointer.draw(this.ctx); 330 } 331 332 private drawSelectionBackground() { 333 const triangleHeight = this.innerHeight / 6; 334 335 // Selection background 336 this.ctx.globalAlpha = 0.8; 337 this.ctx.fillStyle = Color.SELECTION_BACKGROUND; 338 const width = this.selection.to - this.selection.from; 339 this.ctx.fillRect( 340 this.selection.from, 341 this.padding.top + triangleHeight / 2, 342 width, 343 this.innerHeight - triangleHeight / 2 344 ); 345 this.ctx.restore(); 346 } 347 348 private drawTraceLines() { 349 const lineHeight = this.innerHeight / 8; 350 351 let fromTop = this.padding.top + (this.innerHeight * 2) / 3 - lineHeight; 352 353 this.timelineEntries.forEach((entries, traceType) => { 354 // TODO: Only if active or a selected trace 355 for (const entry of entries) { 356 this.ctx.globalAlpha = 0.7; 357 this.ctx.fillStyle = TRACE_INFO[traceType].color; 358 359 const width = 5; 360 this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight); 361 this.ctx.globalAlpha = 1.0; 362 } 363 364 fromTop -= (lineHeight * 4) / 3; 365 }); 366 } 367 368 private drawTimelineGuides() { 369 const edgeBarHeight = (this.innerHeight * 1) / 2; 370 const edgeBarWidth = 4; 371 372 const boldBarHeight = (this.innerHeight * 1) / 5; 373 const boldBarWidth = edgeBarWidth; 374 375 const lightBarHeight = (this.innerHeight * 1) / 6; 376 const lightBarWidth = 2; 377 378 const minSpacing = lightBarWidth * 7; 379 const barsInSetWidth = 9 * lightBarWidth + boldBarWidth; 380 const barSets = Math.floor( 381 (this.getWidth() - edgeBarWidth * 2 - minSpacing) / (barsInSetWidth + 10 * minSpacing) 382 ); 383 const bars = barSets * 10; 384 385 // Draw start bar 386 this.ctx.fillStyle = Color.GUIDE_BAR; 387 this.ctx.fillRect( 388 0, 389 this.padding.top + this.innerHeight - edgeBarHeight, 390 edgeBarWidth, 391 edgeBarHeight 392 ); 393 394 // Draw end bar 395 this.ctx.fillStyle = Color.GUIDE_BAR; 396 this.ctx.fillRect( 397 this.getWidth() - edgeBarWidth, 398 this.padding.top + this.innerHeight - edgeBarHeight, 399 edgeBarWidth, 400 edgeBarHeight 401 ); 402 403 const spacing = (this.getWidth() - barSets * barsInSetWidth - edgeBarWidth) / bars; 404 let start = edgeBarWidth + spacing; 405 for (let i = 1; i < bars; i++) { 406 if (i % 10 === 0) { 407 // Draw boldbar 408 this.ctx.fillStyle = Color.GUIDE_BAR; 409 this.ctx.fillRect( 410 start, 411 this.padding.top + this.innerHeight - boldBarHeight, 412 boldBarWidth, 413 boldBarHeight 414 ); 415 start += boldBarWidth; // TODO: Shift a bit 416 } else { 417 // Draw lightbar 418 this.ctx.fillStyle = Color.GUIDE_BAR_LIGHT; 419 this.ctx.fillRect( 420 start, 421 this.padding.top + this.innerHeight - lightBarHeight, 422 lightBarWidth, 423 lightBarHeight 424 ); 425 start += lightBarWidth; 426 } 427 start += spacing; 428 } 429 } 430} 431