220 lines
5.6 KiB
Dart
220 lines
5.6 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// ASCII 문자 분해 파티클
|
|
class AsciiParticle {
|
|
AsciiParticle({
|
|
required this.char,
|
|
required this.initialX,
|
|
required this.initialY,
|
|
required this.vx,
|
|
required this.vy,
|
|
required this.delay,
|
|
}) : x = initialX,
|
|
y = initialY,
|
|
opacity = 1.0;
|
|
|
|
final String char;
|
|
final double initialX;
|
|
final double initialY;
|
|
final double vx; // X 속도
|
|
final double vy; // Y 속도
|
|
final double delay; // 분해 시작 지연 (0.0 ~ 0.3)
|
|
|
|
double x;
|
|
double y;
|
|
double opacity;
|
|
|
|
/// 진행도에 따라 파티클 상태 업데이트
|
|
void update(double progress) {
|
|
// 지연 적용
|
|
final adjustedProgress = ((progress - delay) / (1.0 - delay)).clamp(
|
|
0.0,
|
|
1.0,
|
|
);
|
|
|
|
if (adjustedProgress <= 0) {
|
|
// 아직 분해 시작 전
|
|
x = initialX;
|
|
y = initialY;
|
|
opacity = 1.0;
|
|
return;
|
|
}
|
|
|
|
// 이징 적용 (가속)
|
|
final easedProgress = Curves.easeOutQuad.transform(adjustedProgress);
|
|
|
|
// 위치 업데이트 (초기 위치에서 이동)
|
|
x = initialX + vx * easedProgress * 3.0;
|
|
y = initialY + vy * easedProgress * 3.0;
|
|
|
|
// 중력 효과
|
|
y += easedProgress * easedProgress * 2.0;
|
|
|
|
// 페이드 아웃 (후반부에 급격히)
|
|
opacity = (1.0 - easedProgress * easedProgress).clamp(0.0, 1.0);
|
|
}
|
|
}
|
|
|
|
/// ASCII 캐릭터 분해 애니메이션 위젯
|
|
///
|
|
/// 캐릭터의 각 ASCII 문자가 파티클로 분해되어 흩어지는 효과
|
|
class AsciiDisintegrateWidget extends StatefulWidget {
|
|
const AsciiDisintegrateWidget({
|
|
super.key,
|
|
required this.characterLines,
|
|
this.charWidth = 8.0,
|
|
this.charHeight = 12.0,
|
|
this.duration = const Duration(milliseconds: 1500),
|
|
this.textColor,
|
|
this.onComplete,
|
|
});
|
|
|
|
/// ASCII 캐릭터 문자열 (줄 단위)
|
|
final List<String> characterLines;
|
|
|
|
/// 문자 너비 (픽셀)
|
|
final double charWidth;
|
|
|
|
/// 문자 높이 (픽셀)
|
|
final double charHeight;
|
|
|
|
/// 애니메이션 지속 시간
|
|
final Duration duration;
|
|
|
|
/// 텍스트 색상 (null이면 테마 색상)
|
|
final Color? textColor;
|
|
|
|
/// 완료 콜백
|
|
final VoidCallback? onComplete;
|
|
|
|
@override
|
|
State<AsciiDisintegrateWidget> createState() =>
|
|
_AsciiDisintegrateWidgetState();
|
|
}
|
|
|
|
class _AsciiDisintegrateWidgetState extends State<AsciiDisintegrateWidget>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late List<AsciiParticle> _particles;
|
|
final Random _random = Random();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initParticles();
|
|
_controller = AnimationController(duration: widget.duration, vsync: this)
|
|
..addListener(() => setState(() {}))
|
|
..addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
widget.onComplete?.call();
|
|
}
|
|
})
|
|
..forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _initParticles() {
|
|
_particles = [];
|
|
|
|
for (int y = 0; y < widget.characterLines.length; y++) {
|
|
final line = widget.characterLines[y];
|
|
for (int x = 0; x < line.length; x++) {
|
|
final char = line[x];
|
|
// 공백은 파티클로 변환하지 않음
|
|
if (char != ' ') {
|
|
_particles.add(
|
|
AsciiParticle(
|
|
char: char,
|
|
initialX: x.toDouble(),
|
|
initialY: y.toDouble(),
|
|
// 랜덤 속도 (위쪽 + 좌우로 퍼짐)
|
|
vx: (_random.nextDouble() - 0.5) * 4.0,
|
|
vy: -_random.nextDouble() * 2.0 - 0.5, // 위쪽으로
|
|
// 랜덤 지연 (안쪽에서 바깥쪽으로 분해)
|
|
delay: _random.nextDouble() * 0.3,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// 파티클 상태 업데이트
|
|
for (final particle in _particles) {
|
|
particle.update(_controller.value);
|
|
}
|
|
|
|
final textColor =
|
|
widget.textColor ?? Theme.of(context).textTheme.bodyMedium?.color;
|
|
|
|
return CustomPaint(
|
|
size: Size(
|
|
widget.characterLines.isNotEmpty
|
|
? widget.characterLines
|
|
.map((l) => l.length)
|
|
.reduce((a, b) => a > b ? a : b) *
|
|
widget.charWidth
|
|
: 0,
|
|
widget.characterLines.length * widget.charHeight,
|
|
),
|
|
painter: _DisintegratePainter(
|
|
particles: _particles,
|
|
charWidth: widget.charWidth,
|
|
charHeight: widget.charHeight,
|
|
textColor: textColor ?? Colors.white,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 분해 파티클 페인터
|
|
class _DisintegratePainter extends CustomPainter {
|
|
_DisintegratePainter({
|
|
required this.particles,
|
|
required this.charWidth,
|
|
required this.charHeight,
|
|
required this.textColor,
|
|
});
|
|
|
|
final List<AsciiParticle> particles;
|
|
final double charWidth;
|
|
final double charHeight;
|
|
final Color textColor;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
for (final particle in particles) {
|
|
if (particle.opacity <= 0) continue;
|
|
|
|
final textPainter = TextPainter(
|
|
text: TextSpan(
|
|
text: particle.char,
|
|
style: TextStyle(
|
|
fontFamily: 'JetBrainsMono',
|
|
fontSize: charHeight * 0.9,
|
|
color: textColor.withValues(alpha: particle.opacity),
|
|
),
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
)..layout();
|
|
|
|
final x = particle.x * charWidth;
|
|
final y = particle.y * charHeight;
|
|
|
|
textPainter.paint(canvas, Offset(x, y));
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _DisintegratePainter oldDelegate) => true;
|
|
}
|