Skip to content

Commit 944572b

Browse files
committed
refactor(transfer): CircularRing component animation fix
1 parent 16ba514 commit 944572b

1 file changed

Lines changed: 37 additions & 41 deletions

File tree

web-app/src/components/common/TransferProgressBar.tsx

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,20 @@ export function formatSpeed(speedBps: number): string {
1616
}
1717
}
1818

19-
// ─── Circular segmented ring (mobile only) ───────────────────────────────────
20-
//
21-
// 30 thin arc segments arranged clockwise from the top (12 o'clock).
22-
// Each segment occupies (360 / 30) = 12° with a small gap between them.
23-
// Fill logic mirrors the horizontal bar: full segments + one partial segment.
24-
25-
const SEGMENT_COUNT = 30
19+
const SEGMENT_COUNT = 18
2620
const SEGMENT_KEYS = Array.from(
2721
{ length: SEGMENT_COUNT },
2822
(_, i) => `segment-${i}`
2923
)
30-
const SEGMENT_ANGLE = 360 / SEGMENT_COUNT // 12° per segment
31-
const GAP_ANGLE = 2.2 // degrees of gap on each side
24+
const SEGMENT_ANGLE = 360 / SEGMENT_COUNT
25+
const GAP_ANGLE = 2.5
3226
const ARC_ANGLE = SEGMENT_ANGLE - GAP_ANGLE * 2
3327

34-
const RING_SIZE = 200 // SVG viewBox size (px)
35-
const CENTER = RING_SIZE / 2 // 100
36-
const RADIUS = 84 // arc radius
37-
const STROKE_WIDTH = 4.5 // thinner stroke for a sleeker look
28+
const RING_SIZE = 200
29+
const CENTER = RING_SIZE / 2
30+
const RADIUS = 84
31+
const STROKE_WIDTH = 4.5
3832

39-
/** Convert polar coordinates (angle from 12 o'clock, clockwise) to Cartesian. */
4033
function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
4134
const angleRad = ((angleDeg - 90) * Math.PI) / 180
4235
return {
@@ -45,7 +38,6 @@ function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
4538
}
4639
}
4740

48-
/** Build an SVG arc path for a segment starting at `startAngle` spanning `sweep` degrees. */
4941
function arcPath(startAngle: number, sweep: number): string {
5042
const start = polarToCartesian(CENTER, CENTER, RADIUS, startAngle)
5143
const end = polarToCartesian(CENTER, CENTER, RADIUS, startAngle + sweep)
@@ -61,9 +53,6 @@ function CircularRing({ percentage }: CircularRingProps) {
6153
const { t } = useTranslation()
6254
const filledSegments = Math.floor((percentage / 100) * SEGMENT_COUNT)
6355

64-
// How far into the current (partial) segment we are, as a 0–1 fraction
65-
const partialFraction = (percentage / 100) * SEGMENT_COUNT - filledSegments
66-
6756
return (
6857
<svg
6958
viewBox={`0 0 ${RING_SIZE} ${RING_SIZE}`}
@@ -78,37 +67,50 @@ function CircularRing({ percentage }: CircularRingProps) {
7867
>
7968
{SEGMENT_KEYS.map((segmentKey, index) => {
8069
const segmentStartAngle = index * SEGMENT_ANGLE + GAP_ANGLE
81-
8270
const isFilled = index < filledSegments
83-
const isPartial = index === filledSegments && partialFraction > 0
71+
const isPartiallyFilled =
72+
index === filledSegments &&
73+
percentage % (100 / SEGMENT_COUNT) > 0
8474

85-
// For the partial segment we shorten the visible arc proportionally
86-
const visibleSweep = isFilled
87-
? ARC_ANGLE
88-
: isPartial
89-
? ARC_ANGLE * partialFraction
90-
: 0
75+
let fillFraction = 0
76+
if (isFilled) {
77+
fillFraction = 1
78+
} else if (isPartiallyFilled) {
79+
fillFraction =
80+
(percentage % (100 / SEGMENT_COUNT)) / (100 / SEGMENT_COUNT)
81+
}
82+
83+
const arcD = arcPath(segmentStartAngle, ARC_ANGLE)
9184

9285
return (
9386
<g key={segmentKey}>
94-
{/* Background (unfilled) arc */}
9587
<path
96-
d={arcPath(segmentStartAngle, ARC_ANGLE)}
88+
d={arcD}
9789
fill="none"
9890
stroke="var(--input)"
9991
strokeWidth={STROKE_WIDTH}
100-
strokeLinecap="round"
101-
className="transition-all duration-300 ease-in-out"
92+
strokeLinecap="butt"
10293
/>
103-
{/* Filled arc (rendered on top) */}
104-
{visibleSweep > 0 && (
94+
{isFilled && (
10595
<path
106-
d={arcPath(segmentStartAngle, visibleSweep)}
96+
d={arcD}
10797
fill="none"
10898
stroke="var(--app-primary)"
10999
strokeWidth={STROKE_WIDTH}
110-
strokeLinecap="round"
111-
className="transition-all duration-300 ease-in-out"
100+
strokeLinecap="butt"
101+
/>
102+
)}
103+
{isPartiallyFilled && (
104+
<path
105+
d={arcD}
106+
fill="none"
107+
stroke="var(--app-primary)"
108+
strokeWidth={STROKE_WIDTH}
109+
strokeLinecap="butt"
110+
pathLength={100}
111+
strokeDasharray="100"
112+
strokeDashoffset={100 - fillFraction * 100}
113+
className="transition-[stroke-dashoffset] duration-300 ease-in-out"
112114
/>
113115
)}
114116
</g>
@@ -118,8 +120,6 @@ function CircularRing({ percentage }: CircularRingProps) {
118120
)
119121
}
120122

121-
// ─── Main component ───────────────────────────────────────────────────────────
122-
123123
export function TransferProgressBar({ progress }: TransferProgressBarProps) {
124124
const { percentage } = progress
125125
const barCount = 30
@@ -128,12 +128,10 @@ export function TransferProgressBar({ progress }: TransferProgressBarProps) {
128128

129129
return (
130130
<div className="space-y-3">
131-
{/* ── Mobile layout: circular ring ── */}
132131
<div className="sm:hidden flex flex-col items-center gap-3">
133132
<div className="relative inline-flex items-center justify-center">
134133
<CircularRing percentage={percentage} />
135134

136-
{/* Labels centred inside the ring */}
137135
<div className="absolute inset-0 flex flex-col items-center justify-center gap-0.5 text-center pointer-events-none">
138136
<span className="text-2xl font-normal leading-none tabular-nums">
139137
{percentage.toFixed(1)}%
@@ -154,7 +152,6 @@ export function TransferProgressBar({ progress }: TransferProgressBarProps) {
154152
</div>
155153
</div>
156154

157-
{/* ── Desktop layout: horizontal segment bars ── */}
158155
<div className="hidden sm:block space-y-2">
159156
<div className="flex items-center justify-between text-xs">
160157
<span>{t('common:transfer.progress')}</span>
@@ -178,7 +175,6 @@ export function TransferProgressBar({ progress }: TransferProgressBarProps) {
178175

179176
return (
180177
<div
181-
// biome-ignore lint/suspicious/noArrayIndexKey: The values are always static so it is okay
182178
key={index}
183179
className="relative flex-1 rounded-sm bg-input transition-all duration-300 ease-in-out"
184180
style={{ minWidth: '3px', height: '100%' }}

0 commit comments

Comments
 (0)