Add image atlas and nine-slice support
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user