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 {assertDefined} from 'common/assert_utils';
18import {Rect} from 'common/rect';
19
20export class CanvasDrawer {
21  private canvas: HTMLCanvasElement | undefined;
22  private ctx: CanvasRenderingContext2D | undefined;
23
24  setCanvas(canvas: HTMLCanvasElement) {
25    this.canvas = canvas;
26    const ctx = this.canvas.getContext('2d');
27    if (ctx === null) {
28      throw new Error("Couldn't get context from canvas");
29    }
30    this.ctx = ctx;
31  }
32
33  drawRect(rect: Rect, color: string, alpha: number) {
34    if (!this.ctx) {
35      throw Error('Canvas not set');
36    }
37
38    const rgbColor = this.hexToRgb(color);
39    if (rgbColor === undefined) {
40      throw new Error('Failed to parse provided hex color');
41    }
42    const {r, g, b} = rgbColor;
43
44    this.defineRectPath(rect, this.ctx);
45    this.ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`;
46    this.ctx.fill();
47
48    this.ctx.restore();
49  }
50
51  drawRectBorder(rect: Rect) {
52    if (!this.ctx) {
53      throw Error('Canvas not set');
54    }
55    this.defineRectPath(rect, this.ctx);
56    this.highlightPath(this.ctx);
57    this.ctx.restore();
58  }
59
60  clear() {
61    assertDefined(this.ctx).clearRect(
62      0,
63      0,
64      this.getScaledCanvasWidth(),
65      this.getScaledCanvasHeight(),
66    );
67  }
68
69  getScaledCanvasWidth() {
70    return Math.floor(assertDefined(this.canvas).width / this.getXScale());
71  }
72
73  getScaledCanvasHeight() {
74    return Math.floor(assertDefined(this.canvas).height / this.getYScale());
75  }
76
77  getXScale(): number {
78    return assertDefined(this.ctx).getTransform().m11;
79  }
80
81  getYScale(): number {
82    return assertDefined(this.ctx).getTransform().m22;
83  }
84
85  private highlightPath(ctx: CanvasRenderingContext2D) {
86    ctx.globalAlpha = 1.0;
87    ctx.lineWidth = 2;
88    ctx.save();
89    ctx.clip();
90    ctx.lineWidth *= 2;
91    ctx.stroke();
92    ctx.restore();
93    ctx.stroke();
94  }
95
96  private defineRectPath(rect: Rect, ctx: CanvasRenderingContext2D) {
97    ctx.beginPath();
98    ctx.moveTo(rect.x, rect.y);
99    ctx.lineTo(rect.x + rect.w, rect.y);
100    ctx.lineTo(rect.x + rect.w, rect.y + rect.h);
101    ctx.lineTo(rect.x, rect.y + rect.h);
102    ctx.lineTo(rect.x, rect.y);
103    ctx.closePath();
104  }
105
106  private hexToRgb(hex: string): {r: number; g: number; b: number} | undefined {
107    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
108    const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
109    hex = hex.replace(shorthandRegex, (m, r, g, b) => {
110      return r + r + g + g + b + b;
111    });
112
113    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
114    return result
115      ? {
116          // tslint:disable-next-line:ban
117          r: parseInt(result[1], 16),
118          // tslint:disable-next-line:ban
119          g: parseInt(result[2], 16),
120          // tslint:disable-next-line:ban
121          b: parseInt(result[3], 16),
122        }
123      : undefined;
124  }
125}
126