feat(animation): 레이어 투명도 및 이펙트 5줄 확장

- AsciiLayer에 opacity 필드 추가
- AsciiCanvasPainter에서 레이어 투명도 렌더링 지원
- 배경 레이어 50% 투명으로 캐릭터 부각
- 모든 무기 이펙트 3줄→5줄로 확장
- 몬스터 공격 이펙트 5줄로 확장
This commit is contained in:
JiWoong Sul
2025-12-27 21:58:45 +09:00
parent 5fa58695ec
commit bfeb58ff29
5 changed files with 83 additions and 72 deletions

View File

@@ -12,12 +12,14 @@ class _ParagraphCacheKey {
required this.color, required this.color,
required this.fontSize, required this.fontSize,
required this.cellWidth, required this.cellWidth,
required this.opacity,
}); });
final String char; final String char;
final AsciiCellColor color; final AsciiCellColor color;
final double fontSize; final double fontSize;
final double cellWidth; final double cellWidth;
final double opacity;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -26,10 +28,11 @@ class _ParagraphCacheKey {
char == other.char && char == other.char &&
color == other.color && color == other.color &&
fontSize == other.fontSize && fontSize == other.fontSize &&
cellWidth == other.cellWidth; cellWidth == other.cellWidth &&
opacity == other.opacity;
@override @override
int get hashCode => Object.hash(char, color, fontSize, cellWidth); int get hashCode => Object.hash(char, color, fontSize, cellWidth, opacity);
} }
/// ASCII Canvas 페인터 (CustomPainter 구현) /// ASCII Canvas 페인터 (CustomPainter 구현)
@@ -124,7 +127,8 @@ class AsciiCanvasPainter extends CustomPainter {
final x = gridX * cellWidth; final x = gridX * cellWidth;
final y = gridY * cellHeight; final y = gridY * cellHeight;
_drawCell(canvas, cell, x, y, cellWidth, cellHeight, fontSize); // 레이어 투명도를 셀 렌더링에 전달
_drawCell(canvas, cell, x, y, cellWidth, cellHeight, fontSize, layer.opacity);
} }
} }
} }
@@ -138,20 +142,22 @@ class AsciiCanvasPainter extends CustomPainter {
double cellWidth, double cellWidth,
double cellHeight, double cellHeight,
double fontSize, double fontSize,
double opacity,
) { ) {
final cacheKey = _ParagraphCacheKey( final cacheKey = _ParagraphCacheKey(
char: cell.char, char: cell.char,
color: cell.color, color: cell.color,
fontSize: fontSize, fontSize: fontSize,
cellWidth: cellWidth, cellWidth: cellWidth,
opacity: opacity,
); );
// 캐시 히트 확인 // 캐시 히트 확인
var paragraph = _paragraphCache[cacheKey]; var paragraph = _paragraphCache[cacheKey];
if (paragraph == null) { if (paragraph == null) {
// 캐시 미스: 새 Paragraph 생성 // 캐시 미스: 새 Paragraph 생성 (opacity 적용)
final color = _getColor(cell.color); final color = _getColor(cell.color).withValues(alpha: opacity);
final paragraphBuilder = ui.ParagraphBuilder( final paragraphBuilder = ui.ParagraphBuilder(
ui.ParagraphStyle( ui.ParagraphStyle(

View File

@@ -9,6 +9,7 @@ class AsciiLayer {
this.zIndex = 0, this.zIndex = 0,
this.offsetX = 0, this.offsetX = 0,
this.offsetY = 0, this.offsetY = 0,
this.opacity = 1.0,
}); });
/// 2D 셀 배열 [row][column] /// 2D 셀 배열 [row][column]
@@ -23,6 +24,9 @@ class AsciiLayer {
/// Y 오프셋 /// Y 오프셋
final int offsetY; final int offsetY;
/// 레이어 투명도 (0.0 ~ 1.0, 배경 레이어 등에서 사용)
final double opacity;
/// 레이어 높이 (줄 수) /// 레이어 높이 (줄 수)
int get height => cells.length; int get height => cells.length;

View File

@@ -99,7 +99,8 @@ class CanvasBattleComposer {
} }
} }
return AsciiLayer(cells: cells, zIndex: 0); // 배경 레이어는 50% 투명으로 캐릭터 부각
return AsciiLayer(cells: cells, zIndex: 0, opacity: 0.5);
} }
/// 캐릭터 레이어 생성 (z=1) /// 캐릭터 레이어 생성 (z=1)
@@ -1107,24 +1108,24 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
} }
// ============================================================================ // ============================================================================
// 몬스터 공격 이펙트 (← 방향, Phase 8) // 몬스터 공격 이펙트 (← 방향, Phase 8) - 5줄
// ============================================================================ // ============================================================================
/// 몬스터 공격 준비 프레임 /// 몬스터 공격 준비 프레임 (5줄)
const _monsterPrepareFrames = <List<String>>[ const _monsterPrepareFrames = <List<String>>[
[r' ', r' <', r' '], [r' ', r' ', r' < ', r' ', r' '],
[r' ', r' <<', r' '], [r' ', r' _ ', r' << ', r' - ', r' '],
]; ];
/// 몬스터 공격 프레임 /// 몬스터 공격 프레임 (5줄)
const _monsterAttackFrames = <List<String>>[ const _monsterAttackFrames = <List<String>>[
[r' ', r' <-- ', r' '], [r' ', r' __ ', r' <-- ', r' -- ', r' '],
[r' ', r' <--- ', r' '], [r' ', r' ___ ', r' <--- ', r' --- ', r' '],
[r' ', r' <-----', r' '], [r' ', r' ____ ', r' <----- ', r' ---- ', r' '],
]; ];
/// 몬스터 히트 프레임 /// 몬스터 히트 프레임 (5줄)
const _monsterHitFrames = <List<String>>[ const _monsterHitFrames = <List<String>>[
[r' *SLASH*', r' <-----', r' '], [r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '],
[r'*ATTACK*', r' <----', r' '], [r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '],
]; ];

View File

@@ -47,95 +47,95 @@ WeaponEffect getWeaponEffect(WeaponCategory category) {
} }
// ============================================================================ // ============================================================================
// 둔기류 - 휘두르기 (3줄) // 둔기류 - 휘두르기 (5줄)
// ============================================================================ // ============================================================================
const _bluntEffect = WeaponEffect( const _bluntEffect = WeaponEffect(
prepareFrames: [ prepareFrames: [
[r' _', r' /', r' /'], [r' ', r' _ ', r' / ', r' / ', r' / '],
[r' _/', r' / ', r' / '], [r' ', r' _/ ', r' / ', r' / ', r' / '],
], ],
attackFrames: [ attackFrames: [
[r' _/ ', r' / ', r'/ '], [r' ', r' _/ ', r' / ', r' / ', r' / '],
[r' /__ ', r'/ ', r' '], [r' ', r' _/ ', r' / ', r' /________', r'/ '],
[r'/__ ', r' ', r' '], [r' ', r' _/ ', r' /________', r'/ ', r' '],
[r'/__=>', r' ', r' '], [r' ', r' _/____=> ', r'/ ', r' ', r' '],
], ],
hitFrames: [ hitFrames: [
[r' *BASH* ', r'/__=> ', r' '], [r' *BASH* ', r' _/____=> ', r'/ ** ', r' ** ', r' '],
[r'*SMASH!*', r' /__ ', r' '], [r' *SMASH!* ', r' _/____ ', r' / ** ', r' ** ', r' '],
], ],
hitSound: '*BASH*', hitSound: '*BASH*',
effectHeight: 3, effectHeight: 5,
effectYStart: 2, effectYStart: 1,
); );
// ============================================================================ // ============================================================================
// 케이블류 - 채찍질 (3줄) // 케이블류 - 채찍질 (5줄)
// ============================================================================ // ============================================================================
const _cableEffect = WeaponEffect( const _cableEffect = WeaponEffect(
prepareFrames: [ prepareFrames: [
[r' ', r'~ ', r' ~ '], [r' ', r'~ ', r' ~ ', r' ~ ', r' ~ '],
[r' ', r'~~ ', r' ~ '], [r' ', r'~~ ', r' ~~ ', r' ~ ', r' ~ '],
], ],
attackFrames: [ attackFrames: [
[r' ', r'~~~ ', r' ~~ '], [r' ', r'~~~ ', r' ~~~ ', r' ~~ ', r' ~ '],
[r' ', r'~~~~ ', r' ~~ '], [r' ', r'~~~~ ', r' ~~~~ ', r' ~~', r' '],
[r' ', r'~~~~~> ', r' ~~ '], [r' ', r'~~~~~> ', r' ~~~~~', r' ', r' '],
[r' ', r'~~~~~~> ', r' ~~'], [r' ', r'~~~~~~> ', r' ~~~~', r' ', r' '],
], ],
hitFrames: [ hitFrames: [
[r' *WHIP*', r'~~~~~~> ', r' ~~'], [r' *WHIP* ', r'~~~~~~> ', r' ~~~~', r' **', r' '],
[r' *CRACK*', r'~~~~~> ', r' ~~ '], [r' *CRACK!* ', r'~~~~~> ', r' ~~~~~', r' **', r' '],
], ],
hitSound: '*WHIP*', hitSound: '*WHIP*',
effectHeight: 3, effectHeight: 5,
effectYStart: 2, effectYStart: 1,
); );
// ============================================================================ // ============================================================================
// 투척류 - 발사 (3줄) // 투척류 - 발사 (5줄)
// ============================================================================ // ============================================================================
const _projectileEffect = WeaponEffect( const _projectileEffect = WeaponEffect(
prepareFrames: [ prepareFrames: [
[r' ', r'[=] ', r' '], [r' ', r' ', r' [=] ', r' ', r' '],
[r' ', r'[==] ', r' '], [r' ', r' _ ', r' [==] ', r' - ', r' '],
], ],
attackFrames: [ attackFrames: [
[r' ', r' [> ', r' '], [r' ', r' . ', r' [> ', r" ' ", r' '],
[r' ', r' [>', r' '], [r' ', r' . ', r' [> ', r" ' ", r' '],
[r' ', r' [>', r' '], [r' ', r' . ', r' [>', r" ' ", r' '],
[r' ', r' [>', r' '], [r' ', r' ', r' [>', r' ', r' '],
], ],
hitFrames: [ hitFrames: [
[r' *CLANG*', r' [>', r' '], [r' *CLANG!* ', r' ***', r' [>', r' ***', r' '],
[r' *CRASH* ', r' [> ', r' '], [r' *CRASH!* ', r' *** ', r' [> ', r' *** ', r' '],
], ],
hitSound: '*CLANG*', hitSound: '*CLANG*',
effectHeight: 3, effectHeight: 5,
effectYStart: 2, effectYStart: 1,
); );
// ============================================================================ // ============================================================================
// 에너지류 - 빔 발사 (3줄) // 에너지류 - 빔 발사 (5줄)
// ============================================================================ // ============================================================================
const _energyEffect = WeaponEffect( const _energyEffect = WeaponEffect(
prepareFrames: [ prepareFrames: [
[r' ', r' <*> ', r' '], [r' ', r' == ', r' <**> ', r' == ', r' '],
[r' == ', r' <**> ', r' == '], [r' ==== ', r' ====== ', r' <****> ', r' ====== ', r' ==== '],
], ],
attackFrames: [ attackFrames: [
[r' ==== ', r'==<*>== ', r' ==== '], [r' ====== ', r' ======== ', r'===<**>===', r' ======== ', r' ====== '],
[r' ====== ', r'===<*>==', r' ====== '], [r' ======== ', r'==========', r'===<**>===', r'==========', r' ======== '],
[r'========', r'===<*>==', r'========'], [r'==========', r'==========', r'====<**>==', r'==========', r'=========='],
[r'========', r'====<*>=', r'========'], [r'==========', r'==========', r'=====<**>=', r'==========', r'=========='],
], ],
hitFrames: [ hitFrames: [
[r'==*ZAP*=', r'===<*>==', r'========'], [r'===*ZAP*==', r'==========', r'====<**>==', r'==========', r'===*ZAP*=='],
[r'*BZZT!*=', r'====<*>=', r'========'], [r'==*BZZT!*=', r'==========', r'=====<**>=', r'==========', r'==*BZZT!*='],
], ],
hitSound: '*ZAP*', hitSound: '*ZAP*',
effectHeight: 3, effectHeight: 5,
effectYStart: 2, effectYStart: 1,
); );
// ============================================================================ // ============================================================================
@@ -162,23 +162,23 @@ const _cosmicEffect = WeaponEffect(
); );
// ============================================================================ // ============================================================================
// 맨손 - 기본 펀치 (3줄) // 맨손 - 기본 펀치 (5줄)
// ============================================================================ // ============================================================================
const _unarmedEffect = WeaponEffect( const _unarmedEffect = WeaponEffect(
prepareFrames: [ prepareFrames: [
[r' ', r' ', r' '], [r' ', r' ', r' > ', r' ', r' '],
[r' ', r' > ', r' '], [r' ', r' _ ', r' -> ', r' - ', r' '],
], ],
attackFrames: [ attackFrames: [
[r' ', r'-> ', r' '], [r' ', r' __ ', r' ---> ', r' -- ', r' '],
[r' ', r'---> ', r' '], [r' ', r' ___ ', r' -----> ', r' --- ', r' '],
[r' ', r'-----> ', r' '], [r' ', r' ____', r' ======>', r' ----', r' '],
], ],
hitFrames: [ hitFrames: [
[r' *POW* ', r'-----> ', r' '], [r' *POW!* ', r' ****', r' ======>', r' ****', r' '],
[r'*PUNCH*', r'----> ', r' '], [r' *PUNCH!* ', r' **** ', r' =====> ', r' **** ', r' '],
], ],
hitSound: '*POW*', hitSound: '*POW*',
effectHeight: 3, effectHeight: 5,
effectYStart: 2, effectYStart: 1,
); );

View File

@@ -520,7 +520,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Phase 7: 고정 4색 팔레트 사용 (colorTheme 무시) // 검정 배경 위에 배경 레이어(50% 투명)가 그려짐
const bgColor = AsciiColors.background; const bgColor = AsciiColors.background;
// 테두리 효과 결정 (전투 이벤트 또는 특수 애니메이션) // 테두리 효과 결정 (전투 이벤트 또는 특수 애니메이션)