This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
@@ -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;
}
};
+200
View File
@@ -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;
}
}
}
+228
View File
@@ -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;
}
}
+153
View File
@@ -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;
}
}
+170
View File
@@ -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;
}
}
+226
View File
@@ -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;
}
}