Animation in Flutter using custompainter

animation in flutter with ctsompainter
animation in flutter with ctsompainter

This Flutter animation beautifully simulates a peaceful night sky with twinkling stars, a floating moon, hills, and a coconut tree. It leverages CustomPainter for pixel-level control and AnimationController for smooth transitions.

we are creating below animation as Example

🔷 1. main() and App Initialization

dartCopy codevoid main() {
  runApp(const StarryNightApp());
}
  • This is the entry point of the app.
  • StarryNightApp is a StatelessWidget that sets up and displays the main animation scene (StarryNightScene).

🔶 2. StarryNightScene (StatefulWidget)

This widget controls the animation.

✅ AnimationController

dartCopy code_controller = AnimationController(
  duration: const Duration(seconds: 60),
  vsync: this,
)..repeat();
  • Runs the animation on a loop, taking 60 seconds to complete one cycle.
  • vsync: this improves performance by syncing the animation with the screen refresh.

🌕 Moon Movement

dartCopy code_moonPosition = (_controller.value * 2 * math.pi) % (2 * math.pi);
  • Converts the controller’s 0–1 value into an angle in radians (0 → 2π).
  • This creates circular motion, used later to position the moon along a wave-like arc.

🖼️ 3. StarryNightPainter (CustomPainter)

This is where all visual elements are drawn directly to the canvas.

🟦 Sky Gradient

dartCopy codefinal skyGradient = LinearGradient(
  begin: Alignment.topCenter,
  end: Alignment.bottomCenter,
  colors: [Color(0xFF0a1a40), Color(0xFF1a2a50)],
);
  • Creates a dark blue gradient from top to bottom to represent the night sky.

✨ Stars with Twinkle

Each star has:

  • random position
  • unique twinkle speed and offset
  • twinkling effect using a sine wave:
dartCopy codefinal twinkleFactor = 0.5 + 0.5 * math.sin(...);
  • This oscillates the star brightness from 0.5 to 1.0, making it appear as if stars are twinkling.

🌕 Moon Animation

dartCopy codefinal moonX = 400 + 100 * math.sin(moonPosition);
final moonY = 600 - moonPosition * 100;
  • moonX: Moves the moon side-to-side using sine wave.
  • moonY: Moves the moon upward in a linear arc.
  • A glowing effect is added using blur + circle + craters.

🌄 Ground (Rolling Hills)

  • A smooth hill is drawn using Bezier curves to create natural-looking terrain.
dartCopy codefinal path = Path();
path.moveTo(0, height);
path.cubicTo(...);

🌴 Coconut Tree

  • Positioned at 75% of the screen width.
  • The trunk is curved using paths for a natural lean.
  • The leaves are drawn with rotated Bezier curves using canvas.transform(...).
  • Coconuts are small brown circles grouped at the top.

🔁 Repainting Logic

dartCopy code@override
bool shouldRepaint(covariant StarryNightPainter oldDelegate) {
  return oldDelegate.moonPosition != moonPosition;
}
  • This tells Flutter to repaint only when the moon moves, optimizing performance.

🌟 Star Class

Each Star object stores:

dartCopy codeclass Star {
  final double x, y, size, twinkleSpeed, twinkleOffset;
}
  • Stars are generated with randomized properties, so the sky looks natural and varied.

Full Source Code

import 'dart:math' as math;

import 'package:flutter/material.dart';

void main() {
runApp(const StarryNightApp());
}

class StarryNightApp extends StatelessWidget {
const StarryNightApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Starry Night',
theme: ThemeData(primarySwatch: Colors.blue),
home: const StarryNightScene(),
);
}
}

class StarryNightScene extends StatefulWidget {
const StarryNightScene({super.key});

@override
State createState() => _StarryNightSceneState();
}

class _StarryNightSceneState extends State
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final List _stars = [];
double _moonPosition = 0.0;

