HOME 을 Figma 「홈」 구성으로 — TODAY'S QUEST · 미니맵·확장 맵(프로토타입 v6.2) · 행성 정보 박스 · 인큐베이터 · 아티스트 소식 · HOT ARCHIVE · 인기 K-INSIDE
This commit is contained in:
@@ -30,7 +30,22 @@ class GalaxyMiniMap extends StatefulWidget {
|
||||
required this.data,
|
||||
required this.onOpen,
|
||||
this.size = 140,
|
||||
});
|
||||
}) : expanded = false,
|
||||
onOpenPlanet = null;
|
||||
|
||||
/// 「GALAXY 맵 확장」 — Figma 「홈_초기사용자」(`343:211127`)의 히어로 전체 갤럭시 맵.
|
||||
/// 부모 크기를 채우고 모든 행성에 프로토타입 HUD 를 그린다. 설명 프레임 「갤럭시맵 화면」
|
||||
/// (`2008:14486`)의 동작 — 끌어 회전(프로토타입 View Rotation ±180° · 관성 없음) ·
|
||||
/// 두 손가락 줌인/줌아웃 · 행성 탭 = 화면 중심 전환 · HUD 탭 = [onOpenPlanet](FPI 상세).
|
||||
const GalaxyMiniMap.expanded({
|
||||
super.key,
|
||||
required this.data,
|
||||
required ValueChanged<PlanetNode> this.onOpenPlanet,
|
||||
}) : expanded = true,
|
||||
onOpen = _noop,
|
||||
size = 0;
|
||||
|
||||
static void _noop() {}
|
||||
|
||||
final GalaxySnapshot data;
|
||||
final VoidCallback onOpen;
|
||||
@@ -38,13 +53,18 @@ class GalaxyMiniMap extends StatefulWidget {
|
||||
/// 정사각 한 변. HOME 은 약 140pt(C-105 · 자리는 C-113 → `HomeHero.miniMapAlignment`).
|
||||
final double size;
|
||||
|
||||
final bool expanded;
|
||||
final ValueChanged<PlanetNode>? onOpenPlanet;
|
||||
|
||||
@override
|
||||
State<GalaxyMiniMap> createState() => _GalaxyMiniMapState();
|
||||
}
|
||||
|
||||
class _GalaxyMiniMapState extends State<GalaxyMiniMap>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _painter = GalaxyMiniMapPainter();
|
||||
late final _painter = widget.expanded
|
||||
? GalaxyMiniMapPainter.figmaScale()
|
||||
: GalaxyMiniMapPainter();
|
||||
late final Ticker _ticker = createTicker(_tick);
|
||||
Duration _last = Duration.zero;
|
||||
|
||||
@@ -92,8 +112,58 @@ class _GalaxyMiniMapState extends State<GalaxyMiniMap>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double _zoomStart = 1;
|
||||
|
||||
Widget _expanded(BuildContext context) {
|
||||
final mine = widget.data.mine;
|
||||
return LayoutBuilder(
|
||||
builder: (context, box) => Semantics(
|
||||
label: [
|
||||
if (mine != null)
|
||||
...GalaxyMiniMapPainter.hudLines(mine)
|
||||
else
|
||||
'GALAXY',
|
||||
].join(' · '),
|
||||
onTap: mine == null ? null : () => widget.onOpenPlanet!(mine),
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onScaleStart: (_) => _zoomStart = _painter.zoom,
|
||||
onScaleUpdate: (d) {
|
||||
if (d.pointerCount > 1) {
|
||||
_painter.zoomTo(_zoomStart * d.scale);
|
||||
} else {
|
||||
_painter.turn(d.focalPointDelta.dx / box.maxWidth * math.pi);
|
||||
}
|
||||
if (!_ticker.isActive) _painter.settle();
|
||||
},
|
||||
onTapUp: (d) {
|
||||
final hit = _painter.hitAt(d.localPosition);
|
||||
if (hit == null) return;
|
||||
if (hit.hud) {
|
||||
widget.onOpenPlanet!(hit.planet);
|
||||
} else {
|
||||
_painter.focus(hit.planet.id);
|
||||
if (!_ticker.isActive) _painter.settle();
|
||||
}
|
||||
},
|
||||
child: ExcludeSemantics(
|
||||
child: RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
size: box.biggest,
|
||||
painter: _painter,
|
||||
isComplex: true,
|
||||
willChange: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.expanded) return _expanded(context);
|
||||
final mine = widget.data.mine;
|
||||
return Semantics(
|
||||
button: true,
|
||||
@@ -153,7 +223,7 @@ const _emerald = Color(0xFF10B981);
|
||||
|
||||
/// 행성 색 — 팬덤 `colorKey` 를 행성 색 팔레트([planetColors])로 칠한다.
|
||||
/// 키가 없거나 모르는 값이면 FPI 순번으로 프로토타입 팔레트를 돈다.
|
||||
Color _planetColor(PlanetNode p, int fpiIndex) =>
|
||||
Color galaxyPlanetColor(PlanetNode p, int fpiIndex) =>
|
||||
planetColorOf(p.colorKey) ?? _palette[fpiIndex % _palette.length];
|
||||
|
||||
/// 프로토타입 게이지 기본값. 조절 UI 는 없다(C-51).
|
||||
@@ -201,6 +271,10 @@ class _Body {
|
||||
double wz = 0;
|
||||
double depth = 0;
|
||||
double hudW = 0;
|
||||
|
||||
/// 마지막 프레임의 화면 좌표·반지름·HUD 상자(탭 판정용).
|
||||
double sx = 0, sy = 0, sr = 0;
|
||||
Rect hudRect = Rect.zero;
|
||||
ui.Picture? hud;
|
||||
ui.Picture? sphere;
|
||||
Float32List sat = Float32List(0);
|
||||
@@ -273,6 +347,8 @@ ui.Picture _hudPicture(PlanetNode p, Color color, double share) {
|
||||
text: text,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
// 프로토타입 JetBrains Mono 대신 기기 고정폭(Android monospace · iOS Menlo).
|
||||
fontFamilyFallback: const ['Menlo', 'Courier', AppFonts.body],
|
||||
fontSize: size,
|
||||
fontWeight: weight,
|
||||
color: c,
|
||||
@@ -343,8 +419,27 @@ ui.Picture _spherePicture(Color c) {
|
||||
|
||||
/// 미니맵 장면 상태와 그리기. 상태는 위젯이 [advance]·[settle] 로 움직인다.
|
||||
class GalaxyMiniMapPainter extends CustomPainter {
|
||||
GalaxyMiniMapPainter() : this._(ValueNotifier<int>(0));
|
||||
GalaxyMiniMapPainter._(this._frame) : super(repaint: _frame);
|
||||
GalaxyMiniMapPainter() : this._(ValueNotifier<int>(0), null);
|
||||
|
||||
/// HOME 「GALAXY 맵 확장」 — Figma `343:211127` 축척. 화면 크기와 무관하게 초점 거리를
|
||||
/// [figmaFocal] 로 고정하고, HUD 카드는 [figmaHud] 고정 크기로 그린다.
|
||||
GalaxyMiniMapPainter.figmaScale() : this._(ValueNotifier<int>(0), figmaFocal);
|
||||
|
||||
GalaxyMiniMapPainter._(this._frame, this.fixedFocal) : super(repaint: _frame);
|
||||
|
||||
/// 고정 초점 거리(pt). null 이면 프로토타입처럼 세로 시야각 70°(`focal = h/2/tan35°`).
|
||||
final double? fixedFocal;
|
||||
|
||||
/// Figma `343:211127` 은 히어로(375)에 프로토타입 캡처(801×555)를 원래 픽셀 크기로 넣었다
|
||||
/// (y124 · 가로 가운데 x−213). 그 캡처의 내 행성(NewJeans, FPI 3200 → 반지름 31 × Mass 1.5)
|
||||
/// 지름이 38pt 다(Figma F1-05 · 343:211127 실측).
|
||||
/// `지름 = 2 × 46.5 × focal / 4031(카메라 거리)` → focal ≈ 1650pt. 같은 시야각이면
|
||||
/// 창 높이 약 2,310px 의 장면이다 — 556pt 창의 프로토타입보다 약 4.2배 크다.
|
||||
static const figmaFocal = 1650.0;
|
||||
|
||||
/// Figma 캡처의 내 행성 HUD 카드 실측 약 144×68pt → 텍스처 비율 2:1 로 144×72.
|
||||
/// 글자는 텍스처(512) 기준 이름 50 → 14pt · FPI 40 → 11pt · RANK/LIVE 24 → 6.75pt.
|
||||
static const figmaHud = Size(144, 72);
|
||||
|
||||
final ValueNotifier<int> _frame;
|
||||
|
||||
@@ -367,6 +462,43 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
double _gx = 0;
|
||||
double _gz = 0;
|
||||
|
||||
/// 확장 맵의 줌(초점 거리 배율). 별밭은 그대로 두고 장면만 키운다.
|
||||
double zoom = 1;
|
||||
static const minZoom = 0.5, maxZoom = 3.0;
|
||||
void zoomTo(double z) {
|
||||
zoom = z.clamp(minZoom, maxZoom);
|
||||
_frame.value++;
|
||||
}
|
||||
|
||||
/// 확장 맵에서 고른 행성 — 카메라가 이 행성을 가운데로 따라간다(없으면 내 행성).
|
||||
_Body? _focus;
|
||||
void focus(String id) => _focus = _bodies[id] ?? _focus;
|
||||
|
||||
@visibleForTesting
|
||||
String? get focusId => (_focus ?? _mine)?.id;
|
||||
|
||||
/// 마지막 프레임 기준 탭 판정 — HUD 카드가 위(프로토타입 depthTest: false)라 먼저 본다.
|
||||
({PlanetNode planet, bool hud})? hitAt(Offset p) {
|
||||
for (final b in _order.reversed) {
|
||||
if (b.hudRect.contains(p)) return (planet: b.node, hud: true);
|
||||
}
|
||||
for (final b in _order.reversed) {
|
||||
// 작은 행성도 손끝으로 잡히게 최소 반경 24.
|
||||
if ((Offset(b.sx, b.sy) - p).distance <= math.max(b.sr, 24)) {
|
||||
return (planet: b.node, hud: false);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 행성의 마지막 화면 좌표 — 테스트가 탭할 자리.
|
||||
@visibleForTesting
|
||||
Offset? screenOf(String id) =>
|
||||
_bodies[id] == null ? null : Offset(_bodies[id]!.sx, _bodies[id]!.sy);
|
||||
|
||||
@visibleForTesting
|
||||
Rect? hudRectOf(String id) => _bodies[id]?.hudRect;
|
||||
|
||||
/// 그리는 행성 수(내 행성은 순위 밖이어도 더한다).
|
||||
static const maxPlanets = 10;
|
||||
|
||||
@@ -512,7 +644,7 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
(_bodies[p.id] ??= _Body(
|
||||
p.id,
|
||||
_bodies.length + 1.0,
|
||||
)).set(p, _planetColor(p, i), share);
|
||||
)).set(p, galaxyPlanetColor(p, i), share);
|
||||
}
|
||||
final ids = {for (final p in shown) p.id};
|
||||
_bodies.removeWhere((id, b) {
|
||||
@@ -524,6 +656,7 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
..clear()
|
||||
..addAll(_bodies.values);
|
||||
_mine = _order.where((b) => b.node.isMine).firstOrNull;
|
||||
if (_focus != null && !_bodies.containsValue(_focus)) _focus = null;
|
||||
_place(0);
|
||||
_frame.value++;
|
||||
}
|
||||
@@ -559,7 +692,7 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
void _place(double follow) {
|
||||
final c = math.cos(_rot);
|
||||
final s = math.sin(_rot);
|
||||
if (_mine case final m?) {
|
||||
if (_focus ?? _mine case final m?) {
|
||||
final a = _angle(m);
|
||||
final lx = math.cos(a) * m.rad;
|
||||
final lz = math.sin(a) * m.rad;
|
||||
@@ -679,9 +812,10 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final focal = size.height / 2 / _tanHalfFov;
|
||||
if (size != _bgSize) _buildBackground(size, focal);
|
||||
final bgFocal = size.height / 2 / _tanHalfFov;
|
||||
if (size != _bgSize) _buildBackground(size, bgFocal);
|
||||
canvas.drawPicture(_bg!);
|
||||
final focal = (fixedFocal ?? bgFocal) * zoom;
|
||||
|
||||
// 궤도 링 — 갤럭시 원점 중심, 두께 = 반지름/100, 흰색 15%.
|
||||
_project(_gx, 0, _gz, focal, size);
|
||||
@@ -747,6 +881,10 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
|
||||
// 광륜 + 구체.
|
||||
final rPx = r * focal / d;
|
||||
b
|
||||
..sx = px
|
||||
..sy = py
|
||||
..sr = rPx;
|
||||
canvas.save();
|
||||
canvas.translate(px, py);
|
||||
canvas.scale(rPx);
|
||||
@@ -778,17 +916,41 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
final hudHeight = 57.5 * f;
|
||||
// [fullHudMin] 보다 작으면 프로토타입 카드는 몇 px 라 읽히지 않는다 — 내 행성만
|
||||
// 간략 카드로 그리고 다른 행성 카드는 뺀다(궤도·행성만).
|
||||
final full = size.width >= fullHudMin;
|
||||
final full = fixedFocal != null || size.width >= fullHudMin;
|
||||
for (final b in _order) {
|
||||
b.hudW = 0;
|
||||
b.hudRect = Rect.zero;
|
||||
if (!full && !identical(b, _mine)) continue;
|
||||
if (b.depth < b.baseR * _scale * 2) continue;
|
||||
final y = _scale * (b.baseR * _scale + hudHeight / 2 + 2);
|
||||
_project(b.wx, y, b.wz, focal, size);
|
||||
if (_sd < 1) continue;
|
||||
final w = 115 * f * _scale * focal / _sd;
|
||||
if (fixedFocal != null) {
|
||||
// 고정 크기 카드 — 장면 축척 · 줌과 무관하게 같은 크기. 아래 변을 프로토타입 HUD
|
||||
// 아래 변 자리(행성 위 HUD Dist 2)에 둔다.
|
||||
const hud = figmaHud;
|
||||
_project(b.wx, _scale * (b.baseR * _scale + 2), b.wz, focal, size);
|
||||
if (_sd < 1) continue;
|
||||
final rect = Rect.fromLTWH(
|
||||
_sx - hud.width / 2,
|
||||
_sy - hud.height,
|
||||
hud.width,
|
||||
hud.height,
|
||||
);
|
||||
b
|
||||
..hudW = hud.width
|
||||
..hudRect = rect;
|
||||
canvas.save();
|
||||
canvas.translate(rect.left, rect.top);
|
||||
canvas.scale(hud.width / _hudW, hud.height / _hudH);
|
||||
canvas.drawPicture(b.hud!);
|
||||
canvas.restore();
|
||||
continue;
|
||||
}
|
||||
if (full) {
|
||||
b.hudW = w;
|
||||
b.hudRect = Rect.fromLTWH(_sx - w / 2, _sy - w / 4, w, w / 2);
|
||||
canvas.save();
|
||||
canvas.translate(_sx - w / 2, _sy - w / 4);
|
||||
canvas.scale(w / _hudW, w / 2 / _hudH);
|
||||
|
||||
Reference in New Issue
Block a user