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 aStatelessWidget
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:
- A random position
- A unique twinkle speed and offset
- A 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
andPaint
- Using sine waves for smooth, natural movement
- Managing performance with
shouldRepaint
🔧 Use case: Ideal for splash screens, backgrounds, or learning advanced Flutter animations.
I think there is something missing to finalize the page.
what is missing? we have put full code here.