import 'dart:math' as math; import 'dart:ui' show Offset; import 'package:flutter/foundation.dart'; /// 갤럭시 공간 인터랙션의 조정 손잡이 — **여기 말고 다른 데에 수치를 흩지 않는다.** /// /// 발주사 검토회신서 3절이 요구한 것은 기능이 아니라 체감이다 /// (*드래그·회전 움직임과 관성 · 행성 간 이동감 · 깊이감*). 체감은 숫자 몇 개를 /// 돌려 가며 맞추는 일이라, 값이 위젯 안에 박혀 있으면 조정할 수가 없다. /// /// 기준값의 출처는 발주사 프로토타입이다 — /// `FANUZU_HUD Proto.html` 의 `lerp(cur, target, 0.03~0.08)` 감쇠, /// 행성에 붙은 HUD 스프라이트(`hud.position.y = radius + 5`), /// `time * 0.8/(rank+1)` 궤도 회전([idleSpin]). abstract final class GalaxyMotion { // ── 회전 관성 ───────────────────────────────────────────────────────── /// 손을 대지 않았을 때 [GalaxyClock.spin] 이 흐르는 속도(rad/s). /// /// **행성의 실제 각속도는 이 값이 아니다** — [GalaxyField.positionOf] 가 /// `spin * orbitSpeedOf(rank)` 를 각도로 쓰므로 /// `idleSpin × 1.6/(rank+1)` = `0.4/(rank+1)` rad/s 다. /// 프로토타입 `FANUZU_HUD Proto.html` 이 `time = Date.now()*0.0005`(0.5rad/s)에 /// `speed = 0.8/(rank+1)` 을 곱해 `0.4/(rank+1)` 을 쓴다 — 같은 값이다. /// (rank 2 → 0.133rad/s, 1회전 47초.) static const double idleSpin = 0.25; /// 가로 드래그 속도(px/s) → 각속도(rad/s) 환산 계수. static const double dragSpinGain = 0.0011; /// 궤도를 돌리는 드래그 성분만 골라낸다. 나머지는 0 이다. /// /// [InteractiveViewer] 는 팬·핀치·회전을 한 콜백으로 준다. `focalPointDelta.dx` /// 를 그대로 넘기면 **세로 드래그와 핀치 줌이 각속도를 덮어써** 궤도가 선다 /// ([GalaxyClock.advance] 는 드래그가 있는 프레임에 감쇠를 건너뛴다). static double spinDragOf(int pointerCount, Offset delta) => pointerCount == 1 && delta.dx.abs() > delta.dy.abs() ? delta.dx : 0; /// 각속도가 기본 표류로 돌아가는 감쇠율(1/s). 클수록 빨리 선다. /// e-폴딩 시간 = 1/이 값 ≈ 0.56초. static const double spinFriction = 1.8; /// 각속도 상한(rad/s). 없으면 세게 던졌을 때 궤도가 눈으로 못 따라간다. static const double maxSpin = 2.0; /// 링 맥동 한 주기(초). static const double pulsePeriod = 2.4; // ── 줌 ──────────────────────────────────────────────────────────────── static const double minZoom = 0.55; static const double maxZoom = 3.2; /// 행성을 고르면 이 배율로 다가간다. static const double focusZoom = 1.5; // ── 중심 전환 ───────────────────────────────────────────────────────── /// 「행성 터치시 화면 중심 전환이동」에 드는 시간. static const Duration recenter = Duration(milliseconds: 620); // ── HUD ─────────────────────────────────────────────────────────────── /// HUD 카드 폭(논리 px). static const double hudWidth = 214; /// 행성 표면과 HUD 카드 사이 거리 — 둘을 잇는 선의 길이이기도 하다. /// 프로토타입 `hud.position.y = radius + 5` 의 `proximityOffset` 자리다. static const double hudGap = 18; /// 위/아래를 정할 때 쓰는 HUD 높이 어림값. 실제 높이는 내용이 정한다. /// Figma 명세가 *「HUD - 행성 상단/하단」* 이라 둘 다 정상 배치다. static const double hudHeightHint = 96; /// HUD 가 화면 가장자리에 붙지 않도록 남기는 여백. static const double hudMargin = 12; // ── 깊이감 ──────────────────────────────────────────────────────────── /// 궤도 타원의 세로 눌림. 1 이면 정원(위에서 수직으로 본 것), 작을수록 눕는다. static const double tilt = 0.62; /// 앞뒤 크기 차. 궤도 앞쪽(화면 아래)이 `1+이 값`, 뒤쪽이 `1-이 값` 배. static const double depthScale = 0.26; /// 뒤쪽 행성이 옅어지는 정도. 0 이면 앞뒤가 같은 진하기다. static const double depthFade = 0.34; /// 선택 링의 반지름 배율. **1.6 보다 커야 한다** — 행성 일러스트가 반지름의 /// 1.6배까지 그려지므로 그 안쪽에 그리면 그림에 가려 보이지 않는다. static const double selectionRing = 1.95; /// 별밭이 맵을 따라오는 비율. 0 이면 완전히 고정, 1 이면 맵과 한 몸이다. static const double parallax = 0.12; /// 별밭이 따라오는 최대 거리(px). 넘어가면 별밭이 화면 밖으로 빠진다. static const double parallaxLimit = 160; /// 줌이 별밭에 먹히는 비율. 행성보다 덜 커져야 멀리 있어 보인다. static const double parallaxZoom = 0.25; /// 궤도 각속도 배율 — 바깥 궤도일수록 느리다. /// 프로토타입 `const speed = 0.8 / (data.rank + 1)` 와 같은 꼴이다. static double orbitSpeedOf(int rank) => 1.6 / (rank + 1); } /// 갤럭시의 시간 — 궤도 각도와 맥동을 한 곳에서 굴린다. /// /// [AnimationController.repeat] 를 쓰지 않는 이유는 **각속도가 상수가 아니기 /// 때문**이다. 드래그가 각속도를 밀어 올리고([drag]), 손을 떼면 마찰이 기본 /// 표류로 되돌린다([advance]) — 이것이 검토회신서의 *「자연스러운 관성과 감속」*이다. class GalaxyClock extends ChangeNotifier { /// 궤도 회전 각도(rad). 단조 증가가 아니다 — 반대로 던지면 줄어든다. double spin = 0; /// 0~1 을 반복하는 맥동 위상. double pulse = 0; double _velocity = GalaxyMotion.idleSpin; /// 선택 행성의 서버 상태가 정하는 주변 궤도 표류. 드래그는 그대로 유지한다. double idleSpeedScale = 1; /// 이번 프레임에 쌓인 가로 드래그 거리(px). double _dragged = 0; @visibleForTesting double get velocity => _velocity; /// 드래그가 각속도를 민다. 프레임당 여러 번 들어올 수 있으므로 모아 둔다. void drag(double dx) => _dragged += dx; /// [dt] 초만큼 시간을 흘린다. void advance(double dt) { if (dt <= 0) return; if (_dragged != 0) { // 손이 닿아 있는 동안의 각속도는 **드래그 거리가 아니라 드래그 속도**를 // 따른다. 거리로 누적하면 천천히 오래 끌기만 해도 팽이처럼 돌아 버린다. _velocity = (_dragged / dt * GalaxyMotion.dragSpinGain).clamp( -GalaxyMotion.maxSpin, GalaxyMotion.maxSpin, ); _dragged = 0; } else { // 지수 감쇠 — 프레임 간격이 흔들려도 같은 시간에 같은 만큼 준다. final decay = math.exp(-GalaxyMotion.spinFriction * dt); final idle = GalaxyMotion.idleSpin * idleSpeedScale; _velocity = idle + (_velocity - idle) * decay; } spin += _velocity * dt; pulse = (pulse + dt / GalaxyMotion.pulsePeriod) % 1.0; notifyListeners(); } }