Skip to content

Commit 51e0300

Browse files
create MovablePinsBackground
1 parent fe9600e commit 51e0300

File tree

1 file changed

+210
-0
lines changed

1 file changed

+210
-0
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import 'dart:math';
2+
3+
import 'package:algorithm_visualizer/core/resources/theme_manager.dart';
4+
import 'package:flutter/material.dart';
5+
6+
class MovablePinsBackground extends StatefulWidget {
7+
const MovablePinsBackground({this.pinColor = ThemeEnum.whiteD5Color, required this.child, super.key});
8+
final ThemeEnum pinColor;
9+
final Widget child;
10+
@override
11+
State<MovablePinsBackground> createState() => _MovablePinsBackgroundState();
12+
}
13+
14+
class _MovablePinsBackgroundState extends State<MovablePinsBackground> with SingleTickerProviderStateMixin {
15+
late AnimationController _controller;
16+
final List<Particle> _particles = [];
17+
final Random _random = Random();
18+
19+
/// 🔧 Adjustable parameters
20+
double speedFactor = 0.3;
21+
double densityFactor = 0.45;
22+
final int maxPins = 150;
23+
24+
@override
25+
void initState() {
26+
super.initState();
27+
_controller = AnimationController(
28+
vsync: this,
29+
duration: const Duration(hours: 1),
30+
)..addListener(_update);
31+
32+
_controller.repeat();
33+
}
34+
35+
void _createParticles(Size size) {
36+
if (_particles.isNotEmpty) return;
37+
38+
final baseCount = (size.width * size.height / 2500).round();
39+
final count = (baseCount * densityFactor).round();
40+
41+
for (int i = 0; i < count; i++) {
42+
_particles.add(Particle.random(_random, size, speedFactor));
43+
}
44+
}
45+
46+
void _update() {
47+
final size = MediaQuery.of(context).size;
48+
49+
setState(() {
50+
for (int i = _particles.length - 1; i >= 0; i--) {
51+
final p = _particles[i];
52+
p.randomWalk();
53+
p.update();
54+
55+
// remove if outside screen
56+
if (!p.isInside(size)) {
57+
_particles.removeAt(i);
58+
_particles.add(Particle.random(_random, size, speedFactor));
59+
}
60+
}
61+
});
62+
}
63+
64+
@override
65+
void dispose() {
66+
_controller.dispose();
67+
super.dispose();
68+
}
69+
70+
@override
71+
Widget build(BuildContext context) {
72+
final size = MediaQuery.of(context).size;
73+
_createParticles(size);
74+
75+
return GestureDetector(
76+
// set max pins limit
77+
78+
onTapDown: (d) {
79+
setState(() {
80+
// add new pin
81+
_particles.add(Particle(
82+
d.localPosition,
83+
Offset((_random.nextDouble() * 2 - 1) * 0.5, (_random.nextDouble() * 2 - 1) * 0.5),
84+
_random,
85+
size,
86+
speedFactor,
87+
));
88+
89+
// if exceeded maxPins → remove the oldest one (index 0)
90+
if (_particles.length > maxPins) {
91+
_particles.removeAt(0);
92+
}
93+
});
94+
},
95+
onPanUpdate: (d) {
96+
setState(() {
97+
const influenceRadius = 100.0; // 👈 adjust how far the finger affects pins
98+
for (var p in _particles) {
99+
final distance = (p.position - d.localPosition).distance;
100+
if (distance < influenceRadius) {
101+
// push away
102+
final direction = (p.position - d.localPosition).normalize();
103+
p.velocity += direction * 0.5; // 👈 adjust strength
104+
}
105+
}
106+
});
107+
},
108+
109+
child: Stack(
110+
alignment: AlignmentDirectional.center,
111+
children: [
112+
CustomPaint(
113+
painter: ParticlePainter(_particles, context.getColor(widget.pinColor)),
114+
child: const SizedBox.expand(),
115+
),
116+
widget.child
117+
],
118+
),
119+
);
120+
}
121+
}
122+
123+
class Particle {
124+
Offset position;
125+
Offset velocity;
126+
final Size screenSize;
127+
final Random random;
128+
final double speedFactor;
129+
130+
Particle(this.position, this.velocity, this.random, this.screenSize, this.speedFactor);
131+
132+
factory Particle.random(Random random, Size screenSize, double speedFactor) {
133+
return Particle(
134+
Offset(
135+
random.nextDouble() * screenSize.width,
136+
random.nextDouble() * screenSize.height,
137+
),
138+
Offset(
139+
(random.nextDouble() * 2 - 1) * 0.5,
140+
(random.nextDouble() * 2 - 1) * 0.5,
141+
),
142+
random,
143+
screenSize,
144+
speedFactor,
145+
);
146+
}
147+
148+
void randomWalk() {
149+
final dx = (random.nextDouble() * 0.2 - 0.1) * speedFactor;
150+
final dy = (random.nextDouble() * 0.2 - 0.1) * speedFactor;
151+
velocity += Offset(dx, dy);
152+
153+
final maxSpeed = 1.5 * speedFactor;
154+
if (velocity.distance > maxSpeed) {
155+
velocity = (velocity / velocity.distance) * maxSpeed;
156+
}
157+
}
158+
159+
void update() {
160+
position += velocity;
161+
}
162+
163+
bool isInside(Size size) {
164+
return position.dx >= -20 &&
165+
position.dx <= size.width + 20 &&
166+
position.dy >= -20 &&
167+
position.dy <= size.height + 20;
168+
}
169+
}
170+
171+
class ParticlePainter extends CustomPainter {
172+
final List<Particle> particles;
173+
final Color pinColor;
174+
175+
ParticlePainter(this.particles, this.pinColor);
176+
177+
@override
178+
void paint(Canvas canvas, Size size) {
179+
final circlePaint = Paint()..color = pinColor;
180+
final linePaint = Paint()
181+
..color = pinColor.withOpacity(0.2)
182+
..strokeWidth = 0.5;
183+
184+
const maxDistance = 150.0;
185+
186+
for (var p in particles) {
187+
canvas.drawCircle(p.position, 3, circlePaint);
188+
189+
for (var other in particles) {
190+
if (p == other) continue;
191+
final dist = (p.position - other.position).distance;
192+
if (dist < maxDistance) {
193+
linePaint.color = pinColor.withOpacity(1 - dist / maxDistance);
194+
canvas.drawLine(p.position, other.position, linePaint);
195+
}
196+
}
197+
}
198+
}
199+
200+
@override
201+
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
202+
}
203+
204+
extension OffsetX on Offset {
205+
Offset normalize() {
206+
final len = distance;
207+
if (len == 0) return Offset.zero;
208+
return this / len;
209+
}
210+
}

0 commit comments

Comments
 (0)