| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {Size2D, Point2D} from './geom'; |
| import {isString} from './object_utils'; |
| |
| export function drawDoubleHeadedArrow( |
| ctx: CanvasRenderingContext2D, |
| x: number, |
| y: number, |
| length: number, |
| showArrowHeads: boolean, |
| width = 2, |
| color = 'black', |
| ) { |
| ctx.beginPath(); |
| ctx.lineWidth = width; |
| ctx.lineCap = 'round'; |
| ctx.strokeStyle = color; |
| ctx.moveTo(x, y); |
| ctx.lineTo(x + length, y); |
| ctx.stroke(); |
| ctx.closePath(); |
| // Arrowheads on the each end of the line. |
| if (showArrowHeads) { |
| ctx.beginPath(); |
| ctx.moveTo(x + length - 8, y - 4); |
| ctx.lineTo(x + length, y); |
| ctx.lineTo(x + length - 8, y + 4); |
| ctx.stroke(); |
| ctx.closePath(); |
| ctx.beginPath(); |
| ctx.moveTo(x + 8, y - 4); |
| ctx.lineTo(x, y); |
| ctx.lineTo(x + 8, y + 4); |
| ctx.stroke(); |
| ctx.closePath(); |
| } |
| } |
| |
| export function drawIncompleteSlice( |
| ctx: CanvasRenderingContext2D, |
| x: number, |
| y: number, |
| width: number, |
| height: number, |
| showGradient: boolean = true, |
| ) { |
| if (width <= 0 || height <= 0) { |
| return; |
| } |
| ctx.beginPath(); |
| const triangleSize = height / 4; |
| ctx.moveTo(x, y); |
| ctx.lineTo(x + width, y); |
| ctx.lineTo(x + width - 3, y + triangleSize * 0.5); |
| ctx.lineTo(x + width, y + triangleSize); |
| ctx.lineTo(x + width - 3, y + triangleSize * 1.5); |
| ctx.lineTo(x + width, y + 2 * triangleSize); |
| ctx.lineTo(x + width - 3, y + triangleSize * 2.5); |
| ctx.lineTo(x + width, y + 3 * triangleSize); |
| ctx.lineTo(x + width - 3, y + triangleSize * 3.5); |
| ctx.lineTo(x + width, y + 4 * triangleSize); |
| ctx.lineTo(x, y + height); |
| |
| const fillStyle = ctx.fillStyle; |
| if (isString(fillStyle)) { |
| if (showGradient) { |
| const gradient = ctx.createLinearGradient(x, y, x + width, y + height); |
| gradient.addColorStop(0.66, fillStyle); |
| gradient.addColorStop(1, '#FFFFFF'); |
| ctx.fillStyle = gradient; |
| } |
| } else { |
| throw new Error( |
| `drawIncompleteSlice() expects fillStyle to be a simple color not ${fillStyle}`, |
| ); |
| } |
| |
| ctx.fill(); |
| ctx.fillStyle = fillStyle; |
| } |
| |
| export function drawTrackHoverTooltip( |
| ctx: CanvasRenderingContext2D, |
| pos: Point2D, |
| trackSize: Size2D, |
| text: string, |
| text2?: string, |
| ) { |
| ctx.font = '10px Roboto Condensed'; |
| ctx.textBaseline = 'middle'; |
| ctx.textAlign = 'left'; |
| |
| // TODO(hjd): Avoid measuring text all the time (just use monospace?) |
| const textMetrics = ctx.measureText(text); |
| const text2Metrics = ctx.measureText(text2 ?? ''); |
| |
| // Padding on each side of the box containing the tooltip: |
| const paddingPx = 4; |
| |
| // Figure out the width of the tool tip box: |
| let width = Math.max(textMetrics.width, text2Metrics.width); |
| width += paddingPx * 2; |
| |
| // and the height: |
| let height = 0; |
| height += textMetrics.fontBoundingBoxAscent; |
| height += textMetrics.fontBoundingBoxDescent; |
| if (text2 !== undefined) { |
| height += text2Metrics.fontBoundingBoxAscent; |
| height += text2Metrics.fontBoundingBoxDescent; |
| } |
| height += paddingPx * 2; |
| |
| let x = pos.x; |
| let y = pos.y; |
| |
| // Move box to the top right of the mouse: |
| x += 10; |
| y -= 10; |
| |
| // Ensure the box is on screen: |
| const endPx = trackSize.width; |
| if (x + width > endPx) { |
| x -= x + width - endPx; |
| } |
| if (y < 0) { |
| y = 0; |
| } |
| if (y + height > trackSize.height) { |
| y -= y + height - trackSize.height; |
| } |
| |
| // Draw everything: |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; |
| ctx.fillRect(x, y, width, height); |
| |
| ctx.fillStyle = 'hsl(200, 50%, 40%)'; |
| ctx.fillText( |
| text, |
| x + paddingPx, |
| y + paddingPx + textMetrics.fontBoundingBoxAscent, |
| ); |
| if (text2 !== undefined) { |
| const yOffsetPx = |
| textMetrics.fontBoundingBoxAscent + |
| textMetrics.fontBoundingBoxDescent + |
| text2Metrics.fontBoundingBoxAscent; |
| ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx); |
| } |
| } |
| |
| export function canvasClip( |
| ctx: CanvasRenderingContext2D, |
| x: number, |
| y: number, |
| w: number, |
| h: number, |
| ): void { |
| ctx.beginPath(); |
| ctx.rect(x, y, w, h); |
| ctx.clip(); |
| } |
| |
| /** |
| * Save the state of the canvas, returning a disposable which restores the state |
| * when disposed. |
| * |
| * Allows using the |using| keyword to automatically restore the canvas state. |
| * @param ctx - The canvas context to save the state of. |
| * @returns A disposable. |
| * |
| * @example |
| * { |
| * using const _ = canvasSave(ctx); |
| * ctx.translate(123, 456); // Manipulate the canvas state |
| * } // ctx.restore() is automatically called when the _ falls out of scope |
| */ |
| export function canvasSave(ctx: CanvasRenderingContext2D): Disposable { |
| ctx.save(); |
| return { |
| [Symbol.dispose](): void { |
| ctx.restore(); |
| }, |
| }; |
| } |