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