233 lines
6.4 KiB
Dart
233 lines
6.4 KiB
Dart
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,
|
|
});
|
|
}
|