Skip to content

Commit

Permalink
fix: Fix brighten and darken alpha issue (#3414)
Browse files Browse the repository at this point in the history
Fix brighten and darken alpha issue.
This is all thanks to @jtmcdole's insight & fixes
[here](#3407).

---------

Co-authored-by: Lukas Klingsbo <[email protected]>
Co-authored-by: Erick Zanardo <[email protected]>
Co-authored-by: Lukas Klingsbo <[email protected]>
Co-authored-by: John McDole <[email protected]>
  • Loading branch information
5 people authored Dec 17, 2024
1 parent de74a93 commit de8e3bc
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 69 deletions.
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

0 comments on commit de8e3bc

Please sign in to comment.