1
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
import { Shape } from '../shape.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
|
||||
export type EasingFunction = (t: number) => number;
|
||||
type UpdateCall = (t: number) => void;
|
||||
type AnimationConfig = {
|
||||
props: UTSJSONObject;
|
||||
duration: number;
|
||||
easing?: EasingFunction;
|
||||
startValues: Map<string, number>;
|
||||
}
|
||||
|
||||
export class Tween {
|
||||
private target: Shape;
|
||||
private startValues: Map<string, number> = new Map();
|
||||
private endValues: Map<string, number> = new Map();
|
||||
private duration: number;
|
||||
private startTime: number = 0;
|
||||
private isPlaying: boolean = false;
|
||||
private easing: EasingFunction;
|
||||
private onUpdateFun: ((progress:number) => void) | null = null;
|
||||
private onCompleteFun: (() => void) | null = null;
|
||||
private canvas: CanvasContext;
|
||||
private parentICanvas: ICanvas;
|
||||
private requestAnimationFrameId = 0;
|
||||
private loop: number = 0; // 循环次数,0表示不循环,-1表示无限循环
|
||||
private currentLoop: number = 0; // 当前已完成的循环次数
|
||||
private yoyo: boolean = false; // 是否开启往返播放
|
||||
private isReverse: boolean = false; // 当前是否为反向播放
|
||||
private animationQueue: AnimationConfig[] = []; // 动画队列
|
||||
private currentAnimationIndex: number = -1; // 当前执行的动画索引
|
||||
private delayDur = 0
|
||||
private delayDurTid = 12
|
||||
constructor(target: Shape, canvas: ICanvas) {
|
||||
this.target = target;
|
||||
this.canvas = canvas.canvas!;
|
||||
this.parentICanvas = canvas;
|
||||
this.duration = 1000;
|
||||
this.easing = Easing.linear as EasingFunction;
|
||||
}
|
||||
|
||||
to(props: UTSJSONObject, duration: number = 1000): Tween {
|
||||
return this.addTo(props, duration);
|
||||
}
|
||||
|
||||
addTo(props: UTSJSONObject, duration: number = 1000, easing: EasingFunction = this.easing): Tween {
|
||||
const startValues = new Map<string, number>();
|
||||
for (const key in props) {
|
||||
let keyValue = props[key];
|
||||
if (typeof keyValue == 'number') {
|
||||
let startValue = this.target.getAttr(key);
|
||||
if(typeof startValue == 'number'){
|
||||
startValues.set(key, startValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.animationQueue.push({
|
||||
props: props,
|
||||
duration: duration,
|
||||
easing: easing,
|
||||
startValues: startValues
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 在队列中插入一个仅用于延时的动画段
|
||||
delay(ms: number): Tween {
|
||||
this.delayDur = ms;
|
||||
return this;
|
||||
}
|
||||
|
||||
clearAnimations(): Tween {
|
||||
this.stop();
|
||||
this.canvas.cancelAnimationFrame(this.requestAnimationFrameId);
|
||||
this.requestAnimationFrameId = 0;
|
||||
this.animationQueue = [];
|
||||
this.currentAnimationIndex = -1;
|
||||
this.startValues.clear();
|
||||
this.endValues.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
start(): Tween {
|
||||
let _this = this;
|
||||
if (this.isPlaying) {
|
||||
return this;
|
||||
}
|
||||
if(this.delayDur>0){
|
||||
_this.delayDurTid = setTimeout(function() {
|
||||
if (_this.requestAnimationFrameId != 0) {
|
||||
_this.canvas.cancelAnimationFrame(_this.requestAnimationFrameId);
|
||||
_this.requestAnimationFrameId = 0;
|
||||
}
|
||||
|
||||
_this.update();
|
||||
}, _this.delayDur);
|
||||
}else{
|
||||
if (this.requestAnimationFrameId != 0) {
|
||||
this.canvas.cancelAnimationFrame(this.requestAnimationFrameId);
|
||||
this.requestAnimationFrameId = 0;
|
||||
}
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
|
||||
this.isPlaying = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
stop(): Tween {
|
||||
clearTimeout(this.delayDurTid)
|
||||
this.isPlaying = false;
|
||||
if (this.requestAnimationFrameId != 0) {
|
||||
this.canvas.cancelAnimationFrame(this.requestAnimationFrameId);
|
||||
this.requestAnimationFrameId = 0;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
const selftThis = this;
|
||||
let updateSelf = null as null|UpdateCall;
|
||||
selftThis.currentLoop = 1;
|
||||
selftThis.isReverse = false;
|
||||
// 开始执行队列中的第一个动画
|
||||
if (selftThis.currentAnimationIndex == -1 && selftThis.animationQueue.length > 0) {
|
||||
selftThis.currentAnimationIndex = 0;
|
||||
const currentAnimation = selftThis.animationQueue[selftThis.currentAnimationIndex];
|
||||
selftThis.duration = currentAnimation.duration;
|
||||
selftThis.easing = currentAnimation.easing ?? selftThis.easing;
|
||||
selftThis.startValues.clear();
|
||||
selftThis.endValues.clear();
|
||||
for (const key in currentAnimation.props) {
|
||||
let keyValue = currentAnimation.props[key];
|
||||
if (typeof keyValue == 'number') {
|
||||
let startValue = currentAnimation.startValues?.get(key);
|
||||
if(typeof startValue == 'number'){
|
||||
selftThis.startValues.set(key, startValue!);
|
||||
selftThis.endValues.set(key, keyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSelf = (time:number)=>{
|
||||
|
||||
if (!selftThis.isPlaying) return;
|
||||
const currentTime = time;
|
||||
let elapsed = currentTime - selftThis.startTime;
|
||||
|
||||
if (elapsed > selftThis.duration) {
|
||||
|
||||
if (selftThis.yoyo && !selftThis.isReverse && (selftThis.currentLoop < selftThis.loop||selftThis.loop==-1)) {
|
||||
// 开启yoyo且当前为正向播放,切换为反向播放
|
||||
selftThis.isReverse = true;
|
||||
|
||||
if (selftThis.isPlaying) {
|
||||
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
|
||||
selftThis.startTime = t;
|
||||
updateSelf!(t)
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if(this.yoyo){
|
||||
selftThis.isReverse = false;
|
||||
}
|
||||
// 检查是否还有下一个动画
|
||||
if (selftThis.currentAnimationIndex < selftThis.animationQueue.length - 1) {
|
||||
selftThis.currentAnimationIndex++;
|
||||
|
||||
const nextAnimation = selftThis.animationQueue[selftThis.currentAnimationIndex];
|
||||
selftThis.duration = nextAnimation.duration;
|
||||
selftThis.easing = nextAnimation.easing ?? selftThis.easing;
|
||||
selftThis.startValues.clear();
|
||||
selftThis.endValues.clear();
|
||||
for (const key in nextAnimation.props) {
|
||||
let keyValue = nextAnimation.props[key];
|
||||
if (typeof keyValue == 'number') {
|
||||
let startValue = selftThis.target.getAttr(key);
|
||||
if(typeof startValue == 'number'){
|
||||
selftThis.startValues.set(key, nextAnimation.startValues.get(key)!);
|
||||
selftThis.endValues.set(key, keyValue);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
|
||||
selftThis.startTime = t;
|
||||
updateSelf!(t);
|
||||
});
|
||||
return;
|
||||
} else if (selftThis.loop == -1 || selftThis.currentLoop < selftThis.loop) {
|
||||
// 重置动画索引,开始新一轮循环
|
||||
selftThis.currentAnimationIndex = 0;
|
||||
selftThis.currentLoop++;
|
||||
if(selftThis.animationQueue.length>0){
|
||||
const nextAnimation = selftThis.animationQueue[selftThis.currentAnimationIndex];
|
||||
selftThis.duration = nextAnimation.duration;
|
||||
selftThis.easing = nextAnimation.easing ?? selftThis.easing;
|
||||
selftThis.startValues.clear();
|
||||
selftThis.endValues.clear();
|
||||
|
||||
for (const key in nextAnimation.props) {
|
||||
let keyValue = nextAnimation.props[key];
|
||||
if (typeof keyValue == 'number') {
|
||||
let startValue = selftThis.target.getAttr(key);
|
||||
if(typeof startValue == 'number'){
|
||||
selftThis.startValues.set(key, nextAnimation.startValues.get(key)!);
|
||||
selftThis.endValues.set(key, keyValue);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
|
||||
selftThis.startTime = t;
|
||||
updateSelf!(t);
|
||||
});
|
||||
}else{
|
||||
selftThis.duration = currentTime;
|
||||
selftThis.easing = selftThis.easing;
|
||||
selftThis.startValues.clear();
|
||||
selftThis.endValues.clear();
|
||||
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
|
||||
selftThis.startTime = t;
|
||||
updateSelf!(t);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
selftThis.isPlaying = false;
|
||||
selftThis.currentAnimationIndex = -1; // 重置动画索引
|
||||
selftThis.canvas.cancelAnimationFrame(selftThis.requestAnimationFrameId);
|
||||
selftThis.requestAnimationFrameId = 0;
|
||||
selftThis.animationQueue = [];
|
||||
selftThis.startValues.clear();
|
||||
selftThis.endValues.clear();
|
||||
}
|
||||
}
|
||||
|
||||
let progress = selftThis.easing(elapsed / selftThis.duration);
|
||||
if (selftThis.isReverse) {
|
||||
progress = 1 - progress;
|
||||
}
|
||||
|
||||
selftThis.startValues.forEach((startValue, key) => {
|
||||
const endValue = selftThis.endValues.get(key)!;
|
||||
const value = startValue + (endValue - startValue) * progress;
|
||||
selftThis.target.setAttr(key,value);
|
||||
});
|
||||
|
||||
selftThis.target.needsUpdate = true;
|
||||
|
||||
if (selftThis.onUpdateFun!=null) {
|
||||
selftThis.onUpdateFun!(progress);
|
||||
}
|
||||
selftThis.parentICanvas.update();
|
||||
if (!selftThis.isPlaying && selftThis.onCompleteFun!=null) {
|
||||
selftThis.onCompleteFun!();
|
||||
return;
|
||||
}
|
||||
if (selftThis.isPlaying) {
|
||||
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => updateSelf!(t));
|
||||
}
|
||||
}
|
||||
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
|
||||
selftThis.startTime = t;
|
||||
updateSelf!(t);
|
||||
});
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.stop();
|
||||
this.animationQueue = [];
|
||||
this.currentAnimationIndex = -1;
|
||||
this.startValues.clear();
|
||||
this.endValues.clear();
|
||||
this.onUpdateFun = null;
|
||||
this.onCompleteFun = null;
|
||||
this.loop = 0;
|
||||
this.yoyo = false;
|
||||
|
||||
}
|
||||
|
||||
setEasing(easingFunction: EasingFunction): Tween {
|
||||
this.easing = easingFunction;
|
||||
return this;
|
||||
}
|
||||
|
||||
onUpdate(callback: (progress:number) => void): Tween {
|
||||
this.onUpdateFun = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
onComplete(callback: () => void): Tween {
|
||||
this.onCompleteFun = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
setLoop(count: number): Tween {
|
||||
this.loop = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
setYoyo(value: boolean): Tween {
|
||||
this.yoyo = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const Easing = {
|
||||
linear: (t: number): number => t,
|
||||
|
||||
easeInQuad: (t: number): number => t * t,
|
||||
|
||||
easeOutQuad: (t: number): number => t * (2 - t),
|
||||
|
||||
easeInOutQuad: (t: number): number => {
|
||||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
},
|
||||
|
||||
easeInCubic: (t: number): number => t * t * t,
|
||||
|
||||
easeOutCubic: (t: number): number => (t-1) * t * t + 1,
|
||||
|
||||
easeInOutCubic: (t: number): number => {
|
||||
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
|
||||
},
|
||||
|
||||
easeInElastic: (t: number): number => {
|
||||
const c4 = (2 * Math.PI) / 3;
|
||||
return t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
|
||||
},
|
||||
|
||||
easeOutElastic: (t: number): number => {
|
||||
const c4 = (2 * Math.PI) / 3;
|
||||
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
||||
},
|
||||
|
||||
easeInOutElastic: (t: number): number => {
|
||||
const c5 = (2 * Math.PI) / 4.5;
|
||||
return t === 0 ? 0 : t === 1 ? 1 : t < 0.5
|
||||
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
|
||||
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { CanvasRotateCenter, ShapeSetAttrType, IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
export class IArc extends Shape {
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
this.radius = config?.radius ?? 30;
|
||||
this.startAngle = config?.startAngle ?? 0;
|
||||
this.endAngle = config?.endAngle ?? 90;
|
||||
this.type = "IArc"
|
||||
}
|
||||
|
||||
setRadius(value : number) : IArc {
|
||||
this.radius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
// 计算精确的弧段/扇形包围盒
|
||||
const centerX = this.x;
|
||||
const centerY = this.y;
|
||||
const r = Math.max(0, this.radius);
|
||||
const d2r = Math.PI / 180;
|
||||
|
||||
// 角度标准化到 [0, 360)
|
||||
const normalize = (deg:number):number => {
|
||||
let a = deg % 360;
|
||||
if (a < 0) a += 360;
|
||||
return a;
|
||||
};
|
||||
|
||||
let start = normalize(this.startAngle);
|
||||
let end = normalize(this.endAngle);
|
||||
if (end < start) end += 360; // 保证 end >= start(顺时针绘制)
|
||||
|
||||
const withinSweep = (deg:number):boolean => {
|
||||
let a = normalize(deg);
|
||||
if (a < start) a += 360;
|
||||
return a >= start && a <= end;
|
||||
};
|
||||
|
||||
const candidatesX:number[] = [];
|
||||
const candidatesY:number[] = [];
|
||||
|
||||
// 起止点
|
||||
const sx = centerX + r * Math.cos(start * d2r);
|
||||
const sy = centerY + r * Math.sin(start * d2r);
|
||||
const ex = centerX + r * Math.cos(end * d2r);
|
||||
const ey = centerY + r * Math.sin(end * d2r);
|
||||
candidatesX.push(sx, ex);
|
||||
candidatesY.push(sy, ey);
|
||||
|
||||
// 若为扇形(有填充),中心点也参与包围盒
|
||||
const isFilled = (this.fill != "" || this.fillGradient.length > 0);
|
||||
if (isFilled) {
|
||||
candidatesX.push(centerX);
|
||||
candidatesY.push(centerY);
|
||||
}
|
||||
|
||||
// 检查跨越极值角(0, 90, 180, 270)
|
||||
const extrema = [0, 90, 180, 270];
|
||||
for (const deg of extrema) {
|
||||
if (withinSweep(deg)) {
|
||||
const rad = deg * d2r;
|
||||
candidatesX.push(centerX + r * Math.cos(rad));
|
||||
candidatesY.push(centerY + r * Math.sin(rad));
|
||||
}
|
||||
}
|
||||
|
||||
let minX = candidatesX.length>0?candidatesX[0]:centerX;
|
||||
let maxX = candidatesX.length>0?candidatesX[0]:centerX;
|
||||
let minY = candidatesY.length>0?candidatesY[0]:centerY;
|
||||
let maxY = candidatesY.length>0?candidatesY[0]:centerY;
|
||||
for (let i=1;i<candidatesX.length;i++) {
|
||||
const vx = candidatesX[i];
|
||||
if (!isNaN(vx)) {
|
||||
if (vx < minX) minX = vx;
|
||||
if (vx > maxX) maxX = vx;
|
||||
}
|
||||
}
|
||||
for (let i=1;i<candidatesY.length;i++) {
|
||||
const vy = candidatesY[i];
|
||||
if (!isNaN(vy)) {
|
||||
if (vy < minY) minY = vy;
|
||||
if (vy > maxY) maxY = vy;
|
||||
}
|
||||
}
|
||||
|
||||
// 描边扩张
|
||||
const hasStroke = (this.stroke != "" || this.strokeGradient.length > 0);
|
||||
const pad = hasStroke ? this.strokeWidth : 0;
|
||||
minX -= pad;
|
||||
minY -= pad;
|
||||
maxX += pad;
|
||||
maxY += pad;
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: Math.max(0, maxX - minX),
|
||||
height: Math.max(0, maxY - minY)
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override setWidth(value : number) : IArc {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.radius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override setHeight(value : number) : IArc {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.radius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setStartAngle(angle:number):IArc{
|
||||
this.startAngle = angle;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
setEndAngle(angle:number):IArc{
|
||||
this.endAngle = angle;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
ctx.beginPath();
|
||||
// 将角度转换为弧度
|
||||
const startRad = this.startAngle * Math.PI / 180;
|
||||
const endRad = this.endAngle * Math.PI / 180;
|
||||
// 如果有填充色,先移动到圆心,绘制扇形
|
||||
if (this.fill != ""||this.fillGradient.length>0) {
|
||||
ctx.moveTo(this.x, this.y);
|
||||
}
|
||||
// 绘制圆弧
|
||||
ctx.arc(this.x, this.y, this.radius, startRad, endRad, false);
|
||||
|
||||
if (this.fill != ""||this.fillGradient.length>0) {
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.stroke != ""||this.strokeGradient.length>0) {
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
override isPointInPath(x: number, y: number, shapeId: string): boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
const realX = x - (this.offsetX) - this.x;
|
||||
const realY = y - (this.offsetY) - this.y;
|
||||
|
||||
// 计算点到圆心的距离
|
||||
const dx = realX;
|
||||
const dy = realY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 计算点相对于圆心的角度(弧度)
|
||||
const angle = Math.atan2(dy, dx);
|
||||
// 将角度转换为0-360度范围
|
||||
let degrees = angle * 180 / Math.PI;
|
||||
if (degrees < 0) degrees += 360;
|
||||
|
||||
// 将起始角度和结束角度标准化到0-360度范围
|
||||
let start = this.startAngle % 360;
|
||||
if (start < 0) start += 360;
|
||||
let end = this.endAngle % 360;
|
||||
if (end < 0) end += 360;
|
||||
// 确保end大于start
|
||||
if (end < start) end += 360;
|
||||
|
||||
// 检查点的角度是否在弧的范围内
|
||||
const inAngle = degrees >= start && degrees <= end;
|
||||
|
||||
if (this.fill !== "") {
|
||||
// 填充模式:检查是否在扇形内
|
||||
return inAngle && distance <= this.radius;
|
||||
} else if (this.stroke !== "") {
|
||||
// 非填充模式:检查是否在圆弧线附近
|
||||
// 考虑整个线宽作为检测范围
|
||||
const tolerance = this.strokeWidth;
|
||||
const distanceFromArc = Math.abs(distance - this.radius);
|
||||
return inAngle && distanceFromArc <= tolerance;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
|
||||
export class ICircle extends Shape {
|
||||
override type = 'ICircle'
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
this.radius = config?.radius ?? 30;
|
||||
}
|
||||
|
||||
setRadius(value : number) : ICircle {
|
||||
this.radius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
const r = Math.max(0, this.radius);
|
||||
const pad = (this.stroke != "" ? this.strokeWidth / 2 : 0);
|
||||
const d = r * 2 + pad * 2;
|
||||
return {
|
||||
x: this.x - r - pad,
|
||||
y: this.y - r - pad,
|
||||
width: d,
|
||||
height: d
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override setWidth(value : number) : ICircle {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.radius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override setHeight(value : number) : ICircle {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.radius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
|
||||
ctx.closePath();
|
||||
if (this.fill != ""||this.fillGradient.length>0) {
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override isPointInPath(x: number, y: number, shapeId: string): boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
|
||||
// 计算点击位置相对于圆心的实际坐标
|
||||
let realX = x - this.offsetX - this.x;
|
||||
let realY = y - this.offsetY - this.y;
|
||||
|
||||
// 如果有旋转,需要将坐标转换回未旋转状态
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = this.width/2;
|
||||
centerY = this.height/2;
|
||||
break;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
realX = centerX + dx * cos - dy * sin;
|
||||
realY = centerY + dx * sin + dy * cos;
|
||||
}
|
||||
|
||||
// 计算点到圆心的距离
|
||||
const distance = Math.sqrt(realX * realX + realY * realY);
|
||||
|
||||
// 考虑缩放因素
|
||||
const scaledRadius = this.radius * Math.min(this.scaleX, this.scaleY);
|
||||
|
||||
// 如果只有描边模式,检查点是否在圆形线条附近
|
||||
if (this.stroke != "" && this.fill == ""&&this.fillGradient.length==0) {
|
||||
const strokeWidth = this.strokeWidth / 2;
|
||||
return Math.abs(distance - scaledRadius) <= strokeWidth;
|
||||
}
|
||||
|
||||
// 如果同时有描边和填充,或者只有填充
|
||||
if (this.fill != ""||this.fillGradient.length>0) {
|
||||
// 如果有描边,扩大检测范围到描边外缘
|
||||
if (this.stroke != "") {
|
||||
return distance <= (scaledRadius + this.strokeWidth / 2);
|
||||
}
|
||||
|
||||
// 只有填充时,检查点是否在圆内
|
||||
return distance <= scaledRadius;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
|
||||
export class IEllipse extends Shape {
|
||||
override type = 'IEllipse'
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
this.radiusX = config?.radiusX ?? 80;
|
||||
this.radiusY = config?.radiusY ?? 20;
|
||||
}
|
||||
|
||||
setRadiusX(value : number) : IEllipse {
|
||||
this.radiusX = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setRadiusY(value : number) : IEllipse {
|
||||
this.radiusY = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
const rx = Math.max(0, this.radiusX);
|
||||
const ry = Math.max(0, this.radiusY);
|
||||
const pad = (this.stroke != "" ? this.strokeWidth / 2 : 0);
|
||||
return {
|
||||
x: this.x - rx - pad,
|
||||
y: this.y - ry - pad,
|
||||
width: rx * 2 + pad * 2,
|
||||
height: ry * 2 + pad * 2
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override setWidth(value : number) : IEllipse {
|
||||
this.width = value;
|
||||
this.radiusX = value / 2;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override setHeight(value : number) : IEllipse {
|
||||
this.height = value;
|
||||
this.radiusY = value / 2;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(this.x, this.y, this.radiusX, this.radiusY, 0, 0, Math.PI * 2, false);
|
||||
ctx.closePath();
|
||||
if (this.fill != "") {
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override isPointInPath(x: number, y: number, shapeId: string): boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
|
||||
// 计算点击位置相对于椭圆中心的实际坐标
|
||||
let realX = x - this.offsetX - this.x;
|
||||
let realY = y - this.offsetY - this.y;
|
||||
|
||||
// 如果有旋转,需要将坐标转换回未旋转状态
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = this.width/2;
|
||||
centerY = this.height/2;
|
||||
break;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
realX = centerX + dx * cos - dy * sin;
|
||||
realY = centerY + dx * sin + dy * cos;
|
||||
}
|
||||
|
||||
// 考虑缩放因素
|
||||
const scaledRadiusX = this.radiusX * Math.abs(this.scaleX);
|
||||
const scaledRadiusY = this.radiusY * Math.abs(this.scaleY);
|
||||
|
||||
// 计算点是否在椭圆内(标准椭圆方程)
|
||||
const normalizedX = realX / scaledRadiusX;
|
||||
const normalizedY = realY / scaledRadiusY;
|
||||
const distance = normalizedX * normalizedX + normalizedY * normalizedY;
|
||||
|
||||
// 如果只有描边模式,检查点是否在椭圆线条附近
|
||||
if (this.stroke != "" && this.fill == "") {
|
||||
const strokeWidth = this.strokeWidth / 2;
|
||||
const outerDistance = Math.sqrt(distance);
|
||||
return Math.abs(outerDistance - 1) * Math.min(scaledRadiusX, scaledRadiusY) <= strokeWidth;
|
||||
}
|
||||
|
||||
// 如果同时有描边和填充,或者只有填充
|
||||
if (this.fill != "") {
|
||||
// 如果有描边,扩大检测范围到描边外缘
|
||||
if (this.stroke != "") {
|
||||
const strokeOffset = this.strokeWidth / (2 * Math.min(scaledRadiusX, scaledRadiusY));
|
||||
return distance <= (1 + strokeOffset) * (1 + strokeOffset);
|
||||
}
|
||||
// 只有填充时,检查点是否在椭圆内
|
||||
return distance <= 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { IShapeBoundRect, IShapeOptional, ShapeSetAttrType } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
|
||||
/**
|
||||
* 图片
|
||||
* @description 注意创建添加完图片后。要通过方法setSrc(xx)来设置加载图片
|
||||
* @version 1.0.0
|
||||
* @date 2025/2/6
|
||||
* @copright 不允许外发给别人,只可在tmui4x是使用
|
||||
*/
|
||||
export class ImageShape extends Shape {
|
||||
override type = 'ImageShape'
|
||||
private image : any | null = null;
|
||||
loaded : boolean = false;
|
||||
private srcWidth : number = 0;
|
||||
private srcHeight : number = 0;
|
||||
private cropX : number = 0;
|
||||
private cropY : number = 0;
|
||||
private cropWidth : number = 0;
|
||||
private cropHeight : number = 0;
|
||||
private useCrop : boolean = false;
|
||||
private canvasContext : CanvasContext;
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
this.canvasContext = canvas.canvas!
|
||||
this.type = 'ImageShape';
|
||||
if(this.src!=''){
|
||||
this.setSrc(this.src)
|
||||
}
|
||||
}
|
||||
|
||||
setSrc(src : string) : ImageShape {
|
||||
if (!this.visible) return this;
|
||||
this.src = src;
|
||||
this.loaded = false;
|
||||
if (this.src != '') {
|
||||
this.loadImage();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private loadImage() : void {
|
||||
if (this.loaded) return;
|
||||
const image = this.canvasContext.createImage();
|
||||
image.src = this.src;
|
||||
let _this = this;
|
||||
image.onload = () => {
|
||||
_this.image = image;
|
||||
_this.loaded = true;
|
||||
if (_this.width == 0) _this.width = image.width;
|
||||
if (_this.height == 0) _this.height = image.height;
|
||||
_this.srcWidth = image.width;
|
||||
_this.srcHeight = image.height;
|
||||
_this.needsUpdate = true;
|
||||
_this.canvas.update()
|
||||
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
crop(x : number, y : number, width : number, height : number) : ImageShape {
|
||||
this.cropX = x;
|
||||
this.cropY = y;
|
||||
this.cropWidth = width;
|
||||
this.cropHeight = height;
|
||||
this.useCrop = true;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
resetCrop() : ImageShape {
|
||||
this.useCrop = false;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) : void {
|
||||
|
||||
if (!this.loaded || this.image == null) return;
|
||||
super.draw(ctx);
|
||||
|
||||
// #ifdef APP||WEB
|
||||
if (this.useCrop) {
|
||||
ctx.drawImage(
|
||||
this.image! as Image,
|
||||
this.cropX,
|
||||
this.cropY,
|
||||
this.cropWidth,
|
||||
this.cropHeight,
|
||||
this.x,
|
||||
this.y,
|
||||
this.width,
|
||||
this.height
|
||||
);
|
||||
} else {
|
||||
ctx.drawImage(
|
||||
this.image! as Image,
|
||||
this.x,
|
||||
this.y,
|
||||
this.width,
|
||||
this.height
|
||||
);
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
if (this.useCrop) {
|
||||
ctx.drawImage(
|
||||
this.image!,
|
||||
this.cropX,
|
||||
this.cropY,
|
||||
this.cropWidth,
|
||||
this.cropHeight,
|
||||
this.x,
|
||||
this.y,
|
||||
this.width,
|
||||
this.height
|
||||
);
|
||||
} else {
|
||||
ctx.drawImage(
|
||||
this.image!,
|
||||
this.x,
|
||||
this.y,
|
||||
this.width,
|
||||
this.height
|
||||
);
|
||||
}
|
||||
// #endif
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
|
||||
if (!this.visible || !this.loaded || (shapeId != "" && shapeId != this.id)) return false;
|
||||
|
||||
const realX = x - this.x - this.offsetX;
|
||||
const realY = y - this.y - this.offsetY;
|
||||
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = this.width/2;
|
||||
centerY = this.height/2;
|
||||
break;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
const rotatedX = centerX + dx * cos - dy * sin;
|
||||
const rotatedY = centerY + dx * sin + dy * cos;
|
||||
return rotatedX >= 0 && rotatedX <= this.width * this.scaleX && rotatedY >= 0 && rotatedY <= this.height * this.scaleY;
|
||||
}
|
||||
|
||||
return realX >= 0 && realX <= this.width * this.scaleX && realY >= 0 && realY <= this.height * this.scaleY;
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override getAttr(key: string): any | null {
|
||||
const superValue = super.getAttr(key);
|
||||
if(superValue!=null) return superValue;
|
||||
switch (key) {
|
||||
case 'src': return this.src;
|
||||
case 'cropX': return this.cropX;
|
||||
case 'cropY': return this.cropY;
|
||||
case 'cropWidth': return this.cropWidth;
|
||||
case 'cropHeight': return this.cropHeight;
|
||||
case 'useCrop': return this.useCrop;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import { Shape } from '@/uni_modules/tmx-ui/core/canvas/lib/shape.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
type LayoutDirection = 'horizontal'|'vertical'
|
||||
type LayoutAnchor = 'top'|'bottom'
|
||||
type LayoutMode = 'left'|'right'|'between'|'center';//左对齐,右对齐,两边对齐均分,中间对齐
|
||||
type CurSizeType = {width:number,height:number}
|
||||
type RoysType = {items:Shape[], sizes:CurSizeType[], rowWidth:number, rowHeight:number}
|
||||
type CoysType = {items:Shape[], sizes:CurSizeType[], colWidth:number, colHeight:number}
|
||||
export class ILayout {
|
||||
x:number = 0
|
||||
y:number = 0
|
||||
width:number = 0;
|
||||
height:number = 0;
|
||||
shapeList:Shape[] = [];
|
||||
colSpace:number = 10
|
||||
rowSpace:number = 10
|
||||
direction:LayoutDirection = 'horizontal'
|
||||
mode:LayoutMode = 'left'
|
||||
anchor:LayoutAnchor = 'top';//是以顶开始往排,还是以容器底往上排
|
||||
// 自动断行
|
||||
wrap:boolean = false
|
||||
icanvas:ICanvas;
|
||||
/**
|
||||
* 创建布局容器
|
||||
* @param ic 画布实例(默认用于初始化容器宽高)
|
||||
*/
|
||||
constructor(ic:ICanvas){
|
||||
this.icanvas = ic;
|
||||
this.width = ic.width
|
||||
this.height = ic.height
|
||||
}
|
||||
/**
|
||||
* 添加需要参与布局的元素(按 id 去重)
|
||||
* @param shapes 形状列表
|
||||
*/
|
||||
addShape(shapes:Shape[]){
|
||||
let ids = this.shapeList.map((el:Shape):string => el.id)
|
||||
let realShapes = shapes.filter((el:Shape):boolean => !ids.includes(el.id))
|
||||
this.shapeList.push(...realShapes)
|
||||
}
|
||||
/**
|
||||
* 设置容器 X 偏移
|
||||
* @param val X 坐标
|
||||
*/
|
||||
setsX(val:number){
|
||||
this.x = val
|
||||
}
|
||||
/**
|
||||
* 设置容器 Y 偏移
|
||||
* @param val Y 坐标
|
||||
*/
|
||||
setsY(val:number){
|
||||
this.y = val
|
||||
}
|
||||
/**
|
||||
* 设置容器位置
|
||||
* @param x X 坐标
|
||||
* @param y Y 坐标
|
||||
*/
|
||||
setsPosition(x:number,y:number){
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
/**
|
||||
* 设置容器尺寸
|
||||
* @param w 宽度
|
||||
* @param h 高度
|
||||
*/
|
||||
setsSize(w:number,h:number){
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
/**
|
||||
* 设置容器宽度
|
||||
* @param w 宽度
|
||||
*/
|
||||
setsWidth(w:number){
|
||||
this.width = w;
|
||||
}
|
||||
/**
|
||||
* 设置容器高度
|
||||
* @param h 高度
|
||||
*/
|
||||
setsHeight(h:number){
|
||||
this.height = h;
|
||||
}
|
||||
/**
|
||||
* 设置列间距(横向相邻元素间距)
|
||||
* @param val 间距像素
|
||||
*/
|
||||
setsSpaceCol(val:number){
|
||||
this.colSpace = val;
|
||||
}
|
||||
/**
|
||||
* 设置行间距(纵向相邻元素间距)
|
||||
* @param val 间距像素
|
||||
*/
|
||||
setsSpaceRow(val:number){
|
||||
this.rowSpace = val
|
||||
}
|
||||
/**
|
||||
* 同时设置列/行间距
|
||||
* @param col 列间距
|
||||
* @param row 行间距
|
||||
*/
|
||||
setsSpace(col:number,row:number){
|
||||
this.colSpace = col;
|
||||
this.rowSpace = row
|
||||
}
|
||||
/**
|
||||
* 设置主轴方向
|
||||
* @param val 'horizontal' | 'vertical'
|
||||
*/
|
||||
setsLayoutDirection(val:LayoutDirection){
|
||||
this.direction = val;
|
||||
}
|
||||
/**
|
||||
* 设置主轴对齐模式
|
||||
* @param val 'left' | 'right' | 'between' | 'center'
|
||||
*/
|
||||
setsMode(val:LayoutMode){
|
||||
this.mode = val;
|
||||
}
|
||||
/**
|
||||
* 设置交叉轴锚点(顶/底对齐)
|
||||
* @param val 'top' | 'bottom'
|
||||
*/
|
||||
setsAnchor(val:LayoutAnchor){
|
||||
this.anchor = val;
|
||||
}
|
||||
/**
|
||||
* 设置是否自动换行/换列
|
||||
* @param val true 开启
|
||||
*/
|
||||
setsWrap(val:boolean){
|
||||
this.wrap = val;
|
||||
}
|
||||
/**
|
||||
* 执行布局:根据 direction / mode / anchor / wrap / spacing 将 shapeList 定位
|
||||
*/
|
||||
render(){
|
||||
const items = this.shapeList.filter((s:Shape)=> s.visible);
|
||||
if(items.length==0) return;
|
||||
if(this.direction=='horizontal'){
|
||||
this.layoutHorizontal(items);
|
||||
}else{
|
||||
this.layoutVertical(items);
|
||||
}
|
||||
items.forEach((el) => {
|
||||
el.needsUpdate = true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素参与布局的尺寸(来自元素 getBoundRect)
|
||||
* @param shape 形状
|
||||
* @returns width/height
|
||||
*/
|
||||
private getItemSize(shape:Shape):CurSizeType{
|
||||
const rect = shape.getBoundRect();
|
||||
const w = Math.max(0, rect.width);
|
||||
const h = Math.max(0, rect.height);
|
||||
return {width:w,height:h};
|
||||
}
|
||||
|
||||
/**
|
||||
* 横向布局:按主轴水平排列,支持均分与换行
|
||||
* @param items 待布局元素
|
||||
*/
|
||||
private layoutHorizontal(items:Shape[]){
|
||||
if(!this.wrap){
|
||||
// single row
|
||||
const sizes = items.map(s=> this.getItemSize(s));
|
||||
const totalWidth = sizes.reduce((acc,it)=> acc + it.width, 0);
|
||||
const gaps = Math.max(0, items.length-1);
|
||||
let spacing = this.colSpace;
|
||||
let startX = this.x;
|
||||
if(this.mode=='between' && gaps>0){
|
||||
spacing = Math.max(0, (this.width - totalWidth) / gaps);
|
||||
startX = this.x;
|
||||
}else if(this.mode=='center'){
|
||||
startX = this.x + Math.max(0, (this.width - (totalWidth + spacing*gaps)) / 2);
|
||||
}else if(this.mode=='right'){
|
||||
startX = this.x + Math.max(0, this.width - (totalWidth + spacing*gaps));
|
||||
}else{
|
||||
startX = this.x;
|
||||
}
|
||||
let x = startX;
|
||||
for(let i=0;i<items.length;i++){
|
||||
const s = items[i];
|
||||
const sz = sizes[i];
|
||||
const y = this.anchor=='top' ? this.y : this.y + Math.max(0, this.height - sz.height);
|
||||
s.setX(x).setY(y);
|
||||
x += sz.width + spacing;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// wrapping: multiple rows
|
||||
let rows: RoysType[] = [];
|
||||
let curItems:Shape[] = [];
|
||||
let curSizes:CurSizeType[] = [];
|
||||
let curWidth = 0;
|
||||
let curHeight = 0;
|
||||
for(let i=0;i<items.length;i++){
|
||||
const s = items[i];
|
||||
const sz = this.getItemSize(s);
|
||||
const addWidth = (curItems.length>0? this.colSpace:0) + sz.width;
|
||||
if(curItems.length>0 && curWidth + addWidth > this.width){
|
||||
rows.push({items:curItems, sizes:curSizes, rowWidth:curWidth, rowHeight:curHeight});
|
||||
curItems = [];
|
||||
curSizes = [];
|
||||
curWidth = 0;
|
||||
curHeight = 0;
|
||||
}
|
||||
if(curItems.length>0){
|
||||
curWidth += this.colSpace;
|
||||
}
|
||||
curItems.push(s);
|
||||
curSizes.push(sz);
|
||||
curWidth += sz.width;
|
||||
curHeight = Math.max(curHeight, sz.height);
|
||||
}
|
||||
if(curItems.length>0){
|
||||
rows.push({items:curItems, sizes:curSizes, rowWidth:curWidth, rowHeight:curHeight});
|
||||
}
|
||||
// total height with row spaces
|
||||
const totalRowsHeight = rows.reduce((acc,r)=> acc + r.rowHeight, 0);
|
||||
const totalRowSpaces = this.rowSpace * Math.max(0, rows.length-1);
|
||||
let startY = this.anchor=='top' ? this.y : this.y + Math.max(0, this.height - (totalRowsHeight + totalRowSpaces));
|
||||
let y = startY;
|
||||
for(let r=0;r<rows.length;r++){
|
||||
const row = rows[r];
|
||||
// horizontal positioning per row by mode
|
||||
const gaps = Math.max(0, row.items.length-1);
|
||||
let spacing = this.colSpace;
|
||||
let startX = this.x;
|
||||
if(this.mode=='between' && gaps>0){
|
||||
spacing = Math.max(0, (this.width - row.rowWidth + this.colSpace*gaps) / gaps);
|
||||
startX = this.x;
|
||||
}else if(this.mode=='center'){
|
||||
startX = this.x + Math.max(0, (this.width - (row.rowWidth + this.colSpace*gaps)) / 2);
|
||||
}else if(this.mode=='right'){
|
||||
startX = this.x + Math.max(0, this.width - (row.rowWidth + this.colSpace*gaps));
|
||||
}else{
|
||||
startX = this.x;
|
||||
}
|
||||
let x = startX;
|
||||
for(let i=0;i<row.items.length;i++){
|
||||
const s = row.items[i];
|
||||
const sz = row.sizes[i];
|
||||
const offsetY = this.anchor=='top' ? 0 : (row.rowHeight - sz.height);
|
||||
s.setX(x).setY(y + offsetY);
|
||||
x += sz.width + spacing;
|
||||
}
|
||||
y += row.rowHeight + this.rowSpace;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 纵向布局:按主轴垂直排列,支持均分与换列
|
||||
* @param items 待布局元素
|
||||
*/
|
||||
private layoutVertical(items:Shape[]){
|
||||
if(!this.wrap){
|
||||
// single column
|
||||
const sizes = items.map(s=> this.getItemSize(s));
|
||||
const totalHeight = sizes.reduce((acc,it)=> acc + it.height, 0);
|
||||
const gaps = Math.max(0, items.length-1);
|
||||
let spacing = this.rowSpace;
|
||||
let startY = this.y;
|
||||
if(this.mode=='between' && gaps>0){
|
||||
spacing = Math.max(0, (this.height - totalHeight) / gaps);
|
||||
startY = this.y;
|
||||
}else if(this.anchor=='top'){
|
||||
startY = this.y;
|
||||
}else{
|
||||
startY = this.y + Math.max(0, this.height - (totalHeight + spacing*gaps));
|
||||
}
|
||||
let y = startY;
|
||||
for(let i=0;i<items.length;i++){
|
||||
const s = items[i];
|
||||
const sz = sizes[i];
|
||||
let x = this.x;
|
||||
if(this.mode=='center') x = this.x + Math.max(0, (this.width - sz.width)/2);
|
||||
else if(this.mode=='right') x = this.x + Math.max(0, this.width - sz.width);
|
||||
else x = this.x; // left or between on single column
|
||||
s.setX(x).setY(y);
|
||||
y += sz.height + spacing;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// wrapping into columns
|
||||
let cols: CoysType[] = [];
|
||||
let curItems:Shape[] = [];
|
||||
let curSizes:CurSizeType[] = [];
|
||||
let curHeight = 0;
|
||||
let curWidth = 0;
|
||||
for(let i=0;i<items.length;i++){
|
||||
const s = items[i];
|
||||
const sz = this.getItemSize(s);
|
||||
const addHeight = (curItems.length>0? this.rowSpace:0) + sz.height;
|
||||
if(curItems.length>0 && curHeight + addHeight > this.height){
|
||||
cols.push({items:curItems, sizes:curSizes, colWidth:curWidth, colHeight:curHeight});
|
||||
curItems = [];
|
||||
curSizes = [];
|
||||
curHeight = 0;
|
||||
curWidth = 0;
|
||||
}
|
||||
if(curItems.length>0){
|
||||
curHeight += this.rowSpace;
|
||||
}
|
||||
curItems.push(s);
|
||||
curSizes.push(sz);
|
||||
curHeight += sz.height;
|
||||
curWidth = Math.max(curWidth, sz.width);
|
||||
}
|
||||
if(curItems.length>0){
|
||||
cols.push({items:curItems, sizes:curSizes, colWidth:curWidth, colHeight:curHeight});
|
||||
}
|
||||
const totalColsWidth = cols.reduce((acc,c)=> acc + c.colWidth, 0);
|
||||
const totalColSpaces = this.colSpace * Math.max(0, cols.length-1);
|
||||
let startX = this.x;
|
||||
if(this.mode=='between' && cols.length>1){
|
||||
// distribute columns
|
||||
startX = this.x;
|
||||
}else if(this.mode=='center'){
|
||||
startX = this.x + Math.max(0, (this.width - (totalColsWidth + totalColSpaces))/2);
|
||||
}else if(this.mode=='right'){
|
||||
startX = this.x + Math.max(0, this.width - (totalColsWidth + totalColSpaces));
|
||||
}else{
|
||||
startX = this.x;
|
||||
}
|
||||
let x = startX;
|
||||
let betweenSpacing = this.colSpace;
|
||||
if(this.mode=='between' && cols.length>1){
|
||||
betweenSpacing = Math.max(0, (this.width - totalColsWidth) / (cols.length-1));
|
||||
}
|
||||
for(let c=0;c<cols.length;c++){
|
||||
const col = cols[c];
|
||||
let y = this.anchor=='top' ? this.y : this.y + Math.max(0, this.height - col.colHeight - this.rowSpace*(col.items.length-1));
|
||||
for(let i=0;i<col.items.length;i++){
|
||||
const s = col.items[i];
|
||||
const sz = col.sizes[i];
|
||||
let offsetX = 0;
|
||||
if(this.mode=='center') offsetX = Math.max(0, (col.colWidth - sz.width)/2);
|
||||
else if(this.mode=='right') offsetX = Math.max(0, col.colWidth - sz.width);
|
||||
else offsetX = 0;
|
||||
s.setX(x + offsetX).setY(y);
|
||||
y += sz.height + this.rowSpace;
|
||||
}
|
||||
x += col.colWidth + betweenSpacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional, IShapeVector2d } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
|
||||
export class ILine extends Shape {
|
||||
override type = 'ILine'
|
||||
// 线条的起点和终点
|
||||
|
||||
|
||||
constructor(config: IShapeOptional, canvas: ICanvas) {
|
||||
super(config, canvas);
|
||||
// 更新宽高
|
||||
this.updateDimensions();
|
||||
}
|
||||
|
||||
// 设置起点
|
||||
setStart(x: number, y: number): ILine {
|
||||
this.pointStart.x = x;
|
||||
this.pointStart.y = y;
|
||||
this.updateDimensions();
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 设置终点
|
||||
setEnd(x: number, y: number): ILine {
|
||||
this.pointEnd.x = x;
|
||||
this.pointEnd.y = y;
|
||||
this.updateDimensions();
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 根据参考点旋转线条
|
||||
rotateByPoint(isStartPoint: boolean, angle: number): ILine {
|
||||
// 计算线条当前长度
|
||||
const length = Math.sqrt(
|
||||
Math.pow(this.pointEnd.x - this.pointStart.x, 2) +
|
||||
Math.pow(this.pointEnd.y - this.pointStart.y, 2)
|
||||
);
|
||||
|
||||
// 将角度转换为弧度,顺时针为正,逆时针为负
|
||||
const radians = angle * Math.PI / 180;
|
||||
|
||||
if (isStartPoint) {
|
||||
// 以起点为基准旋转
|
||||
this.pointEnd.x = this.pointStart.x + length * Math.cos(radians);
|
||||
this.pointEnd.y = this.pointStart.y + length * Math.sin(radians);
|
||||
} else {
|
||||
// 以终点为基准旋转
|
||||
this.pointStart.x = this.pointEnd.x - length * Math.cos(radians);
|
||||
this.pointStart.y = this.pointEnd.y - length * Math.sin(radians);
|
||||
}
|
||||
|
||||
// 更新线条的包围盒尺寸
|
||||
this.updateDimensions();
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 更新线条的包围盒尺寸
|
||||
private updateDimensions(): void {
|
||||
this.x = Math.min(this.pointStart.x, this.pointEnd.x);
|
||||
this.y = Math.min(this.pointStart.y, this.pointEnd.y);
|
||||
this.width = Math.abs(this.pointEnd.x - this.pointStart.x);
|
||||
this.height = Math.abs(this.pointEnd.y - this.pointStart.y);
|
||||
}
|
||||
|
||||
override getBoundRect(): IShapeBoundRect {
|
||||
// include stroke width padding around the line segment
|
||||
const pad = this.stroke != "" || this.strokeGradient.length>0 ? this.strokeWidth / 2 : 0;
|
||||
return {
|
||||
x: this.x - pad,
|
||||
y: this.y - pad,
|
||||
width: this.width + pad * 2,
|
||||
height: this.height + pad * 2
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override draw(ctx: CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.pointStart.x, this.pointStart.y);
|
||||
ctx.lineTo(this.pointEnd.x, this.pointEnd.y);
|
||||
|
||||
if (this.stroke != ""||this.strokeGradient.length>0) {
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
override setHeight(value : number) : Shape {
|
||||
// 计算线条的中心点
|
||||
const centerX = (this.pointStart.x + this.pointEnd.x) / 2;
|
||||
const centerY = (this.pointStart.y + this.pointEnd.y) / 2;
|
||||
|
||||
// 计算当前线条的宽度(保持不变)
|
||||
const currentWidth = Math.abs(this.pointEnd.x - this.pointStart.x);
|
||||
|
||||
// 根据新的高度重新计算起点和终点的Y坐标
|
||||
// 保持线条的水平方向不变,只改变垂直方向
|
||||
if (this.pointStart.x <= this.pointEnd.x) {
|
||||
// 从左到右的线条
|
||||
this.pointStart.x = centerX - currentWidth / 2;
|
||||
this.pointEnd.x = centerX + currentWidth / 2;
|
||||
} else {
|
||||
// 从右到左的线条
|
||||
this.pointStart.x = centerX + currentWidth / 2;
|
||||
this.pointEnd.x = centerX - currentWidth / 2;
|
||||
}
|
||||
|
||||
// 根据新高度设置Y坐标
|
||||
if (this.pointStart.y <= this.pointEnd.y) {
|
||||
// 从上到下的线条
|
||||
this.pointStart.y = centerY - value / 2;
|
||||
this.pointEnd.y = centerY + value / 2;
|
||||
} else {
|
||||
// 从下到上的线条
|
||||
this.pointStart.y = centerY + value / 2;
|
||||
this.pointEnd.y = centerY - value / 2;
|
||||
}
|
||||
|
||||
this.updateDimensions();
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override setWidth(value : number) : Shape {
|
||||
// 计算线条的中心点
|
||||
const centerX = (this.pointStart.x + this.pointEnd.x) / 2;
|
||||
const centerY = (this.pointStart.y + this.pointEnd.y) / 2;
|
||||
|
||||
// 计算当前线条的高度(保持不变)
|
||||
const currentHeight = Math.abs(this.pointEnd.y - this.pointStart.y);
|
||||
|
||||
// 根据新的宽度重新计算起点和终点的X坐标
|
||||
// 保持线条的垂直方向不变,只改变水平方向
|
||||
if (this.pointStart.y <= this.pointEnd.y) {
|
||||
// 从上到下的线条
|
||||
this.pointStart.y = centerY - currentHeight / 2;
|
||||
this.pointEnd.y = centerY + currentHeight / 2;
|
||||
} else {
|
||||
// 从下到上的线条
|
||||
this.pointStart.y = centerY + currentHeight / 2;
|
||||
this.pointEnd.y = centerY - currentHeight / 2;
|
||||
}
|
||||
|
||||
// 根据新宽度设置X坐标
|
||||
if (this.pointStart.x <= this.pointEnd.x) {
|
||||
// 从左到右的线条
|
||||
this.pointStart.x = centerX - value / 2;
|
||||
this.pointEnd.x = centerX + value / 2;
|
||||
} else {
|
||||
// 从右到左的线条
|
||||
this.pointStart.x = centerX + value / 2;
|
||||
this.pointEnd.x = centerX - value / 2;
|
||||
}
|
||||
|
||||
this.updateDimensions();
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 获取两点之间的角度,以起点为基准,水平向右为0度,顺时针为正,逆时针为负
|
||||
getAngle(): number {
|
||||
// 直接使用起点和终点计算角度
|
||||
const angle = Math.atan2(this.pointEnd.y - this.pointStart.y, this.pointEnd.x - this.pointStart.x);
|
||||
|
||||
// 将弧度转换为角度,顺时针为正
|
||||
return angle * (180 / Math.PI);
|
||||
}
|
||||
getAngleByPoints(start:IShapeVector2d,end:IShapeVector2d): number {
|
||||
// 直接使用起点和终点计算角度
|
||||
const angle = Math.atan2(start.y - end.y, end.x - start.x);
|
||||
|
||||
// 将弧度转换为角度,顺时针为正,逆时针为负
|
||||
return angle * (180 / Math.PI);
|
||||
}
|
||||
|
||||
// 计算线条绕指定锚点的旋转角度
|
||||
getAngleByAnchor(anchorX: number, anchorY: number, startX: number, startY: number, endX: number, endY: number): number {
|
||||
// 计算向量1:从锚点到起点的向量
|
||||
const vector1X = startX - anchorX;
|
||||
const vector1Y = startY - anchorY;
|
||||
|
||||
// 计算向量2:从锚点到终点的向量
|
||||
const vector2X = endX - anchorX;
|
||||
const vector2Y = endY - anchorY;
|
||||
|
||||
// 使用向量的点积和叉积计算角度
|
||||
const dotProduct = vector1X * vector2X + vector1Y * vector2Y;
|
||||
const crossProduct = vector1X * vector2Y - vector1Y * vector2X;
|
||||
|
||||
// 计算角度(弧度)
|
||||
const angle = Math.atan2(crossProduct, dotProduct);
|
||||
|
||||
// 将弧度转换为角度,并确保角度范围在-180到180度之间
|
||||
return angle * (180 / Math.PI);
|
||||
}
|
||||
|
||||
override isPointInPath(x: number, y: number, shapeId: string): boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
|
||||
// 计算点到线段的距离
|
||||
const lineLength = Math.sqrt(
|
||||
Math.pow(this.pointEnd.x - this.pointStart.x, 2) +
|
||||
Math.pow(this.pointEnd.y - this.pointStart.y, 2)
|
||||
);
|
||||
|
||||
if (lineLength === 0) return false;
|
||||
|
||||
const distance = Math.abs(
|
||||
(this.pointEnd.y - this.pointStart.y) * x -
|
||||
(this.pointEnd.x - this.pointStart.x) * y +
|
||||
this.pointEnd.x * this.pointStart.y -
|
||||
this.pointEnd.y * this.pointStart.x
|
||||
) / lineLength;
|
||||
|
||||
// 判断点是否在线段的范围内
|
||||
const dotProduct =
|
||||
((x - this.pointStart.x) * (this.pointEnd.x - this.pointStart.x) +
|
||||
(y - this.pointStart.y) * (this.pointEnd.y - this.pointStart.y)) / lineLength;
|
||||
|
||||
return distance <= this.strokeWidth && dotProduct >= 0 && dotProduct <= lineLength;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
import { IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
|
||||
function getControlPoints(x0 : number, y0 : number, x1 : number, y1 : number, x2 : number, y2 : number, t : number) : number[] {
|
||||
const d01 = Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)),
|
||||
d12 = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)),
|
||||
fa = (t * d01) / (d01 + d12),
|
||||
fb = (t * d12) / (d01 + d12),
|
||||
p1x = x1 - fa * (x2 - x0),
|
||||
p1y = y1 - fa * (y2 - y0),
|
||||
p2x = x1 + fb * (x2 - x0),
|
||||
p2y = y1 + fb * (y2 - y0);
|
||||
|
||||
return [p1x, p1y, p2x, p2y];
|
||||
}
|
||||
|
||||
function expandPoints(p : number[], tension : number) : number[] {
|
||||
const len = p.length
|
||||
const allPoints : Array<number> = [];
|
||||
|
||||
for (let n = 2; n < len - 2; n += 2) {
|
||||
const cp = getControlPoints(
|
||||
p[n - 2],
|
||||
p[n - 1],
|
||||
p[n],
|
||||
p[n + 1],
|
||||
p[n + 2],
|
||||
p[n + 3],
|
||||
tension
|
||||
);
|
||||
if (isNaN(cp[0])) {
|
||||
continue;
|
||||
}
|
||||
allPoints.push(cp[0]);
|
||||
allPoints.push(cp[1]);
|
||||
allPoints.push(p[n]);
|
||||
allPoints.push(p[n + 1]);
|
||||
allPoints.push(cp[2]);
|
||||
allPoints.push(cp[3]);
|
||||
}
|
||||
|
||||
return allPoints;
|
||||
}
|
||||
|
||||
export class ILinePolygon extends Shape {
|
||||
override type = 'ILinePolygon'
|
||||
constructor(config : IShapeOptional, canvas : ICanvas) {
|
||||
super(config, canvas);
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
let points = this.points;
|
||||
if (points.length < 4) {
|
||||
let cp = [] as number[]
|
||||
for(let i=0;i<(4-points.length);i++){
|
||||
cp.push(0)
|
||||
}
|
||||
points = points.concat(cp)
|
||||
return {
|
||||
x: points[0],
|
||||
y: points[1],
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
if (this.tension !== 0) {
|
||||
points = [
|
||||
points[0],
|
||||
points[1],
|
||||
...this._getTensionPoints(false),
|
||||
points[points.length - 2],
|
||||
points[points.length - 1],
|
||||
];
|
||||
} else {
|
||||
points = this.points;
|
||||
}
|
||||
let minX = this.points[0] + this.x;
|
||||
let maxX = this.points[0] + this.x;
|
||||
let minY = this.points[1] + this.y;
|
||||
let maxY = this.points[1] + this.y;
|
||||
let x=0
|
||||
let y =0;
|
||||
for (let i = 0; i < points.length / 2; i++) {
|
||||
x = points[i * 2] + this.x;
|
||||
y = points[i * 2 + 1] + this.y;
|
||||
minX = Math.min(minX, x);
|
||||
maxX = Math.max(maxX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
let borderWidth = (this.stroke!='' ? this.strokeWidth / 2 : 0);
|
||||
return {
|
||||
x: minX - borderWidth,
|
||||
y: minY - borderWidth,
|
||||
width: maxX - minX + borderWidth*2,
|
||||
height: maxY - minY + borderWidth*2,
|
||||
} as IShapeBoundRect;
|
||||
|
||||
|
||||
}
|
||||
|
||||
private getTensionPoints() {
|
||||
return this._getTensionPoints();
|
||||
}
|
||||
private _getTensionPoints(isXYOffset:boolean = true) {
|
||||
|
||||
if (this.closed) {
|
||||
return this._getTensionPointsClosed(isXYOffset);
|
||||
} else {
|
||||
return expandPoints(isXYOffset?this._getPoints():this.points, this.tension);
|
||||
}
|
||||
}
|
||||
private _getTensionPointsClosed(isXYOffset:boolean = true) {
|
||||
const p = isXYOffset?this._getPoints():this.points
|
||||
const len = p.length;
|
||||
const tension = this.tension;
|
||||
const firstControlPoints = getControlPoints(
|
||||
p[len - 2],
|
||||
p[len - 1],
|
||||
p[0],
|
||||
p[1],
|
||||
p[2],
|
||||
p[3],
|
||||
tension
|
||||
);
|
||||
const lastControlPoints = getControlPoints(
|
||||
p[len - 4],
|
||||
p[len - 3],
|
||||
p[len - 2],
|
||||
p[len - 1],
|
||||
p[0],
|
||||
p[1],
|
||||
tension
|
||||
);
|
||||
const middle = expandPoints(p, tension);
|
||||
const tp = [firstControlPoints[2], firstControlPoints[3]]
|
||||
.concat(middle)
|
||||
.concat([
|
||||
lastControlPoints[0],
|
||||
lastControlPoints[1],
|
||||
p[len - 2],
|
||||
p[len - 1],
|
||||
lastControlPoints[2],
|
||||
lastControlPoints[3],
|
||||
firstControlPoints[0],
|
||||
firstControlPoints[1],
|
||||
p[0],
|
||||
p[1],
|
||||
]);
|
||||
|
||||
return tp;
|
||||
}
|
||||
|
||||
private _getPoints():number[]{
|
||||
return this.points.map((el:number,index:number)=>{
|
||||
return (index+1)%2 == 0 ?el+this.y:el+this.x;
|
||||
})
|
||||
}
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false || this.points.length < 4) return;
|
||||
super.draw(ctx);
|
||||
ctx.beginPath();
|
||||
|
||||
let points = this._getPoints()
|
||||
let length = points.length
|
||||
let tension = this.tension
|
||||
let closed = this.closed
|
||||
let bezier = this.bezier
|
||||
let tp = [] as number[]
|
||||
let len = 0
|
||||
let n = 0;
|
||||
|
||||
if (length==0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0], points[1]);
|
||||
|
||||
|
||||
if (tension !== 0 && length > 4) {
|
||||
tp = this.getTensionPoints();
|
||||
len = tp.length;
|
||||
n = closed ? 0 : 4;
|
||||
|
||||
if (!closed) {
|
||||
ctx.quadraticCurveTo(tp[0], tp[1], tp[2], tp[3]);
|
||||
}
|
||||
|
||||
while (n < len - 2) {
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
ctx.bezierCurveTo(
|
||||
tp[(n++).toInt()],
|
||||
tp[(n++).toInt()],
|
||||
tp[(n++).toInt()],
|
||||
tp[(n++).toInt()],
|
||||
tp[(n++).toInt()],
|
||||
tp[(n++).toInt()]
|
||||
);
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
ctx.bezierCurveTo(
|
||||
tp[n++],
|
||||
tp[n++],
|
||||
tp[n++],
|
||||
tp[n++],
|
||||
tp[n++],
|
||||
tp[n++]
|
||||
);
|
||||
// #endif
|
||||
}
|
||||
|
||||
if (!closed) {
|
||||
ctx.quadraticCurveTo(
|
||||
tp[len - 2],
|
||||
tp[len - 1],
|
||||
points[length - 2],
|
||||
points[length - 1]
|
||||
);
|
||||
}
|
||||
} else if (bezier) {
|
||||
|
||||
n = 2;
|
||||
while (n < length) {
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
ctx.bezierCurveTo(
|
||||
points[(n++).toInt()],
|
||||
points[(n++).toInt()],
|
||||
points[(n++).toInt()],
|
||||
points[(n++).toInt()],
|
||||
points[(n++).toInt()],
|
||||
points[(n++).toInt()]
|
||||
);
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
ctx.bezierCurveTo(
|
||||
points[n++],
|
||||
points[n++],
|
||||
points[n++],
|
||||
points[n++],
|
||||
points[n++],
|
||||
points[n++]
|
||||
);
|
||||
// #endif
|
||||
}
|
||||
} else {
|
||||
for (n = 2; n < length; n += 2) {
|
||||
ctx.lineTo(points[n], points[n + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (closed&&this.fill!='') {
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else if(this.stroke!='') {
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
const rect = this.getBoundRect()
|
||||
const realX = x - (this.offsetX) - this.x - this.points[0];
|
||||
const realY = y - (this.offsetY) - this.y - this.points[1];
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = this.width/2;
|
||||
centerY = this.height/2;
|
||||
break;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
const rotatedX = centerX + dx * cos - dy * sin;
|
||||
const rotatedY = centerY + dx * sin + dy * cos;
|
||||
return rotatedX >= 0 && rotatedX <= rect.width* this.scaleX && rotatedY >= 0 && rotatedY <= rect.height* this.scaleY;
|
||||
}
|
||||
let isInRect = realX >= 0 && realX <= rect.width* this.scaleX && realY >= 0 && realY <= rect.height* this.scaleY;
|
||||
|
||||
return isInRect
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
import { generateFrame } from "./qrcode/qrcode.uts"
|
||||
export class IQrcode extends Shape {
|
||||
override type = 'IQrcode'
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
const pad = (this.stroke!='' ? this.strokeWidth/2 : 0);
|
||||
return {
|
||||
x: this.x - pad,
|
||||
y: this.y - pad,
|
||||
width: this.width + pad*2,
|
||||
height: this.height + pad*2
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
setRadius(value : number) : IQrcode {
|
||||
this.radius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 根据新高度调整矩形,以左上角 (x,y) 为起点
|
||||
override setHeight(value : number) : Shape {
|
||||
const originalY = this.y;
|
||||
if (value >= 0) {
|
||||
this.y = originalY;
|
||||
this.height = value;
|
||||
} else {
|
||||
this.y = originalY + value; // value 为负,向上扩展
|
||||
this.height = Math.abs(value);
|
||||
}
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 根据新宽度调整矩形,以左上角 (x,y) 为起点
|
||||
override setWidth(value : number) : Shape {
|
||||
const originalX = this.x;
|
||||
if (value >= 0) {
|
||||
this.x = originalX;
|
||||
this.width = value;
|
||||
} else {
|
||||
this.x = originalX + value; // value 为负,向左扩展
|
||||
this.width = Math.abs(value);
|
||||
}
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 设置二维码图片图片。
|
||||
setQrcodeLabel(text:string){
|
||||
this.qrcodeText = text;
|
||||
this.needsUpdate = true;
|
||||
}
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
|
||||
ctx.beginPath();
|
||||
if (this.radius > 0) {
|
||||
let radiuss = Math.min(this.radius, this.width / 2, this.height / 2);
|
||||
const radius = Math.max(radiuss, 0)
|
||||
ctx.moveTo(this.x + radius, this.y);
|
||||
ctx.lineTo(this.x + this.width - radius, this.y);
|
||||
ctx.arcTo(this.x + this.width, this.y, this.x + this.width, this.y + radius, radius);
|
||||
ctx.lineTo(this.x + this.width, this.y + this.height - radius);
|
||||
ctx.arcTo(this.x + this.width, this.y + this.height, this.x + this.width - radius, this.y + this.height, radius);
|
||||
ctx.lineTo(this.x + radius, this.y + this.height);
|
||||
ctx.arcTo(this.x, this.y + this.height, this.x, this.y + this.height - radius, radius);
|
||||
ctx.lineTo(this.x, this.y + radius);
|
||||
ctx.arcTo(this.x, this.y, this.x + radius, this.y, radius);
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.rect(this.x, this.y, this.width, this.height);
|
||||
}
|
||||
ctx.closePath();
|
||||
if (this.fill != "") {
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
let fo = generateFrame(this.qrcodeText, "H")
|
||||
let points = fo.frameBuffer
|
||||
let width = fo.width
|
||||
let px = this.width / width
|
||||
let borderWidth = 0
|
||||
for (let i = 0; i < width; i++) {
|
||||
for (let j = 0; j < width; j++) {
|
||||
if (points[j * width + i] > 0) {
|
||||
ctx.fillStyle = this.foreground;
|
||||
ctx.fillRect( borderWidth + px * i + this.x, borderWidth + px * j + this.y, px, px)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional, ShapeSetAttrType } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
export class IRect extends Shape {
|
||||
override type = 'IRect'
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
const pad = (this.stroke!='' ? this.strokeWidth/2 : 0);
|
||||
return {
|
||||
x: this.x - pad,
|
||||
y: this.y - pad,
|
||||
width: this.width + pad*2,
|
||||
height: this.height + pad*2
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
setRadius(value : number) : IRect {
|
||||
this.radius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 根据新高度调整矩形,以左上角 (x,y) 为起点
|
||||
override setHeight(value : number) : Shape {
|
||||
const originalY = this.y;
|
||||
if (value >= 0) {
|
||||
this.y = originalY;
|
||||
this.height = value;
|
||||
} else {
|
||||
this.y = originalY + value; // value 为负,向上扩展
|
||||
this.height = Math.abs(value);
|
||||
}
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 根据新宽度调整矩形,以左上角 (x,y) 为起点
|
||||
override setWidth(value : number) : Shape {
|
||||
const originalX = this.x;
|
||||
if (value >= 0) {
|
||||
this.x = originalX;
|
||||
this.width = value;
|
||||
} else {
|
||||
this.x = originalX + value; // value 为负,向左扩展
|
||||
this.width = Math.abs(value);
|
||||
}
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
|
||||
ctx.beginPath();
|
||||
if (this.radius > 0) {
|
||||
let radiuss = Math.min(this.radius, this.width / 2, this.height / 2);
|
||||
const radius = Math.max(radiuss, 0)
|
||||
ctx.moveTo(this.x + radius, this.y);
|
||||
ctx.lineTo(this.x + this.width - radius, this.y);
|
||||
ctx.arcTo(this.x + this.width, this.y, this.x + this.width, this.y + radius, radius);
|
||||
ctx.lineTo(this.x + this.width, this.y + this.height - radius);
|
||||
ctx.arcTo(this.x + this.width, this.y + this.height, this.x + this.width - radius, this.y + this.height, radius);
|
||||
ctx.lineTo(this.x + radius, this.y + this.height);
|
||||
ctx.arcTo(this.x, this.y + this.height, this.x, this.y + this.height - radius, radius);
|
||||
ctx.lineTo(this.x, this.y + radius);
|
||||
ctx.arcTo(this.x, this.y, this.x + radius, this.y, radius);
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.rect(this.x, this.y, this.width, this.height);
|
||||
}
|
||||
ctx.closePath();
|
||||
if (this.fill != "") {
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
import { IShapeBoundRect, IShapeOptional, IShapeVector2d } from '../interface.uts';
|
||||
|
||||
export class IRegularPolygon extends Shape {
|
||||
override type = 'IRegularPolygon'
|
||||
constructor(config : IShapeOptional, canvas : ICanvas) {
|
||||
super(config, canvas);
|
||||
this.radius = config?.radius ?? 30;
|
||||
this.sides = config?.sides ?? 3;
|
||||
}
|
||||
|
||||
setRadius(value : number) : IRegularPolygon {
|
||||
this.radius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setSides(value : number) : IRegularPolygon {
|
||||
this.sides = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
const points = this.getPointsVec();
|
||||
let minX = points[0].x;
|
||||
let maxX = points[0].x;
|
||||
let minY = points[0].y;
|
||||
let maxY = points[0].y;
|
||||
points.forEach((point) => {
|
||||
minX = Math.min(minX, point.x);
|
||||
maxX = Math.max(maxX, point.x);
|
||||
minY = Math.min(minY, point.y);
|
||||
maxY = Math.max(maxY, point.y);
|
||||
});
|
||||
let borderWidth = (this.stroke!='' ? this.strokeWidth/2 : 0)
|
||||
return {
|
||||
x: minX-borderWidth,
|
||||
y: minY-borderWidth,
|
||||
width: maxX - minX+borderWidth*2,
|
||||
height: maxY - minY+borderWidth*2,
|
||||
} as IShapeBoundRect;
|
||||
|
||||
}
|
||||
|
||||
override setWidth(value : number) : IRegularPolygon {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.radius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override setHeight(value : number) : IRegularPolygon {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.radius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
private getPointsVec() : IShapeVector2d[] {
|
||||
const sides = this.sides as number;
|
||||
const radius = this.radius;
|
||||
const points : IShapeVector2d[] = [];
|
||||
for (let n = 0; n < sides; n++) {
|
||||
points.push({
|
||||
x: this.x + radius * Math.sin((n * 2 * Math.PI) / sides),
|
||||
y: this.y + (-1 * radius * Math.cos((n * 2 * Math.PI) / sides)),
|
||||
});
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
ctx.beginPath();
|
||||
const points = this.getPointsVec();
|
||||
ctx.moveTo(points[0].x, points[0].y);
|
||||
for (let n = 1; n < points.length; n++) {
|
||||
ctx.lineTo(points[n].x, points[n].y);
|
||||
}
|
||||
ctx.closePath();
|
||||
if (this.fill != "") {
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
|
||||
// 计算相对于图形原点的坐标
|
||||
let realX = x - this.offsetX - this.x;
|
||||
let realY = y - this.offsetY - this.y;
|
||||
|
||||
// 处理旋转
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = this.width/2;
|
||||
centerY = this.height/2;
|
||||
break;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
realX = centerX + dx * cos - dy * sin;
|
||||
realY = centerY + dx * sin + dy * cos;
|
||||
}
|
||||
|
||||
// 获取多边形的顶点
|
||||
const points = this.getPointsVec();
|
||||
const n = points.length;
|
||||
let inside = false;
|
||||
|
||||
// 使用射线法判断点是否在多边形内部
|
||||
for (let i = 0, j = n - 1; i < n; j = i++) {
|
||||
const xi = points[i].x - this.x;
|
||||
const yi = points[i].y - this.y;
|
||||
const xj = points[j].x - this.x;
|
||||
const yj = points[j].y - this.y;
|
||||
|
||||
if (((yi > realY) != (yj > realY)) &&
|
||||
(realX < (xj - xi) * (realY - yi) / (yj - yi) + xi)) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有描边,检查点是否在描边范围内
|
||||
if (!inside && this.stroke != "") {
|
||||
for (let i = 0, j = n - 1; i < n; j = i++) {
|
||||
const xi = points[i].x - this.x;
|
||||
const yi = points[i].y - this.y;
|
||||
const xj = points[j].x - this.x;
|
||||
const yj = points[j].y - this.y;
|
||||
|
||||
// 计算点到线段的距离
|
||||
const dx = xj - xi;
|
||||
const dy = yj - yi;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
if (len > 0) {
|
||||
const t = ((realX - xi) * dx + (realY - yi) * dy) / (len * len);
|
||||
if (t >= 0 && t <= 1) {
|
||||
const px = xi + t * dx;
|
||||
const py = yi + t * dy;
|
||||
const distance = Math.sqrt((realX - px) * (realX - px) + (realY - py) * (realY - py));
|
||||
if (distance <= this.strokeWidth / 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { CanvasRotateCenter, ShapeSetAttrType, IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
|
||||
|
||||
export class IRing extends Shape {
|
||||
override type = 'IRing'
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
this.width = this.outerRadius * 2
|
||||
this.height = this.outerRadius * 2
|
||||
}
|
||||
|
||||
setInnerRadius(value : number) : IRing {
|
||||
this.innerRadius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
setOuterRadius(value : number) : IRing {
|
||||
this.outerRadius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
const r = Math.max(0, this.outerRadius);
|
||||
const pad = (this.stroke != "" ? this.strokeWidth / 2 : 0);
|
||||
const d = r * 2 + pad * 2;
|
||||
return {
|
||||
x: this.x - r - pad,
|
||||
y: this.y - r - pad,
|
||||
width: d,
|
||||
height: d
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override setWidth(value : number) : IRing {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.outerRadius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override setHeight(value : number) : IRing {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.outerRadius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
ctx.beginPath();
|
||||
// 将角度转换为弧度
|
||||
const startRad = this.startAngle * Math.PI / 180;
|
||||
const endRad = this.endAngle * Math.PI / 180;
|
||||
// 绘制外圆弧
|
||||
ctx.arc(this.x, this.y, this.outerRadius, startRad, endRad, false);
|
||||
// 绘制内圆弧(反方向)
|
||||
ctx.arc(this.x, this.y, this.innerRadius, endRad, startRad, true);
|
||||
ctx.closePath();
|
||||
if (this.fill != "") {
|
||||
ctx.fill();
|
||||
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
|
||||
// 计算点击位置相对于圆心的实际坐标
|
||||
let realX = x - this.offsetX - this.x;
|
||||
let realY = y - this.offsetY - this.y;
|
||||
|
||||
// 如果有旋转,需要将坐标转换回未旋转状态
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = this.width/2;
|
||||
centerY = this.height/2;
|
||||
break;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
realX = centerX + dx * cos - dy * sin;
|
||||
realY = centerY + dx * sin + dy * cos;
|
||||
}
|
||||
|
||||
// 计算点到圆心的距离
|
||||
const distance = Math.sqrt(realX * realX + realY * realY);
|
||||
|
||||
// 考虑缩放因素
|
||||
const scaledInnerRadius = this.innerRadius * Math.min(this.scaleX, this.scaleY);
|
||||
const scaledOuterRadius = this.outerRadius * Math.min(this.scaleX, this.scaleY);
|
||||
|
||||
// 检查距离是否在内径和外径之间
|
||||
if (distance < scaledInnerRadius || distance > scaledOuterRadius) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算点击位置的角度(弧度)
|
||||
let angle = Math.atan2(realY, realX);
|
||||
// 将角度转换为0-2π范围
|
||||
if (angle < 0) angle += 2 * Math.PI;
|
||||
// 将角度转换为度数
|
||||
angle = angle * 180 / Math.PI;
|
||||
|
||||
// 将起始角度和结束角度标准化到0-360度范围
|
||||
let start = this.startAngle % 360;
|
||||
if (start < 0) start += 360;
|
||||
let end = this.endAngle % 360;
|
||||
if (end < 0) end += 360;
|
||||
|
||||
// 处理跨越0度线的情况
|
||||
if (start <= end) {
|
||||
return angle >= start && angle <= end;
|
||||
} else {
|
||||
return angle >= start || angle <= end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
import { IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
|
||||
export class ISector extends Shape {
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
this.radius = config?.radius ?? 30;
|
||||
this.startAngle = config?.startAngle ?? 0;
|
||||
this.endAngle = config?.endAngle ?? 90;
|
||||
this.type = "ISector"
|
||||
}
|
||||
|
||||
setRadius(value : number) : ISector {
|
||||
this.radius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
// 与 IArc 相同的严谨包围盒逻辑:扇形需包含圆心
|
||||
const centerX = this.x;
|
||||
const centerY = this.y;
|
||||
const r = Math.max(0, this.radius);
|
||||
const d2r = Math.PI / 180;
|
||||
const normalize = (deg:number):number => { let a = deg % 360; if (a < 0) a += 360; return a; };
|
||||
let start = normalize(this.startAngle);
|
||||
let end = normalize(this.endAngle);
|
||||
if (end < start) end += 360;
|
||||
const withinSweep = (deg:number):boolean => { let a = normalize(deg); if (a < start) a += 360; return a >= start && a <= end; };
|
||||
const candidatesX:number[] = [centerX];
|
||||
const candidatesY:number[] = [centerY];
|
||||
const sx = centerX + r * Math.cos(start * d2r);
|
||||
const sy = centerY + r * Math.sin(start * d2r);
|
||||
const ex = centerX + r * Math.cos(end * d2r);
|
||||
const ey = centerY + r * Math.sin(end * d2r);
|
||||
candidatesX.push(sx, ex);
|
||||
candidatesY.push(sy, ey);
|
||||
const extrema = [0, 90, 180, 270];
|
||||
for (const deg of extrema) {
|
||||
if (withinSweep(deg)) {
|
||||
const rad = deg * d2r;
|
||||
candidatesX.push(centerX + r * Math.cos(rad));
|
||||
candidatesY.push(centerY + r * Math.sin(rad));
|
||||
}
|
||||
}
|
||||
let minX = candidatesX.length>0?candidatesX[0]:centerX;
|
||||
let maxX = candidatesX.length>0?candidatesX[0]:centerX;
|
||||
let minY = candidatesY.length>0?candidatesY[0]:centerY;
|
||||
let maxY = candidatesY.length>0?candidatesY[0]:centerY;
|
||||
for (let i=1;i<candidatesX.length;i++) {
|
||||
const vx = candidatesX[i];
|
||||
if (!isNaN(vx)) {
|
||||
if (vx < minX) minX = vx;
|
||||
if (vx > maxX) maxX = vx;
|
||||
}
|
||||
}
|
||||
for (let i=1;i<candidatesY.length;i++) {
|
||||
const vy = candidatesY[i];
|
||||
if (!isNaN(vy)) {
|
||||
if (vy < minY) minY = vy;
|
||||
if (vy > maxY) maxY = vy;
|
||||
}
|
||||
}
|
||||
const pad = (this.stroke != "" ? this.strokeWidth / 2 : 0);
|
||||
minX -= pad; minY -= pad; maxX += pad; maxY += pad;
|
||||
return { x: minX, y: minY, width: Math.max(0, maxX - minX), height: Math.max(0, maxY - minY) } as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override setWidth(value : number) : ISector {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.radius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override setHeight(value : number) : ISector {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.radius = value / 2
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
ctx.beginPath();
|
||||
// 将角度转换为弧度
|
||||
const startRad = this.startAngle * Math.PI / 180;
|
||||
const endRad = this.endAngle * Math.PI / 180;
|
||||
// 如果有填充色,先移动到圆心,绘制扇形
|
||||
if (this.fill !== "") {
|
||||
ctx.moveTo(this.x, this.y);
|
||||
}
|
||||
// 绘制圆弧
|
||||
ctx.arc(this.x, this.y, this.radius, startRad, endRad, false);
|
||||
// 如果有填充色,连接回圆心形成扇形
|
||||
if (this.fill != "") {
|
||||
ctx.lineTo(this.x, this.y);
|
||||
}
|
||||
ctx.closePath();
|
||||
if (this.fill != "") {
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override isPointInPath(x: number, y: number, shapeId: string): boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
|
||||
// 计算实际点击位置(考虑偏移量)
|
||||
const realX = x - (this.offsetX) - this.x;
|
||||
const realY = y - (this.offsetY) - this.y;
|
||||
|
||||
// 如果有旋转,先进行反向旋转变换
|
||||
let finalX = realX;
|
||||
let finalY = realY;
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = this.width/2;
|
||||
centerY = this.height/2;
|
||||
break;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
finalX = centerX + dx * cos - dy * sin;
|
||||
finalY = centerY + dx * sin + dy * cos;
|
||||
}
|
||||
|
||||
// 计算点到圆心的距离
|
||||
const distance = Math.sqrt(finalX * finalX + finalY * finalY);
|
||||
|
||||
// 如果点击位置超出半径范围,则不在扇形内
|
||||
if (distance > this.radius * this.scaleX) return false;
|
||||
|
||||
// 计算点击位置的角度(弧度)
|
||||
let angle = Math.atan2(finalY, finalX) * 180 / Math.PI;
|
||||
// 将角度转换为0-360度范围
|
||||
angle = (angle + 360) % 360;
|
||||
|
||||
// 将起始角度和结束角度标准化到0-360度范围
|
||||
let start = (this.startAngle + 360) % 360;
|
||||
let end = (this.endAngle + 360) % 360;
|
||||
|
||||
// 处理跨越0度线的情况
|
||||
if (end < start) {
|
||||
return angle >= start || angle <= end;
|
||||
}
|
||||
|
||||
// 判断点击角度是否在扇形角度范围内
|
||||
return angle >= start && angle <= end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,857 @@
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
// import { Path2DShape } from '@/uni_modules/tmx-ui/core/canvas/lib/path2d.uts';
|
||||
|
||||
import {
|
||||
IShape, IShapeVector2d,
|
||||
ILineJoinType, ILineCapType,
|
||||
IShapeOptional, IShapeBoundRect, ICanvasEvent, CanvasEventType, IEventShapeListener, IEventShape, CanvasRotateCenter, ShapeSetAttrType, ITextAlignType, ITextBaselineType
|
||||
} from '../interface.uts';
|
||||
// #ifdef APP-ANDROID
|
||||
import { IArc } from './arc.uts';
|
||||
import { ICircle } from './circle.uts';
|
||||
import { IEllipse } from './ellipse.uts';
|
||||
import { ImageShape } from './image.uts';
|
||||
import { ILine } from './line.uts';
|
||||
import { ILinePolygon } from './linePolygon.uts';
|
||||
import { Path2DShape } from './path2d.uts';
|
||||
import { IRect } from './rect.uts';
|
||||
import { IRegularPolygon } from './regularPolygon.uts';
|
||||
import { IRing } from './ring.uts';
|
||||
import { ISector } from './sector.uts';
|
||||
import { IStar } from './star.uts';
|
||||
import { IText } from './text.uts';
|
||||
// #endif
|
||||
|
||||
type CloneCallActions = (el:Shape,index:number) => void
|
||||
export class Shape implements IShape {
|
||||
canvas : ICanvas
|
||||
x : number = 0;
|
||||
y : number = 0;
|
||||
width : number = 0;
|
||||
height : number = 0;
|
||||
fill : string = "";
|
||||
stroke : string = "";
|
||||
strokeWidth : number = 0;
|
||||
opacity : number = 1;
|
||||
visible : boolean = true;
|
||||
rotation : number = 0;
|
||||
scaleX : number = 1;
|
||||
scaleY : number = 1;
|
||||
offsetX : number = 0;
|
||||
offsetY : number = 0;
|
||||
draggable : boolean = false;
|
||||
draggableing : boolean = false;
|
||||
// 元素之间上下重叠时,是否允许穿透冒泡逐层触发,默认不允许
|
||||
bubbleEvent = false;
|
||||
toggleStatus = false;
|
||||
|
||||
// 线条样式
|
||||
lineJoin : ILineJoinType = "round";
|
||||
lineDashOffset : number = 0;
|
||||
lineDash : number[] = []
|
||||
lineCap : ILineCapType = "butt";
|
||||
|
||||
text : string = "";
|
||||
fontSize : number = 12;
|
||||
fontFamily : string = "Arial";
|
||||
textAlign : 'left' | 'center' | 'right' = 'left';
|
||||
textBaseline : 'top' | 'middle' | 'bottom' = 'top';
|
||||
padding : number = 0;
|
||||
lineHeight : number = 1.2;
|
||||
|
||||
innerRadius : number = 10;
|
||||
outerRadius : number = 20;
|
||||
startAngle : number = 0;
|
||||
endAngle : number = 360;
|
||||
|
||||
src:string = "";
|
||||
|
||||
foreground:string = 'rgba(0,0,0,1)'
|
||||
qrcodeText:string = 'TMUI4x Great'
|
||||
|
||||
radius : number = 0;
|
||||
|
||||
sides : number = 3;
|
||||
|
||||
points : number[] = [];
|
||||
//多边形线是否自动闭合
|
||||
closed : boolean = true;
|
||||
tension : number = 0
|
||||
bezier : boolean = false;
|
||||
|
||||
radiusX : number = 20;
|
||||
radiusY : number = 80;
|
||||
//星的角数量,默认5角星
|
||||
numPoints : number = 5
|
||||
|
||||
|
||||
pointStart : IShapeVector2d = { x: 0, y: 0 };
|
||||
pointEnd : IShapeVector2d = { x: 0, y: 0 };
|
||||
|
||||
clip : boolean = false;
|
||||
zIndex : number = 0;
|
||||
textBgColor : string = '';
|
||||
|
||||
|
||||
id : string = "shape-" + (Math.random()).toString(8).substring(4, 20)
|
||||
type : string = ''
|
||||
/** 旋转元素时的中心点,默认是topLeft左顶,center表示元素的中间 */
|
||||
rotateCenter : CanvasRotateCenter = "topLeft"
|
||||
|
||||
|
||||
strokeGradient = [] as string[]
|
||||
fillGradient = [] as string[]
|
||||
|
||||
needsUpdate = false
|
||||
private toggleListener = (status : boolean, target : any) => { }
|
||||
private eventListeners : Map<CanvasEventType, IEventShapeListener[]> = new Map();
|
||||
|
||||
|
||||
drag : (eventName : CanvasEventType, parentEventDetail : ICanvasEvent) => void = (eventName : CanvasEventType, parentEventDetail : ICanvasEvent) => {
|
||||
if (!this.draggable) return
|
||||
this.x -= parentEventDetail.detail[0].moveLenX
|
||||
this.y -= parentEventDetail.detail[0].moveLenY
|
||||
|
||||
}
|
||||
constructor(config : IShapeOptional, canvas : ICanvas) {
|
||||
this.canvas = canvas
|
||||
this.x = config?.x ?? this.x;
|
||||
this.y = config?.y ?? this.y;
|
||||
this.width = config?.width ?? this.width;
|
||||
this.height = config?.height ?? this.height;
|
||||
this.fill = config?.fill ?? this.fill;
|
||||
this.stroke = config?.stroke ?? this.stroke;
|
||||
this.strokeWidth = config?.strokeWidth ?? this.strokeWidth;
|
||||
this.opacity = config?.opacity ?? this.opacity;
|
||||
this.visible = config?.visible ?? this.visible;
|
||||
this.rotation = config?.rotation ?? this.rotation;
|
||||
this.scaleX = config?.scaleX ?? this.scaleX;
|
||||
this.scaleY = config?.scaleY ?? this.scaleY;
|
||||
this.offsetX = config?.offsetX ?? this.offsetX;
|
||||
this.offsetY = config?.offsetY ?? this.offsetY;
|
||||
this.draggable = config?.draggable ?? this.draggable;
|
||||
this.bubbleEvent = config?.bubbleEvent ?? this.bubbleEvent;
|
||||
this.rotateCenter = config?.rotateCenter ?? this.rotateCenter;
|
||||
|
||||
this.lineJoin = config?.lineJoin ?? this.lineJoin;
|
||||
this.lineDashOffset = config?.lineDashOffset ?? this.lineDashOffset;
|
||||
this.lineDash = config?.lineDash ?? this.lineDash;
|
||||
this.lineCap = config?.lineCap ?? this.lineCap;
|
||||
|
||||
this.text = config?.text ?? this.text;
|
||||
this.fontSize = config?.fontSize ?? this.fontSize;
|
||||
this.fontFamily = config?.fontFamily ?? this.fontFamily;
|
||||
this.textAlign = config?.textAlign ?? this.textAlign;
|
||||
this.textBaseline = config?.textBaseline ?? this.textBaseline;
|
||||
this.padding = config?.padding ?? this.padding;
|
||||
this.lineHeight = config?.lineHeight ?? this.lineHeight;
|
||||
|
||||
this.innerRadius = config?.innerRadius ?? this.innerRadius;
|
||||
this.outerRadius = config?.outerRadius ?? this.outerRadius;
|
||||
this.startAngle = config?.startAngle ?? this.startAngle;
|
||||
this.endAngle = config?.endAngle ?? this.endAngle;
|
||||
|
||||
this.radius = config?.radius ?? this.radius;
|
||||
this.sides = config?.sides ?? this.sides;
|
||||
|
||||
this.points = config?.points ?? this.points;
|
||||
this.closed = config?.closed ?? this.closed;
|
||||
|
||||
this.tension = config?.tension ?? this.tension;
|
||||
this.bezier = config?.bezier ?? this.bezier;
|
||||
|
||||
this.src = config?.src ?? this.src;
|
||||
|
||||
this.foreground = config?.foreground ?? this.foreground;
|
||||
this.qrcodeText = config?.qrcodeText ?? this.qrcodeText;
|
||||
|
||||
|
||||
this.radiusX = config?.radiusX ?? this.radiusX;
|
||||
this.radiusY = config?.radiusY ?? this.radiusY;
|
||||
this.numPoints = config?.numPoints ?? this.numPoints;
|
||||
this.pointStart = config?.pointStart ?? this.pointStart;
|
||||
this.pointEnd = config?.pointEnd ?? this.pointEnd;
|
||||
|
||||
this.strokeGradient = config?.strokeGradient ?? this.strokeGradient;
|
||||
this.fillGradient = config?.fillGradient ?? this.fillGradient;
|
||||
this.clip = config?.clip ?? this.clip;
|
||||
this.zIndex = config?.zIndex ?? this.zIndex;
|
||||
this.textBgColor = config?.textBgColor ?? this.textBgColor;
|
||||
|
||||
|
||||
}
|
||||
|
||||
draw(ctx : CanvasRenderingContext2D) : void {
|
||||
if (this.visible == false) return;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = this.opacity;
|
||||
ctx.translate(this.x + this.offsetX, this.y + this.offsetY);
|
||||
if (this.rotation != 0) {
|
||||
if (this.rotateCenter == 'topLeft') {
|
||||
ctx.rotate(this.rotation * Math.PI / 180);
|
||||
} else if (this.rotateCenter == 'center') {
|
||||
ctx.translate(this.width / 2, this.height / 2);
|
||||
ctx.rotate(this.rotation * Math.PI / 180);
|
||||
ctx.translate(-this.width / 2, -this.height / 2);
|
||||
} else if (this.rotateCenter == 'topRight') {
|
||||
ctx.translate(this.width, 0);
|
||||
ctx.rotate(this.rotation * Math.PI / 180);
|
||||
ctx.translate(-this.width, 0);
|
||||
} else if (this.rotateCenter == 'bottomLeft') {
|
||||
ctx.translate(0, this.height);
|
||||
ctx.rotate(this.rotation * Math.PI / 180);
|
||||
ctx.translate(0, -this.height);
|
||||
} else if (this.rotateCenter == 'bottomRight') {
|
||||
ctx.translate(this.width, this.height);
|
||||
ctx.rotate(this.rotation * Math.PI / 180);
|
||||
ctx.translate(-this.width, -this.height);
|
||||
}
|
||||
}
|
||||
if (this.scaleX != 1 || this.scaleY != 1) {
|
||||
|
||||
if (this.rotateCenter == 'topLeft') {
|
||||
ctx.scale(this.scaleX, this.scaleY);
|
||||
} else if (this.rotateCenter == 'center') {
|
||||
ctx.translate(this.width / 2, this.height / 2);
|
||||
ctx.scale(this.scaleX, this.scaleY);
|
||||
ctx.translate(-this.width / 2, -this.height / 2);
|
||||
} else if (this.rotateCenter == 'topRight') {
|
||||
ctx.translate(this.width, 0);
|
||||
ctx.scale(this.scaleX, this.scaleY);
|
||||
ctx.translate(-this.width, 0);
|
||||
} else if (this.rotateCenter == 'bottomLeft') {
|
||||
ctx.translate(0, this.height);
|
||||
ctx.scale(this.scaleX, this.scaleY);
|
||||
ctx.translate(0, -this.height);
|
||||
} else if (this.rotateCenter == 'bottomRight') {
|
||||
ctx.translate(this.width, this.height);
|
||||
ctx.scale(this.scaleX, this.scaleY);
|
||||
ctx.translate(-this.width, -this.height);
|
||||
}
|
||||
|
||||
}
|
||||
ctx.translate(-this.x, -this.y);
|
||||
if (this.clip) {
|
||||
ctx.clip()
|
||||
}
|
||||
if (this.fill != "" || this.fillGradient.length > 0) {
|
||||
if (this.fillGradient.length > 0) {
|
||||
const gradient = this.createLinearGradient(ctx, this.fillGradient);
|
||||
ctx.fillStyle = gradient;
|
||||
} else if (this.fill != "") {
|
||||
ctx.fillStyle = this.fill;
|
||||
}
|
||||
}
|
||||
if (this.stroke != "" || this.strokeGradient.length > 0) {
|
||||
if (this.strokeGradient.length > 0) {
|
||||
const gradient = this.createLinearGradient(ctx, this.strokeGradient);
|
||||
ctx.strokeStyle = gradient;
|
||||
} else {
|
||||
ctx.strokeStyle = this.stroke
|
||||
}
|
||||
ctx.lineWidth = this.strokeWidth;
|
||||
ctx.lineJoin = this.lineJoin;
|
||||
ctx.lineCap = this.lineCap;
|
||||
ctx.lineDashOffset = this.lineDashOffset;
|
||||
if (this.lineDash.length > 0) {
|
||||
ctx.setLineDash(this.lineDash)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isPointInPath(x : number, y : number, shapeId : string) : boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
const realX = x - (this.offsetX) - this.x;
|
||||
const realY = y - (this.offsetY) - this.y;
|
||||
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
if (this.rotateCenter == 'topLeft') {
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
} else if (this.rotateCenter == 'center') {
|
||||
centerX = this.width / 2;
|
||||
centerY = this.height / 2;
|
||||
} else if (this.rotateCenter == 'topRight') {
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
} else if (this.rotateCenter == 'bottomLeft') {
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
} else if (this.rotateCenter == 'bottomRight') {
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
const rotatedX = centerX + dx * cos - dy * sin;
|
||||
const rotatedY = centerY + dx * sin + dy * cos;
|
||||
return rotatedX >= 0 && rotatedX <= this.width * this.scaleX && rotatedY >= 0 && rotatedY <= this.height * this.scaleY;
|
||||
}
|
||||
|
||||
return realX >= 0 && realX <= this.width * this.scaleX &&
|
||||
realY >= 0 && realY <= this.height * this.scaleY;
|
||||
}
|
||||
|
||||
addEventListener(eventName : CanvasEventType, listener : IEventShapeListener) : Shape {
|
||||
if (!this.eventListeners.has(eventName)) {
|
||||
this.eventListeners.set(eventName, []);
|
||||
}
|
||||
|
||||
this.eventListeners.get(eventName)!.push(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
removeEventListener(eventName : CanvasEventType, listener : IEventShapeListener) : Shape {
|
||||
const listeners = this.eventListeners.get(eventName);
|
||||
if (listeners == null) return this;
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index != -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
buildEvents(eventName : CanvasEventType, parentEventDetail : ICanvasEvent) {
|
||||
const listeners = this.eventListeners.get(eventName);
|
||||
|
||||
if (eventName == 'click') {
|
||||
this.toggleStatus = !this.toggleStatus
|
||||
this.toggleListener(this.toggleStatus, this)
|
||||
}
|
||||
|
||||
if (listeners == null) return;
|
||||
let layerX = parentEventDetail.x - ((this.offsetX * this.scaleX) + this.x);
|
||||
let layerY = parentEventDetail.y - ((this.offsetY * this.scaleY) + this.y);
|
||||
const events = {
|
||||
type: eventName,
|
||||
x: parentEventDetail.x,
|
||||
y: parentEventDetail.y,
|
||||
/** 元素本身内的坐标X */
|
||||
layerX: layerX,
|
||||
/** 元素本身内的坐标Y */
|
||||
layerY: layerY,
|
||||
target: this,
|
||||
touches: parentEventDetail.touches.map((el : ICanvasEvent) : IEventShape => {
|
||||
let layerX_self = el.x - ((this.offsetX * this.scaleX) + this.x);
|
||||
let layerY_self = el.y - ((this.offsetY * this.scaleY) + this.y);
|
||||
return {
|
||||
touches: [] as IEventShape[],
|
||||
type: eventName,
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
layerX: layerX_self,
|
||||
layerY: layerY_self,
|
||||
target: this
|
||||
} as IEventShape
|
||||
})
|
||||
} as IEventShape
|
||||
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
let children = listeners[i]
|
||||
//执行画布的监听事件
|
||||
children(events);
|
||||
}
|
||||
}
|
||||
|
||||
setRotateCenter(center : CanvasRotateCenter) : Shape {
|
||||
this.rotateCenter = center;
|
||||
return this;
|
||||
}
|
||||
setX(value : number) : Shape {
|
||||
this.x = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setY(value : number) : Shape {
|
||||
this.y = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setWidth(value : number) : Shape {
|
||||
if(
|
||||
this.type=='ILinePolygon'||
|
||||
this.type=='ILayout'||
|
||||
this.type=='Path2DShape'||
|
||||
this.type=='IRegularPolygon'
|
||||
){
|
||||
console.warn(`所在图形:${this.type}不支持本方法`)
|
||||
return this;
|
||||
}
|
||||
this.width = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setHeight(value : number) : Shape {
|
||||
if(
|
||||
this.type=='ILinePolygon'||
|
||||
this.type=='ILayout'||
|
||||
this.type=='Path2DShape'||
|
||||
this.type=='IRegularPolygon'
|
||||
){
|
||||
console.warn(`所在图形:${this.type}不支持本方法`)
|
||||
return this;
|
||||
}
|
||||
this.height = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setFill(value : string) : Shape {
|
||||
this.fill = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setStroke(value : string) : Shape {
|
||||
this.stroke = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setStrokeWidth(value : number) : Shape {
|
||||
this.strokeWidth = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setOpacity(value : number) : Shape {
|
||||
this.opacity = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setVisible(value : boolean) : Shape {
|
||||
this.visible = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setRotation(value : number) : Shape {
|
||||
this.rotation = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setScaleX(value : number) : Shape {
|
||||
this.scaleX = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setScaleY(value : number) : Shape {
|
||||
this.scaleY = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setOffsetX(value : number) : Shape {
|
||||
this.offsetX = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setOffsetY(value : number) : Shape {
|
||||
this.offsetY = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
setLineDash(value : number[]) : Shape {
|
||||
this.lineDash = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDraggable(value : boolean) : Shape {
|
||||
this.draggable = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setBubbleEvent(value : boolean) : Shape {
|
||||
this.bubbleEvent = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
setZindex(value : number) : Shape {
|
||||
this.zIndex = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
setClip(value : boolean) : Shape {
|
||||
this.clip = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
setTextBgColor(value : string) : Shape {
|
||||
this.textBgColor = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setAttr(key : string, value : any) {
|
||||
this.needsUpdate = true;
|
||||
switch (key) {
|
||||
case 'x': { this.x = value as number; break; }
|
||||
case 'y': { this.y = value as number; break; }
|
||||
case 'width': { this.width = value as number; break; }
|
||||
case 'height': { this.height = value as number; break; }
|
||||
case 'fill': { this.fill = value as string; break; }
|
||||
case 'stroke': { this.stroke = value as string; break; }
|
||||
case 'strokeWidth': { this.strokeWidth = value as number; break; }
|
||||
case 'opacity': { this.opacity = value as number; break; }
|
||||
case 'visible': { this.visible = value as boolean; break; }
|
||||
case 'rotation': { this.rotation = value as number; break; }
|
||||
case 'scaleX': { this.scaleX = value as number; break; }
|
||||
case 'scaleY': { this.scaleY = value as number; break; }
|
||||
case 'offsetX': { this.offsetX = value as number; break; }
|
||||
case 'offsetY': { this.offsetY = value as number; break; }
|
||||
case 'draggable': { this.draggable = value as boolean; break; }
|
||||
case 'bubbleEvent': { this.bubbleEvent = value as boolean; break; }
|
||||
case 'lineJoin': { this.lineJoin = value as ILineJoinType; break; }
|
||||
case 'lineDashOffset': { this.lineDashOffset = value as number; break; }
|
||||
case 'lineDash': { this.lineDash = value as number[]; break; }
|
||||
case 'lineCap': { this.lineCap = value as ILineCapType; break; }
|
||||
case 'text': this.text = value as string;
|
||||
case 'src': this.src = value as string;
|
||||
case 'fontSize': this.fontSize = value as number;
|
||||
case 'fontFamily': this.fontFamily = value as string;
|
||||
case 'textAlign': this.textAlign = value as ITextAlignType;
|
||||
case 'textBaseline': this.textBaseline = value as ITextBaselineType;
|
||||
case 'padding': this.padding = value as number;
|
||||
case 'lineHeight': this.lineHeight = value as number;
|
||||
case 'innerRadius': { this.innerRadius = value as number; break; }
|
||||
case 'outerRadius': { this.outerRadius = value as number; break; }
|
||||
case 'startAngle': { this.startAngle = value as number; break; }
|
||||
case 'endAngle': { this.endAngle = value as number; break; }
|
||||
case 'radius': { this.radius = value as number; break; }
|
||||
case 'sides': { this.sides = value as number; break; }
|
||||
case 'points': { this.points = value as number[]; break; }
|
||||
case 'closed': { this.closed = value as boolean; break; }
|
||||
case 'tension': { this.tension = value as number; break; }
|
||||
case 'bezier': { this.bezier = value as boolean; break; }
|
||||
case 'radiusX': { this.radiusX = value as number; break; }
|
||||
case 'radiusY': { this.radiusY = value as number; break; }
|
||||
case 'numPoints': { this.numPoints = value as number; break; }
|
||||
case 'pointStart': { this.pointStart = value as IShapeVector2d; break; }
|
||||
case 'pointEnd': { this.pointEnd = value as IShapeVector2d; break; }
|
||||
case 'clip': { this.clip = value as boolean; break; }
|
||||
case 'zIndex': { this.zIndex = value as number; break; }
|
||||
case 'textBgColor': { this.textBgColor = value as string; break; }
|
||||
case 'foreground': { this.foreground = value as string; break; }
|
||||
case 'qrcodeText': { this.qrcodeText = value as string; break; }
|
||||
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
getAttr(key : string) : any | null {
|
||||
switch (key) {
|
||||
case 'x': return this.x;
|
||||
case 'y': return this.y;
|
||||
case 'src': return this.src;
|
||||
case 'width': return this.width;
|
||||
case 'height': return this.height;
|
||||
case 'fill': return this.fill;
|
||||
case 'stroke': return this.stroke;
|
||||
case 'strokeWidth': return this.strokeWidth;
|
||||
case 'opacity': return this.opacity;
|
||||
case 'visible': return this.visible;
|
||||
case 'rotation': return this.rotation;
|
||||
case 'scaleX': return this.scaleX;
|
||||
case 'scaleY': return this.scaleY;
|
||||
case 'offsetX': return this.offsetX;
|
||||
case 'offsetY': return this.offsetY;
|
||||
case 'draggable': return this.draggable;
|
||||
case 'bubbleEvent': return this.bubbleEvent;
|
||||
case 'lineJoin': return this.lineJoin;
|
||||
case 'lineDashOffset': return this.lineDashOffset;
|
||||
case 'lineDash': return this.lineDash;
|
||||
case 'lineCap': return this.lineCap;
|
||||
case 'text': return this.text;
|
||||
case 'fontSize': return this.fontSize;
|
||||
case 'fontFamily': return this.fontFamily;
|
||||
case 'textAlign': return this.textAlign;
|
||||
case 'textBaseline': return this.textBaseline;
|
||||
case 'padding': return this.padding;
|
||||
case 'lineHeight': return this.lineHeight;
|
||||
case 'innerRadius': return this.innerRadius;
|
||||
case 'outerRadius': return this.outerRadius;
|
||||
case 'startAngle': return this.startAngle;
|
||||
case 'endAngle': return this.endAngle;
|
||||
case 'radius': return this.radius;
|
||||
case 'sides': return this.sides;
|
||||
case 'closed': return this.closed;
|
||||
case 'points': return this.points;
|
||||
case 'tension': return this.tension;
|
||||
case 'bezier': return this.bezier;
|
||||
case 'radiusX': return this.radiusX;
|
||||
case 'radiusY': return this.radiusY;
|
||||
case 'numPoints': return this.numPoints;
|
||||
case 'pointStart': return this.pointStart;
|
||||
case 'pointEnd': return this.pointEnd;
|
||||
case 'clip': return this.pointEnd;
|
||||
case 'zIndex': return this.zIndex;
|
||||
case 'textBgColor': return this.textBgColor;
|
||||
case 'foreground': return this.foreground;
|
||||
case 'qrcodeText': return this.qrcodeText;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
getBoundRect() : IShapeBoundRect {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
|
||||
toggle(call : (status : boolean, target : any) => void) : void {
|
||||
this.toggleListener = call;
|
||||
}
|
||||
|
||||
|
||||
update() : Shape {
|
||||
if (!this.needsUpdate) return this;
|
||||
this.needsUpdate = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆当前图形
|
||||
* @param count 要克隆的数量,默认 1
|
||||
* @returns 克隆出来的新图形数组(长度为 count)
|
||||
*/
|
||||
clone(count : number = 1,callActons:CloneCallActions|null = null) : Shape[] {
|
||||
const clones : Shape[] = [];
|
||||
|
||||
const baseConfig = {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
fill: this.fill,
|
||||
stroke: this.stroke,
|
||||
strokeWidth: this.strokeWidth,
|
||||
opacity: this.opacity,
|
||||
visible: this.visible,
|
||||
rotation: this.rotation,
|
||||
scaleX: this.scaleX,
|
||||
scaleY: this.scaleY,
|
||||
offsetX: this.offsetX,
|
||||
offsetY: this.offsetY,
|
||||
draggable: this.draggable,
|
||||
bubbleEvent: this.bubbleEvent,
|
||||
rotateCenter: this.rotateCenter,
|
||||
lineJoin: this.lineJoin,
|
||||
lineDashOffset: this.lineDashOffset,
|
||||
lineDash: this.lineDash.slice(),
|
||||
lineCap: this.lineCap,
|
||||
text: this.text,
|
||||
fontSize: this.fontSize,
|
||||
fontFamily: this.fontFamily,
|
||||
textAlign: this.textAlign,
|
||||
textBaseline: this.textBaseline,
|
||||
padding: this.padding,
|
||||
lineHeight: this.lineHeight,
|
||||
innerRadius: this.innerRadius,
|
||||
outerRadius: this.outerRadius,
|
||||
startAngle: this.startAngle,
|
||||
endAngle: this.endAngle,
|
||||
radius: this.radius,
|
||||
sides: this.sides,
|
||||
points: this.points.slice(),
|
||||
closed: this.closed,
|
||||
tension: this.tension,
|
||||
bezier: this.bezier,
|
||||
radiusX: this.radiusX,
|
||||
radiusY: this.radiusY,
|
||||
numPoints: this.numPoints,
|
||||
pointStart: { x: this.pointStart.x, y: this.pointStart.y },
|
||||
pointEnd: { x: this.pointEnd.x, y: this.pointEnd.y },
|
||||
strokeGradient: this.strokeGradient.slice(),
|
||||
fillGradient: this.fillGradient.slice(),
|
||||
clip: this.clip,
|
||||
zIndex: this.zIndex,
|
||||
textBgColor: this.textBgColor,
|
||||
src: this.src,
|
||||
} as IShapeOptional;
|
||||
|
||||
for (let i = 0; i < Math.max(1, count); i++) {
|
||||
|
||||
let shape:any|null = null;
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
switch(this.type){
|
||||
case 'IArc':{
|
||||
shape = new IArc(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'ICircle':{
|
||||
shape = new ICircle(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'IEllipse':{
|
||||
shape = new IEllipse(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'ImageShape':{
|
||||
shape = new ImageShape(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'ILine':{
|
||||
shape = new ILine(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'ILinePolygon':{
|
||||
shape = new ILinePolygon(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'Path2DShape':{
|
||||
shape = new Path2DShape(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'IRect':{
|
||||
shape = new IRect(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'IRegularPolygon':{
|
||||
shape = new IRegularPolygon(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'IRing':{
|
||||
shape = new IRing(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'ISector':{
|
||||
shape = new ISector(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'IStar':{
|
||||
shape = new IStar(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
case 'IText':{
|
||||
shape = new IText(baseConfig,this.canvas)
|
||||
break;
|
||||
}
|
||||
default:{
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
const Ctor = (this as any).constructor;
|
||||
shape = new Ctor(baseConfig,this.canvas) as any
|
||||
// #endif
|
||||
|
||||
|
||||
if(shape!=null){
|
||||
const cloned : Shape = shape! as Shape;
|
||||
cloned.needsUpdate = true;
|
||||
if(callActons!=null){
|
||||
callActons!(cloned!,i)
|
||||
}
|
||||
clones.push(cloned);
|
||||
}
|
||||
|
||||
}
|
||||
return clones;
|
||||
}
|
||||
|
||||
private createLinearGradient(ctx : CanvasRenderingContext2D, gradientArray : string[]) : CanvasGradient {
|
||||
if (gradientArray.length < 2) return ctx.createLinearGradient(0, 0, 0, 0);
|
||||
// 解析角度
|
||||
let angle = 0;
|
||||
const firstItem = gradientArray[0];
|
||||
if (firstItem.includes('deg')) {
|
||||
angle = parseFloat(firstItem);
|
||||
gradientArray = gradientArray.slice(1);
|
||||
}
|
||||
|
||||
// 计算渐变的起点和终点
|
||||
const radian = angle * Math.PI / 180;
|
||||
const length = Math.max(this.width, this.height);
|
||||
let startX = this.x;
|
||||
let startY = this.y;
|
||||
let endX = this.x;
|
||||
let endY = this.y;
|
||||
|
||||
if (this.type == 'ICircle') {
|
||||
// 对于圆形,使用半径计算
|
||||
const dx = Math.cos(radian) * this.radius;
|
||||
const dy = Math.sin(radian) * this.radius;
|
||||
startX = this.x - dx;
|
||||
startY = this.y - dy;
|
||||
endX = this.x + dx;
|
||||
endY = this.y + dy;
|
||||
} else if (this.type == 'IEllipse') {
|
||||
// 对于椭圆,使用radiusX和radiusY计算
|
||||
const dx = Math.cos(radian) * this.radiusX;
|
||||
const dy = Math.sin(radian) * this.radiusY;
|
||||
startX = this.x - dx;
|
||||
startY = this.y - dy;
|
||||
endX = this.x + dx;
|
||||
endY = this.y + dy;
|
||||
} else if (this.type == 'IStar') {
|
||||
// 对于星形,使用外接圆半径计算
|
||||
const radius = this.outerRadius;
|
||||
const dx = Math.cos(radian) * radius;
|
||||
const dy = Math.sin(radian) * radius;
|
||||
startX = this.x - dx;
|
||||
startY = this.y - dy;
|
||||
endX = this.x + dx;
|
||||
endY = this.y + dy;
|
||||
} else if (this.type == 'IText') {
|
||||
// 对于文本,使用文本框的宽高计算
|
||||
const dx = Math.cos(radian) * this.width;
|
||||
const dy = Math.sin(radian) * this.height;
|
||||
endX = startX + dx;
|
||||
endY = startY + dy;
|
||||
} else if (this.type == 'IArc' || this.type == 'ISector') {
|
||||
// 对于圆弧和扇形,使用半径计算
|
||||
const dx = Math.cos(radian) * this.radius;
|
||||
const dy = Math.sin(radian) * this.radius;
|
||||
startX = this.x - dx;
|
||||
startY = this.y - dy;
|
||||
endX = this.x + dx;
|
||||
endY = this.y + dy;
|
||||
} else {
|
||||
// 默认使用矩形的宽高计算
|
||||
const dx = Math.cos(radian) * length;
|
||||
const dy = Math.sin(radian) * length;
|
||||
endX = startX + dx;
|
||||
endY = startY + dy;
|
||||
}
|
||||
|
||||
const gradient = ctx.createLinearGradient(startX, startY, endX, endY);
|
||||
// 添加渐变色停止点
|
||||
gradientArray.forEach(item => {
|
||||
const ar = item.split(' ');
|
||||
const color = ar[0];
|
||||
const stop = ar[1];
|
||||
const offset = parseFloat(stop) / 100;
|
||||
|
||||
gradient.addColorStop(offset, color);
|
||||
});
|
||||
|
||||
return gradient;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
|
||||
export class IStar extends Shape {
|
||||
override type = 'IStar'
|
||||
constructor(config: IShapeOptional, canvas: ICanvas) {
|
||||
super(config, canvas);
|
||||
this.numPoints = config?.numPoints ?? 5;
|
||||
this.innerRadius = config?.innerRadius ?? 16;
|
||||
this.outerRadius = config?.outerRadius ?? 30;
|
||||
this.width = this.outerRadius * 2;
|
||||
this.height = this.outerRadius * 2;
|
||||
}
|
||||
|
||||
setInnerRadius(value: number): IStar {
|
||||
this.innerRadius = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setOuterRadius(value: number): IStar {
|
||||
this.outerRadius = value;
|
||||
this.width = value * 2;
|
||||
this.height = value * 2;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setNumPoints(value: number): IStar {
|
||||
this.numPoints = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override getBoundRect(): IShapeBoundRect {
|
||||
// 估算星形的外接矩形(以中心为圆心,外半径为界)
|
||||
const r = Math.max(0, this.outerRadius);
|
||||
const pad = (this.stroke != "" ? this.strokeWidth/2 : 0);
|
||||
const d = r*2 + pad*2;
|
||||
return {
|
||||
x: this.x - r - pad,
|
||||
y: this.y - r - pad,
|
||||
width: d,
|
||||
height: d
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override setWidth(value: number): IStar {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.outerRadius = value / 2;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override setHeight(value: number): IStar {
|
||||
this.height = value;
|
||||
this.width = value;
|
||||
this.outerRadius = value / 2;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override draw(ctx: CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
super.draw(ctx);
|
||||
ctx.beginPath();
|
||||
|
||||
// 计算每个角的角度增量
|
||||
const angleStep = Math.PI * 2 / this.numPoints;
|
||||
// 起始角度(使星形垂直向上)
|
||||
const startAngle = -Math.PI / 2;
|
||||
|
||||
// 绘制第一个点(外部点)
|
||||
let x = this.x + Math.cos(startAngle) * this.outerRadius;
|
||||
let y = this.y + Math.sin(startAngle) * this.outerRadius;
|
||||
ctx.moveTo(x, y);
|
||||
|
||||
// 绘制其余的点
|
||||
for (let i = 1; i <= this.numPoints * 2; i++) {
|
||||
const angle = startAngle + angleStep * i / 2;
|
||||
const radius = i % 2 === 0 ? this.outerRadius : this.innerRadius;
|
||||
x = this.x + Math.cos(angle) * radius;
|
||||
y = this.y + Math.sin(angle) * radius;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
ctx.closePath();
|
||||
if (this.fill != "") {
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override isPointInPath(x: number, y: number, shapeId: string): boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
|
||||
// 计算点击位置相对于星形中心的实际坐标
|
||||
let realX = x - this.offsetX - this.x;
|
||||
let realY = y - this.offsetY - this.y;
|
||||
|
||||
// 如果有旋转,需要将坐标转换回未旋转状态
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = this.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = this.width;
|
||||
centerY = this.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = this.width/2;
|
||||
centerY = this.height/2;
|
||||
break;
|
||||
}
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
realX = centerX + dx * cos - dy * sin;
|
||||
realY = centerY + dx * sin + dy * cos;
|
||||
}
|
||||
|
||||
// 计算点到中心的距离
|
||||
const distance = Math.sqrt(realX * realX + realY * realY);
|
||||
|
||||
// 如果点击位置超出外半径,则不在星形内
|
||||
if (distance > this.outerRadius * Math.max(this.scaleX, this.scaleY)) return false;
|
||||
|
||||
// 计算点击位置的角度
|
||||
let angle = Math.atan2(realY, realX);
|
||||
if (angle < 0) angle += Math.PI * 2;
|
||||
|
||||
// 调整角度使其从垂直向上开始计算
|
||||
angle = (angle + Math.PI / 2) % (Math.PI * 2);
|
||||
|
||||
// 计算点所在的扇区
|
||||
const angleStep = Math.PI * 2 / this.numPoints;
|
||||
const sector = Math.floor(angle / angleStep);
|
||||
const sectorAngle = angle - sector * angleStep;
|
||||
|
||||
// 计算该角度对应的半径
|
||||
const ratio = sectorAngle / angleStep;
|
||||
const radius = this.innerRadius + (this.outerRadius - this.innerRadius) * Math.abs(0.5 - ratio) * 2;
|
||||
// 考虑缩放因素
|
||||
const scaledRadius = radius * Math.max(this.scaleX, this.scaleY);
|
||||
// 如果点击位置在计算出的半径内,则在星形内
|
||||
return distance <= scaledRadius;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { Shape } from './shape.uts';
|
||||
import { ShapeSetAttrType, ITextBaselineType, ITextAlignType, CanvasRotateCenter, IShapeBoundRect, IShapeOptional } from '../interface.uts';
|
||||
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
|
||||
|
||||
|
||||
|
||||
export class IText extends Shape {
|
||||
|
||||
constructor(config : IShapeOptional,canvas:ICanvas) {
|
||||
super(config,canvas);
|
||||
this.type = 'IText'
|
||||
}
|
||||
|
||||
private wrapText(ctx : CanvasRenderingContext2D, text : string, maxWidth : number) : string[] {
|
||||
// 首先处理所有类型的换行符,统一转换为\n
|
||||
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
// 按换行符分割文本
|
||||
const paragraphs = text.split('\n');
|
||||
const lines : string[] = [];
|
||||
|
||||
// 处理每个段落
|
||||
for (let i = 0; i < paragraphs.length; i++) {
|
||||
let paragraph = paragraphs[i]
|
||||
if (paragraph == '') {
|
||||
// 保留空行
|
||||
lines.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
const words = paragraph.split('');
|
||||
let currentLine = '';
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const testLine = currentLine + words[i];
|
||||
const metrics = ctx.measureText(testLine);
|
||||
const testWidth = metrics.width;
|
||||
|
||||
if (testWidth > maxWidth && i > 0) {
|
||||
lines.push(currentLine);
|
||||
currentLine = words[i];
|
||||
} else {
|
||||
currentLine = testLine;
|
||||
}
|
||||
}
|
||||
lines.push(currentLine);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
override draw(ctx : CanvasRenderingContext2D) {
|
||||
if (this.visible == false) return;
|
||||
|
||||
ctx.font = `${this.fontSize}px ${this.fontFamily}`;
|
||||
ctx.textAlign = this.textAlign;
|
||||
ctx.textBaseline = this.textBaseline;
|
||||
const maxWidth = this.width > 0 ? this.width - this.padding * 2 : ctx.canvas.offsetWidth - this.x - this.padding * 2;
|
||||
const lines = this.wrapText(ctx, this.text, maxWidth);
|
||||
const lineHeightPixels = this.fontSize * this.lineHeight;
|
||||
|
||||
this.width = maxWidth + this.padding * 2;
|
||||
this.height = lineHeightPixels * lines.length + this.padding * 2;
|
||||
if(this.textBgColor!=''){
|
||||
ctx.fillStyle = this.textBgColor
|
||||
// ctx.fillRect(this.x,this.y,this.width,this.height)
|
||||
ctx.beginPath();
|
||||
if (this.radius > 0) {
|
||||
let radiuss = Math.min(this.radius, this.width / 2, this.height / 2);
|
||||
const radius = Math.max(radiuss, 0)
|
||||
ctx.moveTo(this.x + radius, this.y);
|
||||
ctx.lineTo(this.x + this.width - radius, this.y);
|
||||
ctx.arcTo(this.x + this.width, this.y, this.x + this.width, this.y + radius, radius);
|
||||
ctx.lineTo(this.x + this.width, this.y + this.height - radius);
|
||||
ctx.arcTo(this.x + this.width, this.y + this.height, this.x + this.width - radius, this.y + this.height, radius);
|
||||
ctx.lineTo(this.x + radius, this.y + this.height);
|
||||
ctx.arcTo(this.x, this.y + this.height, this.x, this.y + this.height - radius, radius);
|
||||
ctx.lineTo(this.x, this.y + radius);
|
||||
ctx.arcTo(this.x, this.y, this.x + radius, this.y, radius);
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.rect(this.x, this.y, this.width, this.height);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill()
|
||||
ctx.fillStyle = this.fill
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const y = this.y + this.padding + i * lineHeightPixels;
|
||||
let adjustedY = y;
|
||||
|
||||
// 根据textBaseline调整y坐标
|
||||
if (this.textBaseline === 'middle') {
|
||||
adjustedY = y + this.fontSize / 2;
|
||||
} else if (this.textBaseline === 'bottom') {
|
||||
adjustedY = y + this.fontSize;
|
||||
}
|
||||
|
||||
let x = this.x + this.padding;
|
||||
if (this.textAlign === 'center') {
|
||||
x = this.x + this.width / 2;
|
||||
} else if (this.textAlign === 'right') {
|
||||
x = this.x + this.width - this.padding;
|
||||
}
|
||||
if (this.fill != "") {
|
||||
ctx.fillStyle = this.fill;
|
||||
ctx.fillText(lines[i], x, adjustedY);
|
||||
}
|
||||
if (this.stroke != "") {
|
||||
ctx.strokeStyle = this.stroke;
|
||||
ctx.lineWidth = this.strokeWidth;
|
||||
ctx.strokeText(lines[i], x, adjustedY);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
setText(value : string) : IText {
|
||||
this.text = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setFontSize(value : number) : IText {
|
||||
this.fontSize = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setFontFamily(value : string) : IText {
|
||||
this.fontFamily = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setTextAlign(value : 'left' | 'center' | 'right') : IText {
|
||||
this.textAlign = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setTextBaseline(value : 'top' | 'middle' | 'bottom') : IText {
|
||||
this.textBaseline = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setPadding(value : number) : IText {
|
||||
this.padding = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
setLineHeight(value : number) : IText {
|
||||
this.lineHeight = value;
|
||||
this.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
override getBoundRect() : IShapeBoundRect {
|
||||
const ctx = this.canvas.ctx!
|
||||
ctx.font = `${this.fontSize}px ${this.fontFamily}`;
|
||||
const maxWidth = this.width > 0 ? this.width - this.padding * 2 : ctx.canvas.offsetWidth - this.x - this.padding * 2;
|
||||
const lines = this.wrapText(ctx, this.text, maxWidth);
|
||||
const lineHeightPixels = this.fontSize * this.lineHeight;
|
||||
const measuredWidth = Math.max(...lines.map(line => ctx.measureText(line).width), 0);
|
||||
const effectiveWidth = (this.width > 0 ? Math.min(measuredWidth, maxWidth) : measuredWidth) + this.padding * 2;
|
||||
const width = effectiveWidth;
|
||||
const height = lineHeightPixels * lines.length + this.padding * 2;
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: width,
|
||||
height: height
|
||||
} as IShapeBoundRect;
|
||||
}
|
||||
|
||||
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
|
||||
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
|
||||
const textBounds = this.getBoundRect();
|
||||
const realX = x - textBounds.x - this.offsetX;
|
||||
const realY = y - textBounds.y - this.offsetY;
|
||||
|
||||
if (this.rotation != 0) {
|
||||
const angle = -this.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
|
||||
// 根据不同的旋转中心点设置centerX和centerY
|
||||
switch(this.rotateCenter) {
|
||||
case 'topLeft':
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'topRight':
|
||||
centerX = textBounds.width;
|
||||
centerY = 0;
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
centerX = 0;
|
||||
centerY = textBounds.height;
|
||||
break;
|
||||
case 'bottomRight':
|
||||
centerX = textBounds.width;
|
||||
centerY = textBounds.height;
|
||||
break;
|
||||
case 'center':
|
||||
default:
|
||||
centerX = textBounds.width/2;
|
||||
centerY = textBounds.height/2;
|
||||
break;
|
||||
}
|
||||
|
||||
const dx = realX - centerX;
|
||||
const dy = realY - centerY;
|
||||
const rotatedX = centerX + dx * cos - dy * sin;
|
||||
const rotatedY = centerY + dx * sin + dy * cos;
|
||||
return rotatedX >= 0 && rotatedX <= textBounds.width * this.scaleX && rotatedY >= 0 && rotatedY <= textBounds.height * this.scaleY;
|
||||
}
|
||||
|
||||
return realX >= 0 && realX <= textBounds.width * this.scaleX && realY >= 0 && realY <= textBounds.height * this.scaleY;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user