Skip to content

Commit edb9003

Browse files
committed
fx-smoke widget added to this repo
1 parent 1ab2ef2 commit edb9003

11 files changed

+981
-18
lines changed

assets/images/smoke/bottom_logo.png

1.14 KB
Loading

assets/images/smoke/main_logo.png

817 Bytes
Loading

lib/main.dart

+46-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
23
import 'package:fx_2_folder/books/books.dart';
34
import 'package:fx_2_folder/circles_selector/CirclesHomeWidget.dart';
45
import 'package:fx_2_folder/folder_shape/folder_home.dart';
6+
import 'package:fx_2_folder/smoke/smoke.dart';
57
import 'package:fx_2_folder/vinyl/vinyl.dart';
68
import 'package:google_fonts/google_fonts.dart';
79

@@ -32,11 +34,13 @@ class AnimationExample {
3234
final String title;
3335
final Widget Function(BuildContext) builder;
3436
final Color? appBarColor;
37+
final bool isFullScreen;
3538

3639
AnimationExample({
3740
required this.title,
3841
required this.builder,
3942
this.appBarColor,
43+
this.isFullScreen = false,
4044
});
4145
}
4246

@@ -47,6 +51,12 @@ class HomeScreen extends StatelessWidget {
4751
builder: (context) => const FolderHomeWidget(
4852
curve: Curves.easeInOutBack, title: 'EaseInOutBack'),
4953
),
54+
AnimationExample(
55+
title: 'Smoke',
56+
builder: (context) => const SmokeHomeWidget(),
57+
appBarColor: Colors.black,
58+
isFullScreen: true,
59+
),
5060
AnimationExample(
5161
title: 'Books',
5262
builder: (context) =>
@@ -66,6 +76,15 @@ class HomeScreen extends StatelessWidget {
6676

6777
HomeScreen({super.key});
6878

79+
void enterFullScreen() {
80+
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
81+
}
82+
83+
void exitFullScreen() {
84+
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
85+
overlays: SystemUiOverlay.values);
86+
}
87+
6988
@override
7089
Widget build(BuildContext context) {
7190
return Scaffold(
@@ -91,13 +110,22 @@ class HomeScreen extends StatelessWidget {
91110
borderRadius: BorderRadius.circular(12),
92111
),
93112
child: InkWell(
94-
onTap: () => Navigator.push(
95-
context,
96-
MaterialPageRoute(
97-
builder: (context) => DetailScreen(
98-
key: UniqueKey(), example: examples[index]),
99-
),
100-
),
113+
onTap: () {
114+
Navigator.push(
115+
context,
116+
MaterialPageRoute(
117+
builder: (context) {
118+
if (examples[index].isFullScreen) {
119+
return FullScreen(
120+
key: UniqueKey(), example: examples[index]);
121+
} else {
122+
return DetailScreen(
123+
key: UniqueKey(), example: examples[index]);
124+
}
125+
},
126+
),
127+
);
128+
},
101129
child: Column(
102130
mainAxisAlignment: MainAxisAlignment.center,
103131
children: [
@@ -140,3 +168,14 @@ class DetailScreen extends StatelessWidget {
140168
);
141169
}
142170
}
171+
172+
class FullScreen extends StatelessWidget {
173+
final AnimationExample example;
174+
175+
const FullScreen({super.key, required this.example});
176+
177+
@override
178+
Widget build(BuildContext context) {
179+
return example.builder(context);
180+
}
181+
}

lib/smoke/animated_circles.dart

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'dart:math' as math;
4+
5+
import 'package:fx_2_folder/smoke/animation_sequence.dart';
6+
import 'package:fx_2_folder/smoke/circle_data.dart';
7+
8+
class AnimatedCircles extends StatefulWidget {
9+
final AnimationSequence sequence;
10+
11+
AnimatedCircles({required this.sequence});
12+
13+
@override
14+
_AnimatedCirclesState createState() => _AnimatedCirclesState();
15+
}
16+
17+
class _AnimatedCirclesState extends State<AnimatedCircles>
18+
with SingleTickerProviderStateMixin {
19+
late AnimationController _controller;
20+
late Animation<double> _animation;
21+
int _currentIndex = 0;
22+
23+
@override
24+
void initState() {
25+
super.initState();
26+
_controller = AnimationController(
27+
duration: widget.sequence.stepDuration,
28+
vsync: this,
29+
);
30+
_animation = CurvedAnimation(
31+
parent: Tween<double>(begin: 0, end: 1).animate(_controller),
32+
curve: Curves.linear,
33+
);
34+
35+
_controller.addStatusListener(_updateSequence);
36+
_controller.forward();
37+
}
38+
39+
void _updateSequence(AnimationStatus status) {
40+
if (status == AnimationStatus.completed) {
41+
setState(() {
42+
_currentIndex = (_currentIndex + 1) % widget.sequence.length;
43+
widget.sequence.onSequenceChange?.call(_currentIndex);
44+
});
45+
_controller.reset();
46+
_controller.forward();
47+
}
48+
}
49+
50+
@override
51+
void dispose() {
52+
_controller.dispose();
53+
super.dispose();
54+
}
55+
56+
@override
57+
Widget build(BuildContext context) {
58+
final screenSize = MediaQuery.of(context).size;
59+
return AnimatedBuilder(
60+
animation: _animation,
61+
builder: (context, child) {
62+
return CustomPaint(
63+
size: Size(screenSize.width, screenSize.height),
64+
painter: CirclesPainter(
65+
startCircles: widget.sequence.sequences[_currentIndex],
66+
endCircles: widget.sequence
67+
.sequences[(_currentIndex + 1) % widget.sequence.length],
68+
progress: _animation.value,
69+
),
70+
);
71+
},
72+
);
73+
}
74+
}
75+
76+
class CirclesPainter extends CustomPainter {
77+
final List<CircleData> startCircles;
78+
final List<CircleData> endCircles;
79+
final double progress;
80+
81+
CirclesPainter({
82+
required this.startCircles,
83+
required this.endCircles,
84+
required this.progress,
85+
});
86+
87+
@override
88+
void paint(Canvas canvas, Size size) {
89+
final random = math.Random(42); // Fixed seed for consistent curves
90+
for (int i = 0; i < startCircles.length; i++) {
91+
var startCircle = startCircles[i];
92+
var endCircle = endCircles.firstWhere((c) => c.id == startCircle.id,
93+
orElse: () => startCircle);
94+
var lerpedCircle = startCircle.lerp(endCircle, progress);
95+
96+
// Generate control points for the Bézier curve
97+
Offset controlPoint1 = _generateControlPoint(
98+
startCircle.normalizedPosition, endCircle.normalizedPosition, random);
99+
Offset controlPoint2 = _generateControlPoint(
100+
startCircle.normalizedPosition, endCircle.normalizedPosition, random);
101+
102+
// Calculate the position along the Bézier curve
103+
Offset position = _calculateBezierPoint(startCircle.normalizedPosition,
104+
controlPoint1, controlPoint2, endCircle.normalizedPosition, progress);
105+
106+
// Convert normalized position to actual position
107+
position = Offset(position.dx * size.width, position.dy * size.height);
108+
// var position = Offset(lerpedCircle.normalizedPosition.dx * size.width,
109+
// lerpedCircle.normalizedPosition.dy * size.height);
110+
111+
var paint = Paint()
112+
..color = lerpedCircle.color.withOpacity(1)
113+
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 100);
114+
115+
canvas.drawCircle(position, lerpedCircle.radius, paint);
116+
}
117+
}
118+
119+
@override
120+
bool shouldRepaint(CustomPainter oldDelegate) => true;
121+
122+
Offset _generateControlPoint(Offset start, Offset end, math.Random random) {
123+
double midX = (start.dx + end.dx) / 2;
124+
double midY = (start.dy + end.dy) / 2;
125+
126+
// Add some randomness to the control point
127+
double offsetX = (random.nextDouble() - 0.5) *
128+
1; // Adjust the 0.5 to control the curve intensity
129+
double offsetY = (random.nextDouble() - 0.5) * 1;
130+
131+
return Offset(midX + offsetX, midY + offsetY);
132+
}
133+
134+
Offset _calculateBezierPoint(
135+
Offset p0, Offset p1, Offset p2, Offset p3, double t) {
136+
double x = _bezierValue(p0.dx, p1.dx, p2.dx, p3.dx, t);
137+
double y = _bezierValue(p0.dy, p1.dy, p2.dy, p3.dy, t);
138+
return Offset(x, y);
139+
}
140+
141+
double _bezierValue(double p0, double p1, double p2, double p3, double t) {
142+
return math.pow(1 - t, 3) * p0 +
143+
3 * math.pow(1 - t, 2) * t * p1 +
144+
3 * (1 - t) * math.pow(t, 2) * p2 +
145+
math.pow(t, 3) * p3;
146+
}
147+
}

lib/smoke/animation_sequence.dart

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import 'package:fx_2_folder/smoke/circle_data.dart';
2+
3+
class AnimationSequence {
4+
final List<List<CircleData>> sequences;
5+
final Duration stepDuration;
6+
final Function(int)? onSequenceChange;
7+
8+
AnimationSequence({
9+
required this.sequences,
10+
this.stepDuration = const Duration(seconds: 1),
11+
this.onSequenceChange,
12+
});
13+
14+
int get length => sequences.length;
15+
}

lib/smoke/circle_data.dart

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import 'dart:ui';
2+
3+
import 'package:flutter/material.dart';
4+
5+
class CircleData {
6+
final String id;
7+
final Offset normalizedPosition;
8+
final double radius;
9+
final Color color;
10+
11+
CircleData({
12+
required this.id,
13+
required this.normalizedPosition,
14+
required this.radius,
15+
required this.color,
16+
}) : assert(
17+
normalizedPosition.dx >= 0 &&
18+
normalizedPosition.dx <= 1 &&
19+
normalizedPosition.dy >= 0 &&
20+
normalizedPosition.dy <= 1,
21+
"Normalized position must be between 0 and 1");
22+
23+
CircleData lerp(CircleData other, double t) {
24+
return CircleData(
25+
id: this.id,
26+
normalizedPosition:
27+
Offset.lerp(this.normalizedPosition, other.normalizedPosition, t)!,
28+
radius: lerpDouble(this.radius, other.radius, t)!,
29+
color: Color.lerp(this.color, other.color, t)!,
30+
);
31+
}
32+
}

lib/smoke/randomizer.dart

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import 'dart:math' as math;
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:fx_2_folder/smoke/circle_data.dart';
5+
6+
List<List<CircleData>> generateRandomCircleSets(int N, int setCount) {
7+
assert(N > 0 && setCount > 0, "N and setCount must be positive integers");
8+
9+
final random = math.Random();
10+
List<List<CircleData>> sequences = [];
11+
Set<int> usedQuadrants = {};
12+
13+
for (int set = 0; set < setCount; set++) {
14+
List<CircleData> circleSet = [];
15+
int quadrant;
16+
17+
// Choose a random quadrant that hasn't been used
18+
do {
19+
quadrant = random.nextInt(4);
20+
} while (usedQuadrants.contains(quadrant) && usedQuadrants.length < 4);
21+
usedQuadrants.add(quadrant);
22+
23+
// Generate the main circle for this set
24+
Offset mainPosition = _getRandomPositionInQuadrant(quadrant, random);
25+
circleSet.add(_generateCircleData('0', mainPosition, random));
26+
27+
// Generate satellite circles
28+
for (int i = 1; i < N; i++) {
29+
Offset satellitePosition =
30+
_getNearbySatellitePosition(mainPosition, random);
31+
circleSet
32+
.add(_generateCircleData(i.toString(), satellitePosition, random));
33+
}
34+
35+
sequences.add(circleSet);
36+
}
37+
38+
return sequences;
39+
}
40+
41+
Offset _getRandomPositionInQuadrant(int quadrant, math.Random random) {
42+
double x, y;
43+
switch (quadrant) {
44+
case 0: // Top-left
45+
x = random.nextDouble() * 0.5;
46+
y = random.nextDouble() * 0.5;
47+
break;
48+
case 1: // Top-right
49+
x = 0.5 + random.nextDouble() * 0.5;
50+
y = random.nextDouble() * 0.5;
51+
break;
52+
case 2: // Bottom-left
53+
x = random.nextDouble() * 0.5;
54+
y = 0.5 + random.nextDouble() * 0.5;
55+
break;
56+
case 3: // Bottom-right
57+
x = 0.5 + random.nextDouble() * 0.5;
58+
y = 0.5 + random.nextDouble() * 0.5;
59+
break;
60+
default:
61+
throw ArgumentError('Invalid quadrant');
62+
}
63+
return Offset(x, y);
64+
}
65+
66+
Offset _getNearbySatellitePosition(Offset mainPosition, math.Random random) {
67+
double angle = random.nextDouble() * 2 * math.pi;
68+
double distance =
69+
random.nextDouble() * 0.5; // Adjust this value to control spread
70+
double x = mainPosition.dx + distance * math.cos(angle);
71+
double y = mainPosition.dy + distance * math.sin(angle);
72+
return Offset(x.clamp(0.0, 1.0), y.clamp(0.0, 1.0));
73+
}
74+
75+
CircleData _generateCircleData(String id, Offset position, math.Random random) {
76+
return CircleData(
77+
id: id,
78+
normalizedPosition: position,
79+
radius: random.nextDouble() * 100 + 30, // Random radius between 10 and 40
80+
color: Color.fromRGBO(
81+
random.nextInt(256),
82+
random.nextInt(256),
83+
random.nextInt(256),
84+
1,
85+
),
86+
);
87+
}

0 commit comments

Comments
 (0)