Files
fanuzu/app/lib/features/galaxy/galaxy_motion.dart
T

149 lines
7.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}