Happy new year ![]()
This week there was a question asked in Slack about applying colors in thick strokes. I learned something new, which I wanted to share here.
I already knew how to create a stroke that starts with one color and ends with another. This was a related question:
In that example, we just modulated the color between two colors. The trick was to use the c_contourPosition shadeStyle variable as the parameter for a sine function.
Since c_contourPosition contains the contour position in pixels, if you want to start the contour with one color and end with another, you need to know the length of the contour in the shadeStyle. We can pass that length to the shader using parameter("len", c.length), like this:
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.imageFit.FitMethod
import org.openrndr.extra.imageFit.fit
import org.openrndr.extra.noise.scatter
import org.openrndr.extra.shapes.hobbycurve.hobbyCurve
fun main() = org.openrndr.application {
program {
val points = drawer.bounds.scatter(100.0).take(7)
val c = hobbyCurve(points, false)
val cFit = c.transform(
c.bounds.fit(
drawer.bounds.offsetEdges(-150.0),
fitMethod = FitMethod.Contain
)
)
val shdr = shadeStyle {
fragmentTransform = """
x_stroke.rgb = mix(x_stroke.rgb, p_endColor.rgb, c_contourPosition / p_len);
""".trimIndent()
}
extend {
drawer.clear(ColorRGBa.WHITE)
drawer.strokeWeight = 75.0
drawer.shadeStyle = shdr
drawer.stroke = ColorRGBa.PINK
shdr.parameter("len", c.length)
shdr.parameter("endColor", ColorRGBa.CYAN)
drawer.contour(cFit)
}
}
}
Notice how we divide c_contourPosition / p_len to obtain a value that is 0.0 at the start of the contour and 1.0 where it ends. The program creates something like this:
Now what if you also want to interpolate colors across the width of the stroke? That’s what I discovered this week. What you can do is to use the va_texCoord0.x variable, which contains a value that is 0.0 in one side of the contour and 1.0 in the opposite side.
For instance, we could make one side 0.5 darker like this:
val shdr = shadeStyle {
fragmentTransform = """
x_stroke.rgb = mix(x_stroke.rgb, p_endColor.rgb, c_contourPosition / p_len);
x_stroke.rgb *= (va_texCoord0.x * 0.5 + 0.5);
""".trimIndent()
}
What we do there is to multiply the red, green and blue components of each pixel by a value between 0.5 and 1.0, resulting in a gradient that goes from PINK to CYAN on one edge of the thick contour, and darker versions of those colors in the opposite edge.
It looks like this:
By twisting the shader code a bit we can darken both edges making it look somewhat 3D:
val shdr = shadeStyle {
fragmentTransform = """
x_stroke.rgb = mix(x_stroke.rgb, p_endColor.rgb, c_contourPosition / p_len);
x_stroke.rgb *= 1.0 - pow(abs(va_texCoord0.x * 2.0 - 1.0), 2.0) * 0.5;
""".trimIndent()
}
I’ll try to explain what goes on in the second line of the shader code: we take va_texCoord0.x which is a number between 0 and 1. By multiplying it by 2.0 and subtracting 1.0, we now have numbers between -1.0 on one edge and 1.0 in the opposite edge. If we then take its absolute value, we go from 1.0 to 0.0 and then back to 1.0, shaped like a v. To make this v more like a u, we do pow with a value greater than 1.0, in this case, 2.0. You could go higher to make the dark edges thinner. I reverse that with 1.0 - because I want dark at the sides, not at the center. Finally I multiply by 0.5 to avoid full black on the sides, getting something more subtle. It looks like this:


