Add image atlas and nine-slice support

This commit is contained in:
gem
2026-06-09 12:30:44 +08:00
parent 409942b4af
commit e2a584d4dc
12 changed files with 453 additions and 24 deletions

View File

@@ -10,6 +10,14 @@ void main() {
'type': 'button',
'parent': 'top_bar',
'asset': 'dice_normal',
'sourceX': 4,
'sourceY': 5,
'sourceWidth': 64,
'sourceHeight': 32,
'sliceLeft': 6,
'sliceTop': 7,
'sliceRight': 8,
'sliceBottom': 9,
'pressedAsset': 'dice_pressed',
'disabledAsset': 'dice_disabled',
'animation': 'idle',
@@ -72,6 +80,14 @@ void main() {
expect(node.type, 'button');
expect(node.parent, 'top_bar');
expect(node.asset, 'dice_normal');
expect(node.sourceX, 4);
expect(node.sourceY, 5);
expect(node.sourceWidth, 64);
expect(node.sourceHeight, 32);
expect(node.sliceLeft, 6);
expect(node.sliceTop, 7);
expect(node.sliceRight, 8);
expect(node.sliceBottom, 9);
expect(node.pressedAsset, 'dice_pressed');
expect(node.disabledAsset, 'dice_disabled');
expect(node.animation, 'idle');
@@ -147,6 +163,14 @@ void main() {
expect(node.textShadowOffsetX, isNull);
expect(node.textShadowOffsetY, isNull);
expect(node.textShadowBlur, isNull);
expect(node.sourceX, isNull);
expect(node.sourceY, isNull);
expect(node.sourceWidth, isNull);
expect(node.sourceHeight, isNull);
expect(node.sliceLeft, isNull);
expect(node.sliceTop, isNull);
expect(node.sliceRight, isNull);
expect(node.sliceBottom, isNull);
expect(node.scrollbarVisible, isTrue);
expect(node.paddingLeft, 0);
expect(node.paddingTop, 0);
@@ -182,6 +206,14 @@ void main() {
'paddingBottom': 11,
'contentWidth': 120,
'contentHeight': 100,
'sourceX': 3,
'sourceY': 4,
'sourceWidth': 40,
'sourceHeight': 41,
'sliceLeft': 5,
'sliceTop': 6,
'sliceRight': 7,
'sliceBottom': 8,
'pressedAsset': 'button_pressed',
'disabledAsset': 'button_disabled',
'scrollX': 90,
@@ -213,6 +245,14 @@ void main() {
expect(updated.paddingBottom, 11);
expect(updated.contentWidth, 120);
expect(updated.contentHeight, 100);
expect(updated.sourceX, 3);
expect(updated.sourceY, 4);
expect(updated.sourceWidth, 40);
expect(updated.sourceHeight, 41);
expect(updated.sliceLeft, 5);
expect(updated.sliceTop, 6);
expect(updated.sliceRight, 7);
expect(updated.sliceBottom, 8);
expect(updated.pressedAsset, 'button_pressed');
expect(updated.disabledAsset, 'button_disabled');
expect(updated.scrollX, 68);
@@ -255,6 +295,22 @@ void main() {
() => RuntimeNode.fromMap({'id': 'a', 'type': 'progress', 'value': 2}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'image',
'sourceWidth': 0,
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'image',
'sliceLeft': -1,
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',

View File

@@ -4,6 +4,7 @@ import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart';
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
import 'package:flutter/material.dart' show Color;
import 'package:flutter/rendering.dart' show Rect;
import 'package:flutter_test/flutter_test.dart';
void main() {
@@ -138,6 +139,50 @@ void main() {
);
});
test('computes atlas source region and clamps to image bounds', () {
expect(
runtimeImageSourceRect(
imageWidth: 100,
imageHeight: 80,
sourceX: 10,
sourceY: 12,
sourceWidth: 30,
sourceHeight: 20,
),
const Rect.fromLTWH(10, 12, 30, 20),
);
expect(
runtimeImageSourceRect(
imageWidth: 100,
imageHeight: 80,
sourceX: 90,
sourceY: 70,
sourceWidth: 30,
sourceHeight: 20,
),
const Rect.fromLTWH(90, 70, 10, 10),
);
});
test('computes nine-slice source and destination rects', () {
final parts = runtimeNineSliceRects(
source: const Rect.fromLTWH(10, 20, 30, 40),
destination: const Rect.fromLTWH(0, 0, 90, 120),
sliceLeft: 5,
sliceTop: 6,
sliceRight: 7,
sliceBottom: 8,
);
expect(parts, hasLength(9));
expect(parts.first.source, const Rect.fromLTRB(10, 20, 15, 26));
expect(parts.first.destination, const Rect.fromLTRB(0, 0, 5, 6));
expect(parts[4].source, const Rect.fromLTRB(15, 26, 33, 52));
expect(parts[4].destination, const Rect.fromLTRB(5, 6, 83, 112));
expect(parts.last.source, const Rect.fromLTRB(33, 52, 40, 60));
expect(parts.last.destination, const Rect.fromLTRB(83, 112, 90, 120));
});
test('updates text alpha style without rebuilding text component', () {
final component = RuntimeComponent(
node: const RuntimeNode(
@@ -161,27 +206,32 @@ void main() {
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
});
test('updates button text alpha style without rebuilding text component', () {
final component = RuntimeComponent(
node: const RuntimeNode(
id: 'button',
type: RuntimeNodeType.button,
text: 'Fade me',
alpha: 0,
),
resources: GameResourceManager(),
onNodeTap: (_, __) {},
);
test(
'updates button text alpha style without rebuilding text component',
() {
final component = RuntimeComponent(
node: const RuntimeNode(
id: 'button',
type: RuntimeNodeType.button,
text: 'Fade me',
alpha: 0,
),
resources: GameResourceManager(),
onNodeTap: (_, __) {},
);
final text = component.children.whereType<TextComponent>().single;
expect(((text.textRenderer as TextPaint).style.color!).a, 0);
final text = component.children.whereType<TextComponent>().single;
expect(((text.textRenderer as TextPaint).style.color!).a, 0);
component.setRuntimeAlpha(1);
component.setRuntimeAlpha(1);
final updatedText = component.children.whereType<TextComponent>().single;
expect(identical(updatedText, text), isTrue);
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
});
final updatedText = component.children
.whereType<TextComponent>()
.single;
expect(identical(updatedText, text), isTrue);
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
},
);
test('applies text shadow style', () {
final component = RuntimeComponent(