I want to make a sparkling animation in flutter
How to make this in flutter??
I would suggest a custom paint approche. My awswer is highly customisable. I only change the innerOuterRadiusRatio and the velocity. You can change the color or Opacity, the number of edges of the star, the rotation(angleOffsetToCenterStar),and the beamLength.
import 'package:flutter/material.dart';
import 'dart:math' as math;
class Sparkling extends StatefulWidget {
const Sparkling({Key? key}) : super(key: key);
#override
_SparklingState createState() => _SparklingState();
}
class _SparklingState extends State<Sparkling>
with SingleTickerProviderStateMixin {
late AnimationController animationController;
late Animation animation;
late List<MyStar> myStars;
#override
void initState() {
super.initState();
myStars = <MyStar>[];
animationController = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 250,
));
animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
for (final star in myStars) {
star.isEnable = math.Random().nextBool();
}
animationController.forward();
}
});
animation = Tween<double>(begin: 0, end: 8).animate(CurvedAnimation(
parent: animationController, curve: Curves.easeInOutSine));
animation.addListener(() {
setState(() {});
});
animationController.forward();
}
void postFrameCallback(_) {
if (!mounted) {
return;
}
final size = MediaQuery.of(context).size;
if (myStars.isEmpty) {
myStars = List.generate(60, (index) {
double velocityX = 2 * (math.Random().nextDouble());//max velocity 2
double velocityY = 2 * (math.Random().nextDouble());
velocityX = math.Random().nextBool() ? velocityX : -velocityX;
velocityY = math.Random().nextBool() ? velocityY : -velocityY;
return MyStar(
isEnable: math.Random().nextBool(),
innerCirclePoints: 4,
beamLength: math.Random().nextDouble() * (8 - 2) + 2,
innerOuterRadiusRatio: 0.0,
angleOffsetToCenterStar: 0,
center: Offset(size.width * (math.Random().nextDouble()),
size.height * (math.Random().nextDouble())),
velocity: Offset(velocityX, velocityY),
color: Colors.white);
});
} else {
for (final star in myStars) {
star.center = star.center + star.velocity;
if (star.isEnable) {
star.innerOuterRadiusRatio = animation.value;
if (star.center.dx >= size.width) {
if (star.velocity.dy > 0) {
star.velocity = const Offset(-1, 1);
} else {
star.velocity = const Offset(-1, -1);
}
star.center = Offset(size.width, star.center.dy);
} else if (star.center.dx <= 0) {
if (star.velocity.dy > 0) {
star.velocity = const Offset(1, 1);
} else {
star.velocity = const Offset(1, -1);
}
star.center = Offset(0, star.center.dy);
} else if (star.center.dy >= size.height) {
if (star.velocity.dx > 0) {
star.velocity = const Offset(1, -1);
} else {
star.velocity = const Offset(-1, -1);
}
star.center = Offset(star.center.dx, size.height);
} else if (star.center.dy <= 0) {
if (star.velocity.dx > 0) {
star.velocity = const Offset(1, 1);
} else {
star.velocity = const Offset(-1, 1);
}
star.center = Offset(star.center.dx, 0);
}
}
}
}
}
#override
Widget build(BuildContext context) {
WidgetsBinding.instance!.addPostFrameCallback(postFrameCallback);
return CustomPaint(
size: MediaQuery.of(context).size,
painter: StarPainter(
myStars: myStars,
));
}
}
The CustomPainter
class StarPainter extends CustomPainter {
List<MyStar> myStars;
StarPainter({required this.myStars});
List<Map> calcStarPoints(
{required double centerX,
required double centerY,
required int innerCirclePoints,
required double innerRadius,
required double outerRadius,
required double angleOffsetToCenterStar}) {
final angle = ((math.pi) / innerCirclePoints);
final totalPoints = innerCirclePoints * 2; // 10 in a 5-points star
List<Map> points = [];
for (int i = 0; i < totalPoints; i++) {
bool isEvenIndex = i % 2 == 0;
final r = isEvenIndex ? outerRadius : innerRadius;
var currY = centerY + math.cos(i * angle + angleOffsetToCenterStar) * r;
var currX = centerX + math.sin(i * angle + angleOffsetToCenterStar) * r;
points.add({'x': currX, 'y': currY});
}
return points;
}
#override
void paint(Canvas canvas, Size size) {
for (final myStar in myStars) {
final innerRadius = myStar.beamLength / myStar.innerCirclePoints;
final outerRadius = innerRadius * myStar.innerOuterRadiusRatio;
List<Map> points = calcStarPoints(
centerX: myStar.center.dx,
centerY: myStar.center.dy,
innerCirclePoints: myStar.innerCirclePoints,
innerRadius: innerRadius,
outerRadius: outerRadius,
angleOffsetToCenterStar: myStar.angleOffsetToCenterStar);
var star = Path()..moveTo(points[0]['x'], points[0]['y']);
for (var point in points) {
star.lineTo(point['x'], point['y']);
}
canvas.drawPath(
star,
Paint()..color = myStar.color,
);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
And the class MyStar.
class MyStar {
bool isEnable;
int innerCirclePoints; //how many edges you need?
double beamLength;
double
innerOuterRadiusRatio; // outter circle is x2 the inner // set star sharpness/chubbiness
double angleOffsetToCenterStar;
Offset center;
Offset velocity;
Color color;
MyStar(
{required this.isEnable,
required this.innerCirclePoints,
required this.beamLength,
required this.innerOuterRadiusRatio,
required this.angleOffsetToCenterStar,
required this.center,
required this.velocity,
required this.color});
}
I would suggest using Lottie animations.
If you make a quick search you can find the one that matches your needs:
https://lottiefiles.com/search?q=star&category=animations
If you found the right now click on it and press download -> lottie.json and then install this package in flutter:
https://pub.dev/packages/lottie
Then you simply add the downloaded json animation in your asset folder and reference it like this:
Lottie.asset(
'assets/LottieLogo1.json',
width: 200,
height: 200,
fit: BoxFit.fill,
animate: true,
repeat: true
),
With this you have an beautiful repeating animation.
You can also use an controller to adapt everything even more.
Basically you can also make an animation in after effect and export it as an json animation with the bodymovin plugin
Use https://rive.app/ for creating animation for flutter applications. You can find many tutorials of creating animation in rive.app and integrating in flutter app.
Related
When using a FixedResolutionViewport the tap event on a PositionComponent is relative to the screen size and not to the resized viewport as expected.
If I remove FixedResolutionViewport the tap is registered correctly.
Am I missing something? What can I do to tap correctly on my Component (Circle)?
This is my code.
I'm using flame 1.1.0.
Vector2 calculateVector(
double x, double y, double fieldW, double fieldH, Vector2 size) {
var calcY = (y / fieldH) * size.y;
var calcX = (y / fieldW) * size.x;
return Vector2(calcX, calcY);
}
class TestGame extends FlameGame with HasTappableComponents, ScaleDetector {
TestGame();
#override
bool debugMode = true;
late double startZoom;
static double fieldW = 1080;
static double fieldH = 1717;
#override
backgroundColor() => const Color.fromARGB(255, 65, 129, 77);
#override
Future<void> onLoad() async {
double maxSide = max(size.x, size.y);
var side = maxSide * (fieldW / fieldH);
camera.viewport =
FixedResolutionViewport(Vector2(side, maxSide), clip: false);
final fieldSprite = await loadSprite('field_checkered.png');
final field = SpriteComponent(
sprite: fieldSprite,
size: Vector2(side, maxSide),
);
var ply = Circle(
position: calculateVector(500, 500, fieldH, fieldH, size),
size: Vector2(50, 50));
await add(field);
await add(ply);
}
}
class Circle extends PositionComponent with TapCallbacks, HasGameRef<TestGame> {
var isSelected = false;
var radius = 0.0;
var paint = Paint()..color = const Color(0xFF80C080);
Circle({required position, required size}) {
super.position = Vector2(
position.x,
position.y,
);
super.size = size;
super.positionType = PositionType.viewport;
radius = size.x / 2;
}
#override
void render(Canvas canvas) {
gameRef.camera.viewport.apply(canvas);
canvas.drawCircle(Offset(radius, radius), radius, paint);
super.render(canvas);
}
#override
void onTapDown(TapDownEvent event) {
isSelected = !isSelected;
if (isSelected) {
paint = Paint()..color = Color.fromARGB(255, 75, 0, 76);
priority = 2;
} else {
paint = Paint()..color = Color.fromARGB(255, 253, 147, 255);
priority = 1;
}
}
}
You should be able to use the Tappable and HasTappables mixins instead. The HasTappableComponents mixin is still in the experimental package and is to be used with the new CameraComponent and not the viewport+camera on the FlameGame.
I don't want use any library if you can write code for my question Please help.
Thank you.
I found a solution :
class DashedLineVerticalPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
double dashHeight = 5, dashSpace = 3, startY = 0;
final paint = Paint()
..color = Colors.black
..strokeWidth = 1;
while (startY < size.height) {
canvas.drawLine(Offset(0, startY), Offset(0, startY + dashHeight), paint);
startY += dashHeight + dashSpace;
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
usage:
CustomPaint(
painter: DashedLineVerticalPainter(),
size: Size(1, double.infinity)))
I have been having trouble trying to implement a method that moves a Rect in flutter using the flame game engine. The ultimate goal is to swipe in a direction and have the Rect move in that direction at a constant velocity. I found this code:
void dragUpdate(DragUpdateDetails d)
{
final delta = d.delta;
final size = gameController.screenSize;
double translateX = delta.dx;
double translateY = delta.dy;
// Make sure that the player never goes outside of the screen in the X-axis
if (playerRect.right + delta.dx >= size.width) {
translateX = size.width - playerRect.right;
} else if (playerRect.left + delta.dx <= 0) {
translateX = -playerRect.left;
}
// Make sure that the player never goes outside of the screen in the Y-axis
if (playerRect.bottom + delta.dy >= size.height) {
translateY = size.height - playerRect.bottom;
} else if (playerRect.top + delta.dy <= 0) {
translateY = -playerRect.top;
}
playerRect = playerRect.translate(translateX, translateY);
}
which at least allows for the free movement of the Rect on screen in regards to finger position. I tried messing with the "translate()" method to have the x/y increment/decrement depending on the delta provide, but to no avail. Any hint or point in the right direction would be greatly appreciate.
Instead of using both the HorizontalDragDetector and VerticalDragDetector, you can use the MultiTouchDragDetector and the code will be a lot simpler.
class MyGame extends BaseGame with MultiTouchDragDetector {
Rect _rect = const Rect.fromLTWH(0, 0, 50, 50);
Offset _velocity = Offset.zero;
MyGame();
#override
void onReceiveDrag(DragEvent event) {
event.onEnd = onDragEnd;
}
void onDragEnd(DragEndDetails details) {
_velocity = details.velocity.pixelsPerSecond;
}
#override
void update(double dt) {
super.update(dt);
final timeStepMovement = _velocity * dt;
_rect = _rect.shift(timeStepMovement);
}
#override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRect(_rect, BasicPalette.white.paint);
}
}
You can set the drag delta X and Y in your dragUpdate(...){ ... } and use those delta values in render(...) { ... }.
Try running this sample app.
import 'dart:ui';
import 'package:flame/game.dart';
import 'package:flame/gestures.dart';
import 'package:flame/util.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
BoxGame game = BoxGame();
runApp(game.widget);
Util flameUtil = Util();
flameUtil.fullScreen();
flameUtil.setOrientation(DeviceOrientation.portraitUp);
}
class BoxGame extends Game
with HorizontalDragDetector, VerticalDragDetector {
Size screenSize;
double posX;
double posY;
double dragDX = 0.0;
double dragDY = 0.0;
void render(Canvas canvas) {
// draw a black background on the whole screen
final Rect bgRect =
Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
final Paint bgPaint = Paint();
bgPaint.color = Color(0xff000000);
canvas.drawRect(bgRect, bgPaint);
// draw a box (make it green if won, white otherwise)
if (posX == null) {
final double screenCenterX = screenSize.width / 2;
final double screenCenterY = screenSize.height / 2;
posX = screenCenterX - 75;
posY = screenCenterY - 75;
}
posX += dragDX;
posY += dragDY;
final Rect boxRect = Rect.fromLTWH(posX, posY, 150, 150);
final Paint boxPaint = Paint();
boxPaint.color = Color(0xffffffff);
canvas.drawRect(boxRect, boxPaint);
}
void update(double t) {}
void resize(Size size) {
screenSize = size;
super.resize(size);
}
#override
void onHorizontalDragUpdate(DragUpdateDetails details) {
dragDX = details.delta.dx;
}
#override
void onVerticalDragUpdate(DragUpdateDetails details) {
dragDY = details.delta.dy;
}
#override
void onHorizontalDragEnd(DragEndDetails details) {
dragDX = 0;
}
#override
void onVerticalDragEnd(DragEndDetails details) {
dragDY = 0;
}
}
when I used aync await method at that time it works properly but when i try to load image in flame's component class I got error:
I have created a Background class that extends flame engine's component class. Now I am tring to load a base64 image using the then function, but I get an error but when I use the async await method for image loading it works properly.
class Background extends Component with Resizable {
static final Paint _paint = Paint();
Size imageSize = Size(411.42857142857144, 822.8571428571429);
#override
void render(Canvas c) {
Rect myRect = const Offset(0.0, 0.0) & Size(size.width, size.height);
Flame.images.fromBase64('demo', imageBase).then((value) {
paintImage(canvas: c, rect: myRect, image: value);
});
}
#override
void update(double t) {
// TODO: implement update
}
void paintImage({
#required Canvas canvas,
#required Rect rect,
#required image.Image image,
String debugImageLabel,
double scale = 1.0,
ColorFilter colorFilter,
BoxFit fit,
Alignment alignment = Alignment.center,
Rect centerSlice,
ImageRepeat repeat = ImageRepeat.noRepeat,
bool flipHorizontally = false,
bool invertColors = false,
FilterQuality filterQuality = FilterQuality.low,
bool isAntiAlias = false,
}) {
if (rect.isEmpty) return;
Size outputSize = rect.size;
Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
Offset sliceBorder;
if (centerSlice != null) {
sliceBorder = Offset(
centerSlice.left + inputSize.width - centerSlice.right,
centerSlice.top + inputSize.height - centerSlice.bottom,
);
outputSize = outputSize - sliceBorder as Size;
inputSize = inputSize - sliceBorder as Size;
}
fit ??= centerSlice == null ? BoxFit.scaleDown : BoxFit.fill;
assert(centerSlice == null || (fit != BoxFit.none && fit != BoxFit.cover));
final FittedSizes fittedSizes =
applyBoxFit(fit, inputSize / scale, outputSize);
final Size sourceSize = fittedSizes.source * scale;
Size destinationSize = fittedSizes.destination;
if (centerSlice != null) {
outputSize += sliceBorder;
destinationSize += sliceBorder;
}
// Output size is fully calculated.
if (repeat != ImageRepeat.noRepeat && destinationSize == outputSize) {
repeat = ImageRepeat.noRepeat;
}
final Paint paint = Paint()..isAntiAlias = isAntiAlias;
if (colorFilter != null) paint.colorFilter = colorFilter;
if (sourceSize != destinationSize) {
paint.filterQuality = filterQuality;
}
paint.invertColors = invertColors;
final double halfWidthDelta =
(outputSize.width - destinationSize.width) / 2.0;
final double halfHeightDelta =
(outputSize.height - destinationSize.height) / 2.0;
final double dx = halfWidthDelta +
(flipHorizontally ? -alignment.x : alignment.x) * halfWidthDelta;
final double dy = halfHeightDelta + alignment.y * halfHeightDelta;
final Offset destinationPosition = rect.topLeft.translate(dx, dy);
final Rect destinationRect = destinationPosition & destinationSize;
final bool needSave = repeat != ImageRepeat.noRepeat || flipHorizontally;
if (needSave) canvas.save();
if (repeat != ImageRepeat.noRepeat) canvas.clipRect(rect);
if (flipHorizontally) {
final double dx = -(rect.left + rect.width / 2.0);
canvas.translate(-dx, 0.0);
canvas.scale(-1.0, 1.0);
canvas.translate(dx, 0.0);
}
if (centerSlice == null) {
final Rect sourceRect = alignment.inscribe(
sourceSize,
Offset.zero & inputSize,
);
if (repeat == ImageRepeat.noRepeat) {
canvas.drawImageRect(image, sourceRect, destinationRect, paint);
} else {
print("no repet else");
}
}
//if (needSave) canvas.restore();
}
}
This is totally not acceptable:
#override
void render(Canvas c) {
Rect myRect = const Offset(0.0, 0.0) & Size(size.width, size.height);
Flame.images.fromBase64('demo', imageBase).then((value) {
paintImage(canvas: c, rect: myRect, image: value);
});
}
The render method must be sync. It takes a canvas object to be rendered right now. You cannot make async operations on here! Of course the canvas will be disposed, it only lives for one frame. The render method is called every frame and must be quick and short lived. When the image actually loads, you no longer will have access to canvas because the whole render cycle will be done. It was already rendered on the screen! You cannot change the past! That doesn't make sense.
What you need to do is to load the image elsewhere and render conditionally if it's loaded. Move the loading to your constructor:
Flame.images.fromBase64('demo', imageBase).then((value) {
this.image = value;
});
And then on the render method, render conditionally:
#override
void render(Canvas c) {
Rect myRect = const Offset(0.0, 0.0) & Size(size.width, size.height);
if (this.image != null) {
paintImage(canvas: c, rect: myRect, image: this.image);
}
}
By creating an image field on your component. Also consider using SpriteComponent instead that does all that for you in the correct way. And never make the render or update methods async ;)
I needed a loading widget that draws the moving sine and cosine functions into a canvas. I coded it with no problem using a CustomPaint widget and a CustomPainter, but when I profile it, i Have discovered it runs on about 49fps, and not on 60fps. The UI thread is working good, taking about 6ms for each frame, but the Raster thread is taking longer. I have tried painting less points on the canvas (doing i=i+5 instead of i++ on the for loop), but the result is quite the same.
¿Can somebody suggest me an idea on how could I improve the performance?. The widget code is below, and so is the DevTools screenshot of what the Raster thread is doing in every frame, in case it can be useful.
import 'dart:math';
import 'package:flutter/material.dart';
class LoadingChart extends StatefulWidget{
final Color color1;
final Color color2;
final double lineWidth;
final bool line;
final Size size;
const LoadingChart({
#required this.color1,
#required this.color2,
#required this.size,
#required this.lineWidth,
this.line = true,
Key key
}): super(key: key);
#override
State<StatefulWidget> createState() => _LoadingChartState();
}
class _LoadingChartState extends State<LoadingChart>
with SingleTickerProviderStateMixin{
AnimationController _controller;
double randomHeight(Random random, double max){
return random.nextDouble()*max;
}
#override
void initState() {
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
_controller.addListener(() {setState(() {});});
_controller.repeat();
super.initState();
}
#override
void dispose(){
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizedBox(
height: widget.size.height,
width: widget.size.width,
child: CustomPaint(
painter: PathPainter(
color1: widget.color1,
color2: widget.color2,
value: _controller.value,
line: widget.line,
),
)
);
}
}
class PathPainter extends CustomPainter {
final Color color1;
final Color color2;
final double lineWidth;
final bool line;
final double value;
PathPainter({
#required this.value,
this.color1=Colors.red,
this.color2=Colors.green,
this.line = true,
this.lineWidth=4.0,
}): super();
#override
void paint(Canvas canvas, Size size) {
final height = size.height;
final width = size.width;
Paint paint1 = Paint()
..color = color1
..style = PaintingStyle.stroke
..strokeWidth = lineWidth;
Paint paint2 = Paint()
..color = color2
..style = PaintingStyle.stroke
..strokeWidth = lineWidth;
Path path1 = Path();
Path path2 = Path();
/* If line is true, draw sin and cos functions, otherwise, just some points */
for (double i = 0; i < width; i=i+5){
double f = i*2*pi/width + 2*pi*value;
double g = i*2*pi/width - 2*pi*value;
if (i == 0){
path1.moveTo(0, height/2 + height/6*sin(f));
path2.moveTo(0, height/2 + height/6*cos(g));
continue;
}
path1.lineTo(i, height/2 + height/6*sin(f));
path2.lineTo(i, height/2 + height/6*cos(g));
}
/* Draw both lines */
canvas.drawPath(path1, paint1);
canvas.drawPath(path2, paint2);
}
#override
bool shouldRepaint(PathPainter oldDelegate) {
return oldDelegate.value != value || oldDelegate.color1 != color1
|| oldDelegate.color2 != color2 || oldDelegate.line != line
|| oldDelegate.lineWidth != lineWidth;
}
}
PS: I'm running the app on profile mode so that shouldn't be the problem. Also I wanted to mention that it's the only widget being redrawn on the screen.
Thanks a lot!!
CustomPainter can receive a listenable so maybe you can use the animation controller there to update it with every tick
class _LoadingChartState extends State<LoadingChart>
with SingleTickerProviderStateMixin{
AnimationController _controller;
double randomHeight(Random random, double max){
return random.nextDouble()*max;
}
#override
void initState() {
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
//_controller.addListener(() {setState(() {});}); no need to setState
_controller.repeat();
super.initState();
}
#override
void dispose(){
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizedBox(
height: widget.size.height,
width: widget.size.width,
child: CustomPaint(
willChange: true, //this can help (Whether the raster cache should be told that this painting is likely)
painter: PathPainter(
color1: widget.color1,
color2: widget.color2,
line: widget.line,
listenable: _controller //pass the controller as it is (An animationController extends a Listenable)
),
)
);
}
}
And in PathPainter you give to the constructor the listenable and pass it to the CustomPainter constructor that accepts a listenable called repaint
class PathPainter extends CustomPainter {
final Animation listenable;
final Color color1;
final Color color2;
final double lineWidth;
final bool line;
PathPainter({
this.listenable,
this.color1=Colors.red,
this.color2=Colors.green,
this.line = true,
this.lineWidth=4.0,
}): super(repaint: listenable); //don't forget calling the CustomPainter constructor with super
#override
void paint(Canvas canvas, Size size) {
double value = listenable.value; // get its value here
final height = size.height;
final width = size.width;
Paint paint1 = Paint()
..color = color1
..style = PaintingStyle.stroke
..strokeWidth = lineWidth;
Paint paint2 = Paint()
..color = color2
..style = PaintingStyle.stroke
..strokeWidth = lineWidth;
Path path1 = Path();
Path path2 = Path();
/* If line is true, draw sin and cos functions, otherwise, just some points */
for (double i = 0; i < width; i=i+5){
double f = i*2*pi/width + 2*pi*value;
double g = i*2*pi/width - 2*pi*value;
if (i == 0){
path1.moveTo(0, height/2 + height/6*sin(f));
path2.moveTo(0, height/2 + height/6*cos(g));
continue;
}
path1.lineTo(i, height/2 + height/6*sin(f));
path2.lineTo(i, height/2 + height/6*cos(g));
}
/* Draw both lines */
canvas.drawPath(path1, paint1);
canvas.drawPath(path2, paint2);
}
#override
bool shouldRepaint(PathPainter oldDelegate) {
//delete the oldDelegate.value, it doesn't exists anymore
return oldDelegate.color1 != color1
|| oldDelegate.color2 != color2 || oldDelegate.line != line
|| oldDelegate.lineWidth != lineWidth;
}
}
I'm in debug mode so I expect you get a better performance in profile mode