Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TexturePacker atlas can be generated from device's file #3006

Merged
merged 24 commits into from Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2367f88
feat: Transfer flame_texturepacker to monorepo
spydon Jan 21, 2024
8d4d1e7
Merge branch 'main' into flame_texturepacker
spydon Jan 21, 2024
74a6fda
Add missing words to dictionary
spydon Jan 21, 2024
ee8ea8e
fix formatting
spydon Jan 21, 2024
b7d1d9a
Merge branch 'main' into flame_texturepacker
spydon Jan 21, 2024
54689a8
Fix according to comments
spydon Jan 21, 2024
04a4ab9
Merge branch 'main' into flame_texturepacker
spydon Jan 22, 2024
f2348c1
Merge branch 'main' into flame_texturepacker
spydon Jan 22, 2024
2b56a92
added functionality to build texture atlas from images existing in a …
gnarhard Jan 22, 2024
6f92293
updated documentation
gnarhard Jan 22, 2024
8126756
added fromStorage condition in image parse method
gnarhard Jan 22, 2024
ca355e4
angles should conform to the value provided by the packer
gnarhard Jan 22, 2024
9a109ec
added my contributor credit
gnarhard Jan 22, 2024
913b656
Fix according to comments
gnarhard Jan 22, 2024
e6345e5
removed broken image rotation code
gnarhard Jan 26, 2024
31d3593
fixed flame version
gnarhard Jan 26, 2024
966e580
Merge branch 'main' into flame_texturepacker
gnarhard Jan 26, 2024
28f9d7b
fix analyze issue
gnarhard Jan 26, 2024
fec5d0a
fix linting issues in readme
gnarhard Jan 26, 2024
8c7ce36
Add Gnarhard to the people dictionary
spydon Jan 27, 2024
952faf0
Remove unused word from dictionary
spydon Jan 28, 2024
fe4ff3c
Merge branch 'main' into flame_texturepacker
spydon Jan 28, 2024
44eb6ac
fixes related to comments
gnarhard Jan 28, 2024
3f6c0fd
Merge branch 'flame_texturepacker' of https://github.com/gnarhard/fla…
gnarhard Jan 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/.cspell/people_usernames.txt
Expand Up @@ -5,6 +5,7 @@ erayzesen # erayzesen.itch.io
erickzanardo # github.com/erickzanardo
feroult # github.com/feroult
fröber # github.com/Brixto
gnarhard # github.com/gnarhard
Klingsbo # github.com/spydon
luanpotter # github.com/luanpotter
Lukas # github.com/spydon
Expand Down
1 change: 0 additions & 1 deletion .github/.cspell/words_dictionary.txt
Expand Up @@ -34,4 +34,3 @@ tappable
tappables
toolset
underutilize
unrotated
15 changes: 15 additions & 0 deletions packages/flame_texturepacker/README.md
Expand Up @@ -33,6 +33,9 @@ flutter pub add flame_texturepacker

## Usage


### Asset Storage

Drop generated atlas file and sprite sheet images into the `assets/` and link the files in your
`pubspec.yaml` file:

Expand All @@ -52,6 +55,16 @@ Load the TextureAtlas passing the path of the sprite sheet atlas file:
final atlas = await fromAtlas('atlas_map.atlas');
```


### File Storage

If you are using file storage, grab your atlas file like this:

```Dart
final documentsPath = (await getApplicationDocumentsDirectory()).path;
final atlas = await fromAtlas("${documentsPath}/atlas_map.atlas", fromStorage: true);
```

Get a list of sprites ordered by their index, you can use the list to generate an animation:

```Dart
Expand Down Expand Up @@ -80,10 +93,12 @@ Note: Sprites used in this example can be found OpenGameArt [here][4].
## Credits

Thanks to [Jonas Fröber][5] for the original implementation.
Thanks to [Gnarhard][6] for the feature to build the atlas file from a device's storage.

[1]: https://www.codeandweb.com/texturepacker 'Code & Web Texture Packer'
[2]: https://github.com/crashinvaders/gdx-texture-packer-gui 'Gdx Texture Packer'
[3]: example/lib/main.dart 'Full working example'
[4]: https://opengameart.org/content/toon-characters-1 'Robot sprite'
[5]: https://github.com/Brixto
[6]: https://github.com/gnarhard

@@ -1,9 +1,4 @@
import 'dart:math';
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/rendering.dart';
import 'package:flame_texturepacker/atlas/model/region.dart';

