247 lines
8.3 KiB
Dart
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)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|