1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {Size2D, Point2D} from './geom'; 16import {isString} from './object_utils'; 17 18export function drawDoubleHeadedArrow( 19 ctx: CanvasRenderingContext2D, 20 x: number, 21 y: number, 22 length: number, 23 showArrowHeads: boolean, 24 width = 2, 25 color = 'black', 26) { 27 ctx.beginPath(); 28 ctx.lineWidth = width; 29 ctx.lineCap = 'round'; 30 ctx.strokeStyle = color; 31 ctx.moveTo(x, y); 32 ctx.lineTo(x + length, y); 33 ctx.stroke(); 34 ctx.closePath(); 35 // Arrowheads on the each end of the line. 36 if (showArrowHeads) { 37 ctx.beginPath(); 38 ctx.moveTo(x + length - 8, y - 4); 39 ctx.lineTo(x + length, y); 40 ctx.lineTo(x + length - 8, y + 4); 41 ctx.stroke(); 42 ctx.closePath(); 43 ctx.beginPath(); 44 ctx.moveTo(x + 8, y - 4); 45 ctx.lineTo(x, y); 46 ctx.lineTo(x + 8, y + 4); 47 ctx.stroke(); 48 ctx.closePath(); 49 } 50} 51 52export function drawIncompleteSlice( 53 ctx: CanvasRenderingContext2D, 54 x: number, 55 y: number, 56 width: number, 57 height: number, 58 showGradient: boolean = true, 59) { 60 if (width <= 0 || height <= 0) { 61 return; 62 } 63 ctx.beginPath(); 64 const triangleSize = height / 4; 65 ctx.moveTo(x, y); 66 ctx.lineTo(x + width, y); 67 ctx.lineTo(x + width - 3, y + triangleSize * 0.5); 68 ctx.lineTo(x + width, y + triangleSize); 69 ctx.lineTo(x + width - 3, y + triangleSize * 1.5); 70 ctx.lineTo(x + width, y + 2 * triangleSize); 71 ctx.lineTo(x + width - 3, y + triangleSize * 2.5); 72 ctx.lineTo(x + width, y + 3 * triangleSize); 73 ctx.lineTo(x + width - 3, y + triangleSize * 3.5); 74 ctx.lineTo(x + width, y + 4 * triangleSize); 75 ctx.lineTo(x, y + height); 76 77 const fillStyle = ctx.fillStyle; 78 if (isString(fillStyle)) { 79 if (showGradient) { 80 const gradient = ctx.createLinearGradient(x, y, x + width, y + height); 81 gradient.addColorStop(0.66, fillStyle); 82 gradient.addColorStop(1, '#FFFFFF'); 83 ctx.fillStyle = gradient; 84 } 85 } else { 86 throw new Error( 87 `drawIncompleteSlice() expects fillStyle to be a simple color not ${fillStyle}`, 88 ); 89 } 90 91 ctx.fill(); 92 ctx.fillStyle = fillStyle; 93} 94 95export function drawTrackHoverTooltip( 96 ctx: CanvasRenderingContext2D, 97 pos: Point2D, 98 trackSize: Size2D, 99 text: string, 100 text2?: string, 101) { 102 ctx.font = '10px Roboto Condensed'; 103 ctx.textBaseline = 'middle'; 104 ctx.textAlign = 'left'; 105 106 // TODO(hjd): Avoid measuring text all the time (just use monospace?) 107 const textMetrics = ctx.measureText(text); 108 const text2Metrics = ctx.measureText(text2 ?? ''); 109 110 // Padding on each side of the box containing the tooltip: 111 const paddingPx = 4; 112 113 // Figure out the width of the tool tip box: 114 let width = Math.max(textMetrics.width, text2Metrics.width); 115 width += paddingPx * 2; 116 117 // and the height: 118 let height = 0; 119 height += textMetrics.fontBoundingBoxAscent; 120 height += textMetrics.fontBoundingBoxDescent; 121 if (text2 !== undefined) { 122 height += text2Metrics.fontBoundingBoxAscent; 123 height += text2Metrics.fontBoundingBoxDescent; 124 } 125 height += paddingPx * 2; 126 127 let x = pos.x; 128 let y = pos.y; 129 130 // Move box to the top right of the mouse: 131 x += 10; 132 y -= 10; 133 134 // Ensure the box is on screen: 135 const endPx = trackSize.width; 136 if (x + width > endPx) { 137 x -= x + width - endPx; 138 } 139 if (y < 0) { 140 y = 0; 141 } 142 if (y + height > trackSize.height) { 143 y -= y + height - trackSize.height; 144 } 145 146 // Draw everything: 147 ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; 148 ctx.fillRect(x, y, width, height); 149 150 ctx.fillStyle = 'hsl(200, 50%, 40%)'; 151 ctx.fillText( 152 text, 153 x + paddingPx, 154 y + paddingPx + textMetrics.fontBoundingBoxAscent, 155 ); 156 if (text2 !== undefined) { 157 const yOffsetPx = 158 textMetrics.fontBoundingBoxAscent + 159 textMetrics.fontBoundingBoxDescent + 160 text2Metrics.fontBoundingBoxAscent; 161 ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx); 162 } 163} 164 165/** 166 * Clip a canvas using a rect-like object. 167 * 168 * @param ctx - The canvas context to clip. 169 * @param rect - The position and dimensions of the rect to clip. 170 */ 171export function canvasClip( 172 ctx: CanvasRenderingContext2D, 173 rect: Point2D & Size2D, 174): void; 175 176/** 177 * Clip a canvas using a separate x, y, width, height values. 178 * 179 * @param ctx - The canvas context to clip. 180 */ 181export function canvasClip( 182 ctx: CanvasRenderingContext2D, 183 x: number, 184 y: number, 185 w: number, 186 h: number, 187): void; 188 189// This function can either take individual x, y, w, h parameters to define the 190// rect, or x can be a rect-like object. 191export function canvasClip( 192 ctx: CanvasRenderingContext2D, 193 x: number | (Point2D & Size2D), 194 y?: number, 195 w?: number, 196 h?: number, 197): void { 198 ctx.beginPath(); 199 if (typeof x === 'number') { 200 // TypeScript ensures y, w, and h are defined here 201 ctx.rect(x, y!, w!, h!); 202 } else { 203 ctx.rect(x.x, x.y, x.width, x.height); 204 } 205 ctx.clip(); 206} 207 208/** 209 * Save the state of the canvas, returning a disposable which restores the state 210 * when disposed. 211 * 212 * Allows using the |using| keyword to automatically restore the canvas state. 213 * @param ctx - The canvas context to save the state of. 214 * @returns A disposable. 215 * 216 * @example 217 * { 218 * using const _ = canvasSave(ctx); 219 * ctx.translate(123, 456); // Manipulate the canvas state 220 * } // ctx.restore() is automatically called when the _ falls out of scope 221 */ 222export function canvasSave(ctx: CanvasRenderingContext2D): Disposable { 223 ctx.save(); 224 return { 225 [Symbol.dispose](): void { 226 ctx.restore(); 227 }, 228 }; 229} 230