/// {@template _texture_packer_sprite}
Expand All @@ -16,25 +11,17 @@ class TexturePackerSprite extends Sprite {
index = region.index,
offsetX = region.offsetX,
offsetY = region.offsetY,
packedWidth = region.rotate ? region.height : region.width,
packedHeight = region.rotate ? region.width : region.height,
packedWidth = region.width,
packedHeight = region.height,
originalWidth = region.originalWidth,
originalHeight = region.originalHeight,
rotate = region.rotate,
degrees = region.degrees,
super(
region.page.texture,
srcPosition: Vector2(region.left, region.top),
srcSize: Vector2(
region.rotate ? region.height : region.width,
region.rotate ? region.width : region.height,
),
) {
decorator = Transform2DDecorator(transform);
if (region.rotate) {
transform.angle = radians(90);
}
}
srcSize: Vector2(region.width, region.height),
);

/// The number at the end of the original image file name, or -1 if none.
///
Expand Down Expand Up @@ -80,53 +67,5 @@ class TexturePackerSprite extends Sprite {
final int degrees;

/// The [degrees] field (angle) represented as radians.
double get angle => degrees * (pi / 180);

late final Decorator decorator;
final Transform2D transform = Transform2D();

// Used to avoid the creation of new Vector2 objects in render.
static final _tmpRenderPosition = Vector2.zero();
static final _tmpRenderSize = Vector2.zero();

@override
void render(
Canvas canvas, {
Vector2? position,
Vector2? size,
Anchor anchor = Anchor.topLeft,
Paint? overridePaint,
}) {
if (position != null) {
_tmpRenderPosition.setFrom(position);
} else {
_tmpRenderPosition.setZero();
}

// If the sprite is rotated on the sprite sheet un-rotate it and adjust the
// size.
final unrotatedAnchor = rotate ? Anchor.bottomLeft : anchor;

final tempSize = size ?? srcSize;
final tempWidth = rotate ? tempSize.y : tempSize.x;
final tempHeight = rotate ? tempSize.x : tempSize.y;

_tmpRenderSize.setValues(tempWidth, tempHeight);

_tmpRenderPosition.setValues(
_tmpRenderPosition.x - (unrotatedAnchor.x * _tmpRenderSize.x),
_tmpRenderPosition.y - (unrotatedAnchor.y * _tmpRenderSize.y),
);

decorator.applyChain(
(applyCanvas) => super.render(
applyCanvas,
position: _tmpRenderPosition,
size: _tmpRenderSize,
anchor: anchor,
overridePaint: overridePaint,
),
canvas,
);
}
double get angle => radians(degrees.toDouble());
}
107 changes: 94 additions & 13 deletions packages/flame_texturepacker/lib/atlas/texture_packer_atlas.dart
@@ -1,13 +1,15 @@
library flame_texturepacker;

import 'dart:convert';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:flame/cache.dart';
import 'package:flame/flame.dart';
import 'package:flame_texturepacker/atlas/model/page.dart';
import 'package:flame_texturepacker/atlas/model/region.dart';
import 'package:flame_texturepacker/atlas/model/texture_packer_sprite.dart';
import 'package:flutter/painting.dart';

final _images = Images(prefix: 'assets/');

Expand All @@ -19,8 +21,20 @@ class TexturePackerAtlas {

/// Loads all the sprites from the atlas that resides on the [path] and
/// returns a new [TexturePackerAtlas].
static Future<TexturePackerAtlas> load(String path) async {
final atlasData = await _load(path);
/// If [fromStorage] is true, the atlas will be loaded from the device's
/// storage instead of the assets folder.
static Future<TexturePackerAtlas> load(
String path, {
bool fromStorage = false,
}) async {
final _TextureAtlasData atlasData;

if (fromStorage) {
atlasData = await _fromStorage(path);
} else {
atlasData = await _fromAssets(path);
}

return TexturePackerAtlas(
atlasData.regions.map(TexturePackerSprite.new).toList(),
);
Expand Down Expand Up @@ -51,27 +65,65 @@ class TexturePackerAtlas {
}
}

Future<_TextureAtlasData> _load(String path) async {
/// Loads images from the assets folder.
/// Uses the [path] to find the image directory.
Future<_TextureAtlasData> _fromAssets(String path) async {
try {
return await _parse(path, fromStorage: false);
} on Exception catch (e) {
throw Exception('Error loading $path from assets: $e');
}
}

/// Loads images from the device's storage.
/// Uses the [path] to find the image directory.
Future<_TextureAtlasData> _fromStorage(String path) async {
try {
return await _parse(path, fromStorage: true);
} on Exception catch (e) {
throw Exception('Error loading $path from storage: $e');
}
}

/// Parses the atlas file and loads the images.
/// Uses the [path] to find the image directory.
/// Atlas will be loaded from the device's storage if [fromStorage] is true.
/// Otherwise, it will be loaded from the assets folder.
/// Returns a [_TextureAtlasData] containing the pages and regions.
Future<_TextureAtlasData> _parse(
String path, {
required bool fromStorage,
}) async {
final pages = <Page>[];
final regions = <Region>[];
final fileAsString = await Flame.assets.readFile(path);
final String fileAsString;

if (fromStorage) {
fileAsString = await File(path).readAsString();
} else {
fileAsString = await Flame.assets.readFile(path);
}

final iterator = LineSplitter.split(fileAsString).iterator;
var line = iterator.moveNextAndGet();
var hasIndexes = false;

while (true) {
if (line == null) {
break;
}

if (line.isEmpty) {
line = iterator.moveNextAndGet();
}

Page? page;

while (true) {
if (line == null) {
break;
}

if (line.isEmpty) {
page = null;
line = iterator.moveNextAndGet();
Expand All @@ -80,7 +132,21 @@ Future<_TextureAtlasData> _load(String path) async {
page.textureFile = line;
final parentPath = (path.split('/')..removeLast()).join('/');
final texturePath = '$parentPath/$line';
page.texture = await _images.load(texturePath);

if (fromStorage) {
try {
final textureFile = File(texturePath);
final bytes = await textureFile.readAsBytes();
final decodedBytes = await decodeImageFromList(bytes);
Flame.images.add(texturePath, decodedBytes);
page.texture = Flame.images.fromCache(texturePath);
} on Exception catch (e) {
throw Exception('Could not add storage file to Flame cache. $e');
}
} else {
page.texture = await _images.load(texturePath);
}

while (true) {
line = iterator.moveNextAndGet();
if (line == null) {
Expand Down Expand Up @@ -142,16 +208,19 @@ Future<_TextureAtlasData> _load(String path) async {
region.originalHeight = double.parse(entry[4]);
case 'rotate':
final value = entry[1];

if (value == 'true') {
region.degrees = 90;
} else if (value == 'false') {
region.degrees = 0;
} else {
region.degrees = int.parse(value);
}

region.rotate = region.degrees == 90;
case 'index':
region.index = int.parse(entry[1]);

if (region.index != -1) {
hasIndexes = true;
}
Expand All @@ -161,10 +230,12 @@ Future<_TextureAtlasData> _load(String path) async {
region.originalWidth = region.width;
region.originalHeight = region.height;
}

regions.add(region);
}
}
}

if (hasIndexes) {
regions.sort((region1, region2) {
var i1 = region1.index;
Expand All @@ -178,28 +249,37 @@ Future<_TextureAtlasData> _load(String path) async {
return i1 - i2;
});
}

return (pages: pages, regions: regions);
}

({int count, List<String> entry}) _readEntry(String inputLine) {
final line = inputLine.trim();
if (line.isEmpty) {
({int count, List<String> entry}) _readEntry(String line) {
final trimmedLine = line.trim();

if (trimmedLine.isEmpty) {
return (count: 0, entry: []);
}
final colonIndex = line.indexOf(':');

final colonIndex = trimmedLine.indexOf(':');

if (colonIndex == -1) {
return (count: 0, entry: []);
}

final entry = <String>[];
entry.add(line.substring(0, colonIndex).trim());
entry.add(trimmedLine.substring(0, colonIndex).trim());

for (var i = 1, lastMatch = colonIndex + 1;; i++) {
final commaIndex = line.indexOf(',', lastMatch);
final commaIndex = trimmedLine.indexOf(',', lastMatch);

if (commaIndex == -1) {
entry.add(line.substring(lastMatch).trim());
entry.add(trimmedLine.substring(lastMatch).trim());
return (count: i, entry: entry);
}
entry.add(line.substring(lastMatch, commaIndex).trim());

entry.add(trimmedLine.substring(lastMatch, commaIndex).trim());
lastMatch = commaIndex + 1;

if (i == 4) {
return (count: 4, entry: entry);
}
Expand All @@ -213,6 +293,7 @@ extension _IteratorExtension on Iterator<String> {
if (moveNext()) {
return current;
}

return null;
}
}
11 changes: 7 additions & 4 deletions packages/flame_texturepacker/lib/flame_texturepacker.dart
Expand Up @@ -6,8 +6,11 @@ import 'package:flame_texturepacker/atlas/texture_packer_atlas.dart';
export 'package:flame_texturepacker/atlas/model/texture_packer_sprite.dart';

extension TexturepackerLoader on Game {
/// Loads the specified pack file, using the parent directory of the pack file
/// to find the page images.
Future<TexturePackerAtlas> fromAtlas(String assetsPath) async =>
TexturePackerAtlas.load(assetsPath);
/// Loads the specified pack file.
/// Uses the parent directory of the pack file to find the page images.
Future<TexturePackerAtlas> fromAtlas(
String assetsPath, {
bool fromStorage = false,
}) async =>
TexturePackerAtlas.load(assetsPath, fromStorage: fromStorage);
}