import { getRotateMatrix, getTranslateMatrix, matrixMutiply, pointMultiplyMatrix } from './matrix'; interface Rect { x: number; y: number; width: number; height: number; } export function getUrlFromDecal(str: string): string | undefined { const head = 'image://'; const index = str?.indexOf?.(head); if (index >= 0) { const s = str.substring(index + head.length); return s; } return; } export function splitGridRect( rect: Rect, dashArrayX: number[], dashArrayY: number[], ) { let w = 0; let h = 0; dashArrayY?.forEach?.((v) => { if (v > 0) { h = h + v; } }); dashArrayX?.forEach?.((v) => { if (v > 0) { w = w + v; } }); const arrays: Rect[][] = []; if (w > 0 && h > 0) { const wc = Math.ceil(rect.width / w); const hc = Math.ceil(rect.height / h); let nextY = rect.y; for (let i = 0; i < hc; i++) { let nextX = rect.x; // eslint-disable-next-line no-loop-func dashArrayY.forEach((height, hIndex) => { let row = []; if (!isNotPaint(hIndex)) { for (let j = 0; j < wc; j++) { row = row.concat(splitToRect(nextX, nextY, height, dashArrayX)); nextX = nextX + w; } } nextY = nextY + height; if (row.length > 0) { arrays.push(row); } }); } } return arrays; } function isNotPaint(index) { return index % 2 === 1; } function splitToRect(x, y, height, dashArrayX: number[]) { let nextX = x; const result = []; dashArrayX?.forEach((width, index) => { if (width > 0) { if (!isNotPaint(index)) { result.push({ x: nextX, y, width, height, }); } nextX = nextX + width; } }); return result; } export function drawDecal( ctx: CanvasRenderingContext2D, rect: Rect, dashArrayX: number[], dashArrayY: number[], render: (ctx: CanvasRenderingContext2D, rect: Rect) => void, ) { const grid: Rect[][] = splitGridRect(rect, dashArrayX, dashArrayY); grid?.forEach?.((array) => { array?.forEach?.((rectangle) => { render(ctx, rectangle); }); }); } export interface DecalProps { symbol: string; dashArrayX?: number | number[]; dashArrayY?: number | number[]; rotation?: number; } export class Decal { dashArrayX: number[] = [5, 5]; dashArrayY: number[] = [5, 5]; symbol: string; rotation = 0; color: string; isValid() { return ['circle', 'rect', 'triangle'].indexOf(this.symbol) >= 0; } init(props: DecalProps) { if (!props) { return; } this.setSymbol(props.symbol); this.setDashArrayX(props.dashArrayX); this.setDashArrayY(props.dashArrayY); this.setRotation(props.rotation); } setColor(color) { this.color = color; } setDashArrayX(array) { if (!array) { return; } let newArray: number[]; if (Array.isArray(array)) { newArray = array; } else { newArray = [array, array]; } this.dashArrayX = newArray; } setDashArrayY(array: number | number[]) { if (!array) { return; } let newArray: number[]; if (Array.isArray(array)) { newArray = array; } else { newArray = [array, array]; } this.dashArrayY = newArray;; } setSymbol(symbol: string) { this.symbol = symbol; } setRotation(rotate = 0) { this.rotation = rotate; } fillCircle(ctx: CanvasRenderingContext2D, circle: { x: number; y: number;radius: number}, color?: string) { const { x, y, radius } = circle; const leftTop = { x: x - radius, y: y - radius, }; const rightBottom = { x: x + radius, y: y + radius, }; const rect = calculateRect([leftTop, rightBottom]); ctx.save(); const points = getPointsFromRect(rect); const center = getRectCenter(rect); // 中心平移回0点 const trans = getTranslateMatrix(-center.x, -center.y); // 矩形旋转 const r = getRotateMatrix(this.rotation); // 从0点平移到原来的位置 const t = getTranslateMatrix(center.x, center.y); const m = matrixMutiply(t, r, trans); // 绕rect中心旋转后的rect上的四个点,此时 const newPoints = points.map((p) => { const p1 = pointMultiplyMatrix(p, m); return p1; }); // 重新计算包围矩形,用来填充图形 const newRect = calculateRect(newPoints); // 变换画布,抵消掉旋转变换 ctx.save(); ctx.translate(center.x, center.y); ctx.rotate(-this.rotation); ctx.translate(-center.x, -center.y); ctx.save(); // 裁剪 // buildPath(ctx, newPoints, false, true); ctx.beginPath(); ctx.arc(center.x, center.y, radius, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); drawDecal( ctx, newRect, this.dashArrayX, this.dashArrayY, (ctx, rectangle) => { this.drawForSymbol(ctx, rectangle, color); }, ); ctx.restore(); ctx.restore(); } fillRect(ctx, rect, color?: string) { ctx.save(); const points = getPointsFromRect(rect); const center = getRectCenter(rect); // 中心平移回0点 const trans = getTranslateMatrix(-center.x, -center.y); // 矩形旋转 const r = getRotateMatrix(this.rotation); // 从0点平移到原来的位置 const t = getTranslateMatrix(center.x, center.y); const m = matrixMutiply(t, r, trans); // 绕rect中心旋转后的rect上的四个点,此时 const newPoints = points.map((p) => { const p1 = pointMultiplyMatrix(p, m); return p1; }); // 重新计算包围矩形,用来填充图形 const newRect = calculateRect(newPoints); // 变换画布,抵消掉旋转变换 ctx.save(); ctx.translate(center.x, center.y); ctx.rotate(-this.rotation); ctx.translate(-center.x, -center.y); ctx.save(); // 裁剪 buildPath(ctx, newPoints, false, true); ctx.clip(); drawDecal( ctx, newRect, this.dashArrayX, this.dashArrayY, (ctx, rectangle) => { this.drawForSymbol(ctx, rectangle, color); }, ); ctx.restore(); ctx.restore(); } private drawForRect(ctx: CanvasRenderingContext2D, rectangle: Rect, color?: string) { ctx.save(); buildPath(ctx, getPointsFromRect(rectangle), false, true); ctx.fillStyle = color || this.color; ctx.fill(); ctx.restore(); } private drawForTriangle(ctx: CanvasRenderingContext2D, rectangle: Rect, color?: string) { const topPoint = { x: rectangle.x + rectangle.width / 2, y: rectangle.y }; const leftPoint = { x: rectangle.x, y: rectangle.y + rectangle.height }; const rightPont = { x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height }; const points = [topPoint, leftPoint, rightPont]; ctx.save(); buildPath(ctx, points, false, true); ctx.fillStyle = color || this.color; ctx.fill(); ctx.restore(); } private drawForCircle(ctx: CanvasRenderingContext2D, rectangle: Rect, color?: string) { const radius = Math.min(rectangle.width, rectangle.height) / 2; const center = getRectCenter(rectangle); ctx.save(); ctx.beginPath(); ctx.arc(center.x, center.y, radius, 0, Math.PI * 2); ctx.closePath(); ctx.fillStyle = color || this.color; ctx.fill(); ctx.restore(); } private drawForSymbol(ctx: CanvasRenderingContext2D, rectangle: Rect, color?: string) { if (this.symbol === 'rect') { this.drawForRect(ctx, rectangle, color); } else if (this.symbol === 'circle') { this.drawForCircle(ctx, rectangle, color); } else if (this.symbol === 'triangle') { this.drawForTriangle(ctx, rectangle, color); } } } export function equal(value1, value2, strict = false): boolean { let result; if (strict) { result = value1 === value2; } else { // eslint-disable-next-line eqeqeq result = value1 == value2; } if (result) { return result; } return typeof value1 === 'number' && typeof value2 === 'number' && isNaN(value1) && isNaN(value2); } export function calculateRect(points) { if (!points || points.length < 1) { return null as any; } let xMax; let yMax; let xMin; let yMin; points.forEach((point) => { if (point && !equal(point.x, null) && !equal(point.y, null)) { if (equal(xMax, null)) { xMax = point.x; } else { if (point.x > xMax) { xMax = point.x; } } if (equal(xMin, null)) { xMin = point.x; } else { if (point.x < xMin) { xMin = point.x; } } if (equal(yMax, null)) { yMax = point.y; } else { if (point.y > yMax) { yMax = point.y; } } if (equal(yMin, null)) { yMin = point.y; } else { if (point.y < yMin) { yMin = point.y; } } } }); if (!equal(xMax, null)) { return { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin }; } } export function getPointsFromRect(rect) { let result: any = []; if (rect) { const lt = { x: rect.x, y: rect.y }; const rt = { x: rect.x + rect.width, y: rect.y }; const rb = { x: rect.x + rect.width, y: rect.y + rect.height }; const lb = { x: rect.x, y: rect.y + rect.height }; result = [lt, rt, rb, lb]; } return result; } export function buildPath( ctx, points: Array<{ x: number; y: number }>, notNeedBeginPath = false, needClosePath = false) { if (points?.[0]) { if (!notNeedBeginPath) { ctx.beginPath(); } ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { const p = points[i]; ctx.lineTo(p.x, p.y); } if (needClosePath) { ctx.closePath(); } return true; } return false; } export function getRectCenter(rect: any): { x: number; y: number } | null { if (validateRect(rect)) { return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }; } return null; } export function validateRect(rect) { return rect && !equal(rect.x, null) && !equal(rect.y, null) && !equal(rect.width, null) && !equal(rect.height, null); }