diff --git a/Progress.md b/Progress.md index 2486d0d..6dfc106 100644 --- a/Progress.md +++ b/Progress.md @@ -650,6 +650,77 @@ Lista rescrisă de utilizator după prima trecere. Ce s-a schimbat: atunci; propoziția pasului a dispărut de pe Neville; fila spline nu mai desenează și polinomul de comparație; nodurile pornesc de la 5 și își scriu numele doar până la 5. +### A treia trecere prin `imbunatatiri.md` (17 august 2026) + +Lista fusese bifată aproape toată în trecerea a doua, dar `InterfataInterpolare.tsx` rămăsese +**la jumătate de refactorizare și nu compila**: `useDerulare` și `PlaybackBar` erau folosite fără +să fie importate, `schimbaFila`, `kAles` și `amplitudine` fuseseră șterse dar erau încă chemate, +iar filei Runge îi rămăseseră ramurile de legendă și de descriere. Zece erori de tipuri, deci nici +`npm run build` nu trecea. Ce s-a terminat: + +- **Lagrange, o singură linie.** Evidențierea din formulă s-a mutat de pe `l_k(x)` pe `P_n(x)`: + multiplicatorul nu se mai desenează, deci nu mai are ce aprinde pe grafic. Legenda a scăpat de + cele două intrări rămase fără desen. +- **Neville, pași cu săgeți.** Bara de derulare și triunghiul stau sub desen; legenda a scăpat de + „verticala punctului evaluat" și de „P₀ₙ(x)", niciuna desenată. +- **Graficul crește prin `raport`, nu prin plafon.** Plafonul ridicat de la 460 la 560 **nu mișcase + niciun pixel**: măsurat în browser, coloana din stânga se oprește la 707px și nu crește nici pe + 1536px, deci înălțimea ieșea din `707 / 1,6 = 442px`, iar plafonul nu era atins la nicio lățime. + Cu `raport = 1,3` se ajunge la **544px**. **De reținut**: `inaltimeMaxima` e o limită, nu o + cerere — cine vrea un desen mai înalt schimbă `raport`. +- **Spline-ul se vede că are două curbe.** Aici măsurătoarea a schimbat decizia. Desenarea + amândurora, făcută în trecerea trecută, nu ajunge: pe `sin(πx)`, la numărul implicit de noduri, + cele două condiții de capăt stau la **2px** una de alta, iar de la nouă noduri se suprapun exact. + + | noduri | 4 | 5 | 7 | 9 | 13 | + | -------- | --- | --- | --- | --- | --- | + | sin(πx) | 8 | 2 | 0 | 0 | 0 | + | x⁵ − 2x³ | 12 | 10 | 5 | 3 | 1,5 | + + Deci funcția de start a devenit `x⁵ − 2x³`. Celelalte două file nu pierd nimic: `max |Pₙ| = 1` + la orice număr de noduri, iar de la șase noduri polinomul reproduce funcția exact. Legenda + numește acum și curba punctată, iar lângă `s″(x₀)` („de ce") stă `depărtarea` („cu cât"). + + Cu trei noduri cele două curbe coincid **exact**: `(−1, 1)`, `(0, 0)`, `(1, −1)` sunt coliniare, + deci amândouă condițiile dau dreapta `y = −x`. Verificat, nu e o scăpare de calcul. + +Verificat în browser, pe ambele teme, la 360px și 1280px, cu `prefers-reduced-motion`: fără erori +în consolă și fără derulare orizontală. + +**Restanța de mai sus e închisă (18 august 2026).** `PlotDreapta` își dă acum cele patru capete +**și** static, nu doar prin `animate`, exact ca `motion.line` din `PlotPunct` — deci atributul intră +în marcajul scris de React, iar linia are geometrie validă din chiar randarea care o inserează. + +**Erorile din consolă n-au putut fi reproduse** pe Chrome, cu `motion` 13.1.0: încărcare curată pe +`derivare-numerica` și pe `ecuatii-neliniare`, plus pași și schimbări de metodă, cu zero erori; un +`MutationObserver` pe nodurile `` adăugate și un cârlig pe `setAttribute` n-au prins nicio +scriere de `undefined` sau `NaN`; iar cele 23 de linii din pagină au toate cele patru atribute +numerice. Explicația cea mai probabilă e `initial={false}`, care face `motion` să randeze direct +ținta din `animate`. Reparația s-a făcut oricum: se aliniază la tiparul folosit deja în `PlotPunct` +și nu mai lasă primul cadru pe seama momentului în care `motion` apucă să scrie atributul. + +### Refacerea derulării se face doar la întoarcere (17 august 2026) + +Urmarea directă a reparației de mai sus, din a doua trecere: odată ce poziția chiar se reținea, s-a +văzut că se **refăcea prea des**. Săgeata „pagina următoare" te lăsa în subsolul paginii următoare, +nu la titlul ei. + +Cauza nu era derularea, ci **unde stau săgețile**: chiar la fundul paginii. Orice pagină părăsită +printr-o săgeată își salva poziția „la fund", iar a doua oară când ajungeai pe ea — oricum ai fi +ajuns — erai pus înapoi acolo. Măsurat pe `interpolare-polinomiala` → `curbe-bezier`, a doua +trecere: **6713 din 6966** pe telefon și **5031 din 5031** pe desktop. + +**Nu era o problemă de telefon**, deși acolo a fost văzută: pe telefon paginile sunt cam de două ori +mai înalte și săgeata se apasă mult mai des decât butonul „înapoi" al browserului, deci se nimerea +mai des. Pe desktop se reproduce identic. + +Acum poziția se reface **doar la `POP`** (`useNavigationType` din react-router) — înapoi/înainte din +browser, adică exact gestul căruia browserul îi reface singur derularea pe un site fără router. Un +clic pe o legătură e `PUSH`: ai cerut altă pagină, deci o primești de la început. + +**De reținut**: „reține poziția paginii" nu e o singură cerință, ci două — _ce_ se salvează și _când_ +se reface. Prima trecere a rezolvat-o pe prima și a presupus că a doua n-are variante. + ### Pagina 13 — `curbe-bezier`, ce e gata și ce nu Ce există: diff --git a/imbunatatiri.md b/imbunatatiri.md index 6c937ff..826ddcd 100644 --- a/imbunatatiri.md +++ b/imbunatatiri.md @@ -1,32 +1,6 @@ -# De schimbat la Cuadraturi: -La interactiv: -1. La Gaussiene, pune prima functie radical(x) -2. Scrie titlul de la Cuadraturi adaptive "Cuadraturi Adaptive" - - -# De schimbat la Newton-Cotes: -Probleme Persistente: -e descentrata partea de sub grafic: SubintervaleN 4h = 0,7854. 4 ar trebui sa stea langa N - - - -# De schimbat la Derivare Numerica: -La interactiv: -1. Fa mai mare graficul, acum e decentrat cu parametrii - - -# De schimbat la interpolari: -La interactiv: -1. La Lagrange: imbina multiplicatorul cu polinomul doar intr-o singura linie, unde sa se vada frumos cum se schimba polinomul la mai multe puncte -2. La Neville: partea cu pasi fa-o sub grafic, nu ca parametru, sa se ruleze cu sagetele. Partea de grade scoate-o sub partea de derulare, iar in locul ei fa graficul mai mare. Scoate partea cu verticala punctului evaluat. Scoate P0n(x) -3. Scoate partea cu functia Runge de tot din interactiv -4. La Spline fa cumva sa se vada mai bine diferenta intre natural si tensionat. Scoate si max |s − f| -0,0200. Schimba functia de start daca e nevoie - - -# La Eliminare Gaussiana si pivotari -La interactiv: adu pe aceeasi linie a(ip) si matricea, mareste simbolul de la a(ip) si mareste per total matricea si linia cu pivotii - - +# Îmbunătățiri +Lista e goală: toate punctele scrise aici au fost rezolvate. Ce s-a schimbat la fiecare e scris în +[`Progress.md`](./Progress.md), în secțiunile despre trecerile prin fișierul ăsta. +Aici se scriu punctele următoare. diff --git a/public/sitemap.xml b/public/sitemap.xml index bd47885..618d1b8 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -7,127 +7,127 @@ 1.0 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/factorizari-lu + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/factorizari-lu 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/norme-si-ortogonalitate + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/norme-si-ortogonalitate 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/eliminare-gaussiana + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/eliminare-gaussiana 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/algoritmul-thomas + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/algoritmul-thomas 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/metode-iterative + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/metode-iterative 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/ecuatii-neliniare + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/ecuatii-neliniare 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/metode-de-gradient + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/metode-de-gradient 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/metodele-puterii + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/metodele-puterii 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/pagerank + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/pagerank 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/algoritmul-qr + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/algoritmul-qr 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/dvs + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/dvs 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/interpolare-polinomiala + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/interpolare-polinomiala 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/curbe-bezier + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/curbe-bezier 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/fft + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/fft 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/derivare-numerica + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/derivare-numerica 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/newton-cotes + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/newton-cotes 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/cuadraturi-adaptive-si-gaussiene + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/cuadraturi-adaptive-si-gaussiene 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/algoritm/ecuatii-diferentiale + https://serbac6.github.io/numerical-methods-visualizer/#/algoritm/ecuatii-diferentiale 2026-08-17 monthly 0.8 - https://serbac6.github.io/numerical-methods-visualizer/contact + https://serbac6.github.io/numerical-methods-visualizer/#/contact 2026-08-17 yearly 0.3 - https://serbac6.github.io/numerical-methods-visualizer/termeni + https://serbac6.github.io/numerical-methods-visualizer/#/termeni 2026-08-17 yearly 0.2 - https://serbac6.github.io/numerical-methods-visualizer/confidentialitate + https://serbac6.github.io/numerical-methods-visualizer/#/confidentialitate 2026-08-17 yearly 0.2 diff --git a/src/components/content/AnimatiaFactorizariiLu.tsx b/src/components/content/AnimatiaFactorizariiLu.tsx index 93d8d57..3c00183 100644 --- a/src/components/content/AnimatiaFactorizariiLu.tsx +++ b/src/components/content/AnimatiaFactorizariiLu.tsx @@ -190,16 +190,24 @@ function Antet({ nota?: readonly string[]; st: number; }) { - const notaSus = 150 + (titlu.length - 1) * 66 + 54; + // Rândurile se așază **față de corpul literei**, nu la 66 și 40 de unități + // fixe. Pasul fix era scris pentru desktop, unde `st` e 1 și corpul iese exact + // 60 și 29; pe un cadru îngust corpul crește până la 81, respectiv 43,5, iar + // rândul al doilea urca peste primul — se vedea la orice titlu sau notă de + // două rânduri. Factorii sunt cei care refac exact valorile de dinainte la + // `st = 1`: 66/60 și 40/29, iar 54/60 pentru golul dintre titlu și notă. + const corpTitlu = 60 * Math.min(st, 1.35); + const corpNota = 29 * Math.min(st, 1.5); + const notaSus = 150 + (titlu.length - 1) * corpTitlu * 1.1 + corpTitlu * 0.9; return ( {titlu.map((linie, i) => ( {linie} @@ -208,9 +216,9 @@ function Antet({ {linie} @@ -722,21 +730,17 @@ function ActExplozie({ st }: { st: number }) { > {rand.eticheta} + {/* Cifra și unitatea stau în **același** text, una după alta, nu la + două x-uri fixe (1580 și 1720). Fiecare avea corpul înmulțit cu + `st`, deci pe un cadru îngust „3.628.800" creștea până sub + „operații" și cele două se scriau una peste alta. Într-un singur + text, ancorat la dreapta, golul e `dx` — crește și el cu corpul, + deci nu se mai poate închide. */} - operații - - @@ -746,10 +750,20 @@ function ActExplozie({ st }: { st: number }) { 18 + {/* Readuce linia de bază de unde a ridicat-o exponentul: + același corp, același `dy`, cu semnul schimbat. */} + ) : ( cuMii(Math.round(p * rand.valoare)) )} + + operații + = 0.999; - const corp = 34 * Math.min(st, 1.35); + // Cifra a crescut de la 34 odată cu scoaterea etichetei din colț: acum are + // celula pentru ea singură, iar cel mai lung text din desen („x₁", „40") are + // trei semne, deci încape lejer în cei 94 de unități de lățime interioară. + const corp = 38 * Math.min(st, 1.35); const culoare = pivot ? "var(--viz-pivot-text)" @@ -561,17 +568,6 @@ function Celula({ stroke={activ > 0.02 ? culoareRol(ROL_ACTIUNE) : "var(--bordura)"} strokeWidth={activ > 0.02 ? 2 + 2 * activ : 2} /> - {eticheta && ( - - )} {m < 0.999 && ( @@ -933,7 +929,6 @@ function Desen() { key={`m${linie}-${coloana}`} coloana={GX + coloana * CW} linie={GY + linie * CH} - eticheta={`${esteA ? "a" : esteB ? "b" : "c"}${indice(linie + 1)}`} vechi={num(valoare)} nou={dupa === undefined ? undefined : num(dupa)} trecere={dupa === undefined ? 0 : schimba(T, cand)} @@ -983,7 +978,6 @@ function Desen() { key={`d${linie}`} coloana={DCOL} linie={GY + linie * CH} - eticheta={`d${indice(linie + 1)}`} vechi={num(D[linie]!)} nou={linie > 0 ? num(D_DUPA_ELIMINARE[linie]!) : undefined} trecere={linie > 0 ? schimba(T, cand) : 0} diff --git a/src/components/content/InterfataInterpolare.tsx b/src/components/content/InterfataInterpolare.tsx index a736004..aaf874e 100644 --- a/src/components/content/InterfataInterpolare.tsx +++ b/src/components/content/InterfataInterpolare.tsx @@ -4,11 +4,7 @@ import { FUNCTII_INTERPOLATE, getFunctieInterpolata, } from "@/algorithms/interpolare-polinomiala/functii-interpolare"; -import { - esantioneaza, - multiplicatorLagrange, - polinomLagrange, -} from "@/algorithms/interpolare-polinomiala/lagrange"; +import { esantioneaza, polinomLagrange } from "@/algorithms/interpolare-polinomiala/lagrange"; import { schemaNeville, valoareNeville } from "@/algorithms/interpolare-polinomiala/neville"; import { evalueazaSpline, @@ -36,11 +32,12 @@ import { ControlPanel } from "@/components/viz/ControlPanel"; import { FormulaBlock } from "@/components/viz/FormulaBlock"; import { Legend, type ElementLegenda } from "@/components/viz/Legend"; import { Notatie } from "@/components/viz/Notatie"; +import { PlaybackBar } from "@/components/viz/PlaybackBar"; import { Plot } from "@/components/viz/Plot"; import { PlotCurba } from "@/components/viz/PlotCurba"; -import { PlotDreapta } from "@/components/viz/PlotDreapta"; import { PlotPunct } from "@/components/viz/PlotPunct"; import { PlotPunctTras } from "@/components/viz/PlotPunctTras"; +import { useDerulare } from "@/hooks/use-derulare"; import { zecimale } from "@/lib/numere"; import { cn } from "@/lib/utils"; @@ -75,14 +72,71 @@ const NODURI_IMPLICIT = 5; const ESANTIOANE = 400; /** - * Funcția cu care pornește interfața. + * Funcția cu care pornește fiecare filă. + * + * Sunt trei intrări, nu una, fiindcă fiecare filă își ține de acum funcția ei. + * Neville și Spline pornesc pe `x⁵ − 2x³`, din motivul măsurat mai jos. Lagrange + * pornește însă pe funcția lui Runge: fila ei e singurul loc rămas unde se vede + * un polinom întins peste tot intervalul, iar de când fenomenul nu mai are filă + * proprie, acolo se mai poate scoate la iveală trăgând de numărul de noduri. + * Spline-ul n-are de ce s-o care și el — de asta a fost despărțită starea. + * + * `x⁵ − 2x³`, fiindcă pe ea se vede diferența dintre cele două condiții de + * capăt ale spline-ului — singurul lucru de pe pagină care se citește doar + * comparând două curbe. Diferența dintre ele, măsurată ca depărtare maximă și + * transformată în pixeli pe înălțimea desenului: + * + * | noduri | 4 | 5 | 7 | 9 | 13 | + * | --------- | ---- | ---- | ---- | ---- | ---- | + * | sin(πx) | 8 | 2 | 0 | 0 | 0 | + * | x⁵ − 2x³ | 12 | 10 | 5 | 3 | 1,5 | + * + * Pe sinusoidă, la numărul implicit de noduri, cele două curbe stau la **doi + * pixeli** una de alta, iar de la nouă noduri se suprapun exact — adică fila + * arăta o singură linie și cerea să se creadă că sunt două. Pe polinom rămân + * despărțite pe aproape tot intervalul cursorului. * - * Sinusoida: polinomul stă lipit de curbă, deci se vede lucrul pe care îl arată - * fila, nu oscilația de la capete. E și funcția pe care cele două condiții de - * capăt ale spline-ului se despart cel mai tare — panta ei la capete e `±π`, - * iar spline-ul natural o ignoră din construcție. + * Celelalte două file nu pierd nimic: polinomul de interpolare nu iese din + * cadru la niciun număr de noduri (`max |Pₙ| = 1`, cât funcția însăși), iar de + * la șase noduri îl reproduce exact — pe puncte distincte, polinomul de grad + * cel mult `n` care trece prin ele e unic. + * + * Singurul loc unde nu se vede nimic e capătul de jos al cursorului: cu trei + * noduri, `(−1, 1)`, `(0, 0)` și `(1, −1)` sunt coliniare, deci amândouă + * condițiile dau chiar dreapta `y = −x`. Nu e o scăpare de calcul, e adevărat. */ -const FUNCTIE_IMPLICITA = "sinusoida"; +const FUNCTIE_IMPLICITA: Record = { + lagrange: "runge", + neville: "polinom", + spline: "polinom", +}; + +/** + * Tot ce ține de o filă și numai de ea. + * + * Nodurile nu se pot împărți între file odată ce funcția e a fiecăreia: sunt + * chiar valorile funcției alese acolo, iar `xEval` cade în intervalul ei. Altfel + * fila de Spline pornea cu nodurile funcției de pe Lagrange, puse pe altă curbă. + */ +type StareFila = { + idFunctie: string; + /** Câte noduri s-au cerut — se ține separat, ca să treacă peste schimbarea funcției. */ + cate: number; + noduri: Nod[]; + /** Punctul în care se evaluează schema Neville; nefolosit pe celelalte file. */ + xEval: number; +}; + +function stareInitiala(idFunctie: string, cate = NODURI_IMPLICIT): StareFila { + const f = getFunctieInterpolata(idFunctie); + const [a, b] = f.interval; + return { + idFunctie, + cate, + noduri: noduriEchidistante(f.f, f.interval, cate), + xEval: a + (b - a) * 0.62, + }; +} /* ───────────────────────── cadrul de desen ───────────────────────── */ @@ -105,61 +159,66 @@ function extremele(valori: readonly number[]): [number, number] { /** * Interfața interactivă a paginii 12. * - * **Un singur desen, patru întrebări.** Nodurile, funcția și cadrul rămân + * **Un singur desen, trei întrebări.** Nodurile, funcția și cadrul rămân * aceleași de la o filă la alta; se schimbă doar ce se desenează peste ele: - * multiplicatorul unui nod, nivelurile schemei Neville, oscilația de la capete - * sau bucățile spline-ului. Așa se vede că Lagrange și Neville dau **același** - * polinom, iar spline-ul e altceva — nu patru pagini care nu se ating. + * polinomul întreg, nivelurile schemei Neville sau bucățile spline-ului. Așa se + * vede că Lagrange și Neville dau **același** polinom, iar spline-ul e altceva — + * nu trei pagini care nu se ating. * - * **Nodurile se trag cu mâna** (`PlotPunctTras`), în afara filei cu funcția - * Runge: acolo fenomenul e chiar despre noduri echidistante, iar unul mutat cu - * degetul ar desființa exemplul. + * **Nodurile se trag cu mâna** (`PlotPunctTras`), pe toate filele. Fila cu + * funcția Runge, singura pe care nodurile stăteau pe loc — acolo fenomenul e + * chiar despre noduri echidistante —, a fost scoasă la cerere; funcția rămâne + * în lista din panou, deci oscilația se poate vedea în continuare. * * Matematica nu stă aici: vine din `src/algorithms/interpolare-polinomiala/`. */ export function InterfataInterpolare() { - const [idFunctie, setIdFunctie] = useState(FUNCTIE_IMPLICITA); - const [cate, setCate] = useState(NODURI_IMPLICIT); const [fila, setFila] = useState("lagrange"); const [tipSpline, setTipSpline] = useState("natural"); + const [stari, setStari] = useState>(() => ({ + lagrange: stareInitiala(FUNCTIE_IMPLICITA.lagrange), + neville: stareInitiala(FUNCTIE_IMPLICITA.neville), + spline: stareInitiala(FUNCTIE_IMPLICITA.spline), + })); + const { idFunctie, cate, noduri, xEval } = stari[fila]; const functie = getFunctieInterpolata(idFunctie); const [a, b] = functie.interval; - const [noduri, setNoduri] = useState(() => - noduriEchidistante( - getFunctieInterpolata(FUNCTIE_IMPLICITA).f, - getFunctieInterpolata(FUNCTIE_IMPLICITA).interval, - NODURI_IMPLICIT, - ), - ); - const [xEval, setXEval] = useState(() => a + (b - a) * 0.62); - const n = noduri.length - 1; + /** Schimbă doar fila deschisă; celelalte două rămân cum au fost lăsate. */ + const modifica = (schimbare: Partial) => + setStari((v) => ({ ...v, [fila]: { ...v[fila], ...schimbare } })); + + const setNoduri = (urmatoare: (vechi: Nod[]) => Nod[]) => + setStari((v) => ({ ...v, [fila]: { ...v[fila], noduri: urmatoare(v[fila].noduri) } })); + + const setXEval = (x: number) => modifica({ xEval: x }); + // Nivelurile schemei Neville se derulează, nu se trag dintr-un cursor din // panoul de parametri: sunt pași ai unui algoritm, iar pașii se iau cu // săgețile, sub desen, ca peste tot pe site. const derulareNivel = useDerulare(n + 1); const nivelAles = Math.min(derulareNivel.pas, n); - /** Nodurile se pot fi mutat de pe funcție; butonul de resetare le pune la loc. */ - const asazaPeFunctie = (cateNoduri = cate, f = functie) => - setNoduri(noduriEchidistante(f.f, f.interval, cateNoduri)); - - const asazaPe = (noua: ReturnType) => { - asazaPeFunctie(cate, noua); - setXEval(noua.interval[0] + (noua.interval[1] - noua.interval[0]) * 0.62); - }; - - const schimbaFunctia = (id: string) => { - setIdFunctie(id); - asazaPe(getFunctieInterpolata(id)); - }; - - const schimbaNumarul = (cateNoduri: number) => { - setCate(cateNoduri); - asazaPeFunctie(cateNoduri); + /** + * Altă funcție înseamnă alt interval și alte valori în noduri, deci fila + * pornește de la zero — dar păstrează câte noduri ceruse. + */ + const schimbaFunctia = (id: string) => modifica(stareInitiala(id, cate)); + + const schimbaNumarul = (cateNoduri: number) => + modifica({ + cate: cateNoduri, + noduri: noduriEchidistante(functie.f, functie.interval, cateNoduri), + }); + + /** Trecerea pe altă filă repune derularea la zero: pașii sunt ai lui Neville. */ + const schimbaFila = (noua: Fila) => { + setFila(noua); + derulareNivel.setPas(0); + derulareNivel.setRuleaza(false); }; /* ── ce se calculează ─────────────────────────────────────────────── */ @@ -262,6 +321,24 @@ export function InterfataInterpolare() { () => abatereMaxima((x) => evalueazaSpline(spline, x), functie.f, [a, b], 2000), [spline, functie, a, b], ); + + /** + * Cât de departe ajung una de alta cele două condiții de capăt. + * + * E cifra care spune dacă mai e ceva de văzut pe desen: sub câțiva pixeli, + * curbele se suprapun, iar asta nu se poate deosebi cu ochiul de o pană a + * interfeței. Se măsoară doar pe suportul nodurilor, unde spline-ul e definit. + */ + const departareaSplineurilor = useMemo( + () => + abatereMaxima( + (x) => evalueazaSpline(spline, x), + (x) => evalueazaSpline(splineCealalta, x), + [noduri[0]!.x, noduri[n]!.x], + 2000, + ), + [spline, splineCealalta, noduri, n], + ); /* ── mutarea unui nod ─────────────────────────────────────────────── */ const mutaNodul = (i: number, punct: Punct) => { @@ -312,6 +389,9 @@ export function InterfataInterpolare() { activa={FILE.findIndex((f) => f.id === fila)} /> + {/* Schimbarea filei nu mai atinge nodurile: toate trei lucrează pe funcția + aleasă din panou, deci nu mai există filă care să-și aducă funcția ei + și să le reașeze pe alta. */} schimbaFila(v as Fila)}> {FILE.map((f) => ( @@ -325,6 +405,16 @@ export function InterfataInterpolare() {
+ {/* Locul lăsat liber de triunghiul lui Neville, mutat sub bara de + derulare, se dă graficului — dar prin `raport`, nu prin + `inaltimeMaxima`. + + Măsurat: coloana din stânga se oprește la 707px și nu crește + nici pe ecran de 1536px, deci înălțimea ieșea din `707 / 1,6 = + 442px`. Plafonul nu era atins la nicio lățime, adică ridicat + singur n-ar fi mișcat niciun pixel. Cu `1,3` ies 544px, iar + plafonul rămâne pus ca să nu urce mai mult decât rândul de + cifre de alături. */} {/* Funcția de la care s-au luat nodurile. Punctată: nu e ce calculează metoda, ci ce ar vrea să nimerească. */} @@ -409,19 +499,18 @@ export function InterfataInterpolare() { ))} - +
{ - schimbaNumarul(NODURI_IMPLICIT); + modifica(stareInitiala(FUNCTIE_IMPLICITA[fila])); setTipSpline("natural"); - setXEval(a + (b - a) * 0.62); derulareNivel.setPas(0); + derulareNivel.setRuleaza(false); }} incorporat className="border-bordura min-w-0 border-t lg:border-t-0 lg:border-l" @@ -518,10 +607,10 @@ export function InterfataInterpolare() { fila, n, eroarePolinom, - eroareSpline, schema, nivel: nivelAles, spline, + departareaSplineurilor, noduri, }).map(([cheie, valoare]) => (
; nivel: number; spline: ReturnType; + departareaSplineurilor: number; noduri: readonly Nod[]; }): [string, string][] { if (fila === "lagrange") { @@ -721,6 +805,11 @@ function cifrele({ return [ ["bucăți", String(Math.max(0, noduri.length - 1))], ["s″(x₀)", zecimale(2 * (spline.c[0] ?? 0), 3)], + // Cifra care spune cât de mare e diferența pe care fila o desenează: + // `s″(x₀)` arată **de ce** se despart curbele, asta arată **cu cât**. + // Scrisă în cuvânt, nu ca `max |s − s̃|`: al doilea spline n-are simbol în + // curs, iar unul inventat aici ar cere el însuși o legendă. + ["depărtarea", zecimale(departareaSplineurilor, 4)], ]; } @@ -750,13 +839,6 @@ function legendaFilei(fila: Fila, tipSpline: TipSpline): ElementLegenda[] { forma: "linie", explicatie: "Suma multiplicatorilor, fiecare ponderat cu valoarea din nodul lui.", }, - { - rol: "interval", - eticheta: "multiplicatorul lₖ al nodului ales", - forma: "linie", - explicatie: "Trece prin 1 în nodul lui și prin 0 în toate celelalte.", - }, - { rol: "pivot", eticheta: "nodul ales și valoarea lₖ(xₖ) = 1", forma: "punct" }, ]; } @@ -768,28 +850,8 @@ function legendaFilei(fila: Fila, tipSpline: TipSpline): ElementLegenda[] { rol: "interval", eticheta: "polinoamele Pᵢⱼ de pe nivelul ales", forma: "linie", - explicatie: "Fiecare e trasat doar peste nodurile din care iese.", - }, - { rol: "grila", eticheta: "verticala punctului evaluat", forma: "linie-punctata" }, - { rol: "solutie", eticheta: "P₀ₙ(x), rezultatul schemei", forma: "punct" }, - ]; - } - - if (fila === "runge") { - return [ - LEGENDA_FUNCTIE, - { rol: "anterior", eticheta: "nodurile echidistante", forma: "punct" }, - { - rol: "curent", - eticheta: "polinomul de interpolare Pₙ", - forma: "linie", - explicatie: "Un singur polinom, pe tot intervalul.", - }, - { - rol: "solutie", - eticheta: "spline-ul cubic, pe aceleași noduri", - forma: "linie", - explicatie: "Câte o cubică pe fiecare subinterval, lipite în noduri.", + explicatie: + "Fiecare e trasat doar peste nodurile din care iese, cu un punct acolo unde e evaluat.", }, ]; } @@ -806,6 +868,13 @@ function legendaFilei(fila: Fila, tipSpline: TipSpline): ElementLegenda[] { ? "Curbura se anulează la capete." : "Panta de la capete e chiar cea a funcției.", }, + { + rol: "interval", + eticheta: tipSpline === "natural" ? "spline-ul tensionat" : "spline-ul natural", + forma: "linie-punctata", + explicatie: + "Cealaltă condiție de capăt, pe aceleași noduri: singura piesă cu care se compară.", + }, ]; } @@ -817,11 +886,10 @@ function descrieDesenul({ noduri, eroarePolinom, eroareSpline, - amplitudine, + departare, nivel, xEval, rezultat, - k, tipSpline, }: { fila: Fila; @@ -829,11 +897,10 @@ function descrieDesenul({ noduri: readonly Nod[]; eroarePolinom: number; eroareSpline: number; - amplitudine: number; + departare: number; nivel: number; xEval: number; rezultat: number; - k: number; tipSpline: TipSpline; }): string { const n = noduri.length - 1; @@ -841,8 +908,8 @@ function descrieDesenul({ if (fila === "lagrange") { return ( - `${comun} Se vede multiplicatorul nodului ${k}: valorează 1 în nodul lui și 0 în celelalte. ` + - `Polinomul de interpolare se abate de funcție cu cel mult ${zecimale(eroarePolinom, 4)}.` + `${comun} Se vede o singură linie, polinomul de interpolare: trece prin toate cele ` + + `${noduri.length} noduri și se abate de funcție cu cel mult ${zecimale(eroarePolinom, 4)}.` ); } if (fila === "neville") { @@ -851,15 +918,11 @@ function descrieDesenul({ `x = ${zecimale(xEval, 3)}. Rezultatul întregii scheme acolo e ${zecimale(rezultat, 5)}.` ); } - if (fila === "runge") { - return ( - `${comun} Nodurile sunt echidistante. Polinomul urcă până la ${zecimale(amplitudine, 3)} ` + - `și se abate de funcție cu ${zecimale(eroarePolinom, 4)}; spline-ul cubic pe aceleași ` + - `noduri se abate doar cu ${zecimale(eroareSpline, 4)}.` - ); - } + const ales = tipSpline === "natural" ? "natural" : "tensionat"; + const celalalt = tipSpline === "natural" ? "tensionat" : "natural"; return ( - `${comun} Spline-ul ${tipSpline === "natural" ? "natural" : "tensionat"} se abate de funcție ` + - `cu cel mult ${zecimale(eroareSpline, 4)}.` + `${comun} Spline-ul ${ales} se abate de funcție cu cel mult ` + + `${zecimale(eroareSpline, 4)}. Punctat, pe aceleași noduri, e spline-ul ${celalalt}: ` + + `cele două se depărtează una de alta cu cel mult ${zecimale(departare, 4)}.` ); } diff --git a/src/components/content/TeorieScurta.tsx b/src/components/content/TeorieScurta.tsx index a1f8e8f..8e2851a 100644 --- a/src/components/content/TeorieScurta.tsx +++ b/src/components/content/TeorieScurta.tsx @@ -38,6 +38,7 @@ function Bloc({ bloc }: { bloc: BlocTeorie }) { cu 16px mai mult loc. */} @@ -48,11 +49,18 @@ function Bloc({ bloc }: { bloc: BlocTeorie }) {

Ce înseamnă literele

-
+ {/* Pe telefon nu e grilă: simbolul și explicația curg în același + paragraf, ca al doilea rând al unei explicații lungi să înceapă + de la marginea din stânga, nu de sub semnul egal. Coloana de + explicații are ~200px pe 360px, deci aproape orice sens de două + propoziții se rupe — iar rupt, textul indentat sub „=" arăta ca + o intrare nouă, cu simbolul lipsă. Peste `sm` revine grila, unde + lățimea ajunge și alinierea semnelor egal chiar ajută la citit. */} +
{bloc.legenda.map((intrare) => (
{/* Pe ecran lat, aliniat la dreapta: semnele egal cad unul sub altul, deși simbolurile au lățimi diferite („a" față de @@ -61,13 +69,15 @@ function Bloc({ bloc }: { bloc: BlocTeorie }) { („r = √(x² + y²)") se rupe pe două rânduri: aliniat la dreapta, rândurile lui plecau din locuri diferite și nu se mai vedea unde începe intrarea. */} -
+
{intrare.simbol} -
-
{intrare.sens}
+ {" "} +
+ {intrare.sens} +
))}
diff --git a/src/components/content/VideoFft.tsx b/src/components/content/VideoFft.tsx index f56956f..3484d89 100644 --- a/src/components/content/VideoFft.tsx +++ b/src/components/content/VideoFft.tsx @@ -18,7 +18,6 @@ export function VideoFft() { id="htCj9exbGo0" titlu="The FFT Algorithm - Simple Step by Step" autor="Simon Xu" - limba="în engleză" miniatura={miniatura} descriere={ "Clip: transformata Fourier rapidă, pas cu pas. Se pornește de la suma care dă " + diff --git a/src/components/layout/ScrollToTop.tsx b/src/components/layout/ScrollToTop.tsx index d9e51b7..f83193d 100644 --- a/src/components/layout/ScrollToTop.tsx +++ b/src/components/layout/ScrollToTop.tsx @@ -1,5 +1,5 @@ import { useEffect, useLayoutEffect, useRef } from "react"; -import { useLocation } from "react-router"; +import { useLocation, useNavigationType } from "react-router"; /** * Cât așteptăm să apară elementul-țintă înainte să renunțăm și să mergem în @@ -18,13 +18,26 @@ const ASTEPTARE_TINTA_MS = 2000; const pozitii = new Map(); /** - * La schimbarea rutei pagina pornește de sus — **în afară de paginile pe care - * le-ai mai deschis o dată**, unde se așază unde rămăsese. Pe paginile lungi de - * aici capul paginii e la câteva ecrane distanță de locul din care ai plecat, - * iar plimbarea „ies o clipă, mă întorc" e cea obișnuită: se pleacă din mijlocul - * teoriei la cuprins sau la pagina vecină și se revine tot acolo. Nu contează - * cum te-ai întors — cu butonul „înapoi" al browserului sau printr-un link —, - * fiindcă poziția înseamnă același lucru în amândouă cazurile. + * La schimbarea rutei pagina pornește de sus — **în afară de întoarcerile cu + * butonul „înapoi"/„înainte" al browserului**, unde se așază unde rămăsese. Pe + * paginile lungi de aici capul paginii e la câteva ecrane distanță de locul din + * care ai plecat, iar plimbarea „ies o clipă, mă întorc" e cea obișnuită: se + * pleacă din mijlocul teoriei la cuprins și se revine tot acolo. + * + * **Cum te-ai întors contează**, și asta a fost lecția unui bug: apăsând + * săgeata „pagina următoare" ajungeai direct în subsolul paginii următoare, nu + * la titlul ei. Săgețile de navigație stau chiar la **fundul** paginii, deci + * orice pagină părăsită printr-una din ele își salvează poziția „la fund"; a + * doua oară când ajungeai pe ea, oricum ai fi ajuns, erai pus înapoi la fund. + * Măsurat pe `interpolare-polinomiala` → `curbe-bezier`: a doua trecere te lăsa + * la 6713 din 6966 pe telefon și la 5031 din 5031 pe desktop — adică exact la + * capătul de jos. Se vedea mai tare pe telefon, unde paginile sunt de două ori + * mai înalte, dar nu era o problemă de telefon. + * + * Deci poziția se reface **doar la `POP`** — înapoi/înainte din browser, adică + * gestul căruia browserul însuși îi reface derularea pe un site obișnuit. Un + * clic pe o legătură e `PUSH`: ai cerut altă pagină, deci o primești de la + * început. * * Fără `behavior: "smooth"` — `html { scroll-behavior: smooth }` din * `index.css` ar face un derulaj lung și inutil între pagini. @@ -37,6 +50,7 @@ const pozitii = new Map(); */ export function ScrollToTop() { const { pathname, hash } = useLocation(); + const tipNavigare = useNavigationType(); // Calea și poziția paginii pe care tocmai o părăsim. Amândouă în `ref`-uri, // fiindcă se citesc din curățarea efectului, când `pathname` de mai sus e deja @@ -74,10 +88,11 @@ export function ScrollToTop() { useEffect(() => { if (!hash) { - // Pagina se așază unde era, dacă am mai fost pe ea. Poziția se cere cadru - // cu cadru, din același motiv ca la ancoră — pagina se montează `lazy()`, - // deci în primul cadru documentul e prea scurt ca să se poată derula. - const salvata = pozitii.get(pathname); + // Pagina se așază unde era **doar dacă te-ai întors**, nu ori de câte ori + // ai mai fost pe ea. Poziția se cere cadru cu cadru, din același motiv ca + // la ancoră — pagina se montează `lazy()`, deci în primul cadru + // documentul e prea scurt ca să se poată derula. + const salvata = tipNavigare === "POP" ? pozitii.get(pathname) : undefined; if (salvata) { const limita = performance.now() + ASTEPTARE_TINTA_MS; let cadru = 0; @@ -117,7 +132,7 @@ export function ScrollToTop() { cadru = requestAnimationFrame(incearca); return () => cancelAnimationFrame(cadru); - }, [pathname, hash]); + }, [pathname, hash, tipNavigare]); return null; } diff --git a/src/components/viz/Clip.tsx b/src/components/viz/Clip.tsx index 05659ef..de65c68 100644 --- a/src/components/viz/Clip.tsx +++ b/src/components/viz/Clip.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ContextClip, type StareClip } from "@/components/viz/clip-context"; import { PlaybackClip } from "@/components/viz/PlaybackClip"; +import { BANDA_SUBTITRARE } from "@/components/viz/Subtitrari"; import { repere, type Scena } from "@/lib/compozitie"; import type { Viteza } from "@/lib/playback"; import { cn } from "@/lib/utils"; @@ -261,21 +262,32 @@ export function Clip({ scene, descriere, timpStatic, className, children }: Clip aria-keyshortcuts="Space ArrowLeft ArrowRight" > -
- {/* `@container`: textele din desen și din subtitrări se scalează după - lățimea cadrului, nu după cea a ferestrei — altfel pe telefon, în - peisaj, ar fi ilizibile. - - Pe tot ecranul, cadrul renunță la 16:9 și ia toată suprafața: SVG-ul - dinăuntru are `preserveAspectRatio` implicit, deci desenul rămâne - nedeformat și doar se centrează, pe același fundal ca pagina. */} + {/* `@container` stă pe `figure`, nu pe cadru: textele din desen și din + subtitrări se scalează după lățimea cadrului, nu după cea a + ferestrei — altfel pe telefon, în peisaj, ar fi ilizibile. Lățimea e + aceeași la amândouă (cadrul e `w-full`), dar unitățile `cq*` se + măsoară întotdeauna față de un **strămoș**, deci cadrul nu-și poate + citi propria lățime ca să-și calculeze rezerva de mai jos. */} +
+ {/* Pe tot ecranul, cadrul renunță la 16:9 și ia toată suprafața: + SVG-ul dinăuntru are `preserveAspectRatio` implicit, deci desenul + rămâne nedeformat și doar se centrează, pe același fundal ca + pagina. + + `paddingBottom` e rezerva subtitrării, și e zero pe orice cadru mai + lat de ~674px. Sub prag, corpul literei se oprește la podeaua lui + (altfel ar ajunge la 6px) și banda ar crește peste desen; atunci + cadrul îi dă înălțimea cerută, iar SVG-ul se micșorează cu ea. + Subtitrarea e poziționată pe `bottom-0`, adică pe marginea cutiei + de padding, deci rămâne exact în banda eliberată. */}
peisajStramt && setBaraCeruta((v) => !v)} + style={{ paddingBottom: BANDA_SUBTITRARE }} className={cn( - "border-bordura bg-fundal shadow-jos @container relative w-full overflow-hidden rounded-xl border", + "border-bordura bg-fundal shadow-jos relative w-full overflow-hidden rounded-xl border", plinEcran ? "h-full" : "aspect-video", )} > diff --git a/src/components/viz/FormulaBlock.tsx b/src/components/viz/FormulaBlock.tsx index 4baebb1..602f74c 100644 --- a/src/components/viz/FormulaBlock.tsx +++ b/src/components/viz/FormulaBlock.tsx @@ -16,9 +16,33 @@ export type FormulaBlockProps = { * se aprinde și în matrice, și în formulă. */ evidentiaza?: string[]; + /** + * Micșorează formula cât să încapă în casetă, în loc s-o lase să deruleze. + * + * Se pune **doar** pe formulele care n-au unde să se rupă — un tabel + * `array`, o matrice —, fiindcă acolo ruperea pe rânduri (vezi `index.css`) + * nu are ce tăia, iar singurul lucru rămas înaintea derulării e corpul mai + * mic. Ordinea din regulile de mobil e: întâi rupere, apoi corp mai mic, + * abia la urmă derulare. + */ + incapeInCaseta?: boolean; className?: string; }; +/** + * Sub ce mărime nu se coboară, oricât ar lipsi din lățime. + * + * Pragul e pe **corpul randat** (`.katex`), nu pe factorul de micșorare: un + * factor n-ar însemna nimic singur, fiindcă pornește de la `--text-formula`, + * care e el însuși fluid. Măsurat pe pagina metodelor iterative, la 360px: + * tabelul cere 459px într-o casetă de 252px, deci ajunge la 10,6px — iar + * rândurile lui, scrise cu `\large`, se văd la 12,7px. + * + * Ce n-ar încăpea nici așa e o formulă de rescris, nu de strâns mai departe: + * rămâne la 10px și derulează, ca înainte. + */ +const MARIME_MINIMA = 10; + /** * Afișează o formulă cu KaTeX. * @@ -31,6 +55,7 @@ export function FormulaBlock({ inline = false, eticheta, evidentiaza, + incapeInCaseta = false, className, }: FormulaBlockProps) { const [html, setHtml] = useState(null); @@ -135,6 +160,74 @@ export function FormulaBlock({ }; }, [html, inline]); + /** + * Micșorarea până încape, pentru formulele care n-au unde să se rupă. + * + * Mărimea se pune pe `.katex-display` (în `em`, deci se înmulțește cu + * `--text-formula`), nu pe `.katex`: foaia nelayerată a KaTeX îi impune + * acolo `font-size: 1.21em` și bate orice regulă de-a noastră — aceeași + * capcană descrisă în `index.css`. + * + * Se măsoară de fiecare dată **de la mărimea nemicșorată**; altfel al doilea + * apel ar împărți la o lățime deja strânsă și formula s-ar micșora la + * infinit. Un al doilea pas de ajustare e necesar fiindcă lățimea nu scade + * chiar liniar cu corpul (rotunjiri de glife): din prima încercare rămâneau + * 2px de derulare, măsurat. + */ + useEffect(() => { + const radacina = container.current; + if (!radacina || html === null || inline || !incapeInCaseta) return; + + let cadru = 0; + let latimeVazuta = -1; + + const potriveste = () => { + const zona = radacina.querySelector(".katex-display"); + if (!zona) return; + + zona.style.fontSize = ""; + const disponibil = zona.clientWidth; + const necesar = zona.scrollWidth; + if (disponibil === 0 || necesar <= disponibil) return; + + const corp = zona.querySelector(".katex"); + if (!corp) return; + const marime = parseFloat(getComputedStyle(corp).fontSize); + const factorMinim = Math.min(1, MARIME_MINIMA / marime); + + let factor = Math.max((disponibil - 1) / necesar, factorMinim); + zona.style.fontSize = `${factor}em`; + + for (let pas = 0; pas < 4; pas++) { + if (zona.scrollWidth <= zona.clientWidth || factor <= factorMinim) break; + factor = Math.max((factor * (zona.clientWidth - 1)) / zona.scrollWidth, factorMinim); + zona.style.fontSize = `${factor}em`; + } + }; + + const ruleaza = () => { + cancelAnimationFrame(cadru); + cadru = requestAnimationFrame(potriveste); + }; + + ruleaza(); + // Până sosesc fonturile KaTeX măsurăm fontul de rezervă, adică alte lățimi. + void document.fonts?.ready.then(ruleaza); + + const observator = new ResizeObserver(([intrare]) => { + const latime = intrare?.contentRect.width ?? 0; + if (latime === latimeVazuta) return; + latimeVazuta = latime; + ruleaza(); + }); + observator.observe(radacina); + + return () => { + cancelAnimationFrame(cadru); + observator.disconnect(); + }; + }, [html, inline, incapeInCaseta]); + // Evidențierea se aplică peste HTML-ul deja randat, ca să nu re-compilăm // formula la fiecare pas al animației. useEffect(() => { diff --git a/src/components/viz/PlaybackClip.tsx b/src/components/viz/PlaybackClip.tsx index 503f745..c21fa78 100644 --- a/src/components/viz/PlaybackClip.tsx +++ b/src/components/viz/PlaybackClip.tsx @@ -94,8 +94,27 @@ export function PlaybackClip({ {/* Al doilea rând pe telefon. Peste `sm`, învelișurile astea dispar din așezare (`contents`) și copiii lor redevin celule ale rândului unic — de aceea n-au `role`: un `role` pe un element cu `display: contents` - poate cădea din arborele de accesibilitate. */} -
+ poate cădea din arborele de accesibilitate. + + **Sub 400px toate golurile sunt `gap-1`, și rândul nu se mai împinge + cu `justify-between`.** Cele două nu sunt cerințe separate: + `justify-between` împărțea prisosul doar între cele trei grupuri de + nivel întâi, deci golul dintre redare și viteze era de câteva ori cât + cel dintre „reia" și „pornește" — iar când prisosul ajungea zero, + rândul se rupea, deși butoanele încă încăpeau. Cu goluri egale, + lățimea cerută e fixă și se poate socoti: 6 × 44px (ținta de atingere) + + 5 × 4px = 284px. Rama lasă `lățimea ecranului − 32` (`Container + px-4`) `− 2` (rama) `− 16` (`p-2`), deci rândul ține până pe la 334px + — sub etalonul de 360px, cu rezervă. `flex-wrap` rămâne pentru ce e + mai îngust de atât: acolo se rupe curat, între grupuri. + + **De la 400px în sus rămâne `justify-between`**, adică așezarea + dinainte: acolo prisosul e destul cât să nu rupă nimic, iar comenzile + întinse pe toată lățimea ramei citesc mai bine decât un pâlc lipit de + marginea din stânga, cu un gol cât un buton în dreapta. Pragul e scris + cu `min-[400px]:` fiindcă nu e o treaptă a temei — e exact lățimea de + la care rândul are loc să respire. */} +