diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adcc61..deaafb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Added TexturePacker frame, manual source-region, and nine-slice image rendering fields for image-capable nodes. -- Fixed nine-slice image seams by overlapping destination slices during runtime rendering. +- Fixed nine-slice image seams by overlapping destination slices and using inset source sampling during runtime rendering. - Fixed Runtime alpha inheritance so parent fade commands apply to the full child subtree. - 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. diff --git a/lib/runtime/rendering/runtime_component.dart b/lib/runtime/rendering/runtime_component.dart index 42484f5..39111ee 100644 --- a/lib/runtime/rendering/runtime_component.dart +++ b/lib/runtime/rendering/runtime_component.dart @@ -44,6 +44,7 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({ double sliceRight = 0, double sliceBottom = 0, double destinationOverlap = 0, + double sourceInset = 0, }) { if (source.width <= 0 || source.height <= 0 || @@ -88,7 +89,7 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({ final parts = <({Rect source, Rect destination})>[]; for (var y = 0; y < 3; y++) { for (var x = 0; x < 3; x++) { - final sourcePart = Rect.fromLTRB( + final rawSourcePart = Rect.fromLTRB( sourceXs[x], sourceYs[y], sourceXs[x + 1], @@ -100,12 +101,17 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({ destXs[x + 1], destYs[y + 1], ); - if (sourcePart.width <= 0 || - sourcePart.height <= 0 || + if (rawSourcePart.width <= 0 || + rawSourcePart.height <= 0 || rawDestPart.width <= 0 || rawDestPart.height <= 0) { continue; } + final sourcePart = _insetNineSliceSourceRect( + rawSourcePart, + bounds: source, + inset: sourceInset, + ); final destPart = _overlapNineSliceDestinationRect( rawDestPart, x: x, @@ -119,6 +125,22 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({ return parts; } +Rect _insetNineSliceSourceRect( + Rect rect, { + required Rect bounds, + required double inset, +}) { + if (inset <= 0 || rect.width <= inset * 2 || rect.height <= inset * 2) { + return rect; + } + return Rect.fromLTRB( + math.min(rect.right, math.max(bounds.left, rect.left + inset)), + math.min(rect.bottom, math.max(bounds.top, rect.top + inset)), + math.max(rect.left, math.min(bounds.right, rect.right - inset)), + math.max(rect.top, math.min(bounds.bottom, rect.bottom - inset)), + ); +} + Rect _overlapNineSliceDestinationRect( Rect rect, { required int x, @@ -575,7 +597,13 @@ class RuntimeComponent extends PositionComponent ..color = composeRuntimeColorAlpha(Colors.white, renderAlpha); final source = _imageSourceRect(image, _currentImageFrame(_node)); if (_usesNineSlice(source, rect)) { - _drawNineSliceImage(canvas, image, source, rect, imagePaint); + _drawNineSliceImage( + canvas, + image, + source, + rect, + imagePaint..filterQuality = FilterQuality.none, + ); } else { canvas.drawImageRect(image, source, rect, imagePaint); } @@ -630,7 +658,8 @@ class RuntimeComponent extends PositionComponent final parts = runtimeNineSliceRects( source: source, destination: destination, - destinationOverlap: 0.5, + destinationOverlap: 1, + sourceInset: 0.5, sliceLeft: _node.sliceLeft ?? 0, sliceTop: _node.sliceTop ?? 0, sliceRight: _node.sliceRight ?? 0, diff --git a/test/runtime/rendering/runtime_component_test.dart b/test/runtime/rendering/runtime_component_test.dart index c2b3848..fcca967 100644 --- a/test/runtime/rendering/runtime_component_test.dart +++ b/test/runtime/rendering/runtime_component_test.dart @@ -284,6 +284,36 @@ void main() { expect(parts[4].source, const Rect.fromLTRB(10, 10, 20, 20)); }); + test('insets nine-slice source rects to avoid atlas edge sampling', () { + final parts = runtimeNineSliceRects( + source: const Rect.fromLTWH(10, 20, 30, 30), + destination: const Rect.fromLTWH(0, 0, 90, 90), + sliceLeft: 10, + sliceTop: 10, + sliceRight: 10, + sliceBottom: 10, + sourceInset: 0.5, + ); + + expect(parts.first.source, const Rect.fromLTRB(10.5, 20.5, 19.5, 29.5)); + expect(parts[4].source, const Rect.fromLTRB(20.5, 30.5, 29.5, 39.5)); + expect(parts.last.source, const Rect.fromLTRB(30.5, 40.5, 39.5, 49.5)); + }); + + test('keeps tiny nine-slice source rects when inset would collapse them', () { + final parts = runtimeNineSliceRects( + source: const Rect.fromLTWH(0, 0, 3, 3), + destination: const Rect.fromLTWH(0, 0, 30, 30), + sliceLeft: 1, + sliceTop: 1, + sliceRight: 1, + sliceBottom: 1, + sourceInset: 0.5, + ); + + expect(parts[4].source, const Rect.fromLTRB(1, 1, 2, 2)); + }); + test('updates text alpha style without rebuilding text component', () { final component = RuntimeComponent( node: const RuntimeNode(