feat(ui): 화면 및 공통 위젯 개선
- FrontScreen 개선 - GamePlayScreen, GameSessionController 업데이트 - ArenaBattleScreen, NewCharacterScreen 정리 - AsciiDisintegrateWidget 추가
This commit is contained in:
@@ -50,6 +50,8 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
late final SettingsRepository _settingsRepository;
|
late final SettingsRepository _settingsRepository;
|
||||||
late final AudioService _audioService;
|
late final AudioService _audioService;
|
||||||
late final HallOfFameStorage _hallOfFameStorage;
|
late final HallOfFameStorage _hallOfFameStorage;
|
||||||
|
final RouteObserver<ModalRoute<void>> _routeObserver =
|
||||||
|
RouteObserver<ModalRoute<void>>();
|
||||||
bool _isCheckingSave = true;
|
bool _isCheckingSave = true;
|
||||||
bool _hasSave = false;
|
bool _hasSave = false;
|
||||||
SavedGamePreview? _savedGamePreview;
|
SavedGamePreview? _savedGamePreview;
|
||||||
@@ -437,6 +439,7 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
theme: _lightTheme,
|
theme: _lightTheme,
|
||||||
darkTheme: _darkTheme,
|
darkTheme: _darkTheme,
|
||||||
themeMode: _themeMode,
|
themeMode: _themeMode,
|
||||||
|
navigatorObservers: [_routeObserver],
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
// 현재 로케일을 게임 텍스트 l10n 시스템에 동기화
|
// 현재 로케일을 게임 텍스트 l10n 시스템에 동기화
|
||||||
final locale = Localizations.localeOf(context);
|
final locale = Localizations.localeOf(context);
|
||||||
@@ -465,6 +468,11 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
hasSaveFile: _hasSave,
|
hasSaveFile: _hasSave,
|
||||||
savedGamePreview: _savedGamePreview,
|
savedGamePreview: _savedGamePreview,
|
||||||
hallOfFameCount: _hallOfFame.count,
|
hallOfFameCount: _hallOfFame.count,
|
||||||
|
routeObserver: _routeObserver,
|
||||||
|
onRefresh: () {
|
||||||
|
_checkForExistingSave();
|
||||||
|
_loadHallOfFame();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,8 +488,9 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
// 새 게임 후 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생)
|
// 새 게임 후 돌아오면 세이브 정보 및 명예의 전당 갱신
|
||||||
_checkForExistingSave();
|
_checkForExistingSave();
|
||||||
|
_loadHallOfFame();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,8 +569,9 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
// 게임에서 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생)
|
// 게임에서 돌아오면 세이브 정보 및 명예의 전당 갱신
|
||||||
_checkForExistingSave();
|
_checkForExistingSave();
|
||||||
|
_loadHallOfFame();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,7 +584,8 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
// 명예의 전당에서 돌아오면 타이틀 BGM 재생
|
// 명예의 전당에서 돌아오면 명예의 전당 갱신 및 타이틀 BGM 재생
|
||||||
|
_loadHallOfFame();
|
||||||
_audioService.playBgm('title');
|
_audioService.playBgm('title');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
|||||||
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
|
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
|
||||||
import 'package:asciineverdie/src/features/arena/widgets/arena_combat_log.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_combat_log.dart';
|
||||||
import 'package:asciineverdie/src/features/arena/widgets/arena_result_panel.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_result_panel.dart';
|
||||||
import 'package:asciineverdie/src/features/arena/widgets/ascii_disintegrate_widget.dart';
|
import 'package:asciineverdie/src/shared/widgets/ascii_disintegrate_widget.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
||||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'package:asciineverdie/src/features/front/widgets/hero_vs_boss_animation.
|
|||||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||||
|
|
||||||
class FrontScreen extends StatelessWidget {
|
class FrontScreen extends StatefulWidget {
|
||||||
const FrontScreen({
|
const FrontScreen({
|
||||||
super.key,
|
super.key,
|
||||||
this.onNewCharacter,
|
this.onNewCharacter,
|
||||||
@@ -20,6 +20,8 @@ class FrontScreen extends StatelessWidget {
|
|||||||
this.hasSaveFile = false,
|
this.hasSaveFile = false,
|
||||||
this.savedGamePreview,
|
this.savedGamePreview,
|
||||||
this.hallOfFameCount = 0,
|
this.hallOfFameCount = 0,
|
||||||
|
this.routeObserver,
|
||||||
|
this.onRefresh,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// "New character" 버튼 클릭 시 호출
|
/// "New character" 버튼 클릭 시 호출
|
||||||
@@ -43,12 +45,45 @@ class FrontScreen extends StatelessWidget {
|
|||||||
/// 명예의 전당 캐릭터 수 (아레나 활성화 조건: 2명 이상)
|
/// 명예의 전당 캐릭터 수 (아레나 활성화 조건: 2명 이상)
|
||||||
final int hallOfFameCount;
|
final int hallOfFameCount;
|
||||||
|
|
||||||
|
/// RouteObserver (화면 복귀 시 갱신용)
|
||||||
|
final RouteObserver<ModalRoute<void>>? routeObserver;
|
||||||
|
|
||||||
|
/// 화면 복귀 시 호출할 콜백
|
||||||
|
final VoidCallback? onRefresh;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FrontScreen> createState() => _FrontScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FrontScreenState extends State<FrontScreen> with RouteAware {
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
// RouteObserver 구독
|
||||||
|
final route = ModalRoute.of(context);
|
||||||
|
if (route != null) {
|
||||||
|
widget.routeObserver?.subscribe(this, route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.routeObserver?.unsubscribe(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPopNext() {
|
||||||
|
// 다른 화면에서 돌아왔을 때 갱신
|
||||||
|
widget.onRefresh?.call();
|
||||||
|
}
|
||||||
|
|
||||||
/// 새 캐릭터 생성 시 세이브 파일 존재하면 경고 표시
|
/// 새 캐릭터 생성 시 세이브 파일 존재하면 경고 표시
|
||||||
void _handleNewCharacter(BuildContext context) {
|
void _handleNewCharacter(BuildContext context) {
|
||||||
if (hasSaveFile) {
|
if (widget.hasSaveFile) {
|
||||||
_showDeleteWarningDialog(context);
|
_showDeleteWarningDialog(context);
|
||||||
} else {
|
} else {
|
||||||
onNewCharacter?.call(context);
|
widget.onNewCharacter?.call(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +102,7 @@ class FrontScreen extends StatelessWidget {
|
|||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
onNewCharacter?.call(context);
|
widget.onNewCharacter?.call(context);
|
||||||
},
|
},
|
||||||
child: Text(game_l10n.buttonConfirm),
|
child: Text(game_l10n.buttonConfirm),
|
||||||
),
|
),
|
||||||
@@ -98,21 +133,21 @@ class FrontScreen extends StatelessWidget {
|
|||||||
const _AnimationPanel(),
|
const _AnimationPanel(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_ActionButtons(
|
_ActionButtons(
|
||||||
onNewCharacter: onNewCharacter != null
|
onNewCharacter: widget.onNewCharacter != null
|
||||||
? () => _handleNewCharacter(context)
|
? () => _handleNewCharacter(context)
|
||||||
: null,
|
: null,
|
||||||
onLoadSave: onLoadSave != null
|
onLoadSave: widget.onLoadSave != null
|
||||||
? () => onLoadSave!(context)
|
? () => widget.onLoadSave!(context)
|
||||||
: null,
|
: null,
|
||||||
onHallOfFame: onHallOfFame != null
|
onHallOfFame: widget.onHallOfFame != null
|
||||||
? () => onHallOfFame!(context)
|
? () => widget.onHallOfFame!(context)
|
||||||
: null,
|
: null,
|
||||||
onLocalArena:
|
onLocalArena: widget.onLocalArena != null &&
|
||||||
onLocalArena != null && hallOfFameCount >= 2
|
widget.hallOfFameCount >= 2
|
||||||
? () => onLocalArena!(context)
|
? () => widget.onLocalArena!(context)
|
||||||
: null,
|
: null,
|
||||||
savedGamePreview: savedGamePreview,
|
savedGamePreview: widget.savedGamePreview,
|
||||||
hallOfFameCount: hallOfFameCount,
|
hallOfFameCount: widget.hallOfFameCount,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -262,16 +297,14 @@ class _ActionButtons extends StatelessWidget {
|
|||||||
onPressed: onHallOfFame,
|
onPressed: onHallOfFame,
|
||||||
isPrimary: false,
|
isPrimary: false,
|
||||||
),
|
),
|
||||||
// 로컬 아레나 (2명 이상일 때만 활성화)
|
// 로컬 아레나 (항상 표시, 2명 이상일 때만 활성화)
|
||||||
if (hallOfFameCount >= 2) ...[
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
RetroTextButton(
|
||||||
RetroTextButton(
|
text: game_l10n.uiLocalArena,
|
||||||
text: game_l10n.uiLocalArena,
|
icon: Icons.sports_kabaddi,
|
||||||
icon: Icons.sports_kabaddi,
|
onPressed: hallOfFameCount >= 2 ? onLocalArena : null,
|
||||||
onPressed: onLocalArena,
|
isPrimary: false,
|
||||||
isPrimary: false,
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1181,6 +1181,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
state.progress.currentCombat?.monsterStats.hpCurrent,
|
state.progress.currentCombat?.monsterStats.hpCurrent,
|
||||||
monsterHpMax: state.progress.currentCombat?.monsterStats.hpMax,
|
monsterHpMax: state.progress.currentCombat?.monsterStats.hpMax,
|
||||||
monsterName: state.progress.currentCombat?.monsterStats.name,
|
monsterName: state.progress.currentCombat?.monsterStats.name,
|
||||||
|
monsterLevel: state.progress.currentCombat?.monsterStats.level,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Experience 바
|
// Experience 바
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
if (isNewGame) {
|
if (isNewGame) {
|
||||||
_sessionStats = SessionStatistics.empty();
|
_sessionStats = SessionStatistics.empty();
|
||||||
await _statisticsStorage.recordGameStart();
|
await _statisticsStorage.recordGameStart();
|
||||||
|
} else {
|
||||||
|
// 게임 로드 시 저장된 사망 횟수 복원
|
||||||
|
_sessionStats = _sessionStats.copyWith(
|
||||||
|
deathCount: state.progress.deathCount,
|
||||||
|
questsCompleted: state.progress.questCount,
|
||||||
|
monstersKilled: state.progress.monstersKilled,
|
||||||
|
playTimeMs: state.skillSystem.elapsedMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_initPreviousValues(state);
|
_initPreviousValues(state);
|
||||||
|
|
||||||
@@ -341,7 +349,7 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
|
|
||||||
final entry = HallOfFameEntry.fromGameState(
|
final entry = HallOfFameEntry.fromGameState(
|
||||||
state: _state!,
|
state: _state!,
|
||||||
totalDeaths: _sessionStats.deathCount,
|
totalDeaths: _state!.progress.deathCount, // GameState에 저장된 값 사용
|
||||||
monstersKilled: _state!.progress.monstersKilled,
|
monstersKilled: _state!.progress.monstersKilled,
|
||||||
combatStats: combatStats,
|
combatStats: combatStats,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
seed: gameSeed,
|
seed: gameSeed,
|
||||||
traits: traits,
|
traits: traits,
|
||||||
stats: finalStats,
|
stats: finalStats,
|
||||||
inventory: const Inventory(gold: 0, items: []),
|
inventory: Inventory.empty(),
|
||||||
equipment: Equipment.empty(),
|
equipment: Equipment.empty(),
|
||||||
skillBook: SkillBook.empty(),
|
skillBook: SkillBook.empty(),
|
||||||
progress: ProgressState.empty(),
|
progress: ProgressState.empty(),
|
||||||
|
|||||||
219
lib/src/shared/widgets/ascii_disintegrate_widget.dart
Normal file
219
lib/src/shared/widgets/ascii_disintegrate_widget.dart
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user