premier app version beta
This commit is contained in:
232
lib/features/statistics/widgets/heat_map_widget.dart
Normal file
232
lib/features/statistics/widgets/heat_map_widget.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../services/statistics_service.dart';
|
||||
|
||||
class HeatMapWidget extends StatelessWidget {
|
||||
final HeatMap heatMap;
|
||||
final double size;
|
||||
|
||||
const HeatMapWidget({
|
||||
super.key,
|
||||
required this.heatMap,
|
||||
this.size = 250,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (heatMap.zones.isEmpty || heatMap.totalShots == 0) {
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: const Center(
|
||||
child: Text('Aucune donnee'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
child: CustomPaint(
|
||||
size: Size(size, size),
|
||||
painter: _HeatMapFogPainter(heatMap: heatMap),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeatMapFogPainter extends CustomPainter {
|
||||
final HeatMap heatMap;
|
||||
|
||||
_HeatMapFogPainter({required this.heatMap});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (heatMap.zones.isEmpty) return;
|
||||
|
||||
// Draw base background (cold blue)
|
||||
final bgPaint = Paint()
|
||||
..color = const Color(0xFF1A237E).withValues(alpha: 0.3); // Dark blue base
|
||||
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), bgPaint);
|
||||
|
||||
final cellWidth = size.width / heatMap.gridSize;
|
||||
final cellHeight = size.height / heatMap.gridSize;
|
||||
|
||||
// Collect all shot positions with their intensities for fog effect
|
||||
final hotSpots = <_HotSpot>[];
|
||||
|
||||
for (int row = 0; row < heatMap.gridSize; row++) {
|
||||
for (int col = 0; col < heatMap.gridSize; col++) {
|
||||
final zone = heatMap.zones[row][col];
|
||||
if (zone.shotCount > 0) {
|
||||
hotSpots.add(_HotSpot(
|
||||
x: (col + 0.5) * cellWidth,
|
||||
y: (row + 0.5) * cellHeight,
|
||||
intensity: zone.intensity,
|
||||
shotCount: zone.shotCount,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw fog effect using radial gradients for each hot spot
|
||||
for (final spot in hotSpots) {
|
||||
_drawFogSpot(canvas, size, spot, cellWidth, cellHeight);
|
||||
}
|
||||
|
||||
// Draw target overlay (concentric circles)
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final maxRadius = size.width / 2;
|
||||
final circlePaint = Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.5)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1;
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
canvas.drawCircle(center, maxRadius * (i / 5), circlePaint);
|
||||
}
|
||||
|
||||
// Draw crosshair
|
||||
canvas.drawLine(
|
||||
Offset(center.dx, 0),
|
||||
Offset(center.dx, size.height),
|
||||
circlePaint,
|
||||
);
|
||||
canvas.drawLine(
|
||||
Offset(0, center.dy),
|
||||
Offset(size.width, center.dy),
|
||||
circlePaint,
|
||||
);
|
||||
|
||||
// Draw shot counts
|
||||
for (final spot in hotSpots) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '${spot.shotCount}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: [
|
||||
Shadow(color: Colors.black.withValues(alpha: 0.8), blurRadius: 4),
|
||||
Shadow(color: Colors.black.withValues(alpha: 0.8), blurRadius: 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(spot.x - textPainter.width / 2, spot.y - textPainter.height / 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawFogSpot(Canvas canvas, Size size, _HotSpot spot, double cellWidth, double cellHeight) {
|
||||
// Calculate fog radius based on intensity and cell size
|
||||
final baseRadius = math.max(cellWidth, cellHeight) * 1.5;
|
||||
final radius = baseRadius * (0.5 + spot.intensity * 0.5);
|
||||
|
||||
// Create gradient from hot (red/orange) to transparent
|
||||
final gradient = ui.Gradient.radial(
|
||||
Offset(spot.x, spot.y),
|
||||
radius,
|
||||
[
|
||||
_getHeatColor(spot.intensity).withValues(alpha: 0.7 * spot.intensity + 0.3),
|
||||
_getHeatColor(spot.intensity * 0.5).withValues(alpha: 0.3 * spot.intensity),
|
||||
Colors.transparent,
|
||||
],
|
||||
[0.0, 0.5, 1.0],
|
||||
);
|
||||
|
||||
final paint = Paint()
|
||||
..shader = gradient
|
||||
..blendMode = BlendMode.screen; // Additive blending for fog effect
|
||||
|
||||
canvas.drawCircle(Offset(spot.x, spot.y), radius, paint);
|
||||
|
||||
// Add a second layer for more intensity
|
||||
if (spot.intensity > 0.3) {
|
||||
final innerGradient = ui.Gradient.radial(
|
||||
Offset(spot.x, spot.y),
|
||||
radius * 0.6,
|
||||
[
|
||||
_getHeatColor(spot.intensity).withValues(alpha: 0.5 * spot.intensity),
|
||||
Colors.transparent,
|
||||
],
|
||||
[0.0, 1.0],
|
||||
);
|
||||
|
||||
final innerPaint = Paint()
|
||||
..shader = innerGradient
|
||||
..blendMode = BlendMode.screen;
|
||||
|
||||
canvas.drawCircle(Offset(spot.x, spot.y), radius * 0.6, innerPaint);
|
||||
}
|
||||
}
|
||||
|
||||
Color _getHeatColor(double intensity) {
|
||||
// Gradient from blue (cold) to red (hot)
|
||||
if (intensity <= 0) return const Color(0xFF2196F3); // Blue
|
||||
if (intensity >= 1) return const Color(0xFFFF1744); // Red
|
||||
|
||||
// Interpolate between blue -> cyan -> yellow -> orange -> red
|
||||
if (intensity < 0.25) {
|
||||
// Blue to Cyan
|
||||
return Color.lerp(
|
||||
const Color(0xFF2196F3), // Blue
|
||||
const Color(0xFF00BCD4), // Cyan
|
||||
intensity / 0.25,
|
||||
)!;
|
||||
} else if (intensity < 0.5) {
|
||||
// Cyan to Yellow
|
||||
return Color.lerp(
|
||||
const Color(0xFF00BCD4), // Cyan
|
||||
const Color(0xFFFFEB3B), // Yellow
|
||||
(intensity - 0.25) / 0.25,
|
||||
)!;
|
||||
} else if (intensity < 0.75) {
|
||||
// Yellow to Orange
|
||||
return Color.lerp(
|
||||
const Color(0xFFFFEB3B), // Yellow
|
||||
const Color(0xFFFF9800), // Orange
|
||||
(intensity - 0.5) / 0.25,
|
||||
)!;
|
||||
} else {
|
||||
// Orange to Red
|
||||
return Color.lerp(
|
||||
const Color(0xFFFF9800), // Orange
|
||||
const Color(0xFFFF1744), // Red
|
||||
(intensity - 0.75) / 0.25,
|
||||
)!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _HeatMapFogPainter oldDelegate) {
|
||||
return heatMap != oldDelegate.heatMap;
|
||||
}
|
||||
}
|
||||
|
||||
class _HotSpot {
|
||||
final double x;
|
||||
final double y;
|
||||
final double intensity;
|
||||
final int shotCount;
|
||||
|
||||
_HotSpot({
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.intensity,
|
||||
required this.shotCount,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user