CustomMask.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { _decorator, Component, Mask, Graphics, UITransform } from 'cc';
  2. const { ccclass, property, executeInEditMode, requireComponent } = _decorator;
  3. @ccclass('CustomMask')
  4. @requireComponent(Mask)
  5. @executeInEditMode // 允许在编辑器内预览绘制效果
  6. export class CustomMask extends Component {
  7. @property({ tooltip: '左上角圆角半径(像素)' })
  8. leftTopRadius = 0;
  9. @property({ tooltip: '右上角圆角半径(像素)' })
  10. rightTopRadius = 20;
  11. @property({ tooltip: '右下角圆角半径(像素)' })
  12. rightBottomRadius = 20;
  13. @property({ tooltip: '左下角圆角半径(像素)' })
  14. leftBottomRadius = 0;
  15. private _mask: Mask | null = null;
  16. onEnable() {
  17. this.updateMask();
  18. }
  19. // 当在编辑器中修改属性时自动刷新
  20. onValidate() {
  21. this.updateMask();
  22. }
  23. /** 绘制四个圆角可独立设置的遮罩路径。 */
  24. public updateMask() {
  25. this._mask = this.getComponent(Mask);
  26. if (!this._mask) return;
  27. // 自定义模板遮罩,避免矩形模式覆盖自定义路径。
  28. this._mask.type = Mask.Type.GRAPHICS_STENCIL;
  29. // 获取或添加 Graphics 组件
  30. let g = this.getComponent(Graphics);
  31. if (!g) {
  32. g = this.addComponent(Graphics);
  33. }
  34. const uiTrans = this.getComponent(UITransform);
  35. if (!uiTrans) return;
  36. const width = uiTrans.width;
  37. const height = uiTrans.height;
  38. const anchorX = uiTrans.anchorX;
  39. const anchorY = uiTrans.anchorY;
  40. // 根据锚点计算左下角起始坐标 (x, y)
  41. const x = -width * anchorX;
  42. const y = -height * anchorY;
  43. const leftTop = this.getRadius(this.leftTopRadius, width, height);
  44. const rightTop = this.getRadius(this.rightTopRadius, width, height);
  45. const rightBottom = this.getRadius(this.rightBottomRadius, width, height);
  46. const leftBottom = this.getRadius(this.leftBottomRadius, width, height);
  47. // 清除旧路径
  48. g.clear();
  49. // 从底边开始逆时针绘制,所有圆角保持在同一条闭合路径中。
  50. g.moveTo(x + leftBottom, y);
  51. g.lineTo(x + width - rightBottom, y);
  52. this.drawArc(g, x + width - rightBottom, y + rightBottom, rightBottom, -Math.PI / 2, 0);
  53. g.lineTo(x + width, y + height - rightTop);
  54. this.drawArc(g, x + width - rightTop, y + height - rightTop, rightTop, 0, Math.PI / 2);
  55. g.lineTo(x + leftTop, y + height);
  56. this.drawArc(g, x + leftTop, y + height - leftTop, leftTop, Math.PI / 2, Math.PI);
  57. g.lineTo(x, y + leftBottom);
  58. this.drawArc(g, x + leftBottom, y + leftBottom, leftBottom, Math.PI, Math.PI * 1.5);
  59. g.close();
  60. g.fill();
  61. }
  62. private getRadius(radius: number, width: number, height: number): number {
  63. if (!Number.isFinite(radius)) return 0;
  64. return Math.max(0, Math.min(radius, width / 2, height / 2));
  65. }
  66. private drawArc(
  67. graphics: Graphics,
  68. centerX: number,
  69. centerY: number,
  70. radius: number,
  71. startAngle: number,
  72. endAngle: number,
  73. ): void {
  74. if (radius > 0) {
  75. // Graphics.arc 内部会 moveTo,不能用于连续的圆角轮廓。
  76. // 使用三次贝塞尔近似四分之一圆,控制点沿端点切线排列。
  77. const tangent = radius * 4 / 3 * Math.tan((endAngle - startAngle) / 4);
  78. const startX = centerX + radius * Math.cos(startAngle);
  79. const startY = centerY + radius * Math.sin(startAngle);
  80. const endX = centerX + radius * Math.cos(endAngle);
  81. const endY = centerY + radius * Math.sin(endAngle);
  82. graphics.bezierCurveTo(
  83. startX - tangent * Math.sin(startAngle),
  84. startY + tangent * Math.cos(startAngle),
  85. endX + tangent * Math.sin(endAngle),
  86. endY - tangent * Math.cos(endAngle),
  87. endX,
  88. endY,
  89. );
  90. }
  91. }
  92. }