@override
void initState() {
super.initState();

// Create stars with random properties
for (int i = 0; i < 25; i++) {
  _stars.add(
    Star(
      x: math.Random().nextDouble() * 800,
      y: math.Random().nextDouble() * 450,
      size: 1.5 + math.Random().nextDouble() * 1.5,
      twinkleSpeed: 2 + math.Random().nextDouble() * 3,
      twinkleOffset: math.Random().nextDouble() * 2 * math.pi,
    ),
  );
}

_controller = AnimationController(
  duration: const Duration(seconds: 60),
  vsync: this,
)..repeat();

}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
// Update moon position
_moonPosition = (_controller.value * 2 * math.pi) % (2 * math.pi);

      return CustomPaint(
        size: Size.infinite,
        painter: StarryNightPainter(
          stars: _stars,
          moonPosition: _moonPosition,
        ),
      );
    },
  ),
);

}
}

class StarryNightPainter extends CustomPainter {
final List stars;
final double moonPosition;

StarryNightPainter({required this.stars, required this.moonPosition});

@override
void paint(Canvas canvas, Size size) {
final width = size.width;
final height = size.height;

// Background gradient
final skyGradient = LinearGradient(
  begin: Alignment.topCenter,
  end: Alignment.bottomCenter,
  colors: [const Color(0xFF0a1a40), const Color(0xFF1a2a50)],
).createShader(Rect.fromLTWH(0, 0, width, height));

final backgroundPaint = Paint()..shader = skyGradient;
canvas.drawRect(Rect.fromLTWH(0, 0, width, height), backgroundPaint);

// Draw stars
for (final star in stars) {
  final twinkleFactor =
      0.5 +
      0.5 * math.sin(moonPosition * star.twinkleSpeed + star.twinkleOffset);
  final starSize = star.size * (0.8 + 0.4 * twinkleFactor);
  final starOpacity = 0.4 + 0.6 * twinkleFactor;

  final starPaint =
      Paint()
        ..color = Colors.white.withOpacity(starOpacity)
        ..style = PaintingStyle.fill;

  canvas.drawCircle(
    Offset(star.x * width / 800, star.y * height / 600),
    starSize,
    starPaint,
  );
}

// Calculate moon position along path
final moonX =
    400 + 100 * math.sin(moonPosition); // Optional horizontal motion
final moonY =
    600 - moonPosition * 100; // Moves up as moonPosition increases

// Draw moon glow
final moonGlowPaint =
    Paint()
      ..color = Colors.white.withOpacity(
        0.1 + 0.1 * math.sin(moonPosition * 0.2),
      )
      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10);
canvas.drawCircle(
  Offset(moonX * width / 800, moonY * height / 600),
  50,
  moonGlowPaint,
);

// Draw moon
final moonPaint =
    Paint()
      ..color = Colors.white.withOpacity(0.9)
      ..style = PaintingStyle.fill;
canvas.drawCircle(
  Offset(moonX * width / 800, moonY * height / 600),
  40,
  moonPaint,
);

// Draw moon craters
final craterPaint =
    Paint()
      ..color = Colors.grey.withOpacity(0.7)
      ..style = PaintingStyle.fill;

canvas.drawCircle(
  Offset((moonX - 10) * width / 800, (moonY - 10) * height / 600),
  8,
  craterPaint,
);
canvas.drawCircle(
  Offset((moonX + 20) * width / 800, (moonY + 10) * height / 600),
  6,
  craterPaint,
);
canvas.drawCircle(
  Offset((moonX - 20) * width / 800, (moonY + 20) * height / 600),
  5,
  craterPaint,
);

// Draw ground silhouette
final groundPaint =
    Paint()
      ..color = const Color(0xFF0a1a40)
      ..style = PaintingStyle.fill;

final groundPath =
    Path()
      ..moveTo(0, height * 0.833)
      ..cubicTo(
        width * 0.125,
        height * 0.8,
        width * 0.25,
        height * 0.866,
        width * 0.375,
        height * 0.816,
      )
      ..cubicTo(
        width * 0.5,
        height * 0.766,
        width * 0.625,
        height * 0.85,
        width * 0.75,
        height * 0.8,
      )
      ..cubicTo(
        width * 0.875,
        height * 0.75,
        width,
        height * 0.816,
        width,
        height * 0.833,
      )
      ..lineTo(width, height)
      ..lineTo(0, height)
      ..close();

canvas.drawPath(groundPath, groundPaint);

// Draw coconut tree
_drawCoconutTree(canvas, size);

}

