import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { ERenderItemEvent, RenderItem, RenderItemPosition } 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
}
const enum GraphicModeProp {
Fill,
Stroke,
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';
private propFill: boolean = true;
private propStroke: boolean = false;
private strokeAndFill: boolean = false;
private propFillSet: boolean = false;
/**
* 设置描边绘制的信息
* @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;
this.update();
}
/**
* 设置填充样式
* @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();
}
/**
* 检查渲染模式,参考 {@link GraphicPropsBase} 中的 fill stroke strokeAndFill 属性
*/
private checkMode(mode: GraphicModeProp, value: boolean) {
switch (mode) {
case GraphicModeProp.Fill:
this.propFill = value;
this.propFillSet = true;
break;
case GraphicModeProp.Stroke:
this.propStroke = value;
break;
case GraphicModeProp.StrokeAndFill:
this.strokeAndFill = value;
break;
}
if (this.strokeAndFill) {
this.mode = GraphicMode.StrokeAndFill;
} else {
if (!this.propFillSet) {
if (this.propStroke) {
this.mode = GraphicMode.Stroke;
} else {
this.mode = GraphicMode.Fill;
}
} else {
if (this.propFill && this.propStroke) {
this.mode = GraphicMode.FillAndStroke;
} else if (this.propFill) {
this.mode = GraphicMode.Fill;
} else if (this.propStroke) {
this.mode = GraphicMode.Stroke;
} else {
this.mode = GraphicMode.Fill;
}
}
}
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 {
if (isNil(prevValue) && isNil(nextValue)) return;
switch (key) {
case 'fill':
if (!this.assertType(nextValue, 'number', key)) return;
this.checkMode(GraphicModeProp.Fill, nextValue);
break;
case 'stroke':
if (!this.assertType(nextValue, 'number', key)) return;
this.checkMode(GraphicModeProp.Stroke, nextValue);
break;
case 'strokeAndFill':
if (!this.assertType(nextValue, 'number', key)) return;
this.checkMode(GraphicModeProp.StrokeAndFill, nextValue);
break;
case 'fillRule':
if (!this.assertType(nextValue, 'string', key)) return;
this.setFillRule(nextValue);
break;
case 'fillStyle':
this.setFillStyle(nextValue);
break;
case 'strokeStyle':
this.setStrokeStyle(nextValue);
break;
case 'lineWidth':
if (!this.assertType(nextValue, 'number', key)) return;
this.lineWidth = nextValue;
this.update();
break;
case 'lineDash':
if (!this.assertType(nextValue, Array, key)) return;
this.lineDash = nextValue;
this.update();
break;
case 'lineDashOffset':
if (!this.assertType(nextValue, 'number', key)) return;
this.lineDashOffset = nextValue;
this.update();
break;
case 'lineJoin':
if (!this.assertType(nextValue, 'string', key)) return;
this.lineJoin = nextValue;
this.update();
break;
case 'lineCap':
if (!this.assertType(nextValue, 'string', key)) return;
this.lineCap = nextValue;
this.update();
break;
case 'miterLimit':
if (!this.assertType(nextValue, 'number', key)) return;
this.miterLimit = nextValue;
this.update();
break;
}
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);
}
}
const enum RectRType {
/** 圆角为椭圆 */
Ellipse,
/** 圆角为二次贝塞尔曲线 */
Quad,
/** 圆角为三次贝塞尔曲线。该模式下,包含两个控制点,一个控制点位于上下矩形边延长线,另一个控制点位于左右矩形边延长线 */
Cubic,
/** 圆角为直线连接 */
Line
}
export class RectR extends GraphicItemBase {
/** 矩形路径 */
private path: Path2D;
/** 圆角类型 */
roundType: RectRType = RectRType.Ellipse;
/** 横向圆角半径 */
radiusX: number = 0;
/** 纵向圆角半径 */
radiusY: number = 0;
/**
* 二次贝塞尔曲线下,表示控制点的横向比例,控制点在上下矩形边与圆角的交界处为0,在左右矩形边与圆角的交界处延长线为1
* 三次贝塞尔曲线下,表示在上下矩形边延长线上的控制点的比例
*/
cpx: number = 0;
/**
* 二次贝塞尔曲线下,表示控制点的纵向比例,控制点在左右矩形边与圆角的交界处为0,在上下矩形边与圆角的交界处延长线为1
* 三次贝塞尔曲线下,表示在左右矩形边延长线上的控制点的比例
*/
cpy: number = 0;
constructor(
type: RenderItemPosition,
cache: boolean = false,
fall: boolean = false
) {
super(type, cache, fall);
const path = new Path2D();
path.rect(this.x, this.y, this.width, this.height);
this.path = path;
}
/**
* 更新路径
*/
private updatePath() {}
/**
* 设置圆角半径
* @param x 横向半径
* @param y 纵向半径
*/
setRadius(x: number, y: number) {}
/**
* 设置贝塞尔曲线模式下的控制点
* @param x cpx
* @param y cpy
*/
setControl(x: number, y: number) {}
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;
}
}
patchProp(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) {
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
}
}