[go: nahoru, domu]

blob: 8bb46d49817818ddf16b658815b3d45a72ee0899 [file] [log] [blame]
Brandon Goddard7ceb5012020-07-09 19:36:571// 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 Pfaffe77c04d82023-01-26 09:34:5331// eslint-disable-next-line rulesdir/es_modules_import
32import {
33 rgbaToHsla,
34 rgbaToHwba,
35 type Color4D,
36} from '../front_end/core/common/ColorUtils.js';
Alex Rudenkoe6338292021-04-01 07:23:2837
Jack Franklin3a802602022-07-13 08:39:4238import {type Bounds, type PathCommands, type Quad} from './common.js';
Brandon Goddard7ceb5012020-07-09 19:36:5739
Alex Rudenkoc294d472020-10-02 07:07:4940export type PathBounds = Bounds&{
Sigurd Schneider15761862021-02-04 08:05:3641 leftmostXForY: {[key: string]: number},
42 rightmostXForY: {[key: string]: number},
43 topmostYForX: {[key: string]: number},
44 bottommostYForX: {[key: string]: number},
Tim van der Lippe0ebbf492020-12-03 12:13:2145};
Brandon Goddard7ceb5012020-07-09 19:36:5746
Patrick Brosset5282f412020-11-13 16:32:0947export interface LineStyle {
48 color?: string;
49 pattern?: LinePattern;
50}
51
Patrick Brosset7c7e8f72020-11-19 12:38:0952export interface BoxStyle {
53 fillColor?: string;
54 hatchColor?: string;
55}
56
Alex Rudenkoe6338292021-04-01 07:23:2857export const enum LinePattern {
Patrick Brosset5282f412020-11-13 16:32:0958 Solid = 'solid',
59 Dotted = 'dotted',
Sigurd Schneider9c075672021-02-03 13:16:3960 Dashed = 'dashed',
Patrick Brosset5282f412020-11-13 16:32:0961}
62
Patrick Brosset0f58c2f2020-11-26 16:05:4663export function drawPathWithLineStyle(
64 context: CanvasRenderingContext2D, path: Path2D, lineStyle?: LineStyle, lineWidth: number = 1) {
Patrick Brosset5282f412020-11-13 16:32:0965 if (lineStyle && lineStyle.color) {
66 context.save();
67 context.translate(0.5, 0.5);
Patrick Brosset0f58c2f2020-11-26 16:05:4668 context.lineWidth = lineWidth;
Patrick Brosset5282f412020-11-13 16:32:0969 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 Brosset74a4f4f2021-02-12 08:55:1181export 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 Rudenkoc294d472020-10-02 07:07:4998export function buildPath(commands: Array<string|number>, bounds: PathBounds, emulationScaleFactor: number): Path2D {
Brandon Goddard7ceb5012020-07-09 19:36:5799 let commandsIndex = 0;
100
Alex Rudenkoc294d472020-10-02 07:07:49101 function extractPoints(count: number): number[] {
Brandon Goddard7ceb5012020-07-09 19:36:57102 const points = [];
103
104 for (let i = 0; i < count; ++i) {
Alex Rudenkoc294d472020-10-02 07:07:49105 const x = Math.round(commands[commandsIndex++] as number * emulationScaleFactor);
Brandon Goddard7ceb5012020-07-09 19:36:57106 bounds.maxX = Math.max(bounds.maxX, x);
107 bounds.minX = Math.min(bounds.minX, x);
108
Alex Rudenkoc294d472020-10-02 07:07:49109 const y = Math.round(commands[commandsIndex++] as number * emulationScaleFactor);
Brandon Goddard7ceb5012020-07-09 19:36:57110 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 Brosset8eafacc2020-08-05 08:18:41117
118 bounds.allPoints.push({x, y});
119
Brandon Goddard7ceb5012020-07-09 19:36:57120 points.push(x, y);
121 }
Alex Rudenkoc294d472020-10-02 07:07:49122
Brandon Goddard7ceb5012020-07-09 19:36:57123 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 Rudenkoc294d472020-10-02 07:07:49131 path.moveTo.apply(path, extractPoints(1) as [number, number]);
Brandon Goddard7ceb5012020-07-09 19:36:57132 break;
133 case 'L':
Alex Rudenkoc294d472020-10-02 07:07:49134 path.lineTo.apply(path, extractPoints(1) as [number, number]);
Brandon Goddard7ceb5012020-07-09 19:36:57135 break;
136 case 'C':
Alex Rudenkoc294d472020-10-02 07:07:49137 path.bezierCurveTo.apply(path, extractPoints(3) as [number, number, number, number, number, number]);
Brandon Goddard7ceb5012020-07-09 19:36:57138 break;
139 case 'Q':
Alex Rudenkoc294d472020-10-02 07:07:49140 path.quadraticCurveTo.apply(path, extractPoints(2) as [number, number, number, number]);
Brandon Goddard7ceb5012020-07-09 19:36:57141 break;
142 case 'Z':
143 path.closePath();
144 break;
145 }
146 }
147
148 return path;
149}
150
Alex Rudenkoc294d472020-10-02 07:07:49151export function emptyBounds(): PathBounds {
Brandon Goddard7ceb5012020-07-09 19:36:57152 const bounds = {
153 minX: Number.MAX_VALUE,
154 minY: Number.MAX_VALUE,
Alex Rudenkoe6338292021-04-01 07:23:28155 maxX: -Number.MAX_VALUE,
156 maxY: -Number.MAX_VALUE,
Brandon Goddard7ceb5012020-07-09 19:36:57157 leftmostXForY: {},
158 rightmostXForY: {},
159 topmostYForX: {},
Patrick Brosset8eafacc2020-08-05 08:18:41160 bottommostYForX: {},
Alex Rudenkoc294d472020-10-02 07:07:49161 allPoints: [],
Brandon Goddard7ceb5012020-07-09 19:36:57162 };
163 return bounds;
164}
Patrick Brossetd7508752020-08-21 08:36:51165
Sigurd Schneider15761862021-02-04 08:05:36166export function applyMatrixToPoint(point: {x: number, y: number}, matrix: DOMMatrix): {x: number, y: number} {
Alex Rudenkoc294d472020-10-02 07:07:49167 let domPoint = new DOMPoint(point.x, point.y);
168 domPoint = domPoint.matrixTransform(matrix);
169 return {x: domPoint.x, y: domPoint.y};
Patrick Brossetd7508752020-08-21 08:36:51170}
Patrick Brosset7c7e8f72020-11-19 12:38:09171
Patrick Brosseta1da2af2020-12-18 13:28:39172const HATCH_LINE_LENGTH = 5;
173const HATCH_LINE_GAP = 3;
174let hatchLinePattern: CanvasPattern;
175let hatchLineColor: string = '';
176
Patrick Brosset7c7e8f72020-11-19 12:38:09177/**
178 * Draw line hatching at a 45 degree angle for a given
179 * path.
180 * __________
181 * |\ \ \ |
182 * | \ \ \|
183 * | \ \ |
184 * |\ \ \ |
185 * **********
186 */
187export function hatchFillPath(
188 context: CanvasRenderingContext2D, path: Path2D, bounds: Bounds, delta: number, color: string,
189 rotationAngle: number, flipDirection: boolean|undefined) {
Patrick Brosseta1da2af2020-12-18 13:28:39190 // 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 Brosset7c7e8f72020-11-19 12:38:09200 }
Patrick Brosseta1da2af2020-12-18 13:28:39201
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 Brosset7c7e8f72020-11-19 12:38:09227 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 */
234export 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 Brosset8bb911c2020-12-02 17:46:14261
Philip Pfaffe77c04d82023-01-26 09:34:53262export 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 Brosset8bb911c2020-12-02 17:46:14264}
265
Philip Pfaffe77c04d82023-01-26 09:34:53266export function formatRgba(rgba: Color4D, colorFormat: 'rgb'|'hsl'|'hwb'): string {
Patrick Brosset8bb911c2020-12-02 17:46:14267 if (colorFormat === 'rgb') {
Alex Rudenko529498f2021-02-05 14:08:27268 const [r, g, b, a] = rgba;
Patrick Brosset8bb911c2020-12-02 17:46:14269 // 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 Rudenko529498f2021-02-05 14:08:27275 const [h, s, l, a] = rgbaToHsla(rgba);
Patrick Brosset8bb911c2020-12-02 17:46:14276 // hsl(hdeg s l [ / a])
277 return `hsl(${Math.round(h * 360)}deg ${Math.round(s * 100)} ${Math.round(l * 100)}${
Philip Pfaffe77c04d82023-01-26 09:34:53278 a === 1 ? '' : ' / ' + Math.round((a ?? 1) * 100) / 100})`;
Patrick Brosset8bb911c2020-12-02 17:46:14279 }
280
Jan Keitel8363c282022-03-09 18:45:20281 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 Pfaffe77c04d82023-01-26 09:34:53285 a === 1 ? '' : ' / ' + Math.round((a ?? 1) * 100) / 100})`;
Jan Keitel8363c282022-03-09 18:45:20286 }
287
Alex Rudenko529498f2021-02-05 14:08:27288 throw new Error('NOT_REACHED');
289}
290
291export function formatColor(hexa: string, colorFormat: string): string {
Jan Keitel8363c282022-03-09 18:45:20292 if (colorFormat === 'rgb' || colorFormat === 'hsl' || colorFormat === 'hwb') {
Alex Rudenko529498f2021-02-05 14:08:27293 return formatRgba(parseHexa(hexa), colorFormat);
294 }
295
Patrick Brosset8bb911c2020-12-02 17:46:14296 if (hexa.endsWith('FF')) {
297 // short hex if no alpha
298 return hexa.substr(0, 7);
299 }
300
301 return hexa;
302}
Alex Rudenkoe6338292021-04-01 07:23:28303
304export 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}