149 lines
7.5 KiB
Dart
149 lines
7.5 KiB
Dart
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();
|
||
}
|
||
}
|