diff --git a/.github/workflows/mini_sprite.yaml b/.github/workflows/mini_sprite.yaml
new file mode 100644
index 0000000..bdbea2e
--- /dev/null
+++ b/.github/workflows/mini_sprite.yaml
@@ -0,0 +1,45 @@
+name: mini_sprite
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - .github/workflows/mini_sprite.yaml
+ - packages/mini_sprite/**
+
+ pull_request:
+ branches:
+ - main
+ paths:
+ - .github/workflows/mini_sprite.yaml
+ - packages/mini_sprite/**
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2.3.4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: 'stable'
+ cache: true
+ - uses: bluefireteam/melos-action@main
+
+ - name: Install Dependencies
+ run: melos bootstrap
+
+ - name: Format
+ run: melos exec --scope mini_sprite dart format --set-exit-if-changed lib
+
+ - name: Analyze
+ run: melos exec --scope mini_sprite dart analyze --fatal-infos --fatal-warnings .
+
+ - name: Run Tests
+ run: melos exec --scope mini_sprite flutter test --coverage
+
+ - name: Check Code Coverage
+ uses: VeryGoodOpenSource/very_good_coverage@v1
+ with:
+ path: packages/mini_sprite/coverage/lcov.info
diff --git a/.github/workflows/mini_sprite_editor.yaml b/.github/workflows/mini_sprite_editor.yaml
new file mode 100644
index 0000000..dd292f6
--- /dev/null
+++ b/.github/workflows/mini_sprite_editor.yaml
@@ -0,0 +1,45 @@
+name: mini_sprite_editor
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - .github/workflows/mini_sprite_editor.yaml
+ - packages/mini_sprite_editor/**
+
+ pull_request:
+ branches:
+ - main
+ paths:
+ - .github/workflows/mini_sprite_editor.yaml
+ - packages/mini_sprite_editor/**
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2.3.4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: 'stable'
+ cache: true
+ - uses: bluefireteam/melos-action@main
+
+ - name: Install Dependencies
+ run: melos bootstrap
+
+ - name: Format
+ run: melos exec --scope mini_sprite_editor flutter format --set-exit-if-changed lib
+
+ - name: Analyze
+ run: melos exec --scope mini_sprite_editor flutter analyze --fatal-infos --fatal-warnings .
+
+ - name: Run Tests
+ run: melos exec --scope mini_sprite_editor flutter test --coverage
+
+ - name: Check Code Coverage
+ uses: VeryGoodOpenSource/very_good_coverage@v1
+ with:
+ path: packages/mini_sprite_editor/coverage/lcov.info
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..4ae1ed7
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+mini_sprite
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..5d344fd
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/melos_bootstrap.xml b/.idea/runConfigurations/melos_bootstrap.xml
new file mode 100644
index 0000000..d571530
--- /dev/null
+++ b/.idea/runConfigurations/melos_bootstrap.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/runConfigurations/melos_clean.xml b/.idea/runConfigurations/melos_clean.xml
new file mode 100644
index 0000000..f45d436
--- /dev/null
+++ b/.idea/runConfigurations/melos_clean.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/runConfigurations/melos_flutter_run_mini_sprite_editor.xml b/.idea/runConfigurations/melos_flutter_run_mini_sprite_editor.xml
new file mode 100644
index 0000000..ac2f0a3
--- /dev/null
+++ b/.idea/runConfigurations/melos_flutter_run_mini_sprite_editor.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 7f0854e..d14dc4a 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-# mini_sprite
+# Mini Sprite
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
-A Very Good Project created by Very Good CLI.
+Mini sprite is a simple, matrix based format for creating 1bit styled graphics.
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
diff --git a/melos.yaml b/melos.yaml
new file mode 100644
index 0000000..4433e34
--- /dev/null
+++ b/melos.yaml
@@ -0,0 +1,5 @@
+name: mini_sprite
+repository: https://github.com/bluefireteam/mini_sprite
+packages:
+ - packages/**
+ - .
diff --git a/melos_mini_sprite.iml b/melos_mini_sprite.iml
new file mode 100644
index 0000000..9681559
--- /dev/null
+++ b/melos_mini_sprite.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/mini_sprite/.gitignore b/packages/mini_sprite/.gitignore
new file mode 100644
index 0000000..526da15
--- /dev/null
+++ b/packages/mini_sprite/.gitignore
@@ -0,0 +1,7 @@
+# See https://www.dartlang.org/guides/libraries/private-files
+
+# Files and directories created by pub
+.dart_tool/
+.packages
+build/
+pubspec.lock
\ No newline at end of file
diff --git a/packages/mini_sprite/README.md b/packages/mini_sprite/README.md
new file mode 100644
index 0000000..7f0854e
--- /dev/null
+++ b/packages/mini_sprite/README.md
@@ -0,0 +1,11 @@
+# mini_sprite
+
+[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
+[![License: MIT][license_badge]][license_link]
+
+A Very Good Project created by Very Good CLI.
+
+[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
+[license_link]: https://opensource.org/licenses/MIT
+[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
+[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
diff --git a/packages/mini_sprite/analysis_options.yaml b/packages/mini_sprite/analysis_options.yaml
new file mode 100644
index 0000000..b08262c
--- /dev/null
+++ b/packages/mini_sprite/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:very_good_analysis/analysis_options.3.0.0.yaml
diff --git a/packages/mini_sprite/lib/mini_sprite.dart b/packages/mini_sprite/lib/mini_sprite.dart
new file mode 100644
index 0000000..3f7ba4e
--- /dev/null
+++ b/packages/mini_sprite/lib/mini_sprite.dart
@@ -0,0 +1,3 @@
+library mini_sprite;
+
+export 'src/mini_sprite.dart';
diff --git a/packages/mini_sprite/lib/src/mini_sprite.dart b/packages/mini_sprite/lib/src/mini_sprite.dart
new file mode 100644
index 0000000..afb5abb
--- /dev/null
+++ b/packages/mini_sprite/lib/src/mini_sprite.dart
@@ -0,0 +1,88 @@
+/// {@template mini_sprite}
+/// A class used to manipulate a matrix of pixels.
+/// first dimension of the [pixels] array is the y coordinate,
+/// second is the x coordinate.
+/// {@endtemplate}
+class MiniSprite {
+ /// {@macro mini_sprite}
+ MiniSprite(this.pixels);
+
+ /// {@macro mini_sprite}
+ ///
+ /// Creates an empty sprite with the given width and height.
+ MiniSprite.empty(int width, int height)
+ : pixels =
+ List.generate(height, (_) => List.generate(width, (_) => false));
+
+ /// {@macro mini_sprite}
+ ///
+ /// Returns a [MiniSprite] from the serialized data.
+ factory MiniSprite.fromDataString(String value) {
+ final blocks = value.split(';');
+
+ final size = blocks.removeAt(0).split(',');
+ final height = int.parse(size[0]);
+ final width = int.parse(size[1]);
+
+ final flatten = blocks.map((rawBlock) {
+ final blockSplit = rawBlock.split(',');
+
+ final count = int.parse(blockSplit[0]);
+ final value = int.parse(blockSplit[1]) == 1;
+
+ return List.filled(count, value);
+ }).fold>(List.empty(), (value, list) {
+ return [
+ ...value,
+ ...list,
+ ];
+ });
+
+ final pixels = List.generate(
+ height,
+ (_) => List.generate(
+ width,
+ (_) {
+ return flatten.removeAt(0);
+ },
+ ),
+ );
+
+ return MiniSprite(pixels);
+ }
+
+ /// The matrix of pixels.
+ final List> pixels;
+
+ /// Returns this as a data string.
+ String toDataString() {
+ final dimensions = '${pixels.length},${pixels[0].length}';
+
+ var counter = 0;
+ bool? last;
+
+ final blocks = [];
+
+ for (var y = 0; y < pixels.length; y++) {
+ for (var x = 0; x < pixels[y].length; x++) {
+ if (last == null) {
+ last = pixels[y][x];
+ counter = 1;
+ } else {
+ if (last == pixels[y][x]) {
+ counter++;
+ } else {
+ blocks.add('$counter,${last ? 1 : 0}');
+ last = pixels[y][x];
+ counter = 1;
+ }
+ }
+ }
+ }
+ if (last != null) {
+ blocks.add('$counter,${last ? 1 : 0}');
+ }
+
+ return '$dimensions;${blocks.join(';')}';
+ }
+}
diff --git a/packages/mini_sprite/melos_mini_sprite.iml b/packages/mini_sprite/melos_mini_sprite.iml
new file mode 100644
index 0000000..389d07a
--- /dev/null
+++ b/packages/mini_sprite/melos_mini_sprite.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/mini_sprite/pubspec.yaml b/packages/mini_sprite/pubspec.yaml
new file mode 100644
index 0000000..27b0c47
--- /dev/null
+++ b/packages/mini_sprite/pubspec.yaml
@@ -0,0 +1,13 @@
+name: mini_sprite
+description: A simple sprite format for building 1bit styled graphics.
+version: 1.0.0+1
+publish_to: none
+
+environment:
+ sdk: ">=2.17.0 <3.0.0"
+
+dev_dependencies:
+ coverage: ^1.1.0
+ mocktail: ^0.3.0
+ test: ^1.19.2
+ very_good_analysis: ^3.0.0
diff --git a/packages/mini_sprite/test/src/mini_sprite_test.dart b/packages/mini_sprite/test/src/mini_sprite_test.dart
new file mode 100644
index 0000000..162f9f1
--- /dev/null
+++ b/packages/mini_sprite/test/src/mini_sprite_test.dart
@@ -0,0 +1,96 @@
+import 'package:mini_sprite/mini_sprite.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('MiniSprite', () {
+ test('empty returns an empty sprite', () {
+ expect(
+ MiniSprite.empty(2, 2).pixels,
+ equals([
+ [
+ false,
+ false,
+ ],
+ [
+ false,
+ false,
+ ],
+ ]),
+ );
+ });
+
+ test('toDataString returns the correct data', () {
+ expect(
+ MiniSprite([
+ [true, true],
+ [true, true],
+ ]).toDataString(),
+ equals('2,2;4,1'),
+ );
+
+ expect(
+ MiniSprite([
+ [true, true],
+ [true, false],
+ ]).toDataString(),
+ equals('2,2;3,1;1,0'),
+ );
+
+ expect(
+ MiniSprite([
+ [true, false],
+ [true, false],
+ ]).toDataString(),
+ equals('2,2;1,1;1,0;1,1;1,0'),
+ );
+ });
+
+ test('fromDataString returns the correct parsed instance', () {
+ expect(
+ MiniSprite.fromDataString('2,2;4,1').pixels,
+ equals([
+ [true, true],
+ [true, true],
+ ]),
+ );
+
+ expect(
+ MiniSprite.fromDataString('2,2;3,1;1,0').pixels,
+ equals([
+ [true, true],
+ [true, false],
+ ]),
+ );
+
+ expect(
+ MiniSprite.fromDataString('2,2;1,1;1,0;1,1;1,0').pixels,
+ equals([
+ [true, false],
+ [true, false],
+ ]),
+ );
+ });
+
+ group('when dimensions are not symmetrical', () {
+ test('toDataString returns the correct data', () {
+ expect(
+ MiniSprite([
+ [true, true, true],
+ [true, false, false],
+ ]).toDataString(),
+ equals('2,3;4,1;2,0'),
+ );
+ });
+
+ test('fromDataString returns the correct parsed instance', () {
+ expect(
+ MiniSprite.fromDataString('2,3;4,1;2,0').pixels,
+ equals([
+ [true, true, true],
+ [true, false, false],
+ ]),
+ );
+ });
+ });
+ });
+}
diff --git a/packages/mini_sprite_editor/.github/PULL_REQUEST_TEMPLATE.md b/packages/mini_sprite_editor/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..6b9372e
--- /dev/null
+++ b/packages/mini_sprite_editor/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,23 @@
+
+
+## Description
+
+
+
+## Type of Change
+
+
+
+- [ ] β¨ New feature (non-breaking change which adds functionality)
+- [ ] π οΈ Bug fix (non-breaking change which fixes an issue)
+- [ ] β Breaking change (fix or feature that would cause existing functionality to change)
+- [ ] π§Ή Code refactor
+- [ ] β
Build configuration change
+- [ ] π Documentation
+- [ ] ποΈ Chore
diff --git a/packages/mini_sprite_editor/.github/workflows/main.yaml b/packages/mini_sprite_editor/.github/workflows/main.yaml
new file mode 100644
index 0000000..b554ded
--- /dev/null
+++ b/packages/mini_sprite_editor/.github/workflows/main.yaml
@@ -0,0 +1,10 @@
+name: mini_sprite_editor
+
+on: [pull_request, push]
+
+jobs:
+ build:
+ uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
+ with:
+ flutter_channel: stable
+ flutter_version: 3.0.0
diff --git a/packages/mini_sprite_editor/.gitignore b/packages/mini_sprite_editor/.gitignore
new file mode 100644
index 0000000..bd315f7
--- /dev/null
+++ b/packages/mini_sprite_editor/.gitignore
@@ -0,0 +1,127 @@
+# Miscellaneous
+*.class
+*.lock
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/*
+
+# Visual Studio Code related
+.classpath
+.project
+.settings/
+.vscode/*
+
+# Flutter repo-specific
+/bin/cache/
+/bin/mingit/
+/dev/benchmarks/mega_gallery/
+/dev/bots/.recipe_deps
+/dev/bots/android_tools/
+/dev/docs/doc/
+/dev/docs/flutter.docs.zip
+/dev/docs/lib/
+/dev/docs/pubspec.yaml
+/dev/integration_tests/**/xcuserdata
+/dev/integration_tests/**/Pods
+/packages/flutter/coverage/
+version
+
+# packages file containing multi-root paths
+.packages.generated
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+flutter_*.png
+linked_*.ds
+unlinked.ds
+unlinked_spec.ds
+.fvm/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+**/android/key.properties
+**/android/.idea/
+*.jks
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/.last_build_id
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Coverage
+coverage/
+
+# Submodules
+!pubspec.lock
+packages/**/pubspec.lock
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Exceptions to the above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+!/dev/ci/**/Gemfile.lock
+!.vscode/extensions.json
+!.vscode/launch.json
+!.idea/codeStyles/
+!.idea/dictionaries/
+!.idea/runConfigurations/
diff --git a/packages/mini_sprite_editor/.idea/runConfigurations/development.xml b/packages/mini_sprite_editor/.idea/runConfigurations/development.xml
new file mode 100644
index 0000000..37d45fe
--- /dev/null
+++ b/packages/mini_sprite_editor/.idea/runConfigurations/development.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/mini_sprite_editor/.idea/runConfigurations/production.xml b/packages/mini_sprite_editor/.idea/runConfigurations/production.xml
new file mode 100644
index 0000000..e55961d
--- /dev/null
+++ b/packages/mini_sprite_editor/.idea/runConfigurations/production.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/mini_sprite_editor/.idea/runConfigurations/staging.xml b/packages/mini_sprite_editor/.idea/runConfigurations/staging.xml
new file mode 100644
index 0000000..3f7d621
--- /dev/null
+++ b/packages/mini_sprite_editor/.idea/runConfigurations/staging.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/mini_sprite_editor/.metadata b/packages/mini_sprite_editor/.metadata
new file mode 100644
index 0000000..cd984dd
--- /dev/null
+++ b/packages/mini_sprite_editor/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
+ channel: stable
+
+project_type: app
diff --git a/packages/mini_sprite_editor/.vscode/extensions.json b/packages/mini_sprite_editor/.vscode/extensions.json
new file mode 100644
index 0000000..03e0c3b
--- /dev/null
+++ b/packages/mini_sprite_editor/.vscode/extensions.json
@@ -0,0 +1,9 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=827846
+ // for the documentation about the extensions.json format
+ "recommendations": [
+ "dart-code.dart-code",
+ "dart-code.flutter",
+ "felixangelov.bloc"
+ ]
+}
diff --git a/packages/mini_sprite_editor/.vscode/launch.json b/packages/mini_sprite_editor/.vscode/launch.json
new file mode 100644
index 0000000..b4e33ce
--- /dev/null
+++ b/packages/mini_sprite_editor/.vscode/launch.json
@@ -0,0 +1,34 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch development",
+ "request": "launch",
+ "type": "dart",
+ "program": "lib/main_development.dart",
+ "args": [
+ "--flavor",
+ "development",
+ "--target",
+ "lib/main_development.dart"
+ ]
+ },
+ {
+ "name": "Launch staging",
+ "request": "launch",
+ "type": "dart",
+ "program": "lib/main_staging.dart",
+ "args": ["--flavor", "staging", "--target", "lib/main_staging.dart"]
+ },
+ {
+ "name": "Launch production",
+ "request": "launch",
+ "type": "dart",
+ "program": "lib/main_production.dart",
+ "args": ["--flavor", "production", "--target", "lib/main_production.dart"]
+ }
+ ]
+}
diff --git a/packages/mini_sprite_editor/LICENSE b/packages/mini_sprite_editor/LICENSE
new file mode 100644
index 0000000..7b93245
--- /dev/null
+++ b/packages/mini_sprite_editor/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Very Good Ventures
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/mini_sprite_editor/README.md b/packages/mini_sprite_editor/README.md
new file mode 100644
index 0000000..e70cf7f
--- /dev/null
+++ b/packages/mini_sprite_editor/README.md
@@ -0,0 +1,164 @@
+# Mini Sprite Editor
+
+![coverage][coverage_badge]
+[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
+[![License: MIT][license_badge]][license_link]
+
+Generated by the [Very Good CLI][very_good_cli_link] π€
+
+Small, standalone application that can read and write mini sprites.
+
+---
+
+## Getting Started π
+
+This project contains 3 flavors:
+
+- development
+- staging
+- production
+
+To run the desired flavor either use the launch configuration in VSCode/Android Studio or use the following commands:
+
+```sh
+# Development
+$ flutter run --flavor development --target lib/main_development.dart
+
+# Staging
+$ flutter run --flavor staging --target lib/main_staging.dart
+
+# Production
+$ flutter run --flavor production --target lib/main_production.dart
+```
+
+_\*Mini Sprite Editor works on iOS, Android, Web, and Windows._
+
+---
+
+## Running Tests π§ͺ
+
+To run all unit and widget tests use the following command:
+
+```sh
+$ flutter test --coverage --test-randomize-ordering-seed random
+```
+
+To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov).
+
+```sh
+# Generate Coverage Report
+$ genhtml coverage/lcov.info -o coverage/
+
+# Open Coverage Report
+$ open coverage/index.html
+```
+
+---
+
+## Working with Translations π
+
+This project relies on [flutter_localizations][flutter_localizations_link] and follows the [official internationalization guide for Flutter][internationalization_link].
+
+### Adding Strings
+
+1. To add a new localizable string, open the `app_en.arb` file at `lib/l10n/arb/app_en.arb`.
+
+```arb
+{
+ "@@locale": "en",
+ "counterAppBarTitle": "Counter",
+ "@counterAppBarTitle": {
+ "description": "Text shown in the AppBar of the Counter Page"
+ }
+}
+```
+
+2. Then add a new key/value and description
+
+```arb
+{
+ "@@locale": "en",
+ "counterAppBarTitle": "Counter",
+ "@counterAppBarTitle": {
+ "description": "Text shown in the AppBar of the Counter Page"
+ },
+ "helloWorld": "Hello World",
+ "@helloWorld": {
+ "description": "Hello World Text"
+ }
+}
+```
+
+3. Use the new string
+
+```dart
+import 'package:mini_sprite_editor/l10n/l10n.dart';
+
+@override
+Widget build(BuildContext context) {
+ final l10n = context.l10n;
+ return Text(l10n.helloWorld);
+}
+```
+
+### Adding Supported Locales
+
+Update the `CFBundleLocalizations` array in the `Info.plist` at `ios/Runner/Info.plist` to include the new locale.
+
+```xml
+ ...
+
+ CFBundleLocalizations
+
+ en
+ es
+
+
+ ...
+```
+
+### Adding Translations
+
+1. For each supported locale, add a new ARB file in `lib/l10n/arb`.
+
+```
+βββ l10n
+β βββ arb
+β β βββ app_en.arb
+β β βββ app_es.arb
+```
+
+2. Add the translated strings to each `.arb` file:
+
+`app_en.arb`
+
+```arb
+{
+ "@@locale": "en",
+ "counterAppBarTitle": "Counter",
+ "@counterAppBarTitle": {
+ "description": "Text shown in the AppBar of the Counter Page"
+ }
+}
+```
+
+`app_es.arb`
+
+```arb
+{
+ "@@locale": "es",
+ "counterAppBarTitle": "Contador",
+ "@counterAppBarTitle": {
+ "description": "Texto mostrado en la AppBar de la pΓ‘gina del contador"
+ }
+}
+```
+
+[coverage_badge]: coverage_badge.svg
+[flutter_localizations_link]: https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html
+[internationalization_link]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization
+[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
+[license_link]: https://opensource.org/licenses/MIT
+[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
+[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
+[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli
diff --git a/packages/mini_sprite_editor/analysis_options.yaml b/packages/mini_sprite_editor/analysis_options.yaml
new file mode 100644
index 0000000..b4d573c
--- /dev/null
+++ b/packages/mini_sprite_editor/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:very_good_analysis/analysis_options.3.0.0.yaml
+linter:
+ rules:
+ public_member_api_docs: false
diff --git a/packages/mini_sprite_editor/coverage_badge.svg b/packages/mini_sprite_editor/coverage_badge.svg
new file mode 100644
index 0000000..88bfadf
--- /dev/null
+++ b/packages/mini_sprite_editor/coverage_badge.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/packages/mini_sprite_editor/l10n.yaml b/packages/mini_sprite_editor/l10n.yaml
new file mode 100644
index 0000000..6f72a55
--- /dev/null
+++ b/packages/mini_sprite_editor/l10n.yaml
@@ -0,0 +1,4 @@
+arb-dir: lib/l10n/arb
+template-arb-file: app_en.arb
+output-localization-file: app_localizations.dart
+nullable-getter: false
diff --git a/packages/mini_sprite_editor/lib/app/app.dart b/packages/mini_sprite_editor/lib/app/app.dart
new file mode 100644
index 0000000..f23ab3c
--- /dev/null
+++ b/packages/mini_sprite_editor/lib/app/app.dart
@@ -0,0 +1 @@
+export 'view/app.dart';
diff --git a/packages/mini_sprite_editor/lib/app/view/app.dart b/packages/mini_sprite_editor/lib/app/view/app.dart
new file mode 100644
index 0000000..473129c
--- /dev/null
+++ b/packages/mini_sprite_editor/lib/app/view/app.dart
@@ -0,0 +1,22 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:mini_sprite_editor/l10n/l10n.dart';
+import 'package:mini_sprite_editor/sprite/sprite.dart';
+
+class App extends StatelessWidget {
+ const App({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ theme: ThemeData(),
+ darkTheme: ThemeData.dark(),
+ localizationsDelegates: const [
+ AppLocalizations.delegate,
+ GlobalMaterialLocalizations.delegate,
+ ],
+ supportedLocales: AppLocalizations.supportedLocales,
+ home: const SpritePage(),
+ );
+ }
+}
diff --git a/packages/mini_sprite_editor/lib/bootstrap.dart b/packages/mini_sprite_editor/lib/bootstrap.dart
new file mode 100644
index 0000000..e6c6e7a
--- /dev/null
+++ b/packages/mini_sprite_editor/lib/bootstrap.dart
@@ -0,0 +1,35 @@
+import 'dart:async';
+import 'dart:developer';
+
+import 'package:bloc/bloc.dart';
+import 'package:flutter/widgets.dart';
+
+class AppBlocObserver extends BlocObserver {
+ @override
+ void onChange(BlocBase bloc, Change change) {
+ super.onChange(bloc, change);
+ log('onChange(${bloc.runtimeType}, $change)');
+ }
+
+ @override
+ void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
+ log('onError(${bloc.runtimeType}, $error, $stackTrace)');
+ super.onError(bloc, error, stackTrace);
+ }
+}
+
+Future bootstrap(FutureOr Function() builder) async {
+ FlutterError.onError = (details) {
+ log(details.exceptionAsString(), stackTrace: details.stack);
+ };
+
+ await runZonedGuarded(
+ () async {
+ await BlocOverrides.runZoned(
+ () async => runApp(await builder()),
+ blocObserver: AppBlocObserver(),
+ );
+ },
+ (error, stackTrace) => log(error.toString(), stackTrace: stackTrace),
+ );
+}
diff --git a/packages/mini_sprite_editor/lib/l10n/arb/app_en.arb b/packages/mini_sprite_editor/lib/l10n/arb/app_en.arb
new file mode 100644
index 0000000..acf8cc6
--- /dev/null
+++ b/packages/mini_sprite_editor/lib/l10n/arb/app_en.arb
@@ -0,0 +1,43 @@
+{
+ "@@locale": "en",
+ "zoomIn": "Zoom In",
+ "@zoomIn": {
+ "description": "Zoom In tooltip"
+ },
+ "zoomOut": "Zoom Out",
+ "@zoomOut": {
+ "description": "Zoom Out tooltip"
+ },
+ "copyToClipboard": "Copy to clipboard",
+ "@copyToClipboard": {
+ "description": "Copy to clipboard label"
+ },
+ "copiedWithSuccess": "Data copied to clipboard",
+ "@copiedWithSuccess": {
+ "description": "Message of data copied to clipboard"
+ },
+ "importFromClipBoard": "Import from clipboard",
+ "@importFromClipBoard": {
+ "description": "Import from clipboard label"
+ },
+ "importSuccess": "Data imported from clipboard",
+ "@importSuccess": {
+ "description": "Message of data imported to clipboard"
+ },
+ "brush": "Brush",
+ "@brush": {
+ "description": "Brush label"
+ },
+ "eraser": "Eraser",
+ "@eraser": {
+ "description": "Eraser label"
+ },
+ "bucket": "Fill",
+ "@bucket": {
+ "description": "Fill label"
+ },
+ "bucketEraser": "Unfill",
+ "@bucketEraser": {
+ "description": "Unfill label"
+ }
+}
diff --git a/packages/mini_sprite_editor/lib/l10n/l10n.dart b/packages/mini_sprite_editor/lib/l10n/l10n.dart
new file mode 100644
index 0000000..17c891b
--- /dev/null
+++ b/packages/mini_sprite_editor/lib/l10n/l10n.dart
@@ -0,0 +1,8 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+export 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+extension AppLocalizationsX on BuildContext {
+ AppLocalizations get l10n => AppLocalizations.of(this);
+}
diff --git a/packages/mini_sprite_editor/lib/main.dart b/packages/mini_sprite_editor/lib/main.dart
new file mode 100644
index 0000000..7faf4e3
--- /dev/null
+++ b/packages/mini_sprite_editor/lib/main.dart
@@ -0,0 +1,6 @@
+import 'package:mini_sprite_editor/app/app.dart';
+import 'package:mini_sprite_editor/bootstrap.dart';
+
+void main() {
+ bootstrap(() => const App());
+}
diff --git a/packages/mini_sprite_editor/lib/sprite/cubit/sprite_cubit.dart b/packages/mini_sprite_editor/lib/sprite/cubit/sprite_cubit.dart
new file mode 100644
index 0000000..f1ab0a2
--- /dev/null
+++ b/packages/mini_sprite_editor/lib/sprite/cubit/sprite_cubit.dart
@@ -0,0 +1,136 @@
+import 'dart:ui';
+
+import 'package:bloc/bloc.dart';
+import 'package:equatable/equatable.dart';
+import 'package:flutter/services.dart';
+import 'package:mini_sprite/mini_sprite.dart';
+
+part 'sprite_state.dart';
+
+class SpriteCubit extends Cubit {
+ SpriteCubit({
+ Future Function(ClipboardData)? setClipboardData,
+ Future Function(String)? getClipboardData,
+ }) : _setClipboardData = setClipboardData ?? Clipboard.setData,
+ _getClipboardData = getClipboardData ?? Clipboard.getData,
+ super(SpriteState.initial());
+
+ final Future Function(ClipboardData) _setClipboardData;
+ final Future Function(String) _getClipboardData;
+
+ void copyToClipboard() {
+ final sprite = MiniSprite(state.pixels);
+ final data = sprite.toDataString();
+ _setClipboardData(ClipboardData(text: data));
+ }
+
+ Future importFromClipboard() async {
+ final data = await _getClipboardData('text/plain');
+ final text = data?.text;
+ if (text != null) {
+ final sprite = MiniSprite.fromDataString(text);
+ emit(state.copyWith(pixels: sprite.pixels));
+ }
+ }
+
+ void zoomIn() {
+ emit(state.copyWith(pixelSize: state.pixelSize + 10));
+ }
+
+ void zoomOut() {
+ emit(state.copyWith(pixelSize: state.pixelSize - 10));
+ }
+
+ void cursorLeft() {
+ emit(state.copyWith(cursorPosition: const Offset(-1, -1)));
+ }
+
+ void selectTool(SpriteTool tool) {
+ emit(state.copyWith(tool: tool));
+ }
+
+ Offset _projectOffset(Offset position) {
+ final projected = position / state.pixelSize.toDouble();
+ final x = projected.dx.floorToDouble();
+ final y = projected.dy.floorToDouble();
+
+ return Offset(x, y);
+ }
+
+ void _setPixel(bool value) {
+ final newPixels = [
+ ...state.pixels.map((e) => [...e]),
+ ];
+ final x = state.cursorPosition.dx.toInt();
+ final y = state.cursorPosition.dy.toInt();
+
+ newPixels[y][x] = value;
+ emit(state.copyWith(pixels: newPixels));
+ }
+
+ void _floodFillSeek(int x, int y, bool value, List> pixels) {
+ if (y < 0 || y >= pixels.length || x < 0 || x >= pixels[0].length) {
+ return;
+ }
+
+ if (pixels[y][x] == value) {
+ return;
+ }
+ pixels[y][x] = value;
+ _floodFillSeek(x + 1, y, value, pixels);
+ _floodFillSeek(x - 1, y, value, pixels);
+ _floodFillSeek(x, y + 1, value, pixels);
+ _floodFillSeek(x, y - 1, value, pixels);
+ }
+
+ void _floodFill(bool value) {
+ final newPixels = [
+ ...state.pixels.map((e) => [...e]),
+ ];
+
+ _floodFillSeek(
+ state.cursorPosition.dx.toInt(),
+ state.cursorPosition.dy.toInt(),
+ value,
+ newPixels,
+ );
+
+ emit(state.copyWith(pixels: newPixels, toolActive: false));
+ }
+
+ void _processTool() {
+ switch (state.tool) {
+ case SpriteTool.brush:
+ case SpriteTool.eraser:
+ if (state.toolActive) {
+ _setPixel(state.tool == SpriteTool.brush);
+ }
+ break;
+ case SpriteTool.bucket:
+ case SpriteTool.bucketEraser:
+ if (state.toolActive) {
+ _floodFill(state.tool == SpriteTool.bucket);
+ }
+ break;
+ }
+ }
+
+ void cursorHover(Offset position) {
+ final projected = _projectOffset(position);
+ if (projected != state.cursorPosition) {
+ emit(state.copyWith(cursorPosition: projected));
+ _processTool();
+ }
+ }
+
+ void cursorDown(Offset position) {
+ final projected = _projectOffset(position);
+ emit(state.copyWith(cursorPosition: projected, toolActive: true));
+ _processTool();
+ }
+
+ void cursorUp() {
+ emit(state.copyWith(toolActive: false));
+ _processTool();
+ }
+}
diff --git a/packages/mini_sprite_editor/lib/sprite/cubit/sprite_state.dart b/packages/mini_sprite_editor/lib/sprite/cubit/sprite_state.dart
new file mode 100644
index 0000000..848b037
--- /dev/null
+++ b/packages/mini_sprite_editor/lib/sprite/cubit/sprite_state.dart
@@ -0,0 +1,63 @@
+part of 'sprite_cubit.dart';
+
+const _defaultSpriteSize = 16;
+
+enum SpriteTool {
+ brush,
+ eraser,
+ bucket,
+ bucketEraser,
+}
+
+class SpriteState extends Equatable {
+ const SpriteState({
+ required this.pixelSize,
+ required this.pixels,
+ required this.cursorPosition,
+ required this.tool,
+ required this.toolActive,
+ });
+
+ SpriteState.initial()
+ : this(
+ pixels: List.filled(
+ _defaultSpriteSize,
+ List.filled(_defaultSpriteSize, false),
+ ),
+ pixelSize: 25,
+ cursorPosition: const Offset(-1, -1),
+ tool: SpriteTool.brush,
+ toolActive: false,
+ );
+
+ final int pixelSize;
+ final List> pixels;
+ final Offset cursorPosition;
+ final SpriteTool tool;
+ final bool toolActive;
+
+ SpriteState copyWith({
+ int? pixelSize,
+ List>? pixels,
+ Offset? cursorPosition,
+ SpriteTool? tool,
+ bool? toolActive,
+ }) {
+ return SpriteState(
+ pixelSize: pixelSize ?? this.pixelSize,
+ pixels: pixels ?? this.pixels,
+ cursorPosition: cursorPosition ?? this.cursorPosition,
+ tool: tool ?? this.tool,
+ toolActive: toolActive ?? this.toolActive,
+ );
+ }
+
+ @override
+ List