import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class SpriteEffect { final Offset position; final String assetPath; final int frameCount; final double tileWidth; final double tileHeight; final double scale; ui.Image? image; int currentFrame = 0; bool isFinished = false; SpriteEffect({ required this.position, required this.assetPath, required this.frameCount, this.tileWidth = 100.0, this.tileHeight = 100.0, this.scale = 2.0, }); } class EffectSpriteWidget extends StatefulWidget { const EffectSpriteWidget({super.key}); @override EffectSpriteWidgetState createState() => EffectSpriteWidgetState(); } class EffectSpriteWidgetState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; final List _effects = []; final Map _imageCache = {}; @override void initState() { super.initState(); // Approximately 10 FPS (100ms per frame) _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 100), ); _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _updateFrames(); if (_effects.isNotEmpty) { _controller.forward(from: 0); } } }); } @override void dispose() { _controller.dispose(); super.dispose(); } Future playEffect({ required Offset position, required String assetPath, required int frameCount, double tileWidth = 100.0, double tileHeight = 100.0, double scale = 2.0, }) async { final effect = SpriteEffect( position: position, assetPath: assetPath, frameCount: frameCount, tileWidth: tileWidth, tileHeight: tileHeight, scale: scale, ); // Preload image if not cached if (!_imageCache.containsKey(assetPath)) { try { final ByteData data = await rootBundle.load(assetPath); final List bytes = data.buffer.asUint8List(); final Completer completer = Completer(); ui.decodeImageFromList(Uint8List.fromList(bytes), (ui.Image img) { completer.complete(img); }); _imageCache[assetPath] = await completer.future; } catch (e) { debugPrint('Failed to load effect image $assetPath: $e'); return; } } effect.image = _imageCache[assetPath]; setState(() { _effects.add(effect); if (!_controller.isAnimating) { _controller.forward(from: 0); } }); } void _updateFrames() { if (_effects.isEmpty) return; setState(() { for (var i = _effects.length - 1; i >= 0; i--) { final effect = _effects[i]; effect.currentFrame++; if (effect.currentFrame >= effect.frameCount) { effect.isFinished = true; _effects.removeAt(i); } } }); } @override Widget build(BuildContext context) { if (_effects.isEmpty) return const SizedBox.shrink(); return IgnorePointer( child: CustomPaint( size: Size.infinite, painter: MultiSpriteEffectPainter(effects: _effects), ), ); } } class MultiSpriteEffectPainter extends CustomPainter { final List effects; MultiSpriteEffectPainter({required this.effects}); @override void paint(Canvas canvas, Size size) { for (final effect in effects) { if (effect.image == null) continue; final double srcX = effect.currentFrame * effect.tileWidth; final double srcY = 0.0; final Rect src = Rect.fromLTWH(srcX, srcY, effect.tileWidth, effect.tileHeight); final double drawWidth = effect.tileWidth * effect.scale; final double drawHeight = effect.tileHeight * effect.scale; // Center the effect on the position final Rect dst = Rect.fromLTWH( effect.position.dx - drawWidth / 2, effect.position.dy - drawHeight / 2, drawWidth, drawHeight, ); canvas.drawImageRect( effect.image!, src, dst, Paint()..filterQuality = FilterQuality.none, ); } } @override bool shouldRepaint(covariant MultiSpriteEffectPainter oldDelegate) { return true; // Repaint constantly while animating } }