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 165export function canvasClip( 166 ctx: CanvasRenderingContext2D, 167 x: number, 168 y: number, 169 w: number, 170 h: number, 171): void { 172 ctx.beginPath(); 173 ctx.rect(x, y, w, h); 174 ctx.clip(); 175} 176 177/** 178 * Save the state of the canvas, returning a disposable which restores the state 179 * when disposed. 180 * 181 * Allows using the |using| keyword to automatically restore the canvas state. 182 * @param ctx - The canvas context to save the state of. 183 * @returns A disposable. 184 * 185 * @example 186 * { 187 * using const _ = canvasSave(ctx); 188 * ctx.translate(123, 456); // Manipulate the canvas state 189 * } // ctx.restore() is automatically called when the _ falls out of scope 190 */ 191export function canvasSave(ctx: CanvasRenderingContext2D): Disposable { 192 ctx.save(); 193 return { 194 [Symbol.dispose](): void { 195 ctx.restore(); 196 }, 197 }; 198} 199