xref: /aosp_15_r20/development/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2023 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 {TimelineUtils} from 'app/components/timeline/timeline_utils';
18import {assertDefined} from 'common/assert_utils';
19import {Rect} from 'common/geometry/rect';
20
21export class CanvasDrawer {
22  private canvas: HTMLCanvasElement | undefined;
23  private ctx: CanvasRenderingContext2D | undefined;
24
25  setCanvas(canvas: HTMLCanvasElement) {
26    this.canvas = canvas;
27    const ctx = this.canvas.getContext('2d');
28    if (ctx === null) {
29      throw new Error("Couldn't get context from canvas");
30    }
31    this.ctx = ctx;
32  }
33
34  drawRect(
35    rect: Rect,
36    hexColor: string,
37    alpha: number,
38    withGradientStart = false,
39    withGradientEnd = false,
40  ) {
41    if (!this.ctx) {
42      throw new Error('Canvas not set');
43    }
44
45    const rgbColor = TimelineUtils.convertHexToRgb(hexColor);
46    if (rgbColor === undefined) {
47      throw new Error('Failed to parse provided hex color');
48    }
49    const {r, g, b} = rgbColor;
50    const rgbaColor = `rgba(${r},${g},${b},${alpha})`;
51    const transparentColor = `rgba(${r},${g},${b},${0})`;
52
53    this.defineRectPath(rect, this.ctx);
54    if (withGradientStart || withGradientEnd) {
55      const gradient = this.ctx.createLinearGradient(
56        rect.x,
57        0,
58        rect.x + rect.w,
59        0,
60      );
61      const gradientRatio = Math.max(0, Math.min(25 / rect.w, 1));
62      gradient.addColorStop(
63        0,
64        withGradientStart ? transparentColor : rgbaColor,
65      );
66      gradient.addColorStop(1, withGradientEnd ? transparentColor : rgbaColor);
67      gradient.addColorStop(gradientRatio, rgbaColor);
68      gradient.addColorStop(1 - gradientRatio, rgbaColor);
69      this.ctx.fillStyle = gradient;
70    } else {
71      this.ctx.fillStyle = rgbaColor;
72    }
73    this.ctx.fill();
74
75    this.ctx.fillStyle = 'black';
76    const centerY = rect.y + rect.h / 2;
77    const marginOffset = 5;
78    if (withGradientStart) {
79      this.drawEllipsis(centerY, rect.x + marginOffset, true, rect.x + rect.w);
80    }
81
82    if (withGradientEnd) {
83      this.drawEllipsis(centerY, rect.x + rect.w - marginOffset, false, rect.x);
84    }
85
86    this.ctx.restore();
87  }
88
89  private drawEllipsis(
90    centerY: number,
91    startX: number,
92    forwards: boolean,
93    xLim: number,
94  ) {
95    if (!this.ctx) {
96      return;
97    }
98    let centerX = startX;
99    let i = 0;
100    const radius = 2;
101    while (i < 3) {
102      if (forwards && centerX + radius >= xLim) {
103        break;
104      }
105      if (!forwards && centerX + radius <= xLim) {
106        break;
107      }
108      this.ctx.beginPath();
109      this.ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
110      this.ctx.fill();
111      centerX = forwards ? centerX + 7 : centerX - 7;
112      i++;
113    }
114  }
115
116  drawRectBorder(rect: Rect) {
117    if (!this.ctx) {
118      throw new Error('Canvas not set');
119    }
120    this.defineRectPath(rect, this.ctx);
121    this.highlightPath(this.ctx);
122    this.ctx.restore();
123  }
124
125  clear() {
126    assertDefined(this.ctx).clearRect(
127      0,
128      0,
129      this.getScaledCanvasWidth(),
130      this.getScaledCanvasHeight(),
131    );
132  }
133
134  getScaledCanvasWidth() {
135    return Math.floor(assertDefined(this.canvas).width / this.getXScale());
136  }
137
138  getScaledCanvasHeight() {
139    return Math.floor(assertDefined(this.canvas).height / this.getYScale());
140  }
141
142  getXScale(): number {
143    return assertDefined(this.ctx).getTransform().m11;
144  }
145
146  getYScale(): number {
147    return assertDefined(this.ctx).getTransform().m22;
148  }
149
150  private highlightPath(ctx: CanvasRenderingContext2D) {
151    ctx.globalAlpha = 1.0;
152    ctx.lineWidth = 2;
153    ctx.save();
154    ctx.clip();
155    ctx.lineWidth *= 2;
156    ctx.stroke();
157    ctx.restore();
158    ctx.stroke();
159  }
160
161  private defineRectPath(rect: Rect, ctx: CanvasRenderingContext2D) {
162    ctx.beginPath();
163    ctx.moveTo(rect.x, rect.y);
164    ctx.lineTo(rect.x + rect.w, rect.y);
165    ctx.lineTo(rect.x + rect.w, rect.y + rect.h);
166    ctx.lineTo(rect.x, rect.y + rect.h);
167    ctx.lineTo(rect.x, rect.y);
168    ctx.closePath();
169  }
170}
171