diff --git a/examples/lib/stories/image/brighten.dart b/examples/lib/stories/image/brighten.dart new file mode 100644 index 00000000000..ed0b07f9bc9 --- /dev/null +++ b/examples/lib/stories/image/brighten.dart @@ -0,0 +1,40 @@ +import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; + +class ImageBrightnessExample extends FlameGame { + ImageBrightnessExample({ + required this.brightness, + }); + + final double brightness; + + static const String description = ''' + Shows how a dart:ui `Image` can be brightened using Flame Image extensions. + Use the properties on the side to change the brightness of the image. + '''; + + @override + Future onLoad() async { + final image = await images.load('flame.png'); + final brightenedImage = await image.brighten(brightness / 100); + + add( + SpriteComponent( + sprite: Sprite(image), + position: (size / 2) - Vector2(0, image.height / 2), + size: image.size, + anchor: Anchor.center, + ), + ); + + add( + SpriteComponent( + sprite: Sprite(brightenedImage), + position: (size / 2) + Vector2(0, brightenedImage.height / 2), + size: image.size, + anchor: Anchor.center, + ), + ); + } +} diff --git a/examples/lib/stories/image/darken.dart b/examples/lib/stories/image/darken.dart new file mode 100644 index 00000000000..f3b8d25c777 --- /dev/null +++ b/examples/lib/stories/image/darken.dart @@ -0,0 +1,40 @@ +import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; + +class ImageDarknessExample extends FlameGame { + ImageDarknessExample({ + required this.darkness, + }); + + final double darkness; + + static const String description = ''' + Shows how a dart:ui `Image` can be darkened using Flame Image extensions. + Use the properties on the side to change the darkness of the image. + '''; + + @override + Future onLoad() async { + final image = await images.load('flame.png'); + final darkenedImage = await image.darken(darkness / 100); + + add( + SpriteComponent( + sprite: Sprite(image), + position: (size / 2) - Vector2(0, image.height / 2), + size: image.size, + anchor: Anchor.center, + ), + ); + + add( + SpriteComponent( + sprite: Sprite(darkenedImage), + position: (size / 2) + Vector2(0, darkenedImage.height / 2), + size: image.size, + anchor: Anchor.center, + ), + ); + } +} diff --git a/examples/lib/stories/image/image.dart b/examples/lib/stories/image/image.dart index af4eae7bfbc..181c9862fda 100644 --- a/examples/lib/stories/image/image.dart +++ b/examples/lib/stories/image/image.dart @@ -1,6 +1,8 @@ import 'package:dashbook/dashbook.dart'; import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/image/brighten.dart'; +import 'package:examples/stories/image/darken.dart'; import 'package:examples/stories/image/resize.dart'; import 'package:flame/game.dart'; @@ -19,5 +21,25 @@ void addImageStories(Dashbook dashbook) { ), codeLink: baseLink('image/resize.dart'), info: ImageResizeExample.description, + ) + ..add( + 'brightness', + (context) => GameWidget( + game: ImageBrightnessExample( + brightness: context.numberProperty('brightness', 50), + ), + ), + codeLink: baseLink('image/brighten.dart'), + info: ImageBrightnessExample.description, + ) + ..add( + 'darkness', + (context) => GameWidget( + game: ImageDarknessExample( + darkness: context.numberProperty('darkness', 50), + ), + ), + codeLink: baseLink('image/darkness.dart'), + info: ImageDarknessExample.description, ); } diff --git a/packages/flame/lib/src/extensions/color.dart b/packages/flame/lib/src/extensions/color.dart index 2cfb16c2b8b..4c78ae28ed8 100644 --- a/packages/flame/lib/src/extensions/color.dart +++ b/packages/flame/lib/src/extensions/color.dart @@ -1,6 +1,8 @@ import 'dart:math'; import 'dart:ui'; +import 'package:flutter/painting.dart' show HSLColor; + export 'dart:ui' show Color; extension ColorExtension on Color { @@ -12,13 +14,12 @@ extension ColorExtension on Color { Color darken(double amount) { assert(amount >= 0 && amount <= 1); - final f = 1 - amount; - return Color.fromARGB( - a ~/ 255, - (r * f).round(), - (g * f).round(), - (b * f).round(), - ); + final hsl = HSLColor.fromColor(this); + return hsl + .withLightness( + clampDouble(hsl.lightness * amount, 0.0, 1.0), + ) + .toColor(); } /// Brighten the shade of the color by the [amount]. @@ -29,12 +30,12 @@ extension ColorExtension on Color { Color brighten(double amount) { assert(amount >= 0 && amount <= 1); - return Color.fromARGB( - a ~/ 255, - (r + ((1.0 - r) * amount)) ~/ 255, - (g + ((1.0 - g) * amount)) ~/ 255, - (b + ((1.0 - b) * amount)) ~/ 255, - ); + final hsl = HSLColor.fromColor(this); + return hsl + .withLightness( + clampDouble(hsl.lightness + (1 - hsl.lightness) * amount, 0.0, 1.0), + ) + .toColor(); } // used as an example hex color code on the documentation below diff --git a/packages/flame/lib/src/extensions/image.dart b/packages/flame/lib/src/extensions/image.dart index d6fe177f08d..20c345f8d66 100644 --- a/packages/flame/lib/src/extensions/image.dart +++ b/packages/flame/lib/src/extensions/image.dart @@ -37,6 +37,45 @@ extension ImageExtension on Image { return (await toByteData())!.buffer.asUint8List(); } + Future transformPixels( + Color Function(Color) transform, { + bool reversePremultipliedAlpha = true, + }) async { + final pixelData = await pixelsInUint8(); + final newPixelData = Uint8List(pixelData.length); + + for (var i = 0; i < pixelData.length; i += 4) { + final r = pixelData[i + 0] / 255; + final g = pixelData[i + 1] / 255; + final b = pixelData[i + 2] / 255; + final a = pixelData[i + 3] / 255; + + final d = a == 0 || !reversePremultipliedAlpha ? 1 : a; + + // Reverse the pre-multiplied alpha. + final color = Color.from( + alpha: a, + red: r / d, + green: g / d, + blue: b / d, + ); + + final newColor = a == 0 ? color : transform(color); + + final newR = newColor.r; + final newG = newColor.g; + final newB = newColor.b; + + // Pre-multiply the alpha back into the new color. + newPixelData[i + 0] = (newR * d * 255).round(); + newPixelData[i + 1] = (newG * d * 255).round(); + newPixelData[i + 2] = (newB * d * 255).round(); + newPixelData[i + 3] = pixelData[i + 3]; + } + + return fromPixels(newPixelData, width, height); + } + /// Returns the bounding [Rect] of the image. Rect getBoundingRect() => Vector2.zero() & size; @@ -46,51 +85,31 @@ extension ImageExtension on Image { /// Change each pixel's color to be darker and return a new [Image]. /// /// The [amount] is a double value between 0 and 1. - Future darken(double amount) async { + Future darken( + double amount, { + bool reversePremultipliedAlpha = true, + }) async { assert(amount >= 0 && amount <= 1); - final pixelData = await pixelsInUint8(); - final newPixelData = Uint8List(pixelData.length); - - for (var i = 0; i < pixelData.length; i += 4) { - final color = Color.fromARGB( - pixelData[i + 3], - pixelData[i + 0], - pixelData[i + 1], - pixelData[i + 2], - ).darken(amount); - - newPixelData[i] = color.r ~/ 255; - newPixelData[i + 1] = color.g ~/ 255; - newPixelData[i + 2] = color.b ~/ 255; - newPixelData[i + 3] = color.a ~/ 255; - } - return fromPixels(newPixelData, width, height); + return transformPixels( + (color) => color.darken(amount), + reversePremultipliedAlpha: reversePremultipliedAlpha, + ); } /// Change each pixel's color to be brighter and return a new [Image]. /// /// The [amount] is a double value between 0 and 1. - Future brighten(double amount) async { + Future brighten( + double amount, { + bool reversePremultipliedAlpha = false, + }) async { assert(amount >= 0 && amount <= 1); - final pixelData = await pixelsInUint8(); - final newPixelData = Uint8List(pixelData.length); - - for (var i = 0; i < pixelData.length; i += 4) { - final color = Color.fromARGB( - pixelData[i + 3], - pixelData[i + 0], - pixelData[i + 1], - pixelData[i + 2], - ).brighten(amount); - - newPixelData[i] = color.r ~/ 255; - newPixelData[i + 1] = color.g ~/ 255; - newPixelData[i + 2] = color.b ~/ 255; - newPixelData[i + 3] = color.a ~/ 255; - } - return fromPixels(newPixelData, width, height); + return transformPixels( + (color) => color.brighten(amount), + reversePremultipliedAlpha: reversePremultipliedAlpha, + ); } /// Resizes this image to the given [newSize]. diff --git a/packages/flame/test/extensions/image_extension_test.dart b/packages/flame/test/extensions/image_extension_test.dart index e2f0cc5aee1..919866dd715 100644 --- a/packages/flame/test/extensions/image_extension_test.dart +++ b/packages/flame/test/extensions/image_extension_test.dart @@ -54,53 +54,66 @@ void main() { }); test('darken colors each pixel darker', () async { - const originalColor = Color.fromARGB(193, 135, 73, 73); + const transparentColor = Color.fromARGB(0, 255, 0, 255); + const originalColor = Color.fromARGB(255, 135, 73, 73); final pixels = Uint8List.fromList( List.generate( 100 * 4, - (index) => _colorBit(index, originalColor), + (index) => _colorBit( + index, + index < 200 ? transparentColor : originalColor, + ), ), ); final image = await ImageExtension.fromPixels(pixels, 10, 10); const darkenAmount = 0.5; - final originalDarkenImage = await image.darken(darkenAmount); - final originalDarkenPixelsList = - await originalDarkenImage.pixelsInUint8(); + final actualDarkenedImage = await image.darken(darkenAmount); + final actualDarkenedPixels = await actualDarkenedImage.pixelsInUint8(); - final darkenColor = originalColor.darken(darkenAmount); + final darkenedColor = originalColor.darken(darkenAmount); final expectedDarkenPixels = Uint8List.fromList( List.generate( 100 * 4, - (index) => _colorBit(index, darkenColor), + (index) => _colorBit( + index, + index < 200 ? transparentColor : darkenedColor, + ), ), ); - expect(originalDarkenPixelsList, expectedDarkenPixels); + expect(actualDarkenedPixels, expectedDarkenPixels); }); test('brighten colors each pixel brighter', () async { - const originalColor = Color.fromARGB(193, 135, 73, 73); + const transparentColor = Color.fromARGB(0, 255, 0, 255); + const originalColor = Color.fromARGB(255, 255, 0, 0); + final pixels = Uint8List.fromList( List.generate( 100 * 4, - (index) => _colorBit(index, originalColor), + (index) => _colorBit( + index, + index < 200 ? transparentColor : originalColor, + ), ), ); final image = await ImageExtension.fromPixels(pixels, 10, 10); const brightenAmount = 0.5; - final originalBrightenImage = await image.brighten(brightenAmount); - final originalBrightenPixelsList = - await originalBrightenImage.pixelsInUint8(); + final brightenedImage = await image.brighten(brightenAmount); + final actualBrightenedPixels = await brightenedImage.pixelsInUint8(); final brightenColor = originalColor.brighten(brightenAmount); final expectedBrightenPixels = Uint8List.fromList( List.generate( 100 * 4, - (index) => _colorBit(index, brightenColor), + (index) => _colorBit( + index, + index < 200 ? transparentColor : brightenColor, + ), ), ); - expect(originalBrightenPixelsList, expectedBrightenPixels); + expect(actualBrightenedPixels, expectedBrightenPixels); }); test('resize resizes the image', () async { @@ -121,10 +134,10 @@ void main() { int _colorBit(int index, Color color) { return switch (index % 4) { - 0 => color.r ~/ 255, - 1 => color.g ~/ 255, - 2 => color.b ~/ 255, - 3 => color.a ~/ 255, + 0 => (color.r * 255).round(), + 1 => (color.g * 255).round(), + 2 => (color.b * 255).round(), + 3 => (color.a * 255).round(), _ => throw UnimplementedError(), }; } diff --git a/packages/flame/test/extensions/paint_test.dart b/packages/flame/test/extensions/paint_test.dart index 8f735ecdb81..7f5ad8f0fc1 100644 --- a/packages/flame/test/extensions/paint_test.dart +++ b/packages/flame/test/extensions/paint_test.dart @@ -32,7 +32,7 @@ void main() { paint.brighten(brightenAmount); - expect( + expectColor( paint.color, brightenBaseColor, reason: "Paint's color does not match brighten color",