From 72cd5284296e4cdb3fe88f2982ec7528604b6618 Mon Sep 17 00:00:00 2001 From: flyskyko Date: Sun, 4 Jul 2021 13:17:07 +0900 Subject: [PATCH] add features for border-style dashed, dotted, double. (#2531) --- src/css/property-descriptors/border-style.ts | 11 +- src/css/property-descriptors/display.ts | 1 + src/global.d.ts | 6 +- src/render/border.ts | 99 +++++++++++ src/render/bound-curves.ts | 161 +++++++++++++++++- src/render/canvas/canvas-renderer.ts | 168 ++++++++++++++++++- tests/karma.ts | 15 +- 7 files changed, 434 insertions(+), 27 deletions(-) diff --git a/src/css/property-descriptors/border-style.ts b/src/css/property-descriptors/border-style.ts index 8ebdb5c..b70ff07 100644 --- a/src/css/property-descriptors/border-style.ts +++ b/src/css/property-descriptors/border-style.ts @@ -1,7 +1,10 @@ import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; export enum BORDER_STYLE { NONE = 0, - SOLID = 1 + SOLID = 1, + DASHED = 2, + DOTTED = 3, + DOUBLE = 4 } const borderStyleForSide = (side: string): IPropertyIdentValueDescriptor => ({ @@ -13,6 +16,12 @@ const borderStyleForSide = (side: string): IPropertyIdentValueDescriptor = { const parseDisplayValue = (display: string): Display => { switch (display) { case 'block': + case '-webkit-box': return DISPLAY.BLOCK; case 'inline': return DISPLAY.INLINE; diff --git a/src/global.d.ts b/src/global.d.ts index a94aa96..88c1d52 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,7 +1,7 @@ interface CSSStyleDeclaration { - textDecorationColor: string | null; - textDecorationLine: string | null; - overflowWrap: string | null; + textDecorationColor: string; + textDecorationLine: string; + overflowWrap: string; } interface DocumentType extends Node, ChildNode { diff --git a/src/render/border.ts b/src/render/border.ts index 05c6a99..20882c7 100644 --- a/src/render/border.ts +++ b/src/render/border.ts @@ -36,6 +36,105 @@ export const parsePathForBorder = (curves: BoundCurves, borderSide: number): Pat } }; +export const parsePathForBorderDoubleOuter = (curves: BoundCurves, borderSide: number): Path[] => { + switch (borderSide) { + case 0: + return createPathFromCurves( + curves.topLeftBorderBox, + curves.topLeftBorderDoubleOuterBox, + curves.topRightBorderBox, + curves.topRightBorderDoubleOuterBox + ); + case 1: + return createPathFromCurves( + curves.topRightBorderBox, + curves.topRightBorderDoubleOuterBox, + curves.bottomRightBorderBox, + curves.bottomRightBorderDoubleOuterBox + ); + case 2: + return createPathFromCurves( + curves.bottomRightBorderBox, + curves.bottomRightBorderDoubleOuterBox, + curves.bottomLeftBorderBox, + curves.bottomLeftBorderDoubleOuterBox + ); + case 3: + default: + return createPathFromCurves( + curves.bottomLeftBorderBox, + curves.bottomLeftBorderDoubleOuterBox, + curves.topLeftBorderBox, + curves.topLeftBorderDoubleOuterBox + ); + } +}; + +export const parsePathForBorderDoubleInner = (curves: BoundCurves, borderSide: number): Path[] => { + switch (borderSide) { + case 0: + return createPathFromCurves( + curves.topLeftBorderDoubleInnerBox, + curves.topLeftPaddingBox, + curves.topRightBorderDoubleInnerBox, + curves.topRightPaddingBox + ); + case 1: + return createPathFromCurves( + curves.topRightBorderDoubleInnerBox, + curves.topRightPaddingBox, + curves.bottomRightBorderDoubleInnerBox, + curves.bottomRightPaddingBox + ); + case 2: + return createPathFromCurves( + curves.bottomRightBorderDoubleInnerBox, + curves.bottomRightPaddingBox, + curves.bottomLeftBorderDoubleInnerBox, + curves.bottomLeftPaddingBox + ); + case 3: + default: + return createPathFromCurves( + curves.bottomLeftBorderDoubleInnerBox, + curves.bottomLeftPaddingBox, + curves.topLeftBorderDoubleInnerBox, + curves.topLeftPaddingBox + ); + } +}; + +export const parsePathForBorderStroke = (curves: BoundCurves, borderSide: number): Path[] => { + switch (borderSide) { + case 0: + return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke); + case 1: + return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke); + case 2: + return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke); + case 3: + default: + return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke); + } +}; + +const createStrokePathFromCurves = (outer1: Path, outer2: Path): Path[] => { + const path = []; + if (isBezierCurve(outer1)) { + path.push(outer1.subdivide(0.5, false)); + } else { + path.push(outer1); + } + + if (isBezierCurve(outer2)) { + path.push(outer2.subdivide(0.5, true)); + } else { + path.push(outer2); + } + + return path; +}; + const createPathFromCurves = (outer1: Path, inner1: Path, outer2: Path, inner2: Path): Path[] => { const path = []; if (isBezierCurve(outer1)) { diff --git a/src/render/bound-curves.ts b/src/render/bound-curves.ts index 6f19899..f6d19d4 100644 --- a/src/render/bound-curves.ts +++ b/src/render/bound-curves.ts @@ -5,6 +5,18 @@ import {BezierCurve} from './bezier-curve'; import {Path} from './path'; export class BoundCurves { + readonly topLeftBorderDoubleOuterBox: Path; + readonly topRightBorderDoubleOuterBox: Path; + readonly bottomRightBorderDoubleOuterBox: Path; + readonly bottomLeftBorderDoubleOuterBox: Path; + readonly topLeftBorderDoubleInnerBox: Path; + readonly topRightBorderDoubleInnerBox: Path; + readonly bottomRightBorderDoubleInnerBox: Path; + readonly bottomLeftBorderDoubleInnerBox: Path; + readonly topLeftBorderStroke: Path; + readonly topRightBorderStroke: Path; + readonly bottomRightBorderStroke: Path; + readonly bottomLeftBorderStroke: Path; readonly topLeftBorderBox: Path; readonly topRightBorderBox: Path; readonly bottomRightBorderBox: Path; @@ -60,6 +72,141 @@ export class BoundCurves { const paddingBottom = getAbsoluteValue(styles.paddingBottom, element.bounds.width); const paddingLeft = getAbsoluteValue(styles.paddingLeft, element.bounds.width); + this.topLeftBorderDoubleOuterBox = + tlh > 0 || tlv > 0 + ? getCurvePoints( + bounds.left + borderLeftWidth / 3, + bounds.top + borderTopWidth / 3, + tlh - borderLeftWidth / 3, + tlv - borderTopWidth / 3, + CORNER.TOP_LEFT + ) + : new Vector(bounds.left + borderLeftWidth / 3, bounds.top + borderTopWidth / 3); + this.topRightBorderDoubleOuterBox = + tlh > 0 || tlv > 0 + ? getCurvePoints( + bounds.left + topWidth, + bounds.top + borderTopWidth / 3, + trh - borderRightWidth / 3, + trv - borderTopWidth / 3, + CORNER.TOP_RIGHT + ) + : new Vector(bounds.left + bounds.width - borderRightWidth / 3, bounds.top + borderTopWidth / 3); + this.bottomRightBorderDoubleOuterBox = + brh > 0 || brv > 0 + ? getCurvePoints( + bounds.left + bottomWidth, + bounds.top + rightHeight, + brh - borderRightWidth / 3, + brv - borderBottomWidth / 3, + CORNER.BOTTOM_RIGHT + ) + : new Vector( + bounds.left + bounds.width - borderRightWidth / 3, + bounds.top + bounds.height - borderBottomWidth / 3 + ); + this.bottomLeftBorderDoubleOuterBox = + blh > 0 || blv > 0 + ? getCurvePoints( + bounds.left + borderLeftWidth / 3, + bounds.top + leftHeight, + blh - borderLeftWidth / 3, + blv - borderBottomWidth / 3, + CORNER.BOTTOM_LEFT + ) + : new Vector(bounds.left + borderLeftWidth / 3, bounds.top + bounds.height - borderBottomWidth / 3); + this.topLeftBorderDoubleInnerBox = + tlh > 0 || tlv > 0 + ? getCurvePoints( + bounds.left + (borderLeftWidth * 2) / 3, + bounds.top + (borderTopWidth * 2) / 3, + tlh - (borderLeftWidth * 2) / 3, + tlv - (borderTopWidth * 2) / 3, + CORNER.TOP_LEFT + ) + : new Vector(bounds.left + (borderLeftWidth * 2) / 3, bounds.top + (borderTopWidth * 2) / 3); + this.topRightBorderDoubleInnerBox = + tlh > 0 || tlv > 0 + ? getCurvePoints( + bounds.left + topWidth, + bounds.top + (borderTopWidth * 2) / 3, + trh - (borderRightWidth * 2) / 3, + trv - (borderTopWidth * 2) / 3, + CORNER.TOP_RIGHT + ) + : new Vector( + bounds.left + bounds.width - (borderRightWidth * 2) / 3, + bounds.top + (borderTopWidth * 2) / 3 + ); + this.bottomRightBorderDoubleInnerBox = + brh > 0 || brv > 0 + ? getCurvePoints( + bounds.left + bottomWidth, + bounds.top + rightHeight, + brh - (borderRightWidth * 2) / 3, + brv - (borderBottomWidth * 2) / 3, + CORNER.BOTTOM_RIGHT + ) + : new Vector( + bounds.left + bounds.width - (borderRightWidth * 2) / 3, + bounds.top + bounds.height - (borderBottomWidth * 2) / 3 + ); + this.bottomLeftBorderDoubleInnerBox = + blh > 0 || blv > 0 + ? getCurvePoints( + bounds.left + (borderLeftWidth * 2) / 3, + bounds.top + leftHeight, + blh - (borderLeftWidth * 2) / 3, + blv - (borderBottomWidth * 2) / 3, + CORNER.BOTTOM_LEFT + ) + : new Vector( + bounds.left + (borderLeftWidth * 2) / 3, + bounds.top + bounds.height - (borderBottomWidth * 2) / 3 + ); + this.topLeftBorderStroke = + tlh > 0 || tlv > 0 + ? getCurvePoints( + bounds.left + borderLeftWidth / 2, + bounds.top + borderTopWidth / 2, + tlh - borderLeftWidth / 2, + tlv - borderTopWidth / 2, + CORNER.TOP_LEFT + ) + : new Vector(bounds.left + borderLeftWidth / 2, bounds.top + borderTopWidth / 2); + this.topRightBorderStroke = + tlh > 0 || tlv > 0 + ? getCurvePoints( + bounds.left + topWidth, + bounds.top + borderTopWidth / 2, + trh - borderRightWidth / 2, + trv - borderTopWidth / 2, + CORNER.TOP_RIGHT + ) + : new Vector(bounds.left + bounds.width - borderRightWidth / 2, bounds.top + borderTopWidth / 2); + this.bottomRightBorderStroke = + brh > 0 || brv > 0 + ? getCurvePoints( + bounds.left + bottomWidth, + bounds.top + rightHeight, + brh - borderRightWidth / 2, + brv - borderBottomWidth / 2, + CORNER.BOTTOM_RIGHT + ) + : new Vector( + bounds.left + bounds.width - borderRightWidth / 2, + bounds.top + bounds.height - borderBottomWidth / 2 + ); + this.bottomLeftBorderStroke = + blh > 0 || blv > 0 + ? getCurvePoints( + bounds.left + borderLeftWidth / 2, + bounds.top + leftHeight, + blh - borderLeftWidth / 2, + blv - borderBottomWidth / 2, + CORNER.BOTTOM_LEFT + ) + : new Vector(bounds.left + borderLeftWidth / 2, bounds.top + bounds.height - borderBottomWidth / 2); this.topLeftBorderBox = tlh > 0 || tlv > 0 ? getCurvePoints(bounds.left, bounds.top, tlh, tlv, CORNER.TOP_LEFT) @@ -89,10 +236,10 @@ export class BoundCurves { this.topRightPaddingBox = trh > 0 || trv > 0 ? getCurvePoints( - bounds.left + Math.min(topWidth, bounds.width + borderLeftWidth), + bounds.left + Math.min(topWidth, bounds.width - borderRightWidth), bounds.top + borderTopWidth, - topWidth > bounds.width + borderRightWidth ? 0 : trh - borderRightWidth, - trv - borderTopWidth, + topWidth > bounds.width + borderRightWidth ? 0 : Math.max(0, trh - borderRightWidth), + Math.max(0, trv - borderTopWidth), CORNER.TOP_RIGHT ) : new Vector(bounds.left + bounds.width - borderRightWidth, bounds.top + borderTopWidth); @@ -100,9 +247,9 @@ export class BoundCurves { brh > 0 || brv > 0 ? getCurvePoints( bounds.left + Math.min(bottomWidth, bounds.width - borderLeftWidth), - bounds.top + Math.min(rightHeight, bounds.height + borderTopWidth), + bounds.top + Math.min(rightHeight, bounds.height - borderBottomWidth), Math.max(0, brh - borderRightWidth), - brv - borderBottomWidth, + Math.max(0, brv - borderBottomWidth), CORNER.BOTTOM_RIGHT ) : new Vector( @@ -113,9 +260,9 @@ export class BoundCurves { blh > 0 || blv > 0 ? getCurvePoints( bounds.left + borderLeftWidth, - bounds.top + leftHeight, + bounds.top + Math.min(leftHeight, bounds.height - borderBottomWidth), Math.max(0, blh - borderLeftWidth), - blv - borderBottomWidth, + Math.max(0, blv - borderBottomWidth), CORNER.BOTTOM_LEFT ) : new Vector(bounds.left + borderLeftWidth, bounds.top + bounds.height - borderBottomWidth); diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index f6d3a4a..ae72b45 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -8,10 +8,15 @@ import {TextContainer} from '../../dom/text-container'; import {Path, transformPath} from '../path'; import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip'; import {BoundCurves, calculateBorderBoxPath, calculateContentBoxPath, calculatePaddingBoxPath} from '../bound-curves'; -import {isBezierCurve} from '../bezier-curve'; +import {BezierCurve, isBezierCurve} from '../bezier-curve'; import {Vector} from '../vector'; import {CSSImageType, CSSURLImage, isLinearGradient, isRadialGradient} from '../../css/types/image'; -import {parsePathForBorder} from '../border'; +import { + parsePathForBorder, + parsePathForBorderDoubleInner, + parsePathForBorderDoubleOuter, + parsePathForBorderStroke +} from '../border'; import {Cache} from '../../core/cache-storage'; import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background'; import {isDimensionToken} from '../../css/syntax/parser'; @@ -630,22 +635,37 @@ export class CanvasRenderer { } } - async renderBorder(color: Color, side: number, curvePoints: BoundCurves) { + async renderSolidBorder(color: Color, side: number, curvePoints: BoundCurves) { this.path(parsePathForBorder(curvePoints, side)); this.ctx.fillStyle = asString(color); this.ctx.fill(); } + async renderDoubleBorder(color: Color, width: number, side: number, curvePoints: BoundCurves) { + if (width < 3) { + await this.renderSolidBorder(color, side, curvePoints); + return; + } + + const outerPaths = parsePathForBorderDoubleOuter(curvePoints, side); + this.path(outerPaths); + this.ctx.fillStyle = asString(color); + this.ctx.fill(); + const innerPaths = parsePathForBorderDoubleInner(curvePoints, side); + this.path(innerPaths); + this.ctx.fill(); + } + async renderNodeBackgroundAndBorders(paint: ElementPaint) { this.applyEffects(paint.effects, EffectTarget.BACKGROUND_BORDERS); const styles = paint.container.styles; const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length; const borders = [ - {style: styles.borderTopStyle, color: styles.borderTopColor}, - {style: styles.borderRightStyle, color: styles.borderRightColor}, - {style: styles.borderBottomStyle, color: styles.borderBottomColor}, - {style: styles.borderLeftStyle, color: styles.borderLeftColor} + {style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth}, + {style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth}, + {style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth}, + {style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth} ]; const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea( @@ -705,13 +725,143 @@ export class CanvasRenderer { let side = 0; for (const border of borders) { - if (border.style !== BORDER_STYLE.NONE && !isTransparent(border.color)) { - await this.renderBorder(border.color, side, paint.curves); + if (border.style !== BORDER_STYLE.NONE && !isTransparent(border.color) && border.width > 0) { + if (border.style === BORDER_STYLE.DASHED) { + await this.renderDashedDottedBorder( + border.color, + border.width, + side, + paint.curves, + BORDER_STYLE.DASHED + ); + } else if (border.style === BORDER_STYLE.DOTTED) { + await this.renderDashedDottedBorder( + border.color, + border.width, + side, + paint.curves, + BORDER_STYLE.DOTTED + ); + } else if (border.style === BORDER_STYLE.DOUBLE) { + await this.renderDoubleBorder(border.color, border.width, side, paint.curves); + } else { + await this.renderSolidBorder(border.color, side, paint.curves); + } } side++; } } + async renderDashedDottedBorder( + color: Color, + width: number, + side: number, + curvePoints: BoundCurves, + style: BORDER_STYLE + ) { + this.ctx.save(); + + const strokePaths = parsePathForBorderStroke(curvePoints, side); + const boxPaths = parsePathForBorder(curvePoints, side); + + if (style === BORDER_STYLE.DASHED) { + this.path(boxPaths); + this.ctx.clip(); + } + + let startX, startY, endX, endY; + if (isBezierCurve(boxPaths[0])) { + startX = (boxPaths[0] as BezierCurve).start.x; + startY = (boxPaths[0] as BezierCurve).start.y; + } else { + startX = (boxPaths[0] as Vector).x; + startY = (boxPaths[0] as Vector).y; + } + if (isBezierCurve(boxPaths[1])) { + endX = (boxPaths[1] as BezierCurve).end.x; + endY = (boxPaths[1] as BezierCurve).end.y; + } else { + endX = (boxPaths[1] as Vector).x; + endY = (boxPaths[1] as Vector).y; + } + + let length; + if (side === 0 || side === 2) { + length = Math.abs(startX - endX); + } else { + length = Math.abs(startY - endY); + } + + this.ctx.beginPath(); + if (style === BORDER_STYLE.DOTTED) { + this.formatPath(strokePaths); + } else { + this.formatPath(boxPaths.slice(0, 2)); + } + + let dashLength = width < 3 ? width * 3 : width * 2; + let spaceLength = width < 3 ? width * 2 : width; + if (style === BORDER_STYLE.DOTTED) { + dashLength = width; + spaceLength = width; + } + + let useLineDash = true; + if (length <= dashLength * 2) { + useLineDash = false; + } else if (length <= dashLength * 2 + spaceLength) { + const multiplier = length / (2 * dashLength + spaceLength); + dashLength *= multiplier; + spaceLength *= multiplier; + } else { + const numberOfDashes = Math.floor((length + spaceLength) / (dashLength + spaceLength)); + const minSpace = (length - numberOfDashes * dashLength) / (numberOfDashes - 1); + const maxSpace = (length - (numberOfDashes + 1) * dashLength) / numberOfDashes; + spaceLength = + maxSpace <= 0 || Math.abs(spaceLength - minSpace) < Math.abs(spaceLength - maxSpace) + ? minSpace + : maxSpace; + } + + if (useLineDash) { + if (style === BORDER_STYLE.DOTTED) { + this.ctx.setLineDash([0, dashLength + spaceLength]); + } else { + this.ctx.setLineDash([dashLength, spaceLength]); + } + } + + if (style === BORDER_STYLE.DOTTED) { + this.ctx.lineCap = 'round'; + this.ctx.lineWidth = width; + } else { + this.ctx.lineWidth = width * 2 + 1.1; + } + this.ctx.strokeStyle = asString(color); + this.ctx.stroke(); + this.ctx.setLineDash([]); + + // dashed round edge gap + if (style === BORDER_STYLE.DASHED) { + if (isBezierCurve(boxPaths[0])) { + const path1 = boxPaths[3] as BezierCurve; + const path2 = boxPaths[0] as BezierCurve; + this.ctx.beginPath(); + this.formatPath([new Vector(path1.end.x, path1.end.y), new Vector(path2.start.x, path2.start.y)]); + this.ctx.stroke(); + } + if (isBezierCurve(boxPaths[1])) { + const path1 = boxPaths[1] as BezierCurve; + const path2 = boxPaths[2] as BezierCurve; + this.ctx.beginPath(); + this.formatPath([new Vector(path1.end.x, path1.end.y), new Vector(path2.start.x, path2.start.y)]); + this.ctx.stroke(); + } + } + + this.ctx.restore(); + } + async render(element: ElementContainer): Promise { if (this.options.backgroundColor) { this.ctx.fillStyle = asString(this.options.backgroundColor); diff --git a/tests/karma.ts b/tests/karma.ts index 454db1b..edf8960 100644 --- a/tests/karma.ts +++ b/tests/karma.ts @@ -23,10 +23,11 @@ const servers: Server[] = []; servers.push(screenshotApp.listen(8000)); servers.push(corsApp.listen(8081)); -karmaTestRunner().then(() => { - servers.forEach(server => server.close()); -}).catch(e => { - console.error(e); - process.exit(1); -}); - +karmaTestRunner() + .then(() => { + servers.forEach(server => server.close()); + }) + .catch(e => { + console.error(e); + process.exit(1); + });