MatchMaster/assets/Script/module/Tool/RoundBox.ts
2026-06-17 15:43:40 +08:00

309 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*******************************************************************************
* 作者: 水煮肉片饭(27185709@qq.com)
* 版本: v1.3
* 描述: 圆角矩形,支持合批
*******************************************************************************/
const CENTER_IDATA = [0, 9, 11, 0, 11, 1, 2, 8, 10, 2, 4, 8, 3, 5, 7, 3, 7, 6];
export enum SizeMode { 'CUSTOM: 自定义尺寸', 'TRIMMED: 原始尺寸裁剪透明像素', 'RAW: 图片原始尺寸' }
const { ccclass, property, menu } = cc._decorator;
@ccclass('Corner')
class Corner {
@property({ displayName: CC_DEV && '↙ 左下' })
leftBottom: boolean = true;
@property({ displayName: CC_DEV && '↘ 右下' })
rightBottom: boolean = true;
@property({ displayName: CC_DEV && '↗ 右上' })
rightTop: boolean = true;
@property({ displayName: CC_DEV && '↖ 左上' })
leftTop: boolean = true;
visible: boolean[] = null;
}
@ccclass
@menu('Comp/RoundBox')
export default class RoundBox extends cc.RenderComponent {
@property({ type: cc.SpriteAtlas, serializable: false, readonly: true, displayName: CC_DEV && 'Atlas' })
private atlas: cc.SpriteAtlas = null;
@property
private _spriteFrame: cc.SpriteFrame = null;
@property({ type: cc.SpriteFrame, displayName: CC_DEV && 'Sprite Frame' })
get spriteFrame() { return this._spriteFrame; }
set spriteFrame(value: cc.SpriteFrame) {
this._spriteFrame = value;
this.updateSpriteFrame();
this.updateSizeMode();
}
@property
private _sizeMode: SizeMode = SizeMode['TRIMMED: 原始尺寸裁剪透明像素'];
@property({ type: cc.Enum(SizeMode), displayName: CC_DEV && 'Size Mode' })
get sizeMode() { return this._sizeMode; }
set sizeMode(value: SizeMode) {
this._sizeMode = value;
this.updateSizeMode();
}
@property
private _radius: number = 100;
@property({ displayName: CC_DEV && '圆角半径' })
get radius() { return this._radius; }
set radius(value: number) {
this._radius = Math.max(value, 0);
this['setVertsDirty']();
}
@property
private _segment: number = 5;
@property({ type: cc.Integer, displayName: CC_DEV && '线段数量' })
get segment() { return this._segment; }
set segment(value: number) {
this._segment = Math.max(value, 1);
this.createBuffer();
this.updateIndice();
this['setVertsDirty']();
this.node['_renderFlag'] |= cc['RenderFlow'].FLAG_OPACITY_COLOR;
}
@property
private _corner: Corner = new Corner();
@property({ displayName: CC_DEV && '圆角可见性' })
get corner() { return this._corner; }
set corner(value: Corner) {
this._corner = value;
this.updateCorner();
this.createBuffer();
this.updateIndice();
this['setVertsDirty']();
this.node['_renderFlag'] |= cc['RenderFlow'].FLAG_OPACITY_COLOR;
}
private renderData = null; //提交给GPU的渲染数据,包括vDatas、uintVDatas、iDatas
private xyOffset: number = 1e8; //顶点坐标数据,在顶点数组中的偏移
private uvOffset: number = 1e8; //顶点uv数据,在顶点数组中的偏移
private colorOffset: number = 1e8; //顶点颜色数据,在顶点数组中的偏移
private step: number = 0; //单个顶点数据的长度,例如:顶点格式“x,y,u,v,color” step = 5
private local: number[] = []; //顶点本地坐标
protected _resetAssembler() {
//定制Assembler
let assembler = this['_assembler'] = new cc['Assembler']();
assembler.updateRenderData = this.updateVData.bind(this);
assembler.updateColor = this.updateColor.bind(this);
assembler.init(this);
//定制RenderData
this.renderData = new cc['RenderData']();
this.renderData.init(assembler);
//初始化顶点格式
let vfmt = assembler.getVfmt();
let fmtElement = vfmt._elements;
for (let i = fmtElement.length - 1; i > -1; this.step += fmtElement[i--].bytes >> 2);
let fmtAttr = vfmt._attr2el;
this.xyOffset = fmtAttr.a_position.offset >> 2;
this.uvOffset = fmtAttr.a_uv0.offset >> 2;
this.colorOffset = fmtAttr.a_color.offset >> 2;
}
protected onLoad() {
this.updateSpriteFrame();
this.updateSizeMode();
this.updateCorner();
this.createBuffer();
this.updateIndice();
this.node.on(cc.Node.EventType.ANCHOR_CHANGED, this.onAnchorChanged, this);
this.node.on(cc.Node.EventType.SIZE_CHANGED, this.onSizeChanged, this);
}
protected onDestroy() {
super.onDestroy();
this.node.off(cc.Node.EventType.ANCHOR_CHANGED, this.onAnchorChanged, this);
this.node.off(cc.Node.EventType.SIZE_CHANGED, this.onSizeChanged, this);
}
//更新圆角数据
private updateCorner() {
let corner = this._corner;
corner.visible = [corner.leftBottom, corner.rightBottom, corner.rightTop, corner.leftTop];
}
//设置顶点个数和三角形个数
private createBuffer() {
let cornerCnt = 0;
for (let i = 0, visible = this._corner.visible; i < 4; visible[i++] && ++cornerCnt);
let vertices = new Float32Array(5 * (12 + cornerCnt * (this._segment - 1)));
let indices = new Uint16Array(3 * (6 + cornerCnt * this._segment));
this.renderData.updateMesh(0, vertices, indices);
}
//Web平台,将renderData的数据提交给GPU渲染,vDatas使用世界坐标
//原生平台并不会执行该函数,引擎另外实现了渲染函数,vDatas使用本地坐标
private fillBuffers(comp: cc.RenderComponent, renderer: any) {
let vData = this.renderData.vDatas[0];
let iData = this.renderData.iDatas[0];
renderer.worldMatDirty && this.fitXY(vData);
let buffer = renderer._meshBuffer;
let offsetInfo = buffer.request(vData.length, iData.length);
let vertexOffset = offsetInfo.byteOffset >> 2;
let vbuf = buffer._vData;
if (vData.length + vertexOffset > vbuf.length) {
vbuf.set(vData.subarray(0, vbuf.length - vertexOffset), vertexOffset);
} else {
vbuf.set(vData, vertexOffset);
}
let ibuf = buffer._iData;
let indiceOffset = offsetInfo.indiceOffset;
let vertexId = offsetInfo.vertexOffset;
for (let i = 0, len = iData.length; i < len; ibuf[indiceOffset++] = vertexId + iData[i++]);
}
//可以传入cc.SpriteFrame图集帧(支持合批,推荐),或单张图片cc.Texture2D
private updateSpriteFrame() {
let frame = this._spriteFrame;
this['_assembler'].fillBuffers = frame ? this.fillBuffers.bind(this) : () => { };
let material = this.getMaterial(0) || cc.Material.getBuiltinMaterial('2d-sprite');
material.define("USE_TEXTURE", true);
material.setProperty("texture", frame ? frame.getTexture() : null);
if (CC_EDITOR) {
if (frame && frame.isValid && frame['_atlasUuid']) {
cc.assetManager.loadAny(frame['_atlasUuid'], (err, asset: cc.SpriteAtlas) => {
this.atlas = asset;
});
} else {
this.atlas = null;
}
}
}
//根据尺寸模式,修改节点尺寸
private updateSizeMode() {
if (!this._spriteFrame) return;
switch (this._sizeMode) {
case SizeMode['TRIMMED: 原始尺寸裁剪透明像素']: this.node.setContentSize(this._spriteFrame['_rect'].size); break;
case SizeMode['RAW: 图片原始尺寸']: this.node.setContentSize(this._spriteFrame['_originalSize']); break;
}
}
//计算VData数据,包括xy,uv,color
private updateVData() {
let vData = this.renderData.vDatas[0];
let local = cc.sys.isNative ? vData : this.local;
let node = this.node;
let cw = node.width, ch = node.height;
let l = -cw * node.anchorX;
let b = -ch * node.anchorY;
let r = cw * (1 - node.anchorX);
let t = ch * (1 - node.anchorY);
let radius = Math.min(this._radius, Math.min(cw, ch) / 2);
let lo = l + radius;
let bo = b + radius;
let ro = r - radius;
let to = t - radius;
let corner = this._corner;
local[0] = lo; local[1] = corner.leftBottom ? bo : b;
local[5] = l; local[6] = local[1];
local[10] = lo; local[11] = b;
local[15] = ro; local[16] = corner.rightBottom ? bo : b;
local[20] = ro; local[21] = b;
local[25] = r; local[26] = local[16];
local[30] = ro; local[31] = corner.rightTop ? to : t;
local[35] = r; local[36] = local[31];
local[40] = ro; local[41] = t;
local[45] = lo; local[46] = corner.leftTop ? to : t;
local[50] = lo; local[51] = t;
local[55] = l; local[56] = local[46];
let radian = Math.PI / (this._segment << 1);
let cos = Math.cos(radian);
let sin = Math.sin(radian);
let visible = corner.visible;
for (let i = 0, offset = 60, step = this.step; i < 4; ++i) {
if (!visible[i]) continue;
let id = 3 * i * step;
let ox = local[id];
let oy = local[id + 1];
id += step;
let deltX = local[id] - ox;
let deltY = local[id + 1] - oy;
for (let j = 0, len = this._segment - 1; j < len; ++j) {
local[offset] = ox + deltX * cos - deltY * sin;
local[offset + 1] = oy + deltY * cos + deltX * sin;
deltX = local[offset] - ox;
deltY = local[offset + 1] - oy;
offset += step;
}
}
!cc.sys.isNative && this.fitXY(vData);
for (let i = 0, len = vData.length, step = this.step; i < len; i += step) {
vData[i + 2] = (local[i] - l) / cw;
vData[i + 3] = 1 - (local[i + 1] - b) / ch;
}
this.fitUV(vData);
}
//自动适配XY,修改顶点xy数据后需主动调用该函数
private fitXY(vData: Float32Array) {
let m = this.node['_worldMatrix'].m;
let m0 = m[0], m1 = m[1], m4 = m[4], m5 = m[5], m12 = m[12], m13 = m[13];
for (let i = this.xyOffset, len = vData.length, step = this.step, local = this.local; i < len; i += step) {
let x = local[i], y = local[i + 1];
vData[i] = x * m0 + y * m4 + m12;
vData[i + 1] = x * m1 + y * m5 + m13;
}
}
//自动适配UV,修改顶点uv数据后需主动调用该函数
private fitUV(vData: Float32Array) {
let frame = this._spriteFrame;
if (frame === null) return;
let atlasW = frame['_texture'].width, atlasH = frame['_texture'].height;
let frameRect = frame['_rect'];
//计算图集帧在大图中的UV坐标
if (frame['_rotated']) {//如果图集帧发生旋转,计算UV时需回正
for (let i = this.uvOffset, id = 0, len = vData.length, step = this.step; i < len; i += step, ++id) {
let tmp = vData[i];
vData[i] = ((1 - vData[i + 1]) * frameRect.height + frameRect.x) / atlasW;
vData[i + 1] = (tmp * frameRect.width + frameRect.y) / atlasH;
}
} else {//如果图集帧未发生旋转,正常计算即可
for (let i = this.uvOffset, id = 0, len = vData.length, step = this.step; i < len; i += step, ++id) {
vData[i] = (vData[i] * frameRect.width + frameRect.x) / atlasW;
vData[i + 1] = (vData[i + 1] * frameRect.height + frameRect.y) / atlasH;
}
}
}
//计算顶点颜色
private updateColor() {
let uintVData = this.renderData.uintVDatas[0];
let color = this.node.color['_val'];
for (let i = this.colorOffset, len = uintVData.length, step = this.step; i < len; uintVData[i] = color, i += step);
}
//计算顶点索引
private updateIndice() {
let iData = this.renderData.iDatas[0];
for (let i = CENTER_IDATA.length - 1; i > -1; iData[i] = CENTER_IDATA[i--]);
let offset = CENTER_IDATA.length;
let visible = this._corner.visible;
let id = 36;
for (let i = 0; i < 4; ++i) {
if (!visible[i]) continue;
let o = 3 * i;
let a = o + 1;
let b = id / 3;
for (let j = 0, len = this._segment - 1; j < len; ++j) {
iData[offset++] = o;
iData[offset++] = a;
iData[offset++] = b;
a = b++;
id += 3;
}
iData[offset++] = o;
iData[offset++] = a;
iData[offset++] = o + 2;
}
}
//修改节点锚点后,更新顶点数据
private onAnchorChanged() {
this['setVertsDirty']();
}
//修改节点尺寸后,更新顶点数据,并根据sizeMode设置图片宽高
private onSizeChanged() {
this['setVertsDirty']();
if (this._spriteFrame) {
switch (this._sizeMode) {
case SizeMode['TRIMMED: 原始尺寸裁剪透明像素']:
let rect = this._spriteFrame['_rect'].size;
if (this.node.width === rect.width && this.node.height === rect.height) return;
break;
case SizeMode['RAW: 图片原始尺寸']:
let size = this._spriteFrame['_originalSize'];
if (this.node.width === size.width && this.node.height === size.height) return;
break;
}
}
this._sizeMode = SizeMode['CUSTOM: 自定义尺寸'];
}
}