HumanBreak/packages/render-elements/src/graphics.ts

1093 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Transform,
ERenderItemEvent,
RenderItem,
MotaOffscreenCanvas2D,
CanvasStyle
} from '@motajs/render-core';
import { logger } from '@motajs/common';
import { clamp, isEqual, isNil } from 'lodash-es';
export type CircleParams = [
cx?: number,
cy?: number,
radius?: number,
start?: number,
end?: number
];
export type EllipseParams = [
cx?: number,
cy?: number,
radiusX?: number,
radiusY?: number,
start?: number,
end?: number
];
export type LineParams = [x1: number, y1: number, x2: number, y2: number];
export type BezierParams = [
sx: number,
sy: number,
cp1x: number,
cp1y: number,
cp2x: number,
cp2y: number,
ex: number,
ey: number
];
export type QuadParams = [
sx: number,
sy: number,
cpx: number,
cpy: number,
ex: number,
ey: number
];
export type RectRCircleParams = [
r1: number,
r2?: number,
r3?: number,
r4?: number
];
export type RectREllipseParams = [
rx1: number,
ry1: number,
rx2?: number,
ry2?: number,
rx3?: number,
ry3?: number,
rx4?: number,
ry4?: number
];
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<EGraphicItemEvent>
implements Required<ILineProperty>
{
mode: GraphicMode = GraphicMode.Fill;
fill: CanvasStyle = '#ddd';
stroke: CanvasStyle = '#ddd';
lineWidth: number = 2;
lineDash: number[] = [];
lineDashOffset: number = 0;
lineJoin: CanvasLineJoin = 'bevel';
lineCap: CanvasLineCap = 'butt';
miterLimit: number = 10;
fillRule: CanvasFillRule = 'nonzero';
enableCache: boolean = false;
private propFill: boolean = true;
private propStroke: boolean = false;
private strokeAndFill: boolean = false;
private propFillSet: boolean = false;
private actionStroke: boolean = false;
private cachePath?: Path2D;
protected pathDirty: boolean = true;
/**
* 获取这个元素的绘制路径
*/
abstract getPath(): Path2D;
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
const ctx = canvas.ctx;
this.setCanvasState(canvas);
if (this.pathDirty) {
this.cachePath = this.getPath();
this.pathDirty = false;
}
const path = this.cachePath;
if (!path) return;
switch (this.mode) {
case GraphicMode.Fill:
ctx.fill(path, this.fillRule);
break;
case GraphicMode.Stroke:
ctx.stroke(path);
break;
case GraphicMode.FillAndStroke:
ctx.fill(path, this.fillRule);
ctx.stroke(path);
break;
case GraphicMode.StrokeAndFill:
ctx.stroke(path);
ctx.fill(path, this.fillRule);
break;
}
}
protected isActionInElement(x: number, y: number): boolean {
const ctx = this.cache.ctx;
if (this.pathDirty) {
this.cachePath = this.getPath();
this.pathDirty = false;
}
const path = this.cachePath;
if (!path) return false;
const fixX = x * devicePixelRatio;
const fixY = y * devicePixelRatio;
ctx.lineWidth = this.lineWidth;
ctx.lineCap = this.lineCap;
ctx.lineJoin = this.lineJoin;
ctx.setLineDash(this.lineDash);
if (this.actionStroke) {
return ctx.isPointInStroke(path, fixX, fixY);
}
switch (this.mode) {
case GraphicMode.Fill:
return ctx.isPointInPath(path, fixX, fixY, this.fillRule);
case GraphicMode.Stroke:
case GraphicMode.FillAndStroke:
case GraphicMode.StrokeAndFill:
return (
ctx.isPointInPath(path, fixX, fixY, this.fillRule) ||
ctx.isPointInStroke(path, fixX, fixY)
);
}
}
/**
* 设置描边绘制的信息
* @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;
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;
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'fill':
if (!this.assertType(nextValue, 'boolean', key)) return false;
this.checkMode(GraphicModeProp.Fill, nextValue);
return true;
case 'stroke':
if (!this.assertType(nextValue, 'boolean', key)) return false;
this.checkMode(GraphicModeProp.Stroke, nextValue);
return true;
case 'strokeAndFill':
if (!this.assertType(nextValue, 'boolean', key)) return false;
this.checkMode(GraphicModeProp.StrokeAndFill, nextValue);
return true;
case 'fillRule':
if (!this.assertType(nextValue, 'string', key)) return false;
this.setFillRule(nextValue);
return true;
case 'fillStyle':
this.setFillStyle(nextValue);
return true;
case 'strokeStyle':
this.setStrokeStyle(nextValue);
return true;
case 'lineWidth':
if (!this.assertType(nextValue, 'number', key)) return false;
this.lineWidth = nextValue;
this.update();
return true;
case 'lineDash':
if (!this.assertType(nextValue, Array, key)) return false;
this.lineDash = nextValue as number[];
this.update();
return true;
case 'lineDashOffset':
if (!this.assertType(nextValue, 'number', key)) return false;
this.lineDashOffset = nextValue;
this.update();
return true;
case 'lineJoin':
if (!this.assertType(nextValue, 'string', key)) return false;
this.lineJoin = nextValue;
this.update();
return true;
case 'lineCap':
if (!this.assertType(nextValue, 'string', key)) return false;
this.lineCap = nextValue;
this.update();
return true;
case 'miterLimit':
if (!this.assertType(nextValue, 'number', key)) return false;
this.miterLimit = nextValue;
this.update();
return true;
case 'actionStroke':
if (!this.assertType(nextValue, 'boolean', key)) return false;
this.actionStroke = nextValue;
return true;
}
return false;
}
}
export class Rect extends GraphicItemBase {
pos(x: number, y: number): void {
super.pos(x, y);
this.pathDirty = true;
}
size(width: number, height: number): void {
super.size(width, height);
this.pathDirty = true;
}
getPath(): Path2D {
const path = new Path2D();
path.rect(0, 0, this.width, this.height);
return path;
}
}
export class Circle extends GraphicItemBase {
radius: number = 10;
start: number = 0;
end: number = Math.PI * 2;
anchorX: number = 0.5;
anchorY: number = 0.5;
getPath(): Path2D {
const path = new Path2D();
path.arc(this.radius, this.radius, this.radius, this.start, this.end);
return path;
}
/**
* 设置圆的半径
* @param radius 半径
*/
setRadius(radius: number) {
this.radius = radius;
this.size(radius * 2, radius * 2);
this.pathDirty = true;
this.update();
}
/**
* 设置圆的起始与终止角度
* @param start 起始角度
* @param end 终止角度
*/
setAngle(start: number, end: number) {
this.start = start;
this.end = end;
this.pathDirty = true;
this.update();
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'radius':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(nextValue);
return true;
case 'start':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(nextValue, this.end);
return true;
case 'end':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(this.start, nextValue);
return true;
case 'circle': {
if (isEqual(nextValue, prevValue)) return true;
const value = nextValue as CircleParams;
if (!this.assertType(value, Array, key)) return false;
const [cx, cy, radius, start, end] = value;
if (!isNil(cx) && !isNil(cy)) {
this.pos(cx, cy);
}
if (!isNil(radius)) {
this.setRadius(radius);
}
if (!isNil(start) && !isNil(end)) {
this.setAngle(start, end);
}
return true;
}
}
return super.handleProps(key, prevValue, nextValue);
}
}
export class Ellipse extends GraphicItemBase {
radiusX: number = 10;
radiusY: number = 10;
start: number = 0;
end: number = Math.PI * 2;
anchorX: number = 0.5;
anchorY: number = 0.5;
getPath(): Path2D {
const path = new Path2D();
path.ellipse(
this.radiusX,
this.radiusY,
this.radiusX,
this.radiusY,
0,
this.start,
this.end
);
return path;
}
/**
* 设置椭圆的横纵轴长度
* @param x 横轴长度
* @param y 纵轴长度
*/
setRadius(x: number, y: number) {
this.radiusX = x;
this.radiusY = y;
this.size(x, y);
this.pathDirty = true;
this.update();
}
/**
* 设置椭圆的起始与终止角度
* @param start 起始角度
* @param end 终止角度
*/
setAngle(start: number, end: number) {
this.start = start;
this.end = end;
this.pathDirty = true;
this.update();
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'radiusX':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(nextValue, this.radiusY);
return true;
case 'radiusY':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(this.radiusY, nextValue);
return true;
case 'start':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(nextValue, this.end);
return true;
case 'end':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(this.start, nextValue);
return true;
case 'ellipse': {
if (isEqual(nextValue, prevValue)) return true;
const value = nextValue as EllipseParams;
if (!this.assertType(value, Array, key)) return false;
const [cx, cy, radiusX, radiusY, start, end] = value;
if (!isNil(cx) && !isNil(cy)) {
this.pos(cx, cy);
}
if (!isNil(radiusX) && !isNil(radiusY)) {
this.setRadius(radiusX, radiusY);
}
if (!isNil(start) && !isNil(end)) {
this.setAngle(start, end);
}
return true;
}
}
return super.handleProps(key, prevValue, nextValue);
}
}
export class Line extends GraphicItemBase {
x1: number = 0;
y1: number = 0;
x2: number = 0;
y2: number = 0;
mode: GraphicMode = GraphicMode.Stroke;
getPath(): Path2D {
const path = new Path2D();
const x = this.x;
const y = this.y;
path.moveTo(this.x1 - x, this.y1 - y);
path.lineTo(this.x2 - x, this.y2 - y);
return path;
}
/**
* 设置第一个点的横纵坐标
*/
setPoint1(x: number, y: number) {
this.x1 = x;
this.y1 = y;
this.fitRect();
this.update();
}
/**
* 设置第二个点的横纵坐标
*/
setPoint2(x: number, y: number) {
this.x2 = x;
this.y2 = y;
this.fitRect();
this.update();
}
private fitRect() {
const left = Math.min(this.x1, this.x2);
const top = Math.min(this.y1, this.y2);
const right = Math.max(this.x1, this.x2);
const bottom = Math.max(this.y1, this.y2);
this.pos(left, top);
this.size(right - left, bottom - top);
this.pathDirty = true;
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'x1':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint1(nextValue, this.y1);
return true;
case 'y1':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint1(this.x1, nextValue);
return true;
case 'x2':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint2(nextValue, this.y2);
return true;
case 'y2':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint2(this.x2, nextValue);
return true;
case 'line':
if (isEqual(nextValue, prevValue)) return true;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}
this.setPoint1(nextValue[0], nextValue[1]);
this.setPoint2(nextValue[2], nextValue[3]);
return true;
}
return super.handleProps(key, prevValue, nextValue);
}
}
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;
mode: GraphicMode = GraphicMode.Stroke;
getPath(): Path2D {
const path = new Path2D();
const x = this.x;
const y = this.y;
path.moveTo(this.sx - x, this.sy - y);
path.bezierCurveTo(
this.cp1x - x,
this.cp1y - y,
this.cp2x - x,
this.cp2y - y,
this.ex - x,
this.ey - y
);
return path;
}
/**
* 设置起始点坐标
*/
setStart(x: number, y: number) {
this.sx = x;
this.sy = y;
this.fitRect();
this.update();
}
/**
* 设置控制点1坐标
*/
setControl1(x: number, y: number) {
this.cp1x = x;
this.cp1y = y;
this.fitRect();
this.update();
}
/**
* 设置控制点2坐标
*/
setControl2(x: number, y: number) {
this.cp2x = x;
this.cp2y = y;
this.fitRect();
this.update();
}
/**
* 设置终点坐标
*/
setEnd(x: number, y: number) {
this.ex = x;
this.ey = y;
this.fitRect();
this.update();
}
protected isActionInElement(x: number, y: number): boolean {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
private fitRect() {
const left = Math.min(this.sx, this.cp1x, this.cp2x, this.ex);
const top = Math.min(this.sy, this.cp1y, this.cp2y, this.ey);
const right = Math.max(this.sx, this.cp1x, this.cp2x, this.ex);
const bottom = Math.max(this.sy, this.cp1y, this.cp2y, this.ey);
this.pos(left, top);
this.size(right - left, bottom - top);
this.pathDirty = true;
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'sx':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(nextValue, this.sy);
return true;
case 'sy':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(this.sx, nextValue);
return true;
case 'cp1x':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl1(nextValue, this.cp1y);
return true;
case 'cp1y':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl1(this.cp1x, nextValue);
return true;
case 'cp2x':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl2(nextValue, this.cp2y);
return true;
case 'cp2y':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl2(this.cp2x, nextValue);
return true;
case 'ex':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(nextValue, this.ey);
return true;
case 'ey':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(this.ex, nextValue);
return true;
case 'curve':
if (isEqual(nextValue, prevValue)) return true;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}
this.setStart(nextValue[0], nextValue[1]);
this.setControl1(nextValue[2], nextValue[3]);
this.setControl2(nextValue[4], nextValue[5]);
this.setEnd(nextValue[6], nextValue[7]);
return true;
}
return super.handleProps(key, prevValue, nextValue);
}
}
export class QuadraticCurve extends GraphicItemBase {
sx: number = 0;
sy: number = 0;
cpx: number = 0;
cpy: number = 0;
ex: number = 0;
ey: number = 0;
mode: GraphicMode = GraphicMode.Stroke;
getPath(): Path2D {
const path = new Path2D();
const x = this.x;
const y = this.y;
path.moveTo(this.sx - x, this.sy - y);
path.quadraticCurveTo(
this.cpx - x,
this.cpy - y,
this.ex - x,
this.ey - y
);
return path;
}
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.fitRect();
this.update();
}
/**
* 设置控制点坐标
*/
setControl(x: number, y: number) {
this.cpx = x;
this.cpy = y;
this.fitRect();
this.update();
}
/**
* 设置终点坐标
*/
setEnd(x: number, y: number) {
this.ex = x;
this.ey = y;
this.fitRect();
this.update();
}
private fitRect() {
const left = Math.min(this.sx, this.cpx, this.ex);
const top = Math.min(this.sy, this.cpy, this.ey);
const right = Math.max(this.sx, this.cpx, this.ex);
const bottom = Math.max(this.sy, this.cpy, this.ey);
this.pos(left, top);
this.size(right - left, bottom - top);
this.pathDirty = true;
}
protected isActionInElement(x: number, y: number): boolean {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'sx':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(nextValue, this.sy);
return true;
case 'sy':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(this.sx, nextValue);
return true;
case 'cpx':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl(nextValue, this.cpy);
return true;
case 'cpy':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl(this.cpx, nextValue);
return true;
case 'ex':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(nextValue, this.ey);
return true;
case 'ey':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(this.ex, nextValue);
return true;
case 'curve':
if (isEqual(nextValue, prevValue)) return true;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}
this.setStart(nextValue[0], nextValue[1]);
this.setControl(nextValue[2], nextValue[3]);
this.setEnd(nextValue[4], nextValue[5]);
return true;
}
return super.handleProps(key, prevValue, nextValue);
}
}
export class Path extends GraphicItemBase {
/** 路径 */
path: Path2D = new Path2D();
/**
* 获取当前路径
*/
getPath() {
return this.path;
}
/**
* 为路径添加路径
* @param path 要添加的路径
*/
addPath(path: Path2D) {
this.path.addPath(path);
this.pathDirty = true;
this.update();
}
protected isActionInElement(x: number, y: number): boolean {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'path':
if (!this.assertType(nextValue, Path2D, key)) return false;
this.path = nextValue;
this.pathDirty = true;
this.update();
return true;
}
return super.handleProps(key, prevValue, nextValue);
}
}
export const enum RectRCorner {
TopLeft,
TopRight,
BottomRight,
BottomLeft
}
export class RectR extends GraphicItemBase {
/** 圆角属性,四元素数组,每个元素是一个二元素数组,表示这个角的半径,顺序为 左上,右上,右下,左下 */
readonly corner: [radiusX: number, radiusY: number][] = [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
];
getPath(): Path2D {
const path = new Path2D();
const { width: w, height: h } = this;
const [[xtl, ytl], [xtr, ytr], [xbr, ybr], [xbl, ybl]] = this.corner;
// 左上圆角终点
path.moveTo(xtl, 0);
// 右上圆角起点
path.lineTo(w - xtr, 0);
// 右上圆角终点
path.ellipse(w - xtr, ytr, xtr, ytr, 0, -Math.PI / 2, 0);
// 右下圆角起点
path.lineTo(w, h - ybr);
// 右下圆角终点
path.ellipse(w - xbr, h - ybr, xbr, ybr, 0, 0, Math.PI / 2);
// 左下圆角起点
path.lineTo(xbl, h);
// 左下圆角终点
path.ellipse(xbl, h - ybl, xbl, ybl, 0, Math.PI / 2, Math.PI);
// 左上圆角起点
path.lineTo(0, ytl);
// 左上圆角终点
path.ellipse(xtl, ytl, xtl, ytl, 0, Math.PI, -Math.PI / 2);
path.closePath();
return path;
}
/**
* 设置圆角半径
* @param x 横向半径
* @param y 纵向半径
*/
setRadius(x: number, y: number, corner: RectRCorner) {
const hw = this.width / 2;
const hh = this.height / 2;
this.corner[corner] = [clamp(x, 0, hw), clamp(y, 0, hh)];
this.pathDirty = true;
this.update();
}
/**
* 设置圆形圆角参数
* @param circle 圆形圆角参数
*/
setCircle(circle: RectRCircleParams) {
const [r1, r2 = 0, r3 = 0, r4 = 0] = circle;
switch (circle.length) {
case 1: {
this.setRadius(r1, r1, RectRCorner.BottomLeft);
this.setRadius(r1, r1, RectRCorner.BottomRight);
this.setRadius(r1, r1, RectRCorner.TopLeft);
this.setRadius(r1, r1, RectRCorner.TopRight);
break;
}
case 2: {
this.setRadius(r1, r1, RectRCorner.TopLeft);
this.setRadius(r1, r1, RectRCorner.BottomRight);
this.setRadius(r2, r2, RectRCorner.BottomLeft);
this.setRadius(r2, r2, RectRCorner.TopRight);
break;
}
case 3: {
this.setRadius(r1, r1, RectRCorner.TopLeft);
this.setRadius(r2, r2, RectRCorner.TopRight);
this.setRadius(r2, r2, RectRCorner.BottomLeft);
this.setRadius(r3, r3, RectRCorner.BottomRight);
break;
}
case 4: {
this.setRadius(r1, r1, RectRCorner.TopLeft);
this.setRadius(r2, r2, RectRCorner.TopRight);
this.setRadius(r3, r3, RectRCorner.BottomRight);
this.setRadius(r4, r4, RectRCorner.BottomLeft);
break;
}
}
}
/**
* 设置椭圆圆角参数
* @param ellipse 椭圆圆角参数
*/
setEllipse(ellipse: RectREllipseParams) {
const [rx1, ry1, rx2 = 0, ry2 = 0, rx3 = 0, ry3 = 0, rx4 = 0, ry4 = 0] =
ellipse;
switch (ellipse.length) {
case 2: {
this.setRadius(rx1, ry1, RectRCorner.BottomLeft);
this.setRadius(rx1, ry1, RectRCorner.BottomRight);
this.setRadius(rx1, ry1, RectRCorner.TopLeft);
this.setRadius(rx1, ry1, RectRCorner.TopRight);
break;
}
case 4: {
this.setRadius(rx1, ry1, RectRCorner.TopLeft);
this.setRadius(rx1, ry1, RectRCorner.BottomRight);
this.setRadius(rx2, ry2, RectRCorner.BottomLeft);
this.setRadius(rx2, ry2, RectRCorner.TopRight);
break;
}
case 6: {
this.setRadius(rx1, ry1, RectRCorner.TopLeft);
this.setRadius(rx2, ry2, RectRCorner.TopRight);
this.setRadius(rx2, ry2, RectRCorner.BottomLeft);
this.setRadius(rx3, ry3, RectRCorner.BottomRight);
break;
}
case 8: {
this.setRadius(rx1, ry1, RectRCorner.TopLeft);
this.setRadius(rx2, ry2, RectRCorner.TopRight);
this.setRadius(rx3, ry3, RectRCorner.BottomRight);
this.setRadius(rx4, ry4, RectRCorner.BottomLeft);
break;
}
default: {
logger.warn(58, ellipse.length.toString());
}
}
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'circle': {
const value = nextValue as RectRCircleParams;
if (!this.assertType(value, Array, key)) return false;
this.setCircle(value);
return true;
}
case 'ellipse': {
const value = nextValue as RectREllipseParams;
if (!this.assertType(value, Array, key)) return false;
this.setEllipse(value);
return true;
}
}
return super.handleProps(key, prevValue, nextValue);
}
}