...
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImagePixelAnalyzer {
|
||||
ByteData? _byteData;
|
||||
int _width = 0;
|
||||
int _height = 0;
|
||||
|
||||
final Set<int> _hitPixels = {}; // 맞춘 픽셀 (Target)
|
||||
final Set<int> _wrongPixels = {}; // 틀린 픽셀 (Background)
|
||||
|
||||
int _totalTargetPixels = 0;
|
||||
|
||||
bool get isLoaded => _byteData != null;
|
||||
int get width => _width;
|
||||
int get height => _height;
|
||||
|
||||
// ... (loadFromAsset, loadFromUiImage는 기존과 동일) ...
|
||||
Future<void> loadFromAsset(String assetPath) async {
|
||||
final ByteData data = await rootBundle.load(assetPath);
|
||||
final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
|
||||
final ui.FrameInfo fi = await codec.getNextFrame();
|
||||
await loadFromUiImage(fi.image);
|
||||
}
|
||||
|
||||
Future<void> loadFromUiImage(ui.Image image) async {
|
||||
_width = image.width;
|
||||
_height = image.height;
|
||||
_byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
_calculateTargetPixels();
|
||||
}
|
||||
|
||||
void _calculateTargetPixels() {
|
||||
_totalTargetPixels = 0;
|
||||
_hitPixels.clear();
|
||||
_wrongPixels.clear();
|
||||
|
||||
if (_byteData == null) return;
|
||||
|
||||
for (int i = 0; i < _byteData!.lengthInBytes; i += 4) {
|
||||
final int alpha = _byteData!.getUint8(i + 3);
|
||||
if (alpha > 50) {
|
||||
_totalTargetPixels++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_hitPixels.clear();
|
||||
_wrongPixels.clear();
|
||||
}
|
||||
|
||||
/// 🔽 [핵심 수정] 터치 판정 로직 개선
|
||||
/// 선에 '조금이라도' 닿아 있으면 빗나간 부분을 용서해줍니다.
|
||||
bool checkHit(Offset position, Size displaySize, {double touchSize = 15.0}) {
|
||||
if (_byteData == null) return false;
|
||||
|
||||
final double scaleX = _width / displaySize.width;
|
||||
final double scaleY = _height / displaySize.height;
|
||||
final int imgX = (position.dx * scaleX).round();
|
||||
final int imgY = (position.dy * scaleY).round();
|
||||
final int radius = (touchSize * scaleX).round();
|
||||
|
||||
// 이번 터치 이벤트에서 발견된 픽셀들 임시 저장
|
||||
List<int> currentHits = [];
|
||||
List<int> currentWrongs = [];
|
||||
|
||||
for (int x = imgX - radius; x <= imgX + radius; x++) {
|
||||
for (int y = imgY - radius; y <= imgY + radius; y++) {
|
||||
if (x < 0 || x >= _width || y < 0 || y >= _height) continue;
|
||||
if ((x - imgX) * (x - imgX) + (y - imgY) * (y - imgY) > radius * radius) continue;
|
||||
|
||||
final int pixelIndex = (y * _width + x) * 4;
|
||||
final int alpha = _byteData!.getUint8(pixelIndex + 3);
|
||||
|
||||
if (alpha > 50) {
|
||||
// 목표 영역임
|
||||
if (!_hitPixels.contains(pixelIndex)) {
|
||||
currentHits.add(pixelIndex);
|
||||
}
|
||||
} else {
|
||||
// 빈 공간임
|
||||
if (!_wrongPixels.contains(pixelIndex)) {
|
||||
currentWrongs.add(pixelIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool hasChange = false;
|
||||
|
||||
// [판정 로직]
|
||||
// 1. 이번 터치 범위 내에 '목표 픽셀'이 하나라도 있었는가?
|
||||
bool touchedTarget = currentHits.isNotEmpty;
|
||||
|
||||
// 이미 방문했던 곳이라 currentHits가 0일 수도 있으니,
|
||||
// 주변에 방문한 _hitPixels가 있는지도 체크하면 더 좋지만,
|
||||
// 일단은 "새로 칠한 Hit가 있거나" OR "중심점이 Target 위에 있는지"로 판단
|
||||
if (!touchedTarget) {
|
||||
// 중심점 체크 (이미 칠한 곳 위를 지나갈 때 감점 방지)
|
||||
int centerIndex = (imgY * _width + imgX) * 4;
|
||||
if (centerIndex >= 0 && centerIndex < _byteData!.lengthInBytes) {
|
||||
if (_byteData!.getUint8(centerIndex + 3) > 50) {
|
||||
touchedTarget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (touchedTarget) {
|
||||
// ✅ 선 위를 지나가는 중 -> Hit만 인정, Wrong(삐져나간 것)은 무시!
|
||||
if (currentHits.isNotEmpty) {
|
||||
_hitPixels.addAll(currentHits);
|
||||
hasChange = true;
|
||||
}
|
||||
// currentWrongs는 추가하지 않음 (관대함)
|
||||
} else {
|
||||
// ❌ 선과 전혀 상관없는 허공을 칠함 -> Wrong 모두 기록 (감점)
|
||||
if (currentWrongs.isNotEmpty) {
|
||||
_wrongPixels.addAll(currentWrongs);
|
||||
hasChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChange;
|
||||
}
|
||||
|
||||
/// 🔽 [수정] 가중치(penaltyWeight)를 외부에서 받도록 변경
|
||||
double getScore({double penaltyWeight = 0.3}) {
|
||||
if (_totalTargetPixels == 0) return 0.0;
|
||||
|
||||
double rawScore = _hitPixels.length.toDouble();
|
||||
double penalty = _wrongPixels.length * penaltyWeight;
|
||||
|
||||
// 점수 = (성공 - 패널티) / 전체
|
||||
double finalScore = (rawScore - penalty) / _totalTargetPixels;
|
||||
|
||||
return finalScore.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_byteData = null;
|
||||
_hitPixels.clear();
|
||||
_wrongPixels.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TextBitmapGenerator {
|
||||
static Future<ui.Image> generate(String text, Size size) async {
|
||||
final ui.PictureRecorder recorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(recorder);
|
||||
|
||||
canvas.drawColor(Colors.transparent, BlendMode.clear);
|
||||
|
||||
// 🔽 [수정] 폰트 크기 자동 조절 (Auto-fit)
|
||||
// 기본 크기: 높이의 75%
|
||||
double fontSize = size.height * 0.75;
|
||||
|
||||
// 글자가 길어지면 너비에 맞춰 줄임
|
||||
// (대략적인 계산: 한 글자당 너비가 fontSize * 0.6 ~ 1.0 정도 차지함)
|
||||
if (text.length > 1) {
|
||||
// 화면 너비를 글자 수로 나눈 것과 비교하여 더 작은 값 선택
|
||||
// 여유를 두기 위해 0.9 곱함
|
||||
double maxWidthPerChar = (size.width / text.length) * 1.3;
|
||||
if (fontSize > maxWidthPerChar) {
|
||||
fontSize = maxWidthPerChar;
|
||||
}
|
||||
}
|
||||
|
||||
final TextSpan span = TextSpan(
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Roboto', // 한글 지원 폰트 권장
|
||||
),
|
||||
text: text,
|
||||
);
|
||||
|
||||
final TextPainter tp = TextPainter(
|
||||
text: span,
|
||||
textAlign: TextAlign.center,
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
|
||||
tp.layout(minWidth: size.width, maxWidth: size.width);
|
||||
|
||||
// 중앙 정렬
|
||||
final Offset textOffset = Offset(
|
||||
(size.width - tp.width) / 2,
|
||||
(size.height - tp.height) / 2,
|
||||
);
|
||||
|
||||
tp.paint(canvas, textOffset);
|
||||
|
||||
final ui.Picture picture = recorder.endRecording();
|
||||
return await picture.toImage(size.width.toInt(), size.height.toInt());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user