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

fix: Fix brighten and darken alpha issue #3414

Merged
merged 13 commits into from
Dec 17, 2024
40 changes: 40 additions & 0 deletions examples/lib/stories/image/brighten.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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,
),
);
}
}
40 changes: 40 additions & 0 deletions examples/lib/stories/image/darken.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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,
),
);
}
}
22 changes: 22 additions & 0 deletions examples/lib/stories/image/image.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
);
}
27 changes: 14 additions & 13 deletions packages/flame/lib/src/extensions/color.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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].
Expand All @@ -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
Expand Down
91 changes: 55 additions & 36 deletions packages/flame/lib/src/extensions/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,45 @@ extension ImageExtension on Image {
return (await toByteData())!.buffer.asUint8List();
}

Future<Image> 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;

Expand All @@ -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<Image> darken(double amount) async {
Future<Image> 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<Image> brighten(double amount) async {
Future<Image> 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].
Expand Down
51 changes: 32 additions & 19 deletions packages/flame/test/extensions/image_extension_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>.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<int>.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<int>.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<int>.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 {
Expand All @@ -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(),
};
}
2 changes: 1 addition & 1 deletion packages/flame/test/extensions/paint_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ void main() {

paint.brighten(brightenAmount);

expect(
expectColor(
paint.color,
brightenBaseColor,
reason: "Paint's color does not match brighten color",
Expand Down
Loading