/** * Ścieżka paska zakładek, który „roztapia się" wokół aktywnej pozycji. * * Technika wzięta z reela `_code_and_chill_`: aktywna zakładka nie jest przesuwającą się * pigułką, tylko kroplą wyrastającą z paska, a przejście między nimi to **łuki styczne** * do jednego i drugiego. Autor pisze „every socket is solved for tangency, so the liquid * never creases" i to jest dosłownie opis tej funkcji. * * **Dlaczego nie filtr „goo" (rozmycie + próg alfy).** Byłby o rząd wielkości prostszy, * ale na Androidzie `react-native-svg` stosuje filtry przez pełną bitmapę: w * `RenderableView.java` każde rysowanie robi `Bitmap.createBitmap(canvas.getWidth(), * canvas.getHeight(), ARGB_8888)`, rysuje do niej element, nakłada programowy filtr * i wrzuca wynik z powrotem. Przy animowanej kropli to alokacja pełnoekranowej bitmapy * NA KLATKĘ, w nagłówku każdego ekranu. Ścieżka liczona analitycznie to jeden ``, * bez alokacji i bez filtra. * * Geometria (oś Y rośnie w dół, jak w SVG): * * ╭─ kropla (cx, cy, r) * ───╯ ╰─── ← łuki styczne (fillet) o promieniu k, siedzące W pasku * ───────── ← górna krawędź paska, y = top * * Łuk styczny ma środek na `y = top + k` (styczność do krawędzi paska) i jest stycznie * zewnętrznie do kropli, czyli odległość jego środka od środka kropli wynosi `r + k`. * Stąd pozioma odległość: `dx = sqrt((r+k)² - (top + k - cy)²)`. * * Gdy kropla siedzi zbyt głęboko w pasku, wyrażenie pod pierwiastkiem schodzi poniżej * zera i styczność nie istnieje — wtedy oddajemy sam pasek, bo lepszy prosty prostokąt * niż `NaN` w ścieżce (RN rysuje wtedy nic i pasek znika). */ export interface LiquidTabPathOpts { /** Szerokość paska. */ width: number; /** Górna krawędź paska (Y). */ top: number; /** Dolna krawędź paska (Y). */ bottom: number; /** Środek kropli w poziomie. */ cx: number; /** Środek kropli w pionie (zwykle powyżej `top`). */ cy: number; /** Promień kropli. */ r: number; /** Promień łuku stycznego. Im większy, tym „bardziej ciekły" przelew. */ fillet: number; /** Zaokrąglenie końców paska. */ barRadius: number; } /** Bezpieczny pierwiastek: ujemne pod pierwiastkiem → brak styczności. */ function safeSqrt(v: number): number | null { return v > 0 ? Math.sqrt(v) : null; } /** * Zwraca `d` dla pojedynczej ``: pasek z wtopioną w niego kroplą. * Kształt jest domknięty, więc nadaje się do wypełnienia jednym kolorem. */ export function liquidTabPath(o: LiquidTabPathOpts): string { const { width, top, bottom, cx, cy, r, fillet: k, barRadius: br } = o; // Kropla musi WYSTAWAĆ ponad krawędź paska. Bez tego warunku wzór na styczność ma // rozwiązanie także wtedy, gdy kropla siedzi w całości w pasku: dla top=26, cy=45, // r=17, k=12 wychodzi dx≈28,1, choć kropla sięga tylko do y=28, czyli jest pod // krawędzią. Powstałoby gniazdo, w które nie ma się co wtopić. Widać to dopiero // przy sprawdzeniu przypadków brzegowych, nie po samym wzorze. const protrudes = cy - r < top; const dxSq = (r + k) * (r + k) - (top + k - cy) * (top + k - cy); const dx = protrudes ? safeSqrt(dxSq) : null; // Brak styczności albo kropla nie wystaje → sam pasek, bez gniazda. if (dx === null) { return plainBar(width, top, bottom, br); } // Środki łuków stycznych, po obu stronach kropli. const fLeftX = cx - dx; const fRightX = cx + dx; const fy = top + k; // Punkty styczności łuk↔kropla: na prostej środek-kropli → środek-łuku, w odległości r. const tp = (fx: number) => { const vx = fx - cx; const vy = fy - cy; const len = Math.hypot(vx, vy) || 1; return { x: cx + (vx / len) * r, y: cy + (vy / len) * r }; }; const tLeft = tp(fLeftX); const tRight = tp(fRightX); // Rysujemy zgodnie z ruchem wskazówek: górna krawędź od lewej, gniazdo, prawa krawędź, // dół paska z powrotem. `sweepFlag` łuków gniazda = 0 (wklęsłe), kropli = 1 (wypukłe). return [ `M ${br} ${top}`, `L ${fLeftX} ${top}`, `A ${k} ${k} 0 0 0 ${tLeft.x} ${tLeft.y}`, `A ${r} ${r} 0 0 1 ${tRight.x} ${tRight.y}`, `A ${k} ${k} 0 0 0 ${fRightX} ${top}`, `L ${width - br} ${top}`, `A ${br} ${br} 0 0 1 ${width - br} ${bottom}`, `L ${br} ${bottom}`, `A ${br} ${br} 0 0 1 ${br} ${top}`, 'Z', ].join(' '); } /** Sam pasek, bez gniazda. Używane też jako stan spoczynku. */ export function plainBar(width: number, top: number, bottom: number, br: number): string { return [ `M ${br} ${top}`, `L ${width - br} ${top}`, `A ${br} ${br} 0 0 1 ${width - br} ${bottom}`, `L ${br} ${bottom}`, `A ${br} ${br} 0 0 1 ${br} ${top}`, 'Z', ].join(' '); }