import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; import { ERenderItemEvent, RenderItem } from '../item'; import { Transform } from '../transform'; import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { isNil } from 'lodash-es'; /* * Expected usage (this comment needs to be deleted after implementing correctly): * * * * * * Line BezierCurve QuadraticCurve 无法设置填充属性,如设置则无效 */ export interface ILineProperty { /** 线宽 */ lineWidth: number; /** 线的虚线设置 */ lineDash?: number[]; /** 虚线偏移量 */ lineDashOffset?: number; /** 线的连接样式 */ lineJoin: CanvasLineJoin; /** 线的顶端样式 */ lineCap: CanvasLineCap; /** 线的斜接限制,当连接为miter类型时可填,默认为10 */ miterLimit: number; } export interface IGraphicProperty extends ILineProperty { /** 渲染模式,参考 {@link GraphicMode} */ mode: GraphicMode; /** 填充样式 */ fill: CanvasStyle; /** 描边样式 */ stroke: CanvasStyle; /** 填充算法 */ fillRule: CanvasFillRule; } export const enum GraphicMode { /** 仅填充 */ Fill, /** 仅描边 */ Stroke, /** 先填充,然后描边 */ FillAndStroke, /** 先描边,然后填充 */ StrokeAndFill } export interface EGraphicItemEvent extends ERenderItemEvent {} export abstract class GraphicItemBase extends RenderItem implements Required { mode: number = GraphicMode.Fill; fill: CanvasStyle = '#fff'; stroke: CanvasStyle = '#fff'; lineWidth: number = 2; lineDash: number[] = []; lineDashOffset: number = 0; lineJoin: CanvasLineJoin = 'bevel'; lineCap: CanvasLineCap = 'butt'; miterLimit: number = 10; fillRule: CanvasFillRule = 'nonzero'; /** * 设置描边绘制的信息 * @param options 线的信息 */ setLineOption(options: Partial) { if (!isNil(options.lineWidth)) this.lineWidth = options.lineWidth; if (!isNil(options.lineDash)) this.lineDash = options.lineDash; if (!isNil(options.lineDashOffset)) this.lineDashOffset = options.lineDashOffset; if (!isNil(options.lineJoin)) this.lineJoin = options.lineJoin; if (!isNil(options.lineCap)) this.lineCap = options.lineCap; if (!isNil(options.miterLimit)) this.miterLimit = options.miterLimit; } /** * 设置填充样式 * @param style 绘制样式 */ setFillStyle(style: CanvasStyle) { this.fill = style; this.update(); } /** * 设置描边样式 * @param style 绘制样式 */ setStrokeStyle(style: CanvasStyle) { this.stroke = style; this.update(); } /** * 设置填充原则 * @param rule 填充原则 */ setFillRule(rule: CanvasFillRule) { this.fillRule = rule; this.update(); } /** * 设置绘制模式,是描边还是填充 * @param mode 绘制模式 */ setMode(mode: GraphicMode) { this.mode = mode; this.update(); } /** * 设置画布的渲染状态,在实际渲染前调用 * @param canvas 要设置的画布 */ protected setCanvasState(canvas: MotaOffscreenCanvas2D) { const ctx = canvas.ctx; ctx.fillStyle = this.fill; ctx.strokeStyle = this.stroke; ctx.lineWidth = this.lineWidth; ctx.setLineDash(this.lineDash) ctx.lineDashOffset = this.lineDashOffset; ctx.lineJoin = this.lineJoin; ctx.lineCap = this.lineCap; ctx.miterLimit = this.miterLimit; ctx.fill(this.fillRule); this.update(); } patchProp( key: string, prevValue: any, nextValue: any, namespace?: ElementNamespace, parentComponent?: ComponentInternalInstance | null ): void { switch (key) { } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } } export class Rect extends GraphicItemBase { protected render( canvas: MotaOffscreenCanvas2D, transform: Transform ): void { const ctx = canvas.ctx; this.setCanvasState(canvas); ctx.beginPath(); ctx.rect(this.x, this.y, this.width, this.height); switch (this.mode) { case GraphicMode.Fill: ctx.fill(this.fillRule); break; case GraphicMode.Stroke: ctx.stroke(); break; case GraphicMode.FillAndStroke: ctx.fill(this.fillRule); ctx.stroke(); break; case GraphicMode.StrokeAndFill: ctx.stroke(); ctx.fill(this.fillRule); break; } } } export class Circle extends GraphicItemBase { radius: number = 10; start: number = 0; end: number = Math.PI * 2; protected render( canvas: MotaOffscreenCanvas2D, transform: Transform ): void { const ctx = canvas.ctx; this.setCanvasState(canvas); ctx.beginPath(); ctx.arc(this.x,this.y,this.radius,this.start,this.end); switch (this.mode) { case GraphicMode.Fill: ctx.fill(this.fillRule); break; case GraphicMode.Stroke: ctx.stroke(); break; case GraphicMode.FillAndStroke: ctx.fill(this.fillRule); ctx.stroke(); break; case GraphicMode.StrokeAndFill: ctx.stroke(); ctx.fill(this.fillRule); break; } } /** * 设置圆的半径 * @param radius 半径 */ setRadius(radius: number) { this.radius = radius; this.size(radius*2,radius*2); this.update(); } /** * 设置圆的起始与终止角度 * @param start 起始角度 * @param end 终止角度 */ setAngle(start: number, end: number) { this.start = start; this.end = end; this.update(); } patchProp( key: string, prevValue: any, nextValue: any, namespace?: ElementNamespace, parentComponent?: ComponentInternalInstance | null ): void { switch (key) { case 'radius': if (!this.assertType(nextValue, 'number', key)) return; this.setRadius(nextValue); return; case 'start': if (!this.assertType(nextValue, 'number', key)) return; this.setAngle(nextValue,this.end); return; case 'end': if (!this.assertType(nextValue, 'number', key)) return; this.setAngle(this.start,nextValue); return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } } export class Ellipse extends GraphicItemBase { radiusX: number = 10; radiusY: number = 10; start: number = 0; end: number = Math.PI * 2; protected render( canvas: MotaOffscreenCanvas2D, transform: Transform ): void { const ctx = canvas.ctx; this.setCanvasState(canvas); ctx.beginPath(); ctx.ellipse(this.x,this.y,this.radiusX,this.radiusY,0,this.start,this.end); switch (this.mode) { case GraphicMode.Fill: ctx.fill(this.fillRule); break; case GraphicMode.Stroke: ctx.stroke(); break; case GraphicMode.FillAndStroke: ctx.fill(this.fillRule); ctx.stroke(); break; case GraphicMode.StrokeAndFill: ctx.stroke(); ctx.fill(this.fillRule); break; } } /** * 设置椭圆的横纵轴长度 * @param x 横轴长度 * @param y 纵轴长度 */ setRadius(x: number, y: number) { this.radiusX = x; this.radiusY = y; this.update(); } /** * 设置椭圆的起始与终止角度 * @param start 起始角度 * @param end 终止角度 */ setAngle(start: number, end: number) { this.start = start; this.end = end; this.update(); } patchProp( key: string, prevValue: any, nextValue: any, namespace?: ElementNamespace, parentComponent?: ComponentInternalInstance | null ): void { switch (key) { case 'radiusX': if (!this.assertType(nextValue, 'number', key)) return; this.setRadius(nextValue,this.radiusY); return; case 'radiusY': if (!this.assertType(nextValue, 'number', key)) return; this.setRadius(this.radiusY,nextValue); return; case 'start': if (!this.assertType(nextValue, 'number', key)) return; this.setAngle(nextValue,this.end); return; case 'end': if (!this.assertType(nextValue, 'number', key)) return; this.setAngle(this.start,nextValue); return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } } export class Line extends GraphicItemBase { x1: number = 0; y1: number = 0; x2: number = 0; y2: number = 0; protected render( canvas: MotaOffscreenCanvas2D, transform: Transform ): void { const ctx = canvas.ctx; this.setCanvasState(canvas); ctx.beginPath(); ctx.moveTo(this.x1,this.y1) ctx.lineTo(this.x2,this.y2); ctx.stroke(); } /** * 设置第一个点的横纵坐标 */ setPoint1(x: number, y: number) { this.x1 = x; this.y1 = y; this.update(); } /** * 设置第二个点的横纵坐标 */ setPoint2(x: number, y: number) { this.x2 = x; this.y2 = y; this.update(); } patchProp( key: string, prevValue: any, nextValue: any, namespace?: ElementNamespace, parentComponent?: ComponentInternalInstance | null ): void { switch (key) { case 'x1': if (!this.assertType(nextValue, 'number', key)) return; this.setPoint1(nextValue,this.y1); return; case 'y1': if (!this.assertType(nextValue, 'number', key)) return; this.setPoint1(this.x1,nextValue); return; case 'x2': if (!this.assertType(nextValue, 'number', key)) return; this.setPoint2(nextValue,this.y2); return; case 'y2': if (!this.assertType(nextValue, 'number', key)) return; this.setPoint2(this.x2,nextValue); return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } } export class BezierCurve extends GraphicItemBase { sx: number = 0; sy: number = 0; cp1x: number = 0; cp1y: number = 0; cp2x: number = 0; cp2y: number = 0; ex: number = 0; ey: number = 0; protected render( canvas: MotaOffscreenCanvas2D, transform: Transform ): void { const ctx = canvas.ctx; this.setCanvasState(canvas); ctx.beginPath(); ctx.moveTo(this.sx,this.sy) ctx.bezierCurveTo(this.cp1x,this.cp1y,this.cp2x,this.cp2y,this.ex,this.ey); ctx.stroke(); } /** * 设置起始点坐标 */ setStart(x: number, y: number) { this.sx = x; this.sy = y; this.update(); } /** * 设置控制点1坐标 */ setControl1(x: number, y: number) { this.cp1x = x; this.cp1y = y; this.update(); } /** * 设置控制点2坐标 */ setControl2(x: number, y: number) { this.cp2x = x; this.cp2y = y; this.update(); } /** * 设置终点坐标 */ setEnd(x: number, y: number) { this.ex = x; this.ey = y; this.update(); } patchProp( key: string, prevValue: any, nextValue: any, namespace?: ElementNamespace, parentComponent?: ComponentInternalInstance | null ): void { switch (key) { case 'sx': if (!this.assertType(nextValue, 'number', key)) return; this.setStart(nextValue,this.sy); return; case 'sy': if (!this.assertType(nextValue, 'number', key)) return; this.setStart(this.sx,nextValue); return; case 'cp1x': if (!this.assertType(nextValue, 'number', key)) return; this.setControl1(nextValue,this.cp1y); return; case 'cp1y': if (!this.assertType(nextValue, 'number', key)) return; this.setControl1(this.cp1x,nextValue); return; case 'cp2x': if (!this.assertType(nextValue, 'number', key)) return; this.setControl2(nextValue,this.cp2y); return; case 'cp2y': if (!this.assertType(nextValue, 'number', key)) return; this.setControl2(this.cp2x,nextValue); return; case 'ex': if (!this.assertType(nextValue, 'number', key)) return; this.setEnd(nextValue,this.ey); return; case 'ey': if (!this.assertType(nextValue, 'number', key)) return; this.setEnd(this.ex,nextValue); return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } } export class QuadraticCurve extends GraphicItemBase { sx: number = 0; sy: number = 0; cpx: number = 0; cpy: number = 0; ex: number = 0; ey: number = 0; protected render( canvas: MotaOffscreenCanvas2D, transform: Transform ): void { const ctx = canvas.ctx; this.setCanvasState(canvas); ctx.beginPath(); ctx.moveTo(this.sx,this.sy) ctx.quadraticCurveTo(this.cpx,this.cpy,this.ex,this.ey); ctx.stroke(); } /** * 设置起始点坐标 */ setStart(x: number, y: number) { this.sx = x; this.sy = y; this.update(); } /** * 设置控制点坐标 */ setControl(x: number, y: number) { this.cpx = x; this.cpy = y; this.update(); } /** * 设置终点坐标 */ setEnd(x: number, y: number) { this.ex = x; this.ey = y; this.update(); } patchProp( key: string, prevValue: any, nextValue: any, namespace?: ElementNamespace, parentComponent?: ComponentInternalInstance | null ): void { switch (key) { case 'sx': if (!this.assertType(nextValue, 'number', key)) return; this.setStart(nextValue,this.sy); return; case 'sy': if (!this.assertType(nextValue, 'number', key)) return; this.setStart(this.sx,nextValue); return; case 'cpx': if (!this.assertType(nextValue, 'number', key)) return; this.setControl(nextValue,this.cpy); return; case 'cpy': if (!this.assertType(nextValue, 'number', key)) return; this.setControl(this.cpx,nextValue); return; case 'ex': if (!this.assertType(nextValue, 'number', key)) return; this.setEnd(nextValue,this.ey); return; case 'ey': if (!this.assertType(nextValue, 'number', key)) return; this.setEnd(this.ex,nextValue); return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } } export class Path extends GraphicItemBase { /** 路径 */ path: Path2D = new Path2D(); protected render( canvas: MotaOffscreenCanvas2D, transform: Transform ): void { const ctx = canvas.ctx; this.setCanvasState(canvas); switch (this.mode) { case GraphicMode.Fill: ctx.fill(this.path,this.fillRule); break; case GraphicMode.Stroke: ctx.stroke(this.path); break; case GraphicMode.FillAndStroke: ctx.fill(this.path,this.fillRule); ctx.stroke(this.path); break; case GraphicMode.StrokeAndFill: ctx.stroke(this.path); ctx.fill(this.path,this.fillRule); break; } } /** * 获取当前路径 */ getPath() { return this.path; } /** * 为路径添加路径 * @param path 要添加的路径 */ addPath(path: Path2D) { this.path.addPath(path); this.update(); } patchProp( key: string, prevValue: any, nextValue: any, namespace?: ElementNamespace, parentComponent?: ComponentInternalInstance | null ): void { switch (key) { case 'path': if (!this.assertType(nextValue, Path2D, key)) return; this.path = nextValue; this.update(); return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } }