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