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

- 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:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 13:30:21 +03:00
1 parent 756e915ff9
commit ed063d3763
28 files changed
+923 -188

No files matched your search

+3 -56
View File
@@ -1,45 +1,9 @@
// Отрисовка кнопки «Моя волна» на Canvas 2D — порт прототипа docs/Разработка дизайна приложения/wave.js
// (варианты orb и liquid; «Кольца» не реализуем — docs/design.md).
// (варианты orb и liquid; «Кольца» не реализуем — docs/design.md). Физика и цвета — в @pipu/core (wave-anim).
export type Rgb = [number, number, number];
import { mix, rgb, rgba, type Rgb, type WaveState } from '@pipu/core';
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 function rgb(hex: string): Rgb {
const h = hex.replace('#', '');
const full = h.length === 3 ? [...h].map((c) => c + c).join('') : h;
const n = parseInt(full, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
const rgba = (c: Rgb, a: number) => `rgba(${c[0]},${c[1]},${c[2]},${a})`;
const mix = (a: Rgb, b: Rgb, k: number): Rgb => [
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),
];
export { rgb, step, type Rgb, type WaveState } from '@pipu/core';
export function drawOrb(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, s: WaveState, t: number) {
const [c1, c2, c3] = s.colors.map(rgb) as [Rgb, Rgb, Rgb];
@@ -139,20 +103,3 @@ export function drawLiquid(ctx: CanvasRenderingContext2D, cx: number, cy: number
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
}
/** Шаг физики: пружины hover / press, амплитуда при игре, пульс в такт BPM (docs/design.md#3). */
export function step(s: WaveState, dt: number) {
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;
}
}