Hello can anyone tell me why the background to this masking attempt is black. This must be close but I just can't kill the background. I've seen others reference that saveLayer(rect, paint) is the key here as that shoves the whole canvas rect in to the masking operation. This question (no masking operation) and this one (no actual answer) are similar but were no use to me.
main.dart
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ui.Image mask;
ui.Image image;
#override
void initState() {
super.initState();
load('images/squircle.png').then((i) {
setState(() {
mask = i;
});
});
load('images/noodlejpg.jpg').then((i) {
setState(() {
image = i;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Colors.blue, title: Text('I am a title')),
body: SafeArea(
child: SizedBox(
width: 200.0,
height: 200.0,
child: CustomPaint(painter: OverlayPainter(mask, image)),
),
),
);
}
Future<ui.Image> load(String asset) async {
ByteData data = await rootBundle.load(asset);
ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
ui.FrameInfo fi = await codec.getNextFrame();
return fi.image;
}
}
class OverlayPainter extends CustomPainter {
ui.Image mask;
ui.Image image;
OverlayPainter(this.mask, this.image);
#override
void paint(Canvas canvas, Size size) {
var paint = new Paint();
paint.isAntiAlias = true;
if (image != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
canvas.save();
final Rect sourceRect = Alignment.center.inscribe(
sourceSize,
Offset.zero & inputSize,
);
canvas.drawImageRect(image, sourceRect, rect, paint);
canvas.restore();
}
if (mask != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Size inputSize = Size(mask.width.toDouble(), mask.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
canvas.saveLayer(rect, Paint()..blendMode = BlendMode.dstIn);
final Rect sourceRect = Alignment.center.inscribe(
sourceSize,
Offset.zero & inputSize,
);
canvas.drawImageRect(mask, sourceRect, rect, paint);
canvas.restore();
}
}
#override
bool shouldRepaint(OverlayPainter oldDelegate) {
return mask != oldDelegate.mask || image != oldDelegate.image;
}
}
noodlejpg.jpg
squircle.jpg
result
The key is in when to call saveLayer and when to call restore.
From here:
When using Canvas.saveLayer and Canvas.restore, the blend mode of the Paint given to the Canvas.saveLayer will be applied when Canvas.restore is called. Each call to Canvas.saveLayer introduces a new layer onto which shapes and images are painted; when Canvas.restore is called, that layer is then composited onto the parent layer, with the source being the most-recently-drawn shapes and images, and the destination being the parent layer. (For the first Canvas.saveLayer call, the parent layer is the canvas itself.)
Working code
#override
void paint(Canvas canvas, Size size) {
if (image != null && mask != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Paint paint = new Paint();
//Mask
Size maskInputSize = Size(mask.width.toDouble(), mask.height.toDouble());
final FittedSizes maskFittedSizes =
applyBoxFit(BoxFit.cover, maskInputSize, outputSize);
final Size maskSourceSize = maskFittedSizes.source;
final Rect maskSourceRect = Alignment.center
.inscribe(maskSourceSize, Offset.zero & maskInputSize);
canvas.saveLayer(rect, paint);
canvas.drawImageRect(mask, maskSourceRect, rect, paint);
//Image
Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
final Rect sourceRect =
Alignment.center.inscribe(sourceSize, Offset.zero & inputSize);
canvas.drawImageRect(
image, sourceRect, rect, paint..blendMode = BlendMode.srcIn);
canvas.restore();
}
}
Result:
as an option without any CustomPainters
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Center(
child: SizedBox(
width: 300,
height: 300,
child: MaskedImage(asset: 'images/noodlejpg.jpeg', mask: 'images/circle.png'),
),
),
),
),
);
}
}
class MaskedImage extends StatelessWidget {
final String asset;
final String mask;
MaskedImage({#required this.asset, #required this.mask});
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return FutureBuilder<List>(
future: _createShaderAndImage(asset, mask, constraints.maxWidth, constraints.maxHeight),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox.shrink();
return ShaderMask(
blendMode: BlendMode.dstATop,
shaderCallback: (rect) => snapshot.data[0],
child: snapshot.data[1],
);
},
);
});
}
Future<List> _createShaderAndImage(String asset, String mask, double w, double h) async {
ByteData data = await rootBundle.load(asset);
ByteData maskData = await rootBundle.load(mask);
Codec codec = await instantiateImageCodec(maskData.buffer.asUint8List(), targetWidth: w.toInt(), targetHeight: h.toInt());
FrameInfo fi = await codec.getNextFrame();
ImageShader shader = ImageShader(fi.image, TileMode.clamp, TileMode.clamp, Matrix4.identity().storage);
Image image = Image.memory(data.buffer.asUint8List(), fit: BoxFit.cover, width: w, height: h);
return [shader, image];
}
}
Related
I want to create a moving point that moves across an image. The picture is drawn on canvas. The point should approach the specified x and y coordinates.Image and point are generated. The point should move to the new specified position every 500ms by a timer.But it only moves when I move the mouse over buttons on the app.
class five_og extends StatefulWidget {
#override
State createState() {
return _five_og();
}
}
class _five_og extends State<five_og> {
ui.Image? image;
Timer? timer;
void refreshImage() {
if (i >= xList.length - 1) {
i = 0;
}
ImagePainter(image!);
i++;
}
#override
void dispose() {
super.dispose();
timer!.cancel();
}
#override
void initState() {
super.initState();
loadImage('assets/images/Erdgeschoss.png');
timer = Timer.periodic(
Duration(milliseconds: 500),
(timer) {
refreshImage();
},
);
}
Future loadImage(String path) async {
final data = await rootBundle.load(path);
final bytes = data.buffer.asUint8List();
final image = await decodeImageFromList(bytes);
setState(() {
this.image = image;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test'),
),
body: Center(
child: image == null
? CircularProgressIndicator()
: Container(
height: 600,
width: 600,
child: FittedBox(
child: SizedBox(
width: image!.width.toDouble(),
height: image!.width.toDouble(),
child: CustomPaint(
painter: ImagePainter(image!),
),
),
)
)
),
);
}
}
class ImagePainter extends CustomPainter {
final ui.Image image;
const ImagePainter(this.image);
#override
void paint(Canvas canvas, Size size) {
//final paint = Paint();
canvas.drawImage(image, Offset.zero, Paint());
final paint = Paint()
..color = Color.fromARGB(255, 15, 182, 253)
..style = PaintingStyle.fill;
canvas.drawCircle(Offset(xList[i], yList[i]), 13, paint);
}
#override
bool shouldRepaint(ImagePainter oldDelegate) {
return false;
}
}
I am stuck to find a solution for drawing over a photo and then save this photo including the drawing on the phone.
I am able to draw over it, but how to save the canvas with the photo ?
I tried different solution as below, but in this example I am stuck to get an ui.Image from my photo for adding it to the canvas.
Any angel for help on this ?
class PhotoPainterW extends ConsumerStatefulWidget {
PhotoPainterW({Key? key, required this.photoPath});
final String photoPath;
#override
_PhotoPainterWState createState() => new _PhotoPainterWState();
}
class _PhotoPainterWState extends ConsumerState<PhotoPainterW> {
List<Offset?> _points = <Offset?>[];
void saveImage(String photoPath) async {
ui.Image? image = await getUiImage(photoPath);
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
canvas.drawImage(image, new Offset(50.0, 50.0), new Paint());
SignaturePainter painter = SignaturePainter(points: _points);
var size = context.size;
painter.paint(canvas, size!);
final picture = recorder.endRecording();
final img = await picture.toImage(500,200);
final pngBytes = await img.toByteData(format: ImageByteFormat.png);
if (pictureAppDirectory == null) await initPhotoDirectory(context);
String drawPath = '${pictureAppDirectory!.path}/Drawing';
await Directory(drawPath).create(recursive: true);
File('$drawPath/test.png').writeAsBytesSync(pngBytes!.buffer.asInt8List());
OpenFile.open('$drawPath/test.png');
}
Future<ui.Image> getUiImage(String imageAssetPath) async {
Uint8List bytes = File(imageAssetPath).readAsBytesSync();
final ByteData assetImageByteData = ByteData.view(bytes.buffer);
image.Image? baseSizeImage = image.decodeImage(assetImageByteData.buffer.asUint8List());
ui.Codec codec = await ui.instantiateImageCodec(baseSizeImage!.getBytes());
ui.FrameInfo frameInfo = await codec.getNextFrame();
return frameInfo.image;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children:[
Image.file(File(widget.photoPath)),
Container(
child: new GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox? renderBox = context.findRenderObject() as RenderBox;
Offset _localPosition = renderBox.globalToLocal(details.globalPosition);
_points = new List.from(_points)..add(_localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: new CustomPaint(
painter: new SignaturePainter(points: _points),
size: Size.infinite,
),
),
),
]),
),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter({required this.points});
List<Offset?>? points;
#override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()
..color = Colors.blue
..strokeCap = StrokeCap.round
..strokeWidth = 10.0;
for (int i = 0; i < points!.length - 1; i++) {
if (points![i] != null && points![i + 1] != null) {
canvas.drawLine(points![i]!, points![i + 1]!, paint);
}
}
}
#override
bool shouldRepaint(SignaturePainter oldDelegate) => oldDelegate.points != points;
}
One option you have is use screenshot, and take screenshot from Stack widget.like this:
First wrap your Stack widget with Screenshot:
Screenshot(
controller: screenshotController,
child: Stack(
children:[...],
),
),
when you need your image do this:
screenshotController.capture().then((Uint8List image) {
//your image
}).catchError((onError) {
print(onError);
});
Note that this way may affect you image quality.
I use the code below to draw part of the image. Every time I change the image. The widget turns white for a few milliseconds. Any idea how to make the widget transparent while the canvas is being drawn?
//----------------------------------------------------------------------------------
PartImagePainter(
originalImageWidth: imageWidth, originalImageHeight: imageHeight,
imageData: imageData,
rect: imageRects[index]
)
//----------------------------------------------------------------------------------
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class PartImagePainter extends StatefulWidget {
Uint8List imageData;
int originalImageWidth;
int originalImageHeight;
Rect rect;
PartImagePainter({required this.imageData, required this.originalImageWidth, required this.originalImageHeight, required this.rect});
#override
_PartImagePainterState createState() => _PartImagePainterState();
}
class _PartImagePainterState extends State<PartImagePainter> {
Future<ui.Image> getImage(Uint8List imageData) async {
final codec = await ui.instantiateImageCodec(
imageData,
targetWidth: widget.originalImageWidth,
targetHeight: widget.originalImageHeight,
);
final image = (await codec.getNextFrame()).image;
return image;
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getImage(widget.imageData),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return paintImage(snapshot.data);
} else {
// Otherwise, display a loading indicator.
return SizedBox(child: CircularProgressIndicator());
}
});
}
paintImage(image) {
return CustomPaint(
painter: ImagePainter(image, widget.rect),
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: widget.rect.height,
),
);
}
}
class ImagePainter extends CustomPainter {
ui.Image resImage;
Rect rectCrop;
ImagePainter(this.resImage, this.rectCrop);
#override
void paint(Canvas canvas, Size size) {
if (resImage == null) {
return;
}
final Rect rect = Offset.zero & size;
final Size imageSize =
Size(resImage.width.toDouble(), resImage.height.toDouble());
FittedSizes sizes = applyBoxFit(BoxFit.fitWidth, imageSize, size);
Rect inputSubRect = rectCrop;
final Rect outputSubRect =
Alignment.center.inscribe(sizes.destination, rect);
canvas.drawImageRect(resImage, inputSubRect, outputSubRect, Paint());
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
You can try use a Stack with a Positioned.fill to make the background transparent. Example:
Stack(
children: [
Positioned.fill(
child: Container(
color: Colors.transparent,
),
),
PartImagePainter(
originalImageWidth: imageWidth,
originalImageHeight: imageHeight,
imageData: imageData,
rect: imageRects[index],
),
],
)
You can also use a Container with a transparent color. Example:
Container(
color: Colors.transparent,
child: PartImagePainter(
originalImageWidth: imageWidth,
originalImageHeight: imageHeight,
imageData: imageData,
rect: imageRects[index],
),
),
i want to add heart beat horizontal line for splash screen, it will start animating from one side and go all the way to other if the initial data is still not loaded it will continue animation
i tried below code but its only pulse kinda animation
import 'dart:math';
import 'package:flutter/material.dart';
class SpritePainter extends CustomPainter {
final Animation<double> _animation;
SpritePainter(this._animation) : super(repaint: _animation);
void circle(Canvas canvas, Rect rect, double value) {
double opacity = (1.0 - (value / 4.0)).clamp(0.0, 1.0);
Color color = Color.fromRGBO(0, 117, 194, opacity);
double size = rect.width / 2;
double area = size * size;
double radius = sqrt(area * value / 4);
final Paint paint = Paint()..color = color;
canvas.drawCircle(rect.center, radius, paint);
}
#override
void paint(Canvas canvas, Size size) {
Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
for (int wave = 3; wave >= 0; wave--) {
circle(canvas, rect, wave + _animation.value);
}
}
#override
bool shouldRepaint(SpritePainter oldDelegate) {
return true;
}
}
class SpriteDemo extends StatefulWidget {
#override
SpriteDemoState createState() => SpriteDemoState();
}
class SpriteDemoState extends State<SpriteDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
);
//_startAnimation();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _startAnimation() {
_controller
..stop()
..reset()
..repeat(period: const Duration(seconds: 1));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pulse')),
body: CustomPaint(
painter: SpritePainter(_controller),
child: SizedBox(
width: 200.0,
height: 200.0,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _startAnimation,
child: new Icon(Icons.play_arrow),
),
);
}
}
void main() {
runApp(
MaterialApp(
home: SpriteDemo(),
),
);
}
I want it to start(animate) from center left and go all the way to right just like in image
I am trying to add multiple Rectangles in the Canvas and rotate them with user pan action. But the Constructor I found till now for Rect is all to draw them without Rotation. and I found a method canvas.rotate() which will rotate the whole canvas.
How to achieve this? Any code where rotation of the Rectangle is dealt with user pan action without using canvas.rotate() will be helpful.
The solution is simple as #pskink answered in the comment above.
There is only canvas.rotate() and canvas.transform() to rotate anything in the flutter canvas and there is canvas.scale() to scale them.
now if you want to rotate one object 120, and another 40 degrees you need to draw them inside a canvas.save() ... canvas.restore() block. then your objects will be rotated at a different angles. look at the below code for example:
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:ui' as ui;
const kCanvasSize = 300.0;
class ImageInsideRectPage extends StatefulWidget {
const ImageInsideRectPage({Key? key}) : super(key: key);
#override
_ImageInsideRectPageState createState() => _ImageInsideRectPageState();
}
class _ImageInsideRectPageState extends State<ImageInsideRectPage> {
ui.Image? image;
#override
void initState() {
_load('assets/img.png');
super.initState();
}
void _load(String path) async {
var bytes = await rootBundle.load(path);
image = await decodeImageFromList(bytes.buffer.asUint8List());
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.greenAccent, width: 2)),
height: kCanvasSize,
width: kCanvasSize,
child: CustomPaint(
painter: ImageInsideRectangle(context: context, image: image),
child: SizedBox.expand(),
),
),
),
);
}
}
class ImageInsideRectangle extends CustomPainter {
ImageInsideRectangle({required this.context, required this.image});
ui.Image? image;
final BuildContext context;
#override
void paint(Canvas canvas, Size size) async {
canvas.clipRRect(ui.RRect.fromRectXY(
Rect.fromPoints(Offset(0, 0), Offset(kCanvasSize - 4, kCanvasSize - 4)),
0,
0,
));
Paint greenBrush = Paint()..color = Colors.greenAccent;
if (image != null) {
canvas.save();
rotate(
canvas: canvas,
cx: image!.width.toDouble() / 2,
cy: image!.height.toDouble() / 2,
angle: -0.3);
canvas.scale(kCanvasSize / image!.height);
canvas.drawImage(image!, Offset(0, 0), greenBrush);
canvas.restore();
}
canvas.save();
rotate(canvas: canvas, cx: 200 + 50, cy: 100 + 50, angle: 0.5);
canvas.drawRect(Rect.fromLTWH(200, 100, 100, 100), greenBrush);
canvas.restore();
}
void rotate(
{required Canvas canvas,
required double cx,
required double cy,
required double angle}) {
canvas.translate(cx, cy);
canvas.rotate(angle);
canvas.translate(-cx, -cy);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Future<ui.Image> loadUiImage(String imageAssetPath) async {
final ByteData data = await rootBundle.load(imageAssetPath);
final Completer<ui.Image> completer = Completer();
ui.decodeImageFromList(Uint8List.view(data.buffer), (ui.Image img) {
return completer.complete(img);
});
return completer.future;
}
This way you can rotate multiple objects in multiple directions. also, there is an example of loading an image from local asset and rotating it around its own center.