import 'dart:math'; import 'package:flutter/material.dart'; /// Puzzle en pièces éparpillées, déplacées par glisser-déposer avec /// accrochage ("snap") à la bonne position. Équivalent de `PuzzleGame.tsx` /// (web) — la variante non-sliding (`isSlidingPuzzle == false`). class GuidedPathPuzzle extends StatefulWidget { final String imageUrl; final int rows; final int cols; final Color accentColor; final bool isGame; final VoidCallback onSolved; const GuidedPathPuzzle({ Key? key, required this.imageUrl, required this.rows, required this.cols, required this.accentColor, required this.isGame, required this.onSolved, }) : super(key: key); @override State createState() => _GuidedPathPuzzleState(); } class _Piece { final int row; final int col; Offset pos; // position courante (px depuis le coin haut-gauche du board) bool placed = false; double z; _Piece({required this.row, required this.col, required this.pos, this.z = 0}); int get id => row * 1000 + col; } class _GuidedPathPuzzleState extends State { List<_Piece> _pieces = []; bool _showHint = false; bool _solved = false; double _topZ = 1; int? _draggingId; @override void initState() { super.initState(); } void _initPieces(double board) { if (_pieces.isNotEmpty) return; final pw = board / widget.cols; final ph = board / widget.rows; final rand = Random(); final list = <_Piece>[]; var z = 1.0; for (int r = 0; r < widget.rows; r++) { for (int c = 0; c < widget.cols; c++) { list.add(_Piece( row: r, col: c, pos: Offset(rand.nextDouble() * (board - pw), rand.nextDouble() * (board - ph)), z: z++, )); } } _pieces = list; _topZ = z; } void _onPanStart(_Piece piece) { if (piece.placed || _solved) return; setState(() { _draggingId = piece.id; piece.z = _topZ++; }); } void _onPanUpdate(_Piece piece, DragUpdateDetails details, double board) { if (piece.placed || _solved) return; setState(() { final pw = board / widget.cols; final ph = board / widget.rows; piece.pos = Offset( (piece.pos.dx + details.delta.dx).clamp(0.0, board - pw), (piece.pos.dy + details.delta.dy).clamp(0.0, board - ph), ); }); } void _onPanEnd(_Piece piece, double board) { if (piece.placed || _solved) return; final pw = board / widget.cols; final ph = board / widget.rows; final targetX = piece.col * pw; final targetY = piece.row * ph; final dx = piece.pos.dx - targetX; final dy = piece.pos.dy - targetY; final threshold = min(pw, ph) * 0.35; setState(() { _draggingId = null; if (dx.abs() < threshold && dy.abs() < threshold) { piece.pos = Offset(targetX, targetY); piece.placed = true; piece.z = 0; } if (_pieces.every((p) => p.placed)) { _solved = true; Future.delayed(const Duration(milliseconds: 400), () { if (mounted) widget.onSolved(); }); } }); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final board = constraints.maxWidth.isFinite ? constraints.maxWidth : MediaQuery.of(context).size.width - 64; _initPieces(board); final pw = board / widget.cols; final ph = board / widget.rows; final sorted = [..._pieces]..sort((a, b) => a.z.compareTo(b.z)); return SizedBox( width: board, height: board, child: Stack( clipBehavior: Clip.none, children: [ // Cadre du plateau (toujours visible) Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black.withOpacity(0.18), width: 1.5), color: Colors.white.withOpacity(0.4), ), ), if (_showHint && !_solved) Positioned.fill( child: Opacity( opacity: 0.55, child: Image.network(widget.imageUrl, fit: BoxFit.cover), ), ), for (final piece in sorted) Positioned( left: piece.pos.dx, top: piece.pos.dy, width: pw, height: ph, child: GestureDetector( onPanStart: (_) => _onPanStart(piece), onPanUpdate: (d) => _onPanUpdate(piece, d, board), onPanEnd: (_) => _onPanEnd(piece, board), child: Container( margin: const EdgeInsets.all(1), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.black.withOpacity(0.15)), boxShadow: piece.placed ? [] : [ BoxShadow( color: Colors.black.withOpacity( _draggingId == piece.id ? 0.35 : 0.2), blurRadius: _draggingId == piece.id ? 12 : 6, ), ], ), transform: _draggingId == piece.id ? (Matrix4.identity()..scaleByDouble(1.06, 1.06, 1.06, 1.0)) : Matrix4.identity(), child: ClipRRect( borderRadius: BorderRadius.circular(6), child: OverflowBox( maxWidth: board, maxHeight: board, alignment: Alignment( widget.cols == 1 ? 0.0 : -1.0 + (piece.col * 2 / (widget.cols - 1)), widget.rows == 1 ? 0.0 : -1.0 + (piece.row * 2 / (widget.rows - 1)), ), child: SizedBox( width: board, height: board, child: Image.network(widget.imageUrl, fit: BoxFit.cover), ), ), ), ), ), ), if (_solved) Positioned.fill( child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.35), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: const Text( 'Puzzle résolu !', style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600), ), ), ), if (!_solved) Positioned( right: 8, bottom: 8, child: GestureDetector( onTap: () => setState(() => _showHint = !_showHint), child: Container( width: 36, height: 36, decoration: BoxDecoration( shape: BoxShape.circle, color: _showHint ? widget.accentColor : (widget.isGame ? Colors.white.withOpacity(0.1) : Colors.white), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 8)], ), child: Icon( Icons.remove_red_eye, size: 18, color: _showHint ? (widget.isGame ? const Color(0xFF1a1305) : Colors.white) : (widget.isGame ? const Color(0xFFD4AF37) : const Color(0xFF54636E)), ), ), ), ), ], ), ); }, ); } }