add features for border-style dashed, dotted, double. (#2531)

This commit is contained in:
flyskyko 2021-07-04 13:17:07 +09:00 committed by GitHub
parent 2a013e20c8
commit 72cd528429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 434 additions and 27 deletions

View File

@ -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<BORDER_STYLE> => ({
@ -13,6 +16,12 @@ const borderStyleForSide = (side: string): IPropertyIdentValueDescriptor<BORDER_
switch (style) {
case 'none':
return BORDER_STYLE.NONE;
case 'dashed':
return BORDER_STYLE.DASHED;
case 'dotted':
return BORDER_STYLE.DOTTED;
case 'double':
return BORDER_STYLE.DOUBLE;
}
return BORDER_STYLE.SOLID;
}

View File

@ -50,6 +50,7 @@ export const display: IPropertyListDescriptor<Display> = {
const parseDisplayValue = (display: string): Display => {
switch (display) {
case 'block':
case '-webkit-box':
return DISPLAY.BLOCK;
case 'inline':
return DISPLAY.INLINE;

6
src/global.d.ts vendored
View File

@ -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 {

View File

@ -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)) {

View File

@ -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);

View File

@ -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<HTMLCanvasElement> {
if (this.options.backgroundColor) {
this.ctx.fillStyle = asString(this.options.backgroundColor);

View File

@ -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);
});