174 lines
4.3 KiB
Dart
174 lines
4.3 KiB
Dart
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<EffectSpriteWidget>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
final List<SpriteEffect> _effects = [];
|
|
final Map<String, ui.Image> _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<void> 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<int> bytes = data.buffer.asUint8List();
|
|
final Completer<ui.Image> 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<SpriteEffect> 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
|
|
}
|
|
}
|