-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Closed
Labels
r: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer version
Description
Steps to reproduce
Call toImage on picture recorder or RenderPaintBoundaries containing an canvas where it has a background and rendered paths on it.
Tested on Linux Fedora 43 with flutter 3.38.1, on web this problem doesn't exist. Last stable 3.35.7 didn't have the issue.
The problem is really visible in my note taking app https://github.com/LinwoodDev/Butterfly
Expected results
Rendered output like in the app. Background yellow and above the paths.
Actual results
The background behind the strokes won't be rendered
Code sample
Code sample
import 'dart:math';
import 'dart:ui' as ui;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Rainbow Paths',
theme: ThemeData(
// Using a modern Material 3 theme.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const RainbowPathsPage(),
);
}
}
class RainbowPathsPage extends StatefulWidget {
const RainbowPathsPage({super.key});
@override
State<RainbowPathsPage> createState() => _RainbowPathsPageState();
}
class _RainbowPathsPageState extends State<RainbowPathsPage> {
List<List<Offset>> _pathsData = [];
final GlobalKey _repaintBoundaryKey = GlobalKey();
// Configuration for path generation:
final int _numberOfPaths = 100; // How many distinct paths to draw.
final int _segmentsPerPath =
15; // How many line segments each path will have.
// Maximum length a segment can extend from the previous point, to keep paths somewhat continuous.
final double _maxSegmentLength = 100.0;
@override
void initState() {
super.initState();
// Schedule the path generation after the first frame has been rendered.
// This ensures that `context` is fully initialized and has valid layout information
// (like the screen size) when `_generatePathsForSize` is called.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_generate(MediaQuery.of(context).size, 0);
}
});
}
void _generate(Size size, [int? seed]) {
setState(() {
_pathsData = _createPaths(size, seed);
});
}
List<List<Offset>> _createPaths(Size size, [int? seed]) {
final Random random = Random(seed);
final List<List<Offset>> newPaths = [];
for (int i = 0; i < _numberOfPaths; i++) {
final List<Offset> pathPoints = [];
// Start each path from a random point within the canvas
Offset currentPoint = Offset(
random.nextDouble() * size.width,
random.nextDouble() * size.height,
);
pathPoints.add(currentPoint);
for (int j = 0; j < _segmentsPerPath; j++) {
// Generate a random delta (dx, dy) for the next point.
// `_random.nextDouble() * 2 - 1` generates a value between -1 and 1.
final double dx = (random.nextDouble() * 2 - 1) * _maxSegmentLength;
final double dy = (random.nextDouble() * 2 - 1) * _maxSegmentLength;
// Calculate the potential next point
double nextX = currentPoint.dx + dx;
double nextY = currentPoint.dy + dy;
// Clamp the next point's coordinates to ensure it stays within the canvas bounds
nextX = nextX.clamp(0.0, size.width);
nextY = nextY.clamp(0.0, size.height);
currentPoint = Offset(nextX, nextY);
pathPoints.add(currentPoint);
}
newPaths.add(pathPoints);
}
return newPaths;
}
/// Determines the color for a path based on its index.
/// It creates a color gradient that transitions from red (for the first path, index 0)
/// through blue (around the middle paths) to black (for the last path).
Color _getPathColor(int index) {
if (_numberOfPaths <= 1) {
// Handle cases with 0 or 1 path to avoid division by zero or incorrect gradient.
return index == 0 ? Colors.red : Colors.black;
}
// Normalize the index to a value 't' between 0.0 and 1.0,
// representing the path's position in the sequence.
double t = index / (_numberOfPaths - 1);
// Interpolate colors: Red -> Blue for the first half, then Blue -> Black for the second half.
if (t < 0.5) {
// For paths in the first half (0.0 <= t < 0.5), interpolate from Red to Blue.
// `t * 2` scales 't' from [0.0, 0.5) to [0.0, 1.0) for the `Color.lerp` function.
return Color.lerp(Colors.red, Colors.blue, t * 2)!;
} else {
// For paths in the second half (0.5 <= t <= 1.0), interpolate from Blue to Black.
// `(t - 0.5) * 2` scales 't' from [0.5, 1.0] to [0.0, 1.0] for the `Color.lerp` function.
return Color.lerp(Colors.blue, Colors.black, (t - 0.5) * 2)!;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Rainbow Random Paths'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: GestureDetector(
child: RepaintBoundary(
key: _repaintBoundaryKey,
child: CustomPaint(
// Ensure CustomPaint takes up all available space within its parent.
size: Size.infinite,
// Only provide a painter if `_pathsData` is not empty.
// This prevents `CustomPaint` from attempting to draw before data is ready.
painter: _pathsData.isEmpty
? null
: RainbowPathsPainter(
pathsData: _pathsData,
getPathColor: _getPathColor,
),
),
),
),
floatingActionButton: FloatingActionButton(
heroTag: 'downloadFab',
onPressed: _saveCurrentView,
tooltip: 'Download Snapshot',
child: const Icon(Icons.download),
),
);
}
Future<void> _saveCurrentView() async {
if (kIsWeb) {
_showSnackBar('Download not supported on web.');
return;
}
final BuildContext? boundaryContext = _repaintBoundaryKey.currentContext;
final RenderObject? renderObject = boundaryContext?.findRenderObject();
if (renderObject is! RenderRepaintBoundary) {
_showSnackBar('Nothing to save right now.');
return;
}
final RenderRepaintBoundary boundary = renderObject;
try {
final double pixelRatio = MediaQuery.of(context).devicePixelRatio;
final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
final ByteData? byteData = await image.toByteData(
format: ui.ImageByteFormat.png,
);
image.dispose();
if (byteData == null) {
_showSnackBar('Failed to encode image.');
return;
}
final Directory dir = Directory.current;
final String fileName =
'randomsequence_${DateTime.now().millisecondsSinceEpoch}.png';
final File file = File(_joinPath(dir.path, fileName));
await file.writeAsBytes(byteData.buffer.asUint8List());
_showSnackBar('Saved to ${file.path}');
} catch (error) {
_showSnackBar('Save failed: $error');
}
}
String _joinPath(String base, String segment) {
if (base.endsWith(Platform.pathSeparator)) {
return '$base$segment';
}
return '$base${Platform.pathSeparator}$segment';
}
void _showSnackBar(String message) {
if (!mounted) {
return;
}
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
}
/// A custom painter responsible for drawing the generated paths onto the canvas.
class RainbowPathsPainter extends CustomPainter {
final List<List<Offset>> pathsData;
final Function(int index) getPathColor;
RainbowPathsPainter({required this.pathsData, required this.getPathColor});
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = Colors.yellow,
);
// Iterate through each path in the `pathsData` list.
for (int i = 0; i < pathsData.length; i++) {
final List<Offset> pathPoints = pathsData[i];
if (pathPoints.length < 2) {
continue;
}
final Paint paint = Paint()
..color = getPathColor(i)
..strokeWidth = 3.0 + (i * 0.05)
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..isAntiAlias = true;
final path = Path()..moveTo(pathPoints.first.dx, pathPoints.first.dy);
for (int j = 1; j < pathPoints.length; j++) {
final Offset point = pathPoints[j];
path.lineTo(point.dx, point.dy);
}
// 3. Draw the path to the canvas
canvas.drawPath(path, paint);
}
}
@override
bool shouldRepaint(covariant RainbowPathsPainter oldDelegate) {
// This method determines if the painter needs to redraw.
// We want to repaint if the `pathsData` (the list of all paths) has changed.
// Since `_pathsData` is reassigned to a new list instance every time paths are regenerated,
// this comparison (`!=`) correctly triggers a repaint.
return oldDelegate.pathsData != pathsData;
}
}
Screenshots or Video
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.38.1, on Fedora Linux 43 (KDE Plasma Desktop Edition) 6.17.7-300.fc43.x86_64, locale de_DE.UTF-8) [36ms]
• Flutter version 3.38.1 on channel stable at /home/codedoctor/Tools/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision b45fa18946 (2 days ago), 2025-11-12 22:09:06 -0600
• Engine revision b5990e5ccc
• Dart version 3.10.0
• DevTools version 2.51.1
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0-rc1) [1.259ms]
• Android SDK at /home/codedoctor/Android/Sdk
• Emulator version 36.2.12.0 (build_id 14214601) (CL:N/A)
• Platform android-36, build-tools 36.1.0-rc1
• Java binary at: /home/codedoctor/.local/share/JetBrains/Toolbox/apps/android-studio/jbr/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72)
• All Android licenses accepted.
[✓] Chrome - develop for the web [5ms]
• CHROME_EXECUTABLE = /usr/bin/chromium-browser
[✓] Linux toolchain - develop for Linux desktop [405ms]
• clang version 21.1.4 (Fedora 21.1.4-1.fc43)
• cmake version 3.31.6
• ninja version 1.13.1
• pkg-config version 2.3.0
• OpenGL core renderer: NVIDIA GeForce RTX 4070 Ti SUPER/PCIe/SSE2
• OpenGL core version: 4.6.0 NVIDIA 580.105.08
• OpenGL core shading language version: 4.60 NVIDIA
• OpenGL ES renderer: NVIDIA GeForce RTX 4070 Ti SUPER/PCIe/SSE2
• OpenGL ES version: OpenGL ES 3.2 NVIDIA 580.105.08
• OpenGL ES shading language version: OpenGL ES GLSL ES 3.20
• GL_EXT_framebuffer_blit: yes
• GL_EXT_texture_format_BGRA8888: yes
[✓] Connected device (2 available) [133ms]
• Linux (desktop) • linux • linux-x64 • Fedora Linux 43 (KDE Plasma Desktop Edition) 6.17.7-300.fc43.x86_64
• Chrome (web) • chrome • web-javascript • Chromium 142.0.7444.134 Fedora Project
[✓] Network resources [541ms]
• All expected network resources are available.
• No issues found!adil192
Metadata
Metadata
Assignees
Labels
r: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer version

