| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Copyright (C) 2012 Google Inc. All rights reserved. |
| |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions |
| // are met: |
| |
| // 1. Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // 2. Redistributions in binary form must reproduce the above copyright |
| // notice, this list of conditions and the following disclaimer in the |
| // documentation and/or other materials provided with the distribution. |
| // 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| // its contributors may be used to endorse or promote products derived |
| // from this software without specific prior written permission. |
| |
| // THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import {rgbaToHsla} from '../front_end/core/common/ColorUtils.js'; |
| |
| import {Bounds, PathCommands, Quad} from './common.js'; |
| |
| export type PathBounds = Bounds&{ |
| leftmostXForY: {[key: string]: number}, |
| rightmostXForY: {[key: string]: number}, |
| topmostYForX: {[key: string]: number}, |
| bottommostYForX: {[key: string]: number}, |
| }; |
| |
| export interface LineStyle { |
| color?: string; |
| pattern?: LinePattern; |
| } |
| |
| export interface BoxStyle { |
| fillColor?: string; |
| hatchColor?: string; |
| } |
| |
| export const enum LinePattern { |
| Solid = 'solid', |
| Dotted = 'dotted', |
| Dashed = 'dashed', |
| } |
| |
| export function drawPathWithLineStyle( |
| context: CanvasRenderingContext2D, path: Path2D, lineStyle?: LineStyle, lineWidth: number = 1) { |
| if (lineStyle && lineStyle.color) { |
| context.save(); |
| context.translate(0.5, 0.5); |
| context.lineWidth = lineWidth; |
| if (lineStyle.pattern === LinePattern.Dashed) { |
| context.setLineDash([3, 3]); |
| } |
| if (lineStyle.pattern === LinePattern.Dotted) { |
| context.setLineDash([2, 2]); |
| } |
| context.strokeStyle = lineStyle.color; |
| context.stroke(path); |
| context.restore(); |
| } |
| } |
| |
| export function fillPathWithBoxStyle( |
| context: CanvasRenderingContext2D, path: Path2D, bounds: PathBounds, angle: number, boxStyle?: BoxStyle) { |
| if (!boxStyle) { |
| return; |
| } |
| |
| context.save(); |
| if (boxStyle.fillColor) { |
| context.fillStyle = boxStyle.fillColor; |
| context.fill(path); |
| } |
| if (boxStyle.hatchColor) { |
| hatchFillPath(context, path, bounds, 10, boxStyle.hatchColor, angle, false); |
| } |
| context.restore(); |
| } |
| |
| export function buildPath(commands: Array<string|number>, bounds: PathBounds, emulationScaleFactor: number): Path2D { |
| let commandsIndex = 0; |
| |
| function extractPoints(count: number): number[] { |
| const points = []; |
| |
| for (let i = 0; i < count; ++i) { |
| const x = Math.round(commands[commandsIndex++] as number * emulationScaleFactor); |
| bounds.maxX = Math.max(bounds.maxX, x); |
| bounds.minX = Math.min(bounds.minX, x); |
| |
| const y = Math.round(commands[commandsIndex++] as number * emulationScaleFactor); |
| bounds.maxY = Math.max(bounds.maxY, y); |
| bounds.minY = Math.min(bounds.minY, y); |
| |
| bounds.leftmostXForY[y] = Math.min(bounds.leftmostXForY[y] || Number.MAX_VALUE, x); |
| bounds.rightmostXForY[y] = Math.max(bounds.rightmostXForY[y] || Number.MIN_VALUE, x); |
| bounds.topmostYForX[x] = Math.min(bounds.topmostYForX[x] || Number.MAX_VALUE, y); |
| bounds.bottommostYForX[x] = Math.max(bounds.bottommostYForX[x] || Number.MIN_VALUE, y); |
| |
| bounds.allPoints.push({x, y}); |
| |
| points.push(x, y); |
| } |
| |
| return points; |
| } |
| |
| const commandsLength = commands.length; |
| const path = new Path2D(); |
| while (commandsIndex < commandsLength) { |
| switch (commands[commandsIndex++]) { |
| case 'M': |
| path.moveTo.apply(path, extractPoints(1) as [number, number]); |
| break; |
| case 'L': |
| path.lineTo.apply(path, extractPoints(1) as [number, number]); |
| break; |
| case 'C': |
| path.bezierCurveTo.apply(path, extractPoints(3) as [number, number, number, number, number, number]); |
| break; |
| case 'Q': |
| path.quadraticCurveTo.apply(path, extractPoints(2) as [number, number, number, number]); |
| break; |
| case 'Z': |
| path.closePath(); |
| break; |
| } |
| } |
| |
| return path; |
| } |
| |
| export function emptyBounds(): PathBounds { |
| const bounds = { |
| minX: Number.MAX_VALUE, |
| minY: Number.MAX_VALUE, |
| maxX: -Number.MAX_VALUE, |
| maxY: -Number.MAX_VALUE, |
| leftmostXForY: {}, |
| rightmostXForY: {}, |
| topmostYForX: {}, |
| bottommostYForX: {}, |
| allPoints: [], |
| }; |
| return bounds; |
| } |
| |
| export function applyMatrixToPoint(point: {x: number, y: number}, matrix: DOMMatrix): {x: number, y: number} { |
| let domPoint = new DOMPoint(point.x, point.y); |
| domPoint = domPoint.matrixTransform(matrix); |
| return {x: domPoint.x, y: domPoint.y}; |
| } |
| |
| const HATCH_LINE_LENGTH = 5; |
| const HATCH_LINE_GAP = 3; |
| let hatchLinePattern: CanvasPattern; |
| let hatchLineColor: string = ''; |
| |
| /** |
| * Draw line hatching at a 45 degree angle for a given |
| * path. |
| * __________ |
| * |\ \ \ | |
| * | \ \ \| |
| * | \ \ | |
| * |\ \ \ | |
| * ********** |
| */ |
| export function hatchFillPath( |
| context: CanvasRenderingContext2D, path: Path2D, bounds: Bounds, delta: number, color: string, |
| rotationAngle: number, flipDirection: boolean|undefined) { |
| // Make the bounds be at most the canvas size if it is bigger in any direction. |
| // Making the bounds bigger than the canvas is useless as what's drawn there won't be visible. |
| if (context.canvas.width < bounds.maxX - bounds.minX || context.canvas.height < bounds.maxY - bounds.minY) { |
| bounds = { |
| minX: 0, |
| maxX: context.canvas.width, |
| minY: 0, |
| maxY: context.canvas.height, |
| allPoints: [], |
| }; |
| } |
| |
| // If we haven't done it yet, initialize an offscreen canvas used to create the dashed line repeated pattern. |
| if (!hatchLinePattern || color !== hatchLineColor) { |
| hatchLineColor = color; |
| |
| const offscreenCanvas = document.createElement('canvas'); |
| offscreenCanvas.width = delta; |
| offscreenCanvas.height = HATCH_LINE_LENGTH + HATCH_LINE_GAP; |
| |
| const offscreenCtx = offscreenCanvas.getContext('2d') as CanvasRenderingContext2D; |
| offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height); |
| offscreenCtx.rect(0, 0, 1, HATCH_LINE_LENGTH); |
| offscreenCtx.fillStyle = color; |
| offscreenCtx.fill(); |
| |
| hatchLinePattern = context.createPattern(offscreenCanvas, 'repeat') as CanvasPattern; |
| } |
| |
| context.save(); |
| |
| const matrix = new DOMMatrix(); |
| hatchLinePattern.setTransform(matrix.scale(flipDirection ? -1 : 1, 1).rotate(0, 0, -45 + rotationAngle)); |
| |
| context.fillStyle = hatchLinePattern; |
| context.fill(path); |
| |
| context.restore(); |
| } |
| |
| /** |
| * Given a quad, create the corresponding path object. This also accepts a list of quads to clip from the resulting |
| * path. |
| */ |
| export function createPathForQuad( |
| outerQuad: Quad, quadsToClip: Quad[], bounds: PathBounds, emulationScaleFactor: number) { |
| let commands = [ |
| 'M', |
| outerQuad.p1.x, |
| outerQuad.p1.y, |
| 'L', |
| outerQuad.p2.x, |
| outerQuad.p2.y, |
| 'L', |
| outerQuad.p3.x, |
| outerQuad.p3.y, |
| 'L', |
| outerQuad.p4.x, |
| outerQuad.p4.y, |
| ]; |
| for (const quad of quadsToClip) { |
| commands = [ |
| ...commands, 'L', quad.p4.x, quad.p4.y, 'L', quad.p3.x, quad.p3.y, 'L', quad.p2.x, |
| quad.p2.y, 'L', quad.p1.x, quad.p1.y, 'L', quad.p4.x, quad.p4.y, 'L', outerQuad.p4.x, |
| outerQuad.p4.y, |
| ]; |
| } |
| commands.push('Z'); |
| |
| return buildPath(commands, bounds, emulationScaleFactor); |
| } |
| |
| export function parseHexa(hexa: string): Array<number> { |
| return (hexa.match(/#(\w\w)(\w\w)(\w\w)(\w\w)/) || []).slice(1).map(c => parseInt(c, 16) / 255); |
| } |
| |
| export function formatRgba(rgba: number[], colorFormat: 'rgb'|'hsl'): string { |
| if (colorFormat === 'rgb') { |
| const [r, g, b, a] = rgba; |
| // rgb(r g b [ / a]) |
| return `rgb(${(r * 255).toFixed()} ${(g * 255).toFixed()} ${(b * 255).toFixed()}${ |
| a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`; |
| } |
| |
| if (colorFormat === 'hsl') { |
| const [h, s, l, a] = rgbaToHsla(rgba); |
| // hsl(hdeg s l [ / a]) |
| return `hsl(${Math.round(h * 360)}deg ${Math.round(s * 100)} ${Math.round(l * 100)}${ |
| a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`; |
| } |
| |
| throw new Error('NOT_REACHED'); |
| } |
| |
| export function formatColor(hexa: string, colorFormat: string): string { |
| if (colorFormat === 'rgb' || colorFormat === 'hsl') { |
| return formatRgba(parseHexa(hexa), colorFormat); |
| } |
| |
| if (hexa.endsWith('FF')) { |
| // short hex if no alpha |
| return hexa.substr(0, 7); |
| } |
| |
| return hexa; |
| } |
| |
| export function drawPath( |
| context: CanvasRenderingContext2D, commands: PathCommands, fillColor: string|undefined, |
| outlineColor: string|undefined, outlinePattern: LinePattern|undefined, bounds: PathBounds, |
| emulationScaleFactor: number) { |
| context.save(); |
| const path = buildPath(commands, bounds, emulationScaleFactor); |
| if (fillColor) { |
| context.fillStyle = fillColor; |
| context.fill(path); |
| } |
| if (outlineColor) { |
| if (outlinePattern === LinePattern.Dashed) { |
| context.setLineDash([3, 3]); |
| } |
| if (outlinePattern === LinePattern.Dotted) { |
| context.setLineDash([2, 2]); |
| } |
| context.lineWidth = 2; |
| context.strokeStyle = outlineColor; |
| context.stroke(path); |
| } |
| context.restore(); |
| return path; |
| } |