diff --git a/packages/renderer/src/draw/base.ts b/packages/renderer/src/draw/base.ts index d2239e5..97bb8b0 100644 --- a/packages/renderer/src/draw/base.ts +++ b/packages/renderer/src/draw/base.ts @@ -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, 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, 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(); } diff --git a/packages/renderer/src/draw/circle.ts b/packages/renderer/src/draw/circle.ts index a8b3915..2349acd 100644 --- a/packages/renderer/src/draw/circle.ts +++ b/packages/renderer/src/draw/circle.ts @@ -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(); diff --git a/packages/renderer/src/draw/color.ts b/packages/renderer/src/draw/color.ts new file mode 100644 index 0000000..1c99e26 --- /dev/null +++ b/packages/renderer/src/draw/color.ts @@ -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'; +} diff --git a/packages/types/src/lib/data.ts b/packages/types/src/lib/data.ts index 69445b6..f8ff923 100644 --- a/packages/types/src/lib/data.ts +++ b/packages/types/src/lib/data.ts @@ -5,7 +5,7 @@ export interface Data = Record> { assets?: ElementAssets; } -export type ColorMatrix = [ +export type Matrix = [ number, number, number, @@ -27,3 +27,5 @@ export type ColorMatrix = [ number, number ]; + +export type ColorMatrix = Matrix; diff --git a/packages/types/src/lib/element.ts b/packages/types/src/lib/element.ts index 3a8fe1f..b67f9ef 100644 --- a/packages/types/src/lib/element.ts +++ b/packages/types/src/lib/element.ts @@ -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; + 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; + 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] diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index fda305b..052858b 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -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'; diff --git a/packages/util/src/lib/color.ts b/packages/util/src/lib/color.ts index d09afd0..1880530 100644 --- a/packages/util/src/lib/color.ts +++ b/packages/util/src/lib/color.ts @@ -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; +} diff --git a/packages/util/src/lib/element.ts b/packages/util/src/lib/element.ts index e67a352..6621559 100644 --- a/packages/util/src/lib/element.ts +++ b/packages/util/src/lib/element.ts @@ -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 = Element>(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)?.forEach((item, i) => { + // @ts-ignore + originElem[commonKey][i] = item; + }); + // @ts-ignore + originElem[commonKey] = [...originElem[commonKey], ...updateContent[commonKey]]; + } } } } diff --git a/packages/util/src/lib/matrix.ts b/packages/util/src/lib/matrix.ts new file mode 100644 index 0000000..4761724 --- /dev/null +++ b/packages/util/src/lib/matrix.ts @@ -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; +}