[go: nahoru, domu]

blob: 3397cfeafaab69f516aef72fb72cf05b7d94019c [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
Patrick Brosset8bb911c2020-12-02 17:46:1431import {rgbaToHsla} from '../front_end/common/ColorUtils.js';
Patrick Brosset7c7e8f72020-11-19 12:38:0932import {Bounds, Quad} from './common.js';
Brandon Goddard7ceb5012020-07-09 19:36:5733
Alex Rudenkoc294d472020-10-02 07:07:4934export 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 Goddard7ceb5012020-07-09 19:36:5740
Patrick Brosset5282f412020-11-13 16:32:0941export interface LineStyle {
42 color?: string;
43 pattern?: LinePattern;
44}
45
Patrick Brosset7c7e8f72020-11-19 12:38:0946export interface BoxStyle {
47 fillColor?: string;
48 hatchColor?: string;
49}
50
Jack Franklin8d634c22020-11-30 14:47:3551const enum LinePattern {
Patrick Brosset5282f412020-11-13 16:32:0952 Solid = 'solid',
53 Dotted = 'dotted',
54 Dashed = 'dashed'
55}
56
Patrick Brosset0f58c2f2020-11-26 16:05:4657export function drawPathWithLineStyle(
58 context: CanvasRenderingContext2D, path: Path2D, lineStyle?: LineStyle, lineWidth: number = 1) {
Patrick Brosset5282f412020-11-13 16:32:0959 if (lineStyle && lineStyle.color) {
60 context.save();
61 context.translate(0.5, 0.5);
Patrick Brosset0f58c2f2020-11-26 16:05:4662 context.lineWidth = lineWidth;
Patrick Brosset5282f412020-11-13 16:32:0963 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 Rudenkoc294d472020-10-02 07:07:4975export function buildPath(commands: Array<string|number>, bounds: PathBounds, emulationScaleFactor: number): Path2D {
Brandon Goddard7ceb5012020-07-09 19:36:5776 let commandsIndex = 0;
77
Alex Rudenkoc294d472020-10-02 07:07:4978 function extractPoints(count: number): number[] {
Brandon Goddard7ceb5012020-07-09 19:36:5779 const points = [];
80
81 for (let i = 0; i < count; ++i) {
Alex Rudenkoc294d472020-10-02 07:07:4982 const x = Math.round(commands[commandsIndex++] as number * emulationScaleFactor);
Brandon Goddard7ceb5012020-07-09 19:36:5783 bounds.maxX = Math.max(bounds.maxX, x);
84 bounds.minX = Math.min(bounds.minX, x);
85
Alex Rudenkoc294d472020-10-02 07:07:4986 const y = Math.round(commands[commandsIndex++] as number * emulationScaleFactor);
Brandon Goddard7ceb5012020-07-09 19:36:5787 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 Brosset8eafacc2020-08-05 08:18:4194
95 bounds.allPoints.push({x, y});
96
Brandon Goddard7ceb5012020-07-09 19:36:5797 points.push(x, y);
98 }
Alex Rudenkoc294d472020-10-02 07:07:4999
Brandon Goddard7ceb5012020-07-09 19:36:57100 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 Rudenkoc294d472020-10-02 07:07:49108 path.moveTo.apply(path, extractPoints(1) as [number, number]);
Brandon Goddard7ceb5012020-07-09 19:36:57109 break;
110 case 'L':
Alex Rudenkoc294d472020-10-02 07:07:49111 path.lineTo.apply(path, extractPoints(1) as [number, number]);
Brandon Goddard7ceb5012020-07-09 19:36:57112 break;
113 case 'C':
Alex Rudenkoc294d472020-10-02 07:07:49114 path.bezierCurveTo.apply(path, extractPoints(3) as [number, number, number, number, number, number]);
Brandon Goddard7ceb5012020-07-09 19:36:57115 break;
116 case 'Q':
Alex Rudenkoc294d472020-10-02 07:07:49117 path.quadraticCurveTo.apply(path, extractPoints(2) as [number, number, number, number]);
Brandon Goddard7ceb5012020-07-09 19:36:57118 break;
119 case 'Z':
120 path.closePath();
121 break;
122 }
123 }
124
125 return path;
126}
127
Alex Rudenkoc294d472020-10-02 07:07:49128export function emptyBounds(): PathBounds {
Brandon Goddard7ceb5012020-07-09 19:36:57129 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 Brosset8eafacc2020-08-05 08:18:41137 bottommostYForX: {},
Alex Rudenkoc294d472020-10-02 07:07:49138 allPoints: [],
Brandon Goddard7ceb5012020-07-09 19:36:57139 };
140 return bounds;
141}
Patrick Brossetd7508752020-08-21 08:36:51142
Alex Rudenkoc294d472020-10-02 07:07:49143export 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 Brossetd7508752020-08-21 08:36:51147}
Patrick Brosset7c7e8f72020-11-19 12:38:09148
149/**
150 * Draw line hatching at a 45 degree angle for a given
151 * path.
152 * __________
153 * |\ \ \ |
154 * | \ \ \|
155 * | \ \ |
156 * |\ \ \ |
157 * **********
158 */
159export 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 */
197export 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 Brosset8bb911c2020-12-02 17:46:14224
225export 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
229export 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}