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 | |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame^] | 31 | import {rgbaToHsla} from '../front_end/common/ColorUtils.js'; |
Patrick Brosset | 7c7e8f7 | 2020-11-19 12:38:09 | [diff] [blame] | 32 | import {Bounds, Quad} from './common.js'; |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 33 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 34 | export type PathBounds = Bounds&{ |
| 35 | leftmostXForY: {[key: string]: number}; |
| 36 | rightmostXForY: {[key: string]: number}; |
| 37 | topmostYForX: {[key: string]: number}; |
| 38 | bottommostYForX: {[key: string]: number}; |
| 39 | } |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 40 | |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 41 | export interface LineStyle { |
| 42 | color?: string; |
| 43 | pattern?: LinePattern; |
| 44 | } |
| 45 | |
Patrick Brosset | 7c7e8f7 | 2020-11-19 12:38:09 | [diff] [blame] | 46 | export interface BoxStyle { |
| 47 | fillColor?: string; |
| 48 | hatchColor?: string; |
| 49 | } |
| 50 | |
Jack Franklin | 8d634c2 | 2020-11-30 14:47:35 | [diff] [blame] | 51 | const enum LinePattern { |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 52 | Solid = 'solid', |
| 53 | Dotted = 'dotted', |
| 54 | Dashed = 'dashed' |
| 55 | } |
| 56 | |
Patrick Brosset | 0f58c2f | 2020-11-26 16:05:46 | [diff] [blame] | 57 | export function drawPathWithLineStyle( |
| 58 | context: CanvasRenderingContext2D, path: Path2D, lineStyle?: LineStyle, lineWidth: number = 1) { |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 59 | if (lineStyle && lineStyle.color) { |
| 60 | context.save(); |
| 61 | context.translate(0.5, 0.5); |
Patrick Brosset | 0f58c2f | 2020-11-26 16:05:46 | [diff] [blame] | 62 | context.lineWidth = lineWidth; |
Patrick Brosset | 5282f41 | 2020-11-13 16:32:09 | [diff] [blame] | 63 | if (lineStyle.pattern === LinePattern.Dashed) { |
| 64 | context.setLineDash([3, 3]); |
| 65 | } |
| 66 | if (lineStyle.pattern === LinePattern.Dotted) { |
| 67 | context.setLineDash([2, 2]); |
| 68 | } |
| 69 | context.strokeStyle = lineStyle.color; |
| 70 | context.stroke(path); |
| 71 | context.restore(); |
| 72 | } |
| 73 | } |
| 74 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 75 | export function buildPath(commands: Array<string|number>, bounds: PathBounds, emulationScaleFactor: number): Path2D { |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 76 | let commandsIndex = 0; |
| 77 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 78 | function extractPoints(count: number): number[] { |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 79 | const points = []; |
| 80 | |
| 81 | for (let i = 0; i < count; ++i) { |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 82 | const x = Math.round(commands[commandsIndex++] as number * emulationScaleFactor); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 83 | bounds.maxX = Math.max(bounds.maxX, x); |
| 84 | bounds.minX = Math.min(bounds.minX, x); |
| 85 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 86 | const y = Math.round(commands[commandsIndex++] as number * emulationScaleFactor); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 87 | bounds.maxY = Math.max(bounds.maxY, y); |
| 88 | bounds.minY = Math.min(bounds.minY, y); |
| 89 | |
| 90 | bounds.leftmostXForY[y] = Math.min(bounds.leftmostXForY[y] || Number.MAX_VALUE, x); |
| 91 | bounds.rightmostXForY[y] = Math.max(bounds.rightmostXForY[y] || Number.MIN_VALUE, x); |
| 92 | bounds.topmostYForX[x] = Math.min(bounds.topmostYForX[x] || Number.MAX_VALUE, y); |
| 93 | bounds.bottommostYForX[x] = Math.max(bounds.bottommostYForX[x] || Number.MIN_VALUE, y); |
Patrick Brosset | 8eafacc | 2020-08-05 08:18:41 | [diff] [blame] | 94 | |
| 95 | bounds.allPoints.push({x, y}); |
| 96 | |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 97 | points.push(x, y); |
| 98 | } |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 99 | |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 100 | return points; |
| 101 | } |
| 102 | |
| 103 | const commandsLength = commands.length; |
| 104 | const path = new Path2D(); |
| 105 | while (commandsIndex < commandsLength) { |
| 106 | switch (commands[commandsIndex++]) { |
| 107 | case 'M': |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 108 | path.moveTo.apply(path, extractPoints(1) as [number, number]); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 109 | break; |
| 110 | case 'L': |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 111 | path.lineTo.apply(path, extractPoints(1) as [number, number]); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 112 | break; |
| 113 | case 'C': |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 114 | path.bezierCurveTo.apply(path, extractPoints(3) as [number, number, number, number, number, number]); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 115 | break; |
| 116 | case 'Q': |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 117 | path.quadraticCurveTo.apply(path, extractPoints(2) as [number, number, number, number]); |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 118 | break; |
| 119 | case 'Z': |
| 120 | path.closePath(); |
| 121 | break; |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | return path; |
| 126 | } |
| 127 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 128 | export function emptyBounds(): PathBounds { |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 129 | const bounds = { |
| 130 | minX: Number.MAX_VALUE, |
| 131 | minY: Number.MAX_VALUE, |
| 132 | maxX: Number.MIN_VALUE, |
| 133 | maxY: Number.MIN_VALUE, |
| 134 | leftmostXForY: {}, |
| 135 | rightmostXForY: {}, |
| 136 | topmostYForX: {}, |
Patrick Brosset | 8eafacc | 2020-08-05 08:18:41 | [diff] [blame] | 137 | bottommostYForX: {}, |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 138 | allPoints: [], |
Brandon Goddard | 7ceb501 | 2020-07-09 19:36:57 | [diff] [blame] | 139 | }; |
| 140 | return bounds; |
| 141 | } |
Patrick Brosset | d750875 | 2020-08-21 08:36:51 | [diff] [blame] | 142 | |
Alex Rudenko | c294d47 | 2020-10-02 07:07:49 | [diff] [blame] | 143 | export function applyMatrixToPoint(point: {x: number; y: number;}, matrix: DOMMatrix): {x: number; y: number;} { |
| 144 | let domPoint = new DOMPoint(point.x, point.y); |
| 145 | domPoint = domPoint.matrixTransform(matrix); |
| 146 | return {x: domPoint.x, y: domPoint.y}; |
Patrick Brosset | d750875 | 2020-08-21 08:36:51 | [diff] [blame] | 147 | } |
Patrick Brosset | 7c7e8f7 | 2020-11-19 12:38:09 | [diff] [blame] | 148 | |
| 149 | /** |
| 150 | * Draw line hatching at a 45 degree angle for a given |
| 151 | * path. |
| 152 | * __________ |
| 153 | * |\ \ \ | |
| 154 | * | \ \ \| |
| 155 | * | \ \ | |
| 156 | * |\ \ \ | |
| 157 | * ********** |
| 158 | */ |
| 159 | export function hatchFillPath( |
| 160 | context: CanvasRenderingContext2D, path: Path2D, bounds: Bounds, delta: number, color: string, |
| 161 | rotationAngle: number, flipDirection: boolean|undefined) { |
| 162 | const dx = bounds.maxX - bounds.minX; |
| 163 | const dy = bounds.maxY - bounds.minY; |
| 164 | context.rect(bounds.minX, bounds.minY, dx, dy); |
| 165 | context.save(); |
| 166 | context.clip(path); |
| 167 | context.setLineDash([5, 3]); |
| 168 | const majorAxis = Math.max(dx, dy); |
| 169 | context.strokeStyle = color; |
| 170 | const centerX = bounds.minX + dx / 2; |
| 171 | const centerY = bounds.minY + dy / 2; |
| 172 | context.translate(centerX, centerY); |
| 173 | context.rotate(rotationAngle * Math.PI / 180); |
| 174 | context.translate(-centerX, -centerY); |
| 175 | if (flipDirection) { |
| 176 | for (let i = -majorAxis; i < majorAxis; i += delta) { |
| 177 | context.beginPath(); |
| 178 | context.moveTo(bounds.maxX - i, bounds.minY); |
| 179 | context.lineTo(bounds.maxX - dy - i, bounds.maxY); |
| 180 | context.stroke(); |
| 181 | } |
| 182 | } else { |
| 183 | for (let i = -majorAxis; i < majorAxis; i += delta) { |
| 184 | context.beginPath(); |
| 185 | context.moveTo(i + bounds.minX, bounds.minY); |
| 186 | context.lineTo(dy + i + bounds.minX, bounds.maxY); |
| 187 | context.stroke(); |
| 188 | } |
| 189 | } |
| 190 | context.restore(); |
| 191 | } |
| 192 | |
| 193 | /** |
| 194 | * Given a quad, create the corresponding path object. This also accepts a list of quads to clip from the resulting |
| 195 | * path. |
| 196 | */ |
| 197 | export function createPathForQuad( |
| 198 | outerQuad: Quad, quadsToClip: Quad[], bounds: PathBounds, emulationScaleFactor: number) { |
| 199 | let commands = [ |
| 200 | 'M', |
| 201 | outerQuad.p1.x, |
| 202 | outerQuad.p1.y, |
| 203 | 'L', |
| 204 | outerQuad.p2.x, |
| 205 | outerQuad.p2.y, |
| 206 | 'L', |
| 207 | outerQuad.p3.x, |
| 208 | outerQuad.p3.y, |
| 209 | 'L', |
| 210 | outerQuad.p4.x, |
| 211 | outerQuad.p4.y, |
| 212 | ]; |
| 213 | for (const quad of quadsToClip) { |
| 214 | commands = [ |
| 215 | ...commands, 'L', quad.p4.x, quad.p4.y, 'L', quad.p3.x, quad.p3.y, 'L', quad.p2.x, |
| 216 | quad.p2.y, 'L', quad.p1.x, quad.p1.y, 'L', quad.p4.x, quad.p4.y, 'L', outerQuad.p4.x, |
| 217 | outerQuad.p4.y, |
| 218 | ]; |
| 219 | } |
| 220 | commands.push('Z'); |
| 221 | |
| 222 | return buildPath(commands, bounds, emulationScaleFactor); |
| 223 | } |
Patrick Brosset | 8bb911c | 2020-12-02 17:46:14 | [diff] [blame^] | 224 | |
| 225 | export function parseHexa(hexa: string): Array<number> { |
| 226 | return (hexa.match(/#(\w\w)(\w\w)(\w\w)(\w\w)/) || []).slice(1).map(c => parseInt(c, 16) / 255); |
| 227 | } |
| 228 | |
| 229 | export function formatColor(hexa: string, colorFormat: string): string { |
| 230 | if (colorFormat === 'rgb') { |
| 231 | const [r, g, b, a] = parseHexa(hexa); |
| 232 | // rgb(r g b [ / a]) |
| 233 | return `rgb(${(r * 255).toFixed()} ${(g * 255).toFixed()} ${(b * 255).toFixed()}${ |
| 234 | a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`; |
| 235 | } |
| 236 | |
| 237 | if (colorFormat === 'hsl') { |
| 238 | const [h, s, l, a] = rgbaToHsla(parseHexa(hexa)); |
| 239 | // hsl(hdeg s l [ / a]) |
| 240 | return `hsl(${Math.round(h * 360)}deg ${Math.round(s * 100)} ${Math.round(l * 100)}${ |
| 241 | a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`; |
| 242 | } |
| 243 | |
| 244 | if (hexa.endsWith('FF')) { |
| 245 | // short hex if no alpha |
| 246 | return hexa.substr(0, 7); |
| 247 | } |
| 248 | |
| 249 | return hexa; |
| 250 | } |