import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// Mode d’extension du fond pastel. enum CarteStretch { /// 9 zones (bulles / cartes content). both, /// Uniquement horizontal — bouts en demi-cercle (bandeau couple). horizontal, } /// Fond pastel extensible (capacité par défaut des « cartes » UI). /// /// Coins / bouts à taille logique fixe ; le reste s’étire selon [stretch]. class Carte extends StatefulWidget { final String assetPath; final CarteStretch stretch; final Rect centerSlice; final double cornerWidth; final double cornerHeight; final Widget? child; final EdgeInsetsGeometry? padding; final BoxConstraints? constraints; final double? width; final double? height; /// Carte / bulle content (`card_*_h.png`, ~1024×512). const Carte({ super.key, required this.assetPath, this.child, this.padding, this.constraints, this.width, this.height, this.stretch = CarteStretch.both, this.centerSlice = const Rect.fromLTRB(160, 90, 864, 422), this.cornerWidth = 56, this.cornerHeight = 40, }); /// Bandeau couple enfant / nounou (`bandeau_*.png`, ~1024×160). /// Étirement horizontal seul pour garder les bouts ronds. const Carte.bandeau({ super.key, required this.assetPath, this.child, this.padding, this.constraints, this.width, this.height, }) : stretch = CarteStretch.horizontal, // Demi-cercle source ≈ moitié de la hauteur (160/2 = 80). centerSlice = const Rect.fromLTRB(80, 0, 944, 160), cornerWidth = 45, cornerHeight = 45; @override State createState() => _CarteState(); } class _CarteImageCache { static final Map> _futures = {}; static Future load(String path) { return _futures.putIfAbsent(path, () async { final data = await rootBundle.load(path); final codec = await ui.instantiateImageCodec( data.buffer.asUint8List(), ); final frame = await codec.getNextFrame(); return frame.image; }); } } class _CarteState extends State { ui.Image? _image; String? _loadedPath; @override void initState() { super.initState(); _load(); } @override void didUpdateWidget(covariant Carte oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.assetPath != widget.assetPath) { _load(); } } Future _load() async { final path = widget.assetPath; final image = await _CarteImageCache.load(path); if (!mounted) return; setState(() { _image = image; _loadedPath = path; }); } @override Widget build(BuildContext context) { final img = _image; final ready = img != null && _loadedPath == widget.assetPath; return SizedBox( width: widget.width, height: widget.height, child: Container( constraints: widget.constraints, child: CustomPaint( painter: ready ? _CartePainter( image: img!, stretch: widget.stretch, centerSlice: widget.centerSlice, cornerWidth: widget.cornerWidth, cornerHeight: widget.cornerHeight, ) : null, child: Padding( padding: widget.padding ?? EdgeInsets.zero, child: widget.child, ), ), ), ); } } class _CartePainter extends CustomPainter { final ui.Image image; final CarteStretch stretch; final Rect centerSlice; final double cornerWidth; final double cornerHeight; _CartePainter({ required this.image, required this.stretch, required this.centerSlice, required this.cornerWidth, required this.cornerHeight, }); @override void paint(Canvas canvas, Size size) { if (size.width <= 0 || size.height <= 0) return; final paint = Paint()..filterQuality = FilterQuality.high; final srcW = image.width.toDouble(); final srcH = image.height.toDouble(); void draw(Rect src, Rect dst) { if (src.width <= 0 || src.height <= 0) return; if (dst.width <= 0 || dst.height <= 0) return; canvas.drawImageRect(image, src, dst, paint); } if (stretch == CarteStretch.horizontal) { // 3 bandes pleine hauteur : bout | milieu | bout (pilule). // Largeur du bout = moitié de la hauteur → demi-cercle propre. final tip = (size.height / 2).clamp(1.0, size.width / 2); final srcTip = centerSlice.left.clamp(1.0, srcW / 2); final midW = (size.width - tip * 2).clamp(0.0, size.width); draw( Rect.fromLTWH(0, 0, srcTip, srcH), Rect.fromLTWH(0, 0, tip, size.height), ); draw( Rect.fromLTWH(srcTip, 0, (srcW - srcTip * 2).clamp(1.0, srcW), srcH), Rect.fromLTWH(tip, 0, midW, size.height), ); draw( Rect.fromLTWH(srcW - srcTip, 0, srcTip, srcH), Rect.fromLTWH(tip + midW, 0, tip, size.height), ); return; } // 9 zones (bulles). final srcLeft = centerSlice.left.clamp(0.0, srcW); final srcTop = centerSlice.top.clamp(0.0, srcH); final srcRight = centerSlice.right.clamp(0.0, srcW); final srcBottom = centerSlice.bottom.clamp(0.0, srcH); final srcLeftW = srcLeft; final srcTopH = srcTop; final srcRightW = srcW - srcRight; final srcBottomH = srcH - srcBottom; final srcCenterW = (srcRight - srcLeft).clamp(1.0, srcW); final srcCenterH = (srcBottom - srcTop).clamp(1.0, srcH); final dstLeftW = cornerWidth.clamp(1.0, size.width / 2); final dstRightW = cornerWidth.clamp(1.0, size.width / 2); final dstTopH = cornerHeight.clamp(1.0, size.height / 2); final dstBottomH = cornerHeight.clamp(1.0, size.height / 2); final dstCenterW = (size.width - dstLeftW - dstRightW).clamp(0.0, size.width); final dstCenterH = (size.height - dstTopH - dstBottomH).clamp(0.0, size.height); // Haut draw(Rect.fromLTWH(0, 0, srcLeftW, srcTopH), Rect.fromLTWH(0, 0, dstLeftW, dstTopH)); draw(Rect.fromLTWH(srcLeft, 0, srcCenterW, srcTopH), Rect.fromLTWH(dstLeftW, 0, dstCenterW, dstTopH)); draw(Rect.fromLTWH(srcRight, 0, srcRightW, srcTopH), Rect.fromLTWH(dstLeftW + dstCenterW, 0, dstRightW, dstTopH)); // Milieu draw(Rect.fromLTWH(0, srcTop, srcLeftW, srcCenterH), Rect.fromLTWH(0, dstTopH, dstLeftW, dstCenterH)); draw(Rect.fromLTWH(srcLeft, srcTop, srcCenterW, srcCenterH), Rect.fromLTWH(dstLeftW, dstTopH, dstCenterW, dstCenterH)); draw(Rect.fromLTWH(srcRight, srcTop, srcRightW, srcCenterH), Rect.fromLTWH(dstLeftW + dstCenterW, dstTopH, dstRightW, dstCenterH)); // Bas draw(Rect.fromLTWH(0, srcBottom, srcLeftW, srcBottomH), Rect.fromLTWH(0, dstTopH + dstCenterH, dstLeftW, dstBottomH)); draw(Rect.fromLTWH(srcLeft, srcBottom, srcCenterW, srcBottomH), Rect.fromLTWH(dstLeftW, dstTopH + dstCenterH, dstCenterW, dstBottomH)); draw( Rect.fromLTWH(srcRight, srcBottom, srcRightW, srcBottomH), Rect.fromLTWH( dstLeftW + dstCenterW, dstTopH + dstCenterH, dstRightW, dstBottomH, ), ); } @override bool shouldRepaint(covariant _CartePainter oldDelegate) { return oldDelegate.image != image || oldDelegate.stretch != stretch || oldDelegate.centerSlice != centerSlice || oldDelegate.cornerWidth != cornerWidth || oldDelegate.cornerHeight != cornerHeight; } }