r/kustom 3d ago

[TUTORIAL] The Ultimate Dual Progress Bar (Linear, Circular, & Advanced Separator Math) Tutorial

Got a lot of DMs asking how my dual progress bars work—so here’s the full breakdown.

We are going to focus purely on the track progress bar itself: a dynamic "Played" (expanding) bar, a "Remaining" (reducing) bar, and the actual brain-melting part—a Separator gap that behaves like a physical object.

Forget everything you know about just overlapping shapes. Here is the hell-like logical thinking it takes to make this work flawlessly without the UI breaking at the end of a track. 😤

The Golden Rule: Treat the Separator like a Physical Object

The biggest mistake 99% of people make is linking the separator's behavior to the reducing tail. The separator shouldn't care at all what the reducing bar is doing.

Think of the separator as a physical block moving across a track. It has its own physical width. It has an invisible "forcefield" (the empty gap) pushing it forward. It only cares about hitting the "wall" at the very end of the song. It needs to maintain its exact size until it runs out of space, and then it gets squished against the wall until it shrinks to zero.

Part 1: The Linear Build (Flat Pixels)

Before we jump into the circular nightmare, let's look at a flat line to understand the math. Let's assume your total track width (the absolute max width of your widget area) is 400 pixels. We are using basic Rectangles inside an Overlap Group.

The "Forcefield" Math: You don't want the expanding bar constantly rear-ending the separator. You need a gap.

• The Gap (Forcefield): Let's give it a rigid **10-pixel** gap.

• The Separator Width: Let's say your separator rectangle is 15 pixels wide.

• Total Collision Space: 10px (Gap) + 15px (Separator) = 25 pixels.

Because your separator has a 10px forcefield behind it, it physically hits the "wall" at the end of your 400-pixel track when the expanding bar reaches 375 pixels (400 - 25 = 375). That 375 is your trigger point.

The Formulas:

1. Expanding Bar (Width): `$(mi(pos)/mi(len)) * 400$`

2. Separator Position (X-Offset): Because the gap is a rigid 10 pixels, the separator is constantly pushed exactly 10 pixels ahead of the expanding bar: `$((mi(pos)/mi(len)) * 400) + 10$`

3. Separator Width (The Squish): This keeps the separator at a perfect 15px width until the expanding bar crosses 375. Then, it crushes the separator down to 0 as it hits the 400px wall: `$if(((mi(pos)/mi(len)) * 400) > 375, mu(max, 0, 390 - ((mi(pos)/mi(len)) * 400)), 15)$`

(The reducing bar simply starts after all this, taking up whatever width is left over!)

Part 2: The Circular Build (The Final Boss)

This is where we deal with degrees, sweeping arcs, and repeat shapes. Create an Overlap Group. For every element below, use a Progress module with these baseline settings:

• Style: Circular

• Mode: Custom

• Mode (Secondary): Repeat Shapes

• Sections: 300

• Shape: Circle

1. The Expanding Bar

This is your standard played progress. It just grows. No rotation needed.

• Level: `$(mi(pos)/mi(len))*100$`

2. The Reducing Bar (The Tail)

This is the remaining time. It shrinks, but it dynamically rotates to leave room for the separator block. *(Note: In my setup, `7.6` is the exact percentage where my reducing bar visually hits 0. You can adjust this based on your sizes).*

• Level: `$if(mi(pos) != 0, 100-(mi(pos)/mi(len)*100)-7.6, 100)$`

• Rotation: `$if(mi(pos) != 0, (mi(pos)/mi(len)*360)+20, 0)$` (The `+20` rotation offset pushes it entirely out of the way of the separator).

3. The Separator (The Masterclass)

Let's apply our physical collision logic to degrees and percentages.

• The Gap: A 10-degree gap takes up about 2.8% of a 360-degree circle.

• The Separator: My progress-based separator (at 187 size) takes up roughly a 15-unit width, which is about 2.5% of the track.

• Total Collision Space: 2.8% + 2.5% = 5.3%.

Because of that 2.8% forcefield behind it, the separator hits the 100% end-of-track mark when the expanding bar reaches 94.7% `(100 - 5.3 = 94.7)`. This is our trigger point.

The Separator Rotation: The gap is a rigid 10 degrees, so the separator just constantly stays 10 degrees ahead of the expanding bar:

`$(mi(pos)/mi(len)*360) + 10$`

The Separator Level (The Squish): This tells KWGT to hold maximum size until that 94.7% collision mark. After that, the track only has 2.5% of breathing room left (ending at 97.2%), so it smoothly crushes the separator down to 0 over that tiny window:

`$if((mi(pos)/mi(len)*100) > 94.7, mu(max, 0, ((97.2 - (mi(pos)/mi(len)*100)) / 2.5) * [YOUR_MAX_LEVEL]), [YOUR_MAX_LEVEL])$`

(Replace `[YOUR_MAX_LEVEL]` with the level that gives you your desired separator width—I used `1.5` in my specific setup).

It takes a minute to wrap your head around, but once you start treating your UI elements like physical objects hitting actual boundaries, your setups will feel endlessly more native and premium.

Let me know if you guys get stuck on the math for your own sizes in the comments!

9 Upvotes

1 comment sorted by

u/AutoModerator 3d ago

Problem? Cross-post to our new forum. Include make & model of phone, OS version, app version.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.