mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-19 17:16:08 +08:00
657 lines
18 KiB
TypeScript
657 lines
18 KiB
TypeScript
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):
|
||
* <rect x={10} y={30} width={50} height={30} fill stroke /> <!-- 表现为先填充,后描边 -->
|
||
* <circle x={10} y={50} radius={10} start={Math.PI / 2} end={Math.PI} stroke /> <!-- 表现为仅描边 -->
|
||
* <ellipse x={100} y={50} radiusX={10} radiusY={50} strokeAndFill /> <!-- 表现为先描边后填充 -->
|
||
* <rect x={100} y={50} width={50} height={30} fill /> <!-- 表现为仅填充 -->
|
||
* <rect x={100} y={50} width={50} height={30} /> <!-- 表现为仅填充 -->
|
||
* 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<EGraphicItemEvent>
|
||
implements Required<ILineProperty>
|
||
{
|
||
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<ILineProperty>) {
|
||
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);
|
||
}
|
||
}
|