Skip to content

Commit de8e3bc

Browse files
luanpotterspydonerickzanardojtmcdole
authored
fix: Fix brighten and darken alpha issue (#3414)
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]>
1 parent de74a93 commit de8e3bc

File tree

7 files changed

+204
-69
lines changed

7 files changed

+204
-69
lines changed
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'package:flame/components.dart';
2+
import 'package:flame/extensions.dart';
3+
import 'package:flame/game.dart';
4+
5+
class ImageBrightnessExample extends FlameGame {
6+
ImageBrightnessExample({
7+
required this.brightness,
8+
});
9+
10+
final double brightness;
11+
12+
static const String description = '''
13+
Shows how a dart:ui `Image` can be brightened using Flame Image extensions.
14+
Use the properties on the side to change the brightness of the image.
15+
''';
16+
17+
@override
18+
Future<void> onLoad() async {
19+
final image = await images.load('flame.png');
20+
final brightenedImage = await image.brighten(brightness / 100);
21+
22+
add(
23+
SpriteComponent(
24+
sprite: Sprite(image),
25+
position: (size / 2) - Vector2(0, image.height / 2),
26+
size: image.size,
27+
anchor: Anchor.center,
28+
),
29+
);
30+
31+
add(
32+
SpriteComponent(
33+
sprite: Sprite(brightenedImage),
34+
position: (size / 2) + Vector2(0, brightenedImage.height / 2),
35+
size: image.size,
36+
anchor: Anchor.center,
37+
),
38+
);
39+
}
40+
}
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'package:flame/components.dart';
2+
import 'package:flame/extensions.dart';
3+
import 'package:flame/game.dart';
4+
5+
class ImageDarknessExample extends FlameGame {
6+
ImageDarknessExample({
7+
required this.darkness,
8+
});
9+
10+
final double darkness;
11+
12+
static const String description = '''
13+
Shows how a dart:ui `Image` can be darkened using Flame Image extensions.
14+
Use the properties on the side to change the darkness of the image.
15+
''';
16+
17+
@override
18+
Future<void> onLoad() async {
19+
final image = await images.load('flame.png');
20+
final darkenedImage = await image.darken(darkness / 100);
21+
22+
add(
23+
SpriteComponent(
24+
sprite: Sprite(image),
25+
position: (size / 2) - Vector2(0, image.height / 2),
26+
size: image.size,
27+
anchor: Anchor.center,
28+
),
29+
);
30+
31+
add(
32+
SpriteComponent(
33+
sprite: Sprite(darkenedImage),
34+
position: (size / 2) + Vector2(0, darkenedImage.height / 2),
35+
size: image.size,
36+
anchor: Anchor.center,
37+
),
38+
);
39+
}
40+
}

examples/lib/stories/image/image.dart

+22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:dashbook/dashbook.dart';
22

33
import 'package:examples/commons/commons.dart';
4+
import 'package:examples/stories/image/brighten.dart';
5+
import 'package:examples/stories/image/darken.dart';
46
import 'package:examples/stories/image/resize.dart';
57
import 'package:flame/game.dart';
68

@@ -19,5 +21,25 @@ void addImageStories(Dashbook dashbook) {
1921
),
2022
codeLink: baseLink('image/resize.dart'),
2123
info: ImageResizeExample.description,
24+
)
25+
..add(
26+
'brightness',
27+
(context) => GameWidget(
28+
game: ImageBrightnessExample(
29+
brightness: context.numberProperty('brightness', 50),
30+
),
31+
),
32+
codeLink: baseLink('image/brighten.dart'),
33+
info: ImageBrightnessExample.description,
34+
)
35+
..add(
36+
'darkness',
37+
(context) => GameWidget(
38+
game: ImageDarknessExample(
39+
darkness: context.numberProperty('darkness', 50),
40+
),
41+
),
42+
codeLink: baseLink('image/darkness.dart'),
43+
info: ImageDarknessExample.description,
2244
);
2345
}

packages/flame/lib/src/extensions/color.dart

+14-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:math';
22
import 'dart:ui';
33

4+
import 'package:flutter/painting.dart' show HSLColor;
5+
46
export 'dart:ui' show Color;
57

