Skip to content

Instantly share code, notes, and snippets.

@lucanicoletti
Created June 9, 2025 08:46
Show Gist options
  • Save lucanicoletti/0aa6376eba31b761238bac3a9b08871d to your computer and use it in GitHub Desktop.
Save lucanicoletti/0aa6376eba31b761238bac3a9b08871d to your computer and use it in GitHub Desktop.
RibbonModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.PathMeasure
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin
private fun Modifier.ribbon(
brush: Brush,
stroke: Dp = 4.dp,
loops: Int = 7,
progress: Float = 1f,
): Modifier {
return drawWithCache {
val loops = loops - .5f
val ribbonPath = createRibbon(
start = Offset(0f, size.height * .5f),
end = Offset(size.width, size.height * .5f),
radius = (size.height * .5f) + stroke.toPx(),
startAngle = -90f,
loops = loops,
)
val measure = PathMeasure()
measure.setPath(ribbonPath, false)
var isPositive = measure.getTangent(0f).y > 0f
val distanceArray = mutableListOf<Float>()
var segmentStartDistance = 0f
val resolution = 500
for (i in 0..resolution) {
val t = i / resolution.toFloat()
val distance = t * measure.length
val tan = measure.getTangent(distance)
val currentIsPositive = tan.y > 0f
if (currentIsPositive != isPositive) {
val segmentLength = distance - segmentStartDistance
distanceArray.add(segmentLength)
segmentStartDistance = distance
isPositive = currentIsPositive
} else if (i == resolution) {
val segmentLength = distance - segmentStartDistance
distanceArray.add(segmentLength)
}
}
onDrawWithContent {
if (progress > 0f)
drawPath(
path = ribbonPath,
brush = brush,
style = Stroke(
width = stroke.toPx(),
cap = StrokeCap.Round,
join = StrokeJoin.Round,
pathEffect = PathEffect.chainPathEffect(
PathEffect.dashPathEffect(
intervals = distanceArray.toFloatArray(),
),
PathEffect.dashPathEffect(
intervals = floatArrayOf(
measure.length * progress,
measure.length,
)
)
)
)
)
drawContent()
if (progress > 0f)
drawPath(
path = ribbonPath,
brush = brush,
style = Stroke(
width = stroke.toPx(),
cap = StrokeCap.Round,
join = StrokeJoin.Round,
pathEffect = PathEffect.chainPathEffect(
PathEffect.dashPathEffect(
intervals = distanceArray.toFloatArray(),
phase = distanceArray.first()
),
PathEffect.dashPathEffect(
intervals = floatArrayOf(
measure.length * progress,
measure.length,
)
)
)
)
)
}
}
}
private fun createRibbon(
start: Offset,
end: Offset,
radius: Float,
startAngle: Float = 90f,
loops: Float = 5f,
resolution: Int = 1000
): Path {
val ribbon = Path()
ribbon.moveTo(start)
(0..resolution).forEach { i ->
val t = i / resolution.toFloat()
val min = min(startAngle, (360f * loops) - startAngle)
val max = max(startAngle, (360f * loops) - startAngle)
val degree = lerp(
start = min,
stop = max,
fraction = t
)
if (i == 0) {
ribbon.polarMoveTo(
degrees = degree,
distance = radius,
origin = start
)
}
ribbon.polarLineTo(
degrees = degree,
distance = radius,
origin = androidx.compose.ui.geometry.lerp(
start = start,
stop = end,
fraction = t,
),
)
}
return ribbon
}
private fun Path.polarMoveTo(
degrees: Float,
distance: Float,
origin: Offset = Offset.Zero
) = moveTo(polarToCart(degrees, distance, origin))
private fun Path.polarLineTo(
degrees: Float,
distance: Float,
origin: Offset = Offset.Zero
) = lineTo(polarToCart(degrees, distance, origin))
private fun polarToCart(
degrees: Float,
distance: Float,
origin: Offset = Offset.Zero
): Offset = Offset(
x = distance * cos(-degrees * (PI / 180)).toFloat(),
y = distance * sin(-degrees * (PI / 180)).toFloat(),
) + origin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment