Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 1 | // Copyright 2020 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | // Copyright (C) 2012 Google Inc. All rights reserved. |
| 6 | |
| 7 | // Redistribution and use in source and binary forms, with or without |
| 8 | // modification, are permitted provided that the following conditions |
| 9 | // are met: |
| 10 | |
| 11 | // 1. Redistributions of source code must retain the above copyright |
| 12 | // notice, this list of conditions and the following disclaimer. |
| 13 | // 2. Redistributions in binary form must reproduce the above copyright |
| 14 | // notice, this list of conditions and the following disclaimer in the |
| 15 | // documentation and/or other materials provided with the distribution. |
| 16 | // 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| 17 | // its contributors may be used to endorse or promote products derived |
| 18 | // from this software without specific prior written permission. |
| 19 | |
| 20 | // THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| 21 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 22 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 23 | // DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| 24 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| 25 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| 26 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| 27 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| 29 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 30 | |
Philip Pfaffe | 77c04d8 | 2023-01-26 09:34:53 | [diff] [blame] | 31 | // eslint-disable-next-line rulesdir/es_modules_import |
| 32 | import { |
| 33 | rgbaToHsla, |
| 34 | rgbaToHwba, |
| 35 | type Color4D, |
| 36 | } from '../front_end/core/common/ColorUtils.js'; |
Alex Rudenko | e633829 | 2021-04-01 07:23:28 | [diff] [blame] | 37 | |
Jack Franklin | 3a80260 | 2022-07-13 08:39:42 | [diff] [blame] | 38 | import {type Bounds, type PathCommands, type Quad} from './common.js'; |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 39 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 40 | export type PathBounds = Bounds&{ |
Sigurd Schneider | 1576186 | 2021-02-04 08:05:36 | [diff] [blame] | 41 | leftmostXForY: {[key: string]: number}, |
| 42 | rightmostXForY: {[key: string]: number}, |
| 43 | topmostYForX: {[key: string]: number}, |
| 44 | bottommostYForX: {[key: string]: number}, |
Tim van der Lippe | 0ebbf49 | 2020-12-03 12:13:21 | [diff] [blame] | 45 | }; |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 46 | |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 47 | export interface LineStyle { |
| 48 | color?: string; |
| 49 | pattern?: LinePattern; |
| 50 | } |
| 51 | |
Patrick Brosset | 7c7e8f7 | 2020-11-19 12:38:09 | [diff] [blame] | 52 | export interface BoxStyle { |
| 53 | fillColor?: string; |
| 54 | hatchColor?: string; |
| 55 | } |
| 56 | |
Alex Rudenko | e633829 | 2021-04-01 07:23:28 | [diff] [blame] | 57 | export const enum LinePattern { |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 58 | Solid = 'solid', |
| 59 | Dotted = 'dotted', |
Sigurd Schneider | 9c07567 | 2021-02-03 13:16:39 | [diff] [blame] | 60 | Dashed = 'dashed', |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 61 | } |
| 62 | |
Patrick Brosset | 0f58c2f | 2020-11-26 16:05:46 | [diff] [blame] | 63 | export function drawPathWithLineStyle( |
| 64 | context: CanvasRenderingContext2D, path: Path2D, lineStyle?: LineStyle, lineWidth: number = 1) { |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 65 | if (lineStyle && lineStyle.color) { |
| 66 | context.save(); |
| 67 | context.translate(0.5, 0.5); |
Patrick Brosset | 0f58c2f | 2020-11-26 16:05:46 | [diff] [blame] | 68 | context.lineWidth = lineWidth; |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 69 | if (lineStyle.pattern === LinePattern.Dashed) { |
| 70 | context.setLineDash([3, 3]); |
| 71 | } |
| 72 | if (lineStyle.pattern === LinePattern.Dotted) { |
| 73 | context.setLineDash([2, 2]); |
| 74 | } |
| 75 | context.strokeStyle = lineStyle.color; |
| 76 | context.stroke(path); |
| 77 | context.restore(); |
| 78 | } |
| 79 | } |
| 80 | |
Patrick Brosset | 74a4f4f | 2021-02-12 08:55:11 | [diff] [blame] | 81 | export function fillPathWithBoxStyle( |
| 82 | context: CanvasRenderingContext2D, path: Path2D, bounds: PathBounds, angle: number, boxStyle?: BoxStyle) { |
| 83 | if (!boxStyle) { |
| 84 | return; |
| 85 | } |
| 86 | |
| 87 | context.save(); |
| 88 | if (boxStyle.fillColor) { |
| 89 | context.fillStyle = boxStyle.fillColor; |
| 90 | context.fill(path); |
| 91 | } |
| 92 | if (boxStyle.hatchColor) { |
| 93 | hatchFillPath(context, path, bounds, 10, boxStyle.hatchColor, angle, false); |
| 94 | } |
| 95 | context.restore(); |
| 96 | } |
| 97 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 98 | export function buildPath(commands: Array<string|number>, bounds: PathBounds, emulationScaleFactor: number): Path2D { |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 99 | let commandsIndex = 0; |
| 100 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 101 | function extractPoints(count: number): number[] { |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 102 | const points = []; |
| 103 | |
| 104 | for (let i = 0; i < count; ++i) { |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 105 | const x = Math.round(commands[commandsIndex++] as number * emulationScaleFactor); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 106 | bounds.maxX = Math.max(bounds.maxX, x); |
| 107 | bounds.minX = Math.min(bounds.minX, x); |
| 108 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 109 | const y = Math.round(commands[commandsIndex++] as number * emulationScaleFactor); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 110 | bounds.maxY = Math.max(bounds.maxY, y); |
| 111 | bounds.minY = Math.min(bounds.minY, y); |
| 112 | |
| 113 | bounds.leftmostXForY[y] = Math.min(bounds.leftmostXForY[y] || Number.MAX_VALUE, x); |
| 114 | bounds.rightmostXForY[y] = Math.max(bounds.rightmostXForY[y] || Number.MIN_VALUE, x); |
| 115 | bounds.topmostYForX[x] = Math.min(bounds.topmostYForX[x] || Number.MAX_VALUE, y); |
| 116 | bounds.bottommostYForX[x] = Math.max(bounds.bottommostYForX[x] || Number.MIN_VALUE, y); |
Patrick Brosset | 8eafacc | 2020-08-05 08:18:41 | [diff] [blame] | 117 | |
| 118 | bounds.allPoints.push({x, y}); |
| 119 | |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 120 | points.push(x, y); |
| 121 | } |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 122 | |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 123 | return points; |
| 124 | } |
| 125 | |
| 126 | const commandsLength = commands.length; |
| 127 | const path = new Path2D(); |
| 128 | while (commandsIndex < commandsLength) { |
| 129 | switch (commands[commandsIndex++]) { |
| 130 | case 'M': |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 131 | path.moveTo.apply(path, extractPoints(1) as [number, number]); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 132 | break; |
| 133 | case 'L': |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 134 | path.lineTo.apply(path, extractPoints(1) as [number, number]); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 135 | break; |
| 136 | case 'C': |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 137 | path.bezierCurveTo.apply(path, extractPoints(3) as [number, number, number, number, number, number]); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 138 | break; |
| 139 | case 'Q': |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 140 | path.quadraticCurveTo.apply(path, extractPoints(2) as [number, number, number, number]); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 141 | break; |
| 142 | case 'Z': |
| 143 | path.closePath(); |
| 144 | break; |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | return path; |
| 149 | } |
| 150 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 151 | export function emptyBounds(): PathBounds { |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 152 | const bounds = { |
| 153 | minX: Number.MAX_VALUE, |
| 154 | minY: Number.MAX_VALUE, |
Alex Rudenko | e633829 | 2021-04-01 07:23:28 | [diff] [blame] | 155 | maxX: -Number.MAX_VALUE, |
| 156 | maxY: -Number.MAX_VALUE, |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 157 | leftmostXForY: {}, |
| 158 | rightmostXForY: {}, |
| 159 | topmostYForX: {}, |
Patrick Brosset | 8eafacc | 2020-08-05 08:18:41 | [diff] [blame] | 160 | bottommostYForX: {}, |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 161 | allPoints: [], |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 162 | }; |
| 163 | return bounds; |
| 164 | } |
Patrick Brosset | d750875 | 2020-08-21 08:36:51 | [diff] [blame] | 165 | |
Sigurd Schneider | 1576186 | 2021-02-04 08:05:36 | [diff] [blame] | 166 | export function applyMatrixToPoint(point: {x: number, y: number}, matrix: DOMMatrix): {x: number, y: number} { |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 167 | let domPoint = new DOMPoint(point.x, point.y); |
| 168 | domPoint = domPoint.matrixTransform(matrix); |
| 169 | return {x: domPoint.x, y: domPoint.y}; |
Patrick Brosset | d750875 | 2020-08-21 08:36:51 | [diff] [blame] | 170 | } |
Patrick Brosset | 7c7e8f7 | 2020-11-19 12:38:09 | [diff] [blame] | 171 | |
Patrick Brosset | a1da2af | 2020-12-18 13:28:39 | [diff] [blame] | 172 | const HATCH_LINE_LENGTH = 5; |
| 173 | const HATCH_LINE_GAP = 3; |
| 174 | let hatchLinePattern: CanvasPattern; |
| 175 | let hatchLineColor: string = ''; |
| 176 | |
Patrick Brosset | 7c7e8f7 | 2020-11-19 12:38:09 | [diff] [blame] | 177 | /** |
| 178 | * Draw line hatching at a 45 degree angle for a given |
| 179 | * path. |
| 180 | * __________ |
| 181 | * |\ \ \ | |
| 182 | * | \ \ \| |
| 183 | * | \ \ | |
| 184 | * |\ \ \ | |
| 185 | * ********** |
| 186 | */ |
| 187 | export function hatchFillPath( |
| 188 | context: CanvasRenderingContext2D, path: Path2D, bounds: Bounds, delta: number, color: string, |
| 189 | rotationAngle: number, flipDirection: boolean|undefined) { |
Patrick Brosset | a1da2af | 2020-12-18 13:28:39 | [diff] [blame] | 190 | // Make the bounds be at most the canvas size if it is bigger in any direction. |
| 191 | // Making the bounds bigger than the canvas is useless as what's drawn there won't be visible. |
| 192 | if (context.canvas.width < bounds.maxX - bounds.minX || context.canvas.height < bounds.maxY - bounds.minY) { |
| 193 | bounds = { |
| 194 | minX: 0, |
| 195 | maxX: context.canvas.width, |
| 196 | minY: 0, |
| 197 | maxY: context.canvas.height, |
| 198 | allPoints: [], |
| 199 | }; |
Patrick Brosset | 7c7e8f7 | 2020-11-19 12:38:09 | [diff] [blame] | 200 | } |
Patrick Brosset | a1da2af | 2020-12-18 13:28:39 | [diff] [blame] | 201 | |
| 202 | // If we haven't done it yet, initialize an offscreen canvas used to create the dashed line repeated pattern. |
| 203 | if (!hatchLinePattern || color !== hatchLineColor) { |
| 204 | hatchLineColor = color; |
| 205 | |
| 206 | const offscreenCanvas = document.createElement('canvas'); |
| 207 | offscreenCanvas.width = delta; |
| 208 | offscreenCanvas.height = HATCH_LINE_LENGTH + HATCH_LINE_GAP; |
| 209 | |
| 210 | const offscreenCtx = offscreenCanvas.getContext('2d') as CanvasRenderingContext2D; |
| 211 | offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height); |
| 212 | offscreenCtx.rect(0, 0, 1, HATCH_LINE_LENGTH); |
| 213 | offscreenCtx.fillStyle = color; |
| 214 | offscreenCtx.fill(); |
| 215 | |
| 216 | hatchLinePattern = context.createPattern(offscreenCanvas, 'repeat') as CanvasPattern; |
| 217 | } |
| 218 | |
| 219 | context.save(); |
| 220 | |
| 221 | const matrix = new DOMMatrix(); |
| 222 | hatchLinePattern.setTransform(matrix.scale(flipDirection ? -1 : 1, 1).rotate(0, 0, -45 + rotationAngle)); |
| 223 | |
| 224 | context.fillStyle = hatchLinePattern; |
| 225 | context.fill(path); |
| 226 | |
Patrick Brosset | 7c7e8f7 | 2020-11-19 12:38:09 | [diff] [blame] | 227 | context.restore(); |
| 228 | } |
| 229 | |
| 230 | /** |
| 231 | * Given a quad, create the corresponding path object. This also accepts a list of quads to clip from the resulting |
| 232 | * path. |
| 233 | */ |
| 234 | export function createPathForQuad( |
| 235 | outerQuad: Quad, quadsToClip: Quad[], bounds: PathBounds, emulationScaleFactor: number) { |
| 236 | let commands = [ |
| 237 | 'M', |
| 238 | outerQuad.p1.x, |
| 239 | outerQuad.p1.y, |
| 240 | 'L', |
| 241 | outerQuad.p2.x, |
| 242 | outerQuad.p2.y, |
| 243 | 'L', |
| 244 | outerQuad.p3.x, |
| 245 | outerQuad.p3.y, |
| 246 | 'L', |
| 247 | outerQuad.p4.x, |
| 248 | outerQuad.p4.y, |
| 249 | ]; |
| 250 | for (const quad of quadsToClip) { |
| 251 | commands = [ |
| 252 | ...commands, 'L', quad.p4.x, quad.p4.y, 'L', quad.p3.x, quad.p3.y, 'L', quad.p2.x, |
| 253 | quad.p2.y, 'L', quad.p1.x, quad.p1.y, 'L', quad.p4.x, quad.p4.y, 'L', outerQuad.p4.x, |
| 254 | outerQuad.p4.y, |
| 255 | ]; |
| 256 | } |
| 257 | commands.push('Z'); |
| 258 | |
| 259 | return buildPath(commands, bounds, emulationScaleFactor); |
| 260 | } |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame] | 261 | |
Philip Pfaffe | 77c04d8 | 2023-01-26 09:34:53 | [diff] [blame] | 262 | export function parseHexa(hexa: string): Color4D { |
| 263 | return (hexa.match(/#(\w\w)(\w\w)(\w\w)(\w\w)/) || []).slice(1).map(c => parseInt(c, 16) / 255) as Color4D; |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame] | 264 | } |
| 265 | |
Philip Pfaffe | 77c04d8 | 2023-01-26 09:34:53 | [diff] [blame] | 266 | export function formatRgba(rgba: Color4D, colorFormat: 'rgb'|'hsl'|'hwb'): string { |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame] | 267 | if (colorFormat === 'rgb') { |
Alex Rudenko | 529498f | 2021-02-05 14:08:27 | [diff] [blame] | 268 | const [r, g, b, a] = rgba; |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame] | 269 | // rgb(r g b [ / a]) |
| 270 | return `rgb(${(r * 255).toFixed()} ${(g * 255).toFixed()} ${(b * 255).toFixed()}${ |
| 271 | a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`; |
| 272 | } |
| 273 | |
| 274 | if (colorFormat === 'hsl') { |
Alex Rudenko | 529498f | 2021-02-05 14:08:27 | [diff] [blame] | 275 | const [h, s, l, a] = rgbaToHsla(rgba); |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame] | 276 | // hsl(hdeg s l [ / a]) |
| 277 | return `hsl(${Math.round(h * 360)}deg ${Math.round(s * 100)} ${Math.round(l * 100)}${ |
Philip Pfaffe | 77c04d8 | 2023-01-26 09:34:53 | [diff] [blame] | 278 | a === 1 ? '' : ' / ' + Math.round((a ?? 1) * 100) / 100})`; |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame] | 279 | } |
| 280 | |
Jan Keitel | 8363c28 | 2022-03-09 18:45:20 | [diff] [blame] | 281 | if (colorFormat === 'hwb') { |
| 282 | const [h, w, b, a] = rgbaToHwba(rgba); |
| 283 | // hwb(hdeg w b [ / a]) |
| 284 | return `hwb(${Math.round(h * 360)}deg ${Math.round(w * 100)} ${Math.round(b * 100)}${ |
Philip Pfaffe | 77c04d8 | 2023-01-26 09:34:53 | [diff] [blame] | 285 | a === 1 ? '' : ' / ' + Math.round((a ?? 1) * 100) / 100})`; |
Jan Keitel | 8363c28 | 2022-03-09 18:45:20 | [diff] [blame] | 286 | } |
| 287 | |
Alex Rudenko | 529498f | 2021-02-05 14:08:27 | [diff] [blame] | 288 | throw new Error('NOT_REACHED'); |
| 289 | } |
| 290 | |
| 291 | export function formatColor(hexa: string, colorFormat: string): string { |
Jan Keitel | 8363c28 | 2022-03-09 18:45:20 | [diff] [blame] | 292 | if (colorFormat === 'rgb' || colorFormat === 'hsl' || colorFormat === 'hwb') { |
Alex Rudenko | 529498f | 2021-02-05 14:08:27 | [diff] [blame] | 293 | return formatRgba(parseHexa(hexa), colorFormat); |
| 294 | } |
| 295 | |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame] | 296 | if (hexa.endsWith('FF')) { |
| 297 | // short hex if no alpha |
| 298 | return hexa.substr(0, 7); |
| 299 | } |
| 300 | |
| 301 | return hexa; |
| 302 | } |
Alex Rudenko | e633829 | 2021-04-01 07:23:28 | [diff] [blame] | 303 | |
| 304 | export function drawPath( |
| 305 | context: CanvasRenderingContext2D, commands: PathCommands, fillColor: string|undefined, |
| 306 | outlineColor: string|undefined, outlinePattern: LinePattern|undefined, bounds: PathBounds, |
| 307 | emulationScaleFactor: number) { |
| 308 | context.save(); |
| 309 | const path = buildPath(commands, bounds, emulationScaleFactor); |
| 310 | if (fillColor) { |
| 311 | context.fillStyle = fillColor; |
| 312 | context.fill(path); |
| 313 | } |
| 314 | if (outlineColor) { |
| 315 | if (outlinePattern === LinePattern.Dashed) { |
| 316 | context.setLineDash([3, 3]); |
| 317 | } |
| 318 | if (outlinePattern === LinePattern.Dotted) { |
| 319 | context.setLineDash([2, 2]); |
| 320 | } |
| 321 | context.lineWidth = 2; |
| 322 | context.strokeStyle = outlineColor; |
| 323 | context.stroke(path); |
| 324 | } |
| 325 | context.restore(); |
| 326 | return path; |
| 327 | } |