Skip to content

Commit 8c68690

Browse files
committed
Image fills apply their opacity once
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UErtKMSFkZWHUh8DrQMcZW
1 parent 0535661 commit 8c68690

3 files changed

Lines changed: 54 additions & 1 deletion

File tree

‎lib/src/transformers/node_transformers/passive_rectangle_transformer.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ Widget buildFill(
807807
node: node,
808808
bytes: bytes,
809809
useInk: useInk,
810+
applyPaintOpacity: false,
810811
);
811812
}
812813
}

‎lib/src/utils/image_builder.dart‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ class UltimateImageBuilder extends StatefulWidget {
2525
final BlendMode? blendMode;
2626
final bool useInk;
2727

28+
/// Whether to paint the image at [paint]'s opacity. Off when the caller
29+
/// applies the opacity itself, like an image fill, which wraps every kind
30+
/// of image (network, bytes, SVG) in one [Opacity].
31+
final bool applyPaintOpacity;
32+
2833
const UltimateImageBuilder({
2934
super.key,
3035
this.url,
@@ -42,6 +47,7 @@ class UltimateImageBuilder extends StatefulWidget {
4247
this.node,
4348
this.blendMode,
4449
this.useInk = false,
50+
this.applyPaintOpacity = true,
4551
}) : assert(
4652
url != null || bytes != null || paint != null,
4753
'url or bytes or paint must be provided',
@@ -259,7 +265,9 @@ class _UltimateImageBuilderState extends State<UltimateImageBuilder> {
259265
repeat: repeat,
260266
scale: scale,
261267
color: widget.color,
262-
opacity: AlwaysStoppedAnimation(widget.paint?.opacity ?? 1),
268+
opacity: AlwaysStoppedAnimation(
269+
widget.applyPaintOpacity ? widget.paint?.opacity ?? 1 : 1,
270+
),
263271
filterQuality: FilterQuality.medium,
264272
colorBlendMode: colorBlendMode,
265273
// On the web, hosts that don't allow cross-origin reads (no CORS

‎test/image_fill_opacity_test.dart‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:codelessly_sdk/codelessly_sdk.dart';
3+
import 'package:flutter/widgets.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
void main() {
7+
// An image fill at 35% opacity painted at about 12%: the fill wrapped the
8+
// image in an Opacity and the network image applied the opacity again.
9+
testWidgets('an image fill applies its opacity once', (tester) async {
10+
final RectangleNode node = RectangleNode(
11+
id: 'paper',
12+
name: 'Paper',
13+
basicBoxLocal: NodeBox(0, 0, 100, 100),
14+
fills: [
15+
PaintModel(
16+
id: 'topo',
17+
type: PaintType.image,
18+
downloadUrl: 'https://example.com/topo.png',
19+
opacity: 0.35,
20+
fit: Fit.cover,
21+
),
22+
],
23+
);
24+
await tester.pumpWidget(
25+
Directionality(
26+
textDirection: TextDirection.ltr,
27+
child: buildFill(
28+
node,
29+
index: 0,
30+
useInk: false,
31+
settings: const WidgetBuildSettings(debugLabel: 'test'),
32+
scopedValues: ScopedValues.empty(),
33+
),
34+
),
35+
);
36+
final double opacity = tester
37+
.widgetList<Opacity>(find.byType(Opacity))
38+
.fold(1.0, (value, widget) => value * widget.opacity);
39+
final double imageOpacity = tester
40+
.widgetList<Image>(find.byType(Image))
41+
.fold(1.0, (value, widget) => value * (widget.opacity?.value ?? 1));
42+
expect(opacity * imageOpacity, closeTo(0.35, 0.001));
43+
});
44+
}

0 commit comments

Comments
 (0)