Skip to content

Instantly share code, notes, and snippets.

@definev
Last active December 6, 2025 16:25
Show Gist options
  • Select an option

  • Save definev/210e50ffa5ac0ce65f41a05f0c203824 to your computer and use it in GitHub Desktop.

Select an option

Save definev/210e50ffa5ac0ce65f41a05f0c203824 to your computer and use it in GitHub Desktop.
CRT Effect for Flutter
import 'dart:math';
import 'package:flutter/material.dart';
class CrtScreen extends StatefulWidget {
final Widget child;
final double scanlineGap;
final double scanlineThickness;
final double verticalLineGap;
final double horizontalShakeRange;
final double verticalShakeRange;
final Color backgroundColor;
const CrtScreen({
super.key,
required this.child,
this.scanlineGap = 2.3,
this.scanlineThickness = 1,
this.verticalLineGap = 2.5,
this.horizontalShakeRange = 1.0,
this.verticalShakeRange = 2.0,
this.backgroundColor = Colors.black,
});
@override
State<CrtScreen> createState() => _CrtScreenState();
}
class _CrtScreenState extends State<CrtScreen>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
final Random _random = Random();
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ColoredBox(
color: widget.backgroundColor,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
// Generate a small random offset for the shake effect
final shakeOffset = Offset(
(_random.nextDouble() - 0.5) * widget.horizontalShakeRange,
(_random.nextDouble() - 0.5) * widget.verticalShakeRange,
);
return Transform.translate(
offset: shakeOffset,
child: CustomPaint(
foregroundPainter: CrtPainter(
shakeOffset: -shakeOffset,
scanlineGap: widget.scanlineGap,
scanlineThickness: widget.scanlineThickness,
verticalLineGap: widget.verticalLineGap,
backgroundColor: widget.backgroundColor,
),
child: widget.child,
),
);
},
),
);
}
}
class CrtPainter extends CustomPainter {
final Offset shakeOffset;
final double scanlineGap;
final double scanlineThickness;
final double verticalLineGap;
final Color backgroundColor;
CrtPainter({
required this.shakeOffset,
required this.scanlineGap,
required this.scanlineThickness,
required this.verticalLineGap,
required this.backgroundColor,
});
@override
void paint(Canvas canvas, Size size) {
// Apply shake
canvas.save();
canvas.translate(shakeOffset.dx, shakeOffset.dy);
// 1. Draw Vertical Aperture Grille Lines (New)
final vLinePaint = Paint()
..color = backgroundColor.withValues(
alpha: 0.0 + 0.2 * Random().nextDouble(),
)
..style = PaintingStyle.stroke
..strokeWidth = 1;
if (verticalLineGap > 0) {
for (double x = 0; x < size.width; x += verticalLineGap) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), vLinePaint);
}
}
// 2. Draw Heavy Scanlines
final paint = Paint()
..color = backgroundColor.withValues(
alpha: 1.0 - 0.4 * Random().nextDouble(),
)
..style = PaintingStyle.fill;
double lineHeight = scanlineThickness;
double gap = scanlineGap;
if (lineHeight > 0 || gap > 0) {
for (double i = 0; i < size.height; i += (lineHeight + gap)) {
canvas.drawRect(Rect.fromLTWH(0, i, size.width, lineHeight), paint);
}
}
canvas.restore();
}
@override
bool shouldRepaint(covariant CrtPainter oldDelegate) {
return oldDelegate.shakeOffset != shakeOffset ||
oldDelegate.scanlineGap != scanlineGap ||
oldDelegate.scanlineThickness != scanlineThickness ||
oldDelegate.verticalLineGap != verticalLineGap ||
oldDelegate.backgroundColor != backgroundColor;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment