|
// Copyright © Ivanna Kacevica https://happycode.studio/ |
|
// |
|
// Submission for the Flutteristas 2025 Code Challenge. |
|
// |
|
// Permission is granted to use, modify, and distribute this code |
|
// under the terms of the MIT License that can be found |
|
// at https://opensource.org/licenses/MIT. |
|
|
|
import 'dart:math' as math; |
|
import 'package:flutter/material.dart'; |
|
import 'package:flutter/rendering.dart'; |
|
import 'dart:ui'; |
|
|
|
void main() { |
|
runApp(const MyApp()); |
|
} |
|
|
|
class MyApp extends StatelessWidget { |
|
const MyApp({super.key}); |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
return MaterialApp( |
|
debugShowCheckedModeBanner: false, |
|
theme: ThemeData.dark(), |
|
home: const KaleidoscopeScreen(), |
|
); |
|
} |
|
} |
|
|
|
class KaleidoscopeScreen extends StatefulWidget { |
|
const KaleidoscopeScreen({super.key}); |
|
|
|
@override |
|
State<KaleidoscopeScreen> createState() => _KaleidoscopeScreenState(); |
|
} |
|
|
|
class _KaleidoscopeScreenState extends State<KaleidoscopeScreen> |
|
with SingleTickerProviderStateMixin { |
|
late AnimationController _controller; |
|
|
|
final List<KaleidoscopeParticle> _particles = []; |
|
final int _numSegments = 8; |
|
double _lastUpdateTime = 0; |
|
final double _kaleidoscopeRotationSpeed = 0.5; |
|
double _prevRotationValue = 0.0; |
|
final random = math.Random(); |
|
final double _baseParticleSize = 0.15; |
|
@override |
|
void initState() { |
|
super.initState(); |
|
|
|
_initializeParticles(); |
|
|
|
_controller = AnimationController( |
|
vsync: this, |
|
duration: const Duration(seconds: 30), |
|
)..repeat(); |
|
|
|
_controller.addListener(() { |
|
setState(() { |
|
final now = DateTime.now().millisecondsSinceEpoch / 1000.0; |
|
final deltaTime = _lastUpdateTime == 0 ? 0.016 : now - _lastUpdateTime; |
|
_lastUpdateTime = now; |
|
|
|
final currentRotation = _controller.value * 2 * math.pi / 2; |
|
final rotationDelta = _calculateDeltaAngle( |
|
currentRotation, |
|
_prevRotationValue, |
|
); |
|
_prevRotationValue = currentRotation; |
|
|
|
_updateParticles(deltaTime, rotationDelta); |
|
}); |
|
}); |
|
} |
|
|
|
double _calculateDeltaAngle(double current, double previous) { |
|
double delta = current - previous; |
|
if (delta > math.pi) delta -= 2 * math.pi; |
|
if (delta < -math.pi) delta += 2 * math.pi; |
|
return delta; |
|
} |
|
|
|
void _initializeParticles() { |
|
final random = math.Random(); |
|
|
|
final baseSize = _baseParticleSize; |
|
|
|
Widget createSizedWidget( |
|
Widget Function(double size) widgetBuilder, |
|
double scaleFactor, |
|
) { |
|
return Builder( |
|
builder: (context) { |
|
final size = MediaQuery.of(context).size; |
|
final circleRadius = size.shortestSide * 0.75 / 2; |
|
final widgetSize = circleRadius * baseSize * scaleFactor; |
|
return widgetBuilder(widgetSize); |
|
}, |
|
); |
|
} |
|
|
|
final particleTypes = [ |
|
ParticleProperties( |
|
widgets: [ |
|
createSizedWidget( |
|
(size) => Text('💎', style: TextStyle(fontSize: size)), |
|
1.2, |
|
), |
|
createSizedWidget( |
|
(size) => Text('🌟', style: TextStyle(fontSize: size)), |
|
1.2, |
|
), |
|
], |
|
massMultiplier: 2.5, |
|
restitution: 0.85, |
|
frictionCoefficient: 0.02, |
|
momentOfInertiaMultiplier: 1.2, |
|
dragCoefficient: 0.01, |
|
), |
|
ParticleProperties( |
|
widgets: [ |
|
createSizedWidget( |
|
(size) => Text('🌸', style: TextStyle(fontSize: size)), |
|
1.0, |
|
), |
|
createSizedWidget( |
|
(size) => Text('🌹', style: TextStyle(fontSize: size)), |
|
1.0, |
|
), |
|
createSizedWidget( |
|
(size) => Text('🦋', style: TextStyle(fontSize: size)), |
|
1.0, |
|
), |
|
], |
|
massMultiplier: 1.0, |
|
restitution: 0.65, |
|
frictionCoefficient: 0.04, |
|
momentOfInertiaMultiplier: 1.0, |
|
dragCoefficient: 0.03, |
|
), |
|
ParticleProperties( |
|
widgets: [ |
|
createSizedWidget( |
|
(size) => Icon( |
|
Icons.auto_awesome, |
|
color: Colors.purpleAccent, |
|
size: size, |
|
), |
|
0.9, |
|
), |
|
createSizedWidget( |
|
(size) => FlutterLogo(size: size, style: FlutterLogoStyle.markOnly), |
|
1.2, |
|
), |
|
], |
|
massMultiplier: 0.7, |
|
restitution: 0.5, |
|
frictionCoefficient: 0.05, |
|
momentOfInertiaMultiplier: 0.8, |
|
dragCoefficient: 0.05, |
|
), |
|
ParticleProperties( |
|
widgets: [ |
|
createSizedWidget( |
|
(size) => Text('🍭', style: TextStyle(fontSize: size)), |
|
1.1, |
|
), |
|
createSizedWidget( |
|
(size) => Text('👑', style: TextStyle(fontSize: size)), |
|
1.1, |
|
), |
|
createSizedWidget( |
|
(size) => Text('🍀', style: TextStyle(fontSize: size)), |
|
1.1, |
|
), |
|
createSizedWidget( |
|
(size) => Text('🎪', style: TextStyle(fontSize: size)), |
|
1.1, |
|
), |
|
], |
|
massMultiplier: 1.5, |
|
restitution: 0.7, |
|
frictionCoefficient: 0.03, |
|
momentOfInertiaMultiplier: 1.1, |
|
dragCoefficient: 0.02, |
|
), |
|
]; |
|
|
|
for (final particleType in particleTypes) { |
|
for (final widget in particleType.widgets) { |
|
final scale = 0.5 + random.nextDouble() * 0.7; |
|
final collisionRadius = scale * 20.0; |
|
_particles.add( |
|
KaleidoscopeParticle( |
|
widget: widget, |
|
position: Offset( |
|
0.1 + random.nextDouble() * 0.7, |
|
0.1 + random.nextDouble() * 0.7, |
|
), |
|
velocity: Offset( |
|
(random.nextDouble() - 0.5) * 0.005, |
|
(random.nextDouble() - 0.5) * 0.005, |
|
), |
|
scale: scale, |
|
collisionRadius: collisionRadius, |
|
mass: scale * particleType.massMultiplier, |
|
restitution: particleType.restitution, |
|
frictionCoefficient: particleType.frictionCoefficient, |
|
momentOfInertia: |
|
math.pow(collisionRadius, 2) * |
|
particleType.momentOfInertiaMultiplier, |
|
dragCoefficient: particleType.dragCoefficient, |
|
), |
|
); |
|
|
|
_particles.last.angularVelocity = (random.nextDouble() - 0.5) * 0.1; |
|
} |
|
} |
|
} |
|
|
|
void _updateParticles(double deltaTime, double rotationDelta) { |
|
final random = math.Random(); |
|
|
|
for (var particle in _particles) { |
|
final distanceFromCenter = math.sqrt( |
|
math.pow(particle.position.dx - 0.5, 2) + |
|
math.pow(particle.position.dy - 0.5, 2), |
|
); |
|
|
|
if (distanceFromCenter > 0.01) { |
|
final directionToCenter = Offset( |
|
(0.5 - particle.position.dx) / distanceFromCenter, |
|
(0.5 - particle.position.dy) / distanceFromCenter, |
|
); |
|
|
|
final forceMagnitude = |
|
0.00002 / (distanceFromCenter * distanceFromCenter); |
|
|
|
particle.applyForce( |
|
directionToCenter.scale(forceMagnitude, forceMagnitude), |
|
); |
|
} |
|
|
|
if (rotationDelta.abs() > 0.0001) { |
|
final centrifugalMagnitude = |
|
rotationDelta * |
|
_kaleidoscopeRotationSpeed * |
|
distanceFromCenter * |
|
0.01 * |
|
particle.mass; |
|
|
|
final directionFromCenter = Offset( |
|
(particle.position.dx - 0.5) / distanceFromCenter, |
|
(particle.position.dy - 0.5) / distanceFromCenter, |
|
); |
|
|
|
particle.applyForce( |
|
directionFromCenter.scale(centrifugalMagnitude, centrifugalMagnitude), |
|
); |
|
} |
|
|
|
if (random.nextDouble() < 0.01) { |
|
particle.applyForce( |
|
Offset( |
|
(random.nextDouble() - 0.5) * 0.00005, |
|
(random.nextDouble() - 0.5) * 0.00005, |
|
), |
|
); |
|
} |
|
|
|
final velocity = math.sqrt( |
|
math.pow(particle.velocity.dx, 2) + math.pow(particle.velocity.dy, 2), |
|
); |
|
|
|
if (velocity > 0.0001) { |
|
final dragMagnitude = particle.dragCoefficient * velocity * velocity; |
|
final dragForce = Offset( |
|
-particle.velocity.dx / velocity * dragMagnitude, |
|
-particle.velocity.dy / velocity * dragMagnitude, |
|
); |
|
|
|
particle.applyForce(dragForce); |
|
} |
|
|
|
particle.update(deltaTime * 10); |
|
|
|
_handleBoundaryCollision(particle); |
|
} |
|
|
|
_handleParticleCollisions(); |
|
} |
|
|
|
void _handleBoundaryCollision(KaleidoscopeParticle particle) { |
|
final boundaryMin = 0.05; |
|
final boundaryMax = 0.85; |
|
|
|
final distanceFromLeft = particle.position.dx - boundaryMin; |
|
final distanceFromRight = boundaryMax - particle.position.dx; |
|
final distanceFromTop = particle.position.dy - boundaryMin; |
|
final distanceFromBottom = boundaryMax - particle.position.dy; |
|
|
|
final collisionThreshold = particle.collisionRadius / 500; |
|
if (distanceFromLeft < collisionThreshold && particle.velocity.dx < 0) { |
|
particle.position = Offset( |
|
boundaryMin + collisionThreshold, |
|
particle.position.dy, |
|
); |
|
|
|
particle.velocity = Offset( |
|
-particle.velocity.dx * particle.restitution, |
|
particle.velocity.dy * (1.0 - particle.frictionCoefficient), |
|
); |
|
|
|
final contactPointOffsetY = random.nextDouble() * 0.4 - 0.2; |
|
final torque = contactPointOffsetY * particle.velocity.dx.abs() * 0.3; |
|
particle.applyTorque(torque); |
|
} else if (distanceFromRight < collisionThreshold && |
|
particle.velocity.dx > 0) { |
|
particle.position = Offset( |
|
boundaryMax - collisionThreshold, |
|
particle.position.dy, |
|
); |
|
|
|
particle.velocity = Offset( |
|
-particle.velocity.dx * particle.restitution, |
|
particle.velocity.dy * (1.0 - particle.frictionCoefficient), |
|
); |
|
|
|
final contactPointOffsetY = random.nextDouble() * 0.4 - 0.2; |
|
final torque = -contactPointOffsetY * particle.velocity.dx.abs() * 0.3; |
|
particle.applyTorque(torque); |
|
} |
|
|
|
if (distanceFromTop < collisionThreshold && particle.velocity.dy < 0) { |
|
particle.position = Offset( |
|
particle.position.dx, |
|
boundaryMin + collisionThreshold, |
|
); |
|
|
|
particle.velocity = Offset( |
|
particle.velocity.dx * (1.0 - particle.frictionCoefficient), |
|
-particle.velocity.dy * particle.restitution, |
|
); |
|
|
|
final contactPointOffsetX = random.nextDouble() * 0.4 - 0.2; |
|
final torque = contactPointOffsetX * particle.velocity.dy.abs() * 0.3; |
|
particle.applyTorque(torque); |
|
} else if (distanceFromBottom < collisionThreshold && |
|
particle.velocity.dy > 0) { |
|
particle.position = Offset( |
|
particle.position.dx, |
|
boundaryMax - collisionThreshold, |
|
); |
|
|
|
particle.velocity = Offset( |
|
particle.velocity.dx * (1.0 - particle.frictionCoefficient), |
|
-particle.velocity.dy * particle.restitution, |
|
); |
|
|
|
final contactPointOffsetX = random.nextDouble() * 0.4 - 0.2; |
|
final torque = -contactPointOffsetX * particle.velocity.dy.abs() * 0.3; |
|
particle.applyTorque(torque); |
|
} |
|
} |
|
|
|
void _handleParticleCollisions() { |
|
for (int i = 0; i < _particles.length; i++) { |
|
final particleA = _particles[i]; |
|
|
|
for (int j = i + 1; j < _particles.length; j++) { |
|
final particleB = _particles[j]; |
|
|
|
final dx = particleB.position.dx - particleA.position.dx; |
|
final dy = particleB.position.dy - particleA.position.dy; |
|
final distanceSquared = dx * dx + dy * dy; |
|
|
|
final combinedRadius = |
|
(particleA.collisionRadius + particleB.collisionRadius) / 500; |
|
final minDistanceSquared = combinedRadius * combinedRadius; |
|
|
|
if (distanceSquared < minDistanceSquared) { |
|
final distance = math.sqrt(distanceSquared); |
|
|
|
final nx = dx / distance; |
|
final ny = dy / distance; |
|
|
|
final overlap = combinedRadius - distance; |
|
|
|
final totalMass = particleA.mass + particleB.mass; |
|
final particleAShare = particleB.mass / totalMass; |
|
final particleBShare = particleA.mass / totalMass; |
|
|
|
particleA.position = Offset( |
|
particleA.position.dx - nx * overlap * particleAShare, |
|
particleA.position.dy - ny * overlap * particleAShare, |
|
); |
|
|
|
particleB.position = Offset( |
|
particleB.position.dx + nx * overlap * particleBShare, |
|
particleB.position.dy + ny * overlap * particleBShare, |
|
); |
|
|
|
final rvx = particleB.velocity.dx - particleA.velocity.dx; |
|
final rvy = particleB.velocity.dy - particleA.velocity.dy; |
|
|
|
final velAlongNormal = rvx * nx + rvy * ny; |
|
|
|
if (velAlongNormal > 0) continue; |
|
|
|
final combinedRestitution = |
|
(particleA.restitution + particleB.restitution) / 2.0; |
|
|
|
final j = -(1.0 + combinedRestitution) * velAlongNormal; |
|
final impulseScalar = j / totalMass; |
|
|
|
final impulseX = nx * impulseScalar; |
|
final impulseY = ny * impulseScalar; |
|
|
|
particleA.velocity = Offset( |
|
particleA.velocity.dx - impulseX * particleB.mass, |
|
particleA.velocity.dy - impulseY * particleB.mass, |
|
); |
|
|
|
particleB.velocity = Offset( |
|
particleB.velocity.dx + impulseX * particleA.mass, |
|
particleB.velocity.dy + impulseY * particleA.mass, |
|
); |
|
|
|
final offNormalX = -ny; |
|
final offNormalY = nx; |
|
|
|
final torqueA = |
|
(offNormalX * rvx + offNormalY * rvy) * |
|
particleB.mass * |
|
0.01 / |
|
particleA.momentOfInertia; |
|
|
|
final torqueB = |
|
-(offNormalX * rvx + offNormalY * rvy) * |
|
particleA.mass * |
|
0.01 / |
|
particleB.momentOfInertia; |
|
|
|
particleA.applyTorque(torqueA); |
|
particleB.applyTorque(torqueB); |
|
} |
|
} |
|
} |
|
} |
|
|
|
@override |
|
void dispose() { |
|
_controller.dispose(); |
|
super.dispose(); |
|
} |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final size = MediaQuery.of(context).size.shortestSide; |
|
|
|
return Scaffold( |
|
backgroundColor: Colors.black, |
|
body: Center( |
|
child: OneSecondLoader( |
|
child: Stack( |
|
alignment: Alignment.center, |
|
children: [ |
|
SizedBox( |
|
width: double.infinity, |
|
height: double.infinity, |
|
child: CustomPaint(painter: BackgroundPainter()), |
|
), |
|
|
|
ClipOval( |
|
child: SizedBox( |
|
width: size * 0.75, |
|
height: size * 0.75, |
|
child: BackdropFilter( |
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), |
|
child: Container( |
|
decoration: BoxDecoration( |
|
color: Colors.lightBlue.shade200.withValues(alpha: 0.3), |
|
shape: BoxShape.circle, |
|
border: Border.all( |
|
color: Colors.white.withValues(alpha: 0.2), |
|
width: 1.5, |
|
), |
|
gradient: LinearGradient( |
|
begin: Alignment.topLeft, |
|
end: Alignment.bottomRight, |
|
colors: [ |
|
Colors.lightBlue.shade200.withValues(alpha: 0.5), |
|
Colors.lightBlue.shade200.withValues(alpha: 0.2), |
|
], |
|
), |
|
), |
|
), |
|
), |
|
), |
|
), |
|
|
|
AnimatedBuilder( |
|
animation: _controller, |
|
builder: (context, child) { |
|
final children = |
|
_particles.map((particle) => particle.widget).toList(); |
|
|
|
return SizedBox( |
|
width: size, |
|
height: size, |
|
child: KaleidoscopeWidget( |
|
particles: _particles, |
|
numSegments: _numSegments, |
|
animationValue: _controller.value, |
|
useFrostedGlass: true, |
|
children: children, |
|
), |
|
); |
|
}, |
|
), |
|
], |
|
), |
|
), |
|
), |
|
); |
|
} |
|
} |
|
|
|
class ParticleProperties { |
|
final List<Widget> widgets; |
|
final double massMultiplier; |
|
final double restitution; |
|
final double frictionCoefficient; |
|
final double momentOfInertiaMultiplier; |
|
final double dragCoefficient; |
|
|
|
ParticleProperties({ |
|
required this.widgets, |
|
required this.massMultiplier, |
|
required this.restitution, |
|
required this.frictionCoefficient, |
|
required this.momentOfInertiaMultiplier, |
|
required this.dragCoefficient, |
|
}); |
|
} |
|
|
|
class KaleidoscopeParticle { |
|
Widget widget; |
|
Offset position; |
|
Offset velocity; |
|
Offset acceleration = Offset.zero; |
|
double scale; |
|
double collisionRadius; |
|
double mass; |
|
double momentOfInertia; |
|
double restitution; |
|
double frictionCoefficient; |
|
double dragCoefficient; |
|
double angle = 0; |
|
double angularVelocity = 0; |
|
double angularAcceleration = 0; |
|
|
|
KaleidoscopeParticle({ |
|
required this.widget, |
|
required this.position, |
|
required this.velocity, |
|
required this.scale, |
|
required this.collisionRadius, |
|
required this.mass, |
|
required this.restitution, |
|
required this.frictionCoefficient, |
|
required this.momentOfInertia, |
|
required this.dragCoefficient, |
|
}); |
|
|
|
void update(double deltaTime) { |
|
velocity += acceleration.scale(deltaTime, deltaTime); |
|
|
|
position += velocity.scale(deltaTime, deltaTime); |
|
|
|
angularVelocity += angularAcceleration * deltaTime; |
|
|
|
angle += angularVelocity * deltaTime; |
|
|
|
acceleration = Offset.zero; |
|
angularAcceleration = 0; |
|
|
|
velocity = velocity.scale( |
|
math.pow(1.0 - frictionCoefficient, deltaTime).toDouble(), |
|
math.pow(1.0 - frictionCoefficient, deltaTime).toDouble(), |
|
); |
|
|
|
angularVelocity *= math.pow(0.99, deltaTime).toDouble(); |
|
} |
|
|
|
void applyForce(Offset force) { |
|
acceleration += force.scale(1.0 / mass, 1.0 / mass); |
|
} |
|
|
|
void applyTorque(double torque) { |
|
angularAcceleration += torque / momentOfInertia; |
|
} |
|
} |
|
|
|
class BackgroundPainter extends CustomPainter { |
|
final List<Color> colorPalette = [ |
|
Color(0xFF087F8C), |
|
Color(0xFF7955BF), |
|
Color(0xFFFF8B04), |
|
Color(0xFFFDD836), |
|
Color(0xFFFF2C8A), |
|
]; |
|
|
|
@override |
|
void paint(Canvas canvas, Size size) { |
|
final random = math.Random(42); |
|
for (int i = 0; i < 40; i++) { |
|
final paint = |
|
Paint() |
|
..color = colorPalette[random.nextInt(colorPalette.length)] |
|
.withValues(alpha: 0.3) |
|
..style = PaintingStyle.fill; |
|
|
|
final x = random.nextDouble() * size.width; |
|
final y = random.nextDouble() * size.height; |
|
final radius = 10 + random.nextDouble() * 50; |
|
canvas.drawCircle(Offset(x, y), radius, paint); |
|
} |
|
} |
|
|
|
@override |
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; |
|
} |
|
|
|
class KaleidoscopeWidget extends MultiChildRenderObjectWidget { |
|
final List<KaleidoscopeParticle> particles; |
|
final int numSegments; |
|
final double animationValue; |
|
final bool useFrostedGlass; |
|
|
|
const KaleidoscopeWidget({ |
|
super.key, |
|
required this.particles, |
|
required this.numSegments, |
|
required this.animationValue, |
|
this.useFrostedGlass = false, |
|
required super.children, |
|
}); |
|
|
|
@override |
|
RenderObject createRenderObject(BuildContext context) { |
|
return RenderKaleidoscope( |
|
particles: particles, |
|
numSegments: numSegments, |
|
animationValue: animationValue, |
|
useFrostedGlass: useFrostedGlass, |
|
); |
|
} |
|
|
|
@override |
|
void updateRenderObject( |
|
BuildContext context, |
|
RenderKaleidoscope renderObject, |
|
) { |
|
renderObject |
|
..particles = particles |
|
..numSegments = numSegments |
|
..animationValue = animationValue |
|
..useFrostedGlass = useFrostedGlass; |
|
} |
|
} |
|
|
|
class KaleidoscopeParentData extends ContainerBoxParentData<RenderBox> { |
|
int particleIndex = 0; |
|
} |
|
|
|
class RenderKaleidoscope extends RenderBox |
|
with |
|
ContainerRenderObjectMixin<RenderBox, KaleidoscopeParentData>, |
|
RenderBoxContainerDefaultsMixin<RenderBox, KaleidoscopeParentData> { |
|
RenderKaleidoscope({ |
|
required List<KaleidoscopeParticle> particles, |
|
required int numSegments, |
|
required double animationValue, |
|
required bool useFrostedGlass, |
|
}) : _particles = particles, |
|
_numSegments = numSegments, |
|
_animationValue = animationValue, |
|
_useFrostedGlass = useFrostedGlass; |
|
|
|
// ignore: unused_field |
|
double _radius = 0; |
|
|
|
List<KaleidoscopeParticle> get particles => _particles; |
|
set particles(List<KaleidoscopeParticle> value) { |
|
if (_particles == value) return; |
|
_particles = value; |
|
markNeedsLayout(); |
|
markNeedsPaint(); |
|
} |
|
|
|
List<KaleidoscopeParticle> _particles; |
|
|
|
int get numSegments => _numSegments; |
|
set numSegments(int value) { |
|
if (_numSegments == value) return; |
|
_numSegments = value; |
|
markNeedsLayout(); |
|
markNeedsPaint(); |
|
} |
|
|
|
int _numSegments; |
|
|
|
double get animationValue => _animationValue; |
|
set animationValue(double value) { |
|
if (_animationValue == value) return; |
|
_animationValue = value; |
|
markNeedsPaint(); |
|
} |
|
|
|
double _animationValue; |
|
|
|
bool get useFrostedGlass => _useFrostedGlass; |
|
set useFrostedGlass(bool value) { |
|
if (_useFrostedGlass == value) return; |
|
_useFrostedGlass = value; |
|
markNeedsPaint(); |
|
} |
|
|
|
bool _useFrostedGlass; |
|
|
|
@override |
|
void setupParentData(RenderBox child) { |
|
if (child.parentData is! KaleidoscopeParentData) { |
|
child.parentData = KaleidoscopeParentData(); |
|
} |
|
} |
|
|
|
@override |
|
void adoptChild(RenderObject child) { |
|
super.adoptChild(child); |
|
|
|
int index = 0; |
|
RenderBox? currentChild = firstChild; |
|
while (currentChild != null) { |
|
final KaleidoscopeParentData parentData = |
|
currentChild.parentData! as KaleidoscopeParentData; |
|
|
|
if (currentChild == child) { |
|
parentData.particleIndex = index < _particles.length ? index : 0; |
|
break; |
|
} |
|
|
|
index++; |
|
currentChild = childAfter(currentChild); |
|
} |
|
} |
|
|
|
@override |
|
void performLayout() { |
|
size = constraints.biggest; |
|
|
|
final centerX = size.width / 2; |
|
final centerY = size.height / 2; |
|
_radius = math.min(centerX, centerY) * 0.75; |
|
|
|
int index = 0; |
|
RenderBox? child = firstChild; |
|
|
|
while (child != null && index < _particles.length) { |
|
final KaleidoscopeParentData parentData = |
|
child.parentData! as KaleidoscopeParentData; |
|
|
|
parentData.particleIndex = index; |
|
|
|
child.layout( |
|
BoxConstraints(maxWidth: double.infinity, maxHeight: double.infinity), |
|
parentUsesSize: true, |
|
); |
|
|
|
child = childAfter(child); |
|
index++; |
|
} |
|
} |
|
|
|
@override |
|
void paint(PaintingContext context, Offset offset) { |
|
final Canvas canvas = context.canvas; |
|
final centerX = size.width / 2; |
|
final centerY = size.height / 2; |
|
final radius = math.min(centerX, centerY) * 0.75; |
|
|
|
_radius = radius; |
|
|
|
canvas.save(); |
|
|
|
canvas.translate(centerX + offset.dx, centerY + offset.dy); |
|
|
|
canvas.rotate(animationValue * 2 * math.pi / 2); |
|
|
|
final segmentAngle = 2 * math.pi / numSegments; |
|
|
|
void paintParticleInSegment( |
|
int particleIndex, |
|
int segmentIndex, |
|
bool mirrored, |
|
) { |
|
if (particleIndex >= _particles.length) return; |
|
|
|
final particle = _particles[particleIndex]; |
|
|
|
RenderBox? child = firstChild; |
|
KaleidoscopeParentData? particleParentData; |
|
|
|
while (child != null) { |
|
final parentData = child.parentData! as KaleidoscopeParentData; |
|
if (parentData.particleIndex == particleIndex) { |
|
particleParentData = parentData; |
|
break; |
|
} |
|
child = childAfter(child); |
|
} |
|
|
|
if (child == null || particleParentData == null) return; |
|
|
|
final particleX = particle.position.dx * radius; |
|
final particleY = particle.position.dy * radius; |
|
|
|
canvas.save(); |
|
|
|
canvas.rotate(segmentIndex * segmentAngle); |
|
|
|
if (mirrored) { |
|
canvas.scale(1, -1); |
|
} |
|
|
|
canvas.translate(particleX, particleY); |
|
|
|
canvas.rotate(particle.angle); |
|
|
|
context.paintChild( |
|
child, |
|
Offset(-child.size.width / 2, -child.size.height / 2), |
|
); |
|
|
|
canvas.restore(); |
|
} |
|
|
|
for (int segment = 0; segment < numSegments; segment++) { |
|
final segmentPath = |
|
Path() |
|
..moveTo(0, 0) |
|
..lineTo(radius, 0) |
|
..arcTo( |
|
Rect.fromCircle(center: Offset.zero, radius: radius), |
|
0, |
|
segmentAngle, |
|
false, |
|
) |
|
..close(); |
|
|
|
canvas.save(); |
|
|
|
canvas.rotate(segment * segmentAngle); |
|
|
|
canvas.clipPath(segmentPath); |
|
|
|
for (int i = 0; i < _particles.length; i++) { |
|
paintParticleInSegment(i, 0, false); |
|
paintParticleInSegment(i, 0, true); |
|
} |
|
|
|
final linePaint = |
|
Paint() |
|
..color = Colors.white.withValues(alpha: 0.3) |
|
..style = PaintingStyle.stroke |
|
..strokeWidth = 1; |
|
|
|
canvas.drawPath(segmentPath, linePaint); |
|
|
|
canvas.restore(); |
|
} |
|
|
|
final borderPaint = |
|
Paint() |
|
..color = Colors.white.withValues(alpha: 0.7) |
|
..style = PaintingStyle.stroke |
|
..strokeWidth = 3; |
|
|
|
canvas.drawCircle(Offset.zero, radius * 0.99, borderPaint); |
|
|
|
canvas.restore(); |
|
} |
|
|
|
@override |
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
|
return defaultHitTestChildren(result, position: position); |
|
} |
|
} |
|
|
|
class OneSecondLoader extends StatefulWidget { |
|
final Widget child; |
|
final Duration loadingDuration; |
|
final VoidCallback? onLoadComplete; |
|
final Color overlayColor; |
|
final Widget? loadingIndicator; |
|
|
|
const OneSecondLoader({ |
|
super.key, |
|
required this.child, |
|
this.loadingDuration = const Duration(seconds: 1), |
|
this.onLoadComplete, |
|
this.overlayColor = Colors.black, |
|
this.loadingIndicator, |
|
}); |
|
|
|
@override |
|
State<OneSecondLoader> createState() => _OneSecondLoaderState(); |
|
} |
|
|
|
class _OneSecondLoaderState extends State<OneSecondLoader> |
|
with SingleTickerProviderStateMixin { |
|
bool _showOverlay = true; |
|
late AnimationController _animationController; |
|
late Animation<double> _animation; |
|
|
|
@override |
|
void initState() { |
|
super.initState(); |
|
|
|
_animationController = AnimationController( |
|
duration: const Duration(milliseconds: 750), |
|
vsync: this, |
|
); |
|
|
|
_animation = TweenSequence<double>([ |
|
TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.5), weight: 1), |
|
TweenSequenceItem(tween: Tween<double>(begin: 1.5, end: 0.8), weight: 1), |
|
TweenSequenceItem(tween: Tween<double>(begin: 0.8, end: 1.0), weight: 1), |
|
]).animate( |
|
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), |
|
); |
|
|
|
_animationController.repeat(); |
|
|
|
_startTimer(); |
|
} |
|
|
|
@override |
|
void dispose() { |
|
_animationController.dispose(); |
|
super.dispose(); |
|
} |
|
|
|
void _startTimer() { |
|
Future.delayed(widget.loadingDuration, () { |
|
if (mounted) { |
|
setState(() { |
|
_showOverlay = false; |
|
}); |
|
if (widget.onLoadComplete != null) { |
|
widget.onLoadComplete!(); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
return Stack( |
|
children: [ |
|
widget.child, |
|
|
|
if (_showOverlay) |
|
Container( |
|
color: widget.overlayColor, |
|
child: Center( |
|
child: |
|
widget.loadingIndicator ?? |
|
AnimatedBuilder( |
|
animation: _animation, |
|
builder: (context, child) { |
|
return Transform.scale( |
|
scale: _animation.value, |
|
child: Icon( |
|
Icons.auto_awesome, |
|
color: Colors.purpleAccent, |
|
size: 48, |
|
), |
|
); |
|
}, |
|
), |
|
), |
|
), |
|
], |
|
); |
|
} |
|
} |