rain and lightning effects addded.
This commit is contained in:
1 parent
adf6de5dc0
commit
e1c5d5b106
3 files changed
+406
No files matched your search
@@ -0,0 +1,233 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:revoltvpn/logic/app_colors.dart';
|
||||||
|
|
||||||
|
/// 2D lightning bolt — a jagged yellow line from top to bottom with glow.
|
||||||
|
|
||||||
|
class LightningEffect extends StatefulWidget {
|
||||||
|
|
||||||
|
final int minIntervalSec;
|
||||||
|
|
||||||
|
final int maxIntervalSec;
|
||||||
|
|
||||||
|
final double doubleChance;
|
||||||
|
|
||||||
|
const LightningEffect({
|
||||||
|
super.key,
|
||||||
|
this.minIntervalSec = 2,
|
||||||
|
this.maxIntervalSec = 10,
|
||||||
|
this.doubleChance = 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LightningEffect> createState() => _LightningEffectState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LightningEffectState extends State<LightningEffect>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _ctrl;
|
||||||
|
Timer? _timer;
|
||||||
|
final _random = Random();
|
||||||
|
|
||||||
|
LightningBolt? _bolt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
|
||||||
|
_scheduleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleNext() {
|
||||||
|
final secs = widget.minIntervalSec +
|
||||||
|
_random.nextInt(widget.maxIntervalSec - widget.minIntervalSec + 1);
|
||||||
|
_timer = Timer(Duration(seconds: secs), _strike);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _strike() {
|
||||||
|
if (!mounted) return;
|
||||||
|
_bolt = LightningBolt.random(_random);
|
||||||
|
_ctrl.forward(from: 0.0);
|
||||||
|
|
||||||
|
if (_random.nextDouble() < widget.doubleChance) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 120), () {
|
||||||
|
if (mounted) {
|
||||||
|
_bolt = LightningBolt.random(_random);
|
||||||
|
_ctrl.forward(from: 0.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sharp flash curve: quick on, quick off. No slow fade.
|
||||||
|
double _opacity(double t) {
|
||||||
|
if (t < 0.08) return t / 0.08; // fast ramp
|
||||||
|
return 1.0 - ((t - 0.08) / 0.92); // linear decay
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _ctrl,
|
||||||
|
builder: (_, child) {
|
||||||
|
final raw = _opacity(_ctrl.value);
|
||||||
|
if (raw <= 0.02 || _bolt == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return IgnorePointer(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (ctx, constraints) {
|
||||||
|
final sz = Size(constraints.maxWidth, constraints.maxHeight);
|
||||||
|
return CustomPaint(
|
||||||
|
size: sz,
|
||||||
|
painter: _LightningPainter(
|
||||||
|
bolt: _bolt!,
|
||||||
|
opacity: raw,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// A single bolt defined by a list of 2D points (0.0–1.0 in width×height space).
|
||||||
|
class LightningBolt {
|
||||||
|
final List<Offset> mainPath;
|
||||||
|
final List<List<Offset>> branches; // 0–2 forks off the main path
|
||||||
|
|
||||||
|
const LightningBolt({required this.mainPath, this.branches = const []});
|
||||||
|
|
||||||
|
/// Generate a random bolt. The path always starts near the top-center
|
||||||
|
/// and ends near the bottom, zigzagging left/right along the way.
|
||||||
|
factory LightningBolt.random(Random rng) {
|
||||||
|
final points = <Offset>[];
|
||||||
|
|
||||||
|
// Start: top, slight horizontal offset
|
||||||
|
double x = 0.5 + (rng.nextDouble() - 0.5) * 0.15;
|
||||||
|
points.add(Offset(x, 0.0));
|
||||||
|
|
||||||
|
// Zigzag down — 6–10 segments
|
||||||
|
final segments = 6 + rng.nextInt(5);
|
||||||
|
for (int i = 1; i < segments; i++) {
|
||||||
|
final y = i / segments;
|
||||||
|
// Random horizontal offset — each segment adds jitter
|
||||||
|
x += (rng.nextDouble() - 0.5) * 0.25;
|
||||||
|
x = x.clamp(0.05, 0.95); // stay on screen
|
||||||
|
points.add(Offset(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// End: bottom
|
||||||
|
points.add(Offset(x.clamp(0.1, 0.9), 1.0));
|
||||||
|
|
||||||
|
// Branches — 0–2 forks from random points along the main path
|
||||||
|
final branches = <List<Offset>>[];
|
||||||
|
final branchCount = rng.nextInt(3);
|
||||||
|
for (int b = 0; b < branchCount; b++) {
|
||||||
|
final branch = <Offset>[];
|
||||||
|
final startIdx = 1 + rng.nextInt(points.length - 2);
|
||||||
|
final start = points[startIdx];
|
||||||
|
branch.add(start);
|
||||||
|
|
||||||
|
double bx = start.dx;
|
||||||
|
double by = start.dy;
|
||||||
|
final branchLen = 2 + rng.nextInt(3);
|
||||||
|
for (int j = 0; j < branchLen; j++) {
|
||||||
|
by += (0.05 + rng.nextDouble() * 0.12);
|
||||||
|
bx += (rng.nextDouble() - 0.5) * 0.3;
|
||||||
|
bx = bx.clamp(0.02, 0.98);
|
||||||
|
by = by.clamp(0.0, 1.0);
|
||||||
|
branch.add(Offset(bx, by));
|
||||||
|
}
|
||||||
|
branches.add(branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LightningBolt(mainPath: points, branches: branches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _LightningPainter extends CustomPainter {
|
||||||
|
final LightningBolt bolt;
|
||||||
|
final double opacity;
|
||||||
|
|
||||||
|
_LightningPainter({required this.bolt, required this.opacity});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final toScreen = (Offset p) => Offset(p.dx * size.width, p.dy * size.height);
|
||||||
|
final scaledMain = bolt.mainPath.map(toScreen).toList();
|
||||||
|
final scaledBranches = bolt.branches
|
||||||
|
.map((b) => b.map(toScreen).toList())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final baseColor = AppColors.accent.withAlpha((opacity * 255).round());
|
||||||
|
final glowColor = AppColors.accent.withAlpha((opacity * 0.35 * 255).round());
|
||||||
|
|
||||||
|
// ── Glow layer (thick, blurry) ──────────────
|
||||||
|
final glowPaint = Paint()
|
||||||
|
..color = glowColor
|
||||||
|
..strokeWidth = 18
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeJoin = StrokeJoin.round
|
||||||
|
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8)
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
_drawPath(canvas, scaledMain, scaledBranches, glowPaint);
|
||||||
|
|
||||||
|
// ── Core bolt (thin, bright) ────────────────
|
||||||
|
final corePaint = Paint()
|
||||||
|
..color = baseColor
|
||||||
|
..strokeWidth = 5
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeJoin = StrokeJoin.round
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
_drawPath(canvas, scaledMain, scaledBranches, corePaint);
|
||||||
|
|
||||||
|
// ── Hot center (extra white core) ───────────
|
||||||
|
if (opacity > 0.6) {
|
||||||
|
final hotPaint = Paint()
|
||||||
|
..color = Colors.white.withAlpha((opacity * 180).round())
|
||||||
|
..strokeWidth = 2
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeJoin = StrokeJoin.round
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
_drawPath(canvas, scaledMain, scaledBranches, hotPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawPath(Canvas canvas, List<Offset> main,
|
||||||
|
List<List<Offset>> branches, Paint paint) {
|
||||||
|
final mainPath = Path()..moveTo(main.first.dx, main.first.dy);
|
||||||
|
for (int i = 1; i < main.length; i++) {
|
||||||
|
mainPath.lineTo(main[i].dx, main[i].dy);
|
||||||
|
}
|
||||||
|
canvas.drawPath(mainPath, paint);
|
||||||
|
|
||||||
|
for (final branch in branches) {
|
||||||
|
final bPath = Path()..moveTo(branch.first.dx, branch.first.dy);
|
||||||
|
for (int i = 1; i < branch.length; i++) {
|
||||||
|
bPath.lineTo(branch[i].dx, branch[i].dy);
|
||||||
|
}
|
||||||
|
canvas.drawPath(bPath, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _LightningPainter old) =>
|
||||||
|
bolt != old.bolt || opacity != old.opacity;
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
/// Subtle animated rain effect — sits behind all UI as a full-screen overlay.
|
||||||
|
///
|
||||||
|
/// Usage (in a Stack, as the first child so it renders behind everything):
|
||||||
|
|
||||||
|
class RainEffect extends StatefulWidget {
|
||||||
|
final int dropCount;
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
final double minLength;
|
||||||
|
|
||||||
|
final double maxLength;
|
||||||
|
|
||||||
|
final double windDrift;
|
||||||
|
|
||||||
|
const RainEffect({
|
||||||
|
super.key,
|
||||||
|
this.dropCount = 150,
|
||||||
|
this.color = const Color(0xFFE8EAF6),
|
||||||
|
this.minLength = 12,
|
||||||
|
this.maxLength = 44,
|
||||||
|
this.windDrift = 4.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RainEffect> createState() => _RainEffectState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RainEffectState extends State<RainEffect>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final Ticker _ticker;
|
||||||
|
final _drops = <_Drop>[];
|
||||||
|
final _random = Random();
|
||||||
|
Duration _last = Duration.zero;
|
||||||
|
|
||||||
|
// ── Lifecycle ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
for (int i = 0; i < widget.dropCount; i++) {
|
||||||
|
_drops.add(_Drop.spawn(_random, widget.minLength, widget.maxLength));
|
||||||
|
}
|
||||||
|
_ticker = createTicker(_onTick)..start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ticker.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tick — move drops, recycle off-screen ones ──────────────────────
|
||||||
|
|
||||||
|
void _onTick(Duration now) {
|
||||||
|
double dt = 0.016; // ~60 fps fallback on first tick
|
||||||
|
if (_last != Duration.zero) {
|
||||||
|
dt = (now - _last).inMicroseconds / 1000000.0;
|
||||||
|
dt = dt.clamp(0.0, 0.1);
|
||||||
|
}
|
||||||
|
_last = now;
|
||||||
|
|
||||||
|
for (final d in _drops) {
|
||||||
|
d.y += d.speed * dt;
|
||||||
|
if (d.y > 1.08) {
|
||||||
|
d.spawn(_random, widget.minLength, widget.maxLength);
|
||||||
|
d.y = -0.06 - _random.nextDouble() * 0.08;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: _RainPainter(
|
||||||
|
drops: _drops,
|
||||||
|
color: widget.color,
|
||||||
|
windDrift: widget.windDrift,
|
||||||
|
),
|
||||||
|
size: Size.infinite,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Drop model
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class _Drop {
|
||||||
|
double x;
|
||||||
|
double y;
|
||||||
|
double speed;
|
||||||
|
double length;
|
||||||
|
double opacity;
|
||||||
|
|
||||||
|
_Drop({
|
||||||
|
required this.x,
|
||||||
|
required this.y,
|
||||||
|
required this.speed,
|
||||||
|
required this.length,
|
||||||
|
required this.opacity,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Create a drop at a random position anywhere on screen.
|
||||||
|
factory _Drop.spawn(Random r, double minLen, double maxLen) {
|
||||||
|
return _Drop(
|
||||||
|
x: r.nextDouble(),
|
||||||
|
y: r.nextDouble(),
|
||||||
|
speed: 0.30 + r.nextDouble() * 0.55,
|
||||||
|
length: minLen + r.nextDouble() * (maxLen - minLen),
|
||||||
|
opacity: 0.05 + r.nextDouble() * 0.12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-randomise all properties (call when recycling a drop to the top).
|
||||||
|
void spawn(Random r, double minLen, double maxLen) {
|
||||||
|
x = r.nextDouble();
|
||||||
|
speed = 0.30 + r.nextDouble() * 0.55;
|
||||||
|
length = minLen + r.nextDouble() * (maxLen - minLen);
|
||||||
|
opacity = 0.05 + r.nextDouble() * 0.12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Painter
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class _RainPainter extends CustomPainter {
|
||||||
|
final List<_Drop> drops;
|
||||||
|
final Color color;
|
||||||
|
final double windDrift;
|
||||||
|
|
||||||
|
_RainPainter({
|
||||||
|
required this.drops,
|
||||||
|
required this.color,
|
||||||
|
required this.windDrift,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeWidth = 1.5;
|
||||||
|
|
||||||
|
for (final d in drops) {
|
||||||
|
final sx = d.x * size.width;
|
||||||
|
final sy = d.y * size.height;
|
||||||
|
final ex = sx - windDrift; // slight leftward slant
|
||||||
|
final ey = sy + d.length;
|
||||||
|
|
||||||
|
paint.color = color.withAlpha((d.opacity * 255).round());
|
||||||
|
canvas.drawLine(Offset(sx, sy), Offset(ex, ey), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_RainPainter old) => true; // always animating
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import 'package:revoltvpn/components/timer_box.dart';
|
|||||||
import 'package:revoltvpn/components/speed_box.dart';
|
import 'package:revoltvpn/components/speed_box.dart';
|
||||||
import 'package:revoltvpn/components/status_bar.dart';
|
import 'package:revoltvpn/components/status_bar.dart';
|
||||||
import 'package:revoltvpn/components/support_button.dart';
|
import 'package:revoltvpn/components/support_button.dart';
|
||||||
|
import 'package:revoltvpn/components/rain.dart';
|
||||||
|
import 'package:revoltvpn/components/lightning.dart';
|
||||||
|
|
||||||
class MainScreen extends StatelessWidget {
|
class MainScreen extends StatelessWidget {
|
||||||
const MainScreen({super.key});
|
const MainScreen({super.key});
|
||||||
@@ -29,6 +31,12 @@ class MainScreen extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
// ── Lightning — behind rain so drops stay visible during flash ──
|
||||||
|
const Positioned.fill(child: LightningEffect()),
|
||||||
|
|
||||||
|
// ── Rain effect — behind all UI, baked into the background ──
|
||||||
|
const Positioned.fill(child: RainEffect()),
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 4,
|
right: 4,
|
||||||
|
|||||||
Reference in new issue
Block a user