# Closed curve coordinates: An exercise

**URL:** <https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452>\
**Category:** Uncategorized\
**Created:** [November 5, 2022, 11:54am UTC](https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452 "2022-11-05T11:54:38Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![Alessandro](https://yyz2.discourse-cdn.com/free1/user_avatar/openrndr.discourse.group/alessandro/32/412_2.png) [@Alessandro](https://openrndr.discourse.group/u/Alessandro)\
**Post date:** [November 5, 2022, 11:54am UTC](https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452/1 "2022-11-05T11:54:38Z")

</div>

I wanted to see if I could make something dynamic with closed contours entirely on the fragment shader via using the c\_contourPosition uniform, so to give a moving particle illusion but where everything is actually really static under the hood.  
Here’s the code

```kotlin
    import org.openrndr.KEY_ENTER
    import org.openrndr.application
    import org.openrndr.color.ColorRGBa
    import org.openrndr.draw.*
    import org.openrndr.extra.fx.blur.GaussianBlur
    import org.openrndr.extra.noise.uniform
    import org.openrndr.extra.videoprofiles.gif
    import org.openrndr.ffmpeg.ScreenRecorder
    import org.openrndr.math.Vector2
    import org.openrndr.math.chaikinSmooth
    import org.openrndr.shape.contour
    import kotlin.math.*
    
    
    fun main() = application {
        configure {
            width = 1000
            height = 1000
        }
    
        program {
            var t = 0.0
           
            val rt = renderTarget(width, height){
                colorBuffer()
                depthBuffer()
            }
    
            val rtBright = renderTarget(width, height){
                colorBuffer()
                depthBuffer()
            }
    
            val img = colorBuffer(width, height)
    
            val blur = GaussianBlur()
    
    
            val nVerts = 3
            val rad = 80.0
            val scl = 0.2
            val nContours = 20
    
            val pnts = List(nVerts) {
                val offset = Double.uniform(rad - 20.0, rad + 20.0)
                val x = (rad + offset) * cos(it * 2.0/nVerts * PI)
                val y = (rad + offset) * sin(it * 2.0/nVerts * PI)
                Vector2(x, y)
            }
    
            val points = chaikinSmooth(pnts, 10, true)
    
            val contours = List(nContours) {
                contour {
                    moveTo(points[0] * (1.0 + scl * it ))
                    for (i in 1 until points.size) {
                        lineTo(points[i] * (1.0 + scl * it ))
                    }
                    lineTo(points[0] * (1.0 + scl * it ))
    
                }
            }

            val speeds = contours.map{Double.uniform(0.1, 0.4)}     

            var backCol = ColorRGBa.fromHex("#232323")
    
            extend(ScreenRecorder()) {
                gif()
            }
            extend {
    
                drawer.fill = null
                drawer.stroke = ColorRGBa.WHITE
                drawer.strokeWeight = 4.0
    
    
                drawer.isolatedWithTarget(rt) {
                    drawer.clear(backCol)
                    contours.forEachIndexed { id, c ->
                        drawer.shadeStyle = shadeStyle {
                            fragmentTransform = """ 
                        // Normalize the curve coordinate between 0.0 and 1.0;
                        float t = c_contourPosition/p_l;
                        // Define starting and ending point, namely b and a;
                        float a = mod(p_t, 1.0);
                        float b = mod(p_t + p_d, 1.0);
                        // Create a "step function" which is 1.0 if a < t < b, and 0.0 otherwise;
                        // Notice that if a > b we have to change the condition since the coordinate t will jump when crossing the origin
                        float d = step(a, b ) * step(a, t) * step(t, b) + step(b, a) * (1.0 - step(b, t) * step(t, a));
                        // Make a gradient going from the background color to white;
                        vec3 br = mix(p_backCol.xyz, vec3(1.0), d);
                        // Compute the mixing coefficient for the color gradient, normalized to (0.0, 1.0), and taking into account when 0 is included in the 
                        // moving window; 
                        float mixCoeff = step(a,b) * (b - t)/(b-a) + step(b, a) * (step(t, b) * ( b - t)/(b + 1.0 - a) + step(b, t) * (b + 1.0 - t)/(b + 1.0 - a));
                        // Create the mixing color in such a way that the line gradient fades to background because art :)
                        vec3 col1 = vec3(34, 193, 295)/255.0;
                        vec3 col2 = vec3(253, 187, 45)/255.0;
                        vec3 col = mix(p_backCol.xyz, pow(vec3(d) * mix(col1, col2, mixCoeff), vec3(1.0)), 1.0 - 0.99 * mixCoeff * d);
                        vec3 color = col + (1.0-vec3(d)) * p_backCol.xyz;
                        x_stroke.rgb = color;
                    """.trimIndent()
                            parameter("l", c.length)
                            parameter("t", t * (1.0 + speeds[id]))
                            parameter("d", 500.0/c.length )
                            parameter("backCol", backCol.toVector4())
    
                        }
                        drawer.pushTransforms()
                        drawer.translate(drawer.bounds.center)
                        drawer.contour(c)
                        drawer.popTransforms()
                    }
                }
    
                rt.colorBuffer(0).copyTo(img)
    
                // Add bloom effect
                if (mouse.position.x < 0.8 * width) {
                    drawer.isolatedWithTarget(rtBright) {
                        drawer.fill = ColorRGBa.WHITE
                        drawer.stroke = null
                        drawer.shadeStyle = shadeStyle {
                            fragmentTransform = """
                            vec2 coord = c_boundsPosition.xy;
                            coord.y = 1.0 - coord.y;
                            vec3 color = texture(p_image, coord).xyz;
                            float br = dot(color, vec3(1.0))/3.0;
                            x_fill.rgb = vec3(step(0.65, br));
                        """.trimIndent()
                            parameter("image", img)
                        }
                        drawer.rectangle(drawer.bounds)
                    }
    
                    blur.window = 10
                    blur.spread = 2.0
                    blur.apply(rtBright.colorBuffer(0), rtBright.colorBuffer(0))
                    blur.apply(rtBright.colorBuffer(0), rtBright.colorBuffer(0))
    
    
                    drawer.isolatedWithTarget(rt) {
                        drawer.fill = ColorRGBa.WHITE
                        drawer.stroke = null
                        drawer.shadeStyle = shadeStyle {
                            fragmentTransform = """
                            vec2 coord = c_boundsPosition.xy;
                            coord.y = 1.0 - coord.y;
                            vec3 color = texture(p_image, coord).xyz;
                            vec3 blurCol = texture(p_image, coord).xyz;
                            x_fill.rgb = color + blurCol;
                        """.trimIndent()
                            parameter("image", img)
                            parameter("blurred", rtBright.colorBuffer(0))
    
    
                        }
                        drawer.rectangle(drawer.bounds)
                    }
    
                }
    
                if (mouse.position.x < 0.8 * width) drawer.image(rt.colorBuffer(0)) else drawer.image(img)
                t += 0.005
            }
        }
    }

```

![gif1](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/bd88ded500b0c910b9a0a9d83d72dc977365cc91.gif)

The basic idea is quite simple as well mischievious: just consider a moving window (a, b) and check if the normalized coordinate t is such that a \< t \< b. All fine, until you realize that a closed curve cannot have a single continuous coordinate, since you have to identify the value 0.0 and 1.0 (mathematicians would say that you [cannot cover a closed simple curve with a single chart](https://en.wikipedia.org/wiki/Atlas_(topology)) ). This problem is solved in the fragment shader with a little bit of “step function acrobatics” to avoid branching, and to have a nice gradient. I think the shader can be further optimized but I am kinda happy with the result. A next step would be to make use of the alpha channel, this should simplify the mixing part.  
I have also added a self-made bloom effect: if you move the mouse to the far right you can see how taxing the double gaussian blur pass really is.

---

<div class="post-metadata">

**Author:** ![abe](https://yyz2.discourse-cdn.com/free1/user_avatar/openrndr.discourse.group/abe/32/700_2.png) [@abe](https://openrndr.discourse.group/u/abe)\
**Post date:** [November 5, 2022, 3:18pm UTC](https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452/2 "2022-11-05T15:18:21Z")

</div>

Thank you for sharing! Much fun! I wrote this variation:

```auto
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.noise.Random
import org.openrndr.shape.Circle

fun main() = application {
    program {
        val c = List(20) {
            Circle(drawer.bounds.center, 100.0 + it * 10.0).contour
        }
        val style = shadeStyle {
            fragmentPreamble = """
                #define linearstep(edge0, edge1, x) clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
            """.trimIndent()
            fragmentTransform = """
                float t = c_contourPosition / p_length;
                float alpha = fract(t - p_time + p_offset);
                // make alpha zero below 0.7, then fade to 1.0
                alpha = linearstep(0.7, 1.0, alpha);
                // square alpha to bias the mix towards the first color
                x_stroke.rgb = mix(vec3(1.0, 0.0, 1.0), vec3(0.0, 1.0, 1.0), alpha*alpha);
                x_stroke.a = alpha;
            """.trimIndent()
        }

        extend {
            drawer.clear(ColorRGBa.WHEAT.shade(0.2))
            drawer.strokeWeight = 4.0
            drawer.fill = null
            drawer.shadeStyle = style
            style.parameter("time", seconds * 0.3)
            c.forEachIndexed { i, c ->
                style.parameter("offset", Random.perlin(i * 0.303, seconds * 0.03 + i * 0.808))
                style.parameter("length", c.length)
                drawer.contour(c)
            }
        }
    }
}

```

![TemplateProgram-2022-11-05-16.20.01](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/eba62cb1316c0f280989f2b01e947ee01308b065.png)

I added a simple `linearstep` method. Online one can find other variations of `smoothstep`, like `circularstep` and others.

---

<div class="post-metadata">

**Author:** ![Alessandro](https://yyz2.discourse-cdn.com/free1/user_avatar/openrndr.discourse.group/alessandro/32/412_2.png) [@Alessandro](https://openrndr.discourse.group/u/Alessandro)\
**Post date:** [November 5, 2022, 3:27pm UTC](https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452/3 "2022-11-05T15:27:56Z")

</div>

Very nice! I guess this is the _dual_ picture, where you keep the window (a, b) fixed, and move the coordinate t.

---

<div class="post-metadata">

**Author:** ![Alessandro](https://yyz2.discourse-cdn.com/free1/user_avatar/openrndr.discourse.group/alessandro/32/412_2.png) [@Alessandro](https://openrndr.discourse.group/u/Alessandro)\
**Post date:** [November 6, 2022, 12:32pm UTC](https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452/4 "2022-11-06T12:32:09Z")

</div>

Here’s a variation of the variation, where I basically compute in advance the flow of some dynamical system with a stable orbit. 🙂

```kotlin
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.noise.Random
import org.openrndr.math.Vector2
import org.openrndr.math.map
import org.openrndr.shape.*

fun main() = application {
    configure {
        width = 1000
        height = 1000
    }

    program {

        fun getFlow(start: Vector2, nSteps: Int, step: Double):ShapeContour {
            var p = start.copy()
            val points = List(nSteps){
                val v = Vector2(p.y, -p.x + 11.0 * p.y * (0.12 - p.x * p.x))
                p += v * step
                p
            }.map{
                Vector2(map(-2.0, 2.0, 0.0, width * 1.0, it.x), map(-2.0, 2.0, height * 1.0, 0.0, it.y))
            }
            val shp = contour {
                moveTo(points[0])
                for (i in 1 until points.size){
                    lineTo(points[i])
                }
            }
            return shp
        }

        var time = 0.0
        val nSteps = 500
        val step = 0.02
        val nSize = 30

        val startPoints = mutableListOf<Vector2>()
        for (x in 0 .. nSize){
            for(y in 0 .. nSize){
                  startPoints.add(Vector2(map(0.0, nSize * 1.0, -1.8, 1.8, x * 1.0 ),
                      map(0.0, nSize * 1.0, -1.8, 1.8, y * 1.0 )))
              }
          }

        val sh = startPoints.map{getFlow(it, nSteps, step)}
        extend {
            drawer.stroke = ColorRGBa.WHITE
            drawer.fill = null
            drawer.strokeWeight = 2.0
            val style = shadeStyle {
                fragmentPreamble = """
                    #define linearstep(edge0, edge1, x) clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
                """.trimIndent()
                fragmentTransform = """
                    float t = c_contourPosition/p_l;
                    float alpha = fract(t - p_time -p_offset);
                    alpha = linearstep(0.92, 1.0, alpha);
                    vec3 col = mix(vec3(0.1, 0.24, 0.4), vec3(0.3, 0.2, 0.9), 1.0 - alpha * alpha);
                    x_stroke.rgb = mix(col, vec3(1.0), 1.0 - alpha * alpha);
                    x_stroke.a = alpha;
                """.trimIndent()
            }

            sh.forEachIndexed {i, c ->
                drawer.shadeStyle = style
                style.parameter("time", time)
                style.parameter("l", c.length)
                style.parameter("offset", Random.perlin(i * 0.303, i * 0.808))
                drawer.contour(c)
            }

            time += 0.005
        }
    }
}

```

![gif1](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/9c7e00cb224db48963cd7fde143d5e3cd2547364.gif)

---

<div class="post-metadata">

**Author:** ![LH99](https://yyz2.discourse-cdn.com/free1/user_avatar/openrndr.discourse.group/lh99/32/575_2.png) [@LH99](https://openrndr.discourse.group/u/LH99)\
**Post date:** [November 6, 2022, 12:59pm UTC](https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452/5 "2022-11-06T12:59:27Z")

</div>

Very cool !  
Can you explain your thought process of creating this ?  
From the idea to writing shaders (efficiently, without syntax error etc.) to getting this awesome attractor shape ?

---

<div class="post-metadata">

**Author:** ![Alessandro](https://yyz2.discourse-cdn.com/free1/user_avatar/openrndr.discourse.group/alessandro/32/412_2.png) [@Alessandro](https://openrndr.discourse.group/u/Alessandro)\
**Post date:** [November 6, 2022, 3:51pm UTC](https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452/6 "2022-11-06T15:51:07Z")

</div>

Hi @LH99 , thank you!  
I can write a few words, but somehow the thought process is almost under the sun here 🙂  
I was basically interested in creating the illusion of real-time dynamics but with the dynamics itself already pre-calculated, so that no computation is needed while drawing the frames. I knew that shaderStyle has a uniform that it is basically a coordinate on the contour, so I thought of using it to display just a little bit of the curve via the fragment shader. I then found a solution, posted it here, then @abe showed a much more elegant approach which I started playing with. Since the [flow](https://en.wikipedia.org/wiki/Flow_(mathematics)) of a dynamical system gives rise to contours, it was immediate to apply this to the flow curves of a dynamical system admitting a stable orbit (well, I’m a mathematician, these are the things that are immediate to me 🙂 ).  
Then I take an idea an reiterate in multiple forms. For instance, the sketch above gives directly rise to this

![gif1](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/bdb269761882154a3f4ba96b04ed4d568527e84f.gif)

If you look at the code you might think it is something completely different, and to a certain extent it is since it is completely shader based. The foundational ideas that I wanted to explore, though, are the same, and to me that’s what matters the most: I rarely know what I will make, most of the time things and paths emerge and I try to follow them.  
As for writing shaders “efficiently, without syntax errors”, I can reassure you that I go through many, many errors while coding! I guess experience reduces some of them, but it is usually a trial and error process, more or less like with any other artistic medium.  
I hope this is somehow useful 🙂

---

<div class="post-metadata">

**Author:** ![LH99](https://yyz2.discourse-cdn.com/free1/user_avatar/openrndr.discourse.group/lh99/32/575_2.png) [@LH99](https://openrndr.discourse.group/u/LH99)\
**Post date:** [November 7, 2022, 8:02am UTC](https://openrndr.discourse.group/t/closed-curve-coordinates-an-exercise/452/7 "2022-11-07T08:02:03Z")

</div>

Thanks @Alessandro,

didn’t really expect anything but trial-and-tweaking and picking the roses on the way 🙂 Still good to know that writing shaders is tricky, even for experienced programmers.

I have a background in mathematics, but have never heard of flow as a generalization of “applying a force field to particles”. Quite fancy, indeed.
