From d2bd98b735a01a56067d2ddeb0501f00676bf343 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sat, 22 Feb 2025 18:33:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20graphics=20=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=E4=B8=8E=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/render/container.ts | 4 +- src/core/render/item.ts | 36 +- src/core/render/preset/graphics.ts | 490 +++++++++++++++--------- src/core/render/preset/misc.ts | 8 +- src/core/render/renderer/map.ts | 18 +- src/core/render/renderer/props.ts | 92 +++-- src/core/render/utils.ts | 1 + src/core/system/ui/controller.ts | 5 + src/data/logger.json | 1 + src/module/render/components/scroll.tsx | 39 +- src/module/render/ui/main.tsx | 7 +- src/module/render/ui/statusBar.tsx | 7 +- 12 files changed, 475 insertions(+), 233 deletions(-) diff --git a/src/core/render/container.ts b/src/core/render/container.ts index d975cce..44a02fd 100644 --- a/src/core/render/container.ts +++ b/src/core/render/container.ts @@ -116,7 +116,9 @@ export class Container if (progress === EventProgress.Capture) { let success = false; for (let i = len - 1; i >= 0; i--) { - if (this.sortedChildren[i].captureEvent(type, event)) { + const child = this.sortedChildren[i]; + if (child.hidden) continue; + if (child.captureEvent(type, event)) { success = true; break; } diff --git a/src/core/render/item.ts b/src/core/render/item.ts index bc292bf..434f617 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -228,6 +228,9 @@ export abstract class RenderItem //#region 元素属性 + /** 是否是注释元素 */ + readonly isComment: boolean = false; + private _id: string = ''; /** * 元素的 id,原则上不可重复 @@ -561,10 +564,22 @@ export abstract class RenderItem ? this.fallTransform : this._transform; if (!tran) return new DOMRectReadOnly(0, 0, this.width, this.height); - const [x1, y1] = tran.transformed(0, 0); - const [x2, y2] = tran.transformed(this.width, 0); - const [x3, y3] = tran.transformed(0, this.height); - const [x4, y4] = tran.transformed(this.width, this.height); + const [x1, y1] = tran.transformed( + -this.anchorX * this.width, + -this.anchorY * this.height + ); + const [x2, y2] = tran.transformed( + this.width * (1 - this.anchorX), + -this.anchorY * this.height + ); + const [x3, y3] = tran.transformed( + -this.anchorX * this.width, + this.height * (1 - this.anchorY) + ); + const [x4, y4] = tran.transformed( + this.width * (1 - this.anchorX), + this.height * (1 - this.anchorY) + ); const left = Math.min(x1, x2, x3, x4); const right = Math.max(x1, x2, x3, x4); const top = Math.min(y1, y2, y3, y4); @@ -1142,6 +1157,19 @@ export abstract class RenderItem this.cursor = nextValue; return; } + case 'scale': { + if (!this.assertType(nextValue, Array, key)) return; + this._transform.setScale( + nextValue[0] as number, + nextValue[1] as number + ); + return; + } + case 'rotate': { + if (!this.assertType(nextValue, 'number', key)) return; + this._transform.setRotate(nextValue); + return; + } } const ev = this.parseEvent(key); if (ev) { diff --git a/src/core/render/preset/graphics.ts b/src/core/render/preset/graphics.ts index d37c7a7..7d6107b 100644 --- a/src/core/render/preset/graphics.ts +++ b/src/core/render/preset/graphics.ts @@ -1,8 +1,9 @@ import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; -import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item'; +import { ERenderItemEvent, RenderItem } from '../item'; import { Transform } from '../transform'; import { ElementNamespace, ComponentInternalInstance } from 'vue'; -import { isNil } from 'lodash-es'; +import { clamp, isNil } from 'lodash-es'; +import { logger } from '@/core/common/logger'; /* * Expected usage (this comment needs to be deleted after implementing correctly): @@ -63,7 +64,7 @@ export abstract class GraphicItemBase extends RenderItem implements Required { - mode: number = GraphicMode.Fill; + mode: GraphicMode = GraphicMode.Fill; fill: CanvasStyle = '#fff'; stroke: CanvasStyle = '#fff'; lineWidth: number = 2; @@ -79,6 +80,9 @@ export abstract class GraphicItemBase private strokeAndFill: boolean = false; private propFillSet: boolean = false; + private cachePath?: Path2D; + protected pathDirty: boolean = false; + /** * 获取这个元素的绘制路径 */ @@ -90,7 +94,12 @@ export abstract class GraphicItemBase ): void { const ctx = canvas.ctx; this.setCanvasState(canvas); - const path = this.getPath(); + 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); @@ -111,12 +120,16 @@ export abstract class GraphicItemBase protected isActionInElement(x: number, y: number): boolean { const ctx = this.cache.ctx; - const path = this.getPath(); + if (this.pathDirty) { + this.cachePath = this.getPath(); + this.pathDirty = false; + } + const path = this.cachePath; + if (!path) return false; switch (this.mode) { case GraphicMode.Fill: return ctx.isPointInPath(path, x, y, this.fillRule); case GraphicMode.Stroke: - return ctx.isPointInStroke(path, x, y); case GraphicMode.FillAndStroke: case GraphicMode.StrokeAndFill: return ( @@ -124,7 +137,6 @@ export abstract class GraphicItemBase ctx.isPointInStroke(path, x, y) ); } - return false; } /** @@ -272,7 +284,7 @@ export abstract class GraphicItemBase break; case 'lineDash': if (!this.assertType(nextValue, Array, key)) return; - this.lineDash = nextValue; + this.lineDash = nextValue as number[]; this.update(); break; case 'lineDashOffset': @@ -301,9 +313,19 @@ export abstract class GraphicItemBase } 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(this.x, this.y, this.width, this.height); + path.rect(0, 0, this.width, this.height); return path; } } @@ -312,32 +334,13 @@ export class Circle extends GraphicItemBase { radius: number = 10; start: number = 0; end: number = Math.PI * 2; + anchorX: number = 0.5; + anchorY: number = 0.5; - 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; - } + getPath(): Path2D { + const path = new Path2D(); + path.arc(this.radius, this.radius, this.radius, this.start, this.end); + return path; } /** @@ -347,6 +350,7 @@ export class Circle extends GraphicItemBase { setRadius(radius: number) { this.radius = radius; this.size(radius * 2, radius * 2); + this.pathDirty = true; this.update(); } @@ -358,6 +362,7 @@ export class Circle extends GraphicItemBase { setAngle(start: number, end: number) { this.start = start; this.end = end; + this.pathDirty = true; this.update(); } @@ -381,6 +386,18 @@ export class Circle extends GraphicItemBase { if (!this.assertType(nextValue, 'number', key)) return; this.setAngle(this.start, nextValue); return; + case 'circle': + if (!this.assertType(nextValue, Array, key)) return; + if (!isNil(nextValue[0])) { + this.setRadius(nextValue[0] as number); + } + if (!isNil(nextValue[1]) && !isNil(nextValue[2])) { + this.setAngle( + nextValue[1] as number, + nextValue[2] as number + ); + } + return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } @@ -391,40 +408,21 @@ export class Ellipse extends GraphicItemBase { radiusY: number = 10; start: number = 0; end: number = Math.PI * 2; + anchorX: number = 0.5; + anchorY: number = 0.5; - protected render( - canvas: MotaOffscreenCanvas2D, - _transform: Transform - ): void { - const ctx = canvas.ctx; - this.setCanvasState(canvas); - ctx.beginPath(); - ctx.ellipse( - this.x, - this.y, + getPath(): Path2D { + const path = new Path2D(); + path.ellipse( + this.radiusX, + this.radiusY, 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; - } + return path; } /** @@ -435,6 +433,8 @@ export class Ellipse extends GraphicItemBase { setRadius(x: number, y: number) { this.radiusX = x; this.radiusY = y; + this.size(x, y); + this.pathDirty = true; this.update(); } @@ -446,6 +446,7 @@ export class Ellipse extends GraphicItemBase { setAngle(start: number, end: number) { this.start = start; this.end = end; + this.pathDirty = true; this.update(); } @@ -473,6 +474,21 @@ export class Ellipse extends GraphicItemBase { if (!this.assertType(nextValue, 'number', key)) return; this.setAngle(this.start, nextValue); return; + case 'ellipse': + if (!this.assertType(nextValue, Array, key)) return; + if (!isNil(nextValue[0]) && !isNil(nextValue[1])) { + this.setRadius( + nextValue[0] as number, + nextValue[1] as number + ); + } + if (!isNil(nextValue[2]) && !isNil(nextValue[3])) { + this.setAngle( + nextValue[2] as number, + nextValue[3] as number + ); + } + return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } @@ -483,17 +499,15 @@ export class Line extends GraphicItemBase { y1: number = 0; x2: number = 0; y2: number = 0; + mode: GraphicMode = GraphicMode.Stroke; - 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(); + 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; } /** @@ -502,6 +516,7 @@ export class Line extends GraphicItemBase { setPoint1(x: number, y: number) { this.x1 = x; this.y1 = y; + this.fitRect(); this.update(); } @@ -511,9 +526,20 @@ export class Line extends GraphicItemBase { 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; + } + patchProp( key: string, prevValue: any, @@ -538,6 +564,11 @@ export class Line extends GraphicItemBase { if (!this.assertType(nextValue, 'number', key)) return; this.setPoint2(this.x2, nextValue); return; + case 'line': + if (!this.assertType(nextValue as number[], Array, key)) return; + this.setPoint1(nextValue[0], nextValue[1]); + this.setPoint2(nextValue[2], nextValue[3]); + return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } @@ -552,24 +583,22 @@ export class BezierCurve extends GraphicItemBase { cp2y: number = 0; ex: number = 0; ey: number = 0; + mode: GraphicMode = GraphicMode.Stroke; - 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 + 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 ); - ctx.stroke(); + return path; } /** @@ -578,6 +607,7 @@ export class BezierCurve extends GraphicItemBase { setStart(x: number, y: number) { this.sx = x; this.sy = y; + this.fitRect(); this.update(); } @@ -587,6 +617,7 @@ export class BezierCurve extends GraphicItemBase { setControl1(x: number, y: number) { this.cp1x = x; this.cp1y = y; + this.fitRect(); this.update(); } @@ -596,6 +627,7 @@ export class BezierCurve extends GraphicItemBase { setControl2(x: number, y: number) { this.cp2x = x; this.cp2y = y; + this.fitRect(); this.update(); } @@ -605,9 +637,20 @@ export class BezierCurve extends GraphicItemBase { setEnd(x: number, y: number) { this.ex = x; this.ey = y; + this.fitRect(); this.update(); } + 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; + } + patchProp( key: string, prevValue: any, @@ -648,6 +691,13 @@ export class BezierCurve extends GraphicItemBase { if (!this.assertType(nextValue, 'number', key)) return; this.setEnd(this.ex, nextValue); return; + case 'curve': + if (!this.assertType(nextValue as number[], Array, key)) return; + 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; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } @@ -660,6 +710,21 @@ export class QuadraticCurve extends GraphicItemBase { 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, @@ -679,6 +744,7 @@ export class QuadraticCurve extends GraphicItemBase { setStart(x: number, y: number) { this.sx = x; this.sy = y; + this.fitRect(); this.update(); } @@ -688,6 +754,7 @@ export class QuadraticCurve extends GraphicItemBase { setControl(x: number, y: number) { this.cpx = x; this.cpy = y; + this.fitRect(); this.update(); } @@ -697,9 +764,20 @@ export class QuadraticCurve extends GraphicItemBase { 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; + } + patchProp( key: string, prevValue: any, @@ -732,6 +810,12 @@ export class QuadraticCurve extends GraphicItemBase { if (!this.assertType(nextValue, 'number', key)) return; this.setEnd(this.ex, nextValue); return; + case 'curve': + if (!this.assertType(nextValue as number[], Array, key)) return; + this.setStart(nextValue[0], nextValue[1]); + this.setControl(nextValue[2], nextValue[3]); + this.setEnd(nextValue[4], nextValue[5]); + return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } @@ -741,30 +825,6 @@ 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; - } - } - /** * 获取当前路径 */ @@ -778,6 +838,7 @@ export class Path extends GraphicItemBase { */ addPath(path: Path2D) { this.path.addPath(path); + this.pathDirty = true; this.update(); } @@ -792,6 +853,7 @@ export class Path extends GraphicItemBase { case 'path': if (!this.assertType(nextValue, Path2D, key)) return; this.path = nextValue; + this.pathDirty = true; this.update(); return; } @@ -799,90 +861,156 @@ export class Path extends GraphicItemBase { } } -const enum RectRType { - /** 圆角为椭圆 */ - Ellipse, - /** 圆角为二次贝塞尔曲线 */ - Quad, - /** 圆角为三次贝塞尔曲线。该模式下,包含两个控制点,一个控制点位于上下矩形边延长线,另一个控制点位于左右矩形边延长线 */ - Cubic, - /** 圆角为直线连接 */ - Line +export const enum RectRCorner { + TopLeft, + TopRight, + BottomRight, + BottomLeft } +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 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); + /** 圆角属性,四元素数组,每个元素是一个二元素数组,表示这个角的半径,顺序为 左上,右上,右下,左下 */ + readonly corner: [radiusX: number, radiusY: number][] = [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ]; + getPath(): Path2D { const path = new Path2D(); - path.rect(this.x, this.y, this.width, this.height); - this.path = path; + 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; } - /** - * 更新路径 - */ - private updatePath() {} - /** * 设置圆角半径 * @param x 横向半径 * @param y 纵向半径 */ - setRadius(x: number, y: number) {} + 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 x cpx - * @param y cpy + * 设置圆形圆角参数 + * @param circle 圆形圆角参数 */ - setControl(x: number, y: number) {} + 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; + } + } + } - 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); + /** + * 设置椭圆圆角参数 + * @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 GraphicMode.Stroke: - ctx.stroke(this.path); + } + 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 GraphicMode.FillAndStroke: - ctx.fill(this.path, this.fillRule); - ctx.stroke(this.path); + } + 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 GraphicMode.StrokeAndFill: - ctx.stroke(this.path); - ctx.fill(this.path, this.fillRule); + } + 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()); + } } } @@ -894,6 +1022,18 @@ export class RectR extends GraphicItemBase { parentComponent?: ComponentInternalInstance | null ): void { switch (key) { + case 'circle': { + const value = nextValue as RectRCircleParams; + if (!this.assertType(value, Array, key)) return; + this.setCircle(value); + return; + } + case 'ellipse': { + const value = nextValue as RectREllipseParams; + if (!this.assertType(value, Array, key)) return; + this.setEllipse(value); + return; + } } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); } diff --git a/src/core/render/preset/misc.ts b/src/core/render/preset/misc.ts index 5294721..38714ca 100644 --- a/src/core/render/preset/misc.ts +++ b/src/core/render/preset/misc.ts @@ -197,11 +197,17 @@ export class Image extends RenderItem { } export class Comment extends RenderItem { + readonly isComment: boolean = true; + constructor(public text: string = '') { - super('static'); + super('static', false, false); this.hide(); } + getBoundingRect(): DOMRectReadOnly { + return new DOMRectReadOnly(0, 0, 0, 0); + } + protected render( _canvas: MotaOffscreenCanvas2D, _transform: Transform diff --git a/src/core/render/renderer/map.ts b/src/core/render/renderer/map.ts index 611e443..bd4a981 100644 --- a/src/core/render/renderer/map.ts +++ b/src/core/render/renderer/map.ts @@ -116,7 +116,7 @@ const enum ElementState { /** * standardElementFor */ -const se = ( +const _se = ( Item: new ( type: RenderItemPosition, cache?: boolean, @@ -235,14 +235,14 @@ tagMap.register('damage', (_0, _1, _props) => { tagMap.register('animation', (_0, _1, _props) => { return new Animate(); }); -tagMap.register('g-rect', se(Rect, 'absolute', ElementState.None)); -tagMap.register('g-circle', se(Circle, 'absolute', ElementState.None)); -tagMap.register('g-ellipse', se(Ellipse, 'absolute', ElementState.None)); -tagMap.register('g-line', se(Line, 'absolute', ElementState.None)); -tagMap.register('g-bezier', se(BezierCurve, 'absolute', ElementState.None)); -tagMap.register('g-quad', se(QuadraticCurve, 'absolute', ElementState.None)); -tagMap.register('g-path', se(Path, 'absolute', ElementState.None)); -tagMap.register('g-rectr', se(RectR, 'absolute', ElementState.None)); +tagMap.register('g-rect', standardElementNoCache(Rect)); +tagMap.register('g-circle', standardElementNoCache(Circle)); +tagMap.register('g-ellipse', standardElementNoCache(Ellipse)); +tagMap.register('g-line', standardElementNoCache(Line)); +tagMap.register('g-bezier', standardElementNoCache(BezierCurve)); +tagMap.register('g-quad', standardElementNoCache(QuadraticCurve)); +tagMap.register('g-path', standardElementNoCache(Path)); +tagMap.register('g-rectr', standardElementNoCache(RectR)); tagMap.register('icon', standardElementNoCache(Icon)); tagMap.register('winskin', (_0, _1, props) => { if (!props) return new Winskin(core.material.images.images['winskin.png']); diff --git a/src/core/render/renderer/props.ts b/src/core/render/renderer/props.ts index 9d2e204..76090b4 100644 --- a/src/core/render/renderer/props.ts +++ b/src/core/render/renderer/props.ts @@ -6,8 +6,12 @@ import { ILayerRenderExtends } from '../preset/layer'; import type { EnemyCollection } from '@/game/enemy/damage'; -import { ILineProperty } from '../preset/graphics'; -import { ElementAnchor, ElementLocator } from '../utils'; +import { + ILineProperty, + RectRCircleParams, + RectREllipseParams +} from '../preset/graphics'; +import { ElementAnchor, ElementLocator, ElementScale } from '../utils'; import { CustomContainerRenderFn } from '../container'; export interface CustomProps { @@ -45,6 +49,10 @@ export interface BaseProps { loc?: ElementLocator; /** 锚点属性,可以填 `[x锚点,y锚点]`,是 anchorX, anchorY 的简写属性 */ anc?: ElementAnchor; + /** 放缩属性,可以填 `[x比例,y比例]`,是 transform 的简写属性之一 */ + scale?: ElementScale; + /** 旋转属性,单位弧度,是 transform 的简写属性之一 */ + rotate?: number; } export interface SpriteProps extends BaseProps { @@ -124,12 +132,44 @@ export interface GraphicPropsBase extends BaseProps, Partial { strokeStyle?: CanvasStyle; } +export type CircleParams = [radius?: number, start?: number, end?: number]; +export type EllipseParams = [ + 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 interface RectProps extends GraphicPropsBase {} export interface CirclesProps extends GraphicPropsBase { radius?: number; start?: number; end?: number; + /** + * 圆属性参数,可以填 `[半径,起始角度,终止角度]`,是 radius, start, end 的简写, + * 其中半径可选,后两项要么都填,要么都不填 + */ + circle?: CircleParams; } export interface EllipseProps extends GraphicPropsBase { @@ -137,6 +177,11 @@ export interface EllipseProps extends GraphicPropsBase { radiusY?: number; start?: number; end?: number; + /** + * 椭圆属性参数,可以填 `[x半径,y半径,起始角度,终止角度]`,是 radiusX, radiusY, start, end 的简写, + * 其中前两项和后两项要么都填,要么都不填 + */ + ellipse?: EllipseParams; } export interface LineProps extends GraphicPropsBase { @@ -144,6 +189,8 @@ export interface LineProps extends GraphicPropsBase { y1?: number; x2?: number; y2?: number; + /** 直线属性参数,可以填 `[x1, y1, x2, y2]`,都是必填 */ + line?: LineParams; } export interface BezierProps extends GraphicPropsBase { @@ -155,6 +202,8 @@ export interface BezierProps extends GraphicPropsBase { cp2y?: number; ex?: number; ey?: number; + /** 三次贝塞尔曲线参数,可以填 `[sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey]`,都是必填 */ + curve?: BezierParams; } export interface QuadraticProps extends GraphicPropsBase { @@ -164,6 +213,8 @@ export interface QuadraticProps extends GraphicPropsBase { cpy?: number; ex?: number; ey?: number; + /** 二次贝塞尔曲线参数,可以填 `[sx, sy, cpx, cpy, ex, ey]`,都是必填 */ + curve?: QuadParams; } export interface PathProps extends GraphicPropsBase { @@ -171,26 +222,23 @@ export interface PathProps extends GraphicPropsBase { } export interface RectRProps extends GraphicPropsBase { - /** 圆角半径,此参数传入时,radiusX 和 radiusY 应保持一致 */ - radius: number; - /** 圆角横向半径 */ - radiusX?: number; - /** 圆角纵向半径 */ - radiusY?: number; - /** 圆角为线模式 */ - line?: boolean; - /** 圆角为椭圆模式,默认值 */ - ellipse?: boolean; - /** 圆角为二次贝塞尔曲线模式 */ - quad?: boolean; - /** 圆角为三次贝塞尔曲线模式 */ - cubic?: boolean; - /** 控制点,此参数传入时,cpx 和 cpy 应保持一致 */ - cp?: number; - /** 横向控制点 */ - cpx?: number; - /** 纵向控制点 */ - cpy?: number; + /** + * 圆形圆角参数,可以填 `[r1, r2, r3, r4]`,后三项可选。填写不同数量下的表现: + * - 1个:每个角都是 `r1` 半径的圆 + * - 2个:左上和右下是 `r1` 半径的圆,右上和左下是 `r2` 半径的圆 + * - 3个:左上是 `r1` 半径的圆,右上和左下是 `r2` 半径的圆,右下是 `r3` 半径的圆 + * - 4个:左上、右上、左下、右下 分别是 `r1, r2, r3, r4` 半径的圆 + */ + circle?: RectRCircleParams; + /** + * 圆形圆角参数,可以填 `[rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4]`, + * 两两一组,后三组可选,填写不同数量下的表现: + * - 1组:每个角都是 `[rx1, ry1]` 半径的椭圆 + * - 2组:左上和右下是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ry2]` 半径的椭圆 + * - 3组:左上是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ey2]` 半径的椭圆,右下是 `[rx3, ry3]` 半径的椭圆 + * - 4组:左上、右上、左下、右下 分别是 `[rx1, ry1], [rx2, ry2], [rx3, ry3], [rx4, ry4]` 半径的椭圆 + */ + ellipse?: RectREllipseParams; } export interface IconProps extends BaseProps { diff --git a/src/core/render/utils.ts b/src/core/render/utils.ts index b1b4308..d3fe8f1 100644 --- a/src/core/render/utils.ts +++ b/src/core/render/utils.ts @@ -29,6 +29,7 @@ export type ElementLocator = [ ]; export type ElementAnchor = [x: number, y: number]; +export type ElementScale = [x: number, y: number]; export function disableViewport() { const adapter = RenderAdapter.get('viewport'); diff --git a/src/core/system/ui/controller.ts b/src/core/system/ui/controller.ts index e4a5c1c..e11f048 100644 --- a/src/core/system/ui/controller.ts +++ b/src/core/system/ui/controller.ts @@ -96,6 +96,11 @@ export class UIController () => this.userShowBack.value && this.sysShowBack.value ); + /** 当前是否显示 UI */ + get active() { + return this.showBack.value; + } + /** 自定义显示模式下的配置信息 */ private config?: IUICustomConfig; /** 是否维持背景 UI */ diff --git a/src/data/logger.json b/src/data/logger.json index 7d823d7..a3b1efa 100644 --- a/src/data/logger.json +++ b/src/data/logger.json @@ -89,6 +89,7 @@ "55": "Unchildable tag '$1' should follow with param.", "56": "Method '$1' has been deprecated. Consider using '$2' instead.", "57": "Repeated UI controller on item '$1', new controller will not work.", + "58": "Fail to set ellipse round rect, since length of 'ellipse' property should only be 2, 4, 6 or 8. delivered: $1", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.", "1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance." } diff --git a/src/module/render/components/scroll.tsx b/src/module/render/components/scroll.tsx index 85017fc..29e6bea 100644 --- a/src/module/render/components/scroll.tsx +++ b/src/module/render/components/scroll.tsx @@ -155,6 +155,7 @@ export const Scroll = defineComponent( scrollTarget = (height.value - scrollLength) * (contentTarget / max); } + if (isNaN(scrollTarget)) scrollTarget = 0; transition.time(time).transition('scroll', scrollTarget); transition.time(time).transition('showScroll', target); }; @@ -200,16 +201,21 @@ export const Scroll = defineComponent( const onTransform = (item: RenderItem) => { const rect = item.getBoundingRect(); const pad = props.pad ?? 0; - if (direction.value === ScrollDirection.Horizontal) { - if (rect.right > maxLength - pad) { - maxLength = rect.right + pad; - updatePosition(); + if (item.parent === content.value) { + if (direction.value === ScrollDirection.Horizontal) { + if (rect.right > maxLength - pad) { + maxLength = rect.right + pad; + updatePosition(); + } + } else { + if (rect.bottom > maxLength - pad) { + maxLength = rect.bottom + pad; + updatePosition(); + } } } else { - if (rect.bottom > maxLength - pad) { - maxLength = rect.bottom + pad; - updatePosition(); - } + item.off('transform', onTransform); + listenedChild.delete(item); } getArea(item, rect); checkItem(item); @@ -222,9 +228,10 @@ export const Scroll = defineComponent( */ const updatePosition = () => { if (direction.value === ScrollDirection.Horizontal) { - scrollLength = Math.max( + scrollLength = clamp( + (height.value / maxLength) * width.value, SCROLL_MIN_LENGTH, - (width.value / maxLength) * width.value + width.value ); const h = props.noscroll ? height.value @@ -234,7 +241,7 @@ export const Scroll = defineComponent( scrollLength = clamp( (height.value / maxLength) * height.value, SCROLL_MIN_LENGTH, - height.value - 10 + height.value ); const w = props.noscroll ? width.value @@ -255,6 +262,7 @@ export const Scroll = defineComponent( listenedChild.clear(); areaMap.clear(); content.value.children.forEach(v => { + if (v.isComment) return; const rect = v.getBoundingRect(); if (direction.value === ScrollDirection.Horizontal) { if (rect.right > max) { @@ -266,11 +274,13 @@ export const Scroll = defineComponent( } } getArea(v, rect); - v.on('transform', onTransform); - listenedChild.add(v); + if (!listenedChild.has(v)) { + v.on('transform', onTransform); + listenedChild.add(v); + } checkItem(v); }); - maxLength = max + (props.pad ?? 0); + maxLength = Math.max(max + (props.pad ?? 0), 10); updatePosition(); scroll.value?.update(); }; @@ -491,6 +501,7 @@ export const Scroll = defineComponent(