mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-24 16:13:24 +08:00
Compare commits
10 Commits
6238c8f00a
...
c5101c9b29
Author | SHA1 | Date | |
---|---|---|---|
c5101c9b29 | |||
6e990fa926 | |||
06abef2595 | |||
9588340b11 | |||
d2bd98b735 | |||
dfa7428ba8 | |||
36f847f4a8 | |||
1e8600c080 | |||
d6e0de28ab | |||
8bc2a00bd2 |
@ -1,3 +1,4 @@
|
||||
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { ActionType, EventProgress, ActionEventMap } from './event';
|
||||
import {
|
||||
@ -14,7 +15,6 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
extends RenderItem<E | EContainerEvent>
|
||||
implements IRenderChildable
|
||||
{
|
||||
children: Set<RenderItem> = new Set();
|
||||
sortedChildren: RenderItem[] = [];
|
||||
|
||||
private needSort: boolean = false;
|
||||
@ -116,7 +116,9 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
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;
|
||||
}
|
||||
@ -133,7 +135,53 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
this.children.forEach(v => {
|
||||
v.destroy();
|
||||
v.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type CustomContainerRenderFn = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
transform: Transform
|
||||
) => void;
|
||||
|
||||
export class ContainerCustom extends Container {
|
||||
private renderFn?: CustomContainerRenderFn;
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
if (!this.renderFn) {
|
||||
super.render(canvas, transform);
|
||||
} else {
|
||||
this.renderFn(canvas, this.sortedChildren, transform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个自定义容器的渲染函数
|
||||
* @param render 渲染函数
|
||||
*/
|
||||
setRenderFn(render?: CustomContainerRenderFn) {
|
||||
this.renderFn = render;
|
||||
}
|
||||
|
||||
patchProp(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any,
|
||||
namespace?: ElementNamespace,
|
||||
parentComponent?: ComponentInternalInstance | null
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'render': {
|
||||
if (!this.assertType(nextValue, 'function', key)) return;
|
||||
this.setRenderFn(nextValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||
}
|
||||
}
|
||||
|
@ -228,6 +228,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
|
||||
//#region 元素属性
|
||||
|
||||
/** 是否是注释元素 */
|
||||
readonly isComment: boolean = false;
|
||||
|
||||
private _id: string = '';
|
||||
/**
|
||||
* 元素的 id,原则上不可重复
|
||||
@ -362,6 +365,11 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
this._transform.bind(this);
|
||||
this.cache = this.requireCanvas();
|
||||
this.cache.withGameScale(true);
|
||||
if (!enableCache) {
|
||||
this.cache.withGameScale(false);
|
||||
this.cache.size(1, 1);
|
||||
this.cache.freeze();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -437,7 +445,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
size(width: number, height: number): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.cache.size(width, height);
|
||||
if (this.enableCache) {
|
||||
this.cache.size(width, height);
|
||||
}
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
@ -480,13 +490,17 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
|
||||
setHD(hd: boolean): void {
|
||||
this.highResolution = hd;
|
||||
this.cache.setHD(hd);
|
||||
if (this.enableCache) {
|
||||
this.cache.setHD(hd);
|
||||
}
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
setAntiAliasing(anti: boolean): void {
|
||||
this.antiAliasing = anti;
|
||||
this.cache.setAntiAliasing(anti);
|
||||
if (this.enableCache) {
|
||||
this.cache.setAntiAliasing(anti);
|
||||
}
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
@ -540,7 +554,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到可以包围这个元素的最小矩形
|
||||
* 获取到可以包围这个元素的最小矩形,相对于父元素
|
||||
*/
|
||||
getBoundingRect(): DOMRectReadOnly {
|
||||
if (this.type === 'absolute') {
|
||||
@ -550,10 +564,22 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
? 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);
|
||||
@ -810,6 +836,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
: this._transform;
|
||||
if (!tran) return null;
|
||||
const [nx, ny] = this.calActionPosition(event, tran);
|
||||
|
||||
const inElement = this.isActionInElement(nx, ny);
|
||||
// 在元素范围内,执行事件
|
||||
const newEvent: ActionEventMap[T] = {
|
||||
@ -1130,6 +1157,19 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
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) {
|
||||
|
@ -1,18 +1,60 @@
|
||||
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):
|
||||
* <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 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 {
|
||||
/** 线宽 */
|
||||
@ -63,7 +105,7 @@ export abstract class GraphicItemBase
|
||||
extends RenderItem<EGraphicItemEvent>
|
||||
implements Required<ILineProperty>
|
||||
{
|
||||
mode: number = GraphicMode.Fill;
|
||||
mode: GraphicMode = GraphicMode.Fill;
|
||||
fill: CanvasStyle = '#fff';
|
||||
stroke: CanvasStyle = '#fff';
|
||||
lineWidth: number = 2;
|
||||
@ -79,6 +121,9 @@ export abstract class GraphicItemBase
|
||||
private strokeAndFill: boolean = false;
|
||||
private propFillSet: boolean = false;
|
||||
|
||||
private cachePath?: Path2D;
|
||||
protected pathDirty: boolean = false;
|
||||
|
||||
/**
|
||||
* 获取这个元素的绘制路径
|
||||
*/
|
||||
@ -90,7 +135,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 +161,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 +178,6 @@ export abstract class GraphicItemBase
|
||||
ctx.isPointInStroke(path, x, y)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -272,7 +325,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 +354,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 +375,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 +391,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 +403,7 @@ export class Circle extends GraphicItemBase {
|
||||
setAngle(start: number, end: number) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.pathDirty = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -381,6 +427,21 @@ export class Circle extends GraphicItemBase {
|
||||
if (!this.assertType(nextValue, 'number', key)) return;
|
||||
this.setAngle(this.start, nextValue);
|
||||
return;
|
||||
case 'circle': {
|
||||
const value = nextValue as CircleParams;
|
||||
if (!this.assertType(value, Array, key)) return;
|
||||
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;
|
||||
}
|
||||
}
|
||||
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||
}
|
||||
@ -391,40 +452,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 +477,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 +490,7 @@ export class Ellipse extends GraphicItemBase {
|
||||
setAngle(start: number, end: number) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.pathDirty = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -473,6 +518,21 @@ export class Ellipse extends GraphicItemBase {
|
||||
if (!this.assertType(nextValue, 'number', key)) return;
|
||||
this.setAngle(this.start, nextValue);
|
||||
return;
|
||||
case 'ellipse': {
|
||||
const value = nextValue as EllipseParams;
|
||||
if (!this.assertType(value, Array, key)) return;
|
||||
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;
|
||||
}
|
||||
}
|
||||
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||
}
|
||||
@ -483,17 +543,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 +560,7 @@ export class Line extends GraphicItemBase {
|
||||
setPoint1(x: number, y: number) {
|
||||
this.x1 = x;
|
||||
this.y1 = y;
|
||||
this.fitRect();
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -511,9 +570,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 +608,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 +627,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 +651,7 @@ export class BezierCurve extends GraphicItemBase {
|
||||
setStart(x: number, y: number) {
|
||||
this.sx = x;
|
||||
this.sy = y;
|
||||
this.fitRect();
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -587,6 +661,7 @@ export class BezierCurve extends GraphicItemBase {
|
||||
setControl1(x: number, y: number) {
|
||||
this.cp1x = x;
|
||||
this.cp1y = y;
|
||||
this.fitRect();
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -596,6 +671,7 @@ export class BezierCurve extends GraphicItemBase {
|
||||
setControl2(x: number, y: number) {
|
||||
this.cp2x = x;
|
||||
this.cp2y = y;
|
||||
this.fitRect();
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -605,9 +681,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 +735,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 +754,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 +788,7 @@ export class QuadraticCurve extends GraphicItemBase {
|
||||
setStart(x: number, y: number) {
|
||||
this.sx = x;
|
||||
this.sy = y;
|
||||
this.fitRect();
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -688,6 +798,7 @@ export class QuadraticCurve extends GraphicItemBase {
|
||||
setControl(x: number, y: number) {
|
||||
this.cpx = x;
|
||||
this.cpy = y;
|
||||
this.fitRect();
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -697,9 +808,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 +854,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 +869,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 +882,7 @@ export class Path extends GraphicItemBase {
|
||||
*/
|
||||
addPath(path: Path2D) {
|
||||
this.path.addPath(path);
|
||||
this.pathDirty = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -792,6 +897,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 +905,139 @@ export class Path extends GraphicItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
const enum RectRType {
|
||||
/** 圆角为椭圆 */
|
||||
Ellipse,
|
||||
/** 圆角为二次贝塞尔曲线 */
|
||||
Quad,
|
||||
/** 圆角为三次贝塞尔曲线。该模式下,包含两个控制点,一个控制点位于上下矩形边延长线,另一个控制点位于左右矩形边延长线 */
|
||||
Cubic,
|
||||
/** 圆角为直线连接 */
|
||||
Line
|
||||
export const enum RectRCorner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft
|
||||
}
|
||||
|
||||
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 +1049,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);
|
||||
}
|
||||
|
@ -107,10 +107,11 @@ export class Text extends RenderItem<ETextEvent> {
|
||||
* 计算字体所占空间,从而确定这个元素的大小
|
||||
*/
|
||||
calBox() {
|
||||
const { width, fontBoundingBoxAscent } = this.measure();
|
||||
const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
|
||||
this.measure();
|
||||
this.length = width;
|
||||
this.descent = fontBoundingBoxAscent;
|
||||
this.size(width, fontBoundingBoxAscent);
|
||||
this.descent = actualBoundingBoxAscent;
|
||||
this.size(width, actualBoundingBoxAscent + actualBoundingBoxDescent);
|
||||
}
|
||||
|
||||
patchProp(
|
||||
@ -197,11 +198,17 @@ export class Image extends RenderItem<EImageEvent> {
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -2,6 +2,7 @@ import { logger } from '../common/logger';
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { Container } from './container';
|
||||
import {
|
||||
ActionEventMap,
|
||||
ActionType,
|
||||
IActionEvent,
|
||||
IWheelEvent,
|
||||
@ -46,6 +47,8 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
|
||||
/** 用于终止 document 上的监听 */
|
||||
private abort?: AbortController;
|
||||
/** 根据捕获行为判断光标样式 */
|
||||
private targetCursor: string = 'auto';
|
||||
|
||||
target!: MotaOffscreenCanvas2D;
|
||||
|
||||
@ -108,6 +111,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
ActionType.Move,
|
||||
this.lastMouse
|
||||
);
|
||||
this.targetCursor = 'auto';
|
||||
this.captureEvent(ActionType.Move, event);
|
||||
});
|
||||
canvas.addEventListener('mouseenter', ev => {
|
||||
@ -250,6 +254,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
const id = this.getMouseIdentifier(type, mouse);
|
||||
const x = event.offsetX / core.domStyle.scale;
|
||||
const y = event.offsetY / core.domStyle.scale;
|
||||
|
||||
return {
|
||||
target: this,
|
||||
identifier: id,
|
||||
@ -380,6 +385,16 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
return list;
|
||||
}
|
||||
|
||||
bubbleEvent<T extends ActionType>(
|
||||
type: T,
|
||||
event: ActionEventMap[T]
|
||||
): ActionEventMap[T] | null {
|
||||
if (this.targetCursor !== this.target.canvas.style.cursor) {
|
||||
this.target.canvas.style.cursor = this.targetCursor;
|
||||
}
|
||||
return super.bubbleEvent(type, event);
|
||||
}
|
||||
|
||||
update(_item: RenderItem = this) {
|
||||
this.cacheDirty = true;
|
||||
}
|
||||
@ -450,7 +465,9 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
}
|
||||
|
||||
hoverElement(element: RenderItem): void {
|
||||
this.target.canvas.style.cursor = element.cursor;
|
||||
if (element.cursor !== 'auto') {
|
||||
this.targetCursor = element.cursor;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -463,10 +480,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
private toTagString(item: RenderItem, space: number, deep: number): string {
|
||||
const name = item.constructor.name;
|
||||
if (item.children.size === 0) {
|
||||
return `${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}"></${name}>\n`;
|
||||
return `${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}"${item.hidden ? ' hidden' : ''}></${name}>\n`;
|
||||
} else {
|
||||
return (
|
||||
`${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}">\n` +
|
||||
`${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}" ${item.hidden ? 'hidden' : ''}>\n` +
|
||||
`${[...item.children].map(v => this.toTagString(v, space, deep + 1)).join('')}` +
|
||||
`${' '.repeat(deep * space)}</${name}>\n`
|
||||
);
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
BezierProps,
|
||||
CirclesProps,
|
||||
CommentProps,
|
||||
ConatinerCustomProps,
|
||||
ContainerProps,
|
||||
CustomProps,
|
||||
DamageProps,
|
||||
@ -84,6 +85,10 @@ declare module 'vue/jsx-runtime' {
|
||||
export interface IntrinsicElements {
|
||||
sprite: TagDefine<SpriteProps, ESpriteEvent>;
|
||||
container: TagDefine<ContainerProps, EContainerEvent>;
|
||||
'container-custom': TagDefine<
|
||||
ConatinerCustomProps,
|
||||
EContainerEvent
|
||||
>;
|
||||
shader: TagDefine<ShaderProps, EShaderEvent>;
|
||||
text: TagDefine<TextProps, ETextEvent>;
|
||||
image: TagDefine<ImageProps, EImageEvent>;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item';
|
||||
import { ElementNamespace, VNodeProps } from 'vue';
|
||||
import { Container } from '../container';
|
||||
import { Container, ContainerCustom } from '../container';
|
||||
import { MotaRenderer } from '../render';
|
||||
import { Sprite } from '../sprite';
|
||||
import {
|
||||
@ -75,8 +75,13 @@ const standardElement = (
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('static');
|
||||
else {
|
||||
const { type = 'static', cache = true, fall = false } = props;
|
||||
return new Item(type, cache, fall);
|
||||
const {
|
||||
type = 'static',
|
||||
cache = true,
|
||||
fall = false,
|
||||
nocache = false
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -91,8 +96,13 @@ const standardElementNoCache = (
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('static');
|
||||
else {
|
||||
const { type = 'static', cache = false, fall = false } = props;
|
||||
return new Item(type, cache, fall);
|
||||
const {
|
||||
type = 'static',
|
||||
cache = false,
|
||||
fall = false,
|
||||
nocache = true
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -106,7 +116,7 @@ const enum ElementState {
|
||||
/**
|
||||
* standardElementFor
|
||||
*/
|
||||
const se = (
|
||||
const _se = (
|
||||
Item: new (
|
||||
type: RenderItemPosition,
|
||||
cache?: boolean,
|
||||
@ -124,15 +134,17 @@ const se = (
|
||||
const {
|
||||
type = position,
|
||||
cache = defaultCache,
|
||||
fall = defautFall
|
||||
fall = defautFall,
|
||||
nocache = !defaultCache
|
||||
} = props;
|
||||
return new Item(type, cache, fall);
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Default elements
|
||||
tagMap.register('container', standardElement(Container));
|
||||
tagMap.register('container-custom', standardElement(ContainerCustom));
|
||||
tagMap.register('template', standardElement(Container));
|
||||
tagMap.register('mota-renderer', (_0, _1, props) => {
|
||||
return new MotaRenderer(props?.id);
|
||||
@ -223,14 +235,14 @@ tagMap.register<EDamageEvent, Damage>('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']);
|
||||
|
@ -6,8 +6,18 @@ import {
|
||||
ILayerRenderExtends
|
||||
} from '../preset/layer';
|
||||
import type { EnemyCollection } from '@/game/enemy/damage';
|
||||
import { ILineProperty } from '../preset/graphics';
|
||||
import { ElementAnchor, ElementLocator } from '../utils';
|
||||
import {
|
||||
BezierParams,
|
||||
CircleParams,
|
||||
EllipseParams,
|
||||
ILineProperty,
|
||||
LineParams,
|
||||
QuadParams,
|
||||
RectRCircleParams,
|
||||
RectREllipseParams
|
||||
} from '../preset/graphics';
|
||||
import { ElementAnchor, ElementLocator, ElementScale } from '../utils';
|
||||
import { CustomContainerRenderFn } from '../container';
|
||||
|
||||
export interface CustomProps {
|
||||
_item: (props: BaseProps) => RenderItem;
|
||||
@ -27,7 +37,10 @@ export interface BaseProps {
|
||||
hidden?: boolean;
|
||||
transform?: Transform;
|
||||
type?: RenderItemPosition;
|
||||
/** 是否启用缓存,用处较少,主要用于一些默认不启用缓存的元素的特殊优化 */
|
||||
cache?: boolean;
|
||||
/** 是否不启用缓存,优先级大于 cache,用处较少,主要用于一些特殊优化 */
|
||||
nocache?: boolean;
|
||||
fall?: boolean;
|
||||
id?: string;
|
||||
alpha?: number;
|
||||
@ -41,6 +54,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 {
|
||||
@ -49,6 +66,10 @@ export interface SpriteProps extends BaseProps {
|
||||
|
||||
export interface ContainerProps extends BaseProps {}
|
||||
|
||||
export interface ConatinerCustomProps extends ContainerProps {
|
||||
render?: CustomContainerRenderFn;
|
||||
}
|
||||
|
||||
export interface GL2Props extends BaseProps {}
|
||||
|
||||
export interface ShaderProps extends BaseProps {}
|
||||
@ -122,6 +143,11 @@ export interface CirclesProps extends GraphicPropsBase {
|
||||
radius?: number;
|
||||
start?: number;
|
||||
end?: number;
|
||||
/**
|
||||
* 圆属性参数,可以填 `[半径,起始角度,终止角度]`,是 radius, start, end 的简写,
|
||||
* 其中半径可选,后两项要么都填,要么都不填
|
||||
*/
|
||||
circle?: CircleParams;
|
||||
}
|
||||
|
||||
export interface EllipseProps extends GraphicPropsBase {
|
||||
@ -129,6 +155,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 {
|
||||
@ -136,6 +167,8 @@ export interface LineProps extends GraphicPropsBase {
|
||||
y1?: number;
|
||||
x2?: number;
|
||||
y2?: number;
|
||||
/** 直线属性参数,可以填 `[x1, y1, x2, y2]`,都是必填 */
|
||||
line?: LineParams;
|
||||
}
|
||||
|
||||
export interface BezierProps extends GraphicPropsBase {
|
||||
@ -147,6 +180,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 {
|
||||
@ -156,6 +191,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 {
|
||||
@ -163,26 +200,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 {
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { Transform } from './transform';
|
||||
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||
import { ActionType, EventProgress, ActionEventMap } from './event';
|
||||
|
||||
export interface ESpriteEvent extends ERenderItemEvent {}
|
||||
|
||||
@ -43,14 +42,6 @@ export class Sprite<
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
protected propagateEvent<T extends ActionType>(
|
||||
type: T,
|
||||
_progress: EventProgress,
|
||||
event: ActionEventMap[T]
|
||||
): void {
|
||||
this.parent?.bubbleEvent(type, event);
|
||||
}
|
||||
|
||||
patchProp(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
|
@ -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<FloorViewport>('viewport');
|
||||
|
@ -96,6 +96,11 @@ export class UIController<C extends UIComponent = UIComponent>
|
||||
() => this.userShowBack.value && this.sysShowBack.value
|
||||
);
|
||||
|
||||
/** 当前是否显示 UI */
|
||||
get active() {
|
||||
return this.showBack.value;
|
||||
}
|
||||
|
||||
/** 自定义显示模式下的配置信息 */
|
||||
private config?: IUICustomConfig<C>;
|
||||
/** 是否维持背景 UI */
|
||||
|
@ -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."
|
||||
}
|
||||
|
266
src/module/render/components/page.tsx
Normal file
266
src/module/render/components/page.tsx
Normal file
@ -0,0 +1,266 @@
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
ref,
|
||||
SlotsType,
|
||||
VNode,
|
||||
watch
|
||||
} from 'vue';
|
||||
import { SetupComponentOptions } from './types';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { ElementLocator } from '@/core/render';
|
||||
|
||||
/** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */
|
||||
const RECT_PAD = 0.1;
|
||||
|
||||
export interface PageProps {
|
||||
/** 共有多少页 */
|
||||
pages: number;
|
||||
/** 页码组件的定位 */
|
||||
loc: ElementLocator;
|
||||
/** 页码的字体大小,默认为 14 */
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface PageExpose {
|
||||
/**
|
||||
* 切换页码
|
||||
* @param page 要切换至的页码数,1 表示第一页
|
||||
*/
|
||||
changePage(page: number): void;
|
||||
}
|
||||
|
||||
type PageSlots = SlotsType<{
|
||||
default: (page: number) => VNode | VNode[];
|
||||
}>;
|
||||
|
||||
const pageProps = {
|
||||
props: ['pages', 'loc', 'pageSize']
|
||||
} satisfies SetupComponentOptions<PageProps, {}, string, PageSlots>;
|
||||
|
||||
export const Page = defineComponent<PageProps, {}, string, PageSlots>(
|
||||
(props, { slots, expose }) => {
|
||||
const nowPage = ref(1);
|
||||
|
||||
// 五个元素的位置
|
||||
const leftLoc = ref<ElementLocator>([]);
|
||||
const leftPageLoc = ref<ElementLocator>([]);
|
||||
const nowPageLoc = ref<ElementLocator>([]);
|
||||
const rightPageLoc = ref<ElementLocator>([]);
|
||||
const rightLoc = ref<ElementLocator>([]);
|
||||
/** 内容的位置 */
|
||||
const contentLoc = ref<ElementLocator>([]);
|
||||
/** 页码容器的位置 */
|
||||
const pageLoc = ref<ElementLocator>([]);
|
||||
/** 页码的矩形框的位置 */
|
||||
const rectLoc = ref<ElementLocator>([0, 0, 0, 0]);
|
||||
/** 页面文字的位置 */
|
||||
const textLoc = ref<ElementLocator>([0, 0, 0, 0]);
|
||||
|
||||
// 两个监听的参数
|
||||
const leftArrow = ref<Path2D>(new Path2D());
|
||||
const rightArrow = ref<Path2D>(new Path2D());
|
||||
|
||||
const isFirst = computed(() => nowPage.value === 1);
|
||||
const isLast = computed(() => nowPage.value === props.pages);
|
||||
const pageSize = computed(() => props.pageSize ?? 14);
|
||||
const width = computed(() => props.loc[2] ?? 200);
|
||||
const height = computed(() => props.loc[3] ?? 200);
|
||||
const round = computed(() => pageSize.value / 4);
|
||||
const pageFont = computed(() => `${pageSize.value}px normal`);
|
||||
const nowPageFont = computed(() => `bold ${pageSize.value}px normal`);
|
||||
|
||||
// 左右箭头的颜色
|
||||
const leftColor = computed(() => (isFirst.value ? '#666' : '#ddd'));
|
||||
const rightColor = computed(() => (isLast.value ? '#666' : '#ddd'));
|
||||
|
||||
let updating = false;
|
||||
const updatePagePos = () => {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
nextTick(() => {
|
||||
updating = false;
|
||||
});
|
||||
const pageH = pageSize.value + 8;
|
||||
contentLoc.value = [0, 0, width.value, height.value - pageH];
|
||||
pageLoc.value = [0, height.value - pageH, width.value, pageH];
|
||||
const center = width.value / 2;
|
||||
const size = pageSize.value * 1.5;
|
||||
nowPageLoc.value = [center, 0, size, size, 0.5, 0];
|
||||
leftPageLoc.value = [center - size * 1.5, 0, size, size, 0.5, 0];
|
||||
leftLoc.value = [center - size * 3, 0, size, size, 0.5, 0];
|
||||
rightPageLoc.value = [center + size * 1.5, 0, size, size, 0.5, 0];
|
||||
rightLoc.value = [center + size * 3, 0, size, size, 0.5, 0];
|
||||
};
|
||||
|
||||
const updateArrowPath = () => {
|
||||
const rectSize = pageSize.value * 1.5;
|
||||
const size = pageSize.value;
|
||||
const pad = rectSize - size;
|
||||
const left = new Path2D();
|
||||
left.moveTo(size, pad);
|
||||
left.lineTo(pad, rectSize / 2);
|
||||
left.lineTo(size, rectSize - pad);
|
||||
const right = new Path2D();
|
||||
right.moveTo(pad, pad);
|
||||
right.lineTo(size, rectSize / 2);
|
||||
right.lineTo(pad, rectSize - pad);
|
||||
leftArrow.value = left;
|
||||
rightArrow.value = right;
|
||||
};
|
||||
|
||||
const updateRectAndText = () => {
|
||||
const size = pageSize.value * 1.5;
|
||||
const pad = RECT_PAD * size;
|
||||
rectLoc.value = [pad, pad, size - pad * 2, size - pad * 2];
|
||||
textLoc.value = [size / 2, size / 2, void 0, void 0, 0.5, 0.5];
|
||||
};
|
||||
|
||||
watch(pageSize, () => {
|
||||
updatePagePos();
|
||||
updateArrowPath();
|
||||
updateRectAndText();
|
||||
});
|
||||
watch(
|
||||
() => props.loc,
|
||||
() => {
|
||||
updatePagePos();
|
||||
updateRectAndText();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 切换页码
|
||||
*/
|
||||
const changePage = (page: number) => {
|
||||
const target = clamp(page, 1, props.pages);
|
||||
nowPage.value = target;
|
||||
};
|
||||
|
||||
const lastPage = () => {
|
||||
changePage(nowPage.value - 1);
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
changePage(nowPage.value + 1);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updatePagePos();
|
||||
updateArrowPath();
|
||||
updateRectAndText();
|
||||
});
|
||||
|
||||
expose({ changePage });
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<container loc={props.loc}>
|
||||
<container loc={contentLoc.value}>
|
||||
{slots.default?.(nowPage.value)}
|
||||
</container>
|
||||
<container loc={pageLoc.value}>
|
||||
<container
|
||||
loc={leftLoc.value}
|
||||
onClick={lastPage}
|
||||
cursor="pointer"
|
||||
>
|
||||
<g-rectr
|
||||
loc={rectLoc.value}
|
||||
circle={[round.value]}
|
||||
strokeStyle={leftColor.value}
|
||||
lineWidth={1}
|
||||
stroke
|
||||
></g-rectr>
|
||||
<g-path
|
||||
path={leftArrow.value}
|
||||
stroke
|
||||
strokeStyle={leftColor.value}
|
||||
lineWidth={1}
|
||||
></g-path>
|
||||
</container>
|
||||
{!isFirst.value && (
|
||||
<container
|
||||
loc={leftPageLoc.value}
|
||||
onClick={lastPage}
|
||||
cursor="pointer"
|
||||
>
|
||||
<g-rectr
|
||||
loc={rectLoc.value}
|
||||
circle={[round.value]}
|
||||
strokeStyle="#ddd"
|
||||
lineWidth={1}
|
||||
stroke
|
||||
></g-rectr>
|
||||
<text
|
||||
loc={textLoc.value}
|
||||
text={(nowPage.value - 1).toString()}
|
||||
font={pageFont.value}
|
||||
></text>
|
||||
</container>
|
||||
)}
|
||||
<container loc={nowPageLoc.value}>
|
||||
<g-rectr
|
||||
loc={rectLoc.value}
|
||||
circle={[round.value]}
|
||||
strokeStyle="#ddd"
|
||||
fillStyle="#ddd"
|
||||
lineWidth={1}
|
||||
fill
|
||||
stroke
|
||||
></g-rectr>
|
||||
<text
|
||||
loc={textLoc.value}
|
||||
text={nowPage.value.toString()}
|
||||
fillStyle="#222"
|
||||
font={nowPageFont.value}
|
||||
></text>
|
||||
</container>
|
||||
{!isLast.value && (
|
||||
<container
|
||||
loc={rightPageLoc.value}
|
||||
onClick={nextPage}
|
||||
cursor="pointer"
|
||||
>
|
||||
<g-rectr
|
||||
loc={rectLoc.value}
|
||||
circle={[round.value]}
|
||||
strokeStyle="#ddd"
|
||||
lineWidth={1}
|
||||
stroke
|
||||
></g-rectr>
|
||||
<text
|
||||
loc={textLoc.value}
|
||||
text={(nowPage.value + 1).toString()}
|
||||
font={pageFont.value}
|
||||
></text>
|
||||
</container>
|
||||
)}
|
||||
<container
|
||||
loc={rightLoc.value}
|
||||
onClick={nextPage}
|
||||
cursor="pointer"
|
||||
>
|
||||
<g-rectr
|
||||
loc={rectLoc.value}
|
||||
circle={[round.value]}
|
||||
strokeStyle={rightColor.value}
|
||||
lineWidth={1}
|
||||
stroke
|
||||
></g-rectr>
|
||||
<g-path
|
||||
path={rightArrow.value}
|
||||
stroke
|
||||
strokeStyle={rightColor.value}
|
||||
lineWidth={1}
|
||||
></g-path>
|
||||
</container>
|
||||
</container>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
},
|
||||
pageProps
|
||||
);
|
@ -1,10 +1,10 @@
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
reactive,
|
||||
ref,
|
||||
SlotsType,
|
||||
VNode,
|
||||
@ -13,31 +13,40 @@ import {
|
||||
import { SetupComponentOptions } from './types';
|
||||
import {
|
||||
Container,
|
||||
ContainerProps,
|
||||
ElementLocator,
|
||||
RenderItem,
|
||||
Sprite,
|
||||
SpriteProps
|
||||
Transform
|
||||
} from '@/core/render';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { hyper, Transition } from 'mutate-animate';
|
||||
import { hyper, linear, Transition } from 'mutate-animate';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { IActionEvent, IWheelEvent, MouseType } from '@/core/render/event';
|
||||
import { transitioned } from '../use';
|
||||
|
||||
export const enum ScrollDirection {
|
||||
Horizontal,
|
||||
Vertical
|
||||
}
|
||||
|
||||
interface ScrollProps {
|
||||
direction: ScrollDirection;
|
||||
export interface ScrollExpose {
|
||||
/**
|
||||
* 控制滚动条滚动至目标位置
|
||||
* @param y 滚动至的目标位置
|
||||
* @param time 滚动的动画时长,默认为无动画
|
||||
*/
|
||||
scrollTo(y: number, time?: number): void;
|
||||
}
|
||||
|
||||
export interface ScrollProps {
|
||||
loc: ElementLocator;
|
||||
hor?: boolean;
|
||||
noscroll?: boolean;
|
||||
/**
|
||||
* 滚动到最下方(最右方)时的填充大小,如果默认的高度计算方式有误,
|
||||
* 那么可以调整此参数来修复错误
|
||||
*/
|
||||
padHeight?: number;
|
||||
pad?: number;
|
||||
}
|
||||
|
||||
type ScrollSlots = SlotsType<{
|
||||
@ -45,76 +54,127 @@ type ScrollSlots = SlotsType<{
|
||||
}>;
|
||||
|
||||
const scrollProps = {
|
||||
props: ['direction', 'noscroll']
|
||||
props: ['hor', 'noscroll', 'loc', 'pad']
|
||||
} satisfies SetupComponentOptions<ScrollProps, {}, string, ScrollSlots>;
|
||||
|
||||
/** 滚动条图示的最短长度 */
|
||||
const SCROLL_MIN_LENGTH = 20;
|
||||
/** 滚动条图示的宽度 */
|
||||
const SCROLL_WIDTH = 10;
|
||||
/** 滚动条的颜色 */
|
||||
const SCROLL_COLOR = '255,255,255';
|
||||
|
||||
/**
|
||||
* 滚动条组件,具有虚拟滚动功能,即在画面外的不渲染。参数参考 {@link ScrollProps},暴露接口参考 {@link ScrollExpose}
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* 使用时,建议使用平铺式布局,即包含很多子元素,而不要用一个 container 将所有内容包裹,
|
||||
* 每个子元素的高度(宽度)不建议过大,以更好地通过虚拟滚动优化
|
||||
*
|
||||
* **推荐写法**:
|
||||
* ```tsx
|
||||
* <Scroll>
|
||||
* <item />
|
||||
* <item />
|
||||
* ...其他元素
|
||||
* <item />
|
||||
* <item />
|
||||
* </Scroll>
|
||||
* ```
|
||||
* **不推荐**使用这种写法:
|
||||
* ```tsx
|
||||
* <Scroll>
|
||||
* <container>
|
||||
* <item />
|
||||
* </container>
|
||||
* <Scroll>
|
||||
* ```
|
||||
*/
|
||||
export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
(props, { slots }) => {
|
||||
const scrollProps: SpriteProps = reactive({
|
||||
loc: [0, 0, 0, 0]
|
||||
});
|
||||
const contentProps: ContainerProps = reactive({
|
||||
loc: [0, 0, 0, 0]
|
||||
});
|
||||
(props, { slots, expose }) => {
|
||||
/** 滚动条的定位 */
|
||||
const sp = ref<ElementLocator>([0, 0, 1, 1]);
|
||||
|
||||
const listenedChild: Set<RenderItem> = new Set();
|
||||
const areaMap: Map<RenderItem, [number, number]> = new Map();
|
||||
const content = ref<Container>();
|
||||
const scroll = ref<Sprite>();
|
||||
|
||||
const scrollAlpha = transitioned(0.5, 100, linear())!;
|
||||
|
||||
const width = computed(() => props.loc[2] ?? 200);
|
||||
const height = computed(() => props.loc[3] ?? 200);
|
||||
const direction = computed(() =>
|
||||
props.hor ? ScrollDirection.Horizontal : ScrollDirection.Vertical
|
||||
);
|
||||
const scrollColor = computed(
|
||||
() => `rgba(${SCROLL_COLOR},${scrollAlpha.ref.value ?? 0.5})`
|
||||
);
|
||||
|
||||
let showScroll = 0;
|
||||
let nowScroll = 0;
|
||||
watch(scrollColor, () => {
|
||||
scroll.value?.update();
|
||||
});
|
||||
|
||||
/** 滚动内容的当前位置 */
|
||||
let contentPos = 0;
|
||||
/** 滚动条的当前位置 */
|
||||
let scrollPos = 0;
|
||||
/** 滚动内容的目标位置 */
|
||||
let contentTarget = 0;
|
||||
/** 滚动条的目标位置 */
|
||||
let scrollTarget = 0;
|
||||
/** 滚动内容的长度 */
|
||||
let maxLength = 0;
|
||||
/** 滚动条的长度 */
|
||||
let scrollLength = SCROLL_MIN_LENGTH;
|
||||
|
||||
const transition = new Transition();
|
||||
transition.value.scroll = 0;
|
||||
transition.value.showScroll = 0;
|
||||
transition.mode(hyper('sin', 'out')).absolute();
|
||||
|
||||
//#region 滚动操作
|
||||
|
||||
transition.ticker.add(() => {
|
||||
if (transition.value.scroll !== nowScroll) {
|
||||
showScroll = transition.value.scroll;
|
||||
scroll.value?.update();
|
||||
if (scrollPos !== scrollTarget) {
|
||||
scrollPos = transition.value.scroll;
|
||||
content.value?.update();
|
||||
}
|
||||
if (contentPos !== contentTarget) {
|
||||
contentPos = transition.value.showScroll;
|
||||
checkAllItem();
|
||||
updatePosition();
|
||||
content.value?.update();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.loc,
|
||||
value => {
|
||||
const width = value[2] ?? 200;
|
||||
const height = value[3] ?? 200;
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
props.loc = [0, height - SCROLL_WIDTH, width, SCROLL_WIDTH];
|
||||
} else {
|
||||
props.loc = [width - SCROLL_WIDTH, 0, SCROLL_WIDTH, height];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 滚动到目标值
|
||||
* @param time 动画时长
|
||||
*/
|
||||
const scrollTo = (y: number, time: number = 1) => {
|
||||
const target = clamp(y, 0, maxLength);
|
||||
transition.time(time).transition('scroll', target);
|
||||
nowScroll = y;
|
||||
const scrollTo = (y: number, time: number = 0) => {
|
||||
if (maxLength < height.value) return;
|
||||
const max = maxLength - height.value;
|
||||
const target = clamp(y, 0, max);
|
||||
contentTarget = target;
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
scrollTarget =
|
||||
(width.value - scrollLength) * (contentTarget / max);
|
||||
} else {
|
||||
scrollTarget =
|
||||
(height.value - scrollLength) * (contentTarget / max);
|
||||
}
|
||||
if (isNaN(scrollTarget)) scrollTarget = 0;
|
||||
transition.time(time).transition('scroll', scrollTarget);
|
||||
transition.time(time).transition('showScroll', target);
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算一个元素会在画面上显示的区域
|
||||
*/
|
||||
const getArea = (item: RenderItem, rect: DOMRectReadOnly) => {
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
areaMap.set(item, [rect.left - width.value, rect.right]);
|
||||
} else {
|
||||
areaMap.set(item, [rect.top - height.value, rect.bottom]);
|
||||
@ -131,31 +191,90 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
return;
|
||||
}
|
||||
const [min, max] = area;
|
||||
if (nowScroll > min - 10 && nowScroll < max + 10) {
|
||||
if (contentPos > min - 10 && contentPos < max + 10) {
|
||||
item.show();
|
||||
} else {
|
||||
item.hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 对所有元素执行显示检查
|
||||
*/
|
||||
const checkAllItem = () => {
|
||||
content.value?.children.forEach(v => checkItem(v));
|
||||
};
|
||||
|
||||
/**
|
||||
* 当一个元素的矩阵发生变换时执行,检查其显示区域
|
||||
*/
|
||||
const onTransform = (item: RenderItem) => {
|
||||
const rect = item.getBoundingRect();
|
||||
const pad = props.pad ?? 0;
|
||||
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 {
|
||||
item.off('transform', onTransform);
|
||||
listenedChild.delete(item);
|
||||
}
|
||||
getArea(item, rect);
|
||||
checkItem(item);
|
||||
scroll.value?.update();
|
||||
content.value?.update();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新滚动条位置
|
||||
*/
|
||||
const updatePosition = () => {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
scrollLength = clamp(
|
||||
(height.value / maxLength) * width.value,
|
||||
SCROLL_MIN_LENGTH,
|
||||
width.value
|
||||
);
|
||||
const h = props.noscroll
|
||||
? height.value
|
||||
: height.value - SCROLL_WIDTH;
|
||||
sp.value = [0, h, width.value, SCROLL_WIDTH];
|
||||
} else {
|
||||
scrollLength = clamp(
|
||||
(height.value / maxLength) * height.value,
|
||||
SCROLL_MIN_LENGTH,
|
||||
height.value
|
||||
);
|
||||
const w = props.noscroll
|
||||
? width.value
|
||||
: width.value - SCROLL_WIDTH;
|
||||
sp.value = [w, 0, SCROLL_WIDTH, height.value];
|
||||
}
|
||||
};
|
||||
|
||||
let updating = false;
|
||||
const updateScroll = () => {
|
||||
if (!content.value) return;
|
||||
if (!content.value || updating) return;
|
||||
updating = true;
|
||||
nextTick(() => {
|
||||
updating = false;
|
||||
});
|
||||
let max = 0;
|
||||
listenedChild.forEach(v => v.off('transform', onTransform));
|
||||
listenedChild.clear();
|
||||
areaMap.clear();
|
||||
content.value.children.forEach(v => {
|
||||
if (v.isComment) return;
|
||||
const rect = v.getBoundingRect();
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
if (rect.right > max) {
|
||||
max = rect.right;
|
||||
}
|
||||
@ -164,81 +283,114 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
max = rect.bottom;
|
||||
}
|
||||
}
|
||||
v.on('transform', onTransform);
|
||||
listenedChild.add(v);
|
||||
getArea(v, rect);
|
||||
if (!listenedChild.has(v)) {
|
||||
v.on('transform', onTransform);
|
||||
listenedChild.add(v);
|
||||
}
|
||||
checkItem(v);
|
||||
});
|
||||
maxLength = max + (props.padHeight ?? 0);
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
scrollLength = Math.max(
|
||||
SCROLL_MIN_LENGTH,
|
||||
(width.value / max) * width.value
|
||||
);
|
||||
const h = props.noscroll
|
||||
? height.value
|
||||
: height.value - SCROLL_WIDTH;
|
||||
contentProps.loc = [-showScroll, 0, width.value, h];
|
||||
} else {
|
||||
scrollLength = clamp(
|
||||
(height.value / max) * height.value,
|
||||
SCROLL_MIN_LENGTH,
|
||||
height.value - 10
|
||||
);
|
||||
const w = props.noscroll
|
||||
? width.value
|
||||
: width.value - SCROLL_WIDTH;
|
||||
contentProps.loc = [0, -showScroll, w, height.value];
|
||||
}
|
||||
maxLength = Math.max(max + (props.pad ?? 0), 10);
|
||||
updatePosition();
|
||||
scroll.value?.update();
|
||||
};
|
||||
|
||||
watch(() => props.loc, updateScroll);
|
||||
onUpdated(updateScroll);
|
||||
onMounted(updateScroll);
|
||||
onUnmounted(() => {
|
||||
listenedChild.forEach(v => v.off('transform', onTransform));
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 渲染滚动
|
||||
|
||||
const drawScroll = (canvas: MotaOffscreenCanvas2D) => {
|
||||
if (props.noscroll) return;
|
||||
const ctx = canvas.ctx;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = 6;
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = scrollColor.value;
|
||||
ctx.beginPath();
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
ctx.moveTo(nowScroll + 5, 5);
|
||||
ctx.lineTo(nowScroll + scrollLength + 5, 5);
|
||||
const scroll = transition.value.scroll;
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
ctx.moveTo(scroll + 5, 5);
|
||||
ctx.lineTo(scroll + scrollLength - 5, 5);
|
||||
} else {
|
||||
ctx.moveTo(5, nowScroll + 5);
|
||||
ctx.lineTo(5, nowScroll + scrollLength + 5);
|
||||
ctx.moveTo(5, scroll + 5);
|
||||
ctx.lineTo(5, scroll + scrollLength - 5);
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const renderContent = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
transform: Transform
|
||||
) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.save();
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
ctx.translate(-contentPos, 0);
|
||||
} else {
|
||||
ctx.translate(0, -contentPos);
|
||||
}
|
||||
children.forEach(v => {
|
||||
if (v.hidden) return;
|
||||
v.renderContent(canvas, transform);
|
||||
});
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 事件监听
|
||||
|
||||
const wheelScroll = (delta: number, max: number) => {
|
||||
const sign = Math.sign(delta);
|
||||
const dx = Math.abs(delta);
|
||||
const movement = Math.min(max, dx) * sign;
|
||||
scrollTo(contentTarget + movement, dx > 10 ? 300 : 0);
|
||||
};
|
||||
|
||||
const wheel = (ev: IWheelEvent) => {
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
if (ev.wheelX !== 0) {
|
||||
scrollTo(nowScroll + ev.wheelX, 300);
|
||||
wheelScroll(ev.wheelX, width.value / 5);
|
||||
} else if (ev.wheelY !== 0) {
|
||||
scrollTo(nowScroll + ev.wheelY, 300);
|
||||
wheelScroll(ev.wheelY, width.value / 5);
|
||||
}
|
||||
} else {
|
||||
scrollTo(nowScroll + ev.wheelY, 300);
|
||||
wheelScroll(ev.wheelY, height.value / 5);
|
||||
}
|
||||
};
|
||||
|
||||
const getPos = (ev: IActionEvent) => {
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
return ev.offsetX;
|
||||
const getPos = (ev: IActionEvent, absolute: boolean = false) => {
|
||||
if (absolute) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
return ev.absoluteX;
|
||||
} else {
|
||||
return ev.absoluteY;
|
||||
}
|
||||
} else {
|
||||
return ev.offsetY;
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
return ev.offsetX;
|
||||
} else {
|
||||
return ev.offsetY;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let identifier: number = -1;
|
||||
let lastPos: number = 0;
|
||||
let identifier = -2;
|
||||
let downPos = 0;
|
||||
/** 拖动内容时,内容原本所在的位置 */
|
||||
let contentBefore = 0;
|
||||
|
||||
const down = (ev: IActionEvent) => {
|
||||
identifier = ev.identifier;
|
||||
lastPos = getPos(ev);
|
||||
downPos = getPos(ev, true);
|
||||
contentBefore = contentTarget;
|
||||
};
|
||||
|
||||
const move = (ev: IActionEvent) => {
|
||||
@ -249,24 +401,30 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
} else {
|
||||
if (ev.buttons & MouseType.Left) {
|
||||
pos = getPos(ev);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const movement = pos - lastPos;
|
||||
scrollTo(nowScroll + movement, 1);
|
||||
lastPos = pos;
|
||||
const movement = pos - downPos;
|
||||
scrollTo(contentBefore - movement, 0);
|
||||
};
|
||||
|
||||
/** 最初滚动条在哪 */
|
||||
let scrollBefore = 0;
|
||||
let scrollIdentifier = -1;
|
||||
/** 本次拖动滚动条的操作标识符 */
|
||||
let scrollIdentifier = -2;
|
||||
/** 点击滚动条时,点击位置在平行于滚动条方向的位置 */
|
||||
let scrollDownPos = 0;
|
||||
/** 是否是点击了滚动条区域中滚动条之外的地方,这样视为类滚轮操作 */
|
||||
let scrollMutate = false;
|
||||
/** 点击滚动条时,点击位置垂直于滚动条方向的位置 */
|
||||
let scrollPin = 0;
|
||||
|
||||
/**
|
||||
* 获取点击滚动条时,垂直于滚动条方向的位置
|
||||
*/
|
||||
const getScrollPin = (ev: IActionEvent) => {
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
return ev.absoluteY;
|
||||
} else {
|
||||
return ev.absoluteX;
|
||||
@ -274,23 +432,25 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
};
|
||||
|
||||
const downScroll = (ev: IActionEvent) => {
|
||||
scrollBefore = nowScroll;
|
||||
scrollBefore = contentTarget;
|
||||
scrollIdentifier = ev.identifier;
|
||||
const pos = getPos(ev);
|
||||
const pos = getPos(ev, true);
|
||||
// 计算点击在了滚动条的哪个位置
|
||||
const sEnd = nowScroll + scrollLength;
|
||||
if (pos >= nowScroll && pos <= sEnd) {
|
||||
scrollDownPos = pos - nowScroll;
|
||||
const offsetPos = getPos(ev);
|
||||
const sEnd = scrollPos + scrollLength;
|
||||
if (offsetPos >= scrollPos && offsetPos <= sEnd) {
|
||||
scrollDownPos = pos - scrollPos;
|
||||
scrollMutate = false;
|
||||
scrollPin = getScrollPin(ev);
|
||||
} else {
|
||||
scrollMutate = true;
|
||||
}
|
||||
scrollAlpha.set(0.9);
|
||||
};
|
||||
|
||||
const moveScroll = (ev: IActionEvent) => {
|
||||
if (ev.identifier !== scrollIdentifier) return;
|
||||
const pos = getPos(ev);
|
||||
if (ev.identifier !== scrollIdentifier || scrollMutate) return;
|
||||
const pos = getPos(ev, true);
|
||||
const scrollPos = pos - scrollDownPos;
|
||||
let deltaPin = 0;
|
||||
let threshold = 0;
|
||||
@ -304,22 +464,34 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
threshold = 100;
|
||||
}
|
||||
if (deltaPin > threshold) {
|
||||
scrollTo(scrollBefore, 1);
|
||||
scrollTo(scrollBefore, 0);
|
||||
} else {
|
||||
scrollTo(scrollPos, 1);
|
||||
const pos = (scrollPos / height.value) * maxLength;
|
||||
scrollTo(pos, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const upScroll = (ev: IActionEvent) => {
|
||||
scrollAlpha.set(0.7);
|
||||
if (!scrollMutate) return;
|
||||
const pos = getPos(ev);
|
||||
if (pos < nowScroll) {
|
||||
scrollTo(pos - 50);
|
||||
if (pos < scrollPos) {
|
||||
scrollTo(contentTarget - 50, 300);
|
||||
} else {
|
||||
scrollTo(pos + 50);
|
||||
scrollTo(contentTarget + 50, 300);
|
||||
}
|
||||
};
|
||||
|
||||
const enter = () => {
|
||||
scrollAlpha.set(0.7);
|
||||
};
|
||||
|
||||
const leave = () => {
|
||||
scrollAlpha.set(0.5);
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
onMounted(() => {
|
||||
scroll.value?.root?.on('move', move);
|
||||
scroll.value?.root?.on('move', moveScroll);
|
||||
@ -328,20 +500,36 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
onUnmounted(() => {
|
||||
scroll.value?.root?.off('move', move);
|
||||
scroll.value?.root?.off('move', moveScroll);
|
||||
transition.ticker.destroy();
|
||||
});
|
||||
|
||||
expose<ScrollExpose>({
|
||||
scrollTo
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<container loc={props.loc} onWheel={wheel}>
|
||||
<container {...contentProps} ref={content} onDown={down}>
|
||||
{slots.default()}
|
||||
</container>
|
||||
<container-custom
|
||||
loc={[0, 0, props.loc[2], props.loc[3]]}
|
||||
ref={content}
|
||||
onDown={down}
|
||||
render={renderContent}
|
||||
zIndex={0}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</container-custom>
|
||||
<sprite
|
||||
{...scrollProps}
|
||||
nocache
|
||||
hidden={props.noscroll}
|
||||
loc={sp.value}
|
||||
ref={scroll}
|
||||
render={drawScroll}
|
||||
onDown={downScroll}
|
||||
onUp={upScroll}
|
||||
zIndex={10}
|
||||
onEnter={enter}
|
||||
onLeave={leave}
|
||||
></sprite>
|
||||
</container>
|
||||
);
|
||||
|
@ -147,7 +147,12 @@ const MainScene = defineComponent(() => {
|
||||
status={rightStatus}
|
||||
></RightStatusBar>
|
||||
)}
|
||||
{mainUIController.render()}
|
||||
<container
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
hidden={mainUIController.showBack.value}
|
||||
>
|
||||
{mainUIController.render()}
|
||||
</container>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { GameUI } from '@/core/system';
|
||||
import { defineComponent } from 'vue';
|
||||
import { SetupComponentOptions } from '../components';
|
||||
import { ElementLocator } from '@/core/render';
|
||||
import { Scroll } from '../components/scroll';
|
||||
|
||||
export interface ILeftHeroStatus {
|
||||
hp: number;
|
||||
@ -168,6 +169,7 @@ export const RightStatusBar = defineComponent<StatusBarProps<IRightHeroStatus>>(
|
||||
return (
|
||||
<container loc={p.loc}>
|
||||
<g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect>
|
||||
<Scroll loc={[0, 0, 180, 100]}></Scroll>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { TimingFn, Transition } from 'mutate-animate';
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
getCurrentInstance,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
Ref
|
||||
} from 'vue';
|
||||
|
||||
export const enum Orientation {
|
||||
/** 横屏 */
|
||||
@ -58,3 +66,65 @@ export function onLoaded(hook: () => void) {
|
||||
hook();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITransitionedController {
|
||||
readonly ref: Ref<number>;
|
||||
readonly value: number;
|
||||
set(value: number): void;
|
||||
}
|
||||
|
||||
class RenderTransition implements ITransitionedController {
|
||||
private static key: number = 0;
|
||||
|
||||
private readonly key: string = `$${RenderTransition.key++}`;
|
||||
|
||||
public readonly ref: Ref<number>;
|
||||
|
||||
set value(v: number) {
|
||||
this.transition.transition(this.key, v);
|
||||
}
|
||||
get value() {
|
||||
return this.transition.value[this.key];
|
||||
}
|
||||
|
||||
constructor(
|
||||
value: number,
|
||||
public readonly transition: Transition,
|
||||
public readonly time: number,
|
||||
public readonly curve: TimingFn
|
||||
) {
|
||||
this.ref = ref(value);
|
||||
transition.ticker.add(() => {
|
||||
this.ref.value = transition.value[this.key];
|
||||
});
|
||||
}
|
||||
|
||||
set(value: number): void {
|
||||
this.transition
|
||||
.time(this.time)
|
||||
.mode(this.curve)
|
||||
.transition(this.key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const transitionMap = new Map<ComponentInternalInstance, Transition>();
|
||||
|
||||
export function transitioned(
|
||||
value: number,
|
||||
time: number,
|
||||
curve: TimingFn
|
||||
): ITransitionedController | null {
|
||||
const instance = getCurrentInstance();
|
||||
if (!instance) return null;
|
||||
if (!transitionMap.has(instance)) {
|
||||
const tran = new Transition();
|
||||
transitionMap.set(instance, tran);
|
||||
onUnmounted(() => {
|
||||
transitionMap.delete(instance);
|
||||
tran.ticker.destroy();
|
||||
});
|
||||
}
|
||||
const tran = transitionMap.get(instance);
|
||||
if (!tran) return null;
|
||||
return new RenderTransition(value, tran, time, curve);
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ const FSHOST = 'http://127.0.0.1:3000/';
|
||||
|
||||
const custom = [
|
||||
'container', 'image', 'sprite', 'shader', 'text', 'comment', 'custom',
|
||||
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin'
|
||||
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin',
|
||||
'container-custom'
|
||||
]
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
|
Loading…
Reference in New Issue
Block a user