feat: add hex trail puzzle, level editor, and visual design guide
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { _decorator, Component, Node, Sprite, SpriteFrame, Texture2D, UITransform, Mask, Graphics, Rect, Size, Vec2 } from 'cc';
|
||||
// Copy src/ui-skin.js beside the shared core; do not re-enter crop values by hand.
|
||||
import { PANELS, ICONS } from '../src/ui-skin.js';
|
||||
const { ccclass, property } = _decorator;
|
||||
type FrameRect = { x:number; y:number; w:number; h:number };
|
||||
/** Apply to any Cocos UI node. Child labels stay native/localizable. */
|
||||
@ccclass('CandyUiSkin')
|
||||
export class CandyUiSkin extends Component {
|
||||
@property(Texture2D) panels: Texture2D | null = null;
|
||||
@property(Texture2D) icons: Texture2D | null = null;
|
||||
@property panel = 'surface';
|
||||
@property icon = '';
|
||||
@property corner = 14;
|
||||
private panelFrame: SpriteFrame | null = null;
|
||||
private iconFrame: SpriteFrame | null = null;
|
||||
private plate: Node | null = null;
|
||||
private fill: Node | null = null;
|
||||
private glyph: Node | null = null;
|
||||
onLoad() {
|
||||
this.refresh();
|
||||
this.node.on(Node.EventType.SIZE_CHANGED, this.refresh, this);
|
||||
this.node.on(Node.EventType.ANCHOR_CHANGED, this.refresh, this);
|
||||
}
|
||||
onDestroy() {
|
||||
this.node.off(Node.EventType.SIZE_CHANGED, this.refresh, this);
|
||||
this.node.off(Node.EventType.ANCHOR_CHANGED, this.refresh, this);
|
||||
for(const n of [this.fill,this.glyph]) {
|
||||
if(n?.isValid){const sp=n.getComponent(Sprite);if(sp)sp.spriteFrame=null;}
|
||||
}
|
||||
this.panelFrame?.destroy();this.iconFrame?.destroy();
|
||||
this.plate?.destroy();this.glyph?.destroy();
|
||||
}
|
||||
private frame(sprite:Sprite, texture:Texture2D, f:FrameRect, current:SpriteFrame | null) {
|
||||
const sf=current || new SpriteFrame(),r=sf.rect;
|
||||
if(sf.texture!==texture || r.x!==f.x || r.y!==f.y || r.width!==f.w || r.height!==f.h) {
|
||||
// Each component owns at most two frames, reused across style changes.
|
||||
sprite.spriteFrame=null;
|
||||
sf.texture=texture;sf.rect=new Rect(f.x,f.y,f.w,f.h);
|
||||
sf.originalSize=new Size(f.w,f.h);sf.offset=new Vec2();sf.packable=false;
|
||||
}
|
||||
sprite.spriteFrame=sf;return sf;
|
||||
}
|
||||
private child(name:string, parent=this.node, sprite=true) {
|
||||
const n=new Node(name);n.layer=this.node.layer;n.parent=parent;
|
||||
n.addComponent(UITransform);
|
||||
if(sprite){const sp=n.addComponent(Sprite);sp.sizeMode=Sprite.SizeMode.CUSTOM;sp.type=Sprite.Type.SIMPLE;}
|
||||
return n;
|
||||
}
|
||||
refresh() {
|
||||
const size=this.getComponent(UITransform);if(!size)return;
|
||||
const width=Math.max(0,size.width),height=Math.max(0,size.height);
|
||||
const x=(.5-size.anchorX)*width,y=(.5-size.anchorY)*height;
|
||||
if(this.panels && PANELS[this.panel]) {
|
||||
if(!this.plate) {
|
||||
this.plate=this.child('Panel clip',this.node,false);
|
||||
this.plate.addComponent(Mask).type=Mask.Type.GRAPHICS_STENCIL;
|
||||
this.fill=this.child('Panel image',this.plate);
|
||||
}
|
||||
this.plate.active=true;this.plate.layer=this.fill!.layer=this.node.layer;
|
||||
this.plate.setSiblingIndex(0);this.plate.setPosition(x,y);
|
||||
this.plate.getComponent(UITransform)!.setContentSize(width,height);
|
||||
this.fill!.getComponent(UITransform)!.setContentSize(width,height);
|
||||
this.panelFrame=this.frame(this.fill!.getComponent(Sprite)!,this.panels,PANELS[this.panel],this.panelFrame);
|
||||
// The 32px PNG contains a baked flat role color. Clip geometry supplies
|
||||
// the corner radius; cap=0 is intentional and never used as a divisor.
|
||||
const g=this.plate.getComponent(Mask)!.subComp as Graphics;
|
||||
g.clear();
|
||||
if(width && height){g.roundRect(-width/2,-height/2,width,height,Math.max(0,Math.min(this.corner,width/2,height/2)));g.fill();}
|
||||
}else if(this.plate)this.plate.active=false;
|
||||
if(this.icons && ICONS[this.icon]) {
|
||||
this.glyph??=this.child('Image icon');const sp=this.glyph.getComponent(Sprite)!;
|
||||
this.glyph.active=true;this.glyph.layer=this.node.layer;
|
||||
const f=ICONS[this.icon];this.iconFrame=this.frame(sp,this.icons,f,this.iconFrame);
|
||||
const scale=Math.min(width/f.w,height/f.h);
|
||||
this.glyph.getComponent(UITransform)!.setContentSize(f.w*scale,f.h*scale);
|
||||
this.glyph.setPosition(x,y);
|
||||
}else if(this.glyph)this.glyph.active=false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user