Compare commits

..

10 Commits

18 changed files with 1215 additions and 355 deletions

View File

@ -1,3 +1,4 @@
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { ActionType, EventProgress, ActionEventMap } from './event'; import { ActionType, EventProgress, ActionEventMap } from './event';
import { import {
@ -14,7 +15,6 @@ export class Container<E extends EContainerEvent = EContainerEvent>
extends RenderItem<E | EContainerEvent> extends RenderItem<E | EContainerEvent>
implements IRenderChildable implements IRenderChildable
{ {
children: Set<RenderItem> = new Set();
sortedChildren: RenderItem[] = []; sortedChildren: RenderItem[] = [];
private needSort: boolean = false; private needSort: boolean = false;
@ -116,7 +116,9 @@ export class Container<E extends EContainerEvent = EContainerEvent>
if (progress === EventProgress.Capture) { if (progress === EventProgress.Capture) {
let success = false; let success = false;
for (let i = len - 1; i >= 0; i--) { 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; success = true;
break; break;
} }
@ -133,7 +135,53 @@ export class Container<E extends EContainerEvent = EContainerEvent>
destroy(): void { destroy(): void {
super.destroy(); super.destroy();
this.children.forEach(v => { 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);
}
}

View File

@ -228,6 +228,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
//#region 元素属性 //#region 元素属性
/** 是否是注释元素 */
readonly isComment: boolean = false;
private _id: string = ''; private _id: string = '';
/** /**
* id * id
@ -362,6 +365,11 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this._transform.bind(this); this._transform.bind(this);
this.cache = this.requireCanvas(); this.cache = this.requireCanvas();
this.cache.withGameScale(true); 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 { size(width: number, height: number): void {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.cache.size(width, height); if (this.enableCache) {
this.cache.size(width, height);
}
this.update(this); this.update(this);
} }
@ -480,13 +490,17 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
setHD(hd: boolean): void { setHD(hd: boolean): void {
this.highResolution = hd; this.highResolution = hd;
this.cache.setHD(hd); if (this.enableCache) {
this.cache.setHD(hd);
}
this.update(this); this.update(this);
} }
setAntiAliasing(anti: boolean): void { setAntiAliasing(anti: boolean): void {
this.antiAliasing = anti; this.antiAliasing = anti;
this.cache.setAntiAliasing(anti); if (this.enableCache) {
this.cache.setAntiAliasing(anti);
}
this.update(this); this.update(this);
} }
@ -540,7 +554,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
} }
/** /**
* *
*/ */
getBoundingRect(): DOMRectReadOnly { getBoundingRect(): DOMRectReadOnly {
if (this.type === 'absolute') { if (this.type === 'absolute') {
@ -550,10 +564,22 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
? this.fallTransform ? this.fallTransform
: this._transform; : this._transform;
if (!tran) return new DOMRectReadOnly(0, 0, this.width, this.height); if (!tran) return new DOMRectReadOnly(0, 0, this.width, this.height);
const [x1, y1] = tran.transformed(0, 0); const [x1, y1] = tran.transformed(
const [x2, y2] = tran.transformed(this.width, 0); -this.anchorX * this.width,
const [x3, y3] = tran.transformed(0, this.height); -this.anchorY * this.height
const [x4, y4] = tran.transformed(this.width, 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 left = Math.min(x1, x2, x3, x4);
const right = Math.max(x1, x2, x3, x4); const right = Math.max(x1, x2, x3, x4);
const top = Math.min(y1, y2, y3, y4); const top = Math.min(y1, y2, y3, y4);
@ -810,6 +836,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
: this._transform; : this._transform;
if (!tran) return null; if (!tran) return null;
const [nx, ny] = this.calActionPosition(event, tran); const [nx, ny] = this.calActionPosition(event, tran);
const inElement = this.isActionInElement(nx, ny); const inElement = this.isActionInElement(nx, ny);
// 在元素范围内,执行事件 // 在元素范围内,执行事件
const newEvent: ActionEventMap[T] = { const newEvent: ActionEventMap[T] = {
@ -1130,6 +1157,19 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.cursor = nextValue; this.cursor = nextValue;
return; 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); const ev = this.parseEvent(key);
if (ev) { if (ev) {

View File

@ -1,18 +1,60 @@
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item'; import { ERenderItemEvent, RenderItem } from '../item';
import { Transform } from '../transform'; import { Transform } from '../transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { isNil } from 'lodash-es'; import { clamp, isNil } from 'lodash-es';
import { logger } from '@/core/common/logger';
/* export type CircleParams = [
* Expected usage (this comment needs to be deleted after implementing correctly): cx?: number,
* <rect x={10} y={30} width={50} height={30} fill stroke /> <!-- --> cy?: number,
* <circle x={10} y={50} radius={10} start={Math.PI / 2} end={Math.PI} stroke /> <!-- --> radius?: number,
* <ellipse x={100} y={50} radiusX={10} radiusY={50} strokeAndFill /> <!-- --> start?: number,
* <rect x={100} y={50} width={50} height={30} fill /> <!-- --> end?: number
* <rect x={100} y={50} width={50} height={30} /> <!-- --> ];
* Line BezierCurve QuadraticCurve 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 { export interface ILineProperty {
/** 线宽 */ /** 线宽 */
@ -63,7 +105,7 @@ export abstract class GraphicItemBase
extends RenderItem<EGraphicItemEvent> extends RenderItem<EGraphicItemEvent>
implements Required<ILineProperty> implements Required<ILineProperty>
{ {
mode: number = GraphicMode.Fill; mode: GraphicMode = GraphicMode.Fill;
fill: CanvasStyle = '#fff'; fill: CanvasStyle = '#fff';
stroke: CanvasStyle = '#fff'; stroke: CanvasStyle = '#fff';
lineWidth: number = 2; lineWidth: number = 2;
@ -79,6 +121,9 @@ export abstract class GraphicItemBase
private strokeAndFill: boolean = false; private strokeAndFill: boolean = false;
private propFillSet: boolean = false; private propFillSet: boolean = false;
private cachePath?: Path2D;
protected pathDirty: boolean = false;
/** /**
* *
*/ */
@ -90,7 +135,12 @@ export abstract class GraphicItemBase
): void { ): void {
const ctx = canvas.ctx; const ctx = canvas.ctx;
this.setCanvasState(canvas); 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) { switch (this.mode) {
case GraphicMode.Fill: case GraphicMode.Fill:
ctx.fill(path, this.fillRule); ctx.fill(path, this.fillRule);
@ -111,12 +161,16 @@ export abstract class GraphicItemBase
protected isActionInElement(x: number, y: number): boolean { protected isActionInElement(x: number, y: number): boolean {
const ctx = this.cache.ctx; 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) { switch (this.mode) {
case GraphicMode.Fill: case GraphicMode.Fill:
return ctx.isPointInPath(path, x, y, this.fillRule); return ctx.isPointInPath(path, x, y, this.fillRule);
case GraphicMode.Stroke: case GraphicMode.Stroke:
return ctx.isPointInStroke(path, x, y);
case GraphicMode.FillAndStroke: case GraphicMode.FillAndStroke:
case GraphicMode.StrokeAndFill: case GraphicMode.StrokeAndFill:
return ( return (
@ -124,7 +178,6 @@ export abstract class GraphicItemBase
ctx.isPointInStroke(path, x, y) ctx.isPointInStroke(path, x, y)
); );
} }
return false;
} }
/** /**
@ -272,7 +325,7 @@ export abstract class GraphicItemBase
break; break;
case 'lineDash': case 'lineDash':
if (!this.assertType(nextValue, Array, key)) return; if (!this.assertType(nextValue, Array, key)) return;
this.lineDash = nextValue; this.lineDash = nextValue as number[];
this.update(); this.update();
break; break;
case 'lineDashOffset': case 'lineDashOffset':
@ -301,9 +354,19 @@ export abstract class GraphicItemBase
} }
export class Rect extends 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 { getPath(): Path2D {
const path = new 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; return path;
} }
} }
@ -312,32 +375,13 @@ export class Circle extends GraphicItemBase {
radius: number = 10; radius: number = 10;
start: number = 0; start: number = 0;
end: number = Math.PI * 2; end: number = Math.PI * 2;
anchorX: number = 0.5;
anchorY: number = 0.5;
protected render( getPath(): Path2D {
canvas: MotaOffscreenCanvas2D, const path = new Path2D();
_transform: Transform path.arc(this.radius, this.radius, this.radius, this.start, this.end);
): void { return path;
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;
}
} }
/** /**
@ -347,6 +391,7 @@ export class Circle extends GraphicItemBase {
setRadius(radius: number) { setRadius(radius: number) {
this.radius = radius; this.radius = radius;
this.size(radius * 2, radius * 2); this.size(radius * 2, radius * 2);
this.pathDirty = true;
this.update(); this.update();
} }
@ -358,6 +403,7 @@ export class Circle extends GraphicItemBase {
setAngle(start: number, end: number) { setAngle(start: number, end: number) {
this.start = start; this.start = start;
this.end = end; this.end = end;
this.pathDirty = true;
this.update(); this.update();
} }
@ -381,6 +427,21 @@ export class Circle extends GraphicItemBase {
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return;
this.setAngle(this.start, nextValue); this.setAngle(this.start, nextValue);
return; 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); super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
} }
@ -391,40 +452,21 @@ export class Ellipse extends GraphicItemBase {
radiusY: number = 10; radiusY: number = 10;
start: number = 0; start: number = 0;
end: number = Math.PI * 2; end: number = Math.PI * 2;
anchorX: number = 0.5;
anchorY: number = 0.5;
protected render( getPath(): Path2D {
canvas: MotaOffscreenCanvas2D, const path = new Path2D();
_transform: Transform path.ellipse(
): void { this.radiusX,
const ctx = canvas.ctx; this.radiusY,
this.setCanvasState(canvas);
ctx.beginPath();
ctx.ellipse(
this.x,
this.y,
this.radiusX, this.radiusX,
this.radiusY, this.radiusY,
0, 0,
this.start, this.start,
this.end this.end
); );
return path;
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;
}
} }
/** /**
@ -435,6 +477,8 @@ export class Ellipse extends GraphicItemBase {
setRadius(x: number, y: number) { setRadius(x: number, y: number) {
this.radiusX = x; this.radiusX = x;
this.radiusY = y; this.radiusY = y;
this.size(x, y);
this.pathDirty = true;
this.update(); this.update();
} }
@ -446,6 +490,7 @@ export class Ellipse extends GraphicItemBase {
setAngle(start: number, end: number) { setAngle(start: number, end: number) {
this.start = start; this.start = start;
this.end = end; this.end = end;
this.pathDirty = true;
this.update(); this.update();
} }
@ -473,6 +518,21 @@ export class Ellipse extends GraphicItemBase {
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return;
this.setAngle(this.start, nextValue); this.setAngle(this.start, nextValue);
return; 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); super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
} }
@ -483,17 +543,15 @@ export class Line extends GraphicItemBase {
y1: number = 0; y1: number = 0;
x2: number = 0; x2: number = 0;
y2: number = 0; y2: number = 0;
mode: GraphicMode = GraphicMode.Stroke;
protected render( getPath(): Path2D {
canvas: MotaOffscreenCanvas2D, const path = new Path2D();
_transform: Transform const x = this.x;
): void { const y = this.y;
const ctx = canvas.ctx; path.moveTo(this.x1 - x, this.y1 - y);
this.setCanvasState(canvas); path.lineTo(this.x2 - x, this.y2 - y);
ctx.beginPath(); return path;
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
ctx.stroke();
} }
/** /**
@ -502,6 +560,7 @@ export class Line extends GraphicItemBase {
setPoint1(x: number, y: number) { setPoint1(x: number, y: number) {
this.x1 = x; this.x1 = x;
this.y1 = y; this.y1 = y;
this.fitRect();
this.update(); this.update();
} }
@ -511,9 +570,20 @@ export class Line extends GraphicItemBase {
setPoint2(x: number, y: number) { setPoint2(x: number, y: number) {
this.x2 = x; this.x2 = x;
this.y2 = y; this.y2 = y;
this.fitRect();
this.update(); 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( patchProp(
key: string, key: string,
prevValue: any, prevValue: any,
@ -538,6 +608,11 @@ export class Line extends GraphicItemBase {
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return;
this.setPoint2(this.x2, nextValue); this.setPoint2(this.x2, nextValue);
return; 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); super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
} }
@ -552,24 +627,22 @@ export class BezierCurve extends GraphicItemBase {
cp2y: number = 0; cp2y: number = 0;
ex: number = 0; ex: number = 0;
ey: number = 0; ey: number = 0;
mode: GraphicMode = GraphicMode.Stroke;
protected render( getPath(): Path2D {
canvas: MotaOffscreenCanvas2D, const path = new Path2D();
_transform: Transform const x = this.x;
): void { const y = this.y;
const ctx = canvas.ctx; path.moveTo(this.sx - x, this.sy - y);
this.setCanvasState(canvas); path.bezierCurveTo(
ctx.beginPath(); this.cp1x - x,
ctx.moveTo(this.sx, this.sy); this.cp1y - y,
ctx.bezierCurveTo( this.cp2x - x,
this.cp1x, this.cp2y - y,
this.cp1y, this.ex - x,
this.cp2x, this.ey - y
this.cp2y,
this.ex,
this.ey
); );
ctx.stroke(); return path;
} }
/** /**
@ -578,6 +651,7 @@ export class BezierCurve extends GraphicItemBase {
setStart(x: number, y: number) { setStart(x: number, y: number) {
this.sx = x; this.sx = x;
this.sy = y; this.sy = y;
this.fitRect();
this.update(); this.update();
} }
@ -587,6 +661,7 @@ export class BezierCurve extends GraphicItemBase {
setControl1(x: number, y: number) { setControl1(x: number, y: number) {
this.cp1x = x; this.cp1x = x;
this.cp1y = y; this.cp1y = y;
this.fitRect();
this.update(); this.update();
} }
@ -596,6 +671,7 @@ export class BezierCurve extends GraphicItemBase {
setControl2(x: number, y: number) { setControl2(x: number, y: number) {
this.cp2x = x; this.cp2x = x;
this.cp2y = y; this.cp2y = y;
this.fitRect();
this.update(); this.update();
} }
@ -605,9 +681,20 @@ export class BezierCurve extends GraphicItemBase {
setEnd(x: number, y: number) { setEnd(x: number, y: number) {
this.ex = x; this.ex = x;
this.ey = y; this.ey = y;
this.fitRect();
this.update(); 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( patchProp(
key: string, key: string,
prevValue: any, prevValue: any,
@ -648,6 +735,13 @@ export class BezierCurve extends GraphicItemBase {
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return;
this.setEnd(this.ex, nextValue); this.setEnd(this.ex, nextValue);
return; 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); super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
} }
@ -660,6 +754,21 @@ export class QuadraticCurve extends GraphicItemBase {
cpy: number = 0; cpy: number = 0;
ex: number = 0; ex: number = 0;
ey: 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( protected render(
canvas: MotaOffscreenCanvas2D, canvas: MotaOffscreenCanvas2D,
@ -679,6 +788,7 @@ export class QuadraticCurve extends GraphicItemBase {
setStart(x: number, y: number) { setStart(x: number, y: number) {
this.sx = x; this.sx = x;
this.sy = y; this.sy = y;
this.fitRect();
this.update(); this.update();
} }
@ -688,6 +798,7 @@ export class QuadraticCurve extends GraphicItemBase {
setControl(x: number, y: number) { setControl(x: number, y: number) {
this.cpx = x; this.cpx = x;
this.cpy = y; this.cpy = y;
this.fitRect();
this.update(); this.update();
} }
@ -697,9 +808,20 @@ export class QuadraticCurve extends GraphicItemBase {
setEnd(x: number, y: number) { setEnd(x: number, y: number) {
this.ex = x; this.ex = x;
this.ey = y; this.ey = y;
this.fitRect();
this.update(); 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( patchProp(
key: string, key: string,
prevValue: any, prevValue: any,
@ -732,6 +854,12 @@ export class QuadraticCurve extends GraphicItemBase {
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return;
this.setEnd(this.ex, nextValue); this.setEnd(this.ex, nextValue);
return; 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); super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
} }
@ -741,30 +869,6 @@ export class Path extends GraphicItemBase {
/** 路径 */ /** 路径 */
path: Path2D = new Path2D(); 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) { addPath(path: Path2D) {
this.path.addPath(path); this.path.addPath(path);
this.pathDirty = true;
this.update(); this.update();
} }
@ -792,6 +897,7 @@ export class Path extends GraphicItemBase {
case 'path': case 'path':
if (!this.assertType(nextValue, Path2D, key)) return; if (!this.assertType(nextValue, Path2D, key)) return;
this.path = nextValue; this.path = nextValue;
this.pathDirty = true;
this.update(); this.update();
return; return;
} }
@ -799,90 +905,139 @@ export class Path extends GraphicItemBase {
} }
} }
const enum RectRType { export const enum RectRCorner {
/** 圆角为椭圆 */ TopLeft,
Ellipse, TopRight,
/** 圆角为二次贝塞尔曲线 */ BottomRight,
Quad, BottomLeft
/** 圆角为三次贝塞尔曲线。该模式下,包含两个控制点,一个控制点位于上下矩形边延长线,另一个控制点位于左右矩形边延长线 */
Cubic,
/** 圆角为直线连接 */
Line
} }
export class RectR extends GraphicItemBase { export class RectR extends GraphicItemBase {
/** 矩形路径 */ /** 圆角属性,四元素数组,每个元素是一个二元素数组,表示这个角的半径,顺序为 左上,右上,右下,左下 */
private path: Path2D; readonly corner: [radiusX: number, radiusY: number][] = [
[0, 0],
/** 圆角类型 */ [0, 0],
roundType: RectRType = RectRType.Ellipse; [0, 0],
/** 横向圆角半径 */ [0, 0]
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);
getPath(): Path2D {
const path = new Path2D(); const path = new Path2D();
path.rect(this.x, this.y, this.width, this.height); const { width: w, height: h } = this;
this.path = path; 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 x
* @param y * @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 circle
* @param y cpy
*/ */
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 * @param ellipse
): void { */
const ctx = canvas.ctx; setEllipse(ellipse: RectREllipseParams) {
this.setCanvasState(canvas); const [rx1, ry1, rx2 = 0, ry2 = 0, rx3 = 0, ry3 = 0, rx4 = 0, ry4 = 0] =
switch (this.mode) { ellipse;
case GraphicMode.Fill:
ctx.fill(this.path, this.fillRule); 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; 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; break;
case GraphicMode.FillAndStroke: }
ctx.fill(this.path, this.fillRule); case 6: {
ctx.stroke(this.path); 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; break;
case GraphicMode.StrokeAndFill: }
ctx.stroke(this.path); case 8: {
ctx.fill(this.path, this.fillRule); 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; break;
}
default: {
logger.warn(58, ellipse.length.toString());
}
} }
} }
@ -894,6 +1049,18 @@ export class RectR extends GraphicItemBase {
parentComponent?: ComponentInternalInstance | null parentComponent?: ComponentInternalInstance | null
): void { ): void {
switch (key) { 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); super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
} }

View File

@ -107,10 +107,11 @@ export class Text extends RenderItem<ETextEvent> {
* *
*/ */
calBox() { calBox() {
const { width, fontBoundingBoxAscent } = this.measure(); const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
this.measure();
this.length = width; this.length = width;
this.descent = fontBoundingBoxAscent; this.descent = actualBoundingBoxAscent;
this.size(width, fontBoundingBoxAscent); this.size(width, actualBoundingBoxAscent + actualBoundingBoxDescent);
} }
patchProp( patchProp(
@ -197,11 +198,17 @@ export class Image extends RenderItem<EImageEvent> {
} }
export class Comment extends RenderItem { export class Comment extends RenderItem {
readonly isComment: boolean = true;
constructor(public text: string = '') { constructor(public text: string = '') {
super('static'); super('static', false, false);
this.hide(); this.hide();
} }
getBoundingRect(): DOMRectReadOnly {
return new DOMRectReadOnly(0, 0, 0, 0);
}
protected render( protected render(
_canvas: MotaOffscreenCanvas2D, _canvas: MotaOffscreenCanvas2D,
_transform: Transform _transform: Transform

View File

@ -2,6 +2,7 @@ import { logger } from '../common/logger';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Container } from './container'; import { Container } from './container';
import { import {
ActionEventMap,
ActionType, ActionType,
IActionEvent, IActionEvent,
IWheelEvent, IWheelEvent,
@ -46,6 +47,8 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
/** 用于终止 document 上的监听 */ /** 用于终止 document 上的监听 */
private abort?: AbortController; private abort?: AbortController;
/** 根据捕获行为判断光标样式 */
private targetCursor: string = 'auto';
target!: MotaOffscreenCanvas2D; target!: MotaOffscreenCanvas2D;
@ -108,6 +111,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
ActionType.Move, ActionType.Move,
this.lastMouse this.lastMouse
); );
this.targetCursor = 'auto';
this.captureEvent(ActionType.Move, event); this.captureEvent(ActionType.Move, event);
}); });
canvas.addEventListener('mouseenter', ev => { canvas.addEventListener('mouseenter', ev => {
@ -250,6 +254,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
const id = this.getMouseIdentifier(type, mouse); const id = this.getMouseIdentifier(type, mouse);
const x = event.offsetX / core.domStyle.scale; const x = event.offsetX / core.domStyle.scale;
const y = event.offsetY / core.domStyle.scale; const y = event.offsetY / core.domStyle.scale;
return { return {
target: this, target: this,
identifier: id, identifier: id,
@ -380,6 +385,16 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
return list; 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) { update(_item: RenderItem = this) {
this.cacheDirty = true; this.cacheDirty = true;
} }
@ -450,7 +465,9 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
} }
hoverElement(element: RenderItem): void { hoverElement(element: RenderItem): void {
this.target.canvas.style.cursor = element.cursor; if (element.cursor !== 'auto') {
this.targetCursor = element.cursor;
}
} }
destroy() { destroy() {
@ -463,10 +480,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
private toTagString(item: RenderItem, space: number, deep: number): string { private toTagString(item: RenderItem, space: number, deep: number): string {
const name = item.constructor.name; const name = item.constructor.name;
if (item.children.size === 0) { 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 { } else {
return ( 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('')}` + `${[...item.children].map(v => this.toTagString(v, space, deep + 1)).join('')}` +
`${' '.repeat(deep * space)}</${name}>\n` `${' '.repeat(deep * space)}</${name}>\n`
); );

View File

@ -13,6 +13,7 @@ import {
BezierProps, BezierProps,
CirclesProps, CirclesProps,
CommentProps, CommentProps,
ConatinerCustomProps,
ContainerProps, ContainerProps,
CustomProps, CustomProps,
DamageProps, DamageProps,
@ -84,6 +85,10 @@ declare module 'vue/jsx-runtime' {
export interface IntrinsicElements { export interface IntrinsicElements {
sprite: TagDefine<SpriteProps, ESpriteEvent>; sprite: TagDefine<SpriteProps, ESpriteEvent>;
container: TagDefine<ContainerProps, EContainerEvent>; container: TagDefine<ContainerProps, EContainerEvent>;
'container-custom': TagDefine<
ConatinerCustomProps,
EContainerEvent
>;
shader: TagDefine<ShaderProps, EShaderEvent>; shader: TagDefine<ShaderProps, EShaderEvent>;
text: TagDefine<TextProps, ETextEvent>; text: TagDefine<TextProps, ETextEvent>;
image: TagDefine<ImageProps, EImageEvent>; image: TagDefine<ImageProps, EImageEvent>;

View File

@ -1,7 +1,7 @@
import { logger } from '@/core/common/logger'; import { logger } from '@/core/common/logger';
import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item'; import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item';
import { ElementNamespace, VNodeProps } from 'vue'; import { ElementNamespace, VNodeProps } from 'vue';
import { Container } from '../container'; import { Container, ContainerCustom } from '../container';
import { MotaRenderer } from '../render'; import { MotaRenderer } from '../render';
import { Sprite } from '../sprite'; import { Sprite } from '../sprite';
import { import {
@ -75,8 +75,13 @@ const standardElement = (
return (_0: any, _1: any, props?: any) => { return (_0: any, _1: any, props?: any) => {
if (!props) return new Item('static'); if (!props) return new Item('static');
else { else {
const { type = 'static', cache = true, fall = false } = props; const {
return new Item(type, cache, fall); 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) => { return (_0: any, _1: any, props?: any) => {
if (!props) return new Item('static'); if (!props) return new Item('static');
else { else {
const { type = 'static', cache = false, fall = false } = props; const {
return new Item(type, cache, fall); type = 'static',
cache = false,
fall = false,
nocache = true
} = props;
return new Item(type, cache && !nocache, fall);
} }
}; };
}; };
@ -106,7 +116,7 @@ const enum ElementState {
/** /**
* standardElementFor * standardElementFor
*/ */
const se = ( const _se = (
Item: new ( Item: new (
type: RenderItemPosition, type: RenderItemPosition,
cache?: boolean, cache?: boolean,
@ -124,15 +134,17 @@ const se = (
const { const {
type = position, type = position,
cache = defaultCache, cache = defaultCache,
fall = defautFall fall = defautFall,
nocache = !defaultCache
} = props; } = props;
return new Item(type, cache, fall); return new Item(type, cache && !nocache, fall);
} }
}; };
}; };
// Default elements // Default elements
tagMap.register('container', standardElement(Container)); tagMap.register('container', standardElement(Container));
tagMap.register('container-custom', standardElement(ContainerCustom));
tagMap.register('template', standardElement(Container)); tagMap.register('template', standardElement(Container));
tagMap.register('mota-renderer', (_0, _1, props) => { tagMap.register('mota-renderer', (_0, _1, props) => {
return new MotaRenderer(props?.id); return new MotaRenderer(props?.id);
@ -223,14 +235,14 @@ tagMap.register<EDamageEvent, Damage>('damage', (_0, _1, _props) => {
tagMap.register('animation', (_0, _1, _props) => { tagMap.register('animation', (_0, _1, _props) => {
return new Animate(); return new Animate();
}); });
tagMap.register('g-rect', se(Rect, 'absolute', ElementState.None)); tagMap.register('g-rect', standardElementNoCache(Rect));
tagMap.register('g-circle', se(Circle, 'absolute', ElementState.None)); tagMap.register('g-circle', standardElementNoCache(Circle));
tagMap.register('g-ellipse', se(Ellipse, 'absolute', ElementState.None)); tagMap.register('g-ellipse', standardElementNoCache(Ellipse));
tagMap.register('g-line', se(Line, 'absolute', ElementState.None)); tagMap.register('g-line', standardElementNoCache(Line));
tagMap.register('g-bezier', se(BezierCurve, 'absolute', ElementState.None)); tagMap.register('g-bezier', standardElementNoCache(BezierCurve));
tagMap.register('g-quad', se(QuadraticCurve, 'absolute', ElementState.None)); tagMap.register('g-quad', standardElementNoCache(QuadraticCurve));
tagMap.register('g-path', se(Path, 'absolute', ElementState.None)); tagMap.register('g-path', standardElementNoCache(Path));
tagMap.register('g-rectr', se(RectR, 'absolute', ElementState.None)); tagMap.register('g-rectr', standardElementNoCache(RectR));
tagMap.register('icon', standardElementNoCache(Icon)); tagMap.register('icon', standardElementNoCache(Icon));
tagMap.register('winskin', (_0, _1, props) => { tagMap.register('winskin', (_0, _1, props) => {
if (!props) return new Winskin(core.material.images.images['winskin.png']); if (!props) return new Winskin(core.material.images.images['winskin.png']);

View File

@ -6,8 +6,18 @@ import {
ILayerRenderExtends ILayerRenderExtends
} from '../preset/layer'; } from '../preset/layer';
import type { EnemyCollection } from '@/game/enemy/damage'; import type { EnemyCollection } from '@/game/enemy/damage';
import { ILineProperty } from '../preset/graphics'; import {
import { ElementAnchor, ElementLocator } from '../utils'; BezierParams,
CircleParams,
EllipseParams,
ILineProperty,
LineParams,
QuadParams,
RectRCircleParams,
RectREllipseParams
} from '../preset/graphics';
import { ElementAnchor, ElementLocator, ElementScale } from '../utils';
import { CustomContainerRenderFn } from '../container';
export interface CustomProps { export interface CustomProps {
_item: (props: BaseProps) => RenderItem; _item: (props: BaseProps) => RenderItem;
@ -27,7 +37,10 @@ export interface BaseProps {
hidden?: boolean; hidden?: boolean;
transform?: Transform; transform?: Transform;
type?: RenderItemPosition; type?: RenderItemPosition;
/** 是否启用缓存,用处较少,主要用于一些默认不启用缓存的元素的特殊优化 */
cache?: boolean; cache?: boolean;
/** 是否不启用缓存,优先级大于 cache用处较少主要用于一些特殊优化 */
nocache?: boolean;
fall?: boolean; fall?: boolean;
id?: string; id?: string;
alpha?: number; alpha?: number;
@ -41,6 +54,10 @@ export interface BaseProps {
loc?: ElementLocator; loc?: ElementLocator;
/** 锚点属性,可以填 `[x锚点y锚点]`,是 anchorX, anchorY 的简写属性 */ /** 锚点属性,可以填 `[x锚点y锚点]`,是 anchorX, anchorY 的简写属性 */
anc?: ElementAnchor; anc?: ElementAnchor;
/** 放缩属性,可以填 `[x比例y比例]`,是 transform 的简写属性之一 */
scale?: ElementScale;
/** 旋转属性,单位弧度,是 transform 的简写属性之一 */
rotate?: number;
} }
export interface SpriteProps extends BaseProps { export interface SpriteProps extends BaseProps {
@ -49,6 +66,10 @@ export interface SpriteProps extends BaseProps {
export interface ContainerProps extends BaseProps {} export interface ContainerProps extends BaseProps {}
export interface ConatinerCustomProps extends ContainerProps {
render?: CustomContainerRenderFn;
}
export interface GL2Props extends BaseProps {} export interface GL2Props extends BaseProps {}
export interface ShaderProps extends BaseProps {} export interface ShaderProps extends BaseProps {}
@ -122,6 +143,11 @@ export interface CirclesProps extends GraphicPropsBase {
radius?: number; radius?: number;
start?: number; start?: number;
end?: number; end?: number;
/**
* `[半径,起始角度,终止角度]` radius, start, end
*
*/
circle?: CircleParams;
} }
export interface EllipseProps extends GraphicPropsBase { export interface EllipseProps extends GraphicPropsBase {
@ -129,6 +155,11 @@ export interface EllipseProps extends GraphicPropsBase {
radiusY?: number; radiusY?: number;
start?: number; start?: number;
end?: number; end?: number;
/**
* `[x半径y半径起始角度终止角度]` radiusX, radiusY, start, end
*
*/
ellipse?: EllipseParams;
} }
export interface LineProps extends GraphicPropsBase { export interface LineProps extends GraphicPropsBase {
@ -136,6 +167,8 @@ export interface LineProps extends GraphicPropsBase {
y1?: number; y1?: number;
x2?: number; x2?: number;
y2?: number; y2?: number;
/** 直线属性参数,可以填 `[x1, y1, x2, y2]`,都是必填 */
line?: LineParams;
} }
export interface BezierProps extends GraphicPropsBase { export interface BezierProps extends GraphicPropsBase {
@ -147,6 +180,8 @@ export interface BezierProps extends GraphicPropsBase {
cp2y?: number; cp2y?: number;
ex?: number; ex?: number;
ey?: number; ey?: number;
/** 三次贝塞尔曲线参数,可以填 `[sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey]`,都是必填 */
curve?: BezierParams;
} }
export interface QuadraticProps extends GraphicPropsBase { export interface QuadraticProps extends GraphicPropsBase {
@ -156,6 +191,8 @@ export interface QuadraticProps extends GraphicPropsBase {
cpy?: number; cpy?: number;
ex?: number; ex?: number;
ey?: number; ey?: number;
/** 二次贝塞尔曲线参数,可以填 `[sx, sy, cpx, cpy, ex, ey]`,都是必填 */
curve?: QuadParams;
} }
export interface PathProps extends GraphicPropsBase { export interface PathProps extends GraphicPropsBase {
@ -163,26 +200,23 @@ export interface PathProps extends GraphicPropsBase {
} }
export interface RectRProps extends GraphicPropsBase { export interface RectRProps extends GraphicPropsBase {
/** 圆角半径此参数传入时radiusX 和 radiusY 应保持一致 */ /**
radius: number; * `[r1, r2, r3, r4]`
/** 圆角横向半径 */ * - 1 `r1`
radiusX?: number; * - 2 `r1` `r2`
/** 圆角纵向半径 */ * - 3 `r1` `r2` `r3`
radiusY?: number; * - 4 `r1, r2, r3, r4`
/** 圆角为线模式 */ */
line?: boolean; circle?: RectRCircleParams;
/** 圆角为椭圆模式,默认值 */ /**
ellipse?: boolean; * `[rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4]`
/** 圆角为二次贝塞尔曲线模式 */ *
quad?: boolean; * - 1 `[rx1, ry1]`
/** 圆角为三次贝塞尔曲线模式 */ * - 2 `[rx1, ry1]` `[rx2, ry2]`
cubic?: boolean; * - 3 `[rx1, ry1]` `[rx2, ey2]` `[rx3, ry3]`
/** 控制点此参数传入时cpx 和 cpy 应保持一致 */ * - 4 `[rx1, ry1], [rx2, ry2], [rx3, ry3], [rx4, ry4]`
cp?: number; */
/** 横向控制点 */ ellipse?: RectREllipseParams;
cpx?: number;
/** 纵向控制点 */
cpy?: number;
} }
export interface IconProps extends BaseProps { export interface IconProps extends BaseProps {

View File

@ -7,7 +7,6 @@ import {
import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Transform } from './transform'; import { Transform } from './transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { ActionType, EventProgress, ActionEventMap } from './event';
export interface ESpriteEvent extends ERenderItemEvent {} export interface ESpriteEvent extends ERenderItemEvent {}
@ -43,14 +42,6 @@ export class Sprite<
this.update(this); this.update(this);
} }
protected propagateEvent<T extends ActionType>(
type: T,
_progress: EventProgress,
event: ActionEventMap[T]
): void {
this.parent?.bubbleEvent(type, event);
}
patchProp( patchProp(
key: string, key: string,
prevValue: any, prevValue: any,

View File

@ -29,6 +29,7 @@ export type ElementLocator = [
]; ];
export type ElementAnchor = [x: number, y: number]; export type ElementAnchor = [x: number, y: number];
export type ElementScale = [x: number, y: number];
export function disableViewport() { export function disableViewport() {
const adapter = RenderAdapter.get<FloorViewport>('viewport'); const adapter = RenderAdapter.get<FloorViewport>('viewport');

View File

@ -96,6 +96,11 @@ export class UIController<C extends UIComponent = UIComponent>
() => this.userShowBack.value && this.sysShowBack.value () => this.userShowBack.value && this.sysShowBack.value
); );
/** 当前是否显示 UI */
get active() {
return this.showBack.value;
}
/** 自定义显示模式下的配置信息 */ /** 自定义显示模式下的配置信息 */
private config?: IUICustomConfig<C>; private config?: IUICustomConfig<C>;
/** 是否维持背景 UI */ /** 是否维持背景 UI */

View File

@ -89,6 +89,7 @@
"55": "Unchildable tag '$1' should follow with param.", "55": "Unchildable tag '$1' should follow with param.",
"56": "Method '$1' has been deprecated. Consider using '$2' instead.", "56": "Method '$1' has been deprecated. Consider using '$2' instead.",
"57": "Repeated UI controller on item '$1', new controller will not work.", "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.", "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." "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."
} }

View 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
);

View File

@ -1,10 +1,10 @@
import { import {
computed, computed,
defineComponent, defineComponent,
nextTick,
onMounted, onMounted,
onUnmounted, onUnmounted,
onUpdated, onUpdated,
reactive,
ref, ref,
SlotsType, SlotsType,
VNode, VNode,
@ -13,31 +13,40 @@ import {
import { SetupComponentOptions } from './types'; import { SetupComponentOptions } from './types';
import { import {
Container, Container,
ContainerProps,
ElementLocator, ElementLocator,
RenderItem, RenderItem,
Sprite, Sprite,
SpriteProps Transform
} from '@/core/render'; } from '@/core/render';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; 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 { clamp } from 'lodash-es';
import { IActionEvent, IWheelEvent, MouseType } from '@/core/render/event'; import { IActionEvent, IWheelEvent, MouseType } from '@/core/render/event';
import { transitioned } from '../use';
export const enum ScrollDirection { export const enum ScrollDirection {
Horizontal, Horizontal,
Vertical Vertical
} }
interface ScrollProps { export interface ScrollExpose {
direction: ScrollDirection; /**
*
* @param y
* @param time
*/
scrollTo(y: number, time?: number): void;
}
export interface ScrollProps {
loc: ElementLocator; loc: ElementLocator;
hor?: boolean;
noscroll?: boolean; noscroll?: boolean;
/** /**
* *
* *
*/ */
padHeight?: number; pad?: number;
} }
type ScrollSlots = SlotsType<{ type ScrollSlots = SlotsType<{
@ -45,76 +54,127 @@ type ScrollSlots = SlotsType<{
}>; }>;
const scrollProps = { const scrollProps = {
props: ['direction', 'noscroll'] props: ['hor', 'noscroll', 'loc', 'pad']
} satisfies SetupComponentOptions<ScrollProps, {}, string, ScrollSlots>; } satisfies SetupComponentOptions<ScrollProps, {}, string, ScrollSlots>;
/** 滚动条图示的最短长度 */ /** 滚动条图示的最短长度 */
const SCROLL_MIN_LENGTH = 20; const SCROLL_MIN_LENGTH = 20;
/** 滚动条图示的宽度 */ /** 滚动条图示的宽度 */
const SCROLL_WIDTH = 10; 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>( export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
(props, { slots }) => { (props, { slots, expose }) => {
const scrollProps: SpriteProps = reactive({ /** 滚动条的定位 */
loc: [0, 0, 0, 0] const sp = ref<ElementLocator>([0, 0, 1, 1]);
});
const contentProps: ContainerProps = reactive({
loc: [0, 0, 0, 0]
});
const listenedChild: Set<RenderItem> = new Set(); const listenedChild: Set<RenderItem> = new Set();
const areaMap: Map<RenderItem, [number, number]> = new Map(); const areaMap: Map<RenderItem, [number, number]> = new Map();
const content = ref<Container>(); const content = ref<Container>();
const scroll = ref<Sprite>(); const scroll = ref<Sprite>();
const scrollAlpha = transitioned(0.5, 100, linear())!;
const width = computed(() => props.loc[2] ?? 200); const width = computed(() => props.loc[2] ?? 200);
const height = computed(() => props.loc[3] ?? 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; watch(scrollColor, () => {
let nowScroll = 0; scroll.value?.update();
});
/** 滚动内容的当前位置 */
let contentPos = 0;
/** 滚动条的当前位置 */
let scrollPos = 0;
/** 滚动内容的目标位置 */
let contentTarget = 0;
/** 滚动条的目标位置 */
let scrollTarget = 0;
/** 滚动内容的长度 */
let maxLength = 0; let maxLength = 0;
/** 滚动条的长度 */
let scrollLength = SCROLL_MIN_LENGTH; let scrollLength = SCROLL_MIN_LENGTH;
const transition = new Transition(); const transition = new Transition();
transition.value.scroll = 0; transition.value.scroll = 0;
transition.value.showScroll = 0;
transition.mode(hyper('sin', 'out')).absolute(); transition.mode(hyper('sin', 'out')).absolute();
//#region 滚动操作
transition.ticker.add(() => { transition.ticker.add(() => {
if (transition.value.scroll !== nowScroll) { if (scrollPos !== scrollTarget) {
showScroll = transition.value.scroll; scrollPos = transition.value.scroll;
scroll.value?.update(); content.value?.update();
}
if (contentPos !== contentTarget) {
contentPos = transition.value.showScroll;
checkAllItem();
updatePosition();
content.value?.update(); 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 * @param time
*/ */
const scrollTo = (y: number, time: number = 1) => { const scrollTo = (y: number, time: number = 0) => {
const target = clamp(y, 0, maxLength); if (maxLength < height.value) return;
transition.time(time).transition('scroll', target); const max = maxLength - height.value;
nowScroll = y; 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) => { 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]); areaMap.set(item, [rect.left - width.value, rect.right]);
} else { } else {
areaMap.set(item, [rect.top - height.value, rect.bottom]); areaMap.set(item, [rect.top - height.value, rect.bottom]);
@ -131,31 +191,90 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
return; return;
} }
const [min, max] = area; const [min, max] = area;
if (nowScroll > min - 10 && nowScroll < max + 10) { if (contentPos > min - 10 && contentPos < max + 10) {
item.show(); item.show();
} else { } else {
item.hide(); item.hide();
} }
}; };
/**
*
*/
const checkAllItem = () => {
content.value?.children.forEach(v => checkItem(v));
};
/** /**
* *
*/ */
const onTransform = (item: RenderItem) => { const onTransform = (item: RenderItem) => {
const rect = item.getBoundingRect(); 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); getArea(item, rect);
checkItem(item); 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 = () => { const updateScroll = () => {
if (!content.value) return; if (!content.value || updating) return;
updating = true;
nextTick(() => {
updating = false;
});
let max = 0; let max = 0;
listenedChild.forEach(v => v.off('transform', onTransform)); listenedChild.forEach(v => v.off('transform', onTransform));
listenedChild.clear(); listenedChild.clear();
areaMap.clear(); areaMap.clear();
content.value.children.forEach(v => { content.value.children.forEach(v => {
if (v.isComment) return;
const rect = v.getBoundingRect(); const rect = v.getBoundingRect();
if (props.direction === ScrollDirection.Horizontal) { if (direction.value === ScrollDirection.Horizontal) {
if (rect.right > max) { if (rect.right > max) {
max = rect.right; max = rect.right;
} }
@ -164,81 +283,114 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
max = rect.bottom; max = rect.bottom;
} }
} }
v.on('transform', onTransform); getArea(v, rect);
listenedChild.add(v); if (!listenedChild.has(v)) {
v.on('transform', onTransform);
listenedChild.add(v);
}
checkItem(v);
}); });
maxLength = max + (props.padHeight ?? 0); maxLength = Math.max(max + (props.pad ?? 0), 10);
if (props.direction === ScrollDirection.Horizontal) { updatePosition();
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];
}
scroll.value?.update(); scroll.value?.update();
}; };
watch(() => props.loc, updateScroll);
onUpdated(updateScroll); onUpdated(updateScroll);
onMounted(updateScroll); onMounted(updateScroll);
onUnmounted(() => { onUnmounted(() => {
listenedChild.forEach(v => v.off('transform', onTransform)); listenedChild.forEach(v => v.off('transform', onTransform));
}); });
//#endregion
//#region 渲染滚动
const drawScroll = (canvas: MotaOffscreenCanvas2D) => { const drawScroll = (canvas: MotaOffscreenCanvas2D) => {
if (props.noscroll) return; if (props.noscroll) return;
const ctx = canvas.ctx; const ctx = canvas.ctx;
ctx.lineCap = 'round'; ctx.lineCap = 'round';
ctx.lineWidth = 6; ctx.lineWidth = 3;
ctx.strokeStyle = '#fff'; ctx.strokeStyle = scrollColor.value;
ctx.beginPath(); ctx.beginPath();
if (props.direction === ScrollDirection.Horizontal) { const scroll = transition.value.scroll;
ctx.moveTo(nowScroll + 5, 5); if (direction.value === ScrollDirection.Horizontal) {
ctx.lineTo(nowScroll + scrollLength + 5, 5); ctx.moveTo(scroll + 5, 5);
ctx.lineTo(scroll + scrollLength - 5, 5);
} else { } else {
ctx.moveTo(5, nowScroll + 5); ctx.moveTo(5, scroll + 5);
ctx.lineTo(5, nowScroll + scrollLength + 5); ctx.lineTo(5, scroll + scrollLength - 5);
} }
ctx.stroke(); 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) => { const wheel = (ev: IWheelEvent) => {
if (props.direction === ScrollDirection.Horizontal) { if (direction.value === ScrollDirection.Horizontal) {
if (ev.wheelX !== 0) { if (ev.wheelX !== 0) {
scrollTo(nowScroll + ev.wheelX, 300); wheelScroll(ev.wheelX, width.value / 5);
} else if (ev.wheelY !== 0) { } else if (ev.wheelY !== 0) {
scrollTo(nowScroll + ev.wheelY, 300); wheelScroll(ev.wheelY, width.value / 5);
} }
} else { } else {
scrollTo(nowScroll + ev.wheelY, 300); wheelScroll(ev.wheelY, height.value / 5);
} }
}; };
const getPos = (ev: IActionEvent) => { const getPos = (ev: IActionEvent, absolute: boolean = false) => {
if (props.direction === ScrollDirection.Horizontal) { if (absolute) {
return ev.offsetX; if (direction.value === ScrollDirection.Horizontal) {
return ev.absoluteX;
} else {
return ev.absoluteY;
}
} else { } else {
return ev.offsetY; if (direction.value === ScrollDirection.Horizontal) {
return ev.offsetX;
} else {
return ev.offsetY;
}
} }
}; };
let identifier: number = -1; let identifier = -2;
let lastPos: number = 0; let downPos = 0;
/** 拖动内容时,内容原本所在的位置 */
let contentBefore = 0;
const down = (ev: IActionEvent) => { const down = (ev: IActionEvent) => {
identifier = ev.identifier; identifier = ev.identifier;
lastPos = getPos(ev); downPos = getPos(ev, true);
contentBefore = contentTarget;
}; };
const move = (ev: IActionEvent) => { const move = (ev: IActionEvent) => {
@ -249,24 +401,30 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
} else { } else {
if (ev.buttons & MouseType.Left) { if (ev.buttons & MouseType.Left) {
pos = getPos(ev); pos = getPos(ev);
} else {
return;
} }
} }
const movement = pos - lastPos; const movement = pos - downPos;
scrollTo(nowScroll + movement, 1); scrollTo(contentBefore - movement, 0);
lastPos = pos;
}; };
/** 最初滚动条在哪 */
let scrollBefore = 0; let scrollBefore = 0;
let scrollIdentifier = -1; /** 本次拖动滚动条的操作标识符 */
let scrollIdentifier = -2;
/** 点击滚动条时,点击位置在平行于滚动条方向的位置 */
let scrollDownPos = 0; let scrollDownPos = 0;
/** 是否是点击了滚动条区域中滚动条之外的地方,这样视为类滚轮操作 */
let scrollMutate = false; let scrollMutate = false;
/** 点击滚动条时,点击位置垂直于滚动条方向的位置 */
let scrollPin = 0; let scrollPin = 0;
/** /**
* *
*/ */
const getScrollPin = (ev: IActionEvent) => { const getScrollPin = (ev: IActionEvent) => {
if (props.direction === ScrollDirection.Horizontal) { if (direction.value === ScrollDirection.Horizontal) {
return ev.absoluteY; return ev.absoluteY;
} else { } else {
return ev.absoluteX; return ev.absoluteX;
@ -274,23 +432,25 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
}; };
const downScroll = (ev: IActionEvent) => { const downScroll = (ev: IActionEvent) => {
scrollBefore = nowScroll; scrollBefore = contentTarget;
scrollIdentifier = ev.identifier; scrollIdentifier = ev.identifier;
const pos = getPos(ev); const pos = getPos(ev, true);
// 计算点击在了滚动条的哪个位置 // 计算点击在了滚动条的哪个位置
const sEnd = nowScroll + scrollLength; const offsetPos = getPos(ev);
if (pos >= nowScroll && pos <= sEnd) { const sEnd = scrollPos + scrollLength;
scrollDownPos = pos - nowScroll; if (offsetPos >= scrollPos && offsetPos <= sEnd) {
scrollDownPos = pos - scrollPos;
scrollMutate = false; scrollMutate = false;
scrollPin = getScrollPin(ev); scrollPin = getScrollPin(ev);
} else { } else {
scrollMutate = true; scrollMutate = true;
} }
scrollAlpha.set(0.9);
}; };
const moveScroll = (ev: IActionEvent) => { const moveScroll = (ev: IActionEvent) => {
if (ev.identifier !== scrollIdentifier) return; if (ev.identifier !== scrollIdentifier || scrollMutate) return;
const pos = getPos(ev); const pos = getPos(ev, true);
const scrollPos = pos - scrollDownPos; const scrollPos = pos - scrollDownPos;
let deltaPin = 0; let deltaPin = 0;
let threshold = 0; let threshold = 0;
@ -304,22 +464,34 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
threshold = 100; threshold = 100;
} }
if (deltaPin > threshold) { if (deltaPin > threshold) {
scrollTo(scrollBefore, 1); scrollTo(scrollBefore, 0);
} else { } else {
scrollTo(scrollPos, 1); const pos = (scrollPos / height.value) * maxLength;
scrollTo(pos, 0);
} }
}; };
const upScroll = (ev: IActionEvent) => { const upScroll = (ev: IActionEvent) => {
scrollAlpha.set(0.7);
if (!scrollMutate) return; if (!scrollMutate) return;
const pos = getPos(ev); const pos = getPos(ev);
if (pos < nowScroll) { if (pos < scrollPos) {
scrollTo(pos - 50); scrollTo(contentTarget - 50, 300);
} else { } else {
scrollTo(pos + 50); scrollTo(contentTarget + 50, 300);
} }
}; };
const enter = () => {
scrollAlpha.set(0.7);
};
const leave = () => {
scrollAlpha.set(0.5);
};
//#endregion
onMounted(() => { onMounted(() => {
scroll.value?.root?.on('move', move); scroll.value?.root?.on('move', move);
scroll.value?.root?.on('move', moveScroll); scroll.value?.root?.on('move', moveScroll);
@ -328,20 +500,36 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
onUnmounted(() => { onUnmounted(() => {
scroll.value?.root?.off('move', move); scroll.value?.root?.off('move', move);
scroll.value?.root?.off('move', moveScroll); scroll.value?.root?.off('move', moveScroll);
transition.ticker.destroy();
});
expose<ScrollExpose>({
scrollTo
}); });
return () => { return () => {
return ( return (
<container loc={props.loc} onWheel={wheel}> <container loc={props.loc} onWheel={wheel}>
<container {...contentProps} ref={content} onDown={down}> <container-custom
{slots.default()} loc={[0, 0, props.loc[2], props.loc[3]]}
</container> ref={content}
onDown={down}
render={renderContent}
zIndex={0}
>
{slots.default?.()}
</container-custom>
<sprite <sprite
{...scrollProps} nocache
hidden={props.noscroll}
loc={sp.value}
ref={scroll} ref={scroll}
render={drawScroll} render={drawScroll}
onDown={downScroll} onDown={downScroll}
onUp={upScroll} onUp={upScroll}
zIndex={10}
onEnter={enter}
onLeave={leave}
></sprite> ></sprite>
</container> </container>
); );

View File

@ -147,7 +147,12 @@ const MainScene = defineComponent(() => {
status={rightStatus} status={rightStatus}
></RightStatusBar> ></RightStatusBar>
)} )}
{mainUIController.render()} <container
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
hidden={mainUIController.showBack.value}
>
{mainUIController.render()}
</container>
</container> </container>
); );
}); });

View File

@ -2,6 +2,7 @@ import { GameUI } from '@/core/system';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { SetupComponentOptions } from '../components'; import { SetupComponentOptions } from '../components';
import { ElementLocator } from '@/core/render'; import { ElementLocator } from '@/core/render';
import { Scroll } from '../components/scroll';
export interface ILeftHeroStatus { export interface ILeftHeroStatus {
hp: number; hp: number;
@ -168,6 +169,7 @@ export const RightStatusBar = defineComponent<StatusBarProps<IRightHeroStatus>>(
return ( return (
<container loc={p.loc}> <container loc={p.loc}>
<g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect> <g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect>
<Scroll loc={[0, 0, 180, 100]}></Scroll>
</container> </container>
); );
}; };

View File

@ -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 { export const enum Orientation {
/** 横屏 */ /** 横屏 */
@ -58,3 +66,65 @@ export function onLoaded(hook: () => void) {
hook(); 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);
}

View File

@ -11,7 +11,8 @@ const FSHOST = 'http://127.0.0.1:3000/';
const custom = [ const custom = [
'container', 'image', 'sprite', 'shader', 'text', 'comment', '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/ // https://vitejs.dev/config/