feat(mobile): Skia wave, wave player view, themes and playback settings (M5)
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m30s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 2m57s
ci / ts / ts (packages + web) (push) Successful in 1m16s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 33s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m30s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 2m57s
ci / ts / ts (packages + web) (push) Successful in 1m16s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 33s
- My Wave is rendered with react-native-skia on the UI thread; physics and colour helpers moved to @pipu/core (wave-anim) and shared with web. - Full-screen player honours ui_prefs.mobile_player (cover / wave). - Theme switching (dark / light / system) via a theme context and useColors() across all screens. - Profile: theme, wave variant, player view, sound quality sheet (Wi-Fi / mobile / downloads), recent-tracks cache clearing on next cold start, and a change-password sheet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
756e915ff9
commit
ed063d3763
28 files changed
+923
-188
No files matched your search
@@ -22,3 +22,4 @@ export {
|
||||
type Track,
|
||||
} from './player';
|
||||
export { checkServer, normalizeServerUrl, SERVER_CHECK_TIMEOUT_MS, type ServerCheck } from './server';
|
||||
export { initialWaveState, mix, rgb, rgba, step, WAVE_COLORS, waveScale, type Rgb, type WaveState } from './wave-anim';
|
||||
@@ -0,0 +1,85 @@
|
||||
// Общие формулы кнопки «Моя волна» (docs/design.md#3): состояние, пружины, пульс в такт BPM и цвета.
|
||||
// Отрисовка — своя на платформе: Canvas 2D в вебе, Skia на Android. Директива 'worklet' нужна Android,
|
||||
// где кадр считается в UI-потоке Reanimated; в вебе это просто строка.
|
||||
|
||||
export type Rgb = [number, number, number];
|
||||
|
||||
export interface WaveState {
|
||||
hover: number;
|
||||
hoverT: number;
|
||||
press: number;
|
||||
pressV: number;
|
||||
pressT: number;
|
||||
mx: number;
|
||||
my: number;
|
||||
ang: number;
|
||||
t: number;
|
||||
amp: number;
|
||||
pulse: number;
|
||||
speed: number;
|
||||
phase: number;
|
||||
variant: 'orb' | 'liquid';
|
||||
playing: boolean;
|
||||
bpm: number;
|
||||
energy: number;
|
||||
colors: [string, string, string];
|
||||
dark: boolean;
|
||||
reduced: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export const WAVE_COLORS: [string, string, string] = ['#2F80FF', '#7C5CFF', '#35D3FF'];
|
||||
|
||||
export function initialWaveState(size: number): WaveState {
|
||||
return {
|
||||
hover: 0, hoverT: 0, press: 0, pressV: 0, pressT: 0, mx: 0, my: 0, ang: 0, t: 0, amp: 0, pulse: 0,
|
||||
speed: 1, phase: 0, variant: 'orb', playing: false, bpm: 100, energy: 0.6, colors: WAVE_COLORS,
|
||||
dark: true, reduced: false, size,
|
||||
};
|
||||
}
|
||||
|
||||
export function rgb(hex: string): Rgb {
|
||||
'worklet';
|
||||
const h = hex.replace('#', '');
|
||||
const full = h.length === 3 ? h[0]! + h[0]! + h[1]! + h[1]! + h[2]! + h[2]! : h;
|
||||
const n = parseInt(full, 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
|
||||
export function rgba(c: Rgb, a: number): string {
|
||||
'worklet';
|
||||
return `rgba(${c[0]},${c[1]},${c[2]},${a})`;
|
||||
}
|
||||
|
||||
export function mix(a: Rgb, b: Rgb, k: number): Rgb {
|
||||
'worklet';
|
||||
return [
|
||||
Math.round(a[0] + (b[0] - a[0]) * k),
|
||||
Math.round(a[1] + (b[1] - a[1]) * k),
|
||||
Math.round(a[2] + (b[2] - a[2]) * k),
|
||||
];
|
||||
}
|
||||
|
||||
/** Шаг физики: пружины hover / press, амплитуда при игре, пульс в такт BPM (docs/design.md#3). */
|
||||
export function step(s: WaveState, dt: number) {
|
||||
'worklet';
|
||||
s.hover += (s.hoverT - s.hover) * 0.08;
|
||||
s.pressV += (s.pressT - s.press) * 0.35;
|
||||
s.pressV *= 0.62;
|
||||
s.press += s.pressV;
|
||||
s.amp += ((s.playing ? 1 : 0) - s.amp) * 0.04;
|
||||
s.speed = s.reduced ? 0.15 : 1 + s.hover * 1.4 + s.amp * 0.7;
|
||||
s.t += dt * s.speed;
|
||||
if (s.playing && !s.reduced) {
|
||||
s.phase = (s.phase + (dt * s.bpm) / 60) % 1;
|
||||
s.pulse += (Math.pow(1 - s.phase, 4) * (0.35 + s.energy * 0.65) - s.pulse) * 0.5;
|
||||
} else {
|
||||
s.pulse += ((s.reduced ? 0 : (Math.sin(s.t * 1.1) * 0.5 + 0.5) * 0.12) - s.pulse) * 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Масштаб круга: сжатие при нажатии, лёгкое увеличение при наведении. */
|
||||
export function waveScale(s: WaveState): number {
|
||||
'worklet';
|
||||
return 1 - s.press * 0.07 + s.hover * 0.02;
|
||||
}
|
||||
@@ -231,6 +231,21 @@
|
||||
"dl_retry": "Retry failed",
|
||||
"dl_empty": "Download tracks, albums and likes to listen without a connection",
|
||||
"dl_pending_few": "{{count}} tracks queued",
|
||||
"dl_pending_many": "{{count}} tracks queued"
|
||||
"dl_pending_many": "{{count}} tracks queued",
|
||||
"player_view": "Player view",
|
||||
"player_cover": "Cover",
|
||||
"player_wave": "Wave",
|
||||
"quality": "Sound quality",
|
||||
"quality_wifi": "On Wi-Fi",
|
||||
"quality_cellular": "On mobile data",
|
||||
"quality_download": "Downloads",
|
||||
"quality_hint": "The new quality applies to the next tracks; downloaded tracks are not re-downloaded.",
|
||||
"q_original": "Original",
|
||||
"q_high": "High",
|
||||
"q_medium": "Medium",
|
||||
"q_low": "Low",
|
||||
"cache": "Recent tracks cache",
|
||||
"cache_clear": "Clear ({{size}} MB)",
|
||||
"cache_pending": "Will be cleared on next launch"
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,21 @@
|
||||
"dl_waiting_wifi": "Загрузки ждут Wi-Fi ({{count}})",
|
||||
"dl_waiting_network": "Загрузки ждут сеть ({{count}})",
|
||||
"dl_retry": "Повторить неудачные",
|
||||
"dl_empty": "Скачивайте треки, альбомы и лайки — они будут доступны без сети"
|
||||
"dl_empty": "Скачивайте треки, альбомы и лайки — они будут доступны без сети",
|
||||
"player_view": "Вид плеера",
|
||||
"player_cover": "Обложка",
|
||||
"player_wave": "Волна",
|
||||
"quality": "Качество звука",
|
||||
"quality_wifi": "По Wi-Fi",
|
||||
"quality_cellular": "По мобильной сети",
|
||||
"quality_download": "Загрузки",
|
||||
"quality_hint": "Новое качество применяется к следующим трекам; уже скачанные не перекачиваются.",
|
||||
"q_original": "Оригинал",
|
||||
"q_high": "Высокое",
|
||||
"q_medium": "Среднее",
|
||||
"q_low": "Низкое",
|
||||
"cache": "Кэш недавних треков",
|
||||
"cache_clear": "Очистить ({{size}} МБ)",
|
||||
"cache_pending": "Очистится при следующем запуске"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user