Last active
December 6, 2025 16:25
-
-
Save definev/210e50ffa5ac0ce65f41a05f0c203824 to your computer and use it in GitHub Desktop.
CRT Effect for Flutter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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