Add image atlas and nine-slice support
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Added atlas source-region and nine-slice image rendering fields for image-capable nodes.
|
||||
- Added Runtime text shadow fields for text-capable nodes.
|
||||
- Fixed Runtime node color alpha composition so `#AARRGGBB` alpha now multiplies with node/runtime alpha instead of being overwritten.
|
||||
|
||||
|
||||
@@ -54,6 +54,21 @@ Text-capable nodes may use flat shadow fields:
|
||||
|
||||
The shadow color alpha is multiplied by `RuntimeNode.alpha` and any runtime animation alpha.
|
||||
|
||||
### Image regions and nine-slice
|
||||
|
||||
Image-capable nodes (`image`, `sprite`, and image-backed `button`) may draw only part of an asset by setting source region fields in image pixels:
|
||||
|
||||
- `sourceX` / `sourceY`: top-left source position inside the loaded image.
|
||||
- `sourceWidth` / `sourceHeight`: source region size.
|
||||
|
||||
When source fields are omitted, the full image is used. This supports atlas-style usage where multiple sprites are packed into one image and nodes reference different source rectangles.
|
||||
|
||||
Image-capable nodes may also use nine-slice scaling with source-pixel insets:
|
||||
|
||||
- `sliceLeft` / `sliceTop` / `sliceRight` / `sliceBottom`.
|
||||
|
||||
Nine-slice keeps corners unscaled, stretches edges on one axis, and stretches the center on both axes. Insets are clamped to the selected source region and destination size.
|
||||
|
||||
## RuntimeCommand
|
||||
|
||||
Runtime commands request generic side effects owned by Dart/Flame.
|
||||
|
||||
@@ -86,6 +86,14 @@
|
||||
---@field type RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
@@ -147,6 +155,14 @@
|
||||
---@field type? RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
|
||||
@@ -86,6 +86,14 @@
|
||||
---@field type RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
@@ -147,6 +155,14 @@
|
||||
---@field type? RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
|
||||
@@ -86,6 +86,14 @@
|
||||
---@field type RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
@@ -147,6 +155,14 @@
|
||||
---@field type? RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
|
||||
@@ -86,6 +86,14 @@
|
||||
---@field type RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
@@ -147,6 +155,14 @@
|
||||
---@field type? RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
|
||||
@@ -8,6 +8,14 @@ class RuntimeNode {
|
||||
required this.type,
|
||||
this.parent,
|
||||
this.asset,
|
||||
this.sourceX,
|
||||
this.sourceY,
|
||||
this.sourceWidth,
|
||||
this.sourceHeight,
|
||||
this.sliceLeft,
|
||||
this.sliceTop,
|
||||
this.sliceRight,
|
||||
this.sliceBottom,
|
||||
this.pressedAsset,
|
||||
this.disabledAsset,
|
||||
this.animation,
|
||||
@@ -70,6 +78,14 @@ class RuntimeNode {
|
||||
final String type;
|
||||
final String? parent;
|
||||
final String? asset;
|
||||
final double? sourceX;
|
||||
final double? sourceY;
|
||||
final double? sourceWidth;
|
||||
final double? sourceHeight;
|
||||
final double? sliceLeft;
|
||||
final double? sliceTop;
|
||||
final double? sliceRight;
|
||||
final double? sliceBottom;
|
||||
final String? pressedAsset;
|
||||
final String? disabledAsset;
|
||||
final String? animation;
|
||||
@@ -213,6 +229,26 @@ class RuntimeNode {
|
||||
type: nextType,
|
||||
parent: _parentProp(props, currentParent: parent, nodeId: id),
|
||||
asset: _stringProp(props, RuntimeProtocolField.asset) ?? asset,
|
||||
sourceX: _nonNegativeDoubleProp(props, RuntimeProtocolField.sourceX) ?? sourceX,
|
||||
sourceY: _nonNegativeDoubleProp(props, RuntimeProtocolField.sourceY) ?? sourceY,
|
||||
sourceWidth:
|
||||
_positiveDoubleProp(props, RuntimeProtocolField.sourceWidth) ??
|
||||
sourceWidth,
|
||||
sourceHeight:
|
||||
_positiveDoubleProp(props, RuntimeProtocolField.sourceHeight) ??
|
||||
sourceHeight,
|
||||
sliceLeft:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.sliceLeft) ??
|
||||
sliceLeft,
|
||||
sliceTop:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.sliceTop) ??
|
||||
sliceTop,
|
||||
sliceRight:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.sliceRight) ??
|
||||
sliceRight,
|
||||
sliceBottom:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.sliceBottom) ??
|
||||
sliceBottom,
|
||||
pressedAsset:
|
||||
_stringProp(props, RuntimeProtocolField.pressedAsset) ?? pressedAsset,
|
||||
disabledAsset:
|
||||
@@ -345,6 +381,20 @@ class RuntimeNode {
|
||||
nodeId: _requiredString(map, RuntimeProtocolField.id),
|
||||
),
|
||||
asset: _stringProp(map, RuntimeProtocolField.asset),
|
||||
sourceX: _nonNegativeDoubleProp(map, RuntimeProtocolField.sourceX),
|
||||
sourceY: _nonNegativeDoubleProp(map, RuntimeProtocolField.sourceY),
|
||||
sourceWidth: _positiveDoubleProp(map, RuntimeProtocolField.sourceWidth),
|
||||
sourceHeight: _positiveDoubleProp(
|
||||
map,
|
||||
RuntimeProtocolField.sourceHeight,
|
||||
),
|
||||
sliceLeft: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceLeft),
|
||||
sliceTop: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceTop),
|
||||
sliceRight: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceRight),
|
||||
sliceBottom: _nonNegativeDoubleProp(
|
||||
map,
|
||||
RuntimeProtocolField.sliceBottom,
|
||||
),
|
||||
pressedAsset: _stringProp(map, RuntimeProtocolField.pressedAsset),
|
||||
disabledAsset: _stringProp(map, RuntimeProtocolField.disabledAsset),
|
||||
animation: _stringProp(map, RuntimeProtocolField.animation),
|
||||
@@ -530,6 +580,17 @@ class RuntimeNode {
|
||||
return value;
|
||||
}
|
||||
|
||||
static double? _positiveDoubleProp(Map<String, Object?> map, String key) {
|
||||
final value = _doubleProp(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value <= 0) {
|
||||
throw FormatException('RuntimeNode.$key must be > 0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static double? _scrollProp(
|
||||
Map<String, Object?> map,
|
||||
String key, {
|
||||
|
||||
@@ -138,6 +138,14 @@ class RuntimeProtocolField {
|
||||
static const target = 'target';
|
||||
static const parent = 'parent';
|
||||
static const asset = 'asset';
|
||||
static const sourceX = 'sourceX';
|
||||
static const sourceY = 'sourceY';
|
||||
static const sourceWidth = 'sourceWidth';
|
||||
static const sourceHeight = 'sourceHeight';
|
||||
static const sliceLeft = 'sliceLeft';
|
||||
static const sliceTop = 'sliceTop';
|
||||
static const sliceRight = 'sliceRight';
|
||||
static const sliceBottom = 'sliceBottom';
|
||||
static const pressedAsset = 'pressedAsset';
|
||||
static const disabledAsset = 'disabledAsset';
|
||||
static const animation = 'animation';
|
||||
@@ -225,6 +233,14 @@ class RuntimeProtocolSchema {
|
||||
RuntimeProtocolField.type,
|
||||
RuntimeProtocolField.parent,
|
||||
RuntimeProtocolField.asset,
|
||||
RuntimeProtocolField.sourceX,
|
||||
RuntimeProtocolField.sourceY,
|
||||
RuntimeProtocolField.sourceWidth,
|
||||
RuntimeProtocolField.sourceHeight,
|
||||
RuntimeProtocolField.sliceLeft,
|
||||
RuntimeProtocolField.sliceTop,
|
||||
RuntimeProtocolField.sliceRight,
|
||||
RuntimeProtocolField.sliceBottom,
|
||||
RuntimeProtocolField.pressedAsset,
|
||||
RuntimeProtocolField.disabledAsset,
|
||||
RuntimeProtocolField.animation,
|
||||
@@ -294,6 +310,14 @@ class RuntimeProtocolSchema {
|
||||
RuntimeProtocolField.type,
|
||||
RuntimeProtocolField.parent,
|
||||
RuntimeProtocolField.asset,
|
||||
RuntimeProtocolField.sourceX,
|
||||
RuntimeProtocolField.sourceY,
|
||||
RuntimeProtocolField.sourceWidth,
|
||||
RuntimeProtocolField.sourceHeight,
|
||||
RuntimeProtocolField.sliceLeft,
|
||||
RuntimeProtocolField.sliceTop,
|
||||
RuntimeProtocolField.sliceRight,
|
||||
RuntimeProtocolField.sliceBottom,
|
||||
RuntimeProtocolField.pressedAsset,
|
||||
RuntimeProtocolField.disabledAsset,
|
||||
RuntimeProtocolField.animation,
|
||||
|
||||
@@ -17,6 +17,102 @@ Color composeRuntimeColorAlpha(Color color, double alpha) {
|
||||
return color.withValues(alpha: color.a * alpha.clamp(0.0, 1.0));
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Rect runtimeImageSourceRect({
|
||||
required double imageWidth,
|
||||
required double imageHeight,
|
||||
double? sourceX,
|
||||
double? sourceY,
|
||||
double? sourceWidth,
|
||||
double? sourceHeight,
|
||||
}) {
|
||||
final x = (sourceX ?? 0).clamp(0.0, imageWidth).toDouble();
|
||||
final y = (sourceY ?? 0).clamp(0.0, imageHeight).toDouble();
|
||||
final maxWidth = imageWidth - x;
|
||||
final maxHeight = imageHeight - y;
|
||||
final width = (sourceWidth ?? maxWidth).clamp(0.0, maxWidth).toDouble();
|
||||
final height = (sourceHeight ?? maxHeight).clamp(0.0, maxHeight).toDouble();
|
||||
return Rect.fromLTWH(x, y, width, height);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
List<({Rect source, Rect destination})> runtimeNineSliceRects({
|
||||
required Rect source,
|
||||
required Rect destination,
|
||||
double sliceLeft = 0,
|
||||
double sliceTop = 0,
|
||||
double sliceRight = 0,
|
||||
double sliceBottom = 0,
|
||||
}) {
|
||||
if (source.width <= 0 ||
|
||||
source.height <= 0 ||
|
||||
destination.width <= 0 ||
|
||||
destination.height <= 0) {
|
||||
return const [];
|
||||
}
|
||||
final left = sliceLeft.clamp(0.0, source.width).toDouble();
|
||||
final top = sliceTop.clamp(0.0, source.height).toDouble();
|
||||
final right = sliceRight.clamp(0.0, source.width - left).toDouble();
|
||||
final bottom = sliceBottom.clamp(0.0, source.height - top).toDouble();
|
||||
final destLeft = left.clamp(0.0, destination.width).toDouble();
|
||||
final destTop = top.clamp(0.0, destination.height).toDouble();
|
||||
final destRight = right.clamp(0.0, destination.width - destLeft).toDouble();
|
||||
final destBottom = bottom
|
||||
.clamp(0.0, destination.height - destTop)
|
||||
.toDouble();
|
||||
|
||||
final sourceXs = [
|
||||
source.left,
|
||||
source.left + left,
|
||||
source.right - right,
|
||||
source.right,
|
||||
];
|
||||
final sourceYs = [
|
||||
source.top,
|
||||
source.top + top,
|
||||
source.bottom - bottom,
|
||||
source.bottom,
|
||||
];
|
||||
final destXs = [
|
||||
destination.left,
|
||||
destination.left + destLeft,
|
||||
destination.right - destRight,
|
||||
destination.right,
|
||||
];
|
||||
final destYs = [
|
||||
destination.top,
|
||||
destination.top + destTop,
|
||||
destination.bottom - destBottom,
|
||||
destination.bottom,
|
||||
];
|
||||
|
||||
final parts = <({Rect source, Rect destination})>[];
|
||||
for (var y = 0; y < 3; y++) {
|
||||
for (var x = 0; x < 3; x++) {
|
||||
final sourcePart = Rect.fromLTRB(
|
||||
sourceXs[x],
|
||||
sourceYs[y],
|
||||
sourceXs[x + 1],
|
||||
sourceYs[y + 1],
|
||||
);
|
||||
final destPart = Rect.fromLTRB(
|
||||
destXs[x],
|
||||
destYs[y],
|
||||
destXs[x + 1],
|
||||
destYs[y + 1],
|
||||
);
|
||||
if (sourcePart.width <= 0 ||
|
||||
sourcePart.height <= 0 ||
|
||||
destPart.width <= 0 ||
|
||||
destPart.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
parts.add((source: sourcePart, destination: destPart));
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
class RuntimeComponent extends PositionComponent
|
||||
with HasVisibility, TapCallbacks {
|
||||
RuntimeComponent({
|
||||
@@ -431,12 +527,14 @@ class RuntimeComponent extends PositionComponent
|
||||
(_node.type == RuntimeNodeType.sprite ||
|
||||
_node.type == RuntimeNodeType.image ||
|
||||
_node.type == RuntimeNodeType.button)) {
|
||||
canvas.drawImageRect(
|
||||
image,
|
||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
|
||||
rect,
|
||||
Paint()..color = composeRuntimeColorAlpha(Colors.white, renderAlpha),
|
||||
);
|
||||
final imagePaint = Paint()
|
||||
..color = composeRuntimeColorAlpha(Colors.white, renderAlpha);
|
||||
final source = _imageSourceRect(image);
|
||||
if (_usesNineSlice(source, rect)) {
|
||||
_drawNineSliceImage(canvas, image, source, rect, imagePaint);
|
||||
} else {
|
||||
canvas.drawImageRect(image, source, rect, imagePaint);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -448,6 +546,50 @@ class RuntimeComponent extends PositionComponent
|
||||
);
|
||||
}
|
||||
|
||||
Rect _imageSourceRect(ui.Image image) {
|
||||
return runtimeImageSourceRect(
|
||||
imageWidth: image.width.toDouble(),
|
||||
imageHeight: image.height.toDouble(),
|
||||
sourceX: _node.sourceX,
|
||||
sourceY: _node.sourceY,
|
||||
sourceWidth: _node.sourceWidth,
|
||||
sourceHeight: _node.sourceHeight,
|
||||
);
|
||||
}
|
||||
|
||||
bool _usesNineSlice(Rect source, Rect destination) {
|
||||
if (source.width <= 0 ||
|
||||
source.height <= 0 ||
|
||||
destination.width <= 0 ||
|
||||
destination.height <= 0) {
|
||||
return false;
|
||||
}
|
||||
return (_node.sliceLeft ?? 0) > 0 ||
|
||||
(_node.sliceTop ?? 0) > 0 ||
|
||||
(_node.sliceRight ?? 0) > 0 ||
|
||||
(_node.sliceBottom ?? 0) > 0;
|
||||
}
|
||||
|
||||
void _drawNineSliceImage(
|
||||
Canvas canvas,
|
||||
ui.Image image,
|
||||
Rect source,
|
||||
Rect destination,
|
||||
Paint paint,
|
||||
) {
|
||||
final parts = runtimeNineSliceRects(
|
||||
source: source,
|
||||
destination: destination,
|
||||
sliceLeft: _node.sliceLeft ?? 0,
|
||||
sliceTop: _node.sliceTop ?? 0,
|
||||
sliceRight: _node.sliceRight ?? 0,
|
||||
sliceBottom: _node.sliceBottom ?? 0,
|
||||
);
|
||||
for (final part in parts) {
|
||||
canvas.drawImageRect(image, part.source, part.destination, paint);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBase(RuntimeNode node) {
|
||||
_syncVisibility();
|
||||
size = Vector2(node.width ?? 40, node.height ?? 40);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -86,6 +86,14 @@
|
||||
---@field type RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
@@ -147,6 +155,14 @@
|
||||
---@field type? RuntimeNodeType
|
||||
---@field parent? string
|
||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||
---@field sourceX? number Source atlas region x in image pixels.
|
||||
---@field sourceY? number Source atlas region y in image pixels.
|
||||
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||
---@field pressedAsset? string Button pressed-state image asset key.
|
||||
---@field disabledAsset? string Button disabled-state image asset key.
|
||||
---@field animation? string
|
||||
|
||||
Reference in New Issue
Block a user