void _drawCoconutTree(Canvas canvas, Size size) {
final treeX = size.width * 0.75;
final groundY = size.height * 0.833;

// Tree trunk
final trunkPaint =
    Paint()
      ..color = Colors.brown
      ..style = PaintingStyle.fill;

// Draw trunk with slight curve
final trunkPath =
    Path()
      ..moveTo(treeX, groundY)
      ..cubicTo(
        treeX - 90,
        groundY - 10,
        treeX - 10,
        groundY,
        treeX - 20,
        groundY - 240,
      )
      ..lineTo(treeX + 10, groundY - 240)
      ..cubicTo(
        treeX + 10,
        groundY - 160,
        treeX + 5,
        groundY - 80,
        treeX,
        groundY,
      );

canvas.drawPath(trunkPath, trunkPaint);

// Tree leaves
final leafPaint =
    Paint()
      ..color = Colors.green
      ..style = PaintingStyle.fill;

// Draw palm leaves
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 30, -30, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 30, 150, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 30, -10, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 30, 170, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 30, 10, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 30, 190, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 30, 30, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 30, 210, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 60, 25, -45, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 60, 25, 135, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 60, 25, 45, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 60, 25, 225, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 60, 25, 60, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 60, 25, 240, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 25, -60, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 25, 120, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 25, 100, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 25, 280, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 25, 80, leafPaint);
_drawPalmLeaf(canvas, treeX - 15, groundY - 240, 80, 25, 260, leafPaint);
// Draw coconuts
final coconutPaint =
    Paint()
      ..color = Colors.brown.withOpacity(0.8)
      ..style = PaintingStyle.fill;

canvas.drawCircle(Offset(treeX - 25, groundY - 210), 8, coconutPaint);
canvas.drawCircle(Offset(treeX - 5, groundY - 220), 8, coconutPaint);
canvas.drawCircle(Offset(treeX - 25, groundY - 230), 8, coconutPaint);

}

void _drawPalmLeaf(
Canvas canvas,
double x,
double y,
double length,
double width,
double angle,
Paint paint,
) {
final path = Path();
final matrix = Matrix4.identity();
matrix.rotateZ(angle * math.pi / 180);

canvas.save();
canvas.translate(x, y);
canvas.transform(matrix.storage);

path.moveTo(0, 0);
path.cubicTo(
  length * 0.3,
  width * 0.5,
  length * 0.6,
  width * 0.2,
  length,
  0,
);
path.cubicTo(length * 0.6, -width * 0.2, length * 0.3, -width * 0.5, 0, 0);

canvas.drawPath(path, paint);
canvas.restore();

}

@override
bool shouldRepaint(covariant StarryNightPainter oldDelegate) {
return oldDelegate.moonPosition != moonPosition;
}
}

class Star {
final double x;
final double y;
final double size;
final double twinkleSpeed;
final double twinkleOffset;

Star({
required this.x,
required this.y,
required this.size,
required this.twinkleSpeed,
required this.twinkleOffset,
});
}

Otput

✅ Final Thoughts

This example is a perfect blend of animation and canvas drawing in Flutter using CustomPainter. It teaches:

  • Animation basics (AnimationController)
  • Drawing with Canvas and Paint
  • Using sine waves for smooth, natural movement
  • Managing performance with shouldRepaint

🔧 Use case: Ideal for splash screens, backgrounds, or learning advanced Flutter animations.

Show 2 Comments

2 Comments

  1. Edem

    I think there is something missing to finalize the page.

Leave a Reply

Your email address will not be published. Required fields are marked *