mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_path_puzzle.dart
2026-07-17 15:23:12 +02:00

247 lines
8.3 KiB
Dart

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<GuidedPathPuzzle> 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<GuidedPathPuzzle> {
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)),
),
),
),
),
],
),
);
},
);
}
}