68
extension ColorExtension on Color {
@@ -12,13 +14,12 @@ extension ColorExtension on Color {
1214
Color darken(double amount) {
1315
assert(amount >= 0 && amount <= 1);
1416

15-
final f = 1 - amount;
16-
return Color.fromARGB(
17-
a ~/ 255,
18-
(r * f).round(),
19-
(g * f).round(),
20-
(b * f).round(),
21-
);
17+
final hsl = HSLColor.fromColor(this);
18+
return hsl
19+
.withLightness(
20+
clampDouble(hsl.lightness * amount, 0.0, 1.0),
21+
)
22+
.toColor();
2223
}
2324

2425
/// Brighten the shade of the color by the [amount].
@@ -29,12 +30,12 @@ extension ColorExtension on Color {
2930
Color brighten(double amount) {
3031
assert(amount >= 0 && amount <= 1);
3132

32-
return Color.fromARGB(
33-
a ~/ 255,
34-
(r + ((1.0 - r) * amount)) ~/ 255,
35-
(g + ((1.0 - g) * amount)) ~/ 255,
36-
(b + ((1.0 - b) * amount)) ~/ 255,
37-
);
33+
final hsl = HSLColor.fromColor(this);
34+
return hsl
35+
.withLightness(
36+
clampDouble(hsl.lightness + (1 - hsl.lightness) * amount, 0.0, 1.0),
37+
)
38+
.toColor();
3839
}
3940

4041
// used as an example hex color code on the documentation below

packages/flame/lib/src/extensions/image.dart

+55-36
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,45 @@ extension ImageExtension on Image {
3737
return (await toByteData())!.buffer.asUint8List();
3838
}
3939

40+
Future<Image> transformPixels(
41+
Color Function(Color) transform, {
42+
bool reversePremultipliedAlpha = true,
43+
}) async {
44+
final pixelData = await pixelsInUint8();
45+
final newPixelData = Uint8List(pixelData.length);
46+
47+
for (var i = 0; i < pixelData.length; i += 4) {
48+
final r = pixelData[i + 0] / 255;
49+
final g = pixelData[i + 1] / 255;
50+
final b = pixelData[i + 2] / 255;
51+
final a = pixelData[i + 3] / 255;
52+
53+
final d = a == 0 || !reversePremultipliedAlpha ? 1 : a;
54+
55+
// Reverse the pre-multiplied alpha.
56+
final color = Color.from(
57+
alpha: a,
58+
red: r / d,
59+
green: g / d,
60+
blue: b / d,
61+
);
62+
63+
final newColor = a == 0 ? color : transform(color);
64+
65+
final newR = newColor.r;
66+
final newG = newColor.g;
67+
final newB = newColor.b;
68+
69+
// Pre-multiply the alpha back into the new color.
70+
newPixelData[i + 0] = (newR * d * 255).round();
71+
newPixelData[i + 1] = (newG * d * 255).round();
72+
newPixelData[i + 2] = (newB * d * 255).round();
73+
newPixelData[i + 3] = pixelData[i + 3];
74+
}
75+
76+
return fromPixels(newPixelData, width, height);
77+
}
78+
4079
/// Returns the bounding [Rect] of the image.
4180
Rect getBoundingRect() => Vector2.zero() & size;
4281

@@ -46,51 +85,31 @@ extension ImageExtension on Image {
4685
/// Change each pixel's color to be darker and return a new [Image].
4786
///
4887
/// The [amount] is a double value between 0 and 1.
49-
Future<Image> darken(double amount) async {
88+
Future<Image> darken(
89+
double amount, {
90+
bool reversePremultipliedAlpha = true,
91+
}) async {
5092
assert(amount >= 0 && amount <= 1);
5193

52-
final pixelData = await pixelsInUint8();
53-
final newPixelData = Uint8List(pixelData.length);
54-
55-
for (var i = 0; i < pixelData.length; i += 4) {
56-
final color = Color.fromARGB(
57-
pixelData[i + 3],
58-
pixelData[i + 0],
59-
pixelData[i + 1],
60-
pixelData[i + 2],
61-
).darken(amount);
62-
63-
newPixelData[i] = color.r ~/ 255;
64-
newPixelData[i + 1] = color.g ~/ 255;
65-
newPixelData[i + 2] = color.b ~/ 255;
66-
newPixelData[i + 3] = color.a ~/ 255;
67-
}
68-
return fromPixels(newPixelData, width, height);
94+
return transformPixels(
95+
(color) => color.darken(amount),
96+
reversePremultipliedAlpha: reversePremultipliedAlpha,
97+
);
6998
}
7099

71100
/// Change each pixel's color to be brighter and return a new [Image].
72101
///
73102
/// The [amount] is a double value between 0 and 1.
74-
Future<Image> brighten(double amount) async {
103+
Future<Image> brighten(
104+
double amount, {
105+
bool reversePremultipliedAlpha = false,
106+
}) async {
75107
assert(amount >= 0 && amount <= 1);
76108

77-
final pixelData = await pixelsInUint8();
78-
final newPixelData = Uint8List(pixelData.length);
79-
80-
for (var i = 0; i < pixelData.length; i += 4) {
81-
final color = Color.fromARGB(
82-
pixelData[i + 3],
83-
pixelData[i + 0],
84-
pixelData[i + 1],
85-
pixelData[i + 2],
86-
).brighten(amount);
87-
88-
newPixelData[i] = color.r ~/ 255;
89-
newPixelData[i + 1] = color.g ~/ 255;
90-
newPixelData[i + 2] = color.b ~/ 255;
91-
newPixelData[i + 3] = color.a ~/ 255;
92-
}
93-
return fromPixels(newPixelData, width, height);
109+
return transformPixels(
110+
(color) => color.brighten(amount),
111+
reversePremultipliedAlpha: reversePremultipliedAlpha,
112+
);
94113
}
95114

96115
/// Resizes this image to the given [newSize].

packages/flame/test/extensions/image_extension_test.dart

+32-19
Original file line numberDiff line numberDiff line change
@@ -54,53 +54,66 @@ void main() {
5454
});
5555

5656
test('darken colors each pixel darker', () async {
57-
const originalColor = Color.fromARGB(193, 135, 73, 73);
57+
const transparentColor = Color.fromARGB(0, 255, 0, 255);
58+
const originalColor = Color.fromARGB(255, 135, 73, 73);
5859
final pixels = Uint8List.fromList(
5960
List<int>.generate(
6061
100 * 4,
61-
(index) => _colorBit(index, originalColor),
62+
(index) => _colorBit(
63+
index,
64+
index < 200 ? transparentColor : originalColor,
65+
),
6266
),
6367
);
6468
final image = await ImageExtension.fromPixels(pixels, 10, 10);
6569

6670
const darkenAmount = 0.5;
67-
final originalDarkenImage = await image.darken(darkenAmount);
68-
final originalDarkenPixelsList =
69-
await originalDarkenImage.pixelsInUint8();
71+
final actualDarkenedImage = await image.darken(darkenAmount);
72+
final actualDarkenedPixels = await actualDarkenedImage.pixelsInUint8();
7073

71-
final darkenColor = originalColor.darken(darkenAmount);
74+
final darkenedColor = originalColor.darken(darkenAmount);
7275
final expectedDarkenPixels = Uint8List.fromList(
7376
List<int>.generate(
7477
100 * 4,
75-
(index) => _colorBit(index, darkenColor),
78+
(index) => _colorBit(
79+
index,
80+
index < 200 ? transparentColor : darkenedColor,
81+
),
7682
),
7783
);
78-
expect(originalDarkenPixelsList, expectedDarkenPixels);
84+
expect(actualDarkenedPixels, expectedDarkenPixels);
7985
});
8086

8187
test('brighten colors each pixel brighter', () async {
82-
const originalColor = Color.fromARGB(193, 135, 73, 73);
88+
const transparentColor = Color.fromARGB(0, 255, 0, 255);
89+
const originalColor = Color.fromARGB(255, 255, 0, 0);
90+
8391
final pixels = Uint8List.fromList(
8492
List<int>.generate(
8593
100 * 4,
86-
(index) => _colorBit(index, originalColor),
94+
(index) => _colorBit(
95+
index,
96+
index < 200 ? transparentColor : originalColor,
97+
),
8798
),
8899
);
89100
final image = await ImageExtension.fromPixels(pixels, 10, 10);
90101

91102
const brightenAmount = 0.5;
92-
final originalBrightenImage = await image.brighten(brightenAmount);
93-
final originalBrightenPixelsList =
94-
await originalBrightenImage.pixelsInUint8();
103+
final brightenedImage = await image.brighten(brightenAmount);
104+
final actualBrightenedPixels = await brightenedImage.pixelsInUint8();
95105

96106
final brightenColor = originalColor.brighten(brightenAmount);
97107
final expectedBrightenPixels = Uint8List.fromList(
98108
List<int>.generate(
99109
100 * 4,
100-
(index) => _colorBit(index, brightenColor),
110+
(index) => _colorBit(
111+
index,
112+
index < 200 ? transparentColor : brightenColor,
113+
),
101114
),
102115
);
103-
expect(originalBrightenPixelsList, expectedBrightenPixels);
116+
expect(actualBrightenedPixels, expectedBrightenPixels);
104117
});
105118

106119
test('resize resizes the image', () async {
@@ -121,10 +134,10 @@ void main() {
121134

122135
int _colorBit(int index, Color color) {
123136
return switch (index % 4) {
124-
0 => color.r ~/ 255,
125-
1 => color.g ~/ 255,
126-
2 => color.b ~/ 255,
127-
3 => color.a ~/ 255,
137+
0 => (color.r * 255).round(),
138+
1 => (color.g * 255).round(),
139+
2 => (color.b * 255).round(),
140+
3 => (color.a * 255).round(),
128141
_ => throw UnimplementedError(),
129142
};
130143
}

packages/flame/test/extensions/paint_test.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ void main() {
3232

3333
paint.brighten(brightenAmount);
3434

35-
expect(
35+
expectColor(
3636
paint.color,
3737
brightenBaseColor,
3838
reason: "Paint's color does not match brighten color",

0 commit comments

Comments
 (0)