Files
pipu-music/apps/mobile/src/ui/wave/draw.ts
T
mrleo1nidandClaude Opus 5.5 ed063d3763
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
feat(mobile): Skia wave, wave player view, themes and playback settings (M5)
- 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>
2026-10-20 13:30:21 +03:00

130 lines
5.1 KiB
TypeScript

// Отрисовка «Моей волны» на Skia — тот же алгоритм, что Canvas 2D в вебе (apps/web/src/widgets/wave/draw.ts).
// Функции — worklet'ы: кадр собирается в UI-потоке Reanimated, JS-поток не участвует.
import { mix, rgb, rgba, type Rgb, type WaveState } from '@pipu/core';
import { BlendMode, ClipOp, PaintStyle, Skia, TileMode, type SkCanvas, type SkPaint } from '@shopify/react-native-skia';
function radial(cx: number, cy: number, r0: number, r1: number, colors: string[], pos: number[]): SkPaint {
'worklet';
const p = Skia.Paint();
p.setAntiAlias(true);
p.setShader(
Skia.Shader.MakeTwoPointConicalGradient(
{ x: cx, y: cy },
r0,
{ x: cx, y: cy },
r1,
colors.map((c) => Skia.Color(c)),
pos,
TileMode.Clamp,
),
);
return p;
}
/** Градиент со смещённым центром (аналог createRadialGradient(x0, y0, 0, x1, y1, r)). */
function conical(x0: number, y0: number, x1: number, y1: number, r: number, colors: string[]): SkPaint {
'worklet';
const p = Skia.Paint();
p.setAntiAlias(true);
p.setShader(Skia.Shader.MakeTwoPointConicalGradient({ x: x0, y: y0 }, 0, { x: x1, y: y1 }, r, colors.map((c) => Skia.Color(c)), [0, 1], TileMode.Clamp));
return p;
}
export function drawOrb(canvas: SkCanvas, cx: number, cy: number, r: number, s: WaveState) {
'worklet';
const t = s.t;
const c1 = rgb(s.colors[0]);
const c2 = rgb(s.colors[1]);
const c3 = rgb(s.colors[2]);
canvas.drawCircle(
cx,
cy,
r * 1.65,
radial(cx, cy, r * 0.6, r * 1.65, [rgba(c1, 0.32 + s.pulse * 0.35 + s.hover * 0.12), rgba(c2, 0.1 + s.pulse * 0.12), rgba(c1, 0)], [0, 0.5, 1]),
);
canvas.save();
const clip = Skia.Path.Make();
clip.addCircle(cx, cy, r);
canvas.clipPath(clip, ClipOp.Intersect, true);
const bg = Skia.Paint();
bg.setColor(Skia.Color(s.dark ? '#060B1C' : '#0B1533'));
const box = Skia.XYWHRect(cx - r, cy - r, r * 2, r * 2);
canvas.drawRect(box, bg);
const blobs: Rgb[] = [c1, c2, c3, c1, c2];
for (let i = 0; i < blobs.length; i++) {
const c = blobs[i]!;
const a = t * (0.25 + i * 0.07) + i * 1.7;
const bx = cx + Math.cos(a) * r * (0.35 + 0.12 * Math.sin(t * 0.3 + i)) + s.mx * r * 0.18 * s.hover;
const by = cy + Math.sin(a * 1.3) * r * 0.38 + s.my * r * 0.18 * s.hover;
const br = r * (0.75 + 0.15 * Math.sin(t * 0.7 + i) + s.pulse * 0.2);
const g = radial(bx, by, 0, br, [rgba(c, 0.55), rgba(c, 0)], [0, 1]);
g.setBlendMode(BlendMode.Plus);
canvas.drawRect(box, g);
}
const lines = 9;
for (let l = 0; l < lines; l++) {
const yy = cy - r * 0.75 + (l / (lines - 1)) * r * 1.5;
const amp = r * (0.03 + 0.05 * s.amp + 0.09 * s.pulse + 0.03 * s.hover);
const path = Skia.Path.Make();
for (let x = -r; x <= r; x += 4) {
const y = yy + Math.sin((x / r) * (3 + l * 0.4) + t * (1.2 + l * 0.15) + l) * amp * Math.cos((x / r) * 1.4);
if (x === -r) path.moveTo(cx + x, y);
else path.lineTo(cx + x, y);
}
const stroke = Skia.Paint();
stroke.setAntiAlias(true);
stroke.setStyle(PaintStyle.Stroke);
stroke.setStrokeWidth(1.5);
stroke.setColor(Skia.Color(`rgba(255,255,255,${0.05 + 0.05 * s.amp + (l % 3 === 0 ? 0.05 : 0)})`));
stroke.setBlendMode(BlendMode.Plus);
canvas.drawPath(path, stroke);
}
canvas.drawRect(box, conical(cx - r * 0.35, cy - r * 0.45, cx - r * 0.35, cy - r * 0.45, r * 0.9, ['rgba(255,255,255,0.18)', 'rgba(255,255,255,0)']));
canvas.restore();
const ring = Skia.Paint();
ring.setAntiAlias(true);
ring.setStyle(PaintStyle.Stroke);
ring.setStrokeWidth(1);
ring.setColor(Skia.Color(rgba(mix(c1, [255, 255, 255], 0.4), 0.35 + s.pulse * 0.3)));
canvas.drawCircle(cx, cy, r - 0.5, ring);
}
export function drawLiquid(canvas: SkCanvas, cx: number, cy: number, r: number, s: WaveState) {
'worklet';
const t = s.t;
const cols: Rgb[] = [rgb(s.colors[0]), rgb(s.colors[1]), rgb(s.colors[2])];
canvas.drawCircle(cx, cy, r * 1.6, radial(cx, cy, r * 0.4, r * 1.6, [rgba(cols[0]!, 0.3 + s.pulse * 0.3), rgba(cols[0]!, 0)], [0, 1]));
for (let L = 0; L < 3; L++) {
const path = Skia.Path.Make();
const P = 140;
for (let j = 0; j <= P; j++) {
const th = (j / P) * Math.PI * 2;
const w =
1 +
0.06 * Math.sin(3 * th + t * (0.9 + L * 0.2) + L * 2) +
0.04 * Math.sin(5 * th - t * (1.3 + L * 0.1) + L) +
0.03 * Math.sin(7 * th + t * 1.7) * (0.5 + s.amp) +
0.08 * s.pulse * Math.sin(2 * th + L) +
0.16 * s.hover * Math.pow(Math.max(0, Math.cos(th - s.ang)), 3);
const rr = r * (0.86 - L * 0.07) * w;
const x = cx + Math.cos(th) * rr;
const y = cy + Math.sin(th) * rr;
if (j) path.lineTo(x, y);
else path.moveTo(x, y);
}
path.close();
const ox = Math.cos(t * 0.4 + L * 2) * r * 0.3;
const oy = Math.sin(t * 0.5 + L) * r * 0.3;
const c = cols[L]!;
const g = conical(cx + ox, cy + oy, cx, cy, r * 1.1, [rgba(mix(c, [255, 255, 255], 0.25), 0.85), rgba(c, 0.25)]);
g.setAlphaf(s.dark ? 0.6 : 0.75);
g.setBlendMode(s.dark ? BlendMode.Plus : BlendMode.SrcOver);
canvas.drawPath(path, g);
}
}