矩形、九宫格与裁剪
做血条、面板、弹窗、滚动列表常用的三样东西。
纯色矩形:ColorRect
ts
import { CanvasLayer, ColorRect, Scene, v } from 'sapling2d'
const BAR_WIDTH = 300
/** 血条:底色 + 填充,都是 ColorRect(原点在左上角,改 scale.x 就从左往右缩)。 */
class HealthBar extends ColorRect {
readonly fill: ColorRect
constructor() {
super({ position: v(40, 60), size: v(BAR_WIDTH, 24), color: 0x302020 })
this.fill = this.add(new ColorRect({ size: v(BAR_WIDTH, 24), color: 0x40d040 }))
}
set ratio(value: number) {
this.fill.scale = v(Math.max(0, Math.min(1, value)), 1)
this.fill.color = value > 0.3 ? 0x40d040 : 0xe04040 // 血少了变红
}
}
export class Battle extends Scene {
hp = 10
bar!: HealthBar
fade!: ColorRect
override ready() {
const hud = this.add(new CanvasLayer())
this.bar = hud.add(new HealthBar())
// 转场黑幕:盖住整个可见区域,透明度从 1 补间到 0
const r = this.tree.viewport.visibleRect
this.fade = hud.add(new ColorRect({ position: r.position, size: r.size, color: 0x000000, zIndex: 100 }))
this.fade.createTween().to(this.fade, { alpha: 0 }, 0.5).call(() => this.fade.queueFree())
}
hit() {
this.hp--
this.bar.ratio = this.hp / 10
}
}ts
for (let i = 0; i < 8; i++) g.scene.hit()
expect(g.scene.bar.fill.scale.x).toBeCloseTo(0.2)
expect(g.scene.bar.fill.color).toBe(0xe04040)
g.stepSeconds(0.6)
expect(g.scene.fade.isFreed).toBe(true) // 黑幕淡出后删掉- 属性:
size、color(0xRRGGBB);透明度用alpha。color和size都能补间。 - 原点在左上角(和
Sprite2D默认居中不同):血条改scale.x,左端固定、从右边缩短。 - 没有
hitArea时,点击区域就是这个矩形(无头模式下也是),可以当简单的按钮底板或遮罩。 - 和贴图一起合批绘制,几十个血条不会增加绘制调用。圆、圆环这类形状用贴图加
modulate。
当按钮或遮罩用时要设 inputPickable: true
不设的话照常画出来,但点不到、也挡不住下面的点击,而且没有任何提示。
九宫格:NineSliceSprite
边框、按钮底板这类要拉伸到不同尺寸的图,用九宫格:四个角不缩放,上下边只横向拉伸,左右边只纵向拉伸,中间两个方向都拉伸。
ts
import { atlas, CanvasLayer, Label, NineSliceSprite, Scene, Signal, v } from 'sapling2d'
// ui.json:TexturePacker 等工具打的界面图集(边框、按钮)
import uiData from './ui.json'
const UI = atlas('ui.png', uiData)
/** 一张升级卡片:木框边框拉成卡片大小,四个角不变形;点一下发出 picked。 */
class Card extends NineSliceSprite {
readonly picked = new Signal()
constructor(title: string) {
// 边框贴图 96×96,四边各 28 像素是不能拉伸的花边
super({ texture: UI.get('panel_wood'), margins: 28, size: v(560, 180), inputPickable: true }) // 没设 hitArea:点击区域就是这个矩形
this.add(new Label({ text: title, fontSize: 40, align: 'center', verticalAlign: 'center', position: v(280, 90) }))
this.clicked.connect(() => this.picked.emit(), this)
}
}
export class Shop extends Scene {
static override assets = { ui: UI }
card!: Card
picks = 0
override ready() {
const hud = this.add(new CanvasLayer())
this.card = hud.add(new Card('多重箭'))
this.card.position = v(95, 500) // 原点在左上角
this.card.picked.connect(() => this.picks++, this)
}
}ts
import { expect, it } from 'vitest'
import { createTestGame } from 'sapling2d/testing'
it('nine slice', async () => {
const g = await createTestGame({ main: Shop })
g.tap(375, 590) // 卡片中间
g.tap(375, 700) // 卡片外面
expect(g.scene.picks).toBe(1)
expect(g.dump()).toContain('Card (Card) position=(95, 500) texture=ui.png#panel_wood size=(560, 180) margins=28')
})- 属性:
texture、margins({ left, top, right, bottom },或一个数字表示四边相同,单位是贴图原始尺寸的像素)、size。 - 原点在左上角,和
ColorRect一样;size能补间。没有hitArea时点击区域就是(0, 0)–size,设inputPickable: true就能当按钮。 - 边框贴图做成正方形、四边是花边、中间可以拉伸的样子,
margins就是花边的宽度。size小于两边边距之和时,四个角按比例缩小。 selfModulate给边框染色(按钮按下时变暗)。图集里裁掉透明边的帧也能用。
裁剪:clipRect
Node2D.clipRect 只画矩形里的部分:
ts
import { ColorRect, Label, Node2D, rect, Scene, v } from 'sapling2d'
/** 商店列表:一个 300×400 的窗口只显示窗口里的卡片,滚出窗口的部分既看不见也点不到。 */
export class Shop extends Scene {
readonly window = new Node2D({ position: v(225, 400), clipRect: rect(0, 0, 300, 400) })
readonly content = new Node2D() // 滚动时移动它
picked = ''
override ready() {
this.add(this.window)
this.window.add(new ColorRect({ size: v(300, 400), color: 0x203040 })) // 窗口底色,和裁剪区一样大
this.window.add(this.content)
for (let i = 0; i < 8; i++) {
const card = this.content.add(new ColorRect({ name: `Card${i}`, position: v(20, 20 + i * 110), size: v(260, 90), color: 0x4060a0, inputPickable: true }))
card.add(new Label({ text: `道具 ${i + 1}`, position: v(16, 28) }))
card.clicked.connect(() => (this.picked = card.name))
}
}
/** 第 i 张卡片滚到窗口顶部(最简单的滚动:直接改内容的 y)。 */
scrollTo(i: number) {
this.content.y = -i * 110
}
}ts
g.tap(375, 465) // 窗口顶部的第一张卡片
expect(g.scene.picked).toBe('Card0')
g.scene.scrollTo(4)
g.tap(375, 465) // 同一个位置现在是第五张
expect(g.scene.picked).toBe('Card4')
g.tap(375, 820) // 第八张卡片的下半部分在窗口外:看不见,也点不到
expect(g.scene.picked).toBe('Card4')
expect(g.dump()).toContain('clipRect=(0, 0, 300, 400)')clipRect是局部坐标的Rect2,默认null(不裁剪)。节点自己的内容和整个子树都被裁,嵌套时取交集;随节点的变换走(旋转、缩放、相机都照常)。Rect2不可变,改范围就赋一个新的。- 拾取也遵守裁剪:被裁掉的部分点不到。被裁掉的位置算空白,会触发
pointerPress()动作。正在拖着的节点滚出裁剪区不会被打断。 - 子树里的
CanvasLayer自成一层,不受祖先裁剪。弹窗里要一起裁掉的东西不要放进 CanvasLayer。 - 用途:滚动列表的窗口、按比例填充的进度条(改填充节点的
clipRect,九宫格不变形)、只露出一部分的图。圆形或贴图形状的遮罩暂不支持。 - 性能:真机实测 10 个裁剪容器约 0.45 ms。