feat: improve box render

This commit is contained in:
chenshenhai 2023-10-29 21:42:39 +08:00
parent a7da9c4bd0
commit 3f9dca1353
9 changed files with 306 additions and 98 deletions

View file

@ -1,5 +1,6 @@
import { ViewContext2D, Element, ElementType, ElementSize, ViewScaleInfo, ViewSizeInfo, TransformAction } from '@idraw/types';
import { istype, isColorStr, generateSVGPath, rotateElement, is } from '@idraw/util';
import { createColorStyle } from './color';
export function drawBox(
ctx: ViewContext2D,
@ -26,9 +27,11 @@ export function drawBox(
viewScaleInfo,
viewSizeInfo,
renderContent: () => {
drawBoxBorder(ctx, viewElem, { viewScaleInfo, viewSizeInfo });
drawBoxBackground(ctx, viewElem, { pattern, viewScaleInfo, viewSizeInfo });
renderContent?.();
drawBoxBorder(ctx, viewElem, { viewScaleInfo, viewSizeInfo });
// TODO
// drawBoxBackground(ctx, viewElem, { pattern, viewScaleInfo, viewSizeInfo });
}
});
ctx.globalAlpha = 1;
@ -84,20 +87,53 @@ function drawBoxBackground(
opts: { pattern?: string | CanvasPattern | null; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }
): void {
const { pattern, viewScaleInfo } = opts;
const { scale } = viewScaleInfo;
let transform: TransformAction[] = [];
let { borderRadius, boxSizing, borderWidth } = viewElem.detail;
if (typeof borderWidth !== 'number') {
// TODO: If borderWidth is an array, borderRadius will not take effect and will become 0.
borderRadius = 0;
}
if (viewElem.detail.background || pattern) {
const { x, y, w, h } = viewElem;
let r: number = (viewElem.detail.borderRadius || 0) * viewScaleInfo.scale;
r = Math.min(r, w / 2, h / 2);
if (w < r * 2 || h < r * 2) {
r = 0;
let { x, y, w, h } = viewElem;
let radiusList: [number, number, number, number] = [0, 0, 0, 0];
if (typeof borderRadius === 'number') {
const br = borderRadius * scale;
radiusList = [br, br, br, br];
} else if (Array.isArray(borderRadius) && borderRadius?.length === 4) {
radiusList = [borderRadius[0] * scale, borderRadius[1] * scale, borderRadius[2] * scale, borderRadius[3] * scale];
}
let bw: number = 0;
if (typeof borderWidth === 'number') {
bw = (borderWidth || 1) * scale;
}
if (boxSizing === 'border-box') {
x = viewElem.x + bw / 2;
y = viewElem.y + bw / 2;
w = viewElem.w - bw;
h = viewElem.h - bw;
} else if (boxSizing === 'content-box') {
x = viewElem.x - bw / 2;
y = viewElem.y - bw / 2;
w = viewElem.w + bw;
h = viewElem.h + bw;
} else {
x = viewElem.x;
y = viewElem.y;
w = viewElem.w;
h = viewElem.h;
}
// r = Math.min(r, w / 2, h / 2);
// if (w < r * 2 || h < r * 2) {
// r = 0;
// }
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.moveTo(x + radiusList[0], y);
ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]);
ctx.arcTo(x + w, y + h, x, y + h, radiusList[2]);
ctx.arcTo(x, y + h, x, y, radiusList[3]);
ctx.arcTo(x, y, x + w, y, radiusList[0]);
ctx.closePath();
if (typeof pattern === 'string') {
ctx.fillStyle = pattern;
@ -106,38 +142,17 @@ function drawBoxBackground(
} else if (typeof viewElem.detail.background === 'string') {
ctx.fillStyle = viewElem.detail.background;
} else if (viewElem.detail.background?.type === 'linearGradient') {
const { start, end, stops } = viewElem.detail.background;
const viewStart = {
x: start.x + x,
y: start.y + y
};
const viewEnd = {
x: end.x + x,
y: end.y + y
};
const linearGradient = ctx.createLinearGradient(viewStart.x, viewStart.y, viewEnd.x, viewEnd.y);
stops.forEach((stop) => {
linearGradient.addColorStop(stop.offset, stop.color);
const colorStyle = createColorStyle(ctx, viewElem.detail.background, {
viewElementSize: { x, y, w, h },
viewScaleInfo
});
ctx.fillStyle = linearGradient;
ctx.fillStyle = colorStyle;
} else if (viewElem.detail.background?.type === 'radialGradient') {
const { inner, outer, stops } = viewElem.detail.background;
transform = viewElem.detail.background.transform || [];
const viewInner = {
x: inner.x,
y: inner.y,
radius: inner.radius * viewScaleInfo.scale
};
const viewOuter = {
x: outer.x,
y: outer.y,
radius: outer.radius * viewScaleInfo.scale
};
const radialGradient = ctx.createRadialGradient(viewInner.x, viewInner.y, viewInner.radius, viewOuter.x, viewOuter.y, viewOuter.radius);
stops.forEach((stop) => {
radialGradient.addColorStop(stop.offset, stop.color);
const colorStyle = createColorStyle(ctx, viewElem.detail.background, {
viewElementSize: { x, y, w, h },
viewScaleInfo
});
ctx.fillStyle = radialGradient;
ctx.fillStyle = colorStyle;
if (transform && transform.length > 0) {
for (let i = 0; i < transform?.length; i++) {
const action = transform[i];
@ -176,17 +191,24 @@ function drawBoxBorder(ctx: ViewContext2D, viewElem: Element<ElementType>, opts:
return;
}
const { viewScaleInfo } = opts;
const { scale } = viewScaleInfo;
let borderColor = '#000000';
if (isColorStr(viewElem.detail.borderColor) === true) {
borderColor = viewElem.detail.borderColor as string;
}
const { borderWidth, borderRadius, borderDash } = viewElem.detail;
const { borderWidth, borderRadius, borderDash, boxSizing } = viewElem.detail;
let bw: number = 0;
if (typeof borderWidth === 'number') {
bw = borderWidth || 1;
}
bw = bw * viewScaleInfo.scale;
let r: number = borderRadius || 0;
bw = bw * scale;
let radiusList: [number, number, number, number] = [0, 0, 0, 0];
if (typeof borderRadius === 'number') {
const br = borderRadius * scale;
radiusList = [br, br, br, br];
} else if (Array.isArray(borderRadius) && borderRadius?.length === 4) {
radiusList = [borderRadius[0] * scale, borderRadius[1] * scale, borderRadius[2] * scale, borderRadius[3] * scale];
}
ctx.strokeStyle = borderColor;
ctx.setLineDash(borderDash || []);
@ -195,72 +217,96 @@ function drawBoxBorder(ctx: ViewContext2D, viewElem: Element<ElementType>, opts:
let borderBottom = 0;
let borderLeft = 0;
if (Array.isArray(borderWidth)) {
borderTop = borderWidth[0] || 0;
borderRight = borderWidth[1] || 0;
borderBottom = borderWidth[2] || 0;
borderLeft = borderWidth[3] || 0;
borderTop = (borderWidth[0] || 0) * scale;
borderRight = (borderWidth[1] || 0) * scale;
borderBottom = (borderWidth[2] || 0) * scale;
borderLeft = (borderWidth[3] || 0) * scale;
}
if (borderLeft || borderRight || borderTop || borderBottom) {
const { x, y, w, h } = viewElem;
if (borderLeft) {
let { x, y, w, h } = viewElem;
if (boxSizing === 'border-box') {
x = x + borderLeft / 2;
y = y + borderTop / 2;
w = w - borderLeft / 2 - borderRight / 2;
h = h - borderTop / 2 - borderBottom / 2;
} else if (boxSizing === 'content-box') {
x = x - borderLeft / 2;
y = y - borderTop / 2;
w = w + borderLeft / 2 + borderRight / 2;
h = h + borderTop / 2 + borderBottom / 2;
} else {
// center-line
x = viewElem.x;
y = viewElem.y;
w = viewElem.w;
h = viewElem.h;
}
if (borderTop) {
ctx.beginPath();
ctx.lineWidth = borderLeft * viewScaleInfo.scale;
ctx.moveTo(x, y);
ctx.lineTo(x, y + h);
ctx.lineWidth = borderTop;
ctx.moveTo(x - borderLeft / 2, y);
ctx.lineTo(x + w + borderRight / 2, y);
ctx.closePath();
ctx.stroke();
}
if (borderRight) {
ctx.beginPath();
ctx.lineWidth = borderRight * viewScaleInfo.scale;
ctx.moveTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.closePath();
ctx.stroke();
}
if (borderTop) {
ctx.beginPath();
ctx.lineWidth = borderTop * viewScaleInfo.scale;
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineWidth = borderRight;
ctx.moveTo(x + w, y - borderTop / 2);
ctx.lineTo(x + w, y + h + borderBottom / 2);
ctx.closePath();
ctx.stroke();
}
if (borderBottom) {
ctx.beginPath();
ctx.lineWidth = borderBottom * viewScaleInfo.scale;
ctx.moveTo(x, y + h);
ctx.lineTo(x + w, y + h);
ctx.lineWidth = borderBottom;
ctx.moveTo(x - borderLeft / 2, y + h);
ctx.lineTo(x + w + borderRight / 2, y + h);
ctx.closePath();
ctx.stroke();
}
if (borderLeft) {
ctx.beginPath();
ctx.lineWidth = borderLeft;
ctx.moveTo(x, y - borderTop / 2);
ctx.lineTo(x, y + h + borderBottom / 2);
ctx.closePath();
ctx.stroke();
}
} else {
let { x, y, w, h } = viewElem;
const { boxSizing } = viewElem.detail;
if (boxSizing === 'border-box') {
x = viewElem.x + bw / 2;
y = viewElem.y + bw / 2;
w = viewElem.w - bw;
h = viewElem.h - bw;
} else if (boxSizing === 'content-box') {
x = viewElem.x - bw / 2;
y = viewElem.y - bw / 2;
w = viewElem.w + bw;
h = viewElem.h + bw;
} else {
// center-line
x = viewElem.x;
y = viewElem.y;
w = viewElem.w;
h = viewElem.h;
} else {
x = viewElem.x - bw;
y = viewElem.y - bw;
w = viewElem.w + bw * 2;
h = viewElem.h + bw * 2;
}
r = Math.min(r, w / 2, h / 2);
if (r < w / 2 && r < h / 2) {
r = r + bw / 2;
}
// r = Math.min(r, w / 2, h / 2);
// if (r < w / 2 && r < h / 2) {
// r = r + bw / 2;
// }
ctx.beginPath();
ctx.lineCap = 'square';
ctx.lineWidth = bw;
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.moveTo(x + radiusList[0], y);
ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]);
ctx.arcTo(x + w, y + h, x, y + h, radiusList[2]);
ctx.arcTo(x, y + h, x, y, radiusList[3]);
ctx.arcTo(x, y, x + w, y, radiusList[0]);
ctx.closePath();
ctx.stroke();
}

View file

@ -1,5 +1,6 @@
import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types';
import { rotateElement } from '@idraw/util';
import { createColorStyle } from './color';
export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: RendererDrawElementOptions) {
const { detail, angle } = elem;
@ -14,7 +15,7 @@ export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: Re
const centerY = y + b;
// draw border
if (borderWidth && borderWidth > 0) {
if (typeof borderWidth === 'number' && borderWidth > 0) {
const ba = borderWidth / 2 + a;
const bb = borderWidth / 2 + b;
ctx.beginPath();
@ -27,7 +28,11 @@ export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: Re
// draw content
ctx.beginPath();
ctx.fillStyle = background;
const fillStyle = createColorStyle(ctx, background, {
viewElementSize: { x, y, w, h },
viewScaleInfo
});
ctx.fillStyle = fillStyle;
ctx.circle(centerX, centerY, a, b, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();

View file

@ -0,0 +1,55 @@
import type { ViewContext2D, ViewScaleInfo, ElementSize, LinearGradientColor, RadialGradientColor } from '@idraw/types';
export function createColorStyle(
ctx: ViewContext2D,
color: string | LinearGradientColor | RadialGradientColor | undefined,
opts: {
viewElementSize: ElementSize;
viewScaleInfo: ViewScaleInfo;
}
): string | CanvasPattern | CanvasGradient {
if (typeof color === 'string') {
return color;
}
const { viewElementSize, viewScaleInfo } = opts;
const { x, y } = viewElementSize;
const { scale } = viewScaleInfo;
if (color?.type === 'linearGradient') {
const { start, end, stops } = color;
const viewStart = {
x: x + start.x * scale,
y: y + start.y * scale
};
const viewEnd = {
x: x + end.x * scale,
y: y + end.y * scale
};
const linearGradient = ctx.createLinearGradient(viewStart.x, viewStart.y, viewEnd.x, viewEnd.y);
stops.forEach((stop) => {
linearGradient.addColorStop(stop.offset, stop.color);
});
return linearGradient;
}
if (color?.type === 'radialGradient') {
const { inner, outer, stops } = color;
const viewInner = {
x: x + inner.x * scale,
y: y + inner.y * scale,
radius: inner.radius * scale
};
const viewOuter = {
x: x + outer.x * scale,
y: y + outer.y * scale,
radius: outer.radius * scale
};
const radialGradient = ctx.createRadialGradient(viewInner.x, viewInner.y, viewInner.radius, viewOuter.x, viewOuter.y, viewOuter.radius);
stops.forEach((stop) => {
radialGradient.addColorStop(stop.offset, stop.color);
});
return radialGradient;
}
return '#000000';
}

View file

@ -5,7 +5,7 @@ export interface Data<E extends Record<string, any> = Record<string, any>> {
assets?: ElementAssets;
}
export type ColorMatrix = [
export type Matrix = [
number,
number,
number,
@ -27,3 +27,5 @@ export type ColorMatrix = [
number,
number
];
export type ColorMatrix = Matrix;

View file

@ -42,14 +42,17 @@ export interface TransformScale {
export type TransformAction = TransformMatrix | TransformTranslate | TransformRotate | TransformScale;
export interface GradientStop {
offset: number;
color: string;
}
export interface LinearGradientColor {
type: 'linearGradient';
start: PointSize;
end: PointSize;
stops: Array<{
offset: number;
color: string;
}>;
stops: Array<GradientStop>;
angle?: number;
transform?: TransformAction[];
}
@ -61,15 +64,13 @@ export interface RadialGradientColor {
type: 'radialGradient';
inner: GadialCircle;
outer: GadialCircle;
stops: Array<{
offset: number;
color: string;
}>;
stops: Array<GradientStop>;
angle?: number;
transform?: TransformAction[];
}
export interface ElementBaseDetail {
// boxSizing?: 'content-box' | 'border-box'; // default content-box
boxSizing?: 'content-box' | 'border-box' | 'center-line'; // default center-line
borderWidth?: number | [number, number, number, number]; // [top, right, bottom, left]
borderColor?: string;
borderRadius?: number | [number, number, number, number]; // [top-left, top-right, bottom-left, bottom-right]

View file

@ -1,6 +1,6 @@
export { delay, compose, throttle } from './lib/time';
export { downloadImageFromCanvas } from './lib/file';
export { toColorHexStr, toColorHexNum, isColorStr, colorNameToHex } from './lib/color';
export { toColorHexStr, toColorHexNum, isColorStr, colorNameToHex, colorToCSS, colorToLinearGradientCSS } from './lib/color';
export { createUUID, isAssetId, createAssetId } from './lib/uuid';
export { deepClone, sortDataAsserts } from './lib/data';
export { istype } from './lib/istype';
@ -53,3 +53,4 @@ export { generateSVGPath, parseSVGPath } from './lib/svg-path';
export { generateHTML, parseHTML } from './lib/html';
export { compressImage } from './lib/image';
export { formatNumber } from './lib/number';
export { matrixToAngle, matrixToRadian } from './lib/matrix';

View file

@ -1,3 +1,6 @@
import type { LinearGradientColor, RadialGradientColor } from '@idraw/types';
import { matrixToRadian } from '@idraw/util';
export function toColorHexNum(color: string): number {
return parseInt(color.replace(/^\#/, '0x'));
}
@ -163,3 +166,51 @@ export function colorNameToHex(name: string): string | null {
}
return null;
}
export function colorToCSS(color?: string | LinearGradientColor | RadialGradientColor): string {
let css = 'transparent';
if (typeof color === 'string') {
css = color;
} else if (color?.type === 'linearGradient') {
const items: string[] = [];
if (typeof color.angle === 'number') {
items.push(`${color.angle}deg`);
} else {
items.push(`0deg`);
}
if (Array.isArray(color.stops)) {
color.stops.forEach((stop) => {
items.push(`${stop.color} ${stop.offset * 100}%`);
});
}
css = `linear-gradient(${items.join(', ')})`;
} else if (color?.type === 'radialGradient') {
const items: string[] = [];
if (Array.isArray(color.stops)) {
color.stops.forEach((stop) => {
items.push(`${stop.color} ${stop.offset * 100}%`);
});
}
css = `radial-gradient(circle, ${items.join(', ')})`;
}
return css;
}
export function colorToLinearGradientCSS(color?: string | LinearGradientColor | RadialGradientColor): string {
let css = 'transparent';
if (typeof color === 'string') {
css = color;
} else if (color?.type === 'radialGradient' || color?.type === 'linearGradient') {
const items: string[] = [];
if (Array.isArray(color.stops) && color.stops.length > 0) {
color.stops.forEach((stop, i) => {
items.push(`${stop.color} ${stop.offset * 100}%`);
if (i === color.stops.length - 1 && stop.offset < 1) {
items.push(`${stop.color} ${stop.offset * 100}%`);
}
});
css = `linear-gradient(90deg, ${items.join(', ')})`;
}
}
return css;
}

View file

@ -1,6 +1,7 @@
import type { Data, Element, Elements, ElementType, ElementSize, ViewContextSize, ViewSizeInfo, RecursivePartial } from '@idraw/types';
import { rotateElementVertexes } from './rotate';
import { isAssetId } from './uuid';
import { istype } from './istype';
// // TODO need to be deprecated
// function getGroupIndexes(elem: Element<'group'>, uuids: string[], parentIndex: string): string[] {
@ -296,13 +297,31 @@ function mergeElement<T extends Element<ElementType> = Element<ElementType>>(ori
originElem[commonKey] = updateContent[commonKey];
} else if (['detail', 'operations'].includes(commonKey)) {
// @ts-ignore
if (istype.json(updateContent[commonKey] as any) && istype.json(originElem[commonKey])) {
if (istype.json(updateContent[commonKey] as any)) {
if (!(originElem as Object)?.hasOwnProperty(commonKey)) {
// @ts-ignore
originElem[commonKey] = {};
}
// @ts-ignore
originElem[commonKey] = { ...originElem[commonKey], ...updateContent[commonKey] };
if (istype.json(originElem[commonKey])) {
// @ts-ignore
originElem[commonKey] = { ...originElem[commonKey], ...updateContent[commonKey] };
}
// @ts-ignore
} else if (istype.array(updateContent[commonKey] as any) && istype.array(originElem[commonKey])) {
} else if (istype.array(updateContent[commonKey] as any)) {
if (!(originElem as Object)?.hasOwnProperty(commonKey)) {
// @ts-ignore
originElem[commonKey] = [];
}
// @ts-ignore
originElem[commonKey] = { ...originElem[commonKey], ...updateContent[commonKey] };
if (istype.array(originElem[commonKey])) {
((updateContent as any)?.[commonKey] as Array<any>)?.forEach((item, i) => {
// @ts-ignore
originElem[commonKey][i] = item;
});
// @ts-ignore
originElem[commonKey] = [...originElem[commonKey], ...updateContent[commonKey]];
}
}
}
}

View file

@ -0,0 +1,28 @@
import type { Matrix } from '@idraw/types';
/**
| x_sc y_sk 0 |
| x_sk y_sc 0 |
| x_tr y_tr 1 |
https://stackoverflow.com/questions/5072271/get-angle-from-matrix
*/
export function matrixToRadian(matrix: Matrix): number | null {
if (matrix[1] != -1 * matrix[3] || matrix[4] != matrix[0] || matrix[0] * matrix[4] - matrix[3] * matrix[1] != 1) {
return null;
} else {
return Math.acos(matrix[0]);
}
}
export function matrixToAngle(matrix: Matrix): number | null {
const radian = matrixToRadian(matrix);
if (typeof radian === 'number') {
const angle = (radian * 180) / Math.PI;
return angle;
}
return radian;
}