I am trying to draw a gradient background in Jetpack Compose, and I would like the gradient to have a fixed angle regardless of the shape of the object I'm drawing into.
However, using Modifier.background(brush=...)
, the best I can find is linearGradient
which calculates the angle from a fixed start and end point of a gradient.
For example, is there a way I can specify that I want a 45 degree angle for my gradient without knowing the final size it's going to be?
Edit: I would like a solution that can work for any given angle, not just 45 degrees.
I realised there is an error in the original code, which distorts the gradient angle. Some more trigonometry is needed in order to constrain the gradient start and ends to within the canvas area (if that is what is desired) while also preserving the gradient angle. Here is the updated solution, with bonus ASCII art.
fun Modifier.angledGradientBackground(colors: List<Color>, degrees: Float) = this.then(
drawBehind {
/*
Have to compute length of gradient vector so that it lies within
the visible rectangle.
--------------------------------------------
| length of gradient ^ / |
| ---> / / |
| / / <- rotation angle |
| / o --------------------| y
| / / |
| / / |
| v / |
--------------------------------------------
x
diagonal angle = atan2(y, x)
(it's hard to draw the diagonal)
Simply rotating the diagonal around the centre of the rectangle
will lead to points outside the rectangle area. Further, just
truncating the coordinate to be at the nearest edge of the
rectangle to the rotated point will distort the angle.
Let α be the desired gradient angle (in radians) and γ be the
angle of the diagonal of the rectangle.
The correct for the length of the gradient is given by:
x/|cos(α)| if -γ <= α <= γ, or π - γ <= α <= π + γ
y/|sin(α)| if γ <= α <= π - γ, or π + γ <= α <= 2π - γ
where γ ∈ (0, π/2) is the angle that the diagonal makes with
the base of the rectangle.
*/
val (x, y) = size
val gamma = atan2(y, x)
if (gamma == 0f || gamma == (PI / 2).toFloat()) {
// degenerate rectangle
return@drawBehind
}
val degreesNormalised = (degrees % 360).let { if (it < 0) it + 360 else it }
val alpha = (degreesNormalised * PI / 180).toFloat()
val gradientLength = when (alpha) {
// ray from centre cuts the right edge of the rectangle
in 0f..gamma, in (2*PI - gamma)..2*PI -> { x / cos(alpha) }
// ray from centre cuts the top edge of the rectangle
in gamma..(PI - gamma).toFloat() -> { y / sin(alpha) }
// ray from centre cuts the left edge of the rectangle
in (PI - gamma)..(PI + gamma) -> { x / -cos(alpha) }
// ray from centre cuts the bottom edge of the rectangle
in (PI + gamma)..(2*PI - gamma) -> { y / -sin(alpha) }
// default case (which shouldn't really happen)
else -> hypot(x, y)
}
val centerOffsetX = cos(alpha) * gradientLength / 2
val centerOffsetY = sin(alpha) * gradientLength / 2
drawRect(
brush = Brush.linearGradient(
colors = colors,
// negative here so that 0 degrees is left -> right
// and 90 degrees is top -> bottom
start = Offset(center.x - centerOffsetX,center.y - centerOffsetY),
end = Offset(center.x + centerOffsetX, center.y + centerOffsetY)
),
size = size
)
}
)
This was my final solution based on @Ehan msz's code. I tweaked his solution so that 0 degrees corresponds to a left-to-right gradient direction, and 90 degrees corresponds to a top-to-bottom direction.
fun Modifier.angledGradient(colors: List<Color>, degrees: Float) = this.then(
Modifier.drawBehind {
val rad = (degrees * PI / 180).toFloat()
val diagonal = sqrt(size.width * size.width + size.height * size.height)
val centerOffsetX = cos(rad) * diagonal / 2
val centerOffsetY = sin(rad) * diagonal / 2
// negative so that 0 degrees is left -> right and 90 degrees is top -> bottom
val startOffset = Offset(
x = (center.x - centerOffsetX).coerceIn(0f, size.width),
y = (center.y - centerOffsetY).coerceIn(0f, size.height)
)
val endOffset = Offset(
x = (center.x + centerOffsetX).coerceIn(0f, size.width),
y = (center.y + centerOffsetY).coerceIn(0f, size.height)
)
drawRect(
brush = Brush.linearGradient(
colors = colors,
start = startOffset,
end = endOffset
),
size = size
)
}