Stack 위젯으로 투명한 PNG를 순서대로 쌓아 캐릭터를 만든다.
피부 레이어 → 그림자 → 머리카락 → 의상 → 얼굴
BlendMode.modulate 사용 → 원본 이미지의 질감을 유지하면서 색상만 변경
Image.asset(
'body.png',
color: skinColor,
colorBlendMode: BlendMode.modulate,
)
렌더링 로직을 한 곳에 캡슐화하여 어디서든 재사용 가능하게 만든다.
class CharacterAvatar extends StatelessWidget {
final String bodyPath;
final String hairPath;
final String facePath;
final String clothPath;
final Color skinColor;
final Color hairColor;
// 생성자 생략
Widget build(BuildContext context) {
return Stack(
children: [
Image.asset(bodyPath, color: skinColor, colorBlendMode: BlendMode.modulate),
Image.asset(hairPath, color: hairColor, colorBlendMode: BlendMode.modulate),
Image.asset(clothPath),
Image.asset(facePath),
],
);
}
}
class CharacterCustomization extends StatefulWidget {
State<CharacterCustomization> createState() => _CharacterCustomizationState();
}
class _CharacterCustomizationState extends State<CharacterCustomization> {
Color selectedSkinColor = const Color(0xFFFFDBBB);
Color selectedHairColor = const Color(0xFF8B4513);
String selectedHair = 'assets/parts/hair_short.png';
String selectedCloth = 'assets/parts/cloth_shirt.png';
void reset() {
setState(() {
selectedSkinColor = const Color(0xFFFFDBBB);
selectedHairColor = const Color(0xFF8B4513);
selectedHair = 'assets/parts/hair_short.png';
selectedCloth = 'assets/parts/cloth_shirt.png';
});
}
void save() {
print('저장 완료');
}
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// 미리보기
CharacterAvatar(
bodyPath: 'assets/parts/body.png',
hairPath: selectedHair,
facePath: 'assets/parts/face.png',
clothPath: selectedCloth,
skinColor: selectedSkinColor,
hairColor: selectedHairColor,
),
const SizedBox(height: 24),
// 색상 선택
Wrap(
children: [
_colorButton(const Color(0xFFFFDBBB)),
_colorButton(const Color(0xFFEDB5A6)),
],
),
// 머리 선택
Wrap(
children: [
_hairButton('짧은 머리', 'assets/parts/hair_short.png'),
_hairButton('긴 머리', 'assets/parts/hair_long.png'),
],
),
const Spacer(),
// 하단 버튼
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: reset,
child: const Text('초기화'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: save,
child: const Text('저장하기'),
),
),
],
),
],
),
);
}
Widget _colorButton(Color color) {
return GestureDetector(
onTap: () => setState(() => selectedSkinColor = color),
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: selectedSkinColor == color ? Colors.black : Colors.grey,
),
),
),
);
}
Widget _hairButton(String label, String path) {
return GestureDetector(
onTap: () => setState(() => selectedHair = path),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: selectedHair == path ? Colors.blue : Colors.grey,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(label),
),
);
}
}
모든 파츠 이미지의 크기와 중심점이 일치해야 한다.
- 모든 파츠: 1000x1200px (동일)
- 중심점: 정확하게 맞춰야 레이어 쌓을 때 틀어지지 않음
// 많은 레이어를 쓸 때는 RepaintBoundary로 감싸기
RepaintBoundary(
child: CharacterAvatar(...),
)
그림자 농도(0.25~0.3)와 색상 팔레트로 프리미엄한 느낌을 전달한다.
CharacterAvatar라는 명확한 인터페이스를 만들면:
새 파츠 추가 시
→ UI 코드 수정 없음
→ 에셋 파일만 추가하면 됨
이로써 확장성과 재사용성을 동시에 확보했다.