Skip to content

Commit

Permalink
feat: Customise grid of NineTileBox (#2495)
Browse files Browse the repository at this point in the history
The previous implementation of the NineTileBox calculates identically sized tiles in a 3x3 grid and does not allow the user to customise this. For example, a 60x60 pixel sprite will be cut into 20x20 pixel tiles. This MR allows the user to specify the sizes of the fixed-width and fixed-height rows and columns so that a completely custom grid is possible.

Example with the following sprite and custom grid sizes.
Note that the stretchable row and column are only 1 pixel wide/high in this example.
  • Loading branch information
projectitis authored Apr 19, 2023
1 parent 87b8a06 commit a25b0a0
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 4 deletions.
Binary file added examples/assets/images/speech-bubble.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';

class NineTileBoxCustomGridExample extends FlameGame
with TapDetector, DoubleTapDetector {
static const String description = '''
If you want to create a background for something that can stretch you can
use the `NineTileBox` which is showcased here. In this example a custom
grid is used.\n\n
Tap to make the box bigger and double tap to make it smaller.
''';

late NineTileBoxComponent nineTileBoxComponent;

@override
Future<void> onLoad() async {
final sprite = Sprite(await images.load('speech-bubble.png'));
final boxSize = Vector2.all(300);
final nineTileBox = NineTileBox.withGrid(
sprite,
leftWidth: 31,
rightWidth: 5,
topHeight: 5,
bottomHeight: 21,
);
add(
nineTileBoxComponent = NineTileBoxComponent(
nineTileBox: nineTileBox,
position: size / 2,
size: boxSize,
anchor: Anchor.center,
),
);
}

@override
void onTap() {
nineTileBoxComponent.scale.scale(1.2);
}

@override
void onDoubleTap() {
nineTileBoxComponent.scale.scale(0.8);
}
}
116 changes: 112 additions & 4 deletions packages/flame/lib/src/nine_tile_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flame/src/extensions/vector2.dart';
import 'package:flame/src/palette.dart';
import 'package:flame/src/sprite.dart';
import 'package:meta/meta.dart';

/// This allows you to create a rectangle textured with a 9-sliced image.
///
Expand All @@ -24,7 +25,9 @@ class NineTileBox {
/// (optionally used to scale the src image).
late int destTileSize;

late final Rect _center;
@visibleForTesting
late Rect center;

late final Rect _dst;

/// Creates a nine-box instance.
Expand All @@ -41,20 +44,125 @@ class NineTileBox {
: tileSize = tileSize ?? sprite.src.width ~/ 3 {
this.destTileSize = destTileSize ?? this.tileSize;
final centerEdge = this.tileSize.toDouble();
_center = Rect.fromLTWH(centerEdge, centerEdge, centerEdge, centerEdge);
center = Rect.fromLTWH(centerEdge, centerEdge, centerEdge, centerEdge);
_dst = Rect.fromLTWH(0, 0, this.destTileSize * 3, this.destTileSize * 3);
}

/// Creates a nine-box instance with the specified grid size
///
/// A nine-box is a grid with 3 rows and 3 columns. The outer-most columns,
/// [leftWidth] and [rightWidth], are a fixed-width. As the nine-box is
/// resized, those columns remain fixed-width and the center column stretches
/// to take up the remaining space. In the same way, the outer-most rows,
/// [topHeight] and [bottomHeight], are a fixed-height. As the nine-box is
/// resized, those rows remain fixed-height and the center row stretches
/// to take up the remaining space.
NineTileBox.withGrid(
this.sprite, {
double leftWidth = 0.0,
double rightWidth = 0.0,
double topHeight = 0.0,
double bottomHeight = 0.0,
}) : tileSize = sprite.src.width ~/ 3 {
destTileSize = tileSize;
center = Rect.fromLTWH(0, 0, sprite.src.width, sprite.src.height);
_dst = Rect.fromLTWH(0, 0, sprite.src.width, sprite.src.height);
setGrid(
leftWidth: leftWidth,
rightWidth: rightWidth,
topHeight: topHeight,
bottomHeight: bottomHeight,
);
}

/// Set different sizes for each of the fixed size rows and columns
///
/// A nine-box is a grid with 3 rows and 3 columns. The outer-most columns,
/// [leftWidth] and [rightWidth], are a fixed-width. As the nine-box is
/// resized, those columns remain fixed-width and the center column stretches
/// to take up the remaining space. In the same way, the outer-most rows,
/// [topHeight] and [bottomHeight], are a fixed-height. As the nine-box is
/// resized, those rows remain fixed-height and the center row stretches
/// to take up the remaining space.
///
/// Any widths or heights that are not specified remain unchanged.
void setGrid({
double? leftWidth,
double? rightWidth,
double? topHeight,
double? bottomHeight,
}) {
if (leftWidth != null && rightWidth != null) {
assert(
leftWidth + rightWidth <= sprite.src.width,
'The left and right columns ($leftWidth + $rightWidth) do '
'not fit in the width of the sprite (${sprite.src.width})',
);
} else if (leftWidth != null) {
assert(
leftWidth <= center.right,
'The left column ($leftWidth) is too large '
'(max ${center.right})',
);
} else if (rightWidth != null) {
assert(
rightWidth + center.left <= sprite.src.width,
'The right column ($rightWidth) is too large '
'(max ${sprite.src.width - center.left})',
);
}
if (topHeight != null && bottomHeight != null) {
assert(
topHeight + bottomHeight <= sprite.src.height,
'The top and bottom rows ($topHeight + $bottomHeight) do not fit '
'in the height of the sprite (${sprite.src.height})',
);
} else if (topHeight != null) {
assert(
topHeight <= center.bottom,
'The top row ($topHeight) is too large '
'(max ${center.bottom})',
);
} else if (bottomHeight != null) {
assert(
bottomHeight + center.top <= sprite.src.height,
'The bottom row ($bottomHeight) is too large '
'(max ${sprite.src.height - center.top})',
);
}

final left = leftWidth ?? center.left;
final top = topHeight ?? center.top;
late final double right;
if (rightWidth == null) {
right = center.right;
} else {
right = sprite.src.width - rightWidth;
}
late final double bottom;
if (bottomHeight == null) {
bottom = center.bottom;
} else {
bottom = sprite.src.height - bottomHeight;
}
center = Rect.fromLTRB(
left,
top,
right,
bottom,
);
}

/// Renders this nine box with the dimensions provided by [dst].
void drawRect(Canvas c, [Rect? dst]) {
c.drawImageNine(sprite.image, _center, dst ?? _dst, _whitePaint);
c.drawImageNine(sprite.image, center, dst ?? _dst, _whitePaint);
}

/// Renders this nine box as a rectangle at [position] with size [size].
void draw(Canvas c, Vector2 position, Vector2 size) {
c.drawImageNine(
sprite.image,
_center,
center,
Rect.fromLTWH(position.x, position.y, size.x, size.y),
_whitePaint,
);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions packages/flame/test/nine_tile_box_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import '_resources/load_image.dart';

void main() {
group('NineTileBox', () {
testGolden(
'Render with default grid',
(game) async {
game.add(_MyComponent1());
},
size: Vector2(300, 200),
goldenFile: '_goldens/nine_tile_box_test_1.png',
);

testGolden(
'Render with specified grid',
(game) async {
game.add(_MyComponent2());
},
size: Vector2(300, 200),
goldenFile: '_goldens/nine_tile_box_test_2.png',
);

test('default tile sizes calculated correctly', () async {
final sprite = Sprite(await loadImage('speech-bubble-1.png'));
final nineTileBox = NineTileBox(sprite);

expect(nineTileBox.tileSize, equals(30));
expect(nineTileBox.destTileSize, equals(30));
});

test('tile sizes set correctly', () async {
final sprite = Sprite(await loadImage('speech-bubble-1.png'));
final nineTileBox = NineTileBox(sprite, tileSize: 20, destTileSize: 25);

expect(nineTileBox.tileSize, equals(20));
expect(nineTileBox.destTileSize, equals(25));
});

test('grid sizes set correctly', () async {
final sprite = Sprite(await loadImage('speech-bubble-2.png'));
final nineTileBox = NineTileBox.withGrid(
sprite,
leftWidth: 31,
rightWidth: 5,
topHeight: 5,
bottomHeight: 21,
);

expect(nineTileBox.center.left, equals(31.0));
expect(nineTileBox.center.right, equals(34.0));
expect(nineTileBox.center.top, equals(5.0));
expect(nineTileBox.center.bottom, equals(18.0));
});
});
}

class _MyComponent1 extends PositionComponent {
_MyComponent1() : super(size: Vector2(300, 200));
late final Sprite sprite;
late final NineTileBox nineTileBox;
final bgPaint = Paint()..color = const Color.fromARGB(255, 57, 113, 158);

@override
Future<void> onLoad() async {
sprite = Sprite(await loadImage('speech-bubble-1.png'));
nineTileBox = NineTileBox(sprite);
}

@override
void render(Canvas canvas) {
canvas.drawRect(
size.toRect(),
bgPaint,
);
nineTileBox.draw(canvas, Vector2(25, 25), Vector2(250, 150));
}
}

class _MyComponent2 extends PositionComponent {
_MyComponent2() : super(size: Vector2(300, 200));
late final Sprite sprite;
late final NineTileBox nineTileBox;
final bgPaint = Paint()..color = const Color.fromARGB(255, 57, 113, 158);

@override
Future<void> onLoad() async {
sprite = Sprite(await loadImage('speech-bubble-2.png'));
nineTileBox = NineTileBox.withGrid(
sprite,
leftWidth: 31,
rightWidth: 5,
topHeight: 5,
bottomHeight: 21,
);
}

@override
void render(Canvas canvas) {
canvas.drawRect(
size.toRect(),
bgPaint,
);
nineTileBox.draw(canvas, Vector2(25, 25), Vector2(250, 150));
}
}

0 comments on commit a25b0a0

Please sign in to comment.